
在校园场景里待过的人都知道宿舍水管漏了、教室灯管坏了、实验室设备出故障这些事看起来不大但一旦报修流程走不顺就能把人折腾够呛。传统方式无非是打电话、填纸质单子、在群里吼一嗓子要么信息对不上要么进度全靠问要么维修师傅到了现场才发现带错工具。我参与开发的桃李园速修系统就是用微信小程序做前端入口、SpringBoot做后端服务把报修这件事从“人肉流程”改造成“标准化流水线”。这篇文章就把这个项目的完整思路、技术选型、核心实现和踩坑记录都摊开来讲希望对正在做类似校园报修、园区工单系统的朋友有帮助。这个小程序解决的问题其实很集中报修入口统一、维修进度可视、派单逻辑可管理、服务评价可追溯。适合谁来参考两类人最合适一类是学校的信息中心、后勤管理人员想用低成本方式提升报修效率另一类是Java后端开发者、全栈学习者想看看SpringBoot如何和小程序端配合做出一个真实可落地的双端系统。1. 项目整体设计与场景拆解1.1 “桃李园速修”到底在修什么标题里的“桃李园”是一个典型的校园园区场景可以理解为学校的宿舍区、教学区、办公区也可以扩展到企事业单位的园区物业。核心业务非常垂直用户端学生/教职工发起报修维修端师傅接收工单并处理管理端后勤管理员负责派单、监督和统计。我一开始拿到需求时第一反应是这系统听起来简单无非就是“提交报修单-师傅维修-确认完成”但真正拆开后发现里面的门道比预想多得多。首先是角色权限学生和老师能看什么、师傅能接什么单、管理员能改什么状态这套权限模型如果不在一开始定清楚后期会改到怀疑人生。其次是报修类型分类水电、门窗、网络、设备不同类别对应不同工种的师傅这在派单逻辑里直接决定工单能不能被正确流转。所以项目整体拆成了三条线用户报修线、工单处理线、管理统计线。用户报修线负责小程序端的登录、提交、进度查看、评价工单处理线负责派单策略、接单确认、维修记录、完成验收管理统计线负责看板报表、维修时效分析、师傅绩效。三条线数据都围绕一张核心表——报修单repair_order展开状态流转是整张表的生命线。1.2 为什么偏偏选微信小程序加SpringBoot选微信小程序而不是独立App原因很直接用户零安装成本。校园场景下让上千个学生去应用商店下载一个专用于报修的App这个推广成本根本不现实。微信小程序扫码即用、用完即走还能通过公众号推送模板消息通知进度这刚好匹配“低频但刚需”的报修场景。选SpringBoot则是Java生态的稳妥选择。项目要对接校园统一身份认证、要写稳定的后台管理接口、要考虑后续扩展成微服务体系SpringBoot的成熟度、周边生态、招人成本都是优势。尤其是Spring Boot 2.7之后对WebFlux、GraalVM原生镜像的支持越来越好即使以后要做高并发改造也有足够的弹性空间。我见过很多团队在这个环节纠结“为什么不直接用云开发的微信小程序”也就是腾讯云开发的Serverless方案。说实话云开发确实能让前端同学独立完成全栈开发但如果学校已有自己的服务器、已有统一认证和数据库规范自建SpringBoot后端反而更容易融入现有IT体系。另外维修工单涉及师傅端、管理端多个角色如果全塞在小程序云函数里后期逻辑维护成本会明显上升。这个项目选择SpringBoot本质上是为了后端的可维护性和权限边界清晰。2. 核心技术选型与项目初始化2.1 SpringBoot版本、ORM和数据库选择的实战考量当前项目我用了Spring Boot 2.7.18没有盲目追新上3.x原因是3.x基于Jakarta EE规范一些老牌依赖比如某些报表组件、工作流引擎在迁移期可能存在兼容性坑。对于校园维修系统这种追求稳定性的业务2.7这个版本能Cover住绝大多数需求而且后续升级到3.x也有清晰的官方迁移指南。ORM层我选择了MyBatis-Plus。如果你是个追求开发效率的人这个选择会非常舒服。报修单的CRUD、分页查询、条件构造器MyBatis-Plus基本能做到零SQL完成哪怕后期要搞多租户隔离它也有内置的拦截器方案。配合Druid连接池做监控数据库层面的运行状态一目了然。数据库方面就是MySQL 8.0没必要上什么分布式数据库。我见过稍微复杂点的系统就盲目引入分库分表、搜索引擎的案例结果业务量根本撑不起这些组件的运维成本。桃李园速修这种场景核心表的日增数据量撑死几百条MySQL单库单表完全够用反而最简单的架构最不容易出问题。2.2 小程序端原生还是UniApp这是所有做小程序项目都会纠结的问题。桃李园速修最终选择了原生微信小程序原因是项目角色少、页面不算多、没有跨端硬需求。原生小程序在性能、调试体验、微信API调用尤其是订阅消息、获取手机号、定位方面都有天然优势踩坑资料也比跨端框架更丰富。当然如果学校后续要求同时出支付宝小程序、抖音小程序UniApp的跨端能力就值回票价了。但跨端框架在遇到复杂原生组件、地图组件、蓝牙组件时往往需要写条件编译处理维护成本并不低。我的个人建议是明确当前核心场景只需要微信端就用原生不确定未来跨端需求的先用一个H5页面顶上别提前为想象中需求买单。2.3 项目目录结构与初始化步骤后端工程结构我按DDD的轻量变体来组织没有强行套用复杂的分层模式但清晰的边界让后期迭代很省心taoliyuan-server/ ├── common/ # 通用返回体、异常处理、工具类 ├── config/ # 全局配置拦截器、跨域、MyBatis-Plus配置 ├── controller/ # 接口层只做参数接收和结果返回 ├── service/ # 业务层核心逻辑全部在这里 ├── mapper/ # 数据访问层继承BaseMapper ├── entity/ # 数据库实体 ├── dto/ # 入参出参对象不做直接透传 └── utils/ # JWT工具、二维码生成、时间处理等小程序端的目录相对简单但也别有讲究miniprogram/ ├── pages/ │ ├── login/ # 登录页 │ ├── home/ # 首页报修入口、公告轮播 │ ├── order/ │ │ ├── create/ # 创建报修单 │ │ ├── detail/ # 报修详情 │ │ └── list/ # 报修记录列表 │ ├── worker/ # 维修师傅工作台 │ └── mine/ # 个人中心 ├── components/ # 自定义组件工单卡片、图片上传等 ├── utils/ │ ├── request.js # wx.request统一封装 │ └── auth.js # 登录态管理 └── app.js一个关键经验controller层不要直接返回entity实体一定要用DTO做隔离。比如报修单实体里有内部备注字段这个字段是给管理员看的不能返回给普通学生如果用实体直接透传迟早会出数据越权的事故。3. 数据库设计与核心表结构3.1 五张核心表的设计逻辑整个系统的数据核心是报修单表围绕它展开的是用户表、派单记录表、维修记录表、评价表。这五张表基本能Cover完整条业务链路。用户表sys_user的关键字段包括openid微信唯一标识、unionid多端绑定用、role枚举值STUDENT/WORKER/ADMIN、name、phone、avatar、building、room。这里值得注意student和worker不能分开建表因为校园场景下身份可能互换比如后勤老师既是管理员也可以自己提交报修单表加角色字段才是最灵活的方案。报修单表repair_order的核心字段字段名类型说明idbigint主键order_novarchar(32)订单号业务展示用user_idbigint报修人IDcategoryvarchar(20)报修类别electric/water/network/equipmenttitlevarchar(100)报修标题descriptiontext详细描述imagesjson图片URL列表最多9张addressvarchar(255)具体位置如“3号楼205室”statustinyint0待派单 1已派单 2维修中 3待验收 4已完成 5已取消 6已退回worker_idbigint当前接单师傅IDprioritytinyint优先级1低 2中 3高create_timedatetime创建时间update_timedatetime更新时间finish_timedatetime完成时间派单记录表和维修记录表都是报修单的衍生数据记录操作轨迹。评价表挂在报修单下包含评分1-5和评论内容用户只有状态变为“已完成”后才能评价这是后端校验的硬性逻辑。有个容易忽略的点是status字段的每次变更都要记录到一张log表。后期做统计报表、排查“为什么某个工单被卡住了”时没有操作日志几乎等于瞎子摸象。这个坑我在早期版本踩过后来补上了order_log表所有状态变更走AOP统一记录排查效率提升了不止一倍。3.2 状态机设计从提交到完成的状态流转报修单的状态流转是整个系统最容易写乱的地方我之前见过同行直接在service层写一堆if-else判断状态后来状态一多就完全失控。桃李园速修从一开始就用枚举配合状态机模式管理public enum OrderStatusEnum { PENDING_ASSIGN(0, 待派单), ASSIGNED(1, 已派单), REPAIRING(2, 维修中), PENDING_ACCEPT(3, 待验收), COMPLETED(4, 已完成), CANCELLED(5, 已取消), REJECTED(6, 已退回); private final int code; private final String desc; }合法的状态流转我定义在一个Map里后端接口在执行业务操作前先校验当前状态是否允许跳转否则直接抛异常private static final MapInteger, ListInteger TRANSITIONS new HashMap(); static { TRANSITIONS.put(0, Arrays.asList(1, 5)); // 待派单 - 已派单/取消 TRANSITIONS.put(1, Arrays.asList(2, 5, 6)); // 已派单 - 维修中/取消/退回 TRANSITIONS.put(2, Arrays.asList(3, 5, 6)); // 维修中 - 待验收/取消/退回 TRANSITIONS.put(3, Arrays.asList(4, 6)); // 待验收 - 已完成/退回 TRANSITIONS.put(4, Collections.emptyList()); // 终态 TRANSITIONS.put(5, Collections.emptyList()); // 终态 TRANSITIONS.put(6, Collections.emptyList()); // 终态 }这套状态机的核心好处是业务流程的规则收敛到一处新增需求时只需要改TRANSITIONS不需要去翻各个service方法。比如管理员想支持“已完成订单重新打开”只需要在4的流转列表里加上2再补充重置条件的逻辑改动成本极低。4. 后端核心接口实现与细节4.1 小程序登录与JWT会话管理小程序登录流程是后端第一个要接的接口。用户在wx.login()拿到code后传给后端后端拿着code加上appId和appSecret去微信的jscode2session接口换openid和session_key。openid是用户的唯一标识但绝对不能把openid明文返回到前端否则任何人都能伪装他人身份。我的做法是后端拿到openid后去用户表查询或创建用户然后生成JWT返回给前端。JWT的有效期设成7天过期后前端拦截到401状态码后引导用户重新走一遍静默登录。这里有一个实操经验session_key不要传到前端哪怕前端说要用来解密手机号也应该让前端把encryptedData和iv传回后端由后端用session_key解密。这样即便小程序的session_key泄露也不会直接导致用户数据被第三方解密。登录接口示意PostMapping(/wx/login) public ResultString login(RequestBody WxLoginRequest request) { // 1. 调用微信接口获取openid和session_key WxSession session wxService.code2Session(request.getCode()); // 2. 查询或创建用户 User user userService.findOrCreate(session.getOpenid()); // 3. 生成JWT附带用户ID和角色 String token jwtUtils.generateToken(user.getId(), user.getRole()); return Result.success(token); }4.2 报修单提交接口的完整链路提交报修单是整个系统的核心入口也是数据校验点最多的接口。前端用户填写标题、描述、选择类别、上传图片、定位地址最后点提交。后端的处理链路可以拆成四步第一步是参数校验标题不能为空、描述长度要大于10个字、图片最多传9张且每张不能超过2MB。这些校验不只是前端做后端必须再做一遍因为防的就是有人绕过前端直接调接口。第二步是订单号生成。订单号格式我定成“TBY年月日5位随机数”比如TBY2024121800037。这里不建议直接用数据库自增ID给用户看一个是有业务语义的订单号在后端日志里更容易检索另一个是为了防止用户通过ID猜测系统订单总量。第三步是保存主单并初始化状态。我用的策略是首次提交时状态直接置为待派单0同时向管理员推送一条订阅消息告知有新报修单待分配。第四步是自动派单逻辑。这不是必须的但如果园区足够大、师傅足够多人工派单效率就低了。桃李园速修实现了简单的自动派单策略根据报修类别匹配对应工种的师傅选择当前待处理工单数最少、且离报修地点最近的师傅距离通过报修地址和师傅常驻楼栋的预先关联关系计算。这个策略不算智能但没有引入地图SDK的复杂度在小规模场景下已经够用。4.3 图片上传方案本地存储还是对象存储图片上传是报修系统的高频操作也是后端最容易忽视的瓶颈。桃李园速修的图片方案一开始选的是本地存储把图片存在服务器磁盘上nginx做静态映射。但很快发现几个问题服务器磁盘空间有限图片积累几个月就满了如果以后部署几台服务器做负载均衡本地图片在这些节点间不同步会出现图片加载404。后来切换到阿里云OSS前端通过后端签名的临时凭证直传OSS后端只负责生成上传凭证和维护图片URL映射关系。为什么不让后端统一接收图片再传到OSS因为图片经过后端转发会有一次额外的带宽消耗高并发时后端会成为瓶颈直传可以让小程序把图片同时传到OSS后端只做凭证管理两边互不影响。实现上后端提供一个获取上传凭证的接口返回OSS的临时访问凭证和文件路径前端拿到后直接走OSS的上传SDK。整个过程对用户无感上传速度也比经后端中转快。4.4 消息通知订阅消息替代传统的模板消息微信小程序不能随意给用户推送消息必须用户在特定行为发生时主动订阅且一次订阅只能换一次推送机会。这就导致了一个很现实的问题学生提交报修单时要引导他订阅“进度通知”或“维修完成通知”每次订阅获取一次推送额度。我踩过的坑是在提交报修表单页做一个开关“接收进度通知”默认勾选用户提交时一次性订阅两个消息模板。这听起来没问题但微信规定订阅消息的授权弹窗必须由用户点击触发如果用户在页面加载时直接调用wx.requestSubscribeMessage会提示“需要在用户点击事件中调用”导致授权失败。正确的做法是把订阅请求绑定在“提交按钮”的点击事件里点击提交时先请求订阅授权然后再调后端提交接口。另外一个细节是如果用户之前已经授权过同一模板再次调用不会弹窗而是直接成功所以不用担心用户每次都要点一次授权弹窗。5. 小程序端关键实现与交互细节5.1 请求封装与登录态处理小程序端的网络层如果不做统一封装每个页面都写wx.request后期维护会非常痛苦。桃李园速修里做了一个request.js工具重点解决三个问题自动附加token、统一处理错误码、多个并发请求失败时只弹一次错误提示。const request (url, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method, data, header: { Content-Type: application/json, Authorization: getToken() }, success: (res) { if (res.statusCode 401) { // token过期重新登录后再发一次请求 relogin() .then(() request(url, method, data)) .then(resolve) .catch(reject); return; } if (res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail: (err) reject(err) }); }); };登录态的处理逻辑是我觉得最值得展开的部分。小程序冷启动时先检查storage里的token是否存在且未过期过期了就调wx.login重新静默登录。但这里有个坑多个请求同时遇到401如果每个都触发relogin会并发调多次wx.login和code2Session虽然微信不会报错但会造成重复建用户和大量无效token。我用了一个简单有效的办法用一个isRelogining标志位和promise队列。第一个401请求触发relogin时置标志位为true后续请求等待同一个登录Promise完成再带着新token重放。这种做法代码量不多但直接避免了并发登录问题。5.2 报修单创建页面的用户体验设计创建报修单页面是用户接触最多的界面这里的交互直接决定用户愿不愿意认真填写。我做了几个细节优化地点选择用了微信的chooseLocation接口用户可以直接在地图上选点或者手动输入楼栋房间号。这里有个问题使用chooseLocation需要在小程序后台申请接口权限有些个人主体小程序申请不了。所以我做了一个降级方案检测到接口不可用时自动退化为手动输入城市和详细地址。图片上传组件用的是wx.chooseMedia一次最多选3张连续拍照或从相册选择都能支持。选完图片后前端要做压缩因为小程序上传原图在弱网环境下很容易超时。压缩策略是图片最长边控制在1280像素以内质量压缩到80%一张2MB的图压缩下来通常只有200KB左右上传速度体验提升非常明显。提交按钮做了防重复点击在提交请求发出后按钮变成loading状态且禁用。后端在创建订单时也做了幂等校验同一用户一秒钟内重复提交的请求后端的防重拦截器会直接拦截。双保险是为了对付那些手速过快的用户。5.3 工作台页面维修师傅的接单视角师傅端的工作台和用户端是两个完全不同的界面范式。用户端强调的是填写表单的顺畅师傅端强调的是待办清单的清晰和操作的高效。师傅的首页是一个工单列表顶部是筛选Tab待接单、维修中、已完成。工单卡片上要突出显示的关键信息是报修类别用不同颜色标签区分、地址、优先级高优先级用红色感叹号标识、提交时间。师傅最关心的是“我要去哪修、修什么”而不是订单的详细描述。接单操作做了双重确认点“接单”后弹出确认框再次点击才真正调后端接口。为什么多这一步因为师傅在地铁上、骑车时容易误触一旦误接单又取消会影响系统里的评分。后端在接单时还会校验师傅角色是否正确、工单是否还在待派单状态。维修中状态师傅可以拍照上传维修前后对比图这个功能在验收环节可以被管理员审核也算是一个服务留痕的证据链。这里有一个运营层面的小技巧在维修完成弹窗里引导师傅选择“维修耗时区间”后端根据这个数据做师傅的工作量统计比单纯记录完成时间更准确也能避免师傅忘记记录开始维修时间的问题。6. 常见问题与排坑记录6.1 小程序登录态失效的排查经历项目上线后收到用户反馈用着用着页面突然“卡住”操作没有任何反应刷新后又正常。排查发现是token过期后重新登录的promise队列没有catch住异常导致多个请求挂起页面一直处于等待状态。修复方案是给重放请求加一个失败兜底如果relogin后重放请求依然401就不再无限循环直接清空token并跳转登录页。另一个登录相关的坑是本地开发者工具和真机的表现不一致。开发工具里logout重进小程序很快但真机上有时会出现wx.login的code长时间不返回的情况尤其是网络不稳定或者用户系统时间不对时。解决办法是在wx.login外面加一层5秒的超时控制超时后提示用户检查网络而不是一直白屏。6.2 订阅消息下发失败的坑订阅消息在测试阶段一切正常上线后就经常发不出去错误提示是“用户拒收”。排查下来发现原因很有意思用户在“提交报修单”这个行为里订阅的是“进度通知”但我们把这个订阅额度用在“订单已完成”时发送通知由于用户已经看过了同类型的进度通知几次微信会根据用户的消息互动频率调整推送策略导致部分用户被系统判定为低活跃用户而拒收。调整方案很直接不再给用户主动推营销类消息只推与他本人强相关的操作结果类消息比如“您的报修单已被师傅接单”“维修已完成待验收”。这类消息和用户行为有明确的蝴蝶效应触达率明显回升。6.3 并发场景下重复派单的修复上线初期出现了一个比较严重的问题管理员在后台点击“派单”时手速快了连点两次一条工单被派给了两个师傅。虽然状态机校验了“已派单状态不能重复派单”但连点两次的第一个请求还没来得及提交事务第二个请求就已经通过校验了——并发读取到的都是旧状态。解决思路是通过数据库层面的条件更新来保证原子性UPDATE repair_order SET worker_id #{workerId}, status 1, update_time NOW() WHERE id #{orderId} AND status 0如果更新影响行数为0说明工单状态已经变了这次派单就是无效操作。直接用SQL条件更新代替“查询-判断-更新”这个三步操作既简单又可靠比加分布式锁的效率高得多。类似的并发问题其实在很多业务里都存在我的经验是能通过SQL条件约束解决的就不要优先引入Redis分布式锁能用简单方案解决的事绝不搞复杂。6.4 小程序审核被拒的经验小程序提审被拒是大概率事件桃李园速修第一次提审被打回来原因是“报修进度查询需要登录后才能查看不符合审核规范”。微信的审核规则倾向于允许用户在不登录的情况下看到部分内容纯登录墙的应用很难过审。我的应对方案是首页增加“公开公告栏”展示园区物业的通知信息不要求登录报修单列表页保留需要登录的设定但在未登录时展示一个友好的登录引导页而不是直接弹一个生硬的登录框。调整后第二次提审顺利通过。如果你的小程序有类似的强制登录逻辑建议提前处理好这个体验盲区别在审核环节浪费时间。7. 部署上线与运营观察7.1 服务器部署与HTTPS配置后端部署用的是标准的三件套Nginx SpringBoot Jar包 MySQL。SpringBoot的Jar包通过systemd托管Java环境用的是OpenJDK 17。部署命令和运维脚本非常简单但有几个细节值得提一下小程序的request域名必须是HTTPS而且必须在微信公众平台配置合法域名。我在最初部署时图省事直接用IP地址访问后端小程序根本不认后来买了域名配了免费的SSL证书才解决。如果你是个人开发SSL证书可以用阿里云/腾讯云的免费证书一年一换成本为零。多环境配置也是上线前必须做的事情。我在SpringBoot里配置了三套环境dev本地开发、test测试环境、prod生产环境通过profile激活。数据库连接、Redis地址、OSS配置都放在各自的application-xxx.yml里部署时用--spring.profiles.activeprod指定环境。这个配置虽然简单但能避免开发人员在本地调试时不小心连上生产数据库酿成大祸。7.2 上线后的真实数据表现桃李园速修上线三个月累计收到报修单1800多单日均20单。高峰期集中在开学季的9月和冬季供暖季这两个时间段的报修量是平时的两倍以上。从报修类别看网络问题占比最大达到了35%这其实符合预期校园里的Wi-Fi不稳定和学生宿舍路由器设置问题是最常见需求水电和门窗维修占比差不多在25%左右设备类的占15%。从时效数据看从报修提交到师傅接单的中位时间是半小时左右师傅到场和提交完成之间的中位维修时长为4小时整体服务效率用户满意度评分在4.2分5分制。这些数据都是上线后通过后台报表自动统计的给后勤管理方提供了非常有价值的改进依据。运营期间我观察到的一个有趣现象是双休日的报修提交量反而比工作日更低。原因倒不是周末没人报修而是周末学生不在宿舍发现问题的概率本身就低。这个规律直接影响了排班策略周末只留必要数量的值班师傅就够了人力可以集中到工作日和开学季。这类数据洞察是纯做交付的系统拿不到的也是自己做运维和运营的价值所在。8. 写在最后做这类系统最值钱的经验如果只让我说一条这个项目最重要的经验我会说技术选型永远要服务于业务场景的复杂度不要为了炫技引入过度设计。桃李园速修这套系统几乎没用上什么“高级”技术Redis主要用来存验证码和热点数据消息推送依赖微信订阅消息并没有引入消息中间件分布式事务更是完全没碰。但整个系统的稳定性、开发效率、后期可维护性反而比很多用了微服务全套架构的项目好得多。第二点经验是业务系统的成败在很大程度取决于用户侧的流程设计。学生报修最怕的是什么是填一堆表单、传一堆图片、还找不到入口。我们在做首页时把“报修”按钮做得大而显眼次要做“进度查询”再往下是“历史记录”。这就是一个典型的高频动作优先的交互设计它看起来简单但真正能坚持这个原则做到极致的团队并不多。最后再分享一个实用小技巧如果你打算在小程序端用订阅消息做状态提醒一定要在测试环境里模拟各种用户权限下的完整流程包括管理员、师傅、学生三个角色的完整链路。我遇到过师傅接单后学生的订阅消息没有发送成功原因是后端给订阅消息传的参数格式和微信要求的data类型不一致这种低级错误如果在开发阶段全链路测试完全可以早发现。桃李园速修还在持续迭代中下个版本计划接入校园统一身份认证让师生直接用学号登录省掉手机号验证的环节。还会做维修知识库把常见故障的处理过程沉淀成图文内容植入到师傅端的工作台里。这一块如果顺手积累起来以后新师傅培训的成本能降低不少。等到做完这几个升级点我再整理一篇进阶版的经验贴把新踩的坑和优化思路都写出来咱们到时候接着聊。