这几年被不少做毕设的同学问过同一个问题选什么题目既能撑起工作量又不至于写到一半想放弃。上门维修服务系统这个选题属于“看着平常做起来有料”的类型——有用户端、师傅端、管理端三类角色有订单状态流转有地图定位和微信生态的天然联动再加上后端主流技术栈springboot整体既不会太简单也不会难到失控。这篇就把我自己做完这个系统后的设计思路、关键逻辑和踩坑记录全部摊开给需要参考的人一份能落地的攻略。先明确这个系统做什么。用户拿起微信小程序发布一条维修预约填写故障类型、地址、期望上门时间。附近的维修师傅看到单子接单后上门处理用户确认完工并评价。这个看起来流畅的服务闭环落到后端就是一张订单在不同状态间的流转以及围绕这张订单的三类角色权限控制。Spring Boot负责提供稳定的RESTful接口微信小程序负责触达C端用户两者通过HTTP数据交互后台再用一个常规管理页面做配置与审计。这篇文章适合谁看准备做springboot加微信小程序毕设的学生、想快速搭建一套“预约服务类”系统原型的开发者以及打算弄一个本地生活服务副业项目的人。下面内容不会贴完整代码但会把我认为真正决定成败的设计决策、表结构、状态机设计和几个低概率高伤害的坑讲透。1. 需求拆分这个系统服务的不是“叫师傅”而是“让流程可追踪”1.1 从打电话报修到线上闭环需求是怎么来的传统上门维修的体验大多数人都有过家里水管漏了翻通讯录找个师傅电话打过去问什么时候能到得到的回答往往是“下午吧”或者“到时候再说”然后用户就陷入被动等待。师傅到了之后报价多少也说不清楚修完也没有一个确认环节事后出了问题想追溯只能靠当时加没加微信。这些问题的根子在于信息不对称核心症结不是“找不到人”而是“没人对整个过程负责”。线上维修系统要解决的从来不是“叫师傅上门”这个动作本身而是把预约、分配、上门、维修、验收、评价这六个环节变成一条可查询、可追踪、可审计的链路。用户能下单能看进度能确认完成能留评价师傅能接单能更新状态能记录工作结果管理员能介入异常订单。这个认知对齐了后面所有设计才有依据。1.2 三类角色的权限边界我把系统角色固定为三类没有额外造一个“派单员”出来。原因是维修服务的核心场景是小规模、同城就近接单人工派单员反而增加复杂度让师傅自行接单更符合实际。三类角色的职责我分得很清用户发布预约单、取消未接单订单、查看订单进度、确认完工、提交评价与投诉。维修师傅浏览可抢订单、接单、更新上门与维修状态、填写故障原因与费用明细。管理员审核师傅入驻、查看所有订单、处理取消申请与投诉、维护维修分类与公告。这个边界直接指导了后面接口权限设计用户接口只查自己的订单师傅接口只操作分配给自己的订单管理端接口全部需要管理员登录标识。初期如果分不清很容易做出“用户能看见别人订单”这种评审一票否决的问题。1.3 编码之前先把订单状态机画死我见过很多同学习惯先建表但状态字段定义全靠写代码时候临场发挥结果就是前端的按钮显示逻辑和后端的校验逻辑各想一套联调时对不上。我的做法是编码之前先定状态链把它当作整个系统的“宪法”待接单 → 已接单 → 上门中 → 维修中 → 待验收 → 已完成 → 已评价异常分支也需要提前定义待接单状态用户可取消订单直接进入已取消已接单状态师傅可发起取消申请管理员审核后订单变为已取消师傅在接单前可直接拒单订单回到待接单池子。这个状态机一旦定稿后端的status字段取值、接口校验逻辑、前端每个按钮的显隐规则、后台筛选标签就全部对上。看起来只是一个前置设计实际上能省掉后面几天的联调返工。2. 技术选型的取舍为什么是Spring Boot加微信小程序2.1 Spring Boot到底赢在哪很多人选技术栈其实是“大家都用所以我也用”但答辩时候老师问一句“为什么不用Spring MVC”就卡住了。Spring Boot能成为Java后端项目的默认选择不是因为名字新而是因为它把集成成本压到了最低。拿这个维修系统举例需要的能力无非是Web接口、数据库访问、登录鉴权、定时任务和HTTP调用微信接口这些在Spring Boot里分别对应spring-boot-starter-web、MyBatis-Plus、JWT工具、Scheduled和RestTemplate一个Spring Boot工程全部覆盖不需要额外配置复杂的XML。对比一下更重的方案如果上Spring Cloud微服务需要注册中心、网关、配置中心对一个小型维修平台来说是纯粹的负担如果退回SSH框架各种XML配置又会浪费大量时间在非业务问题上。毕业设计的评价要点往往是“逻辑完整、能跑通、能讲清楚”Spring Boot恰好在这个位置提供了最好的平衡。2.2 小程序端为什么比App省心我曾经计算过如果做成App至少要维护Android和iOS两套包还要处理应用商店审核、版本兼容、推送通道差异一个毕业生很难在一个学期内搞定。微信小程序天然解决了这些问题一套代码同时跑在微信生态内用户扫一扫或搜一搜就能用不用安装对开发者而言认证、域名配置、发布审核都有成熟流程。还有一个容易被忽视的点维修服务的用户群体非常广泛包括不太会装App的中老年人。小程序在微信里被用过一次之后会留在最近使用列表下一次打开成本极低而且可以顺手转发给家人。这种“用完即走、需要时即回”的属性和上门维修的低频但刚需特征非常匹配。2.3 版本与环境的取舍别用最新版折磨自己Spring Boot的版本选择是我强烈建议新手注意的。不要一看到官网最新版就往上冲尽量选择2.7.x或3.1.x这类已经被生态充分适配的稳定版本。原因很现实MyBatis-Plus、微信支付SDK、某些工具包对太新版本的兼容性还没有被大量验证出了问题想要搜索解决方案都费劲。环境组合我推荐最简单的一套组件推荐版本原因JDK1.8或17对应Spring Boot 2.7或3.x不要混用MySQL5.7或8.0功能稳定资料多Redis选装缓存非核心时间紧可以不用Maven3.6以上管理依赖避免手动导jar包Redis在这里不是必需品。如果项目周期只有两三个月完全可以用数据库字段加索引的方式实现轮询、统计等需求先跑通主流程再考虑用Redis去优化。2.4 后端工程结构让答辩老师一眼看懂分层工程结构要能体现分层思想。我的recommend后端包结构如下repair-server ├── src/main/java/com/example/repair │ ├── controller # 接口层接收参数、返回结果 │ ├── service # 业务层状态流转、权限校验 │ ├── mapper # 数据访问层MyBatis接口 │ ├── entity # 实体类对应数据库表 │ ├── dto # 请求/响应对象 │ ├── config # 全局配置跨域、Jackson、拦截器 │ ├── utils # 工具类JWT、订单号生成、日期处理 │ └── common # 统一返回结果、全局异常 └── src/main/resources ├── mapper # MyBatis XML └── application.yml这个结构中controller只做参数接收和结果包装service层承载状态机和业务校验mapper只负责SQL。答辩时如果被问“架构上怎么考虑的”一句话就能讲清楚表现层、业务层、数据访问层职责分离模块间依赖方向是单向的。这种约定让代码审阅者也舒服因为找任何一个业务逻辑路径都是确定的。3. 从需求倒推数据模型表结构才是项目的“地基”3.1 整体表清单与关系定位很多同学一上来就建十几张表其实大部分用不上。这个系统的核心表我控制在了七张保证覆盖主流程又不冗余表名作用user用户基础信息存微信openidmaintainer维修师傅信息关联user表repair_category维修分类如家电、水电、管道repair_order服务订单主表order_status_log订单状态变更流水evaluation订单评价complaint投诉记录地址和常用联系人信息可以放在user表的一个扩展字段里也可以拆成address表。如果做毕设建议拆出来方便展示“一对多”的关系设计。3.2 user和maintainer表的字段细节user表的核心是openid这是微信生态里用户的唯一身份标识。建表时openid必须加唯一索引否则同一用户重复登录会生成多条记录。其次是nickname和avatar这里注意头像昵称的存储路径不要写死本地绝对路径存URL或相对路径部署后才能灵活调整。maintainer表要冗余存师傅的接单数、评分因为每次展示列表都去聚合order表会拖慢查询。具体字段user_id关联user表的登录账户real_name和phone师傅真实信息入驻时填写skill_tags服务类目用逗号分隔字符串或关联分类表stars和order_count冗余统计字段评价完成后更新status审核状态0待审核、1通过、2拒绝。3.3 订单主表与状态流水表为什么必须拆两张repair_order表承载业务主体字段包括order_no业务订单号用时间戳加随机数生成展示给用户user_id发起维修的用户maintainer_id接单师傅未接单时为空category_id维修分类description故障描述address上门地址expect_time期望上门时间status当前状态对应状态机的枚举值amount最终费用decimal(10,2)create_time、update_time时间戳。单独设计order_status_log表是这个系统的一个亮点。它记录每一次状态变更的from_status、to_status、operator_id、operator_type和操作时间。为什么不能只靠订单表里的status字段因为人工审计和后期排查纠纷时需要回答“师傅是什么时候点的上门”“用户什么时候确认完成”这类问题。如果只有当前状态这些历史信息全部丢失。而且答辩时这张表的价值非常容易讲清楚它是订单生命周期审计的数据依据。3.4 金额和时间字段两个看似不起眼的坑涉及钱的字段千万不要用float或double。二进制浮点数无法精确表示小数累计到一定量就会出现0.10.2不等于0.3的问题。订单金额、师傅结算金额都用decimal(10,2)计算时用BigDecimal。时间字段统一用datetimeJava实体里用LocalDateTime配合Jackson的格式化注解输出成“yyyy-MM-dd HH:mm:ss”。时间戳形式虽然省传输量但对小程序端展示不友好而且联调时容易因为时区问题显示偏差8小时后面会专门提到这个坑。4. 微信小程序端的关键实现登录、预约与订单流转4.1 登录获取openid的正确姿势小程序登录流程有一个非常容易踩的误区就是拿wx.getUserProfile返回的用户信息当成登录依据。正确逻辑其实是两步先用wx.login()拿到临时code把code传给后端后端拿着code调用微信的code2Session接口换取openid和session_key然后后端用openid去查user表不存在就自动创建新用户最后给前端返回业务token作为后续请求的身份凭证。// 小程序端 wx.login({ success: async (res) { if (res.code) { const loginRes await request.post(/api/user/login, { code: res.code }); wx.setStorageSync(token, loginRes.data.token); wx.setStorageSync(userId, loginRes.data.userId); } } });头像和昵称另有一套机制微信官方要求必须使用button的open-typechooseAvatar和input的typenickname来收集不能直接静默获取。很多老教程用的wx.getUserInfo已经被划到限制名单里照老教程写基本会在真机调试时撞墙。4.2 发布预约与地址选择本地缓存还是服务器存储发布预约单时需要的地址建议在用户首次保存后传到服务器而不是只放本地存储。原因很简单用户清理微信缓存或换手机后常用地址不应该丢。我在address表里存了经纬度、详细地址、联系人姓名和电话前端通过wx.chooseLocation调起地图选点直接回填经纬度和地址文本比手动输入定位准确得多。下单表单要展示的字段包括维修分类、故障描述、期望上门时间和地址分类数据通过接口从后端拉取不要写死在前端。这样后台更新分类后前端不用发版。4.3 订单列表后端分页前端别忘了去重订单列表如果一次性全量查询数据量大了之后页面会越来越卡而且微信小程序的setData有性能瓶颈。正确做法是后端分页前端配合“首次加载、下拉刷新、触底加载更多”三段式处理。// 小程序端 onReachBottom() { if (this.data.list.length this.data.total) { this.setData({ page: this.data.page 1 }); this.loadOrders(); } }这里有个细节翻页加载时容易把同一条数据重复追加进去尤其是用户操作触发刷新和触底加载同时发生时。稳妥做法是接口返回total前端在加载前判断当前list长度是否已等于total并且每次下拉刷新都重置page为1、清空数组再请求。列表项里的操作按钮也要按订单状态做条件渲染——待接单显示“取消订单”已接单显示“联系师傅”和“取消申请”维修完成后显示“确认完工”和“去评价”。我会维护一个状态到按钮组的映射函数避免在多个页面里复制粘贴同样的if判断。4.4 师傅端可抢订单池与个人工单Tab师傅端的首页是一个待接单的订单池按时间倒序展示所有状态为待接单的订单。这里可以按师傅的服务分类过滤比如师傅只做水电维修就不展示家电类的单子。点击订单详情能看到用户的地址和故障描述并且可以把地址在小程序内直接拉起地图导航。接单动作对应后端的accept接口师傅端页面要保持谨慎接单成功后立即刷新订单池并清掉这条记录。我的工单页用Tab来分组待上门、维修中、待验收、已完成。师傅每次状态更新都走同一个接口只是传入不同的目标状态前端在本地乐观更新UI失败时再回滚体验会流畅很多。5. Spring Boot后端接口设计、状态机与定时兜底5.1 RESTful接口风格一眼看懂的路径设计接口路径按资源命名是后端设计的常规要求。这套系统的主要接口我整理如下方法路径说明POST/api/user/login微信登录获取tokenGET/api/user/info获取当前用户信息POST/api/order创建预约订单GET/api/order/list分页查询订单支持status过滤GET/api/order/detail/{id}订单详情PUT/api/order/accept师傅接单PUT/api/order/update-status订单状态流转POST/api/order/evaluate提交评价POST/api/maintainer/audit管理员审核师傅入驻路径设计要避免动词堆砌比如不要写/api/acceptOrder。管理员操作可以单独挂/admin前缀用拦截器做权限校验普通用户token无法访问。5.2 状态更新的核心实现校验、加锁、写流水订单状态流转是整个后端最核心的业务逻辑不能设计成“前端传一个状态后端无脑更新”。每次状态更新必须做三件事校验当前状态是否允许跳转到目标状态锁定订单行防止并发写状态流水日志。Transactional public void acceptOrder(Long orderId, Long maintainerId) { Order order orderMapper.selectForUpdate(orderId); if (!OrderStatus.WAIT_ACCEPT.equals(order.getStatus())) { throw new BusinessException(订单状态已变化请刷新后重试); } order.setStatus(OrderStatus.ACCEPTED); order.setMaintainerId(maintainerId); orderMapper.updateById(order); statusLogMapper.insert(new StatusLog(orderId, WAIT_ACCEPT, ACCEPTED, maintainerId)); }这里的关键是selectForUpdate。它在事务内给订单行加了行级锁同一时刻只有一个师傅能成功读到待接单状态并修改它。如果不加锁两个师傅同时点击接单就可能把同一个订单分配给两个人这是真正的致命bug。5.3 没人接单怎么办定时扫描超时订单用户下单后如果一直没人接单系统需要有一个兜底机制。我用了Spring的Scheduled注解写了一个定时任务每隔五分钟扫描一次待接单且创建时间超过一小时没有变化的订单自动标记为已取消同时记录取消原因代码。Scheduled(fixedRate 300000) public void autoCancelTimeoutOrders() { ListOrder timeoutOrders orderMapper.selectTimeoutList(LocalDateTime.now().minusHours(1), OrderStatus.WAIT_ACCEPT); for (Order order : timeoutOrders) { order.setStatus(OrderStatus.CANCELLED); orderMapper.updateById(order); statusLogMapper.insert(new StatusLog(order.getId(), WAIT_ACCEPT, CANCELLED, SystemConstant.AUTO_SYSTEM_ID)); } }定时任务是典型的“兜底设计”它的价值在于即使没有人工干预状态机也不会卡死在某个节点。你说这个功能是亮点答辩时可以展开讲scheduled注解、扫描频率设计、状态流水的审计。另外记得给order表加一个(status, create_time)联合索引否则数据量上来之后这种定时扫描会越来越慢。5.4 统一返回结果与全局异常接口别裸奔前后端联调时最痛苦的事情之一就是一个接口返回{code:200,data:...}另一个接口返回{success:true,result:...}前端封装请求时无法统一处理。项目一开始就要约定统一的返回结构我用的格式是{ code: 200, message: success, data: {} }配合全局异常处理器把参数校验异常、业务异常、系统异常分别映射到不同的code。前端拿到非200的统一走同一个错误提示逻辑不用每个接口都写一遍异常处理。这个设计在答辩时也常被当作“工程规范性”的加分点。6. 最容易翻车的几个坑与完整排查思路6.1 跨域问题的真面目很多同学在开发阶段会遇到“request:fail url not in domain list”或浏览器里的CORS报错。小程序真机请求不受浏览器跨域限制但开发者工具的模拟器依然会有CORS约束而且正式环境要求所有请求域名必须配置在小程序后台的合法域名列表里。开发阶段最简单的处理是在后端加一个CorsConfig允许所有来源和常用方法上线前再把域名换成HTTPS并配置合法域名。6.2 微信登录报40029的排查链路code2Session返回40029的经典场景是code无效或已被使用。第一次遇到时我查了半天最后发现是前端在页面onLoad和用户点击登录时各调了一次wx.login第一次的code还没用就被第二次覆盖了。排查思路是先确认每次wx.login生成的code都只在请求里用一次再看后端是否在同一个请求里重复调用code2Session接口最后检查系统时间是否和标准时间偏差过大时间偏差会导致签名校验失败。按这个链条排查基本都能定位。6.3 时间显示偏了8小时后端返回的LocalDateTime如果不做格式化默认序列化出来可能是数组或UTC字符串小程序端直接显示会和人认知的时间差8小时。解决方案是在application.yml里全局配置Jacksonspring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8同时实体类的日期字段加上JsonFormat注解双重保险。只要全局配好就不要在某个单独接口里自己去拼时间字符串否则越改越乱。6.4 师傅抢单的并发冲突抢单并发我之前提过这里讲一个真实测试场景两个师傅同一毫秒点击接单如果接口不加行锁数据库里最终记录可能是最后一个写入者生效但上一个师傅的前端已经提示“接单成功”造成纠纷。加锁之后第二个请求会在selectForUpdate处阻塞或快速失败返回“订单已被接走”。这两种结果都能接受最不能接受的是状态被覆盖。6.5 部署环节的平稳落地部署的时候后端打成jar包推上服务器命令至少用nohup挂后台运行日志输出到独立文件。MySQL用云端数据库或服务器自建都行但注意服务器的安全组要放通3306和8080端口。小程序前端发布前需要在微信公众平台配置服务器域名、业务域名并把后端接口切到HTTPS。整个部署流程建议写进README因为答辩演示现场如果环境没配好系统启动不起来再好的设计也展示不出来。7. 答辩前一轮自查清单每年评审都有一批项目挂在演示环节。我把这类系统常见的减分项整理成一个清单你在交付前可以逐条过一遍用户能否只看自己的订单接口是否校验了userId订单号是否对用户可见且唯一不是数据库自增id裸奔师傅端接单改状态后用户端是否同步刷新取消订单是否有状态限制已完成的订单不能被取消金额展示是否保留两位小数管理端能否看到所有订单的状态流水微信登录失败时前端是否有友好提示接口响应格式是否全局统一这些点单个来看都不复杂但漏掉任何一条都有可能成为评审老师追问的突破口。建议在演示前用测试账号完整走一遍“下单-接单-上门-维修-验收-评价”的happy path再把异常路径也点一遍做到心里有底。就我实际做完这个系统的体验而言最大的心得体会是毕设项目的难点从来不是某个技术点而是把所有环节串起来的整体性。Spring Boot加微信小程序的组合技术门槛不算高但足够让你完整经历一个产品从需求梳理、数据库设计、接口开发到部署上线的全过程。只要状态机定清楚、表结构建明白、并发边界想到位这套系统无论用于毕设还是将来的实际项目都能站得住脚。