简介:基于微信小程序的社区垃圾回收管理系统毕业设计源码案例,面向计算机专业正在做毕设的学生及需要项目实战练习的学习者,后端采用SSM(Spring、SpringMVC、MyBatis)框架,围绕社区垃圾回收场景实现用户管理、回收请求提交、垃圾分类指导、回收任务分配、进度跟踪和数据统计等功能,并支持通过微信小程序便捷完成回收操作。压缩包共916个文件、约58.9MB,主要包含Java后端源码、Vue管理端页面、JavaScript逻辑脚本、微信小程序WXML/WXSS/JSON页面文件、SQL数据库脚本、PNG/JPG界面截图以及MP4演示视频等,类型覆盖从设计到部署的完整环节。目前已有53人学习下载。除源码与数据库外,还配套演示视频、使用说明和环境安装说明,便于快速运行和部署;阅读源码可学习SSM框架下业务系统模块划分、接口设计与数据流转,以及小程序端与后端服务之间的交互方式,适合作为毕业设计、课程设计、期末大作业的参考,也可在此基础上进行二次开发。
1. 从“收垃圾”到“管垃圾”:这套系统到底在做什么
社区垃圾分类回收这件事,真正动手做过的人都知道,难的不是贴几张宣传标语,而是“谁上门收、收了多少、怎么记账、怎么让居民愿意持续参与”。基于微信小程序的社区垃圾回收管理系统,本质上是把线下回收流程搬到线上:居民在小程序里提交回收请求、选择垃圾类别和上门时间,回收员接单上门,称重计价后系统自动给居民积分,积分可以兑换小礼品或抵扣物业费。这套模式跑通后,社区不用再靠人工登记本和微信群里吼来吼去,后台还能看到每天的回收量、各类垃圾占比、哪个楼栋参与度最高。
我拆过不少类似的毕业设计源码案例,这类项目最容易被低估的不是页面数量,而是小程序与 SSM 后端的登录态打通、订单状态流转、积分事务一致性这几块。新手往往把前端页面做得很漂亮,一联调就翻车。这篇文章从技术选型、数据库设计、核心接口实现到本地复现的坑,按实际开发顺序讲清楚,适合正在做毕设、课设,或者想给社区/物业快速搭一套轻量回收管理系统的开发者。
2. 技术选型:为什么是 SSM + 微信小程序
2.1 后端扎在 SSM 上:不是守旧,是课程体系与课题要求
SSM 指的是 Spring、SpringMVC、MyBatis 三件套。Spring 管对象和事务,SpringMVC 管 HTTP 请求的路由分发,MyBatis 管 SQL 与 Java 对象之间的映射。很多高校的软件工程、计算机科学与技术专业,课程设计大纲还明确写着“基于 SSM 框架”或“基于 Spring + SpringMVC + MyBatis”,所以这套技术栈在毕业设计里不仅没过时,反而是最容易匹配任务书要求的组合。
如果你在纠结“为什么不直接用 Spring Boot”,我的看法是:Spring Boot 确实省去大量 XML 配置,但 SSM 的手动配置过程本身就是课程考核点——Spring 容器怎么加载、DispatcherServlet 怎么注册、MyBatis 的 mapper 怎么扫描,这些在 Spring Boot 里全被自动配置藏起来了。对于需要写“系统实现”章节、需要讲清楚每个配置文件作用的学生来说,SSM 反而更好写。真到工作场景,SSM 的理解也能平滑迁移到 Spring Boot,底层的 IoC 和 AOP 思想完全一致。
项目环境这块,我给一个踩过坑后固定下来的版本组合:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 不要上 11/17,部分老依赖和 Tomcat 版本兼容性会出问题 |
| Tomcat | 8.5 | 与 JDK8 配合最稳,部署 war 包方便 |
| MySQL | 5.7 | 8.0 也可以,但连接驱动和时区配置有差异 |
| Maven | 3.6.x | 管理依赖,镜像用阿里云 |
| 微信开发者工具 | 稳定版即可 | 调试小程序端 |
2.2 小程序端为什么用原生:功能复杂度决定框架选择
微信小程序端的实现,常见方案有原生 WXML/WXSS/JavaScript、uni-app、Taro 等。这类社区回收管理系统页面量不大,核心就四五个页面:首页(提交回收)、订单列表、订单详情、个人中心(积分与地址管理),用原生小程序语法完全够,而且不需要额外引入编译链,遇到问题在微信开发者工具里直接断点调试,对新手更友好。
系统的角色权限闭环是这样的:普通居民通过小程序提交回收请求,回收员在小程序或后台管理系统里接单,上门称重后标记完成,系统按垃圾类别和重量计算积分并写入居民账户,管理员在 Web 后台看统计报表和用户管理。整个闭环里,订单状态是最核心的主线数据,后面所有接口设计都围绕状态机展开。
2.3 业务流程先画清楚,代码才不会写乱
我把这个项目的主流程归纳为一条单线加两条支线。单线是:居民提交回收单 -> 回收员接单 -> 上门回收 -> 称重计价 -> 积分入账 -> 订单完成。两条支线分别是:居民端地址管理(每次下单可以选择默认地址)、后台端的公告发布与回收员管理。
这条主流程决定了数据库表结构、接口数量和事务边界。比如“提交回收单”和“积分入账”不在同一次操作里,而是在回收员点击“确认完成”时,后端需要在一个事务里同时更新订单状态、写入积分流水、累加用户总积分。如果你在编码前没理清这条线,很容易写着写着就把积分更新的逻辑散落在多个 Controller 里,后面排错非常痛苦。
3. 数据库设计:先让业务有地方落脚
3.1 核心表拆分:不过度设计,但这两张表不能省
这类系统的数据库设计,我建议控制在七八张表以内,多了显得臃肿,少了盖不住完整流程。最少要有的核心表是:用户表、回收订单表、垃圾类别表、积分明细表、地址表、公告表。回收员和管理员可以复用用户表,通过 role 字段区分,不值得单独建表。
订单表是最需要仔细设计的,因为它承载了全流程的状态流转。我的建议是 state 字段用 tinyint 存数字状态码:0 表示待接单,1 表示已接单,2 表示已上门,3 表示已完成,4 表示已取消。用数字而不是字符串的好处是,前端枚举映射简单,后端判断状态机流转时写起来也干净。另外两个容易忽略的字段是 delete_flag(逻辑删除)和 create_time / update_time(时间戳),前者避免误删数据,后者是后台做回收量趋势分析的基础。
积分明细表要记录每次积分变动,字段包含用户 id、变动类型(1 增加 / 0 扣减)、变动分值、关联订单 id、备注。之所以强调关联订单 id,是因为后期对账时经常要回答“这 50 积分是哪次回收产生的”,没有订单关联就只能一笔笔查流水,非常被动。
3.2 建库建表脚本:直接能跑的初始化版本
以下建表脚本是我基于常见实现整理的初始版本,包含了核心的用户表、回收订单表和积分明细表。垃圾类别表和地址表比较简单,可以在脚本基础上自行补上。
CREATE DATABASE IF NOT EXISTS community_recycle DEFAULT CHARACTER SET utf8mb4; USE community_recycle; -- 用户表:居民、回收员、管理员统一存放,用 role 区分 CREATE TABLE `t_user` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', `openid` VARCHAR(64) DEFAULT NULL COMMENT '微信小程序openid,登录凭证', `username` VARCHAR(50) NOT NULL COMMENT '用户名/登录账号', `password` VARCHAR(100) DEFAULT NULL COMMENT '密码,仅后台管理员使用', `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称', `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL', `role` TINYINT NOT NULL DEFAULT 0 COMMENT '角色:0居民 1回收员 2管理员', `total_points` INT NOT NULL DEFAULT 0 COMMENT '累计积分', `delete_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0正常 1删除', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 回收订单表:整个系统的状态机核心 CREATE TABLE `t_recycle_order` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '订单ID', `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号,如 RC20250101001', `user_id` INT NOT NULL COMMENT '提交订单的居民ID', `worker_id` INT DEFAULT NULL COMMENT '接单回收员ID,未接单时为NULL', `category_id` INT NOT NULL COMMENT '垃圾类别ID', `address_detail` VARCHAR(255) NOT NULL COMMENT '上门回收地址', `appointment_time` VARCHAR(50) DEFAULT NULL COMMENT '预约上门时间段', `weight` DECIMAL(10,2) DEFAULT NULL COMMENT '实际回收重量(kg),称重后填写', `points` INT DEFAULT NULL COMMENT '本次获得积分,完成时计算', `state` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待接单 1已接单 2已上门 3已完成 4已取消', `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注', `delete_flag` TINYINT NOT NULL DEFAULT 0, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_state` (`state`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='回收订单表'; -- 积分明细表:每次积分变动留痕 CREATE TABLE `t_points_detail` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` INT NOT NULL COMMENT '用户ID', `order_id` INT DEFAULT NULL COMMENT '关联订单ID,可空', `change_type` TINYINT NOT NULL COMMENT '变动类型:1增加 0扣减', `change_points` INT NOT NULL COMMENT '变动积分值', `balance` INT NOT NULL COMMENT '变动后余额', `remark` VARCHAR(255) DEFAULT NULL COMMENT '变动原因', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分明细表';注意几个参数设计点。openid 字段我留了 64 位长度,实际微信返回的 openid 一般在 28 位左右,但留足余量避免以后小程序绑定其他平台时不够用。order_no 用业务编号而不是自增主键直接展示给用户,是为了避免订单号可被遍历猜测,也算是一点安全习惯。DECIMAL(10,2) 用来存重量,不要用 FLOAT,否则累加统计时会出现 0.1 + 0.2 != 0.3 这类精度玄学问题。delete_flag 每个核心表都带上,社区运营中误操作删除订单是常事,逻辑删除可以留一条后悔药。
4. 后端与小程序端实现:核心流程怎么跑通
4.1 后端分层与登录态:小程序登录不是传用户名密码
SSM 后端的包结构我习惯按功能分层而不是按业务模块分层,因为这个项目规模不大,分层过高反而绕。controller 放接口入口,service 放业务逻辑,mapper 放 MyBatis 的接口和 XML,entity 放数据库映射实体,common 放工具类和通用返回体。返回体统一用{ code, msg, data }结构,前端判断 code 是否为 200 决定业务是否成功,比裸返回数据要规范。
小程序登录这块,很多新手会踩一个认知上的坑:小程序里没有用户名密码,微信提供的登录方案是wx.login()获取临时 code,后端拿 code 去微信接口换 openid,再用 openid 去数据库匹配用户。首次登录时自动用 openid 创建一条用户记录,之后每次请求通过 token 识别身份。我在 common 包下写了一个TokenUtil,基于 UUID 生成 token,存入内存 Map 或 Redis,请求头里带token字段,拦截器里做校验。
下面是登录接口的核心代码,在 SSM 下的 controller 和 service 实现:
@Controller @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; /** * 小程序登录:接收 wx.login 返回的 code,换取 openid * 首次登录自动注册,返回自定义 token */ @RequestMapping(value = "/login", method = RequestMethod.POST) @ResponseBody public Result login(@RequestBody JSONObject param) { String code = param.getString("code"); if (StringUtils.isEmpty(code)) { return Result.error("code不能为空"); } return userService.login(code); } }@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public Result login(String code) { // 1. 调用微信接口 code2Session 换取 openid String appid = "你的小程序appid"; String secret = "你的小程序secret"; 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 = JSONObject.parseObject(result); String openid = json.getString("openid"); if (StringUtils.isEmpty(openid)) { return Result.error("openid获取失败: " + json.toJSONString()); } // 2. 根据 openid 查用户,不存在则自动注册 User user = userMapper.selectByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setUsername("wx_" + openid.substring(0, 8)); user.setNickname("微信用户"); user.setRole(0); // 默认为居民 userMapper.insert(user); } // 3. 生成 token 并返回 String token = TokenUtil.generateToken(user.getId()); JSONObject data = new JSONObject(); data.put("token", token); data.put("userId", user.getId()); data.put("nickname", user.getNickname()); data.put("role", user.getRole()); return Result.success(data); } }这段代码里有两个关键参数。code 是wx.login()接口返回的临时登录凭证,有效期只有 5 分钟,且只能用一次,所以前端必须在拿到 code 后立刻请求后端接口,不能存起来过一会儿再用。appid 和 secret 在小程序后台的「开发管理 -> 开发设置」里查看,secret 一旦泄露,别人就可以伪造你的小程序身份,务必放在后端配置里,不要写在小程序前端代码里,否则等于把钥匙挂在门上。
token 生成我这里用的是按用户 id 加时间戳加随机数做 MD5,简单够用。如果想要更严谨,可以引入 JWT,但该项目没有重启后 session 失效的强要求,内存 Map 方案足够。要注意的是,如果做了多实例部署,Map 方案会有登录态丢失问题,毕设单实例部署可以忽略。
4.2 订单状态机:积分入账必须和完成动作在同一事务里
订单创建相对简单,居民提交表单后插入一条 state=0 的记录。真正考验代码功底的是回收员点击“确认完成”这个动作。这个动作涉及三步:更新订单状态为已完成、给居民加积分、写入积分流水。这三步必须在一个事务里完成,否则会出现订单已完成但积分没到账,或者流水重复记录的情况。
我在 Service 层实现这个逻辑,用 Spring 的@Transactional注解保证原子性。关键代码如下:
@Service public class RecycleOrderServiceImpl implements RecycleOrderService { @Autowired private RecycleOrderMapper orderMapper; @Autowired private UserMapper userMapper; @Autowired private PointsDetailMapper pointsDetailMapper; /** * 回收员确认完成订单:更新状态 + 发放积分 + 写流水 * 三个操作在同一事务中,任何一步失败全部回滚 */ @Override @Transactional(rollbackFor = Exception.class) public Result completeOrder(Integer orderId, BigDecimal weight) { // 1. 校验订单存在且状态为“已接单(1)”或“已上门(2)” RecycleOrder order = orderMapper.selectById(orderId); if (order == null) { return Result.error("订单不存在"); } if (order.getState() != 1 && order.getState() != 2) { return Result.error("当前订单状态不可完成"); } // 2. 计算积分:可按重量*系数,也可按垃圾类别固定积分 int points = weight.multiply(new BigDecimal(10)).intValue(); if (points <= 0) { points = 10; // 保底积分 } // 3. 更新订单:状态改为已完成(3),填入重量和积分 order.setState(3); order.setWeight(weight); order.setPoints(points); orderMapper.updateById(order); // 4. 累加用户总积分 User user = userMapper.selectById(order.getUserId()); user.setTotalPoints(user.getTotalPoints() + points); userMapper.updateById(user); // 5. 写入积分流水 PointsDetail detail = new PointsDetail(); detail.setUserId(user.getId()); detail.setOrderId(order.getId()); detail.setChangeType(1); detail.setChangePoints(points); detail.setBalance(user.getTotalPoints()); detail.setRemark("回收订单完成,重量:" + weight + "kg"); pointsDetailMapper.insert(detail); return Result.success(points); } }这里我设置了rollbackFor = Exception.class,这是个容易被忽略的参数。Spring 的@Transactional默认只在 RuntimeException 时回滚,如果你在业务代码里手动 catch 了异常然后抛出一个普通的 Exception,事务是不会回滚的。在毕设代码里,最常见的翻车就是数据库写入成功了,但后续积分计算抛了一个空指针或类型转换异常,结果订单状态改了、积分没加,数据不一致。
状态码的流转校验也很重要。我在代码里限制了只有“已接单(1)”和“已上门(2)”状态的订单才能执行完成操作,这能防止回收员手滑把“待接单(0)”的订单直接点成完成。更完整的做法是把状态机用枚举或常量类定义出来,每个状态变更都校验当前状态是否合法,否则后期加需求时到处是 if-else,改一处漏三处。
4.3 小程序端请求封装:token 带上,错误统一提示
小程序端的核心不是页面样式,而是请求层的封装。如果每个页面都单独写wx.request,代码会非常冗余且难以维护。我在小程序根目录下建了utils/request.js,统一封装请求方法,自动附带 token,统一处理 401 跳转登录。
// utils/request.js const BASE_URL = 'http://localhost:8080'; // 后端接口地址 function request(url, method, data, header) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method || 'GET', data: data || {}, header: Object.assign({ 'Content-Type': 'application/json', 'token': wx.getStorageSync('token') || '' }, header), success(res) { // 后端统一返回 {code, msg, data},code=200 为业务成功 if (res.data && res.data.code === 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(res.data); } }, fail(err) { wx.showToast({ title: '网络异常,请检查后端服务', icon: 'none' }); reject(err); } }); }); } module.exports = { request };这里的关键参数和习惯做几点说明。BASE_URL 在开发阶段填http://localhost:8080,但微信开发者工具里请求本机地址需要勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,否则会被拦截。上线时这里要换成配置了 HTTPS 的正式域名,且该域名必须在小程序后台的 request 合法域名列表里。token 从 storage 里读取,每次请求自动带上,后端拦截器从请求头里取出 token 校验用户身份。
然后是页面调用,以提交回收订单为例:
// pages/recycle/submit.js const { request } = require('../../utils/request'); Page({ data: { categoryList: [], selectedCategory: '', appointmentTime: '上午 9:00-12:00', addressDetail: '', remark: '' }, onLoad() { this.loadCategories(); }, // 加载垃圾类别,下拉选择 async loadCategories() { try { const list = await request('/api/category/list', 'GET'); this.setData({ categoryList: list }); } catch (e) {} }, // 提交回收订单 async onSubmit() { if (!this.data.selectedCategory) { wx.showToast({ title: '请选择垃圾类别', icon: 'none' }); return; } if (!this.data.addressDetail) { wx.showToast({ title: '请填写上门地址', icon: 'none' }); return; } try { await request('/api/order/create', 'POST', { categoryId: this.data.selectedCategory, addressDetail: this.data.addressDetail, appointmentTime: this.data.appointmentTime, remark: this.data.remark }); wx.showToast({ title: '提交成功', icon: 'success' }); setTimeout(() => { wx.navigateTo({ url: '/pages/order/list' }); }, 1000); } catch (e) {} } });提交订单的关键是前端传入 categoryId 而不是类别名称字符串。因为后端在计算积分、统计分类回收量时,需要的是稳定的类别主键,如果前端传中文名称,一旦名称修改、或者前后端编码不一致,数据就对不上了。地址和时间段这类信息属于展示性数据,前端校验必填后传字符串即可,后端不要二次解析。
5. 本地复现避坑:5 个高频问题与排查思路
5.1 微信开发者工具请求直接报 404 或无法连接
现象:小程序页面能打开,但点击登录或加载列表时,控制台一片红,提示request:fail或GET http://localhost:8080/xxx 404 (Not Found)。
原因:多数情况是开发工具默认开启了域名校验,localhost不在合法域名列表里。还有一部分原因是后端 Tomcat 的 context-path 和请求路径拼接错了,比如后端部署在/recycle路径下,但前端 BASE_URL 还是http://localhost:8080,导致接口全路径变成/api/user/login而不是/recycle/api/user/login。
解决:微信开发者工具右上角 ->「详情」->「本地设置」-> 勾选「不校验合法域名...」。后端如果是 Maven 多模块项目,确认 Tomcat 部署的 context path 是什么,BASE_URL 里补齐,例如http://localhost:8080/recycle。改完记得在开发者工具里清缓存重启,有时改了配置不重启不生效。
5.2 数据库时间字段差 8 小时
现象:插入订单后,MySQL 里的 create_time 比本地时间少了 8 小时。
原因:JDBC 连接串没有指定时区,MySQL 5.7 默认时区是系统时区,而驱动和系统时区对美国时间做了换算,导致时间偏移。这是这类管理系统的经典玄学问题,几乎每个做毕设的都会踩一次。
解决:在 JDBC 连接串末尾加上serverTimezone=Asia/Shanghai&useSSL=false。完整示例:
jdbc.url=jdbc:mysql://localhost:3306/community_recycle?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false如果改完还是差时间,检查一下服务器系统时区:Linux 执行date -R看时区是不是 +0800,不是的话用timedatectl set-timezone Asia/Shanghai修改。改完重启 Tomcat 和 MySQL 才会彻底生效。
5.3 小程序登录 code 换 openid 失败
现象:后端日志显示调用微信接口返回errcode: 40029或40163。
原因:40029是 code 无效,一般是 appid 和 secret 不匹配,或者小程序不是同一主体。40163是 code 已被使用过,说明前端在同一个登录流程里调了两次wx.login()或者后端接收了重复请求。
解决:先核对小程序后台的 appid 和 secret 是否复制完整,注意不要混淆测试号和正式小程序的 appid。前端代码里确保wx.login()只在需要登录时调用一次,拿到 code 后立即传给后端,中间不要有异步等待。如果后端临时调试需要手动模拟登录,可以在代码里写一个测试分支,绕过微信接口直接创建测试用户,但上线前务必删掉。
5.4 上传的图片回显失败,路径打不开
现象:居民头像或垃圾图片上传成功后,数据库中存的是/upload/xxx.jpg,但小程序里image标签加载时提示图片不存在。
原因:Tomcat 默认只映射部署目录下的静态资源,上传到自定义磁盘目录(比如D:/upload或/var/upload)的图片不在 Web 应用的访问路径内,所以直接通过/upload/xxx.jpg访问不到。
解决:在 SpringMVC 配置里增加资源映射,把文件系统的上传目录映射为 URL 路径。在 spring-mvc.xml 中配置:
<mvc:resources mapping="/upload/**" location="file:D:/upload/"/>这样访问http://localhost:8080/upload/xxx.jpg时,Tomcat 会去D:/upload/目录下找文件。另一个注意点是 Windows 和 Linux 的路径写法不同,Linux 下写成file:/var/upload/。如果用了 Tomcat 的虚拟目录也可以,但配置资源映射更适合开发调试。
5.5 MyBatis 传入多个参数报错 BindingException
现象:Mapper 接口方法定义了两个以上参数,运行时报Parameter 'xxx' not found. Available parameters are [arg0, arg1, param1, param2]。
原因:MyBatis 在多个参数时不会自动把参数名绑定到 SQL 里的#{userId},需要显式加@Param注解,否则只能用arg0, arg1这种下标访问,非常容易写错。
解决:在 Mapper 接口方法参数前加注解,例如:
List<RecycleOrder> selectOrdersByUserAndState( @Param("userId") Integer userId, @Param("state") Integer state);对应 XML 里写WHERE user_id = #{userId} AND state = #{state}。这个报错在单参数时不会出现,所以很多人写第一个查询没问题,扩展成多条件查询时突然翻车。建议从一开始就约定:所有 Mapper 方法参数超过一个,一律加@Param,不要依赖 MyBatis 的参数名推断。
6. 从能跑到能用:联调验证与下一步改进方向
项目在本地完整跑通后,我习惯先做一套最小验证,而不是直接开始补功能。验证的核心是走完一条居民 + 回收员的完整业务链路,确认订单状态和积分在每个节点的变化都符合预期。下面的表格是我针对这类订单系统整理的验证用例,照着过一遍能覆盖绝大多数接口 bug:
| 操作步骤 | 操作角色 | 预期结果 |
|---|---|---|
| 微信开发者工具打开小程序,点击登录 | 居民 | 数据库 user 表新增一条 openid 记录,返回 token |
| 选择垃圾类别,填写地址,提交回收单 | 居民 | 订单表新增记录,state=0,订单号生成正确 |
| 在订单列表刷新查看 | 居民 | 订单显示“待接单”状态 |
| 回收员登录后台,接单 | 回收员 | 订单 state 变为 1,更新 worker_id |
| 回收员点击“确认完成”,填写重量 | 回收员 | 订单 state 变为 3,weight 和 points 回填 |
| 查看居民积分 | 居民 | total_points 增加,积分明细表多一条记录 |
| 管理员后台查看回收统计 | 管理员 | 今日回收量、分类占比连续且汇总一致 |
验证过程中一旦发现数据对不上,优先查事务边界。比如积分没加,查completeOrder方法是不是抛了异常但被 Controller 层吞掉了;积分加了两次,查是不是前端重复点击“确认完成”,而后端状态机校验没拦住。
跑通之后,如果要让这个系统从“毕设作品”变成“能真正给社区用”,我建议优先补两块:一是定时统计任务,每天凌晨通过 Spring 的@Scheduled注解跑一个汇总,按垃圾类别统计昨日各楼栋回收量,数据落到一张统计表里,后台报表直接查表而不是实时聚合大表;二是小程序订阅消息,居民提交订单后订阅“回收完成通知”,回收员标记完成时后端调用微信订阅消息接口,居民微信里就能收到提醒,这比居民反复刷新订单列表体验好很多。
我自己的一个习惯是:所有接口写完后,先不用小程序测,而是用 Postman 把后端接口按业务流程顺序调一遍,确认每一步的参数和返回结构都正确。这样能快速定位是前端问题还是后端问题。小程序端的报错信息有时会被 wx.request 的 fail 回调吞掉,直接调后端能少走很多弯路。
最后讲一个我见过很多次的翻车现场:数据库、后端、小程序三端字符集不一致,导致中文乱码。MySQL 建库时指定utf8mb4,JDBC 连接串里带characterEncoding=utf8,后端web.xml里配置 Spring 的CharacterEncodingFilter,小程序端请求头里Content-Type带charset=utf-8,这四处有一个漏了,中文就会出现问号。遇到乱码不要到处改代码,先按这条链路排查,基本问题都出在漏了CharacterEncodingFilter。
这个方向做到“能用”并不难,核心就是把订单状态机和积分事务这两块咬死,剩下的页面和字段都是体力活。希望这篇拆解能帮你少踩几个坑,把精力花在真正能写进论文和答辩的项目亮点上。
本文还有配套的精品资源,点击获取