☰
高校就业微信小程序毕设实操:Spring Boot与MySQL打造完整招聘系统
2026/10/10 6:28:48 网站建设 项目流程

简介:这是一套基于微信小程序的高校就业招聘系统完整源码,属于经导师指导并通过验收的高分毕业设计项目,技术栈以Java后端为核心,搭配小程序前端,适合计算机、电子信息工程、数学等专业学生用于毕业设计、课程设计或期末大作业实战演练。压缩包共1436个文件,整体约28.9MB,包含255个vue页面文件、173个js逻辑文件、137个java后端处理类,以及wxss、wxml等小程序样式与结构文件,另有png、jpg图片素材和mp4演示视频,覆盖前端界面、后端接口、样式配置与说明文档等多个层面,目录结构清晰,便于定位和学习。已有111人学习使用。资源内附带完整的项目工程文件、构建脚本(bat)和说明文档,能够帮助读者快速运行项目、理解前后端交互流程,同时为二次开发和功能扩展提供基础骨架,适合需要完整项目参考或进行功能复现的开发者。

1. 高校就业招聘微信小程序:一个能撑起毕设的完整业务闭环

每年毕业设计选题,总有一批人会落在“就业招聘”这类系统上:需求明确、角色清晰、功能可大可小,看起来怎么写都不会偏题。但真正开工后才发现,难题从来不在「有没有源码」,而在「代码拿到手能不能跑、改得动、讲得清」。高校就业招聘系统表面上是一个微信小程序,背后却牵扯着小程序端、后端接口、数据库表设计、身份鉴权、文件上传、审核状态机这一整条链路,任何一个环节断掉,演示时都会翻车。

这篇文章就按这套系统的真实开发顺序,把「小程序端 + Spring Boot 后端 + MySQL 数据库」的完整落地方案拆开讲:技术栈为什么这么选、核心模块的代码怎么组织、参数怎么调,以及 5 个毕设项目里最容易踩的坑。准备做这个方向的同学,可以照着把系统从零搭起来,再把手上的“能跑”变成答辩时的“高分”。

2. 技术选型与系统拆分:先把「能跑」的地基定下来

2.1 为什么是这个技术栈:小程序原生、Spring Boot 与 MySQL 的取舍

高校就业招聘系统的技术栈选择,决定了后续一个月是「写业务」还是「调环境」。我的建议很直接:小程序端用微信原生框架,后端用 Spring Boot + MyBatis Plus,数据库用 MySQL 8.0。这不是最酷的组合,但一定是毕设项目里最稳的组合。

微信原生框架的好处是,微信开发者工具打开就能预览,不需要像 uni-app 那样多一层编译链。个别同学想用 uni-app 一套代码同时出小程序和 App,这个想法本身没问题,但代价是你得同时理解 Vue 语法和微信生命周期,调试报错时多一个环节。对「一两个月内出成果」的毕设来说,不值得。

后端选 Spring Boot 的理由更实际:资料多、报错容易搜、答辩时技术点好讲。SSH(Struts + Spring + Hibernate)那套老框架已经没有新项目在用了,Node.js 写接口虽然轻,但国内高校的评审老师普遍更认可 Java 系的技术栈。MyBatis Plus 解决的是「单表 CRUD 浪费时间」的问题,20 张表以内的业务,几乎不用手写 SQL,可以把精力放在登录、审核、投递这些真正的业务逻辑上。

数据库方面,MySQL 5.7 和 8.0 都行,但既然要新装,我一般直接上 8.0。需要注意 JDBC 连接串里必须显式写serverTimezone=Asia/Shanghai,不然启动时大概率会遇到时区报错,这一点在第 5 章的避坑部分会细说。Redis 在这套系统里不是必须的,token 先存内存或数据库都够用,少一个中间件,部署时就少一个变量。

2.2 三类角色与四条主流程:学生、企业、管理员如何协作

就业招聘系统本质上是一个「连接学生和用人企业」的信息撮合平台,但多了一个管理员角色后,系统就从「两人转」变成了「三角色协同」。这也正是它适合做毕设的原因——有权限区分、有审核流、有状态流转,能讲的东西非常多。

三类角色分别是:学生(小程序端浏览职位、投递简历、收藏职位)、企业(小程序端发布职位、查看收到的简历)、管理员(审核企业注册、审核职位上下架、查看统计数据)。很多同学会忽略一个问题:企业端到底做在小程序里还是做成 Web 后台?常见的做法是两者都做,但为了控制工作量,我建议把「企业端」做进小程序,「管理端」单独做一个极简的 Web 页面,或者在小程序里给管理员留一个隐藏入口。这样标题里的「小程序」和项目的「完整性」都保住了。

系统里有四条核心主流程,答辩时画时序图全靠它们:

  1. 学生登录后完善简历,浏览职位列表,投递简历到某个职位;
  2. 企业登录后发布职位,职位默认进入待审核状态;
  3. 管理员审核职位,审核通过后职位才在学生端可见;
  4. 企业对投递记录进行标记:待处理 / 已查看 / 邀面试 / 不合适。

投递状态是最容易引发后续连锁反应的模块。学生投递后,如果企业把小程序的status从 0(待处理)改成 2(面试),学生端要能看到这个变化。这要求前端不是一锤子买卖,而是每次进入「我的投递」页面时都重新拉取接口数据,不能拿本地缓存糊弄。

2.3 核心数据库表设计:从用户到投递记录一共六张表

先把表结构定下来,后面所有代码都围绕这些表展开。高校就业招聘系统最核心的是六张表,我用一个 SQL 文件把它们串起来。

-- 用户表:学生、企业、管理员统一存放,role 区分身份 CREATE TABLE tbl_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) UNIQUE COMMENT '微信openid,唯一标识', role TINYINT NOT NULL DEFAULT 1 COMMENT '1学生 2企业 3管理员', nickname VARCHAR(64), avatar VARCHAR(255), phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 简历表:学生完善简历后生成,与用户一对一 CREATE TABLE tbl_resume ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, real_name VARCHAR(32), school VARCHAR(64), major VARCHAR(64), education VARCHAR(16), phone VARCHAR(20), email VARCHAR(64), resume_file VARCHAR(255) COMMENT '简历附件URL', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 企业表:企业认证信息,管理员审核通过后 status 为 1 CREATE TABLE tbl_company ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, company_name VARCHAR(128), industry VARCHAR(32), scale VARCHAR(16), address VARCHAR(128), license_url VARCHAR(255) COMMENT '营业执照图片', status TINYINT DEFAULT 0 COMMENT '0待审核 1已通过 2已拒绝' ); -- 职位表:企业发布,管理员审核成功后学生端才可见 CREATE TABLE tbl_job ( id BIGINT PRIMARY KEY AUTO_INCREMENT, company_id BIGINT NOT NULL, title VARCHAR(64), salary_min INT, salary_max INT, requirement TEXT, status TINYINT DEFAULT 0 COMMENT '0待审核 1招聘中 2已下线 3审核拒绝', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 投递表:学生与职位的关联记录,状态是关键 CREATE TABLE tbl_delivery ( id BIGINT PRIMARY KEY AUTO_INCREMENT, job_id BIGINT NOT NULL, user_id BIGINT NOT NULL, resume_id BIGINT NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待处理 1已查看 2面试 3录用 4不合适', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_job_user (job_id, user_id) ); -- 收藏表:学生收藏职位 CREATE TABLE tbl_favorite ( id BIGINT PRIMARY KEY AUTO_INCREMENT, job_id BIGINT NOT NULL, user_id BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );

tbl_user表把三类角色合并在一起,用role字段区分,好处是登录逻辑只用写一套,坏处是学生、企业、管理员的扩展字段会互相污染。对应到实际业务,企业相关字段放在tbl_company,学生简历字段放在tbl_resume,用户表只保留公共信息,这样设计刚好平衡,不会被人说数据库不规范。

tbl_delivery里的唯一索引uk_job_user是防止学生重复投递的数据库底线。前端可以加按钮防抖,后端必须加唯一索引,这是「前端体验」和「后端防御」的双保险。很多同学只做了前端判断,接口被人拿工具刷一下就穿帮了。

3. 小程序端核心模块:登录、职位流与投递状态的实现

3.1 微信登录链路:从 wx.login 到后端换 openid,再签发 token

微信小程序登录是整套系统里第一个真正的门槛。很多同学以为登录就是wx.login拿到 code 后直接当 token 用,这是错的。code是一次性的临时凭证,5 分钟内有效,后端必须拿这个 code 去微信接口换openid,再用openid去自己的用户表里查账号。

小程序端的最简登录代码如下:

// pages/login/login.js const app = getApp() Page({ data: { loading: false }, onLoad() { this.handleLogin() }, async handleLogin() { if (this.data.loading) return this.setData({ loading: true }) // 1. 获取微信登录凭证 code const { code } = await wx.login() // 2. 把 code 发给后端,由后端去微信接口换 openid wx.request({ url: 'https://你的域名/api/auth/login', method: 'POST', data: { code: code }, success: (res) => { const { token, userInfo } = res.data.data // 3. 登录成功,保存 token,后面所有请求都要带上 wx.setStorageSync('token', token) wx.setStorageSync('userInfo', userInfo) wx.switchTab({ url: '/pages/jobs/jobs' }) }, fail: () => { wx.showToast({ title: '登录失败,请重试', icon: 'none' }) }, complete: () => { this.setData({ loading: false }) } }) } })

这段代码里,handleLogin开头的loading判断是防止用户在登录过程中反复点击按钮造成重复请求。wx.login拿到的code每次都会变,所以它本身不能作为用户身份标识,真正的身份标识是后端通过 code 换回来的openid。

后端对应接口的逻辑很简单:接收 code,拼接请求参数,调用微信接口,然后「查库 or 建库」:

@RestController @RequestMapping("/api/auth") public class AuthController { @Resource private UserService userService; @PostMapping("/login") public Result login(@RequestBody Map<String, String> params) { String code = params.get("code"); // 1. 调用微信接口,用 code 换 openid String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code"; String result = HttpUtil.get(url); JSONObject json = JSONUtil.parseObj(result); String openid = json.getStr("openid"); if (StrUtil.isEmpty(openid)) { return Result.error("code 无效或已过期"); } // 2. 根据 openid 查用户,不存在则自动注册 User user = userService.getOne(new LambdaQueryWrapper<User>() .eq(User::getOpenid, openid)); if (user == null) { user = new User(); user.setOpenid(openid); user.setRole(1); // 默认学生身份 userService.save(user); } // 3. 生成 token 并返回 String token = UUID.randomUUID().toString().replace("-", ""); // 项目里可以用 Redis 存 token -> userId,也可以用内存 Map TokenStore.put(token, user.getId()); return Result.ok(new LoginVO(token, user)); } }

这段代码里的appid和secret在小程序后台的「开发管理 → 开发设置」里能拿到,但不能写死在代码里。常见做法是放在application.yml配置文件中,提交源码前从配置里删掉。有不少同学把 secret 直接写进前端代码,这是很严重的安全问题——这就等于把账号密码贴在了门上。

注意jscode2session返回的字段还有一个unionid,但只有小程序绑定开放平台后才有。毕设项目里不要依赖unionid,直接用openid作为用户唯一标识就对了。

3.2 职位列表页:数据绑定、下拉刷新与触底分页

职位列表是就业招聘小程序里流量最大的页面,学生打开小程序第一个看到的就是它。列表页需要同时处理「数据渲染」和「分页加载」两件事,前端代码里最容易出问题的是分页参数。我的习惯是第一页传page=1&size=10,每次触底page++,后端返回的时候用total字段告诉前端总共多少条,前端判断「当前页是否还有下一页」。

职位列表页的 JS 逻辑代码:

// pages/jobs/jobs.js Page({ data: { jobList: [], page: 1, size: 10, hasMore: true, loading: false }, onLoad() { this.getJobList(true) }, // 下拉刷新:重置页码后重新拉取数据 async onPullDownRefresh() { this.setData({ jobList: [], page: 1, hasMore: true }) await this.getJobList(true) wx.stopPullDownRefresh() }, // 触底加载:只有 hasMore 且不在请求中时才继续加载 onReachBottom() { if (!this.data.hasMore || this.data.loading) return this.getJobList(false) }, getJobList(isRefresh) { if (this.data.loading) return this.setData({ loading: true }) const page = isRefresh ? 1 : this.data.page + 1 wx.request({ url: 'https://你的域名/api/jobs', data: { page: page, size: this.data.size, keyword: this.data.keyword || '' }, success: (res) => { const { records, total } = res.data.data const newList = isRefresh ? records : [...this.data.jobList, ...records] this.setData({ jobList: newList, page: page, hasMore: this.data.jobList.length + records.length < total }) }, complete: () => { this.setData({ loading: false }) } }) }, goDetail(e) { const jobId = e.currentTarget.dataset.id wx.navigateTo({ url: `/pages/job-detail/job-detail?id=${jobId}` }) } })

这里两个关键细节。第一,onReachBottom里的loading判断必须写,否则手指快速划动时会连续触发多次请求,后端没做防重处理的话,数据库里就会插入多条分页重复的数据。第二,hasMore的计算用的是当前页 list 长度小于 total而不是records.length === size,后者在「最后一页恰好满 10 条」时会多请求一次。

对应的 WXML 渲染部分,用wx:for循环渲染数组,每条数据带><view class="job-card" wx:for="{{jobList}}" wx:key="id" >// pages/job-detail/job-detail.js Page({ data: { jobId: null, hasResume: false, delivering: false }, onLoad(options) { this.setData({ jobId: options.id }) this.checkResume() }, // 投递前先看用户有没有简历 async checkResume() { const token = wx.getStorageSync('token') if (!token) { wx.navigateTo({ url: '/pages/login/login' }) return } const res = await wx.request({ url: 'https://你的域名/api/resume/status', header: { Authorization: token } }) this.setData({ hasResume: res.data.data.hasResume }) }, async handleDeliver() { if (!this.data.hasResume) { wx.showModal({ title: '提示', content: '请先完善个人简历后再投递', confirmText: '去完善', success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/resume/resume' }) } }) return } if (this.data.delivering) return this.setData({ delivering: true }) wx.request({ url: 'https://你的域名/api/delivery/create', method: 'POST', data: { jobId: this.data.jobId }, header: { Authorization: wx.getStorageSync('token') }, success: (res) => { if (res.data.code === 200) { wx.showToast({ title: '投递成功', icon: 'success' }) } else if (res.data.code === 5001) { wx.showToast({ title: '该职位已投递过', icon: 'none' }) } }, complete: () => { this.setData({ delivering: false }) } }) } })

delivering这个布尔值就是防重复提交的第一道闸门。用户在点击后、接口返回前的这段时间里,只要delivering为 true,后续点击直接 return,不会产生并发请求。后端还要再兜底一次:tbl_delivery表里加了(job_id, user_id)唯一索引,重复插入会被数据库直接拒绝。

后端处理投递时的状态流转是这样的:插入时默认status=0(待处理),企业查看简历后把状态改成1(已查看),发起面试邀请则改成2,录用3,不合适4。这个状态机要和学生端的「我的投递」页面联动。学生端每次onShow时重新请求「投递列表」接口,而不是用本地缓存的数据,这样才能保证企业端已经更新状态后,学生端能看到最新进展。

4. 后端与业务闭环:接口设计、鉴权与审核状态机

4.1 Spring Boot 工程骨架:分层结构与统一返回体

后端工程的分层结构直接影响开发效率。我见过不少同学图省事,把逻辑全写在 Controller 里,几百行代码塞一个方法,后续查错只能靠System.out.println。高校就业招聘系统虽然不算大,但也建议按controller / service / mapper / entity / common五层划分,让每一层只干一件事。

这里是最核心的依赖列表:

<!-- pom.xml 关键依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.22</version> </dependency>

mybatis-plus-boot-starter提供BaseMapper接口,继承了它之后,selectById、insert、updateById这些常用方法直接用,不用自己写 XML 映射文件。hutool-all是一个工具包,我主要用它来发 HTTP 请求(调用微信接口)和做 JSON 解析,比手写原生HttpClient省很多代码。

统一返回体的设计要趁早定,不然后端返给前端的字段名会乱成一片。我的习惯是:

public class Result { private Integer code; // 200 成功,其他为业务错误码 private String msg; private Object data; public static Result ok(Object data) { return new Result(200, "success", data); } public static Result error(String msg) { return new Result(500, msg, null); } }

所有 Controller 方法的返回值都包装成Result,前端在wx.request的success回调里先判断code === 200,再取data。这样有一个好处:业务错误(比如「职位已投递」)和系统错误(比如「服务器内部异常」)可以分开处理,前端不会因为接口返回 200 之外的 HTTP 状态码而走不到success回调。

鉴权部分用拦截器做全局校验。把/api/auth/login排除,其余/api/**接口都检查请求头里的 Authorization token:

@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (StrUtil.isBlank(token) || TokenStore.get(token) == null) { response.setStatus(401); return false; } // 将当前登录用户 id 写入 request attribute,供后续逻辑使用 request.setAttribute("userId", TokenStore.get(token)); return true; } }

拦截器里的TokenStore如果不想引入 Redis,用一个带过期时间的 Map 就能实现。要注意的坑是:拦截器返回 401 后,前端的wx.request只会拿到 HTTP 状态码,必须自己处理statusCode === 401的情况,跳转到登录页,否则用户会看到白屏或空白数据。

4.2 管理员端审核:职位上架前必须过的状态机

职位审核是高校就业招聘系统和普通招聘网站的明显区别——校园场景里,管理员要对招聘信息真实性负责,所以「企业发布 → 管理员审核 → 学生可见」这个链路不能省。

企业发布职位时,前端提交表单后调用/api/jobs/create,后端把职位的status初始化为 0(待审核)。管理员端提供一个「待审核列表」接口,只返回status=0的职位,管理员点「通过」或「拒绝」后更新状态。核心的审核逻辑如下:

@Service public class JobServiceImpl extends ServiceImpl<JobMapper, Job> implements JobService { @Override public Result auditJob(Long jobId, Integer auditStatus, Long adminId) { // 1. 权限校验:只有管理员才能审核 User admin = userService.getById(adminId); if (admin == null || admin.getRole() != 3) { return Result.error("无权限执行审核操作"); } // 2. 校验审核状态只能为 1(通过)或 3(拒绝) if (auditStatus != 1 && auditStatus != 3) { return Result.error("非法的审核状态"); } // 3. 更新职位状态 Job job = new Job(); job.setId(jobId); job.setStatus(auditStatus); this.updateById(job); // 4. 如果审核拒绝,记录拒绝原因(可以在 Job 表加 refuse_reason 字段) if (auditStatus == 3) { job.setRefuseReason("招聘信息不符合规范"); this.updateById(job); } return Result.ok(null); } }

这段代码里,第 1 步的权限校验是重点。很多系统做出来「审核接口」人人可调,学生用调试工具直接给接口传参数,把自己发布的虚假职位改成已上架,这就是典型的安全漏洞。每次接口操作前都要确认「当前登录用户是管理员」,不能因为在小程序端藏了入口就以为安全。

职位状态机完整跑一遍:发布(0 待审核)→ 审核通过(1 招聘中)→ 企业主动下架(2 已下线)→ 审核拒绝(3 拒绝)。学生端职位列表只查status=1的数据,这个筛选条件必须写在后端 SQL 里,不能靠前端过滤——否则学生端一次性会把所有状态的职位都下载到本地,数据安全性很差。

4.3 文件上传与简历附件:本地存储方案的边界

学生简历通常要支持上传 PDF 或 Word 附件,企业的营业执照也要上传图片。毕设项目里最常见的方案是把文件存到本地磁盘,然后通过 Spring Boot 配置静态资源映射来访问。

上传接口的写法:

@RestController @RequestMapping("/api/file") public class FileController { @Value("${file.upload-dir}") private String uploadDir; @PostMapping("/upload") public Result upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("文件不能为空"); } // 1. 生成唯一文件名,避免中文名和重名问题 String originalName = file.getOriginalFilename(); String ext = StrUtil.subAfter(originalName, ".", true); String newName = UUID.randomUUID().toString().replace("-", "") + "." + ext; try { // 2. 存到指定目录 File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, newName)); } catch (IOException e) { return Result.error("文件保存失败"); } // 3. 返回给前端的是相对 URL,不是磁盘物理路径 return Result.ok("/files/" + newName); } }

这段代码特别要注意最后一步:数据库里保存的是/files/xxx.pdf这样的相对 URL,而不是D:/xxx/xxx.pdf这样的绝对路径。绝对路径写在数据库里,换一台机器部署就全部失效;相对 URL 配合下面的资源映射,任何机器上只要把文件放在同一个目录就能访问。

在 Spring Boot 配置类里加一段资源映射,才能让用户访问到上传的文件:

@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${file.upload-dir}") private String uploadDir; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceLocations("file:" + uploadDir + "/"); } }

这段配置的意思是:所有https://域名/files/xxx.pdf的请求,都映射到服务器上的uploadDir目录去找文件。如果不加这段,上传成功后前端拿到的 URL 访问直接 404,这是本地存储方案里最容易漏掉的一步。这里也顺手留了一个伏笔:开发机上的本地文件和部署服务器上的文件不是同一份,这一点会在后面的避坑章节展开。

5. 确实是坑:毕设级小程序最常见的 5 个翻车现场

5.1 真机预览时所有接口全部请求失败:开发工具正常但手机白屏

现象:小程序在微信开发者工具里一切正常,登录、职位列表、投递都能跑,但一换到手机上真机预览,所有wx.request全部失败,控制台报url not in domain list。

原因:微信小程序有严格的域名校验机制。开发工具默认勾选了「不校验合法域名」,所以本地调试能通;真机上没有这个选项,所有请求的域名必须在小程序后台配置为「request 合法域名」,而且只支持 HTTPS 协议。很多同学买不起服务器或者懒得备案,拿 IP 地址加端口号直接当域名用,这在真机上绝对跑不通——合法域名必须是备案过的域名。

解决:如果只是校内答辩演示,两条路。第一条,在微信开发者工具的「详情 → 本地设置」里勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」,然后用工具的真机调试模式,走 localhost 隧道,手机和电脑同一局域网时可以调试。第二条,老老实实买一台云服务器,绑定已备案的 HTTPS 域名,把application.yml里的端口和域名改成线上配置,这才是「能给别人演示」的正式做法。我的建议是直接走第二条,校内答辩的评委一般会要求你在自己手机上演示,本地模式偶尔会抽风。

5.2 同一用户换手机登录后账号数据丢失

现象:用户在 A 手机上登录并填写了简历,换到 B 手机登录后,发现简历是空的,收藏也没了,看起来像「两个不同的账号」。

原因:登录逻辑里把「当前设备临时凭证」当成了用户身份。具体来说,后端如果用前端传过来的某个固定值(比如把 code 当 userId)去建账号,那么每台手机产生的 code 都不同,换设备就相当于新用户。正确做法永远是用openid作为用户唯一标识——它是由微信根据「小程序 appid + 用户微信号」算出来的,同一用户在任何手机上登录都是同一个 openid。

解决:检查后端登录接口,确认是通过jscode2session返回的openid去查tbl_user表;如果查不到再创建新用户,查得到就沿用之前的用户 id。我遇到过一种情况:某开发者在小程序端把用户信息存到了本地 storage,换手机后 storage 清空,就认为「用户不存在」,其实后端数据一直在,只是前端没重新拉取。记住:用户数据只以数据库为准,本地缓存只是性能优化,不是数据源。

5.3 触底翻页时数据重复或永远加载不完

现象:职位列表往下滑,每翻一页,上一页的数据又重复出现一遍;或者翻到最后一页后,底部一直在转圈加载,但已经没数据了。

原因:这是分页参数处理混乱造成的。常见两种情况:一是每次触底都传page=1,导致后端永远返回第一页;二是后端返回的total字段没有被正确使用,前端不知道什么时候该停。还有一个隐蔽的坑:没有用加载锁,onReachBottom在请求未返回时被多次触发,产生多个并发分页请求。

解决:严格按第 3 章的写法来:维护page、size、hasMore、loading四个变量。loading为 true 时直接 return,请求完成后在complete回调里复位;hasMore的判断用当前列表长度 < total。翻页完成后,用wx.stopPullDownRefresh()结束下拉刷新动画,用loaded标志位控制「到底啦」提示的显示。这套逻辑写好后,不仅职位列表能用,消息列表、投递记录列表都能复用同一套分页模式。

5.4 MySQL 连接报错:The server time zone value 'Öйú±ê׼ʱ¼ä'

现象:后端启动时数据库相关异常,错误信息里出现server time zone value...或者中文乱码,项目直接起不来。

原因:MySQL 8.0 的默认时区有时是SYSTEM,而 JDBC 驱动 8.0 版本要求显式指定时区;错误信息里的乱码,其实是「中国标准时间」这几个字在 GBK 编码下的显示乱码,本质是时区解析失败,不是字符集问题。

解决:在application.yml的 JDBC URL 后面加上参数:

spring: datasource: url: jdbc:mysql://localhost:3306/job_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

这个参数在 MySQL 5.7 下加不加无所谓,但 MySQL 8.0 + 新版驱动不加必报错。顺带提醒,driver-class-name如果是 8.0 驱动,写com.mysql.cj.jdbc.Driver,别写成老版的com.mysql.jdbc.Driver,否则同样会报错。

5.5 上传的文件在重启服务后全部变成 404

现象:开发阶段上传的简历 PDF、企业营业执照图片,当时访问正常。代码改完重启 Spring Boot 后,历史文件全部访问不了,新上传的文件又正常了。

原因:文件被写进了 Tomcat 的临时目录。开发工具启动 Spring Boot 时,默认工作目录可能是spring-boot:run生成的临时路径,重启时操作系统把这些临时文件清掉了。另外,很多人用System.getProperty("user.dir")拼路径,这个值在 IDE 启动、命令行启动、部署环境启动下都不一样,所以「换一种启动方式,文件路径就变了」。

解决:把上传目录固定成一个绝对路径,而不是依赖项目相对路径。比如在服务器上固定用/data/job-files/,在 Windows 开发机用D:/job-files/,然后在application.yml里配置file.upload-dir:

file: upload-dir: /data/job-files

启动时检查目录是否存在,不存在就自动创建。配置了固定目录后,加上第 4 章的资源映射,不管重启多少次、换不换端口,文件 URL 都能稳定访问。另外提醒一句:本地开发时的文件不会跟着部署包走,答辩前要在演示环境重新上传一遍所有需要的附件。

6. 从「能跑」到「高分」:答辩演示路径与自查清单

系统跑通只是及格线,高分的关键在于「演示时让老师快速看懂业务闭环」。我的习惯是准备一条完整的演示路径,按顺序操作,每一步都有明确的预期结果。这条路径也是你论文里「系统测试」章节的骨架。

演示步骤操作预期结果
1学生身份微信登录自动注册,进入职位列表页
2搜索「Java」关键词只展示相关职位,分页正常
3查看职位详情并投递简历提示「投递成功」,投递记录出现该职位
4切换企业身份发布职位提示「提交成功,等待审核」
5管理员登录进入审核列表看到待审核职位,点「通过」
6切回学生端刷新职位列表新职位出现在列表顶部,可投递
7企业查看投递并标记「面试」学生端「我的投递」状态变更为面试

演示时不要跳过第 4 步直接展示审核,因为「企业发布 → 管理员审核 → 学生可见」这个完整链路正是这套系统和普通 CRUD 项目的本质区别,也是答辩加分的关键所在。

演示数据要提前造好。职位名称用「Java 后端开发实习生」「前端开发工程师」「数据分析助理」这些真实感强的岗位;简历信息提前填好,不要现场输入;企业信息至少准备三到五家,行业覆盖互联网、制造业、教育,这样搜索和筛选功能演示时才有区分度。

源码交付前过一遍自查清单:application.yml里的数据库密码、微信 secret 是否已脱敏;项目 README 是否写清楚启动步骤和 JDK/MySQL 版本;SQL 初始化文件是否包含测试数据;小程序端的appid是否改成了自己的。一份拿到手能按文档跑起来的源码,比任何花哨的功能描述都更能打动评审老师。

选这个方向做毕设,最值得投入的其实不是「写代码」,而是「把业务链条讲完整」。我见过不少同学代码写得挺顺,但答辩时只会说「这是列表页、这是详情页」,完全讲不出状态机设计和权限校验的考量。把这个系统当成一个真实产品去理解,你的收获会比预期大得多。希望这篇笔记帮到你,也祝你答辩顺利。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询