每年到了毕设季,我都会看到不少人在同一个问题上反复纠结:SpringBoot后端配一个小程序端,到底选什么业务题好写,又不至于撞车撞到天上去?说实话,商城、博客、外卖、疫情管理系统这类题目已经被写烂了,答辩时老师连着听三个一模一样的选题,换我我也疲劳。二手数码回收系统是我比较看好的一个方向,它的业务闭环非常清楚——用户估价、下单回寄、平台检测、最终打款,整个过程里天然携带微信登录、订单状态机、文件上传、配置化规则这些SpringBoot项目的高频考点,前端再配一个原生微信小程序,整条链路就能从头到尾跑通。标题里的“LW”在毕设圈一般就是指论文(LunWen),很多资源包里的“LW参考示例”就是论文文档。所以这篇文章我不只讲后端和小程序端怎么做,也会把论文部分怎么写一并拆开,给正在选题或者准备复现的同学一份可以直接参考的思路。
1. 二手回收项目为什么值得选:业务闭环与核心模块拆解
在动笔写代码之前,我建议你先把这个题目当成一个真实生意来理解。二手数码回收不是简单做一张“提交表单”的页面,它对应的是现实中已经存在的在线回收平台模式:用户手里有闲置手机、平板、笔记本,不知道能卖多少钱,平台需要给出估价、回收、检测、结算这条完整服务链。放在毕设里,它比普通CRUD项目多了一个“业务规则”的层次。
1.1 用户、平台运营方和“估价”在这个系统里的位置
系统可以分成两个典型角色。一个是C端用户,打开小程序就能用;一个是平台运营方,负责后台审核订单、检测设备、修改报价、完成打款。毕设里很多人把后台做成一个简单的Vue页面或者Thymeleaf页面,也有人干脆在小程序里加一个“管理入口”,但我更推荐单独做一套运营后台Web页面,界面丑一点没关系。原因在于,这样SpringBoot项目就可以自然拆成“为小程序提供的用户端API”和“为后台提供的管理端API”两组接口,论文里写接口设计时层次清楚,答辩时也更容易讲明白。
这个系统里有一个概念特别容易混淆,就是“估价”和“最终报价”之间的关系。用户在小程序里根据成色条件得到的是预估价,平台收到实物后检测得出的才是最终报价。二者在业务含义上完全不同:预估价是为了让用户愿意下单,最终报价是平台基于实物状况给出的结算依据。如果数据库里只有一个价格字段,后面遇到“用户拒绝最终报价,平台要退回设备”这类情况,整个价格逻辑就会说不清楚。所以估价记录、订单金额、最终确认金额这三个概念,从需求分析阶段就要拆开。
1.2 回收订单的生命周期:从估价到打款的几个状态节点
订单状态机是这个项目少有的、能让答辩老师眼前一亮的点。我建议把订单状态设计成下面六个:
- 待提交/草稿:用户还在填写设备信息,尚未确认下单
- 已下单/待回寄:用户确认订单,地址信息已填,平台等待设备寄达
- 检测中:平台收到实物,运营人员录入检测结果
- 待确认:平台给出最终报价,等待用户在小程序端接受或拒绝
- 已完成:用户接受报价,平台完成打款并标记完成
- 已取消:用户或平台在任意前置节点取消订单
实际开发时,这六个状态不是随意跳的。“待确认”不能直接改成“已完成”却没有任何打款记录,“已取消”之后也不能再回到“检测中”。我建议把状态机画在论文的设计章节里,不需要画得复杂,只保留六个状态和它们之间的迁移线就够了,用draw.io或Visio都可以。代码层面怎么约束状态跳跃,我放到后面第3.3节详细讲,那里用了一条带条件UPDATE来兜底。
2. SpringBoot服务端选型与数据库表设计的几个关键决策
毕设论文里“相关技术”章节最容易被写成一堆空话,比如直接抄“SpringBoot是当前流行的微服务开发框架”。要写出真正有用的内容,你得把版本为什么这么选、ORM为什么选MyBatis-Plus、接口怎么约定讲明白。这些决策本身就是老师喜欢听的东西。
2.1 版本选型和MyBatis-Plus,理由要能讲给答辩老师听
先聊版本。我自己的推荐是Spring Boot 2.7.x,而不是最新的3.x。原因很实际:2.7.x兼容JDK8、11、17,学校机房和答辩环境大概率还停留在JDK8或11;社区里中文教程、报错解决方案基本都基于2.x,遇到问题时一搜一大把;MyBatis-Plus和很多国产数据库驱动对2.x的适配也最平滑。如果非要用Spring Boot 3.x,就要接受JDK17起步,否则启动直接就报错了,而且老依赖的Maven坐标可能出现各种不兼容。在这个项目上,我不建议追求版本最新。
ORM方面,毕设我默认推荐MyBatis-Plus,不是JPA也不是纯MyBatis。最直接的理由是写代码省事:分页用自带的分页插件,条件查询用LambdaQueryWrapper,代码还能用生成器一次性把实体、Mapper、Service生成出来,你只需要专注于业务SQL。答辩时如果被问到“为什么选它”,你就说:它让开发者把时间花在业务逻辑上,而不是重复的基础增删改查。数据库用MySQL 8.0,字符集用utf8mb4;如果你环境里是5.7,也能跑,但注意不要把utf8mb4_0900_ai_ci写进建表语句,那个排序规则是8.0专属的,5.7不认。JDBC URL里必须带serverTimezone=Asia/Shanghai,否则日期传参可能莫名其妙差8小时。
2.2 核心数据表:不只有订单表,还要有状态日志和检测记录
数据库设计决定了论文第四章的质量。我建议至少建下面这几张核心表:
| 表名 | 核心字段 | 作用 |
|---|---|---|
| sys_user | openid, nickname, avatar, phone, create_time | 用户基础信息,以微信openid为主键逻辑 |
| device_model | brand, model_name, category, base_price, status | 机型库,存基准参考价 |
| estimate_record | user_id, model_id, condition_json, estimated_price, create_time | 用户估价记录,保存估价时的条件快照 |
| recycle_order | order_no, user_id, model_id, estimate_id, final_price, order_status, logistics_no, address_snapshot | 回收订单主表 |
| order_status_log | order_id, from_status, to_status, operator_type, operator_id, remark | 订单状态变更日志 |
| detection_record | order_id, detect_result, images_json, remark, create_time | 平台检测结果 |
| user_address | user_id, receiver_name, phone, province, city, district, detail | 收货地址 |
这里面最值得说明的是两个细节。一是condition_json字段,它存的是用户勾选成色条件后的JSON快照,相当于“估价条件串”。如果把每个条件都拆成独立字段,以后平台想加一个“是否拆修过”的判断,就要改表加列,非常不灵活。用JSON保存,再配合规则因子表去计算,加条件不用动表结构。二是order_status_log状态日志表,很多同学会忽略它,但一旦出现“订单怎么从待确认变成已完成”的争议,这张表能直接查出来是谁、在什么时候、做了什么操作。论文里体现这个设计,明显比单纯一张订单表加分。
2.3 接口文档与统一响应体:联调省心的一半靠约定
小程序端和后端联调时最怕各写各的,字段名和错误码都对不上,来回改很浪费时间。我在动手写页面之前,会先和后端把接口约定敲定:所有业务接口统一以/api开头,按资源划分,比如这些:
| 接口 | 方法 | 说明 |
|---|---|---|
| /api/user/login | POST | 微信登录,code换token |
| /api/model/list | GET | 机型列表,用于估价选择 |
| /api/model/detail | GET | 机型详情 |
| /api/estimate/create | POST | 提交估价条件,生成预估价 |
| /api/order/submit | POST | 提交回收订单 |
| /api/order/list | GET | 订单列表,按状态筛选 |
| /api/order/detail | GET | 订单详情 |
| /api/order/cancel | POST | 取消订单 |
| /api/upload/image | POST | 上传设备图片 |
统一响应体我用了最简单的结构:code、message、data。code=200表示业务成功,401表示未登录或token过期,400表示参数错误,5000开头表示业务失败。全局异常处理用@RestControllerAdvice统一兜底,小程序端只需要在request.js里判断code,不是200就直接弹message,不需要在每个页面里写一堆try catch。这个约定看起来简单,但它是前后端分离项目里最值得写进论文的一段设计。
3. 后端实现的三个关键代码点:登录、估价、订单流转
这一节不打算把所有代码贴出来,只讲三个最容易被问到、也最容易写错的地方。如果你在复现同类项目,这三个点往往耗费最多排查时间。
3.1 微信登录换openid后签发JWT
小程序端调用wx.login拿到临时code,后端拿code去微信服务端换openid,再根据openid查找或创建用户,最后签发token。这里最常见的错误有两个:把code当成openid直接入库;或者后端调用微信接口时没有配置真正的appid和secret。正确流程大致是这样:
@PostMapping("/api/user/login") public Result login(@RequestBody LoginRequest req) { String openId = wxService.code2Session(req.getCode()); User user = userMapper.selectOne(new LambdaQueryWrapper<User>() .eq(User::getOpenid, openId)); if (user == null) { user = new User(); user.setOpenid(openId); userMapper.insert(user); } String token = jwtUtil.createToken(user.getId()); return Result.ok(new LoginVO(token, user)); }签发token我用的是JWT,把userId放进subject,设置一个合理的过期时间,比如7天。后端写一个拦截器,从请求头Authorization里解析“Bearer xxx”的token,把userId放到ThreadLocal里的UserContext,后面的Controller直接取当前用户,非常方便。还要提醒一句:个人开发者的小程序无法直接获取手机号,wx.getPhoneNumber需要企业认证,所以不要让用户卡在手机号授权上,建议在小程序里做一个手动输入手机号的表单,这几乎是毕设项目的必踩坑。
3.2 把估价做成规则配置,而不是if-else
估价逻辑一开始写起来很容易写成if-else:如果屏幕有划痕减80,如果电池不行减120。但等条件加到十几个以后,代码会变得没法看,而且每次调整价格都要重新部署。我的做法是抽一张condition_factor表,字段包含condition_key、label、factor。比如屏幕完好的factor是1.0,轻微划痕是0.95,屏幕碎裂是0.5,电池正常是1.0,电池损耗明显是0.9。计算时读用户提交的condition_json,把选中的因子取出来连乘,再乘上机型表里的base_price,就得到预估价。
public BigDecimal estimatePrice(Long modelId, String conditionJson) { DeviceModel model = deviceModelMapper.selectById(modelId); List<String> keys = JSON.parseArray(conditionJson, String.class); BigDecimal factor = BigDecimal.ONE; for (String key : keys) { ConditionFactor cf = conditionFactorMapper.selectOne( new LambdaQueryWrapper<ConditionFactor>() .eq(ConditionFactor::getConditionKey, key)); factor = factor.multiply(cf.getFactor()); } return model.getBasePrice().multiply(factor).setScale(0, RoundingMode.HALF_UP); }前端展示预估价时,最好不要只给一个固定数字,可以返回一个区间,取估算价的0.9倍到1.05倍,看起来更接近真实平台给用户的“参考价XX-XX”。答辩如果被问“估价依据是什么”,你可以说:基准价来自机型库人工维护,因子来自平台策略,计算过程透明可解释。这比拍脑袋定价格有说服力得多。
3.3 订单状态修改用带条件的SQL,防止乱跳状态
状态机落地最稳的做法,是无论哪个操作,都先用一条带状态的UPDATE试试能不能改成功。拿“用户确认最终报价”举例:
int count = recycleOrderMapper.update(null, new LambdaUpdateWrapper<RecycleOrder>() .set(RecycleOrder::getStatus, "COMPLETED") .set(RecycleOrder::getFinalPrice, request.getFinalPrice()) .eq(RecycleOrder::getId, orderId) .eq(RecycleOrder::getStatus, "WAIT_CONFIRM")); if (count == 0) { throw new BizException("订单状态已发生变化,请刷新页面"); } orderStatusLogService.log(orderId, "WAIT_CONFIRM", "COMPLETED", "USER");这条SQL里有eq(status, "WAIT_CONFIRM"),意思是只有当前状态确实是待确认时,才能改成已完成。如果两个用户同时操作,或者平台管理员和用户同时改单,最多只有一个请求能成功,另一个会收到业务异常提示。这就是乐观锁思想,不用引入分布式锁也能把并发问题处理好。状态日志记录要在同一个事务里写进去,这样日志和状态永远一致。
再补充两个经验。第一,订单状态字段我建议用字符串枚举而不是数字,虽然数字占空间小,但调试时你看到status=3还要去查映射,非常痛苦,直接用WAIT_CONFIRM这类字符串,接口日志和数据库一眼就能看懂。第二,订单号别用数据库自增id,建议生成18位订单号,包含日期和随机数,或者用雪花ID。一来避免暴露平台销量,二来用户找客服报订单号时更好记。
4. 原生微信小程序端:页面结构、请求封装和估价流程落地
标题写的是“原生微信小程序”,这一步很容易被同学理解成“随便用uni-app套一下也行”。但原生的意义不只是技术选型,它直接决定了项目讲起来是否清晰。
4.1 用原生而不是uni-app,到底为了什么
原生小程序指的是直接用WXML、WXSS、JavaScript开发,不套用uniapp或Taro这类跨端框架。选原生在毕设里有几个实际好处:项目结构直观,微信开发者工具打开就是小程序原生目录,不需要理解编译链路;wx.request、wx.login、wx.uploadFile这些API都是微信官方提供,报错信息在网上能搜到大量现成答案;对于只做过网页、没接触过小程序的同学,学习成本反而更低,因为少了一层框架抽象。
目录结构我建议这样设计:
- app.js / app.json / app.wxss
- utils/request.js(网络请求封装)
- pages/index(首页)
- pages/estimate(估价页)
- pages/order-list(订单列表)
- pages/order-detail(订单详情)
- pages/address-edit(地址编辑)
- pages/profile(个人中心)
在app.json里注册全部页面并配置tabBar,tabBar放“首页、订单、我的”三个tab就够了。估价页不做成tab,从首页按钮进入,保持底部导航足够简单。别放五个tab,页面一多,小程序包体积和调试成本都会上来。
4.2 request.js封装与登录态处理
小程序的网络请求没有axios,但wx.request可以封装成一个统一的Promise方法。我的封装思路是:
- 每次请求在header里自动带上Authorization token;
- 如果后端返回code=401,先调用wx.login拿新code,再重新登录换token,然后重放原请求;
- 如果后端返回业务错误码,用wx.showToast统一弹message;
- baseUrl集中放在config.js里,后面换域名只改一个文件。
简易版代码如下:
function request(url, method, data) { return new Promise((resolve, reject) => { wx.request({ url: baseUrl + url, method, data, header: { Authorization: 'Bearer ' + wx.getStorageSync('token') }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data); } else if (res.data.code === 401) { wx.removeStorageSync('token'); handleLogin().then(() => { request(url, method, data).then(resolve).catch(reject); }); } else { wx.showToast({ title: res.data.message, icon: 'none' }); reject(res.data); } }, fail: reject }); }); }这里要提醒一个细节:多个接口同时返回401时,可能会触发多次重新登录,导致token刷新错乱。毕设项目最简单的处理方式是在handleLogin上加一个布尔锁,登录过程中其他请求先等待,登录完成后再依次重放。能做到这一步,小程序端的登录态就算处理得比较完整了。
4.3 估价下单页和订单详情页的实现细节
估价页是整个小程序端的核心页面,结构一般长这样:机型选择用picker组件,数据来自后端/api/model/list;成色条件用一组复选框或自定义卡片,让用户勾选“屏幕有轻微划痕”“边框有磕碰”“电池损耗明显”“功能正常”等;手机号用手动input填写,不用wx.getPhoneNumber;提交按钮先调/api/estimate/create拿预估结果,再调/api/order/submit提交订单。
这里有个新手特别容易掉进去的坑:picker的range如果绑的是对象数组,必须要用range-key指定显示字段,否则页面显示的是[object Object]。联调时我遇到过几次,前端把整个对象传给后端,后端因为取不到model_name而报错。
订单详情页要重点处理状态展示。后端返回的status是WAIT_CONFIRM这种字符串,前端最好有一张statusMap映射表,把状态转成中文和颜色。比如WAIT_CONFIRM显示为橙色“待确认”,DETECTING显示为蓝色“平台检测中”。操作按钮也根据状态动态显示:只有当订单处于待确认状态时,才显示“确认报价”和“拒绝报价”两个按钮;检测中状态只显示提示文字。下拉刷新这个小功能也别忽略,在页面json里开启enablePullDownRefresh,onPullDownRefresh里重新请求列表数据,再调wx.stopPullDownRefresh,体验会很不一样。
5. 论文(LW)写作示例:从一个完整毕设文档的角度串一遍
很多同学代码写完了,却栽在论文上,被导师批“像流水账”。二手回收系统题材其实很适合写成一篇有逻辑的毕设论文,只要把章节比重分配好,再知道哪里容易写得空,就能避坑。
5.1 论文大纲与章节比重分配
毕设论文一般从开题报告开始,完整文档大概1.5万字到3万字。二手数码回收系统的论文大纲可以这么安排:
- 第一章 绪论(10%):背景、国内外回收平台现状、课题意义、本文主要工作
- 第二章 相关技术介绍(15%):SpringBoot、原生微信小程序、MyBatis-Plus、MySQL
- 第三章 系统需求分析(20%):可行性分析、业务流程、功能需求、非功能需求
- 第四章 系统设计(20%):总体架构、功能模块设计、数据库设计、接口设计
- 第五章 系统实现(20%):典型页面和关键模块实现,配截图和关键代码
- 第六章 系统测试(10%):测试环境、功能测试用例、测试结论
- 结论、参考文献、致谢
论文被批“只有功能罗列”的根本原因,是需求分析没做透。你需要写清楚谁在用这个系统、他遇到什么问题、系统要解决哪些功能,再去画用例图。不要一上来就贴代码。
5.2 需求分析、用例图、流程图该怎么画才不会被质疑
用例图只需两个参与者:用户和平台管理员。用户用例可以画:微信登录、查看机型、设备估价、提交回收订单、查看订单列表、确认最终报价。管理员用例可以画:后台登录、机型管理、订单管理、检测录入、报价审核。图画出来后,正好和数据库表、后端接口一一对应,论文前后能对上。
流程图我建议至少画两张:一张是“用户回收主流程”,从选择机型到最后打款;另一张是“订单状态流转图”。画的时候用泳道图或者普通流程图都可以,重点是箭头方向清晰、状态名称和代码里一致。我尤其建议在论文里画出“估价计算流程”:用户选择机型 -> 查询基准价 -> 读取用户勾选条件 -> 匹配因子 -> 相乘得到初始价 -> 生成区间 -> 保存估价记录。这个图把业务规则可视化,在系统实现章节里再对应代码,整个论文的完整性就出来了。
5.3 系统测试部分的用例表格怎么写
系统测试是最容易水但也最容易加分的地方。不要只写一句“经过测试,系统功能正常”,要给出测试用例表,表头至少包含:测试编号、测试项、前置条件、操作步骤、预期结果、实际结果。示例:
| 测试编号 | 测试项 | 前置条件 | 操作步骤 | 预期结果 | 实际结果 |
|---|---|---|---|---|---|
| TC001 | 用户登录 | 后端已启动 | 点击微信授权登录 | 页面显示用户头像和昵称 | 通过 |
| TC002 | 设备估价 | 机型库存在iPhone 14数据 | 选择机型,勾选“屏幕有划痕”,点击估价 | 返回预估价区间,区间小于基准价 | 通过 |
| TC003 | 确认最终报价 | 订单状态为待确认 | 用户点击确认报价 | 订单状态变为已完成,生成状态日志 | 通过 |
每个模块放3到5个用例,最后写一句概括性结论:系统核心功能均已通过测试,满足需求分析中定义的功能需求。这就够了。
6. 跑通全流程的部署步骤与避坑清单
最后这部分是实操干货。我先给一套能够一次跑通的启动步骤,再把我实际开发中遇到的几个坑列出来,希望对正在复现的人有直接用。
6.1 本地环境准备与启动步骤
需要准备的东西:JDK8或11、Maven3.6以上、MySQL8或5.7、IDEA、微信开发者工具。项目核心功能都落在MySQL上,Redis不是必须的,先不加也能跑。
启动顺序:
- 新建数据库recycle_db,导入init.sql,里面包含建表语句和机型、条件因子、banner等初始化数据。
- 用IDEA打开SpringBoot后端项目,修改application.yml里的数据库用户名、密码,端口默认8080。
- 运行启动类,看到Tomcat started说明后端启动成功。
- 用微信开发者工具导入小程序端代码,在config.js里把baseURL改成http://localhost:8080。
- 在开发者工具“详情 -> 本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”。
- 编译小程序,在模拟器里测一遍估价、下单、查看订单流程。
如果后端部署到云服务器,小程序正式环境对request合法域名要求很严,必须是HTTPS域名,不能用IP。个人开发者在毕设阶段一般也拿不到正式线上配置,所以本地开发小程序工具勾选“不校验合法域名”是最常见的做法。
6.2 实际开发中容易踩的坑
这次开发里我真实遇到的几个问题,逐个记录一下:
微信开发者工具默认不允许访问http://localhost,不勾选校验的话请求直接fail。但勾选只在开发者工具里有效,真机预览时还是要配置合法域名,或者用局域网IP进行真机调试。手机访问局域网IP时,后端启动地址要绑定0.0.0.0,并且手机和电脑必须在同一个Wi-Fi下。
数据库datetime和前端JSON时间格式经常不匹配,报JSON parse error。我在后端统一返回Long时间戳或者yyyy-MM-dd HH:mm:ss字符串,前端展示时再转格式,并且设置一个全局Jackson配置,避免每个VO手动加@JsonFormat,省了很多重复劳动。
上传设备图片后静态资源404是最容易让人懵的问题。原因通常是没有把本地上传目录映射成URL。解决方法是写一个WebMvcConfigurer,用addResourceHandlers把/upload/**映射到file:上传路径。之后小程序端用完整地址http://localhost:8080/upload/xxx.jpg访问图片才能正常显示。
微信小程序获取手机号要企业主体认证,个人开发者直接调用wx.getPhoneNumber会报权限错误。毕设项目里一定不要依赖这个接口,让用户手动填写手机号即可。这条在不少教程里都不提,但实际能卡你一天。
还有tabBar页面不能直接传参。从首页跳订单详情不要尝试在tabBar页面url后面拼query参数,要么用全局变量,要么把订单详情页设成非tab页面。这个小问题排查起来也很烦,提前知道能省不少时间。
6.3 答辩或者面试时要怎么讲这个项目
如果只是说“我用了SpringBoot和小程序,做了一套回收系统”,等于什么都没讲。我建议按这个顺序讲:
- 先讲业务背景:二手回收市场存在,核心痛点是信息不透明、估价标准不统一。
- 再讲你的解决方案:用户端估价透明化,平台端标准化检测,后台管理统一运营。
- 然后讲技术架构:SpringBoot提供REST API,原生小程序消费API,MySQL存数据,MyBatis-Plus做ORM。
- 展示两个亮点:估价规则配置化,订单状态日志表可审计。
- 最后诚实说明边界:如果真实生产环境使用,还要引入缓存、对象存储、消息队列,当前项目本地文件上传已满足毕设需求。
被问到“为什么不用Redis做缓存”这类问题时,可以这样回答:毕设数据量不大,MySQL索引优化已经足够,Redis作为可扩展方案在项目里预留了对接空间。诚实比硬吹要好得多。
最后说点个人体会。做完这个项目最大的收获,不是多会了几个框架API,而是意识到一个系统能不能让人听懂,取决于你把业务逻辑讲清楚的能力。二手回收这个题目之所以值得做,就是因为它要求你先理解真实行业里的角色、流程、价格规则,然后才能把代码写好。如果你正在做这个题目,我建议动手前先在纸上画出订单状态机和估价规则表,哪怕画得歪歪扭扭也没关系,把这两样东西定下来,后面写代码和写论文会顺非常多。这个小习惯,我后来用在好几个项目上都成立。祝你的毕设一次过。