简介:基于Java Spring Boot和微信小程序的上门维修系统,源码包含完整的前后端实现,适合需要快速搭建维修服务平台的开发者、毕业设计学生或中小型服务商参考学习。系统提供用户端小程序与管理后台,功能覆盖账号注册登录、维修服务展示、维修订单管理、服务评价、广告信息查看与收藏,以及管理员对用户、维修、评价、广告等模块的增删改查操作。压缩包总计1248个文件,大小约20MB,文件类型涵盖Java后端代码、Vue和JavaScript前端页面、WXML与WXSS小程序界面、PNG和JPG图片素材、SQL数据库脚本等,结构清晰,便于按需查阅。当前已有119人学习,适合用于项目实训、课程设计或二次开发蓝本。资源内附有安装、运行与构建脚本及环境配置说明,能帮助开发者快速理解前后端交互逻辑,显著降低从零搭建同类系统的时间成本。
1. 上门维修系统:Java + 微信小程序这个组合在解决什么
厨房水管漏水的那个周六晚上,你大概率不是打开网页去搜维修公司,而是直接翻微信——找个能马上上门的师傅、看看他的报价和服务评价。这个动作背后,就是一套上门维修系统在承接:用户下单、师傅接单、上门服务、完工结算,每一步都被系统记录和约束。基于Java和微信小程序的上门维修系统,是目前这类项目最常见的选型组合——Java负责订单、用户、结算这些核心业务逻辑,微信小程序省掉安装成本,同时覆盖用户端和师傅端两个入口。这篇文章不是讲PPT架构,而是把一个可运行的源码包拆开:从角色模型、订单状态机、数据库表设计讲到后端接口和小程序页面,最后给你一份能直接照着排错的避坑清单。适合想快速落地同类项目的开发者,也适合拿全栈项目做进阶练习的朋友。
2. 需求拆解与核心模型:角色、订单状态机和数据库表设计
2.1 三个角色与两类小程序端:用户端和师傅端的分工
上门维修系统的角色其实很固定:用户负责下单和付款,师傅负责接单和上门服务,管理员负责师傅入驻审核、订单兜底和结算确认。标题里写着“微信小程序”,常见做法是一个小程序通过登录角色区分身份,而不是拆成两套独立小程序。我参与过的同类项目,大部分都是这个方案——用户和师傅都用微信登录,复用同一套登录链路,管理端做成 Web 页面,小程序只承载用户和师傅两个入口。
从业务动作看,用户侧是发布维修需求、查看订单进度、完工后付款评价;师傅侧是抢单或接单、查看订单详情、开始服务、确认完工、查看自己的结算记录。很多人一开始容易把师傅端设计成一套独立后台,其实师傅端本质上就是一个加了接单开关的订单列表,加上两个状态操作按钮。订单列表负责展示“附近能接的活”,状态操作按钮负责“接单/拒单/开始服务/确认完工”。
这里有个容易漏掉的细节:师傅端要提供“接单开关”和“服务区域”两个配置。师傅下班了或者离开服务区域,就不能再接单,否则一会儿就要被用户投诉“师傅怎么还不来”。把这两个字段直接放在师傅表里,接口查询待接单列表时过滤掉“接单状态为关闭”的师傅,比在订单环节做二次校验简单得多,也少一次无意义的数据库查询。
2.2 订单状态机:一个订单从提交到完成的六个状态
我一般会把订单状态机画清楚再碰代码,这等于给业务留了后悔药——状态边界定清楚,后续加需求、查数据都稳。上门维修系统的订单状态,常见流转是:待接单(0)→ 已接单(1)→ 服务中(2)→ 待付款(3)→ 已完成(4),外加一个任意合法节点可触达的已取消(5)。用户提交订单后进待接单;师傅接单后是已接单;师傅点击“开始服务”进服务中;师傅确认完工后订单变待付款;用户付款并评价后变成已完成。
状态机里最容易翻车的是“取消”和“拒单”。用户只能在待接单和已接单状态下取消,已接单后取消需要记录爽约并通知师傅;师傅在待接单时可以拒单,接单后不能随意取消,否则算爽约。这些规则不能只靠前端按钮显隐来控制,后端每个改状态的接口都要校验“当前状态是否允许发生这次流转”。如果漏了这层校验,用户直接调接口把自己的订单改成已完成,整个结算体系就乱了。
实现层面我推荐用枚举类把状态和允许的流转定义在一处,不要散落在各个 Service 里用 if/else 堆判断。可以定义一个 OrderStatusEnum,每个枚举带当前状态值和允许流转的目标状态集合,Service 里统一走一个 changeStatus 方法。这样做的好处是后面加状态(比如“退款中”)时只需要改枚举,不会出现某个接口漏改导致状态判断不一致。
提示:订单状态字段建议用整数枚举,不要用“待接单”“已接单”这种中文存库。索引、排序、状态机判断都依赖稳定数字,前端展示时再做数字到文案的映射;一旦改了中文文案,所有历史数据的处理都会变成灾难。
2.3 数据库表设计:八张核心表和关键字段
按我拆过类似源码包的经验,这类系统核心表一般在八张左右:用户表、师傅表、服务类型表、订单表、评价表、地址表、结算表、图片表。用户表和师傅表分开,是因为师傅额外有服务区域、接单状态、评分这些字段,混在一起会让用户表的查询变慢,索引也不好设计。
订单表是重中之重,字段设计一句话——存快照,不存关联。服务类型名称、价格、用户地址这些字段在下单那一刻复制进订单表,而不是通过服务类型ID和地址ID去关联查询。原因是商家改价、用户改地址之后,历史订单的展示和结算都必须以当时的记录为准。订单表的核心字段大致如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 业务订单号,对外使用,不用自增id |
| user_id | bigint | 下单用户 |
| master_id | bigint | 接单师傅 |
| service_type_id | bigint | 服务类型ID |
| service_name | varchar(50) | 服务名称快照 |
| price | decimal(10,2) | 价格快照 |
| address | varchar(255) | 服务地址快照 |
| longitude / latitude | decimal(10,6) | 坐标快照 |
| appoint_time | datetime | 预约时间 |
| status | int | 0待接单 1已接单 2服务中 3待付款 4已完成 5已取消 |
| cancel_reason | varchar(255) | 取消原因 |
| cancel_by | varchar(20) | 取消人:user/master/admin |
| accept_time / finish_time | datetime | 接单时间 / 完成时间 |
索引方面,最核心的是订单表 status + create_time 组合索引,因为最常见查询是“用户按时间倒序看订单列表”和“师傅按时间倒序看待接单列表”。不加这个组合索引,订单量到几万条时列表接口就会明显变慢,加完之后基本是毫秒级返回。地址表的经纬度字段建议保留两份:一份是用户选择或填写时的坐标,一份是服务完成后师傅上报的实际地址坐标。这两份数据对售后纠纷定位很有用——用户说师傅走错了门,一对比坐标就知道问题出在谁那边。
2.4 为什么用 MySQL + MyBatis-Plus 而不是更重的方案
标题只写“基于 Java”,没有指定框架,但全套源码最常见的技术栈就是 Spring Boot + MySQL + MyBatis-Plus,管理端偶尔配一个 Vue 页面。选这套的理由很实际:Java 开发者上手门槛低、资料多,MyBatis-Plus 把单表 CRUD 几乎全包了,剩下要手写 SQL 的只有复杂统计和部分关联查询。
上门维修系统的并发量特征很典型——有高峰期(比如周末上午),但整体远远达不到需要上消息队列和微服务的量。单体应用加一个 MySQL 实例在早期完全够用。真正可能成为瓶颈的,是订单量大了之后 order 表的数据膨胀,以及图片上传占用的存储空间,这些是加索引、加存储能解决的问题,不是架构问题。如果一开始就设计成订单服务、用户服务、支付服务三个微服务分开部署,开发成本和运维成本反而会把项目拖死。
如果你是新手,把这个源码当学习项目,我的建议是先不用纠结 MyBatis-Plus 和 MyBatis 的区别,会用 MyBatis-Plus 的 BaseMapper 就够了。优先搞清楚三件事:LambdaQueryWrapper 怎么构建条件查询、分页插件怎么配置、updateById 和条件更新的区别(第 5 章会讲)。这三样在这个项目里占 Dao 层 90% 的代码量,其他的边用边查完全来得及。
3. 后端服务落地:Spring Boot 从项目结构到关键接口
3.1 项目结构与启动入口:拿到源码第一步看什么
拿到一个这样的源码包,第一步不是急着重启,而是把 src/main/java 下面的包结构扫一遍。常见结构是在 com.xxx.repair 包下分 config、controller、service、mapper、entity、utils、dto 这几层。这一步能看出作者有没有做分层,以及业务边界在哪里。如果所有逻辑都堆在 controller 里,那后续改起来会比较痛苦。
配置文件要确认三个关键项:application.yml 里的数据源、微信小程序 appid 和 secret、服务端口。先把数据库连接改成你本地 MySQL 的账号密码,然后在数据库里执行源码附带或文档里提示的 SQL 初始化脚本。确认建表脚本能完整执行之后,再启动启动类。很多新手一上来就点运行,报个连不上数据库的错误就懵了,其实多半是建表脚本没执行。
启动类上常见两个注解:@SpringBootApplication 和 @MapperScan。前者是 Spring Boot 的标配组合注解,后者是告诉 MyBatis-Plus 去哪里扫描 Mapper 接口。如果启动后报“找不到 mapper”或者注入失败,先检查 @MapperScan 的包路径是否写对,十有八九是这个原因。还要看一眼是否加了事务注解 @EnableTransactionManagement,没加的话订单创建这类多表写操作会处于无事务保护状态,一旦中途报错会留下脏数据。
3.2 微信登录与 JWT 鉴权:用户身份的完整链路
小程序端不搞账号密码那套。前端调 wx.login 拿到临时 code,后端拿 code 去微信接口换 openid,再用 openid 查用户表,查不到就自动注册。code 有效期只有五分钟且只能使用一次,所以登录接口的调用链必须是“前端触发 -> 后端换 openid -> 签发 JWT -> 返回 token”。核心代码长这样:
@PostMapping("/auth/login") public Result login(@RequestBody LoginRequest request) { // 1. 用 code 向微信接口换 openid 和 session_key String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid + "&secret=" + secret + "&js_code=" + request.getCode() + "&grant_type=authorization_code"; // Hutool 的 HttpUtil 发起 GET 请求 String response = HttpUtil.get(url); JSONObject obj = JSONUtil.parseObj(response); String openid = obj.getStr("openid"); if (StrUtil.isBlank(openid)) { return Result.fail("微信登录失败: " + obj.getStr("errmsg")); } // 2. 用 openid 查用户,不存在则自动注册 User user = userMapper.selectOne( new LambdaQueryWrapper<User>().eq(User::getOpenid, openid)); if (user == null) { user = new User(); user.setOpenid(openid); user.setRole("user"); userMapper.insert(user); } // 3. 签发 JWT,有效期 7 天,返回给前端 String token = JwtUtil.createToken(user.getId(), user.getRole()); return Result.success(token); }这段代码有三个点值得说。第一,jscode2session 返回的不是用户信息,而是 openid 和 session_key,openid 才是用户的唯一身份标识。第二,openid 不能直接返回给前端存起来,前端只拿 JWT,后续请求在 header 里带 Authorization,防止 openid 泄露后被恶意拼接接口。第三,JWT 过期时间一般设 7 天,小程序端发现接口返回 401 后静默调用 wx.login 重新走一遍登录流程,用户无感知。JwtUtil 内部是用 HMAC 签名还是 RSA 签名,取决于源码用的库,但我建议至少用 HS256,密钥放配置文件里,不要硬编码在类里。
3.3 下单与接单接口:两个核心接口的实现要点
下单接口的核心逻辑是“把订单快照写入订单表”,同时要处理幂等。同一个用户在 30 秒内提交了两次相同服务类型、相同地址、相同预约时间的订单,系统不应该产生两条一样的待接单记录。常见做法是在 service_type_id + appoint_time 上做唯一索引,或者在下单接口里先查一次“是否存在相同条件且状态为待接单的订单”。要求不高的话,用查询拦截就够。
@PostMapping("/order/create") public Result createOrder(@RequestBody OrderCreateRequest req) { // 1. 校验必填参数 if (StrUtil.isBlank(req.getServiceTypeId()) || StrUtil.isBlank(req.getAddressId())) { return Result.fail("请选择服务类型和地址"); } // 2. 查服务类型和地址,组装订单快照 ServiceType type = serviceTypeMapper.selectById(req.getServiceTypeId()); Address addr = addressMapper.selectById(req.getAddressId()); Order order = new Order(); order.setOrderNo(generateOrderNo()); // 时间戳+随机数 order.setUserId(req.getUserId()); order.setServiceTypeId(type.getId()); order.setServiceName(type.getName()); order.setPrice(type.getPrice()); order.setAddress(addr.getDetail()); order.setLatitude(addr.getLatitude()); order.setLongitude(addr.getLongitude()); order.setAppointTime(req.getAppointTime()); order.setStatus(0); // 0 待接单 orderMapper.insert(order); return Result.success(order.getId()); }生成订单号用“时间戳 + 随机数”在单机部署下够用,并发更高时可以用 Redis INCR 或雪花算法,但上门维修场景远没到这个量级。示例代码里 userId 直接取自请求参数,真实项目必须从 JWT 解析,防止用户伪造他人 ID 下单。下单成功返回订单主键 ID,小程序端拿这个 ID 跳转订单详情页。
接单接口比下单更值得小心,基础版本长这样——先查出订单,判断状态,再更新,问题是后面要说的并发:
@PostMapping("/order/accept") public Result acceptOrder(@RequestBody AcceptRequest req) { Order order = orderMapper.selectById(req.getOrderId()); // 核心校验:只能待接单状态下被接单 if (order.getStatus() != 0) { return Result.fail("订单已被接单或已取消"); } Order update = new Order(); update.setId(order.getId()); update.setMasterId(req.getMasterId()); update.setStatus(1); // 已接单 update.setAcceptTime(new Date()); int rows = orderMapper.updateById(update); if (rows == 0) { return Result.fail("接单失败,请重试"); } return Result.success(); }这段代码的问题在于 select 和 update 之间不是原子操作,两个师傅同时进来都会读到 status=0,都以为是自己抢到了。解法我放到第 5 章专门讲,这里先记住一个原则:更新订单状态,永远要带“当前状态”作为条件,而不是先查后改。
3.4 订单状态流转的接口约束:谁有权利改状态
订单状态不是任何接口都能改的。用户能调用的只有下单、取消(限定待接单或已接单状态)、支付确认;师傅能调用的只有接单、拒单、开始服务、确认完工;管理员能做的是兜底关闭异常订单。每一个改状态的接口,第一行代码都应该是权限校验,第二行查当前状态,第三行判断状态是否允许流转。
我见过不少源码项目在状态这块偷懒,把状态更新统一写成一个 /order/updateStatus 接口,前端传什么状态就改成什么状态。这样用户直接调接口就能把自己的订单改成“已完成”,属于严重的安全漏洞。做二次开发时第一件事就要检查这类接口是否存在,有的话立刻拆掉,改成按业务动作拆分的细粒度接口。每个动作一个接口,名称直观,比如 acceptOrder、startService、finishService、cancelOrder,权限校验写在各自接口里,比一个通用接口加一堆判断要安全得多,也好维护。
4. 微信小程序端落地:用户下单与师傅接单的完整页面链路
4.1 小程序端目录结构与用户端四个页面
小程序端目录通常是 app.js、app.json、app.wxss 加 pages 和 utils 两个目录。pages 下面按业务分页面:首页、下单页、订单列表、订单详情、登录、个人中心,师傅端的待接单列表和订单动作一般复用同一套订单列表,只是按角色区分逻辑。目录大概长这样:
miniprogram/ ├── pages/ │ ├── index/ # 首页:服务分类与推荐师傅 │ ├── order/create/ # 下单页 │ ├── order/list/ # 订单列表 │ ├── order/detail/ # 订单详情 │ ├── master/list/ # 师傅端待接单列表 │ ├── user/ # 个人中心 │ └── login/ # 登录页 ├── utils/ │ ├── request.js # wx.request 封装 │ └── auth.js # 登录态管理 ├── app.js ├── app.json └── app.wxssapp.json 里要注意两个事情:pages 数组的第一个元素是首页;tabBar 如果配置了,就只支持 2 到 5 个 tab。用户端常见的 tab 是首页、订单、我的三个;师傅端如果要独立 tabBar,一般是待接单、我的两个。tabBar 的图标必须用本地图片,不能直接用网络图片,否则真机上不显示。
用户端四个页面的职责分工:首页做服务分类入口和推荐师傅,下单页选服务类型、地址、预约时间,订单列表展示自己的历史订单并支持按状态筛选,订单详情展示当前状态和操作按钮。下单页的数据依赖服务类型和地址列表,所以 onLoad 里要并行请求这两个接口,不要串行等待,否则用户会明显感觉到页面 loading 时间变长。
4.2 列表页的加载更多与下拉刷新实现
订单列表是加载更多场景最典型的位置。前端维护 page 和 pageSize 两个分页参数,滚动到底部时请求下一页,下一页数据用 concat 拼到数组后面;下拉刷新时重置 page 为 1,清空数组再重新请求。先封装请求函数:
// utils/request.js 里封装的基础请求 function request(url, data = {}, method = 'GET') { return new Promise((resolve, reject) => { wx.request({ url: getApp().globalData.baseUrl + url, data: data, method: method, header: { 'Authorization': wx.getStorageSync('token') }, success: (res) => { if (res.statusCode === 401) { // token 过期,静默重新登录后重发请求 reLoginAndRetry(url, data, method, resolve, reject); return; } resolve(res.data); }, fail: reject }); }); }订单列表页的加载更多逻辑:
// 订单列表页:加载更多 Page({ data: { orders: [], page: 1, pageSize: 10, hasMore: true }, onReachBottom() { if (!this.data.hasMore) return; this.loadOrders(); }, onPullDownRefresh() { this.setData({ page: 1, orders: [], hasMore: true }); this.loadOrders().finally(() => wx.stopPullDownRefresh()); }, loadOrders() { const { page, pageSize, orders } = this.data; return request(`/order/list`, { page, pageSize }).then(res => { const list = res.data.list; this.setData({ orders: page === 1 ? list : orders.concat(list), page: page + 1, hasMore: list.length === pageSize }); }); } })这段代码有三个点必须说明。第一,onReachBottom 是页面滚动到底部自动触发的生命周期;onPullDownRefresh 需要当前页面的 .json 文件里配置“enablePullDownRefresh”: true,光在 app.json 里配样式没用。第二,拼接数组前要判断 page 是否为 1,否则下拉刷新后会出现重复数据——这是列表页最常见的 bug。第三,hasMore 的判定标准是“本次返回的条数是否等于 pageSize”,等于说明可能还有下一页,小于说明到底了,这个判断方式在数据量精确时最实用。
注意:“enablePullDownRefresh”: true 要配置在当前页面的 .json 文件里,app.json 配置的是全局下拉刷新样式,页面开关没打开不会触发。
另外提一个自定义导航栏的细节:如果 app.json 里把 navigationStyle 设成了 custom,顶部标题栏的高度要自己适配,用 wx.getSystemInfoSync() 拿到 statusBarHeight,再结合胶囊按钮的位置计算标题的 top 值。不然 iPhone 刘海屏上标题会顶到状态栏,做订单列表这类有下拉刷新的页面还容易和刷新手势区域冲突——开发工具里看不出问题,真机上一拉就露馅。
4.3 下单页的表单校验与服务项目选择
下单页的核心表单字段是服务类型、地址、预约时间、备注。服务类型用 picker 组件就够了,从接口拉列表,选择后把 id 和名称存到 data 里;如果服务类型超过六个,可以先在 picker 里选择大类,再到页面里用 radio-group 渲染子类型单选列表,这样用户的操作成本更低。
地址选择推荐跳转到地址列表页,用户选择一个已有地址回填,或者新增地址。新增地址用 wx.chooseLocation,它能返回名称、详细地址和经纬度。但要注意,chooseLocation 必须在用户点击事件后调用,不能一进页面就自动调,否则会被微信拦截,报“请同意使用定位”。这个坑在 iOS 真机上尤其明显,开发工具里几乎测不出来。
表单校验没有现成的库,常见做法是提交的时候逐字段判断。比如:服务类型没选就提示“请选择服务类型”;地址没填就跳转地址页;预约时间早于当前时间就提示重新选择。这套逻辑写在一个 submitForm 方法里,比折腾各种验证框架更实用,出错时用户也能直接看到哪个字段有问题。另外,下单按钮要加一个防重复提交标记——用户连续快速点了三次,只发一个请求,不然订单表里会出现三条几乎一样的记录。
4.4 师傅端视角:接单列表和订单详情动作
师傅端待接单列表和用户端订单列表的区别在两点:接口要按服务区域或距离过滤,订单卡片要展示师傅关心的字段——服务类型、用户地址、预约时间、预估收入。接口请求时带师傅的区域编码或经纬度,后端按距离升序返回,师傅看到的是“附近能接的活”,而不是全平台的单。
订单详情页要按角色渲染操作按钮。用户看到的是“取消订单”“确认完成”“评价”;师傅看到的是“接单”“拒单”“开始服务”“确认完工”。这套按钮组用 wx:if 按 role 和 status 两个条件判断,比每个角色单独做一个页面省代码,也好维护。按钮的显隐规则跟后端状态机保持一致,前端只是展示层,最终校验还在后端接口里。
这个页面容易出的安全问题是 ID 直接暴露。订单详情接口如果是 /order/detail?orderId=1 这样可枚举的自增主键,用户改一下数字就能看到别人的订单信息和地址,属于严重事故。好一点的源码会用 orderNo(随机订单号)替代自增 id 作为对外参数,用户表、师傅表的对外标识也建议用业务编号而不是主键。拿到源码后,第一步检查所有接口路径里有没有直接把数据库主键暴露出来的地方。
5. 避坑与排查:从并发接单到定位偏差的六个常见问题
这章列的问题,每个都是真实调试中踩出来的,按优先级排好。先说血泪经验:这类系统最值得反复测的不是正常流程,而是边界和并发——两个师傅抢同一单、用户取消正在服务的订单、师傅端地图定位偏差,每一样都是上线后最容易爆雷的点。
5.1 两个师傅同时抢一单:并发导致的重复接单
现象:两个师傅同时打开待接单列表,同一瞬间点了同一个订单的“接单”按钮,结果两个师傅的手机都显示“接单成功”,订单表里出现了两条接单记录。
原因:接口实现的常见写法是先 select 查订单,判断 status=0 后执行 update。这个判断和更新之间不是原子操作,两个请求都能读到 status=0,都觉得自己抢到了。
解决:把 update 语句带上 status=0 条件,让数据库行锁保证只有一个请求能更新成功:
int rows = orderMapper.update(null, new LambdaUpdateWrapper<Order>() .eq(Order::getId, req.getOrderId()) .eq(Order::getStatus, 0) // 关键:条件更新 .set(Order::getMasterId, req.getMasterId()) .set(Order::getStatus, 1) .set(Order::getAcceptTime, new Date())); if (rows == 0) { return Result.fail("手慢了,这张单已经被接走了"); }这里用 LambdaUpdateWrapper 构造条件更新,eq 条件同时约束 id 和 status,set 是实际要修改的字段。第一个请求执行时数据库对这行加了写锁,第二个请求的 update 会等待并最终更新 0 行,返回 rows==0,走失败分支。这个方案不需要 Redis 锁,也不需要改表结构,是最稳的单机并发兜底。
5.2 地图坐标偏移导致师傅找不到门
现象:师傅点“开始导航”,按用户下单时选的地址走,到了地方发现偏差三四百米,用户说“我家就在你旁边啊”。
原因:wx.chooseLocation 返回的坐标是 GCJ-02(火星坐标),而高德和腾讯地图对外接口也是 GCJ-02,问题一般出在两个环节:一是后端把坐标存成了 WGS-84,二是某些地图 SDK 默认按 WGS-84 解释。坐标系混用是这类项目的高发问题,不是导航玄学,查到最后几乎都是坐标系没统一。
解决:全链路统一用 GCJ-02,MySQL 里经纬度字段用 decimal(10,6),前端展示和距离计算都基于它。唯一需要转 WGS-84 的场景,是把数据导出给 GPS 设备或国际地图服务时。后端统一写一个 CoordinateUtils 工具类集中做转换,不要散落在各个 Service 里,不然半年后你自己都记不清哪些地方已经转过了。
5.3 用户端订单状态长时间不变
现象:师傅端已经点了“开始服务”,用户刷新订单详情还是“待接单”。
原因:最常见的两个原因:一是详情页数据只在 onLoad 里加载了一次,用户从列表页切到详情页不重新请求;二是后端改了状态但小程序端没有做状态对比,一直显示旧数据。
解决:详情页在 onShow 生命周期里重新调接口,别只写在 onLoad。onShow 每次页面从后台切回前台都会触发,用户从师傅的微信聊天切回小程序时也能拿到最新状态。后端接口统一返回订单对象并包含 updateTime,前端拿到新数据后先对比时间戳,有变化再 setData,避免无意义的重复渲染。
5.4 图片上传失败与域名白名单
现象:开发工具里传图正常,真机上一直转圈,最后报 uploadFile:fail。
原因:微信小程序对网络请求限制严格——request 和 uploadFile 的域名必须在公众平台后台配置白名单,而且必须是 HTTPS。开发工具默认不校验合法域名,所以本地调试看起来没问题,一上真机直接挂。
解决:开发阶段在微信开发者工具的“详情 - 本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,但上线前必须把接口域名加到后台的 request 合法域名和 uploadFile 合法域名里,并确认域名配了有效证书。特别注意:这个配置改完要重新上传体验版或正式发布才会生效,不少人改了后台以为立刻生效,然后白白排查半天。
5.5 小程序包体积超过 2MB 导致无法真机预览
现象:代码写完了,一扫码真机预览提示“主包大小超过 2MB”,无法上传发布。
原因:图片、icon、上百 KB 的组件库全堆在主包。小程序主包限制 2MB,超过就发布不了。
解决:把图片和图标从本地 assets 迁移到 CDN 或 OSS,页面全部使用网络图片;组件库按需引入,不要整个 import。如果业务还是大,拆分包:主包只放用户端四个页面和公共组件,师傅端页面全部放分包,通过 app.json 的 subPackages 字段配置。分包的好处是用户首屏只加载主包,启动速度也会提升。
5.6 师傅已接单后用户取消订单
现象:师傅接单后用户反悔取消,订单在列表里直接消失,师傅一头雾水——“我明明接了,怎么没了”。
原因:取消接口没有做状态限制,直接 delete 或者允许任意状态改到“已取消”,业务数据丢失,师傅端也没有任何感知。
解决:取消要分场景:待接单状态取消,直接改“已取消”;已接单后取消,保留订单记录,记录取消原因和取消人,同时通过微信订阅消息通知师傅。后端接口限制 status 必须等于 0 或 1 才允许用户取消,等于 2(服务中)之后不允许用户直接取消,只能联系客服兜底。这一步的代码核心还是条件更新,和 5.1 是同一个套路。
6. 从源码到上线:最小验证、测试数据与二次开发切入点
6.1 一套最小验证清单:先跑通主链路再谈扩展
首先要做的是最小可运行验证:把后端跑起来,小程序端打开,走通“用户下单 -> 师傅接单 -> 开始服务 -> 确认完工 -> 结单评价”这条主链路。验证不是点几个按钮就完了,建议用接口测试工具把订单流转五个状态的接口按顺序调一遍,确认每一步的返回码和状态字段都符合预期。
数据库方面,这类源码一般会附带 SQL 初始化脚本,导入后里面通常有基础数据。我的习惯是补充三类测试数据:三个不同角色的账号(用户、师傅、管理员)、三个服务类型(水电维修、家电维修、管道疏通)、不同状态的订单各一条,方便手动测试。另外把待接单订单造两三条,专门用来验证 5.1 说的并发抢单兜底逻辑,多测几次值得。
6.2 推荐先做的两个二次开发方向
二次开发切入点,优先建议两个方向:支付与结算的闭环、订阅消息通知。源码里的订单走到“待付款”之后一般只做了标记,接入微信支付需要在小程序后台开通微信支付商户号,后端补支付回调接口,这一步做完系统从演示变成可用。订阅消息则是把“师傅已接单”“师傅已上门”“订单完成”三个关键节点推给用户,能明显提升使用体验。
一个我踩过的细节:上线前检查后端接口的异常处理。很多源码项目的 controller 没有全局异常处理器,数据库异常会直接返回默认错误页,小程序端拿不到约定的 JSON 结构,一解析就崩。花半小时写一个 @RestControllerAdvice 全局异常处理器,把所有异常统一包装成 {code, message, data} 返回,小程序端的失败处理就稳了,这个改动的性价比非常高。
如果你准备把它改造成毕设或者商用项目,这套源码最大的价值是省掉了从零搭基础架构的时间。真正需要自己补的反而是业务边界——师傅入驻审核怎么做、售后纠纷怎么处理,这些代码帮不了你,得靠运营规则来补充。希望这份拆解能帮到你,早日跑通自己的上门维修系统。
本文还有配套的精品资源,点击获取