简介:面向小程序毕业设计或课程设计的开发者,这份资料以微信小程序为前端、Java(SSM/SpringBoot)为后端,实现悬赏信息的发布、审核与展示等完整功能,包含项目源码、数据库脚本及部署工具。代码带有详细注释,结构清晰,即使新手也能快速理解,适合用于毕业设计、期末大作业或课程设计等应用场景。资源共320个文件,压缩包约44.85MB,主要涵盖Java源码与配置文件(java/class/xml/properties)、小程序页面逻辑(js/wxml/wxss/json)、界面图片(png/jpg/gif)以及数据库脚本(sql)等,各类型文件串联起前端界面、后端服务与数据存储的完整开发链路。系统功能完善、界面美观,后台管理便捷,项目已通过严格调试确保可运行,并提供MySql、Tomcat等运行环境建议,便于部署。至今已有45人学习/下载,可直接用于毕设参考或二次开发。
1. 悬赏信息发布系统(Java + 微信小程序):这个 zip 包里到底有什么,值不值得自己搭
拿到“微信小程序-悬赏信息发布系统(java)(包括源码,数据库,教程).zip”这个包的人,多半是在做 Java 课程设计、毕业设计,或者想攒一个前后端打通的小程序项目经验。这个包对应的是一个典型的前后端分离项目:后端用 Java(常见是 Spring Boot)提供悬赏发布、接单、结算这类业务接口,前端是微信小程序,用户在微信里就能发布悬赏任务、浏览列表、接下任务。它解决的核心问题是“需求方发单、接单方抢单、双方确认完成”这条业务链路如何在微信生态里跑通。适合两类人:一是需要一份能跑通、能给老师演示完整流程的课设/毕设源码,二是想学习小程序与 Java 后端如何真实联调的开发者。
2. 悬赏业务的数据模型与后端接口:先想清楚这三张表和状态机
悬赏信息发布,核心业务不复杂:一个用户发布任务,其他用户看到后接单,完成后发布者确认并结算。但正因为链路短,数据模型的设计反而容易偷懒。我拆过不少这类课设包,最常见的翻车点是任务状态用字符串瞎写,或者接单记录和任务状态没有关联约束,导致“任务已经被人接了,列表还能看到”这种低级问题。所以拿到源码第一步不是急着跑,而是先把表结构和状态机读懂。
2.1 核心表结构:用户表、悬赏表、接单记录表
一个能自洽的悬赏系统,最小闭环是四张表:用户表(user)、悬赏任务表(task)、接单记录表(accept_record)、以及资金流水表(wallet_log)。资金流水表在课设里经常被省略,但如果任务结算涉及余额变动,没有这张表,发布者和接单者的钱对不上账,演示时会被老师问住。
user 表的最小字段:
CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `openid` VARCHAR(64) UNIQUE NOT NULL COMMENT '微信openid', `nickname` VARCHAR(64) DEFAULT '' COMMENT '昵称', `avatar_url` VARCHAR(512) DEFAULT '' COMMENT '头像地址', `balance` DECIMAL(10,2) DEFAULT 0.00 COMMENT '账户余额', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有两个容易被忽视的点。第一,openid 必须加唯一索引,因为同一用户在系统里只能有一个账号,而微信小程序换绑手机号不会换 openid,openid 才是真正的用户身份主键。第二,nickname 和 avatar_url 直接冗余存进 user 表,不要每次请求都调 wx.getUserProfile 去微信拿,小程序端拿到的用户信息本来就是前端传过来的,存下来用即可。
task 表是整个系统的核心,字段设计直接决定后面接口好不好写:
CREATE TABLE `task` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `title` VARCHAR(100) NOT NULL COMMENT '悬赏标题', `description` TEXT COMMENT '任务详情', `reward` DECIMAL(10,2) NOT NULL COMMENT '悬赏金额', `publisher_id` INT NOT NULL COMMENT '发布者id', `accepter_id` INT DEFAULT NULL COMMENT '接单者id', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1进行中 2已完成 3已取消', `deadline` DATETIME DEFAULT NULL COMMENT '截止时间', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, KEY `idx_status_create` (`status`, `create_time`), KEY `idx_publisher` (`publisher_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;status 用 TINYINT 而不是 VARCHAR,是为了索引效率和避免字符串拼写错误。这里我推荐保留 1/2/3 三个状态就够,很多课设版本会加“0草稿、4申诉中、5已退款”,状态一多,小程序端的渲染分支和后台管理都要跟着加逻辑,不是核心加分项。
注意 task 表里有 publisher_id 和 accepter_id 两个用户外键,但没有建 FOREIGN KEY 约束。这是刻意的:课设级系统用逻辑外键 + 应用层校验,导入数据、删测试数据都方便,不会因为外键约束导致 SQL 导入顺序对不上。真要做生产级,再加物理外键不迟。
accept_record 表负责记录每一次接单行为:
CREATE TABLE `accept_record` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `task_id` INT NOT NULL, `user_id` INT NOT NULL, `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1已接单 2已完成 3已取消', `accept_time` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_task_user` (`task_id`, `user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;uk_task_user 这个联合唯一索引是接单防重的关键,同一个用户对同一个任务只能有一条接单记录。光靠代码里 if 判断是不够的,并发场景下两次请求同时查到“没人接过”,就可能产生两条记录。数据库唯一索引兜底,是这类系统最便宜的防并发方案。
2.2 悬赏任务的状态机:从发布到结算的流转设计
状态机是这一章最容易写成黑匣子的地方。task.status 的变化路径应该是闭环的:
1 进行中 → 2 已完成(发布者确认) 1 进行中 → 3 已取消(发布者取消,或截止时间到了) 2 已完成 → 无后续状态
这个流转里有一个所有课设都容易忽略的点:接单动作不应该改变 task.status,而应该用 accepter_id 从 NULL 变成具体用户 ID 来表示“已被接”。因为如果把 status 改成“2 进行中”这种状态,那么“已完成”就变成 3、“已取消”变成 4,状态码语义混乱。我的习惯是:task.status 只表达业务终态,接单与否用 accepter_id 是否为空来判断。这样列表页的筛选 SQL 也简单:
-- 查询还没被接且状态进行中的任务 SELECT * FROM task WHERE status = 1 AND accepter_id IS NULL ORDER BY create_time DESC LIMIT 10;2.3 后端接口清单与事务边界:哪些操作必须原子
一套完整的悬赏接口,至少是下面这组,我按业务顺序列:
| 接口 | 方法 | 作用 | 关键逻辑 |
|---|---|---|---|
| /api/user/login | POST | 微信登录换 token | 用 code 换 openid,注册或登录 |
| /api/task/list | GET | 分页查询悬赏列表 | 按状态+时间排序,支持分页 |
| /api/task/detail | GET | 任务详情 | 带出发布者昵称头像,判断是否已接 |
| /api/task/publish | POST | 发布悬赏 | 校验余额、扣预授权或直接扣款 |
| /api/task/accept | POST | 接单 | 校验任务可接,更新 accepter_id |
| /api/task/complete | POST | 发布者确认完成 | 结算金额给接单者,改状态 |
| /api/task/cancel | POST | 取消任务 | 未接单可取消,退款 |
| /api/user/tasks | GET | 我发布/我接单的任务列表 | 按角色区分查询 |
其中有三个操作必须在一个事务里完成,缺一个都会出脏数据。
发布任务时,扣余额和插入任务记录要一起提交。常见做法是先在代码里查余额,足够再插入任务,再 UPDATE 用户余额。但在 Spring 里这三个操作要包在同一个 @Transactional 方法里,否则插入成功、扣款失败,任务挂出去了钱没扣。
确认完成时,更新任务状态为已完成、给接单者加余额、写钱包流水,也是同一个事务。这里最容易漏的是钱包流水,漏掉的后果是余额变了,但没有任何记录能解释这笔变动。
取消任务时同理:任务状态改为已取消、余额退回发布者、流水记录。逆向链路和正向链路要对等,这也是评审老师最爱问的“你的退款逻辑处理了吗”的答案。
3. 后端本地跑通:数据库导入、Spring Boot 配置与 Postman 联调
代码读懂了,就该把它跑起来。很多下载了这个资源包的人卡在第一步:MySQL 装好了、JDK 装好了,项目一启动直接报错,然后就开始怀疑人生。这一章我把从解压到接口联调的完整步骤过一遍,按这个顺序做,能避开九成的新手问题。
3.1 数据库初始化:导入 SQL 与 MySQL 8 字符集、时区处理
资源包里的 database 目录下通常有一个 .sql 文件(常见命名是 bounty_system.sql 或悬赏系统.sql)。导入之前,先把 MySQL 的服务状态确认好。mysql 安装配置教程里总说“默认字符集 utf8”,但 MySQL 8 的默认字符集其实是 utf8mb4,如果你在课程设计文档里写“UTF-8”,到导入 emoji 表情昵称时会直接报错或存成乱码。
导入命令:
mysql -u root -p < bounty_system.sql或者进到 MySQL 控制台后:
CREATE DATABASE IF NOT EXISTS bounty_system DEFAULT CHARSET utf8mb4; USE bounty_system; SOURCE /path/to/bounty_system.sql;导入完成后,验证一下核心表是不是都建出来了:
SHOW TABLES; SELECT COUNT(*) FROM user;如果 user 表有一两条测试数据且不报错,说明导入没问题。这里有个血泪经验:如果 SQL 文件里写了 CREATE DATABASE,那你用 mysql -u root -p < file.sql 导入即可;如果没写,就必须先手动建库再 SOURCE。很多新手在这一步把 SQL 文件直接拖进 Navicat 跑,结果建表语句全报“No database selected”,这不是 SQL 文件坏了,是没指定库。
3.2 application.yml 关键配置:数据源、MyBatis-Plus 与上传路径
Spring Boot 项目的配置文件在 src/main/resources/application.yml(或 .properties)。我见过太多人卡在数据库连接失败,九成是配置里的三个参数和本地环境不一致。下面是我调整过的最小可用配置:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/bounty_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl file: upload-dir: /Users/你的用户名/bounty-upload/逐项说明三个关键点。
url 里的 serverTimezone=Asia/Shanghai 必须加。MySQL 8 默认时区是 UTC,不加这个参数,Java 侧的 LocalDateTime 写入数据库会差 8 小时,任务列表的时间显示全乱。这是新手查半天查不出来的问题,现象是“我明明插的是下午三点,查出来是早上七点”。
password 改成你自己的 MySQL root 密码。资源包自带的教程里经常写 123456,但你本机的 root 密码不一定是这个。注意 MySQL 8 默认用 caching_sha2_password 认证插件,如果你的 JDBC 驱动版本小于 8.0.11,会报“Public Key Retrieval is not allowed”。解决办法是换新版驱动,或者把 root 的认证插件改回 mysql_native_password,二选一,改驱动更干净。
file.upload-dir 是图片上传的保存目录,Windows 上写 D:/bounty-upload/,Mac/Linux 写一个你当前用户有写权限的绝对路径。不要写相对路径,Spring Boot 启动时的工作目录不一定是项目根目录,写相对路径会莫名找不到文件。
改完配置,在项目根目录执行:
mvn spring-boot:run看到类似 “Started BountyApplication in 5.2 seconds” 的日志,后端就算起来了。如果手头没有 Maven,也可以导入 IDEA 后点右上角的运行按钮,效果一样。
3.3 用 Postman 过一遍登录、发布、接单的完整链路
后端起来了,下一步不要急着打开微信开发者工具,先用 Postman 把接口调通。原因很简单:小程序端的报错信息会被吞掉大半,而 Postman 能直接看到响应体,定位问题快得多。
先测登录接口。payload 里的 code 在小程序里是 wx.login 拿到的一次性凭证,在 Postman 里可以先随便填一个字符串,但要注意:如果你用的是微信小程序的正式 appid,这个 code 会被微信服务器校验并拒绝。常见的做法是后端代码里留一个测试开关:
wechat: mock-openid: true开启后,登录接口不真正调微信接口,直接用 code 作为 openid 拼出一个测试用户。这在小程序真机调试前非常有用,否则你每次联调都要真机跑一遍小程序去拿真实 code。给资源包做二次开发时,我一般第一步就是加这个 mock 开关,省掉大量来回折腾。
登录接口的请求和响应大概是:
POST http://localhost:8080/api/user/login Content-Type: application/json { "code": "test-code-001" }响应里会有一个 token 字段,后续所有接口都在请求头里带这个 token:Authorization: token值。
再测发布悬赏。这里注意请求体里的 reward 必须能通过后端校验:
POST http://localhost:8080/api/task/publish Authorization: token值 Content-Type: application/json { "title": "帮我带一份食堂二楼套餐", "description": "送到三号教学楼401,到了放桌上就行", "reward": 8.50, "deadline": "2025-12-31 12:00:00" }如果返回的任务 id 自增正常、任务列表能查到这条记录,说明数据链路通了。最后测接单接口——用另一个 token(另一个用户)去接这个任务,然后回查任务详情,accepter_id 从 NULL 变成了用户 id,整套链路就走通了。
Postman 调联这个环节,还有一个 Windows 上很容易踩的坑:localhost 在 IPv6 环境下会解析成 ::1,MySQL 和 Tomcat 如果只绑了 IPv4,会出现“奇怪地连不上”的现象。遇到这种情况,把 localhost 换成 127.0.0.1 再试,能解决一半的玄学问题。
4. 小程序端对接:登录态、任务列表、发布页与个人中心
后端接口通了,这一章才是这个资源包真正值钱的部分——小程序端怎么把接口一个个接上。微信开发者工具导入项目后,先看根目录下的 app.js、utils/request.js 和 pages 目录结构,理解它的封装方式再动手。最忌讳的是上来就改页面,改到最后发现是请求封装里 baseURL 写错了。
4.1 登录态设计:wx.login 换 token 的完整闭环
小程序端的登录和传统 Web 登录不一样:没有用户名密码,核心是用 wx.login 拿 code,交给后端换 openid。资源包里通常已经封装好了一个 request.js,你要确认的是三件事:baseURL 对不对、token 有没有自动带上、401 有没有统一处理。
// utils/request.js const BASE_URL = 'http://localhost:8080' function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token') wx.request({ url: BASE_URL + path, method, data, header: { 'Content-Type': 'application/json', 'Authorization': token ? token : '' }, success: (res) => { if (res.statusCode === 401) { // token 失效,重新走登录 wx.removeStorageSync('token') login().then(() => { resolve(request(path, method, data)) }) } else if (res.statusCode === 200) { resolve(res.data) } else { reject(res.data) } }, fail: reject }) }) } function login() { return new Promise((resolve, reject) => { wx.login({ success: (res) => { request('/api/user/login', 'POST', { code: res.code }) .then((resp) => { wx.setStorageSync('token', resp.data.token) wx.setStorageSync('userInfo', resp.data.userInfo) resolve(resp) }) .catch(reject) }, fail: reject }) }) } module.exports = { request, login }这段代码解决了小程序端最核心的两个问题:一是所有请求自动携带 token,不用每个页面重复写 header;二是 401 自动重新走 wx.login 登录,用户无感续期。你在二次开发时,只需要把 BASE_URL 换成本机局域网 IP(比如 http://192.168.1.100:8080),真机调试就能连上电脑上的后端。
注意:wx.request 的 url 不能带端口以外的额外路径前缀,BASE_URL 末尾不要加分号,也不要写成 http://localhost:8080/,斜杠会导致拼接出双斜杠,部分 Spring 拦截器会把它当成非法路径。
4.2 任务列表与详情:分页加载、下拉刷新和状态渲染
列表页是所有小程序页面里最值得认真读的,因为分页逻辑写对的人真的不多。常见的翻车写法是:一次性查出所有任务塞进数组,数据多了之后小程序直接卡死,或者触底加载时没有判断“是否还有下一页”,导致重复请求。
正确的分页加载模式是这样的:
// pages/index/index.js const { request } = require('../../utils/request') Page({ data: { tasks: [], page: 1, size: 10, hasMore: true, loading: false }, onLoad() { this.loadTasks(true) }, onPullDownRefresh() { this.loadTasks(true).then(() => wx.stopPullDownRefresh()) }, onReachBottom() { if (!this.data.hasMore || this.data.loading) return this.loadTasks(false) }, loadTasks(reset = false) { if (this.data.loading) return this.setData({ loading: true }) const page = reset ? 1 : this.data.page return request(`/api/task/list?page=${page}&size=${this.data.size}`, 'GET') .then((res) => { const list = reset ? res.data.records : this.data.tasks.concat(res.data.records) this.setData({ tasks: list, page: page + 1, hasMore: page < res.data.pages, loading: false }) }) .catch(() => this.setData({ loading: false })) } })这段代码里有三个细节值得解释。第一,onReachBottom 里同时判断 hasMore 和 loading,这是防止重复请求的关键,触底事件可能在一瞬间触发多次。第二,loadTasks 函数接收 reset 参数,下拉刷新和首次加载都传 true,触底加载传 false,上一页的数组用 concat 拼接而不是覆盖。第三,后端返回的 pages 字段(MyBatis-Plus 分页插件自带)用来判断还有没有下一页,而不是判断 records 长度是否小于 size。
状态渲染方面,任务列表的每一项至少要有三种视觉状态:进行中(可接单)、已被接(按钮置灰)、已完成(显示完成标记)。小程序模板里不建议在模板写复杂逻辑,提前在 JS 里把状态转成文字和样式类:
const statusMap = { 1: { text: '进行中', class: 'status-active' }, 2: { text: '已完成', class: 'status-done' }, 3: { text: '已取消', class: 'status-cancel' } } // 在 setData 之前,把每一条任务映射上状态文案 const decorated = record => ({ ...record, statusText: statusMap[record.status]?.text || '未知', statusClass: statusMap[record.status]?.class || '' })4.3 发布悬赏页:表单校验与 wx.uploadFile 图片上传
发布页是这个系统的交互核心,也最容易出 bug。常见的问题是表单校验形同虚设、图片传不上去、传上去后端存不到。先看发布的核心逻辑:
// pages/publish/publish.js const { request } = require('../../utils/request') Page({ data: { title: '', description: '', reward: '', deadline: '', images: [] }, onTitleInput(e) { this.setData({ title: e.detail.value }) }, // 先把图片传到后端,拿到 url 再随表单提交 publishTask() { const { title, description, reward, deadline, images } = this.data if (!title.trim()) { wx.showToast({ title: '标题不能为空', icon: 'none' }) return } if (!reward || parseFloat(reward) <= 0) { wx.showToast({ title: '悬赏金额必须大于0', icon: 'none' }) return } const uploadTasks = images.map((path, index) => { return wx.uploadFile({ url: 'http://localhost:8080/api/file/upload', filePath: path, name: 'file', formData: { index: String(index) }, header: { Authorization: wx.getStorageSync('token') } }) }) Promise.all(uploadTasks.map(p => p.then(res => JSON.parse(res.data).data.url))) .then((urls) => { return request('/api/task/publish', 'POST', { title: title.trim(), description: description.trim(), reward: parseFloat(reward), deadline: deadline, imageUrls: urls }) }) .then(() => { wx.showToast({ title: '发布成功', icon: 'success' }) wx.redirectTo({ url: '/pages/index/index' }) }) } })这段代码有三个点特别容易踩坑,都在参数层面。
第一个是 wx.uploadFile 的 name 参数。这个 name 必须和后端 Controller 的 multipart 参数名一致。Spring Boot 里最常见的写法是@RequestParam("file") MultipartFile file,所以 name 填 'file' 才是对的;资源包如果用了别的名字(比如 uploadFile),你就要把 name 改成对应的名字,否则报“Required part 'file' is not present”。
第二个是 wx.uploadFile 成功回调里 res.data 是字符串,必须 JSON.parse 之后才能取 data.url。很多人直接当对象用,结果取出来是 undefined,发布于是一堆 undefined 塞进数据库。有同学把这个问题归咎于“后端返回格式不对”,其实是没解析。
第三个是发布悬赏请求里,reward 必须用 parseFloat 转成数字,不能直接把字符串传给后端。Java 后端用 @RequestBody 接收 JSON 时,如果 reward 是字符串 "8.5" 而 DTO 里是 BigDecimal 类型,反序列化会直接报 400。同理 deadline 最好转换成 "yyyy-MM-dd HH:mm:ss" 格式的字符串,避免 LocalDateTime 反序列化因为格式不匹配而失败。
个人中心页的代码逻辑相对简单,需要展示两个列表:我发布的和我接单的。这两个列表其实就是同一个接口/api/user/tasks按 different role 参数区分。注意一点:任务完成后,发布者的余额变动记录要能追溯,个人中心里最好加一个“余额明细”列表,数据来源是 wallet_log 表。做这个列表不难,但它直接回应了答辩时“钱从哪来到哪去”的问题。
5. 避坑:这套系统最常见的 5 个翻车点与排查方法
这一章是整套踩坑经验的浓缩。写下来不是因为资源包质量差,而是这类课设级项目涉及 MySQL、Spring Boot、小程序三端,任何一个环境的差异都会把新手干趴。下面五条是我每次带人跑这种项目必查的清单,按出现频率从高到低排。
5.1 数据库连接失败:认证插件与 serverTimezone
现象:Spring Boot 启动时报Access denied for user 'root'@'localhost'或Public Key Retrieval is not allowed。
原因分两种。前者是密码和 application.yml 里不一致,后者是 MySQL 8 默认的 caching_sha2_password 认证插件和旧版 JDBC 驱动不兼容。很多课程设计配套教程是用 MySQL 5.7 写的,你本机装的是 MySQL 8,就会踩这个。
解决:先在配置里确认密码,再检查 pom.xml 里的 mysql-connector-java 版本,如果低于 8.0.11,升级到 8.x。升级后如果还报 Public Key Retrieval,在 JDBC url 后面加allowPublicKeyRetrieval=true&useSSL=false。这两个参数的组合能解决绝大多数“MySQL 8 连不上”问题。
5.2 小程序请求直接失败:域名校验与 http 明文限制
现象:小程序开发者工具里点按钮,network 面板一直在转圈,最终报request:fail或ERR_CERT_COMMON_NAME_INVALID。
原因:微信小程序默认要求请求地址必须是 HTTPS 且在小程序后台配置了合法域名。你的后端跑在本地,地址是http://localhost:8080,既不是 HTTPS 也没配过域名,自然被拦。
解决:开发阶段在微信开发者工具右上角“详情 → 本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。真机调试时,除了勾选这个选项,还要把 BASE_URL 从 localhost 改成电脑的局域网 IP。注意真机上微信不会放开明文 http 限制(Android 上部分测试版可以,正式版不行),所以完整的本地调试路径是:开发者工具用 localhost,真机预览用局域网 IP。
5.3 图片上传 404:静态资源映射与 multipart 参数名
现象:wx.uploadFile 返回 404,或者上传成功但图片在浏览器里打不开(404)。
原因:两个层面,404 不能一概而论。如果 uploadFile 请求本身返回 404,多半是后端接口路径不对(比如后端是 /api/upload,前端写成 /api/file/upload);如果上传接口返回 200 但访问图片 URL 时 404,那是后端没有把 file.upload-dir 映射成静态资源路径。
解决:先开 Postman 直接测上传接口,排除路径问题。图片访问 404 时,在后端加一个静态资源映射:
@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${file.upload-dir}") private String uploadDir; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/**") .addResourceHandler("file:" + uploadDir); } }加完重启后端,上传返回的 url 如果是http://localhost:8080/uploads/xxx.jpg,就能正常访问了。
5.4 并发接单超发:状态更新丢行数的乐观锁思路
现象:同一个任务,两个用户几乎同时点“接单”,结果两个人都显示接单成功。用 Navicat 看 task 表,accepter_id 只有一个,但 accept_record 里多了两条记录。
原因:接单逻辑写成了“先 SELECT 查状态,再 UPDATE 改状态”,两个请求都查到 status=1 和 accepter_id IS NULL,然后都执行 UPDATE。没有数据库约束兜底的话,两次 UPDATE 都成功,任务就被接了两次。
解决:把接单的 UPDATE 改成带条件的状态更新,用受影响行数判断是否成功:
int rows = taskMapper.update(null, new LambdaUpdateWrapper<Task>() .set(Task::getAccepterId, userId) .eq(Task::getId, taskId) .eq(Task::getStatus, 1) .isNull(Task::getAccepterId)); if (rows == 0) { throw new BusinessException("手慢了,任务已被接走"); }配合 accept_record 表的 uk_task_user 联合唯一索引,双保险,并发问题基本堵死。这就是乐观锁思路在课设项目里的落地:不是真的用 version 字段,而是用“更新条件带上旧状态”来保证原子性。
5.5 token 失效后页面卡死:统一 401 处理
现象:小程序开着过夜,第二天点列表页一直转圈,控制台报 401,页面就是没反应。
原因:token 过期后,wx.request 返回 401,但页面代码没有处理 401 的逻辑,Promise 被 reject,loading 状态没人关掉,页面看起来就“卡死了”。
解决:统一在 request.js 的 401 分支里重新登录,登录成功后再重放一次原始请求(上一章代码里已经写了这个模式)。关键点是重新登录后要用 resolve 返回重试结果,而不是简单地 reject。另外,每次 App 启动时主动检查 token 是否存在,也可以在 app.js 的 onLaunch 里兜底调一次 login。这个处理做得好,演示时不会出现“临时调后端改 token 失效时间”的尴尬。
6. 验证这套系统值不值得交付:一条完整的验收路径
如果你不是为了答辩而是想真正用起来,或者想评估这个资源包的质量,这一章给你一条我在交付前必走的验收路径。按这个顺序过一遍,能跑通且没有明显异常,这系统就能立住。
6.1 按业务主流程走一遍功能验收
用两个微信开发者工具窗口(两个不同账号)模拟发布者和接单者,依次走完:发布者发布悬赏 → 接单者在列表看到任务 → 接单者点接单 → 发布者确认完成 → 接单者余额增加。每一步都回到数据库里核对一次:task.status 的变更、accepter_id 的写入、user.balance 的变动、wallet_log 的新增记录。四个表的数据变动对得上,核心闭环就验收通过了。
异常路径至少补三个用例:重复接单(第二个接单的人必须收到“已被接”提示)、余额不足发布(提示明确且不写入任务表)、取消任务后退款到账(发布者 balance 加回来)。这三个用例覆盖了 90% 的评审提问。
6.2 往后值得扩展的三个方向
第一是微信支付接入。现在系统的余额是模拟的,要真做商用,把发布悬赏时的扣余额替换成微信支付下单,确认完成时再走企业付款到零钱。这个过程不复杂,但需要一个小程序商户号。
第二是订阅消息通知。任务被接、任务完成,给发布者发一条微信订阅消息。后端只需要在状态变更时调一次subscribeMessage.send接口,体验会有质的提升。
第三是管理后台。用 Vue 或纯 HTML 加一个后台页面,查任务、封禁用户、调整悬赏金额。这个方向对 Java 课设来说属于加分项,但也是面试能拿出来讲的亮点。选题的时候不用贪多,把主流程的真实感做好,比堆页面更值得。
最后说说我的习惯:每次拿到这种课设资源包,我不急着看代码,先花一小时把 SQL 文件读一遍,看表结构能不能支撑业务闭环;再花半小时跑通接口,最后才打开小程序端。三步走完,这包值不值得改、要改哪里,心里就有数了。踩过一次“拿到包就改代码,改到第三天发现表结构少一张”的坑之后,这个顺序我再也没变过。希望帮到你。
本文还有配套的精品资源,点击获取