ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

校园兼职小程序与Spring Boot后端:从表设计到并发事务的实战解析

校园兼职小程序与Spring Boot后端:从表设计到并发事务的实战解析 简介一份基于微信小程序与Java后端的校园兼职系统毕业设计面向计算机专业高年级学生及课程设计人群覆盖兼职发布、职位浏览、在线报名、信息管理等典型业务场景。压缩包共1254个文件大小14.79MB主要包含png界面素材、js与vue前后端逻辑、wxml/wxss小程序样式、json配置以及SQL数据库脚本和说明文档完整度高。已有729人学习下载可作为毕设起步模板或项目实战练手素材。资源内含源码、数据库备份、运行说明、部署脚本与项目截图能帮助快速启动系统、理解前后端分离架构及数据表设计。配合清晰的目录结构与备份文件便于二次开发与答辩讲解。1. 这个毕业设计到底在解决什么问题校园兼职一直是刚需但信息分发长期停留在QQ群、微信群公告和共享表格上兼职一发出来就被刷屏学生反复私聊确认是否招满商家要手动统计报名名单管理员更拿不到任何数据。微信小程序 Java 后端的校园兼职系统做的就是把这套流程搬成一条线上链路——商家在小程序里发布兼职学生浏览、报名、查看进度后端用 Java 接口处理登录、发单、接单和状态流转MySQL 把用户、兼职、报名记录全部落库。这套“源码数据库说明”的毕业设计适合需要完整前后端联调经验的 Java 方向学生也适合想在小程序端把登录态、request、渲染闭环走通的人。它的价值不在于功能多花哨而是用最常规的技术栈把真实业务跑通答辩时讲得清、演示得动、改得开。2. 数据库设计用户、兼职、报名三张核心表与字段边界拿到需求先别急着写接口第一步是把表结构定下来。这个系统的主体角色有三种学生、商家、管理员核心业务是“商家发兼职、学生报名、管理员审核”。绝大多数功能都可以收敛到三张表用户表 user、兼职信息表 job、报名记录表 application。登录态、权限判断、列表筛选、报名去重全都可以通过字段设计提前解决而不是靠业务代码硬扛。2.1 用户表怎么区分学生、商家和管理员很多毕设会把学生表和商家表拆开建但实际开发里更常见的是合并成一张 user 表用 role 字段区分身份。原因很直接登录逻辑只用 openid发兼职和报名兼职只是角色权限不同拆表会让登录、鉴权、个人信息维护全都变成两套代码。合并之后一张表既省 Join又方便后面扩展“管理员也是用户”的体系。角色字段用 int 还是 varchar我一般用 int0 表示学生、1 表示商家、2 表示管理员。不用字符串的原因一是比 varchar 省空间二是后端判断时写user.getRole() 1比merchant.equals(...)更顺手也避免拼写错误。user 表的核心字段如下字段类型说明idbigint自增主键内部关联用openidvarchar(64)微信用户唯一标识做唯一索引nicknamevarchar(50)昵称小程序端授权后写入avatarvarchar(255)头像 URLroletinyint0 学生、1 商家、2 管理员phonevarchar(20)手机号商家发布兼职时用于联系方式statustinyint0 禁用、1 正常create_timedatetime注册时间建表 SQL 如下CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL, nickname varchar(50) DEFAULT , avatar varchar(255) DEFAULT , role tinyint NOT NULL DEFAULT 0 COMMENT 0学生 1商家 2管理员, phone varchar(20) DEFAULT , status tinyint NOT NULL DEFAULT 1 COMMENT 0禁用 1正常, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个细节openid 必须建唯一索引。微信登录后每次拿到的是同一个 openid如果没有唯一约束重复登录就会往表里插多条记录用户身份直接串掉。用自增 id 做主键而不是直接用 openid是因为业务表job、application里关联的都是 idbigint 比 varchar 做外键关联更快也避免 openid 泄露到接口返回里。2.2 兼职信息表金额、状态和人数校验兼职表是这个系统的核心业务表字段设计上最容易翻车的三个点是金额类型、状态值、已报名人数。金额必须用 decimal不能用 float 或 double因为浮点数在计算时会有精度误差结算场景下属于硬伤。状态不要用中文字符串用 tinyint0 待审核、1 已发布、2 已下架、3 已结束后续管理员审核就是改这个字段。已报名人数 enrolled_num 和需求人数 need_num 是两个独立字段面试和答辩时经常被问“为什么不直接 count 报名表”。实际原因是列表页要展示“已报 X / 需 Y 人”每次都去 count application 表会多一次查询大列表场景性能不可控。维护一个冗余字段在报名事务里更新它是典型的空间换时间做法。CREATE TABLE job ( id bigint NOT NULL AUTO_INCREMENT, publisher_id bigint NOT NULL COMMENT 发布者用户id, title varchar(100) NOT NULL, content text, type tinyint NOT NULL DEFAULT 0 COMMENT 0传单 1家教 2助教 3其他, salary decimal(10,2) NOT NULL COMMENT 金额, salary_unit tinyint NOT NULL DEFAULT 0 COMMENT 0时薪 1日薪 2月薪, location varchar(255) DEFAULT COMMENT 兼职地点, need_num int NOT NULL DEFAULT 1 COMMENT 需求人数, enrolled_num int NOT NULL DEFAULT 0 COMMENT 已报名人数, deadline datetime DEFAULT NULL COMMENT 报名截止时间, status tinyint NOT NULL DEFAULT 0 COMMENT 0待审核 1已发布 2已下架 3已结束, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_publisher (publisher_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;索引方面publisher_id 和 status 是列表页最常用的查询条件分别建普通索引即可。这里不建外键约束只保留逻辑关联。原因在于毕设项目里删除数据的需求很常见外键会限制删除顺序小项目里用逻辑关联加索引足够真碰上脏数据也能靠事务兜住。报名表的核心是防重复。同一个学生不能重复报名同一个兼职这个约束如果只在 Service 里写“先查再插”并发下照样会插入两条记录。最稳的做法是建唯一索引让数据库层面兜底。CREATE TABLE application ( id bigint NOT NULL AUTO_INCREMENT, job_id bigint NOT NULL, user_id bigint NOT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0已报名 1已录用 2已取消, remark varchar(255) DEFAULT COMMENT 报名备注, apply_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_job_user (job_id, user_id), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.3 初始化数据与 MySQL 连接参数三张表建好之后至少要插入一个管理员账号和几个测试用户否则后端一启动连登录都进不去。管理员账号不能走微信登录直接在 SQL 里预设一条记录即可密码字段按需用 MD5 或 BCrypt 加密毕设阶段一般用 BCrypt。application.yml 里的数据库连接参数是另一个高频踩坑点尤其是时区和连接池配置spring: datasource: url: jdbc:mysql://localhost:3306/campus_job?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2serverTimezoneAsia/Shanghai 必须写。MySQL 8 默认时区是 UTC不指定的话插入的时间会比本地慢 8 个小时小程序里显示“发布时间 1970 年”的经典翻车就是这么来的。连接池用 HikariCPSpring Boot 2.x 之后默认就是它这两个参数够用不需要额外引入 druid。3. Spring Boot 后端登录、发单、接单三个接口与事务边界数据库定好之后后端接口的开发顺序很重要。我习惯先把登录打通因为后续所有接口都依赖用户身份然后再做发布兼职和报名兼职这两个接口覆盖了核心业务和事务场景。项目骨架用 Spring Boot MyBatis-PlusMyBatis-Plus 能让单表 CRUD 少写大量 XML毕设答辩时也容易讲清楚。3.1 统一返回体与全局异常前后端分离项目实战里统一返回结构是第一件事。没有统一包装小程序端每个请求都要单独判断返回码代码会乱成一团。常见的返回结构是 code、message、data 三段式public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } // getter / setter 省略 }这里的 code 和 HTTP 状态码不是一回事。HTTP 200 表示请求到达了服务器业务上的登录过期、无权限、兼职已满是通过 code 区分。小程序端拿到 code 不等于 200 就弹 toast而不是走 wx.showToast 的 fail 回调这个约定要从第一个接口就立住。全局异常处理配合返回体能让错误信息统一格式。用 RestControllerAdvice 捕获业务异常和未知异常业务异常返回具体提示未知异常统一返回“系统繁忙”避免把 SQL 报错直接暴露给前端。3.2 登录接口code2Session 换 openid 再发 JWT微信小程序登录的标准流程是小程序端 wx.login 拿到临时 code传给后端后端拿 code 调用微信的 code2Session 接口换 openid。openid 在数据库存在则直接登录不存在则自动注册。注意 code 只能用一次而且有效期只有五分钟后端拿到 code 后要立即调用微信接口。RestController RequestMapping(/api/auth) public class AuthController { Autowired private AuthService authService; PostMapping(/login) public ResultLoginVO login(RequestBody LoginDTO dto) { // dto 里只有 code 和用户昵称头像等非敏感信息 String openid authService.code2Session(dto.getCode()); User user authService.findOrCreateUser(openid, dto); String token authService.createToken(user); LoginVO vo new LoginVO(token, user.getRole()); return Result.success(vo); } }code2Session 的实现是用 RestTemplate 调微信接口。这里有一个必须守住的安全底线appid 和 secret 只能放在后端绝不能写进小程序代码里。小程序端打包后代码是可以被解包查看的secret 泄露等于任何人都能冒充你的小程序身份。public String code2Session(String code) { String url https://api.weixin.qq.com/sns/jscode2session?appid appid secret secret js_code code grant_typeauthorization_code; String resp restTemplate.getForObject(url, String.class); JSONObject obj JSON.parseObject(resp); if (obj.getInteger(errcode) ! null obj.getInteger(errcode) ! 0) { throw new BusinessException(微信登录失败 obj.getString(errmsg)); } return obj.getString(openid); }token 我习惯用 JWT自包含、无状态后端重启不用重新登录。JWT 的过期时间设置两个小时比较合理太短导致用户频繁重新登录太长又失去意义。签发时把 userId 和 role 放进 payload后续接口从 token 里解析用户身份不需要每次查数据库。3.3 发布兼职接口与报名接口的事务边界发布兼职的逻辑简单但权限要卡死只有 role1 的商家能发布。实现上用拦截器或者注解校验角色不要在每个接口里手写 if 判断。报名兼职是事务的重点核心逻辑是校验兼职状态是已发布、校验当前报名人数小于需求人数、插入报名记录、更新 enrolled_num。这四个操作必须在一个事务里否则会出现“记录插进去了人数没更新”的脏数据。Transactional(rollbackFor Exception.class) public void applyJob(Long jobId, Long userId) { // 1. 查兼职用悲观锁防止并发超报 Job job jobMapper.selectByIdForUpdate(jobId); if (job null || job.getStatus() ! 1) { throw new BusinessException(兼职不存在或已下架); } if (job.getEnrolledNum() job.getNeedNum()) { throw new BusinessException(报名人数已满); } // 2. 插入报名记录数据库唯一索引兜底去重 Application app new Application(); app.setJobId(jobId); app.setUserId(userId); app.setStatus(0); try { applicationMapper.insert(app); } catch (DuplicateKeyException e) { throw new BusinessException(你已经报名过该兼职); } // 3. 更新已报名人数 jobMapper.increaseEnrolledNum(jobId); }selectByIdForUpdate 是行级悲观锁加锁后并发请求会排队避免两个请求同时读到 enrolled_num9 而都通过校验。这里还有一个隐藏问题increaseEnrolledNum 必须用 SQL 里的自增语句UPDATE job SET enrolled_num enrolled_num 1 WHERE id ?而不是先查出来加 1 再更新。后者在并发下会丢失更新即使有锁也有可能因为事务隔离级别产生不一致。锁只保证同时只有一个人能走到更新那一步但用读改写模式配合悲观锁能勉强撑住用自增 SQL 才是真正稳妥的写法。事务的 rollbackFor Exception.class 必须配置。Spring 默认只在 RuntimeException 时回滚如果业务代码里抛的是 checked exception事务不会回滚报名记录插进去了但人数没更新这种问题排查起来非常隐蔽。4. 微信小程序端登录态、wx.request 与页面渲染闭环后端接口完成后小程序端的核心工作就是三件事封装 request、处理登录态、把接口数据渲染到页面。很多毕设翻车不是后端接口问题而是小程序端的登录态没打通或者请求封装不统一导致每个页面都在重复写 wx.request。4.1 request 封装baseURL、token 和错误处理小程序里没有 axios所有请求都走 wx.request。但如果每个页面直接写 wx.request后续改 baseURL、统一加 token、处理 401 都会变成灾难。常见的做法是在 utils/request.js 里封装一层页面只关心业务数据。const BASE_URL http://localhost:8080/api; const request (url, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method || GET, data: data || {}, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || }, success: (res) { if (res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { // token 过期跳转登录页 wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(res.data); } else { wx.showToast({ title: res.data.message, icon: none }); reject(res.data); } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); }; module.exports { request, BASE_URL };BASE_URL 这段在真机调试时要特别注意。开发者工具里写 localhost 能通真机上 localhost 指向的是手机自己必须改成电脑的局域网 IP。而且这个 IP 要在小程序后台的 request 合法域名里配置开发阶段可以勾选“不校验合法域名”上线前必须换 HTTPS 域名。token 存在 wx.getStorageSync 里每次请求从缓存读取。这里不要每次 setStorageSync读写频繁会影响性能登录成功后写一次即可。401 的处理统一在这里跳转登录页页面代码里不需要再关心 token 过期。4.2 登录流程wx.login、授权弹窗和角色跳转小程序的登录流程比普通网页复杂的地方在于用户信息授权和登录是两件事。wx.login 拿到 code 不需要弹窗但获取头像昵称需要用户主动点击授权按钮。常见做法是页面加载时先检查 token 是否存在不存在就先 wx.login 换 code 调后端登录接口头像昵称在用户进入“个人信息”页时再引导授权避免一打开小程序就弹窗把人吓跑。// pages/login/login.js const { request } require(../../utils/request); Page({ async handleLogin() { const { code } await wx.login(); // 获取用户头像昵称用户拒绝也不影响登录 const userProfile await wx.getUserProfile({ desc: 用于完善用户资料 }).catch(() null); const data { code: code, nickname: userProfile ? userProfile.userInfo.nickName : 微信用户, avatar: userProfile ? userProfile.userInfo.avatarUrl : }; const res await request(/auth/login, POST, data); wx.setStorageSync(token, res.token); wx.setStorageSync(role, res.role); // 商家和管理员跳不同首页 if (res.role 1) { wx.switchTab({ url: /pages/publish/publish }); } else { wx.switchTab({ url: /pages/index/index }); } } });wx.getUserProfile 必须在用户点击事件里调用不能在 onLoad 里直接调这是微信的硬性约束触控事件外调用会直接失败。用户拒绝授权时 catch 里拿到 null代码仍然用 code 完成登录只是 nickname 显示默认值。这样的好处是不会因为授权失败卡住整个登录流程用户之后随时可以在个人中心补充资料。4.3 列表页渲染onLoad 拉数据、下拉刷新与已报人数兼职列表页是小程序的门面涉及三个典型问题页面加载时拉取数据、下拉刷新、列表数据渲染。这里不推荐用 scroll-view 做复杂列表原生 Page 的 onPullDownRefresh 配合 request 封装足够。// pages/index/index.js const { request } require(../../utils/request); Page({ data: { jobList: [], page: 1, hasMore: true }, async onLoad() { await this.loadJobs(); }, async onPullDownRefresh() { this.setData({ page: 1, jobList: [], hasMore: true }); await this.loadJobs(); wx.stopPullDownRefresh(); }, async loadJobs() { if (!this.data.hasMore) return; const res await request(/job/list?page${this.data.page}size10, GET); this.setData({ jobList: this.data.jobList.concat(res.records), hasMore: res.records.length 10 }); this.setData({ page: this.data.page 1 }); } });分页条件是res.records.length 10刚好一页满 10 条就认为还有下一页否则没有。这个判断在数据量恰好是 10 的倍数时会多请求一次空页不过返回值是空数组不影响业务满足毕设足够了。下拉刷新时先重置 page 再重新拉数据注意 setData 是异步的this.data.jobList 在 setData 后立即读取不一定是最新值所以用 concat 的是上一次的 data 值而不是刚 set 的值。页面渲染时有一个微信小程序的固有问题顶部导航栏高度在不同机型上不一样iPhone 的刘海屏和普通安卓机的状态栏高度不同。如果页面里有自定义导航栏或吸顶效果用 wx.getWindowInfo().statusBarHeight 动态计算不要硬编码 64 或 20否则在刘海屏上会看到严重偏移。5. 避坑校园兼职系统最容易翻车的 5 个地方这 5 个问题是我自己在这个方向上反复踩过的坑每一个都能让项目在演示现场直接黑屏而且都不是功能逻辑问题是环境、配置和数据层面的隐性坑。5.1 登录接口报 40001code 无效或 appid 不匹配现象开发者工具里登录正常换到真机预览就弹“微信登录失败”。原因有三种可能appid 用的是测试号但后端配置了正式号code 被用了两次secret 配错。解决方式是先看后端日志里 code2Session 的返回值errcode 40001 就是 appid 或 secret 问题40029 是 code 无效。另外 wx.login 的 code 只能用一次如果前端在同一个页面调了两次登录接口第二次必然失败。5.2 图片上传后头像不显示报 404现象开发工具里上传头像正常部署到服务器后头像打不开。原因小程序端 wx.uploadFile 拿到的是临时文件路径这个路径在微信服务器上只保留几分钟直接存库必然失效。解决方式是在后端把上传的临时文件转存到本地 static 目录或对象存储再把持久化后的 URL 存库。本地存储时注意路径不能写死要用 Spring Boot 的静态资源映射上线后还要考虑磁盘空间毕设阶段本地存储够用。5.3 兼职列表的发布时间比当前时间慢 8 小时现象后端插入数据正常小程序展示的发布时间总是前一天。原因JDBC URL 里没配 serverTimezoneMySQL 8 默认用 UTC 时区。解决方式是在连接参数里加 serverTimezoneAsia/Shanghai同时检查服务器系统时区Linux 上用timedatectl看下是否 UTC必要时在启动参数里强制指定时区。5.4 并发报名出现重复记录或超员现象用两个微信号同时报名同一个只剩 1 个名额的兼职两个人都显示报名成功。原因service 层先查人数再插入的两步操作之间没有锁并发请求同时读到 enrolled_num9都通过了 if 判断。解决方式是在事务里用SELECT ... FOR UPDATE锁住 job 行同时靠 application 表的唯一索引兜底捕获 DuplicateKeyException 后提示“你已经报过名了”。5.5 小程序审核被拒类目不对或缺少资质现象正式的微信小程序版本提审时因为涉及招聘类内容被拒。原因校园兼职属于招聘求职类目个人主体的小程序无法通过这类审核。解决方式毕设阶段使用微信开发者工具的测试号或体验版演示即可不要提交正式审核如果必须上线需要企业主体营业执照并在小程序后台选择对应的招聘类目。另一个方式是弱化“招聘”文案改成“校内实践平台”但本质还是类目问题不是文案能掩盖的。6. 答辩前必做的验证与两个提分改进答辩演示和本地跑通是两回事。演示前至少做三轮验证第一关掉后端再启动确认数据库初始化脚本可以一键重建第二用真机而不是开发者工具跑一遍完整流程——发布兼职、切换学生账号报名、查看报名人数变化第三把手机切到飞行模式再打开确认请求失败时有 toast 提示而不是白屏。这三步全过演示才有基本保障。提分改进我建议做两个。第一个是给商家加一个“报名名单”页面展示谁报了名、联系方式是什么、录用状态怎么改。这个功能成本低但能讲出完整的业务闭环比单纯 CRUD 有说服力。第二个是给报名截止和录用结果加微信订阅消息通知。微信订阅消息需要在小程序后台申请模板后端在报名成功或录用时推送一条通知这属于“做了就有亮点”的加分项。我自己的教训是宁可把登录流程和报名事务讲透也不要堆砌一堆没跑通的炫技功能。答辩老师最常追问的就是并发、权限、事务边界这三个点把它们讲清楚项目就立住了。希望这篇笔记能帮你把校园兼职系统从“能跑”做到“能讲”少走我当年走过的弯路。本文还有配套的精品资源点击获取
返回列表