ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

实习生管理系统小程序:全栈链路调试与避坑实战

实习生管理系统小程序:全栈链路调试与避坑实战

简介:针对企业实习生招聘、培训、考勤和评估等环节的管理需求,这份微信小程序实习生管理系统源码资源包可帮助开发者快速搭建完整的移动端管理方案。资源共6个文件,压缩包约28MB,核心为基于Spring Boot的整站源码、db.sql数据库脚本,以及技术文档与毕业论文,配套的docx和doc文件对系统结构、数据库表关系和功能实现流程做了详细说明。

前端采用微信小程序原生框架,涵盖WXML、WXSS、JavaScript等核心组件,后端以Spring Boot为主,涉及API接口设计、数据库与业务逻辑处理,代码结构清晰、注释详尽。学习此资源可掌握小程序前后端联动开发的全流程,也能借助论文理解系统的选题背景、设计理念和架构,为课程设计或毕业设计提供参考。

目前已有99人浏览学习,适合需要复现完整项目、快速上手小程序开发或准备毕业设计的开发者作为学习素材。

1. 实习生管理系统小程序:压缩包里不只有代码,还有一条完整全栈链路

「实习生管理系统小程序(整站源码+sql脚本+论文).zip」这类压缩包,在毕设和课设场景里很常见:一个小程序前端、一个后端工程、一份 SQL 脚本、一篇配套论文。第一次解压的人往往急着找启动教程,结果卡在数据库导入报错、openid 换不到、真机请求失败这些地方。我的看法是,这套东西能不能跑起来只算及格线,真正值钱的是它把实习生管理这条业务线完整串了起来:角色权限、任务派发、每日打卡、周报提交、导师评分,每个环节都有对应的表、接口和页面。适合读这篇内容的人也明确:正在做毕设或课程设计的学生,想摸清小程序全栈链路的新手,以及想快速搭一套内部实习管理工具的小团队。接下来按我实际调试这类系统的顺序来写——先拆业务和库表,再导库启动后端,最后打通登录鉴权,末尾把高频踩坑点和改造建议列全。

2. 拆实习生管理系统业务模块与数据库:读论文之前,先看 SQL 脚本怎么建表

2.1 三类角色与权限边界:管理员、导师、实习生

这套系统的第一件事是角色划分。常见的做法是管理员、导师、实习生三种,不会多也不会少。管理员负责维护部门、导师账号、把实习生分配到导师名下,偶尔看一眼全局统计;导师负责给名下实习生派任务、审周报、打考勤分、写评语;实习生是小程序端最活跃的用户,打卡、看任务、交周报、查评分都在这里完成。

落到代码上,角色通常不建三张用户表,而是统一放一张用户表里,用一个 role 字段区隔。这样设计的原因很实际:三者的登录方式、基础字段完全一致,都是姓名、手机号、微信 openid,只有业务权限不同。前端拿到登录返回的 role 之后决定进入哪个首页、显示哪些 tab;后端在拦截器或过滤器里对 /mentor/、/admin/ 开头接口做角色校验。

这里最容易翻车的设计是前端只靠页面隐藏控制权限。小程序包一旦被解开,或者请求被直接构造,接口就完全暴露了,所以角色判断必须以后端为准。我一般会在调试初期打开后端工程的拦截器,把接口前缀和免鉴权白名单打印出来看一眼,比对着论文里的用例图更直观。常见的白名单只有登录接口和静态资源,其余接口都必须从 token 里解析出对应的角色。

2.2 从任务下达到评分:四个核心流程串起的数据流

业务闭环基本是四段。第一段任务下发:导师创建任务,指定实习生和截止时间,实习生在小程序里看到待办;第二段每日打卡:实习生上班打卡、下班打卡,记录时间生成考勤;第三段周报提交:实习生每周交一篇周报,导师查看后给通过或打回;第四段评分汇总:实习结束时导师给综合评分,管理员可以导出汇总。

这四段的数据库流向很清晰。任务创建写 t_task,打卡写 t_attendance,周报写 t_weekly,评分写 t_evaluation。每一段都会反写一个状态字段:任务从待办变成已完成,周报从待审变成已审,评分完成后把实习生标记为已结业。状态一般存数字,0、1、2 这样的枚举值,比字符串好比较、好索引,也比布尔值好扩展——比如任务状态要表达"待办、进行中、已完成、超时",布尔值就做不到。

理解这条数据流的意义在于联调定位问题。周报提交后导师端看不到,先别急着怀疑推送或者刷新,去查 t_weekly 的状态字段是不是没写对,或者压根没插入成功。我见过不少类似模拟项目把不同表的状态含义搞混,拿任务的 1 去比周报的 1,查来查去查不出原因,最后发现两个枚举定义根本不一样。

2.3 从 SQL 脚本反推业务:五张核心表的关系与字段演进

拿到 SQL 脚本不要急着跑,先把建表语句读完。实习生管理系统最核心的是五张表:用户表、实习生表、任务表、考勤表、周报表,评分表有些版本单独建,有些并进周报表,看脚本里实际怎么写的。用户表存公共信息,实习生表存实习期专属字段,业务表挂在实习生表下面。

-- 用户表:管理员、导师、实习生统一存储 CREATE TABLE `t_user` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', `openid` VARCHAR(64) DEFAULT NULL COMMENT '微信openid,小程序登录凭据', `username` VARCHAR(50) NOT NULL COMMENT '账号/工号', `password` VARCHAR(100) DEFAULT NULL COMMENT '密码,可空,小程序端走openid', `role` TINYINT NOT NULL COMMENT '角色:1管理员 2导师 3实习生', `real_name` VARCHAR(50) NOT NULL COMMENT '真实姓名', `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

这段 DDL 有四个值得留意的参数。openid 用 VARCHAR(64),微信返回的 openid 在 28 到 36 位之间,留点余量没坏处;role 用 TINYINT 而不是字符串,省空间且容易被后端枚举映射;username 建了唯一索引,这是给管理员和导师做账号密码登录用的,实习生如果纯走小程序登录,username 可以自动生成;字符集选 utf8mb4,因为姓名和评语里可能出现中文及表情符号,utf8mb3 存不了 emoji。如果脚本里写的是 utf8 或 gbk,建议建库时直接改成 utf8mb4,后面能少一片乱码的坑。

五张核心表的关联关系用一个表就能说清:

表名关键字段关联关系业务含义
t_userid, openid, username, role根表三种角色统一登录
t_internid, user_id, mentor_id, departmentuser_id → t_user.id实习生扩展信息,mentor_id 也指向用户表
t_taskid, title, assignee_id, publisher_id, deadline, statusassignee_id → t_intern.id导师派发任务
t_attendanceid, intern_id, work_date, check_in, check_outintern_id → t_intern.id每日打卡记录
t_weeklyid, intern_id, week_no, content, audit_statusintern_id → t_intern.id周报与导师审核

用户表是根表,实习生表是一层扩展,业务表全挂在实习生表下面。这样设计的好处是导师离职或者管理员调整角色时只改用户表,业务数据不受影响。看字段命名也能看出版本演进痕迹:有 openid 说明后来接入了小程序登录,有 audit_status 说明审核流是后加的,这些细节在答辩时要能讲出"为什么"。

3. 本地跑通最小可运行版本:导库、启动后端、拉起小程序

3.1 导入 SQL 脚本:字符集、版本与导入后的自检

导入前先检查两件事:脚本开头有没有 CREATE DATABASE,以及脚本是按 MySQL 5.7 还是 8.0 写的。带建库语句的脚本直接导入即可;不带的话要先手动建库,否则所有表可能落到默认库里,后端配置连的库名对不上,启动直接报找不到表。我推荐手动建库,字符集可控,后面排查也简单。

# 第一步:建库,显式指定 utf8mb4 mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS intern_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 第二步:导入脚本,指定目标库 mysql -uroot -p intern_system < intern_system.sql # 第三步:自检表数量 mysql -uroot -p intern_system -e "SHOW TABLES;"

导入后的自检别只看有没有报错。报错会当场中断,但隐患往往是静默的:表数量少了几张、某张表是空表、字段类型和论文描述不一致。所以建议再跑一条SELECT COUNT(*) FROM t_user;,如果用户表里连一条管理员初始记录都没有,说明这份脚本可能只有结构,初始化数据要靠后端启动时自动插入,或者自己手动补一条管理员的 INSERT。这一步没做,后面登录进不去后台,可别急着怪代码。

3.2 启动后端服务:配置文件的三个必改项

后端启动前,配置文件里至少有三处要改。第一是数据库连接,包含库名、账号、密码和服务时区;第二是微信小程序相关的 appid 和 secret;第三是可能被占用的端口。以最常见的 Spring Boot 工程为例,配置文件长这样:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/intern_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 wx: appid: 你的小程序appid secret: 你的小程序secret

逐项说明参数的含义。url 里的 intern_system 必须和建库名一致;characterEncoding=utf8 是兼容写法,配合 utf8mb4 库实际走的是 utf8mb4 编码;serverTimezone=Asia/Shanghai 是让 JDBC 拿到本地时间而不是 UTC,不配的话后面打卡时间会整体差 8 小时。jackson 的 date-format 和 time-zone 控制后端返回给前端的日期格式,统一成 yyyy-MM-dd HH:mm:ss 后,小程序端就不用再做字符串拼接转换。wx.appid 和 secret 在微信公众平台的开发设置里取,测试阶段可以用测试号,但测试号和正式号的登录数据互不相通,这个坑后面单独说。

改完配置再启动。Maven 工程在根目录执行mvn spring-boot:run,没有 Maven 的用 IDE 直接运行标注了 @SpringBootApplication 的主类。启动日志里看到 Tomcat started on port 8080 才算真正起来,此时最好立刻用浏览器访问一下登录接口的路径,确认不是启动即崩溃。如果端口被占用,要么换端口,要么查出占用进程,别硬不改配置去碰运气。

3.3 用微信开发者工具加载小程序端并打通登录

后端起来后,打开微信开发者工具,导入项目时选小程序目录——也就是包含 app.json 的那个目录,不是压缩包解压后的根目录,更不是后端工程目录。AppID 可以先填测试号,等需要真机预览再换正式 AppID。开发者工具里有个关键开关:详情 → 本地设置 → 不校验合法域名,开发阶段必须勾选,否则 wx.request 访问 http://localhost:8080 会被拦下。

登录打通的核心在小程序前端配置的后端地址。常见做法是在 app.js 的 globalData 里维护 baseUrl:

// app.js App({ onLaunch() { const token = wx.getStorageSync('token'); if (token) { // 已有登录凭证,冷启动时预拉用户信息 wx.request({ url: `${this.globalData.baseUrl}/user/info`, header: { Authorization: token }, success: (res) => { if (res.data.code === 200) { this.globalData.userInfo = res.data.data; } } }); } }, globalData: { baseUrl: 'http://localhost:8080/api', // 真机调试时改成局域网IP或HTTPS域名 token: '' } });

onLaunch 里判断有无 token 是这类系统的常规写法,好处是冷启动时把用户信息预加载进全局,各页面打开时不必再各自拉一次。baseUrl 写死是源码包的常态,本地联调没问题,但换真机预览时必须改成开发机在局域网里的 IP,否则手机访问不到 localhost——这个点几乎每次都会被问,后面避坑章还会展开。

打开小程序后先完整走一遍登录页:点登录按钮,看后端日志是否收到 /api/login 请求,再看 t_user 表是否多了一条带 openid 的新记录。这条链路通了,后面任务、打卡、周报就都顺着同一条路走,后面的调试基本都在业务逻辑层,不再碰环境。

4. 登录鉴权与请求联调:wx.login 到 token 的完整链路

4.1 code2Session 换 openid:后端这一步怎么接

小程序端的 wx.login() 拿到的 code 是一次性凭证,有效期五分钟,用一次就作废。它的唯一用途是交给后端去微信接口换 openid 和 session_key。整条链路是:小程序发 code → 后端带 appid、secret、code 调微信 code2Session 接口 → 拿到该用户在本小程序的唯一身份 openid → 后端查用户表,有则登录,无则按待分配处理 → 生成 token 返回前端。这一步对很多新手是个黑匣子,其实拆开看只有三步:

// LoginController.java @PostMapping("/login") public Result login(@RequestBody Map<String, String> body) { String code = body.get("code"); // 1. 用 code + appid + secret 换 openid String openid = wxService.code2Session(code); if (openid == null) { return Result.fail("code 无效或已过期"); } // 2. 查用户表,不存在则创建一条待分配账号 User user = userMapper.selectByOpenid(openid); if (user == null) { user = User.builder() .openid(openid) .role(3) // 默认实习生,管理员后台可改 .username("wx_" + openid.substring(0, 8)) .build(); userMapper.insert(user); } // 3. 生成 token,后续请求放在 Header 里 String token = JwtUtil.createToken(user.getId(), user.getRole()); return Result.ok(ImmutableMap.of("token", token, "role", user.getRole())); }

这段逻辑有三个参数值得专门说。code2Session 返回的 openid 必然存在,session_key 留作以后解密用户手机号等数据用,这里只需要 openid,所以空值判断要做在入参 code 上;role 默认给 3(实习生)是常见选择,因为管理员和导师账号通常是预置的,实习生才是扫码即用的那类人群;token 用 JWT 时 payload 至少要放 userId 和 role,过期时间建议两小时到一天,实习生打卡场景不需要更长。还要提醒一点:code 只能消费一次,前端如果重复调用 wx.login 并把旧 code 再发一次,后端换 openid 会失败,所以登录页的按钮要做好防重复点击。

4.2 请求封装与 401 处理:一套能迁移的写法

登录拿到 token 之后,所有业务请求都必须在 Header 里带 Authorization。小程序原生没有 axios 那样的拦截器,所以大家都会自己封装一个 request 方法,这是整个前端工程里最值得看懂的文件,没有之一。

// utils/request.js const { baseUrl } = require('./config'); const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { wx.request({ url: baseUrl + url, method, data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' }, success(res) { const { code, data } = res.data; if (code === 200) { resolve(data); } else if (code === 401) { // token 失效:清掉本地凭证,回登录页 wx.removeStorageSync('token'); wx.reLaunch({ url: '/pages/index/index' }); reject(res.data); } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(res.data); } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); }; module.exports = request;

这段封装解决三件事。统一注入 token,各页面不用重复写 header;统一处理 401,后端说 token 失效时所有页面行为一致——清缓存、回登录页;统一错误提示,后端返回的业务错误按其 msg 字段展示。参数上,url 传的是 /task/list 这种相对路径,baseUrl 从 config 引入而不是在这里写死,这是为了把环境切换收敛到一个文件里,避免以后改域名满项目找。用法是页面里const data = await request('/task/list', 'GET', { status: 0 });,因为返回的是 Promise,配合 async/await 读写很顺手。

4.3 两个最容易弄错的参数:测试号 openid 与合法域名

调试阶段有两个参数最容易卡人。第一个是 AppID 切换。测试阶段用测试号开发完全没问题,但测试号的 appid 和正式号的 appid 是两套体系,同一个人在两套 appid 下拿到的 openid 完全不一样。如果你先用测试号登录生成了用户记录,换正式号后同一微信号会生成第二条新记录,表现就是登录后角色变成默认的实习生、之前的任务和数据全部"消失"。这不是代码 bug,是 openid 绑定 appid 的机制决定的。所以要在项目早期就决定用哪个 AppID,别在数据库里攒了一堆测试号数据之后才后悔。

第二个是合法域名。开发者工具勾了"不校验合法域名"可以随便访问 http 接口,但真机预览默认会校验:正式 AppID 下,wx.request 的域名必须是备案过的 HTTPS 域名,并且要到微信公众平台的开发设置里配置到 request 合法域名列表。临时方案是打开真机调试模式,能撑一两天;长期方案是给后端配 HTTPS 域名。如果后端只有 IP 或 http,真机上就只能靠调试模式,这是环境限制,不是代码问题,答辩时也别夸口说生产可用。

5. 避坑指南:实习生管理系统小程序本地与真机调试的 5 个翻车点

5.1 真机请求全部失败:合法域名和 baseUrl 双坑叠加

现象:开发者工具里一切正常,扫码换真机预览后所有接口报网络错误或者页面白屏,后端日志里一条请求都看不到。原因往往是两个叠加:真机访问不到 http://localhost:8080,因为 localhost 指向的是手机自己;同时正式 AppID 下未配置合法域名,请求在校验层就被拦了。解决分两步:把 baseUrl 改成开发机在局域网里的 IP,确保手机和电脑在同一个 WiFi;同时在开发者工具里开启真机调试模式绕过域名校验。如果后端部署在服务器上,直接用服务器 IP 更省事,但 http 协议在真机上仍受限制,最终还是要走上 HTTPS 域名。

5.2 登录成功但接口全部 401:token 传递断链

现象:登录接口返回了 token,页面也跳转了,但随后每个业务接口都返回 401,后端日志里全是"未登录"。原因大多在前端:有的页面没走封装的 request 方法,直接拿 wx.request 原生调用,忘了带 Authorization 头;有的是登录时写入 storage 的 key 和请求封装里读取的 key 不一致,一个写 token 一个读 Token,大小写一差就找不到了。解决:全局搜索 token 的读写位置,统一 key 名;再全局搜索 wx.request,把漏网的原生请求都替换成封装好的 request 方法,半小时能根治。这条是最典型的"看着像后端问题,实际是前端问题"。

5.3 打卡时间少了 8 小时:时区默认 UTC 的坑

现象:早上九点打卡,列表里显示凌晨一点,整整差 8 个小时。原因:MySQL 连接串没配 serverTimezone,JDBC 默认用 UTC,而本地时间是 UTC+8;或者后端 Jackson 序列化时间时用了默认时区。解决:连接串加serverTimezone=Asia/Shanghai,后端再统一配置 date-format 和 time-zone GMT+8,前端拿到的时间字符串就正常了。这里不要想着在前端做 8 小时的偏移换算来补救,数据库、后端、前端三层统一时区才是正路,前端硬算只会让导出和统计继续错下去。

5.4 SQL 导入报错:MySQL 8 认证与 sql_mode 冲突

现象:导入脚本时报Unknown database、Table already exists或者字段默认值报错后中断。原因:脚本是按 MySQL 5.7 写的,本机装的是 8.0;8.0 默认的 sql_mode 更严格,比如 TIMESTAMP 默认值不允许为空、部分废弃语法直接报错。解决:先看报错行属于哪种类型。数据库不存在就按前面第 3 章的命令手动建库;字段默认值报错就临时执行SET sql_mode = '';再导入;如果报的是认证插件相关错误,说明客户端版本和服务器认证方式不匹配,优先升级客户端而不是去改服务器认证。不建议为了导一份 SQL 就去降级 MySQL,改脚本或调 session 参数更可控,也更符合实际维护场景。

5.5 论文里的表结构对不上代码:版本漂移

现象:论文里的 E-R 图没有 openid 字段,代码里却有;或者论文说有考勤统计表,SQL 脚本里找不到。原因:这类源码包的论文和工程常常不是同一版本导出的,论文写完后代码又迭代了几轮,最后打包时只同步了代码和脚本,论文没跟着改。解决:以 SQL 脚本和代码为准,交付前把论文的数据库设计章节对着实际脚本改一遍,缺的表补进去,字段说明跟着更新。答辩时被评委问"你论文里这个字段在哪定义的"会非常被动,提前对一遍是值得花的半小时。

6. 把源码变成能交付自己的版本:三个改造与双端验证

6.1 改造一:baseUrl 抽离,区分开发与生产环境

源码包里 baseUrl 散落在 app.js 和各页面里是常态,交付前我建议把它收敛到 config 文件里,一份 JS 维护所有接口前缀:

// utils/config.js module.exports = { env: 'dev', // 切换环境只改这里 baseUrl: 'https://you-domain.com/api', // 生产环境地址 devBaseUrl: 'http://192.168.1.100:8080/api' // 开发环境地址 };

request.js 里按 env 取值,const baseUrl = config.env === 'dev' ? config.devBaseUrl : config.baseUrl;。改造的价值是以后换环境只改一个字段,也方便在演示时证明"这套系统可以平滑切换到服务器部署"。注意这只是前端侧的开关,后端跨域配置和微信合法域名仍然要配套,别以为前端切了地址就万事大吉。

6.2 改造二:打卡加定位与时间水印,做出自己的亮点

答辩时最容易被问"你做了哪些差异化"的位置就是打卡。一个投入产出比很高的改造是打卡时记录经纬度和时间水印,用来防止代打卡。小程序端用 wx.getLocation 拿经纬度,需要先在 app.json 里声明 permission:

{ "permission": { "scope.userLocation": { "desc": "定位将用于记录打卡位置" } } }

用户拒绝授权时回退为仅记录时间,不能因为定位把整个打卡流程卡死。后端打卡表加两个可空字段 latitude、longitude,前端提交时带上,导师端列表展示打卡位置。这样做的增量代码量不大,但把打卡从纯记录变成了带校验逻辑的功能,也自然引出一段"我如何设计防作弊"的答辩输出。

6.3 双端验证清单:模拟器通一次,真机再通一次

交付前我习惯按场景列一张清单整体过一遍:登录注册、任务查看与状态流转、打卡签到、周报提交与审核、评分查看、管理员对用户和部门的增删改查。先走微信开发者工具模拟器,再切真机预览走一遍,重点验证两个地方:权限隔离,实习生账号能不能访问导师接口、导师能否看到其他导师名下的学生;真机能力,定位授权弹窗是否正常,弱网环境下请求超时是否给了友好提示而不是白屏。

这套源码包的最终形态不应该是"解压即用",而是"我清楚每一张表、每一个接口为什么这么设计"。我自己的习惯是拿到这类包先花一个晚上把数据库和核心接口画成关系图,再动手改代码,后面所有调试都快很多。把 baseUrl 抽离、打卡加点自己的逻辑、权限再收紧一层,这套系统才真正从别人的作品变成你的交付物。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进