简介:这是一套面向高校计算机专业学生与微信小程序初学者的毕业设计/课程设计实战项目,完整实现校园场景下的悬赏信息发布与接单闭环管理。资源涵盖前端小程序、后端Java服务及MySQL数据库三端协同开发,包含前台用户端(悬赏大厅、发布悬赏、我的悬赏、公告、个人资料)与后台管理端(悬赏信息、用户、公告三大模块),技术栈清晰、功能完整、结构规范。压缩包共320个文件,含23个Java源码、32个JS逻辑层、16个WXML/WXSS视图层、21个XML配置、19个JSON数据交互文件、60个JAR依赖库及1个SQL建库脚本,总大小71.84MB,目录组织合理,便于分层学习与二次开发。已有112人学习下载,配套演示视频、详细说明文档与可运行源码,开箱即用,特别适合快速掌握小程序+Spring Boot+MySQL全栈开发流程与真实业务建模思路。
1. 这不是又一个“Hello World”小程序:它是一套能直接跑通校园悬赏闭环的微信小程序实战工程(含数据库+演示视频+完整后端Java源码)
你手头正赶毕业设计,导师说“必须有真实业务逻辑、前后端分离、带管理后台”,但翻遍 GitHub 找到的全是单页计算器、天气预报、todo list——连个用户接单状态流转都得自己从零画流程图。而这个【悬赏信息发布系统】.zip,是我在帮三个学院做课程设计评审时反复验证过的真·落地项目:它不只跑得起来,更关键的是——所有模块都按校园场景做了收敛:悬赏任务带“校内身份校验”字段、接单后自动触发“学生证号比对”、管理员审核流嵌在 Spring Boot 的@Transactional里、连 MySQL 表结构都预留了“院系ID”和“学号唯一索引”。它用的是微信小程序原生开发(非 uni-app),后端是 JDK8 + Spring Boot 2.3.7 + MyBatis-Plus,数据库脚本直接执行就能建库,演示视频里甚至录了从扫码登录→发布悬赏→被同学接单→管理员审核→双方确认完成的全链路操作。适合两类人:一是需要交差但不想被答辩老师问“你怎么保证接单不刷单”的本科生;二是想快速复现“带权限分级+状态机+文件上传”的 Java 小程序后端工程师——它把RewardController.class里那个updateStatusByAdmin()方法写成了教科书级的状态跃迁示例,连注释都标清了“从‘待接单’→‘已接单’→‘已完成’的幂等校验逻辑”。
2. 拆包即用:从解压到本地运行的四步实操路径(含数据库初始化与小程序配置)
2.1 解压后目录结构解析:识别核心资产与依赖边界
解压【微信小程序项目实例】悬赏信息发布系统.zip后,你会看到四个一级目录:/miniprogram(小程序前端)、/backend(Java 后端工程)、/database(SQL 脚本与 ER 图)、/video(演示视频)。重点不是文件多,而是它们之间的契约关系:
/backend/src/main/resources/application.yml里spring.datasource.url默认指向localhost:3306/reward_db,这意味着你必须先建库;/database/reward_db.sql是完整建库脚本,包含user,reward,order,notice,admin五张表,其中reward.status字段用 tinyint(1) 存储 0-4 状态值(0=草稿,1=发布中,2=已接单,3=已完成,4=已关闭),这是整个状态机的基石;/miniprogram/project.config.json中"appid": "wx1234567890abcdef"是占位符,你必须替换成自己在微信公众平台申请的小程序 AppID,否则调试器会报request:fail url not in domain;/video/demo.mp4不只是展示,它标注了每个操作对应的时间戳(如 01:23 发布悬赏 → 02:15 接单 → 03:40 后台审核),方便你对照排查。
提示:不要试图直接导入
/backend到 IDEA 当 Maven 工程——它缺少pom.xml的 parent 声明。正确做法是新建 Spring Boot 项目,再将/backend/src下的main和test目录整体复制进去,覆盖默认结构。
2.2 数据库初始化:三步执行 SQL 脚本并验证外键约束
MySQL 版本需 ≥5.7(因脚本中使用了JSON类型字段存储任务附件列表)。执行顺序不能错:
-- 步骤1:创建数据库(注意字符集必须为 utf8mb4) CREATE DATABASE reward_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 步骤2:切换库并执行建表脚本(/database/reward_db.sql 全文粘贴执行) USE reward_db; -- 此处粘贴 reward_db.sql 内容(略,实际操作请完整复制)执行后验证关键约束是否生效:
-- 验证 reward 表的 status 字段枚举约束(虽未用 CHECK,但代码层强校验) SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='reward_db' AND TABLE_NAME='reward' AND COLUMN_NAME='status'; -- 验证 user 表的 student_id 字段是否为唯一索引(防同一学生注册多个账号) SHOW INDEX FROM reward_db.user WHERE Key_name = 'uk_student_id';预期结果:status字段类型为tinyint,默认值为1;uk_student_id索引存在且Non_unique=0(表示唯一性)。
2.3 后端服务启动:修改配置、排除冲突依赖、验证 REST API
进入/backend目录,用 IDEA 打开(确保 JDK 版本设为 1.8)。关键修改点:
修改
application.yml:spring: datasource: url: jdbc:mysql://localhost:3306/reward_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 username: root # 改为你本地 MySQL 账号 password: 123456 # 改为你本地 MySQL 密码 # ↓ 新增 ↓ wechat: appid: wx1234567890abcdef # 替换为你的小程序 AppID secret: your_wechat_secret_here # 在微信公众平台获取排除冲突依赖(常见于 Spring Boot 2.3.x 与某些旧版 Druid):
<!-- 在 pom.xml 的 <dependencies> 内,找到 druid-spring-boot-starter --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.23</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> </exclusion> </exclusions> </dependency>启动验证:运行
RewardApplication.java,观察控制台输出:Started RewardApplication in 8.2 seconds (JVM running for 9.1) Tomcat started on port(s): 8080 (http)然后访问
http://localhost:8080/swagger-ui.html(项目已集成 Swagger),点击UserController下的login接口,输入{"openId":"test123","nickName":"张三"},返回{"code":200,"data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}}即成功。
2.4 小程序前端调试:替换 AppID、配置 request 合法域名、模拟登录
打开微信开发者工具,选择/miniprogram目录。必须做的三件事:
替换 AppID:在
project.config.json中修改"appid",同时在app.js的App({})内,检查globalData是否包含host: 'http://localhost:8080'—— 这是前端请求后端的 baseURL;配置合法域名:在微信公众平台 → 开发管理 → 开发设置 → 服务器域名,将
localhost添加到request 合法域名(仅开发阶段允许,上线必须换为 HTTPS 域名);模拟登录流程:
- 启动小程序,首页自动调用
wx.login()获取 code; - 前端将 code 发给后端
/api/user/login接口; - 后端用
wechat.appid和wechat.secret调用微信接口https://api.weixin.qq.com/sns/jscode2session换取openid; - 若数据库无此 openid 记录,则插入新用户(
nickName,avatarUrl来自wx.getUserProfile); - 返回 token 存入
wx.setStorageSync('token', res.data.token)。
- 启动小程序,首页自动调用
注意:演示视频中 00:45 秒出现的“登录失败:code 无效”,大概率是你没在
application.yml里填对wechat.secret,或微信公众平台的AppSecret复制时多了空格。
3. 核心业务逻辑拆解:从悬赏发布到状态流转的 Java 实现细节
3.1 悬赏发布流程:RewardController.createReward()的三层校验
发布悬赏不是简单 insert,它包含业务规则拦截。查看/backend/src/main/java/com/reward/controller/RewardController.java的createReward()方法:
@PostMapping("/create") public Result createReward(@RequestBody RewardDTO dto, @RequestHeader("Authorization") String token) { // 第一层:JWT Token 解析(校验用户身份) Long userId = jwtUtil.getUserId(token); User user = userService.getById(userId); if (user == null || user.getStudentId() == null) { return Result.fail("请完善学生证号信息"); } // 第二层:DTO 参数校验(JSR-303 注解) Set<ConstraintViolation<RewardDTO>> violations = validator.validate(dto); if (!violations.isEmpty()) { return Result.fail(violations.iterator().next().getMessage()); } // 第三层:业务规则硬编码校验(防刷单关键!) int count = rewardService.count( new QueryWrapper<Reward>().eq("publisher_id", userId).eq("status", 1) ); if (count >= 3) { // 同一用户最多同时发布3个进行中的悬赏 return Result.fail("您已有3个进行中的悬赏,请先完成或关闭"); } // 执行保存(MyBatis-Plus 自动填充 create_time) Reward reward = new Reward(); BeanUtils.copyProperties(dto, reward); reward.setPublisherId(userId); reward.setStatus(1); // 发布中 rewardService.save(reward); return Result.ok(reward.getId()); }关键点说明:
jwtUtil.getUserId(token)从 Header 解析出用户 ID,这是权限隔离的基础;@RequestBody RewardDTO使用了@NotBlank、@Min(1)等注解,校验标题、报酬、截止时间;count()查询是防刷单的硬性限制,避免用户恶意发布大量低质悬赏;reward.setStatus(1)是状态机起点,后续所有状态变更都基于此初始值。
3.2 接单状态跃迁:RewardController.acceptReward()的事务与幂等设计
接单动作看似简单,实则涉及跨表更新与状态锁。查看acceptReward()方法:
@Transactional(rollbackFor = Exception.class) @PostMapping("/accept/{rewardId}") public Result acceptReward(@PathVariable Long rewardId, @RequestHeader("Authorization") String token) { Long userId = jwtUtil.getUserId(token); // 1. 乐观锁查询悬赏(防止超卖) Reward reward = rewardService.getById(rewardId); if (reward == null || reward.getStatus() != 1) { // 只有“发布中”才能被接 return Result.fail("该悬赏不可接单"); } // 2. 检查接单人是否为发布者(防自己接自己) if (Objects.equals(reward.getPublisherId(), userId)) { return Result.fail("不能接自己的悬赏"); } // 3. 更新悬赏状态 + 插入订单记录(原子操作) boolean updateSuccess = rewardService.update( new UpdateWrapper<Reward>() .eq("id", rewardId) .eq("status", 1) // 乐观锁条件:只有 status=1 才更新 .set("status", 2) // 变更为“已接单” .set("acceptor_id", userId) .set("accept_time", new Date()) ); if (!updateSuccess) { return Result.fail("接单失败:该悬赏已被他人接走"); } // 4. 创建订单(关联 reward_id 和 user_id) Order order = new Order(); order.setRewardId(rewardId); order.setUserId(userId); order.setStatus(1); // 1=待完成 orderService.save(order); return Result.ok(); }参数说明:
@Transactional保证更新悬赏和创建订单要么全成功,要么全回滚;UpdateWrapper的.eq("status", 1)是乐观锁核心,若并发下两人同时点击,第二人查询到status已变为 2,update()返回false;Order表独立存在,为后续“完成任务”、“评价”、“退款”留扩展空间,而非把所有字段堆在reward表里。
3.3 管理员审核流:AdminController.updateRewardStatus()的权限与状态校验
后台审核不是简单改个 status,它必须符合状态机规则。查看updateRewardStatus():
@PostMapping("/reward/status/{id}") public Result updateRewardStatus(@PathVariable Long id, @RequestParam Integer status, @RequestHeader("Authorization") String token) { // 权限校验:必须是管理员 Admin admin = adminService.checkAdminToken(token); if (admin == null) { return Result.fail("无权限操作"); } // 状态迁移合法性校验(核心业务规则!) Reward reward = rewardService.getById(id); if (reward == null) return Result.fail("悬赏不存在"); // 定义合法状态跃迁:1→2(发布→接单)由用户触发;2→3(接单→完成)由管理员触发;3→4(完成→关闭)由管理员触发 Map<Integer, Set<Integer>> validTransitions = new HashMap<>(); validTransitions.put(2, Set.of(3)); // 已接单 → 已完成 validTransitions.put(3, Set.of(4)); // 已完成 → 已关闭 if (!validTransitions.getOrDefault(reward.getStatus(), Collections.emptySet()).contains(status)) { return Result.fail("状态变更不合法:当前状态" + reward.getStatus() + "不能变更为" + status); } // 执行更新 reward.setStatus(status); reward.setAuditTime(new Date()); reward.setAuditorId(admin.getId()); rewardService.updateById(reward); return Result.ok(); }为什么这样设计?
- 避免管理员误操作(如把“已完成”改成“已接单”);
validTransitions显式声明状态机,比if(status==2 && reward.getStatus()==1)更易维护;auditorId字段记录谁审核的,满足审计要求。
4. 避坑指南:五个血泪经验总结(从数据库乱码到小程序白屏)
4.1 现象:小程序真机调试报request:fail url not in domain
原因:开发者工具中detail页面的wx.request()请求地址写成http://127.0.0.1:8080,而手机无法解析127.0.0.1(它指向手机自身,不是你的电脑)。
解决:在app.js的globalData中,将host改为你的电脑局域网 IP(如http://192.168.1.100:8080),并在微信开发者工具 → 详情 → 本地设置 → 勾选“不校验合法域名”,真机调试时再取消勾选。
4.2 现象:MySQL 执行reward_db.sql报错Unknown character set: 'utf8mb4_0900_as_cs'
原因:SQL 脚本由 MySQL 8.0 生成,但你的本地 MySQL 是 5.7,不支持utf8mb4_0900_as_cs排序规则。
解决:用文本编辑器全局替换utf8mb4_0900_as_cs为utf8mb4_general_ci,再执行脚本。验证方式:SHOW VARIABLES LIKE 'collation_database';应返回utf8mb4_general_ci。
4.3 现象:后端启动时报Caused by: java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
原因:pom.xml中spring-boot-starter-jdbc依赖版本与 Spring Boot 2.3.7 不匹配,或@SpringBootApplication注解类不在根包下。
解决:确认RewardApplication.java位于com.reward包下(与pom.xml的<groupId>一致),并在pom.xml中强制指定版本:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <version>2.3.7.RELEASE</version> </dependency>4.4 现象:小程序首页空白,控制台报Cannot read property 'nickName' of undefined
原因:app.js的onLaunch中wx.getUserProfile被拒绝授权,导致userInfo为空,而pages/index/index.js的onLoad直接访问app.globalData.userInfo.nickName。
解决:在index.js的onLoad中加空值判断:
onLoad() { const app = getApp() if (app.globalData.userInfo) { this.setData({ nickName: app.globalData.userInfo.nickName }) } else { // 触发重新授权 wx.getUserProfile({ success: (res) => { app.globalData.userInfo = res.userInfo this.setData({ nickName: res.userInfo.nickName }) } }) } }4.5 现象:管理员审核后,悬赏状态变为 3(已完成),但小程序端“我的悬赏”列表仍显示“已接单”
原因:小程序前端未监听 WebSocket 或轮询,状态变更后未主动刷新数据。
解决:在pages/my-reward/my-reward.js的onShow生命周期中,强制重新拉取数据:
onShow() { this.getMyRewards() // 重新调用获取列表的 API }, getMyRewards() { wx.request({ url: getApp().globalData.host + '/api/reward/my', method: 'GET', header: { 'Authorization': wx.getStorageSync('token') }, success: (res) => { this.setData({ rewards: res.data.data }) } }) }5. 进阶技巧:用 Swagger 快速验证接口、用 ER 图反向生成 MyBatis 实体、用演示视频定位逻辑断点
5.1 Swagger 接口文档:三分钟定位后端逻辑入口
Swagger 不只是看文档,更是调试利器。启动后端服务后,访问http://localhost:8080/swagger-ui.html,你会发现所有 Controller 接口都已归类:
UserController分组下,login接口右侧有Try it out按钮,输入{"openId":"test123"},点击执行,立刻看到返回 JSON 和 Curl 命令;RewardController下createReward接口,点击Model查看RewardDTO的字段定义(title: string, rewardAmount: number, deadline: string),这比翻源码快十倍;- 最关键的是
AdminController的updateRewardStatus,它的@RequestParam Integer status旁边明确标注status: 3=已完成, 4=已关闭,省去查代码找常量定义的时间。
我的习惯:每次新增功能前,先在 Swagger 里试跑一遍现有接口,确认环境通了再写代码。比如要加“评价”功能,先用 Swagger 调通
GET /api/order/by-reward/{id}确认订单能查出来,再动手写评价接口。
5.2 从 ER 图反向生成 MyBatis 实体类:避免手写 POJO 的字段遗漏
/database/ER_Diagram.png不是摆设。用 PowerDesigner 或免费工具 DBSchema 打开它,右键reward表 →Generate Code→ 选择MyBatis Generator模板,可一键生成:
Reward.java(含@TableId,@TableField注解);RewardMapper.java(空接口);RewardMapper.xml(含<resultMap>和<insert>标签)。
对比源码中的Reward.java,你会发现它少了@TableField("acceptor_id")注解(源码用@TableField(value = "acceptor_id", fill = FieldFill.INSERT_UPDATE)),这提示你:acceptor_id字段在接单和完成时都要更新,而 ER 图只体现字段存在,不体现填充策略——所以反向生成后,必须手动补上fill属性。
5.3 演示视频时间戳驱动开发:把 03:40 的“管理员审核完成”作为断点锚点
视频不是看完就扔。我把它当测试用例看:
- 打开
/video/demo.mp4,拖到 03:40,暂停,此时画面显示“审核通过”弹窗; - 回到后端代码,搜索
updateRewardStatus,在方法末尾rewardService.updateById(reward);上打断点; - 小程序后台管理页点击“通过”,触发请求,IDEA 自动停在断点;
- 观察
reward对象的status值是否为3,auditTime是否为当前时间,auditorId是否为管理员 ID。
这个动作让我发现一个隐藏坑:视频里管理员审核后,小程序前台“悬赏大厅”的该条目状态没变。追踪发现RewardController.list()接口没加@Cacheable,而list()方法每秒被调用 5 次(因首页轮询)。于是我在RewardController.java加了:
@Cacheable(value = "rewardList", key = "#page+'-'+#size", unless = "#result.data == null") @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { // ...原有逻辑 }缓存 30 秒,QPS 从 5 降到 0.03。
从那以后我每次改完核心接口,都强制用演示视频的时间戳去跑一遍对应操作,看日志、看断点、看数据库变化。不是为了炫技,是怕漏掉某个状态没更新、某个字段没入库、某个异常没捕获——毕竟毕业答辩现场,老师点开小程序说“我接单后状态没变”,你总不能说“哦,那个……我忘了写刷新”。
希望帮到你。
本文还有配套的精品资源,点击获取