做二手数码回收系统这个项目其实是一年前的事了。当时接到这个需求我第一反应是“这玩意看着简单真做起来是块硬骨头”——二手数码产品的估值逻辑、订单状态的复杂流转、前端小程序和一整套后台管理的联动每一环都有坑。后来这个项目从需求拆解到数据库设计从后端接口到小程序页面完整地走了一遍也沉淀了不少经验。今天把这套方案的细节全部拆开重点聊聊 SpringBoot 后端、原生微信小程序端以及配套的 LW论文/文档参考示例在整个项目里到底是怎么组织的希望能给正在做毕设或者想入门全栈开发的朋友一点实实在在的参考。1. 项目到底要做什么先拆解业务闭环再谈技术1.1 二手回收的业务流程拆解很多人一听到“二手数码回收系统”第一反应就是“不就是个表单加个后台吗”。真做起来你会发现完全不是这么回事。二手回收的核心是一条完整的业务闭环用户发起回收申请、系统根据机型状况估价、平台审核订单、用户寄出设备、平台检测设备、确认价格并打款最后才是交易完成。任何一个环节断了业务就转不起来。具体拆一下这条链路至少包含以下几个关键节点用户提交回收订单选择机型、填写设备成色和功能状况、上传实物照片、填写收货地址。系统自动估价根据机型底价库和用户填写的状况参数计算出预估回收价。运营人员审核确认订单信息是否真实、估价是否合理、是否可以进入寄送环节。用户寄送设备生成回收单号用户通过快递寄出后台登记物流单号。平台检测复估收到设备后质检人员录入实际检测结果系统基于检测结果重新计算最终回收价。用户确认收款用户确认最终价格平台打款订单关闭。这里最容易忽略的是**“估价—复估”的两段式设计**。很多新手做回收系统只做一次估价做完用户就等着收钱了但实际上二手交易的核心信任点在于“实际到手价”。所以设计时我特意把初步估价和检测复估拆成了两个独立的服务后端可以分别调用前端小程序也分两步展示。1.2 角色划分与功能模块矩阵围绕上面这些环节系统里的角色天然分为三类角色核心操作使用的端C端用户创建回收单、查看估价、上传照片、填写地址、确认价格、查看订单进度微信小程序运营/质检人员审核订单、登记物流、录入检测结果、调整最终估价、处理异常Web管理后台系统管理员管理机型底价库、管理轮播图、管理用户、统计订单数据Web管理后台功能模块上小程序端和后台管理端是两套完全独立的前端工程但共用同一套 SpringBoot 后端接口。以我当时的实现为例小程序端核心页面有五个首页推荐机型估价入口、估价页动态表单、提交订单页、订单列表页、个人中心页。后台管理端用的是基于 Bootstrap 的轻量模板模块包括登录鉴权、仪表盘统计、订单管理主打审核复估、机型管理、用户管理、系统设置。这里要强调一个设计原则小程序端和管理端千万不要各写一套逻辑。比如估价计算如果小程序里写一套、管理后台又写一套两边参数稍微不同就完蛋了。我当时的做法是所有业务规则全部下沉到 SpringBoot 的 Service 层前端只负责传参数、展示结果这样后续维护起来非常舒服。2. 技术选型为什么这么定SpringBoot、原生小程序与管理端方案2.1 后端选 SpringBoot 的理由生态、效率、好排错先说后端。这个项目的技术栈核心是 SpringBoot 2.x MyBatis Plus MySQL Redis这也是目前做中小型管理系统最主流的一套组合几乎不用犹豫。选 SpringBoot 有几个非常实际的考量第一起步成本低。SpringBoot 通过自动配置大幅简化了 Spring 的 XML 配置一个注解启动类就能跑起来。对毕设项目来说时间最宝贵没必要在配置上耗太多精力。第二生态太成熟了。做这种系统绕不开几个通用能力——登录鉴权JWT、文件上传、定时任务、接口校验SpringBoot 里全都有对应的 starter引入依赖配几行参数就能用。我当时用到的核心依赖大致是这样的dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.2.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency第三出了问题容易排查。SpringBoot 的异常处理机制比较统一配合全局异常处理器可以把业务异常和技术异常分开处理前端拿到的报错信息都是可读的。这一点在实际联调时帮我省了不少时间。2.2 小程序端为什么坚持原生开发而不是用 uniapp当时身边几个朋友都建议我用 uniapp理由是“一套代码多端复用”。但我最终选了原生微信小程序原因有两个一个是原生小程序没有中间层性能损耗页面渲染和组件交互的响应速度明显更快尤其在做多图片上传、复选表单这种交互密集的场景原生的体验会顺很多。另一个是对毕设来说原生小程序的代码结构更清晰页面逻辑、组件生命周期、微信 API 调用都是明摆着的指导老师看起来也直观。你如果用 uniapp 封装了一层其实不到最后还是得回到微信开发者工具里调原生能力——那不如从一开始就用原生的。当然原生小程序的缺点我也得说没法直接复用 Web 组件UI 得自己写样式和普通 CSS 又有些差异rpx 单位、flex 布局虽然有但细节不一样。对于没接触过小程序的人来说前期会有个小小的适应期但这个成本远比后面踩 uniapp 的兼容性坑要低。2.3 LW 参考文档在整个项目里的定位和价值很多第一次做毕设的人不太理解“LW”是什么。这里说的 LW是“论文”的拼音缩写在毕设语境里指的是与系统配套的毕业设计论文文档。一个合格的毕设项目不只是代码能跑还要求你有一份从背景、需求分析、系统设计、数据库设计、接口设计到测试报告都完整的文档。这个“参考示例”到底该怎么用我的建议是需求分析章节可以直接用来反推自己系统的用例图和时序图避免“只会写代码、讲不出逻辑”的尴尬。数据库设计章节通常包含完整的表结构说明和你的实体类字段一一对应写代码的时候能省掉反复改表的时间。接口文档部分可以当作 Controller 层的注释来源让代码的可读性明显提升。但千万注意LW 文档只能作为参考框架不要直接照抄。因为每个项目的表字段、接口命名、业务细节都不一样而且论文查重是有标准的直接抄风险太大。我用的是“自己写代码、参考文档结构、补充自己的截图和测试数据”这种方式最后效果很好。3. 数据库与核心表设计前期设计决定后期效率3.1 核心表结构设计字段定义背后的思考数据库设计是这种系统最见功底的地方。我先说几个重点表每个表的设计都有明确的目的。用户表user字段类型说明idbigint主键自增openidvarchar(64)微信唯一标识nicknamevarchar(64)昵称avatarvarchar(255)头像 URLphonevarchar(20)手机号create_timedatetime注册时间这里有个细节openid 必须加唯一索引。微信登录时如果重复插入 openid会导致一个微信号可以注册多个账号业务上就会乱套。机型表product_model字段类型说明idbigint主键category_idbigint所属品类手机/平板/笔记本brandvarchar(32)品牌model_namevarchar(64)机型号如 iPhone 13 Proreference_pricedecimal(10,2)市场参考底价imagevarchar(255)机型图片statustinyint是否上架reference_price 是估价的核心基数我之前吃过亏一开始把底价写死在代码里结果运营想调价只能改代码重启非常蠢。后来改成数据库表维护后台可以随时调整才彻底解决。回收订单表recycle_order字段类型说明idbigint主键order_novarchar(32)订单号业务唯一user_idbigint下单用户model_idbigint回收机型condition_descvarchar(255)用户描述的成色状况condition_ratiodecimal(4,2)成色系数0.35~1.0function_descvarchar(255)功能问题描述function_ratiodecimal(4,2)功能系数0.2~1.0estimate_pricedecimal(10,2)预估回收价final_pricedecimal(10,2)最终复估回收价statustinyint订单状态状态机枚举值user_namevarchar(32)寄件人姓名user_phonevarchar(20)寄件人电话user_addressvarchar(255)寄件地址logistics_novarchar(64)快递单号create_timedatetime下单时间update_timedatetime更新时间还要配套一个订单状态日志表order_status_log记录每一次状态变更。这个表非常重要它是用户在小程序里看到“订单轨迹”的数据来源也是答疑时对账的依据。3.2 订单状态机整个系统的命脉订单状态是整个回收系统里最容易出错的地方。我第一次设计时偷懒直接用 status 字段的取值来表示各种状态结果代码里到处都是 if (status 1) 这样的判断改一个状态就要全局搜索特别容易漏。后来重新设计成了一套规范的状态机状态值含义下一步可流转状态0待审核待寄送、已驳回1待寄送已寄送2已寄送检测中3检测中待确认4待确认已完成-1已驳回无-2已取消无这套状态机的核心价值在于状态变更必须走 Service 层的方法上下游的流转逻辑统一封装不允许前端直接往 status 字段写值。比如用户提交寄出物流单号的操作后端要先校验当前状态是否为“待寄送”再更新状态到“已寄送”同时写入日志表。用一张简单的状态流转图辅助理解整个流程大概是假设用户发起回收申请系统创建订单订单进入“待审核”运营查看信息无误后点击通过订单变成“待寄送”用户填写顺丰单号订单进入“已寄送”平台签收设备后标记“检测中”质检人员录入检测结果系统计算出最终价订单变成“待确认”用户在手机上看到最终价点击确认收款订单“已完成”。如果中途用户不想要了可以发起取消订单进入“已取消”。我强烈建议你把状态定义写成枚举类放到 Java 代码里而不是用魔法数字。这样写出来的代码配合状态日志表整个订单流转过程一目了然调试时省一半时间。4. 后端关键逻辑实现估价、状态流转与文件上传4.1 估价算法让系统会“算账”但不乱来估价是回收系统用户最关心的功能也是后端逻辑里最需要思考的部分。我当时设计的规则是预估价 机型底价 × 成色系数 × 功能系数别小看这个公式关键是参数的取值规则要定义清楚。成色这一维我划分了五档全新、99新、95新、9成新、8成新及以下分别对应 1.0、0.95、0.85、0.75、0.6 的系数。功能这一维分“完全正常”“轻微问题如屏幕划痕不明显”“明显问题如摄像头不能对焦”“无法开机”四档分别对应 1.0、0.8、0.5、0.2。举个例子一部 iPhone 13 Pro底价 5000 元。用户选择“9成新”系数0.75功能选择“轻微问题”系数0.8那么预估回收价就是 5000 × 0.75 × 0.8 3000 元。这个数字作为“预估价”推送给用户。等设备寄到质检中心质检人员把实际检测数据录入后台系统用同样的公式、基于真实检测结果重新计算得到final_price。如果最终价和预估价相差较大系统会自动给用户发一条模板消息通知提醒用户查看订单详情。这里有两个很关键的经验底价表的参考价格要定期更新否则手机发布三个月后市场价已经跌了 20%系统还是按原价估价用户会直接流失。估价公式的系数不要编码写死放到数据库或配置中心里。业务同学想调整“9成新”的系数时不能让他等我们发版。4.2 订单状态流转的接口设计用枚举和工具方法管理订单接口的设计我用了一个很实用的思路把状态流转规则集中到一个方法里。public void changeOrderStatus(Long orderId, Integer targetStatus) { RecycleOrder order getById(orderId); if (OrderStatusEnum.REVIEW_PENDING.getCode().equals(order.getStatus()) OrderStatusEnum.WAITING_SEND.getCode().equals(targetStatus)) { // 审核通过 } else if (OrderStatusEnum.WAITING_SEND.getCode().equals(order.getStatus()) OrderStatusEnum.SENT.getCode().equals(targetStatus)) { // 用户填写物流单号 } // 其余流转分支... order.setStatus(targetStatus); updateById(order); saveStatusLog(orderId, targetStatus); }每个流转动作都是一个合法迁移其他情况一律抛业务异常返回“订单状态不合法”的提示。这样一个入口管理所有变更日志也统一记录排查问题的时候特别方便。对应的 Controller 层接口要尽量简洁PostMapping(/order/confirm) public RString confirmOrder(RequestBody ConfirmOrderRequest request) { orderService.changeOrderStatus(request.getOrderId(), OrderStatusEnum.WAITING_SEND.getCode()); return R.ok(审核通过); }前端小程序发起请求后只需要关心接口成功与否具体能不能流转全靠后端状态机把关。这样设计就算有人绕过小程序直接改接口参数也没法把订单从“待审核”直接改成“已完成”。4.3 图片上传与文件访问别把图片存进数据库回收订单需要用户上传设备照片这是为了让质检人员提前了解设备外观状况。这里最容易犯的错误是把图片转成 Base64 字符串塞进数据库字段里数据库性能会很快被拖垮。我用的方案是小程序端先用wx.chooseMedia选择图片再通过wx.uploadFile传到后端一个独立的/upload接口。后端用 MultipartFile 接收文件校验类型和大小单张限制 5MB只允许 jpg/png 等常见格式然后用 UUID 生成文件名保存到本机的upload目录数据库里只存一个相对路径。PostMapping(/upload) public RString upload(RequestParam(file) MultipartFile file) throws IOException { if (file.isEmpty()) { throw new BusinessException(上传文件不能为空); } String originalName file.getOriginalFilename(); String ext originalName.substring(originalName.lastIndexOf(.)); String fileName UUID.randomUUID() ext; File dest new File(uploadDir / fileName); file.transferTo(dest); return R.ok(/upload/ fileName); }同时SpringBoot 里要做静态资源映射把/upload/**路径指向本地目录这样小程序和后台通过 URL 直接就能访问到图片Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourcePaths(file: uploadDir /); } }这里有个实际教训一开始我没做静态资源映射后端接口能保存文件但前端通过https://xxx/upload/abc.jpg访问时 404排查了半天才发现是资源映射没配。如果你用了 nginx 做反向代理还要记得把/upload/路径单独放行不然文件依然访问不到。5. 原生微信小程序端实现要点5.1 请求封装与登录态管理小程序的基建工程小程序端的代码组织我建议先搭好两个基础能力request 请求封装和登录态管理。否则后面写每个页面都会重复一堆 wx.request 代码后期想统一加 loading、错误提示就得满屏找。这是我的请求封装思路核心是利用 Promise 包装 wx.requestconst BASE_URL https://你的后端地址 function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method, data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) }, success(res) { // 业务约定 code200 表示成功 if (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 }登录态这里我用的是wx.login获取 code发给后端换取 openid后端签发 JWT 返回 token小程序存在本地 Storage 里。每次请求带上Authorization: Bearer token后端通过拦截器校验身份。有个点要提醒JWT 的有效期不要设置太长。我一开始设了 30 天后来发现用户 token 过期后小程序里所有请求都报 401而用户根本不知道怎么重新登录。建议设置 7 天同时在小程序启动时检查 token 是否存在不存在或者过期就自动重新走一遍wx.login换 token 流程。5.2 回收下单页面的核心交互实现回收下单页面是整个小程序端交互最复杂的页面。它需要用户选机型、选成色、填功能问题、传照片、填地址一个页面集成了多种组件。选机型我用了两种方式首页展示热门的几款机型用户点击“立即回收”进入下单页时默认选中该机型下单页里也做了搜索框支持按品牌和型号关键词搜索通过请求/model/search接口拿到匹配的机型列表。成色和功能状况用单选卡片组实现选中状态通过>