ARTICLE DETAIL

资讯详情

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

校园悬赏小程序实战:Spring Boot+微信原生+状态机设计

校园悬赏小程序实战:Spring Boot+微信原生+状态机设计 简介这是一套面向高校计算机专业学生与微信小程序初学者的毕业设计/课程设计实战项目完整实现校园场景下的悬赏信息发布与接单闭环管理。资源涵盖前端小程序、后端Java服务及MySQL数据库三端协同开发包含前台用户端悬赏大厅、发布悬赏、我的悬赏、公告、个人资料与后台管理端悬赏信息、用户、公告三大模块技术栈清晰、功能完整、结构规范。压缩包共320个文件含23个Java源码、32个JS逻辑层、16个WXML/WXSS视图层、21个XML配置、19个JSON数据交互文件、60个JAR依赖库及1个SQL建库脚本总大小71.84MB目录组织合理便于分层学习与二次开发。已有112人学习下载配套演示视频、详细说明文档与可运行源码开箱即用特别适合快速掌握小程序Spring BootMySQL全栈开发流程与真实业务建模思路。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小程序前端、/backendJava 后端工程、/databaseSQL 脚本与 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_SCHEMAreward_db AND TABLE_NAMEreward AND COLUMN_NAMEstatus; -- 验证 user 表的 student_id 字段是否为唯一索引防同一学生注册多个账号 SHOW INDEX FROM reward_db.user WHERE Key_name uk_student_id;预期结果status字段类型为tinyint默认值为1uk_student_id索引存在且Non_unique0表示唯一性。2.3 后端服务启动修改配置、排除冲突依赖、验证 REST API进入/backend目录用 IDEA 打开确保 JDK 版本设为 1.8。关键修改点修改application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/reward_db?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%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 groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.1.23/version exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-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 注解 SetConstraintViolationRewardDTO violations validator.validate(dto); if (!violations.isEmpty()) { return Result.fail(violations.iterator().next().getMessage()); } // 第三层业务规则硬编码校验防刷单关键 int count rewardService.count( new QueryWrapperReward().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 UpdateWrapperReward() .eq(id, rewardId) .eq(status, 1) // 乐观锁条件只有 status1 才更新 .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已变为 2update()返回falseOrder表独立存在为后续“完成任务”、“评价”、“退款”留扩展空间而非把所有字段堆在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完成→关闭由管理员触发 MapInteger, SetInteger 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(status2 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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId version2.3.7.RELEASE/version /dependency4.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值是否为3auditTime是否为当前时间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。从那以后我每次改完核心接口都强制用演示视频的时间戳去跑一遍对应操作看日志、看断点、看数据库变化。不是为了炫技是怕漏掉某个状态没更新、某个字段没入库、某个异常没捕获——毕竟毕业答辩现场老师点开小程序说“我接单后状态没变”你总不能说“哦那个……我忘了写刷新”。希望帮到你。本文还有配套的精品资源点击获取
返回列表