简介:这是一份基于微信小程序的旧衣回收系统毕业设计论文文档,面向计算机相关专业学生及需要完成类似课题的开发者。内容涵盖系统首页、用户管理、回收人员管理、旧衣信息管理、回收预约与派单、回收订单、积分商品与兑换等核心模块,完整展示了从需求分析、可行性分析到系统流程设计的全过程。文档为单个docx文件,压缩包大小3.64MB,便于下载后直接阅读与编辑。目前已有170人学习下载。资源基于Spring Boot框架结合微信开发者工具与MySQL数据库实现,详细介绍了Java技术、B/S架构等关键技术,并阐明了旧衣回收系统的研究背景、国内外现状及环保价值。通过这份论文,读者可获取完整的系统设计思路、模块功能划分以及论文写作框架,对毕业设计或课程项目具有直接参考意义。
1. 旧衣回收系统:为什么我选 Spring Boot 加微信小程序这套组合
一个社区、一所高校里,旧衣回收的真实痛点不是“没人收”,而是“想捐的人找不到地方,回收箱满了又没人管,上门回收又怕不透明”。我之前帮某开发者做过的模拟项目X,就是一套基于微信小程序的旧衣回收系统,后端用 Spring Boot,既作为毕业设计的技术载体,也真的能跑通“用户预约下单、回收员上门揽收、称重计价、积分兑换”的完整闭环。这篇文章我会把从设计到落地的关键路径拆开讲:技术选型、数据库设计、后端接口、小程序端调用,还有那些不跑一遍根本遇不到的坑。适合正在做同类毕业设计、或者想在小程序里做 O2O 回收业务的开发者,读完至少能少走两周弯路。
那为什么要选 Spring Boot 而不是 Node.js 或 PHP?因为 Spring Boot 生态里做后台管理系统、定时任务、权限控制都有成熟方案,团队里如果有人会 Java,接手和答辩演示都更稳。小程序端选微信原生而不是 uni-app,是为了去掉一层框架的“黑匣子”,出问题时能直接定位到微信的 API 上报信息。这套组合看起来不花哨,但胜在每一步都有据可查,这是做毕业设计或者小规模真实项目最需要的。
2. 系统设计与技术选型:先把角色、状态和表结构想清楚
动手写代码之前,我把整个系统拆成了三条业务线:用户线、回收员线、管理员线。这三条线互相独立又通过“预约单”这个核心实体串起来。先画清角色和状态,再设计表,最后才写接口。这个顺序一旦反了,后面改起来全是血泪教训。
2.1 角色与用例:用户能做什么,回收员能做什么
系统的用户端角色分三类:普通用户、回收员、管理员。普通用户在小程序里完成注册、发布回收预约、查看订单状态、查看积分和兑换记录。回收员在微信小程序里有一版专门的工作台(或者用同一个小程序做角色切换),可以查看待接单列表、接单、上门揽收、称重并录入重量、结算回收金额。管理员则通过 Web 后台管理系统维护回收品类、定价规则、积分规则,查看运营统计和用户反馈。
这里有一个容易忽略的细节:用户和回收员在物理上是同一个人吗?不一定。真实场景里回收员是固定员工或兼职,所以用户表和回收员表要分开设计,而不是在用户表上加一个 role 字段了事。回收员有额外的资质信息、服务区域、每日接单上限。如果当初把角色耦合在一张表里,后面做排班会非常痛苦。
用例上,用户端核心是“预约回收”和“跟踪订单”。回收员端核心是“接单”和“称重结算”。管理员端核心是“配置规则”和“处理纠纷”。围绕这三个核心,我把功能模块拆成了:小程序登录、预约管理、订单管理、品类管理、积分管理、统计数据,一共六个模块。每个模块对应后端一个 controller,不搞过度抽象。
2.2 技术栈选型理由:为什么是 Spring Boot、MyBatis Plus 和 MySQL
后端采用 Spring Boot 2.7 加 MyBatis Plus 加 MySQL 8.0。选 MyBatis Plus 而不是原生 MyBatis,是因为它对单表 CRUD 做了封装,写简单的 insert、selectPage 不需要手写 XML,能省下大量时间来处理业务状态流转。复杂的统计查询再用注解 SQL 或 XML 补充,两者并存不冲突。
小程序端使用微信官方原生框架,不引入第三方 UI 库。原因很简单:微信开发者工具自带的组件和 API 足够完成表单、列表、地图选点这类操作。引入 vant 之类的库会带来样式覆盖和版本兼容问题,在微信基础库升级后经常莫名翻车。对于毕业设计来说,原生写出的包体更小,审核也更容易通过。但如果你打算在真实社区运营,原生写法在复杂表单交互上会多花一点功夫,这一点要有预期。
数据库选 MySQL 是因为它够用、生态好、相关文档多。这套系统的并发量远没到需要 PostgreSQL 特殊功能的地步。存储文件(旧衣照片、用户头像)直接用云存储对象存储服务,数据库只保存 URL,不存二进制。这是一种很常见的做法,避免把数据库拖垮。
2.3 数据库设计:核心表结构与字段约束
主要表有:用户表(user)、回收员表(recycler)、地址表(user_address)、预约单表(appointment_order)、订单流水表(recycle_order)、回收品类表(category)、积分流水表(point_log)、规则配置表(rule_config)。我先说其中最核心的四张表。
用户表字段:user_id(主键)、openid(微信登录唯一标识)、nickname、phone、avatar_url、created_time。openid 必须加唯一索引,因为小程序登录后拿到的 openid 是用户在应用内的唯一身份,不能重复。手机号是用户填写后绑定,用于回收员联系。
地址表字段:address_id、user_id、contact_name、phone、province、city、district、detail_address、is_default。这里要说明的是,微信小程序里有wx.chooseAddress接口,但它返回的是微信账号里保存的收货地址,需要用户授权,而且很多用户不愿意授权。所以我做的是用户手动填写的表单,省去授权弹窗的困扰。
预约单表是核心,这里单独说。字段:appointment_id、user_id、address_id、appointment_time、status、remark。status 是关键的状态字段,我用字典值表示为:0-待接单,1-已接单(待上门),2-已上门(待称重),3-已完成,4-已取消。很多同学会直接把状态塞在订单表里,但预约单和订单是两个概念:预约单是用户提交的“上门请求”,订单是称重后生成的“回收交易”。如果合并成一张表,那“用户预约了两次但只成交一次”的数据就不知道该怎么存了。
回收订单表字段:order_id、appointment_id(关联预约单)、recycler_id、user_id、category_json(回收品类与重量快照)、total_amount、point_value、status、pay_status。这里用 appointment_id 做外键关系,保证一个预约单最多生成一个订单。重量快照用 JSON 存,是为了避免频繁改表结构。
品类表字段:category_id、name、unit_price(每公斤单价)、point_per_kg(每公斤积分)、is_active。规则配置表就存单次回收最低重量、最大预约数、积分兑换比例等参数。想法是管理员能改配置,而不用改代码。
2.4 后端项目结构:分层与包规划
项目结构按常见的 maven 单模块方式组织。我见过有人强行拆成多模块,结果毕业设计答辩时被问“为什么这么分”而答不上来。单模块、分层清晰,更容易讲明白。
com.example.recycle ├── common // 通用返回体、异常处理、JWT工具 ├── config // WebMvc配置、拦截器、定时任务配置 ├── controller // 接口层,按业务模块分包 │ ├── admin │ ├── user │ └── recycler ├── service // 业务逻辑层 ├── mapper // MyBatis Plus的Mapper接口 ├── entity // 数据库实体 ├── dto // 请求和返回的封装对象 ├── task // 定时任务 └── RecycleApplication.javacontroller 层只做参数接收和接口路由,不写业务。service 层处理状态流转、事务控制。mapper 层只做数据库操作。这种分层让你在写毕业论文时有一个天然的整章节素材:每一层的职责都能对应到软件工程理论。同时,单元测试也更容易写。
提示:controller 里不要返回裸对象,自定义一个统一的返回体R<T>,里面包含 code、message、data。这样小程序端处理异常逻辑时会简单很多。我见过很多半成品系统,每个接口返回结构都不一样,前端联调时不得不见一个接口写一个解析,特别浪费时间。
3. 后端 Spring Boot 核心实现:从登录鉴权到订单状态机
这一章是整个系统的毛坯房,把所有关键代码跑通后,后续功能在这个骨架上加就行了。我按“登录 → 预约 → 接单 → 称重 → 完成”这条主线写核心代码,并给出对应的实现思路和参数说明。
3.1 微信小程序登录与 JWT 鉴权
小程序端调用wx.login拿到code,后端拿 code 向微信接口换取openid和session_key。然后后端自己签一个 JWT 返回给小程序,后续请求带上这个 token 即可。为什么不用微信的 session_key 直接做身份验证?因为 session_key 有效期短且需要正确解密,自己签发 JWT 更可控,也方便你后续给回收员端和管理端也用同一套鉴权机制。
后端登录接口的代码片段:
@PostMapping("/login") public R<String> login(@RequestBody LoginRequest req) { // 1. 向微信服务器换取openid String url = "https://api.weixin.qq.com/sns/jscode2session"; RestTemplate rest = new RestTemplate(); Map<String, String> params = new HashMap<>(); params.put("appid", appId); params.put("secret", appSecret); params.put("js_code", req.getCode()); params.put("grant_type", "authorization_code"); String result = rest.getForObject(url + "?appid={appid}&secret={secret}&js_code={js_code}&grant_type={grant_type}", String.class, params); // 2. 解析openid,如果用户不存在则创建新用户 JSONObject obj = JSON.parseObject(result); String openid = obj.getString("openid"); User user = userService.findByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setNickname("微信用户" + openid.substring(openid.length() - 4)); userService.save(user); } // 3. 生成JWT,设置过期时间7天 String token = JwtUtil.createToken(user.getId()); return R.success(token); }逻辑说明:这段代码的核心是先通过 code 换 openid,再用 openid 作为业务主键去查用户表。注意这里没有把 session_key 暴露给前端,而是由后端自己管理用户会话。参数说明:appId和appSecret从配置文件读取,不要硬编码在代码里。JwtUtil中的过期时间我设成 7 天,因为小程序用户不一定会频繁打开,如果过期太短会老是被迫重新登录。
在后续接口上,我通过拦截器从请求头取出Authorization,解析出 userId 并放入 ThreadLocal,这样每个 service 层方法里都能拿到当前用户。这是很常见的做法,但要注意线程池环境下 ThreadLocal 的清理问题,否则在高并发下可能出现用户身份串号。如果只做毕业设计,单机演示没有太大问题,但如果要放到生产环境,建议改成在拦截器里用上下文对象传递。
3.2 预约单创建与回收员接单:状态流转设计
用户提交预约时,需要校验当天是否已经有过未完成的预约单,防止恶意刷单。回收员接单时要用乐观锁或者数据库状态判断,避免两个回收员同时接同一单。
创建预约单的 service 代码:
@Transactional public void createAppointment(Long userId, AppointmentDTO dto) { // 校验当天已有未完成预约单 LocalDate today = LocalDate.now(); List<AppointmentOrder> exists = appointmentMapper.selectList( new LambdaQueryWrapper<AppointmentOrder>() .eq(AppointmentOrder::getUserId, userId) .eq(AppointmentOrder::getStatus, 0) .ge(AppointmentOrder::getCreateTime, today.atStartOfDay()) ); if (!exists.isEmpty()) { throw new BizException("当天已有待处理预约,请勿重复提交"); } AppointmentOrder order = new AppointmentOrder(); order.setUserId(userId); order.setAddressId(dto.getAddressId()); order.setAppointmentTime(dto.getAppointmentTime()); order.setRemark(dto.getRemark()); order.setStatus(0); appointmentMapper.insert(order); }接单的 service 代码:
@Transactional public void acceptAppointment(Long recyclerId, Long appointmentId) { // 使用乐观锁:status=0且更新后status=1 int rows = appointmentMapper.update(null, new LambdaUpdateWrapper<AppointmentOrder>() .set(AppointmentOrder::getStatus, 1) .set(AppointmentOrder::getRecyclerId, recyclerId) .eq(AppointmentOrder::getAppointmentId, appointmentId) .eq(AppointmentOrder::getStatus, 0)); if (rows == 0) { throw new BizException("该预约单已被其他回收员接走"); } }逻辑说明:预约单的创建逻辑把“当天是否已有待接单”作为约束,而不是通过前端按钮禁用,这样能防止绕过界面的恶意请求。接单的乐观锁更新能保证并发情况下只有一个回收员抢到订单。参数说明:set方法会更新非空字段,eq条件里status=0是核心,数据库更新时如果 status 不是 0,受影响行数就是 0,由此判断冲突。
现实中还有一个比较坑的点:预约时间用户可能会选几天后,这时候待接单的预约单会积压。我后来加了一个定时任务,把超过预约时间 2 小时且还没有回收员接单的预约单自动取消,并给用户发一条订阅消息通知。实现不难,就是 Cron 表达式每 10 分钟跑一次,查询 status=0 且 appointment_time 小于当前时间减 2 小时的数据,执行 update。真做系统时这个细节很关键,否则用户的预约单会无声无息地“永远等不到人”。
3.3 回收员称重与结算:金额、积分和订单生成
回收员上门后,需要在小程序端上报每类旧衣的重量。后端根据重量乘以品类单价,计算总金额,并给用户累计积分。注意这里不是先收款,而是生成一个“待支付/已完成”的订单,实际代收可以由回收员线下支付后确认,也可以走线上支付。毕业设计阶段可以先做成订单完成,金额记录在系统里,不接微信支付。
称重结算的代码核心:
@Transactional public void completeOrder(Long recyclerId, CompleteOrderDTO dto) { AppointmentOrder appointment = appointmentMapper.selectById(dto.getAppointmentId()); if (appointment == null || appointment.getStatus() != 2) { throw new BizException("当前状态不可结算"); } double totalAmount = 0; int totalPoint = 0; JSONArray categories = dto.getCategories(); for (int i = 0; i < categories.size(); i++) { JSONObject item = categories.getJSONObject(i); Long categoryId = item.getLong("categoryId"); Double weight = item.getDouble("weightKg"); Category category = categoryMapper.selectById(categoryId); totalAmount += weight * category.getUnitPrice(); totalPoint += weight * category.getPointPerKg(); } RecycleOrder order = new RecycleOrder(); order.setAppointmentId(appointment.getAppointmentId()); order.setRecyclerId(recyclerId); order.setUserId(appointment.getUserId()); order.setTotalAmount(totalAmount); order.setPointValue(totalPoint); order.setStatus(1); // 已完成 order.setCategoryJson(categories.toJSONString()); recycleOrderMapper.insert(order); // 更新预约单状态为已完成 appointmentMapper.update(null, new LambdaUpdateWrapper<AppointmentOrder>() .eq(AppointmentOrder::getAppointmentId, appointment.getAppointmentId()) .set(AppointmentOrder::getStatus, 3)); // 给用户加积分 pointLogService.addPoint(appointment.getUserId(), totalPoint, "回收订单结算"); userService.updatePoint(appointment.getUserId(), totalPoint); }逻辑说明:称重结算是一个多步操作,包括插入订单、更新预约单、加积分,必须放在一个事务里,任何一步失败都要回滚。参数说明:categories数组在前端只能选择后台启用的品类,后端必须重新查品类表拿最新价格,不能信任前端传过来的 price,否则管理员改价后旧价格仍被刷到订单里。appointment.getStatus() != 2的校验能防止跳过上门环节直接结算。
实际运营时,旧衣回收的金额通常不是按所有品类给钱,而是按废旧纺织品的等级分拣后定价。我这个系统简化成按品类固定单价,但表结构已经预留了category_json,以后想细分等级可以扩展。对于论文来说,这种可扩展的设计反而更容易写出深度。
3.4 管理后台接口:统计报表与规则配置
管理员接口我单独放了一层admin包,所有接口都要求管理员角色校验。这里我只截取一个“每日回收统计”的 Mapper 自定义 SQL 作为例子。
@Mapper public interface OrderMapper extends BaseMapper<RecycleOrder> { @Select("SELECT DATE(create_time) as date, COUNT(*) as orderCount, SUM(total_amount) as amount " + "FROM recycle_order " + "WHERE create_time >= #{start} AND create_time < #{end} " + "GROUP BY DATE(create_time) ORDER BY date") List<DailyReportVO> selectDailyReport(String start, String end); }对应的 service 层根据报表接口调用这个方法,返回给前端用柱状图展示。参数说明:#{start}和#{end}都是字符串日期,MySQL 能直接和 datetime 比大小。这种统计 SQL 在数据量大时效率不高,但对于毕业设计或社区级规模足够。如果未来订单量上百万,考虑按天分表或引入数据分析服务,当前阶段不用做。
管理后台的另一个核心是规则配置。我把单价、积分规则、预约限制等配置放在一张rule_config表里,用 key-value 结构存储。配置修改后,小程序端拉取配置时能拿到最新的值。注意:这里需要加缓存,否则每次点击请求三次查询数据库,但缓存更新时机要处理好。我在项目里把配置缓存在本地内存里,管理员修改后调用clearCache接口让缓存失效,实现成本最低。
4. 小程序前端实现与前后端联调
小程序端和后端联调时,最难受的不是写页面,而是处理好请求体的格式、登录态失效、以及不同手机型号上的地图组件表现。我采用原生微信小程序,代码结构按页面分包组织,下边是几个关键实现。
4.1 小程序目录结构与公共请求封装
小程序端的根目录结构:
pages/ ├── index/ // 首页,展示回收品类和快捷预约 ├── order/ // 预约页(选择地址、时间、备注) ├── orderList/ // 我的订单列表 ├── orderDetail/ // 订单详情 ├── profile/ // 个人中心,积分和设置 ├── recycler/ // 回收员工作台(角色切换) ├── login/ // 登录页 └── utils/request.jsrequest.js是所有请求的统一入口。这里我做了两件事:自动附带 JWT token;遇到 401 时自动重新登录并重发请求。下面的代码是核心:
// utils/request.js const BASE_URL = 'https://your-api.example.com/api'; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token'); wx.request({ url: BASE_URL + path, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': token ? 'Bearer ' + token : '' }, success(res) { if (res.data.code === 0) { resolve(res.data.data); } else if (res.data.code === 401) { // token过期,重新登录 wx.removeStorageSync('token'); reLogin().then(() => { request(path, method, data).then(resolve).catch(reject); }); } else { wx.showToast({ title: res.data.message, icon: 'none' }); reject(res.data); } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } function reLogin() { return new Promise((resolve, reject) => { wx.login({ success(res) { wx.request({ url: BASE_URL + '/user/login', method: 'POST', data: { code: res.code }, success(res) { wx.setStorageSync('token', res.data.data); resolve(); }, fail: reject }); }, fail: reject }); }); } module.exports = { request };逻辑说明:这里把 401 当作登录失效的统一处理,避免每个页面自己写重登逻辑。参数说明:BASE_URL需要在“微信开发者工具”的“详情”中配置“不校验合法域名”,才能方便调试 HTTP 接口。真机预览时必须使用 HTTPS 域名,并且在微信公众平台配置服务器域名。
一个常见的翻车点:很多人直接在前端判断statusCode === 200就认为成功,但后端业务错误也同样返回 200。我在后端统一返回 code 字段,只有 code 为 0 才算成功,这样业务异常和网络异常能清晰分开。
4.2 首页与预约表单:从 wx.login 到提交预约
首页的预约表单是整个系统的入口,用户在这里选择回收品类、填写地址、选择时间段。小程序端的首页onLoad里先调用wx.login换取 token,然后拉取品类列表和默认地址。
首页关键代码片段:
// pages/index/index.js const { request } = require('../../utils/request'); Page({ data: { categories: [], addressList: [], selectedAddress: '', appointmentTime: '', remark: '' }, onLoad() { this.loadCategories(); this.loadDefaultAddress(); }, async loadCategories() { const res = await request('/category/list', 'GET'); this.setData({ categories: res }); }, async loadDefaultAddress() { const res = await request('/user/address/default', 'GET'); this.setData({ addressList: res, selectedAddress: res.addressId || '' }); }, submitAppointment() { if (!this.data.selectedAddress) { wx.showToast({ title: '请选择地址', icon: 'none' }); return; } request('/appointment/create', 'POST', { addressId: this.data.selectedAddress, appointmentTime: this.data.appointmentTime, remark: this.data.remark }).then(() => { wx.showToast({ title: '预约成功', icon: 'success' }); wx.switchTab({ url: '/pages/orderList/orderList' }); }); } })预约时间我用了picker组件,mode="date"选择日期,再拼一个pickermode="time"选择时段。这里有个用户体验细节:预约时间不能是过去的时刻,否则回收员到场时发现时间已经过了,产生纠纷。我建议在后端也做相同校验,前端只能做提示。
4.3 订单列表与状态轮询:让用户看得到回收进度
用户提交预约后最关心的是“回收员到哪了”。我做了两个展示方式:预约单状态用列表展示,订单详情里显示流程进度条,如“待接单 → 待上门 → 已完成”。状态更新通过轮询接口实现,每 5 秒拉取一次当前订单状态。
// pages/orderDetail/orderDetail.js const { request } = require('../../utils/request'); Page({ data: { orderId: '', detail: null }, onLoad(options) { this.setData({ orderId: options.id }); this.loadDetail(); this.startPolling(); }, async loadDetail() { const res = await request('/appointment/detail?id=' + this.data.orderId, 'GET'); this.setData({ detail: res }); // 当状态为已完成时停止轮询 if (res.status === 3) { this.stopPolling(); } }, startPolling() { this.timer = setInterval(() => this.loadDetail(), 5000); }, stopPolling() { clearInterval(this.timer); }, onUnload() { this.stopPolling(); } })这个轮询方案对于小规模系统来说是够用的。微信小程序的推送能力其实也具备,但订阅消息需要用户点击允许,而且模板内容有严格限制。在没有后端自建 WebSocket 的情况下,轮询是性价比最高的方案。
注意:页面卸载时一定要clearInterval,否则定时器会持续到后台甚至内存泄漏。我在真机测试时遇到过切到其他页面再回来,定时器仍在跑,导致重复请求大量积压。加上onUnload清楚后就好了。
4.4 回收员工作台:角色切换与扫码确认
回收员端我是在同一个小程序里通过一个“切换角色”按钮进入的。进入回收员工作台后,页面拉取当前回收员名下的待接单列表。接单操作通过后端接口完成,前端只需要处理按钮的 loading 状态,防止用户连续点击两次触发两个请求。
由于回收员工作台不需要在论文里作为重点,我只实现了核心页面:待接单列表、预约单详情(含预约地址、用户联系方式)、称重结算表单。称重结算表单要求回收员逐项输入重量和选择品类,然后调用后端结算接口。这里我没有做扫描二维码确认,而是直接根据订单号绑定的预约单来操作,省去了打印二维码的硬件成本。如果你有更多时间,可以接入小程序原生的扫码 APIwx.scanCode,回收员扫码后自动打开对应的订单详情,效率会高很多。
5. 避坑指南:我从这个项目里踩过的五个坑
这套系统前后端加起来大概花了三周写完第一版,但联调和走查又花了十天。踩过的坑不少,挑五个影响最深的写出来,基本每条都能让你少折腾一整天。
5.1 小程序请求本地后端被拒绝:域名校验问题
现象:在微信开发者工具里请求http://localhost:8080/api,返回request:fail,真机预览更是白屏。原因:微信小程序默认只允许请求 HTTPS 且已经在公众平台配置过的域名,本地开发环境需要特殊处理。解决:在微信开发者工具“详情”->“本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”即可。真机调试时要用局域网 IP 或花生壳之类的内网穿透工具,但注意最终上线必须换成 HTTPS 域名。这个坑不只是新手会遇到,有次我改了电脑的网络环境,代理开着,请求全部被拦截,排查了半天才意识到是代理在作怪。
5.2 回收员重复接单:并发下状态被覆盖
现象:两个回收员同时点“接单”,结果都跳转到了接单成功页面,订单详情里显示两个回收员 ID。原因:前端点击后没有即时禁用按钮,后端也没有校验当前状态,两个请求都通过了 status=0 的条件落库。解决:后端接单接口改成乐观锁更新,update ... set status=1 where appointment_id=? and status=0,并且前端在点击接单后立即setData({ loading: true })禁用按钮。这条规则适用于所有“抢单”类操作,比如管理员审核、优惠券领取。
5.3 定时任务重复执行导致订单状态错乱
现象:我写了一个定时任务,每 10 分钟取消超时未接单的预约单。有一天测试时发现同一个预约单被取消后又变成了“待接单”。原因:定时任务跑了两遍,因为部署环境是多实例或者开发时同时启动了多个服务。解决:给定时任务加权限控制,只允许一台机器的某个实例执行。最简单的做法是在配置里加task.enabled开关,只在主实例打开,或者在数据库里使用分布式锁表。毕业设计阶段可以只跑一个实例,但要在论文里指出多实例下要引入分布式锁。
5.4 用户手机号授权改了规则:不能直接拿微信手机号
现象:我在小程序里用wx.getPhoneNumber想获取用户手机号,结果开发时正常,提交审核后被拒,理由是用户明确拒绝授权时无法调用。原因:微信后来收紧了手机号获取流程,要求必须开通“手机号快速验证组件”,且不能强制用户授权。解决:在用户使用核心业务前,通过表单让用户填写手机号,或者使用微信提供的手机号验证按钮组件(但仍需要用户点按触发)。我最后在预约表单里加了一个手机号输入框,并做了正则校验,既简单又能过审。
5.5 订单金额精度问题:double 导致的积分不对
现象:积分记录里出现12.000000000000002这种诡异数字。原因:我在 Java 代码里用double计算重量乘以单价,浮点数精度丢失。解决:所有涉及到钱的字段统一用BigDecimal,积分则用整数类型。数据库的金额字段用decimal(10,2),不要用 float/double。这个坑看似是常识,但很多人调试时看 log 里金额还是对的,直到用户反馈积分多了 0.0001 才注意到,属于血泪经验。
6. 上线前要做的验证与进阶玩法:从能跑到能交付
系统最后一公里不是写代码,而是验证业务闭环和准备答辩演示。我自己做过一次给某社区试运营用的类似系统,上线前夜发现“回收员接单后无法修改称重重量”这种硬伤,现场手工改库才救回来。现在我会先过一遍线下验证清单。
| 验证点 | 操作 | 预期结果 |
|---|---|---|
| 用户登录 | 首次打开小程序 -> 授权登录 | 后端自动创建用户,返回 token |
| 提交预约 | 填写地址和预约时间 | 订单出现在“我的预约”列表 |
| 回收员接单 | 回收员账号工作台进入待接单 | 状态变为“已接单”,用户端可见 |
| 称重结算 | 回收员录入重量和品类 | 用户获得积分,订单出现金额 |
| 超时取消 | 把预约时间设为过去 2 小时 | 定时任务自动取消订单 |
| 管理后台 | 修改品类单价 | 小程序端下次请求价格已变 |
这些验证点能用 postman 或微信开发者工具直接跑通。答辩演示时最好用两个模拟身份:一个用户账号、一个回收员账号,在同一台电脑上开两个微信开发者工具窗口,实时演示状态变化,比放 PPT 有用得多。
进阶玩法上,我建议优先做三件事。第一是接微信支付,让用户在小程序里直接完成回收金额的提现或者积分兑换,需要申请商户号,并处理好回调通知。第二是把预约地址改成地图选点,使用wx.chooseLocation获取经纬度,后端用高德/腾讯地图逆解析出详细地址,方便回收员规划路线。第三是引入 Redis 缓存热点数据,比如品类列表和积分规则,减少数据库压力。
最后说一个我自己的习惯:每完成一个接口,我会先写一段 pytest 或者用 postman 保存好请求样例,再写前端调用。这样前端出问题时,能迅速判断是接口问题还是页面问题。这套旧衣回收系统的坑主要不在框架,而在业务细节——状态管理、金额精度、并发抢单、定时任务重复触发。把这些处理好,你的系统不仅能写进毕业论文,也能真的拿去给身边社区用一用。希望这些踩坑经验和代码片段能帮到你,少走几段我走过的弯路。
本文还有配套的精品资源,点击获取