前段时间帮人打样一个家庭设备维修服务系统,需求很直接:业主报修、客服派单、师傅上门、完工评价,整个流程线上闭环。技术选型定了 Spring Boot + Web 前后端分离这套经典组合,做完之后发现这类系统最大的难点不在 CRUD,而在工单状态怎么流转、角色权限怎么隔离、前端打包怎么塞进后端这些细节里。这篇就把我从需求分析到部署上线的完整思路和踩坑记录写下来,给正在做类似 Spring Boot 项目或者拿它当毕设选题的朋友一个参考。
1. 先盘需求:这套维修服务系统到底要管哪些角色和流程
做个系统之前最忌讳上来就建表写接口。家庭设备维修服务这类业务,表面上是个预约平台,实际上牵扯到三拨人:报修的人、修东西的人、管单子的人。三拨人关心的事完全不同,需求必须先从角色拆。
1.1 业务角色拆解:业主、维修师傅、后台客服的三方视角
业主端在乎的是“我报修方不方便、师傅什么时候来、修完花了多少钱”。所以业主侧的功能核心是报修表单、订单进度跟踪、完工确认、评价。报修表单不能搞得太复杂,我见过有的系统让用户填设备型号、故障代码、保修期,结果用户根本不知道,填到一半就放弃了。合理的做法是让用户选设备类型(空调、冰箱、洗衣机、电路、水管等),故障描述用一段话自由输入,再传一两张现场照片,后台根据这些信息去判断该派谁。
维修师傅端在乎的是“今天有几单、离我远不远、单子值不值得接”。所以师傅侧功能核心是待接单列表、订单详情(地址、故障描述、用户联系方式)、状态更新操作(接单、出发、到达、完工)、完工填报(用了什么材料、收了多少钱)。这里有个容易被忽略的点:师傅大多在户外,Web 端操作要尽量少点几下按钮,能用一个大按钮完成的就别设计成三步表单。
后台客服端在乎的是“新单怎么分配、有没有用户投诉、师傅完工率怎么样”。所以后台侧功能核心是订单审核、派单管理、订单查询与导出、师傅信息维护、简单的数据统计。很多毕设或小团队系统把后台做得特别重,其实没必要,客服日常高频操作就那么几个,报表统计在首页放几个数字卡片(今日报修量、待派单数、完工率)就够用。
三拨人的系统入口不一样,但数据都落在同一套订单体系里,这就是后面要说的权限设计和数据表设计的基础。
1.2 核心业务流程梳理:报修单从创建到完工经过几道关
把三方角色串起来,整个核心流程就是一条订单生命线:
- 业主提交报修单,系统生成待审核订单
- 客服审核订单信息,确认地址、故障描述是否清晰,不合规的退回补充
- 客服根据设备类型和区域派单给师傅(或系统自动推荐)
- 师傅接单,确认上门时间
- 师傅上门维修,过程中可更新状态(出发、到达)
- 维修完成,师傅填写维修结果和费用,订单变为待确认
- 业主确认完工并评价,订单正式完结
这条流程看起来很顺,实际落地时有两处最容易出乱子。一是取消和改约,业主下单后可能临时取消,师傅到了也可能发现需要二次上门,所以订单状态里必须预留“已取消”和“待改约”这类分支状态,不能只做线性的几个节点。二是费用确认,维修行业经常出现上门先收检查费、换件另外加钱的场景,所以订单上必须有费用明细字段:上门费、检测费、材料费、人工费,而不是一个笼统的总价。
这个阶段梳理清楚后,后面建表、写接口、做页面才有明确依据。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与数据建模:为什么选了这套组合,工单状态怎么落到表里
需求清楚了再谈技术。Spring Boot + Web 这个组合本身没有悬念,真正需要动脑的是版本选型、持久层方案和订单表结构设计。这里我把踩过坑的经验一并说清楚。
2.1 技术栈选择背后的考量:Spring Boot 版本、ORM、数据库与缓存
先说 Spring Boot 版本。这几个月帮人排查过好几个项目启动失败,原因都是“Spring Boot 版本太高”。如果你的项目要搭配 MyBatis、旧版 Shiro 之类组件,或者要跑在较老的 JDK 8 环境下,Spring Boot 2.7.x 会稳得多。Spring Boot 3.x 要求 JDK 17,且部分第三方 starter 尚未跟进,很多面向课程设计和毕设场景的工程没必要赶这个新。所以我的建议很直接:没有特殊需求就用 2.7.x,后续想升级再升,排查成本会低很多。
持久层我选 MyBatis Plus,三个理由:第一,单表 CRUD 不用手写 XML,开箱即用的 BaseMapper 把大部分查询省了;第二,分页插件用起来非常方便,后台订单列表分页直接 new Page 就行;第三,代码生成器可以把实体类、Mapper 一次性生成,省下重复造轮子时间。如果你的系统有复杂的多表联查,MyBatis Plus 也支持自定义 XML,扩展性够用。
数据库用 MySQL 8.0,字符集统一 utf8mb4。缓存用 Redis,主要存两类东西:登录 Token 和热点字典数据(比如设备类型列表)。你可能会问,这种小系统有必要用 Redis 吗?从功能上说没有也能跑,但毕设或简历项目里写了 Redis 是一个实打实的加分点,而且实际用起来也不复杂,登录接口存个 key、报修接口查字典时先读缓存,逻辑很轻。
前端这块,我用的是 Vue 3 + Element Plus,构建工具 Vite。Vue 3 生态现在已经很成熟,Element Plus 的表单、表格、弹窗组件基本把管理后台 80% 的页面需求覆盖了。如果你不熟 Vue 3,只求赶紧做出页面,Vue 2 + Element UI 也能复用下面讲的思路。
2.2 订单表与状态机设计:一张核心表撑起全部业务流程
核心表大概就是下面这几张,但真正花心思的是 repair_order 这张工单表。它相当于整个系统的中枢,用户、师傅、订单、评价全部围绕它展开。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 订单编号,时间戳+随机数生成 |
| user_id | bigint | 报修业主用户 ID |
| technician_id | bigint | 接单师傅用户 ID,未派单时为空 |
| device_type_id | bigint | 设备类型(空调、冰箱等) |
| fault_desc | varchar(255) | 故障描述 |
| address | varchar(255) | 上门地址 |
| status | tinyint | 订单状态,见下面枚举 |
| fee_detail | varchar(255) | 费用明细 JSON 字符串 |
| create_time / update_time | datetime | 创建与更新时间 |
订单状态我建议做成独立的枚举类,状态机流转规则在代码里约束,而不是放任随便改。参考状态如下:
- 0 待审核(业主已提交,客服未处理)
- 1 待派单(审核通过,等待指派师傅)
- 2 待接单(已指派,师傅未接)
- 3 维修中(师傅已接单,出发/到达/维修都算这个状态)
- 4 待确认(师傅填报完工,等待业主确认)
- 5 已完成(业主确认并评价)
- 6 已取消
- 7 待改约(师傅上门后发现需二次处理,重新约时间)
状态用数字存数据库没问题,但代码里一定用枚举类转字符串做校验,例如写一个 OrderStatusEnum,在 Service 层统一封装 changeStatus(orderId, expectedStatus, newStatus) 方法。这样可以防止直接把订单从“待审核”跳到“已完成”这种非法操作。状态机是这种系统的灵魂,这块设计得不好,后面各种权限接口全都会跟着乱。
2.3 用户、设备类型、评价表的边界设计
用户表单独提一下,因为维修系统里用户不只是“用户”,师傅也是用户。最佳实践是一张 user 表存登录账号,加一个 role 字段区分角色(0 业主、1 师傅、2 客服/管理员),然后师傅的额外属性(服务区域、技能类型、接单数、评分)放在 technician_profile 表里。这样登录认证统一,业务数据独立,不会把一张表撑得太胖。
设备类型表 device_type 就是一个简单的字典表(id、名称、图标、排序),下拉框数据来源。你可以把它放在 Redis 里缓存,避免每次报修页面加载都查数据库。
评价表 evaluation 单独建:id、order_id、rating(1-5 分)、content、create_time。评价是师傅排名的依据,后面派单权重会用到,所以字段宁多勿少。
还有一点,订单表里的 fee_detail 我用了 JSON 字符串。你可能想问为什么不用独立子表?因为维修场景里费用明细通常只有 3-5 个条目(上门费+材料费等),JSON 查询时不需要关联聚合,用字符串存反而简单,取出来用 Fastjson 或 Jackson 解析就行。如果后续要做财务对账、多维统计再拆成子表也不迟。
3. 核心业务链路实现:从用户提交报修到师傅完工的完整闭环
数据结构定了,接下来就是核心接口的实现。这部分重点讲三个:报修创建、派单策略、状态更新与 Web 端实时反馈。
3.1 报修接口设计:表单校验、图片上传与事务边界
报修接口是业主用的第一个接口,体验好不好全看这里。我用 POST /api/order/create 接收参数,核心字段包括 deviceTypeId、faultDesc、address、contactPhone、images。这里要注意几个细节:
第一,faultDesc 加 @NotBlank 校验,address 不能只校验非空,还要校验长度,防止有人传 2000 个字符的地址把前端布局撑烂。
第二,图片上传我建议单独做一个 POST /api/file/upload 接口,先传图拿 URL,再连同表单数据一起提交订单。这样做的好处是用户可以先把照片拍好传上去,看看上传成功没有,再填其他信息。如果图片跟着订单一次性提交,网络一抖全部白填。
第三,事务边界。报修创建表面上看只插入一张 repair_order 表,但完整的业务里还可能包含:首次通知客服、给用户发送接单回执、生成操作日志日志。这些操作我建议放在同一个事务方法里,比如:
java复制@Transactional(rollbackFor = Exception.class)
public Long createOrder(OrderCreateDTO dto, Long userId) {
RepairOrder order = new RepairOrder();
// 填充字段...
repairOrderMapper.insert(order);
// 插入操作日志
orderLogMapper.insert(new OrderLog(order.getId(), userId, "用户提交报修", ""));
return order.getId();
}
事务边界原则是:只把必须同时成功的操作放在同一个事务里,发短信、调第三方通知这类失败不应该回滚订单本身,应该用异步或者重试机制处理。这个区分清楚了,后面线上少很多麻烦。
3.2 派单策略:按区域匹配加人工干预,不搞过度自动化
派单是后台最核心的接口。很多毕设喜欢在这上面堆算法,什么智能派单、最优路线规划,实话说小团队用不上。我这边实现的是“系统推荐 + 人工确认”的方案:
- 根据订单的设备类型,筛选出技能匹配且状态为“空闲”的师傅
- 根据订单地址解析出的区域码,优先匹配该区域师傅;如果没有,扩大到上级区域
- 按师傅评分和当月接单数排序,评分高、未爆单的排前面
- 推荐给客服,客服可以一键采用推荐,也可以手动改派
SQL 大致是:
sql复制SELECT t.id, t.employee_code, t.score, t.order_count
FROM technician_profile t
WHERE t.service_region = #{regionCode}
AND t.status = 1
AND t.skill_type LIKE CONCAT('%', #{deviceType}, '%')
ORDER BY t.score DESC, t.order_count ASC
LIMIT 5
注意 skill_type 匹配不要用精确等于,师傅往往兼任多种技能,用逗号分隔字符串,查询时 LIKE 就行。数据量大了以后可以改成关联表,初期这样完全够用。
派单接口要设置幂等性。客服点“派单”按钮,很可能因为网络重试连点两次。我在派单实现里加了状态校验:只有 status = 1(待派单) 才允许执行派单操作,如果已经被派过,直接抛异常提示“该订单已处理”。这就是状态机校验带来的好处,数据库层面也能防止重复派单。
3.3 状态更新与 Web 端实时反馈:轮询还是 WebSocket
订单状态变了,怎么让用户 Web 端第一时间看到?这是很多新手容易纠结的地方。方案无非两种:前端定时轮询和后端 WebSocket 推送。
轮询简单可靠,每隔 3-5 秒请求一次订单状态接口,状态变了就刷新页面。缺点是稍微有点延迟,请求稍微频繁。WebSocket 推送实时性好,但需要后端维护连接,代码更多。我的建议:初期用轮询,把订单状态接口做成轻量接口(只返回 status、师傅信息、费用等有限字段),Redis 缓存当前状态,DB 查询压力很小,等业务量大了再升级 WebSocket 不迟。
具体实现,订单状态查询接口我缓存了当前状态,状态更新时同时更新 Redis:
java复制public Integer getOrderStatus(Long orderId) {
String key = "order:status:" + orderId;
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) return Integer.parseInt(cached);
// Redis 没有则查库并回填
RepairOrder order = repairOrderMapper.selectById(orderId);
redisTemplate.opsForValue().set(key, String.valueOf(order.getStatus()), 30, TimeUnit.MINUTES);
return order.getStatus();
}
师傅端状态更新接口 PUT /api/order/status,只允许师傅角色调用,并且只能在合法流转方向内改。比如师傅接单后把状态从“待接单”改成“维修中”,到了完工阶段再改成“待确认”。不允许从“维修中”直接改成“已完成”,因为系统还没有经过业主确认的步骤。
3.4 完工评价闭环:评分如何反哺派单权重
订单结束后,用户评价数据写入 evaluation 表,同时同步更新 technician_profile 表的 score 字段。具体做法不复杂:评价接口里插入评价记录后,重新计算该师傅近 30 天平均分并更新。计算逻辑可以放在事务里,但更稳妥的做法是异步处理,避免评价接口因为统计慢而超时。
派单排序里已经用到 score 字段,所以评价数据是直接影响师傅接单量的,师傅会因此更重视服务态度,业务上形成正向循环。这个闭环做完,整个维修系统的业务逻辑就算真正完整了。
4. 权限与安全设计:业主、师傅、客服三类账号共存的隔离方案
一个系统三种角色,最怕的就是客服能看师傅的单、师傅能改用户的资料,权限边界不清晰,项目一测全是漏洞。这里把认证和授权的实现思路说透。
4.1 登录认证选型:JWT 无状态认证为什么适合这种系统
登录认证我推荐 JWT。原因很简单:无状态、天然适合前后端分离,后端不需要在内存里保存会话,Redis 里顶多存个注销黑名单。流程是:
- 用户输入账号密码,密码用 BCrypt 加密存储
- 登录成功后用用户 ID、角色生成 JWT Token,过期时间设 24 小时
- 前端把 Token 放在请求头
Authorization: Bearer xxx - 后端拦截器解析 Token,取出用户信息放入 ThreadLocal,供 Controller 层使用
拦截器放行哪些路径要仔细配置:放行登录、注册、首页公开查询接口;其余接口全部校验 Token。用 Spring 拦截器实现,核心逻辑就是重写 preHandle 方法:
java复制public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token)) {
response.setStatus(401);
return false;
}
try {
Claims claims = JwtUtil.parseToken(token.replace("Bearer ", ""));
UserContext.set(claims.get("userId", Long.class), claims.get("role", Integer.class));
return true;
} catch (Exception e) {
response.setStatus(401);
return false;
}
}
注意拦截器里放行列表不能用通配符一刀切,比如 /api/order/** 就不能全部放行,否则订单接口就裸奔了。
4.2 接口权限控制:按角色细分路由与数据范围
认证只解决“你是谁”,授权解决“你能干什么”。最稳妥的做法是 Controler 层用自定义注解或者框架校验角色。
我这边在 Service 层做了两套控制。第一套,数据范围控制:师傅只能操作自己名下订单,查询订单列表时强制拼上 technician_id = 当前登录用户ID,不能在 SQL 里省略这个条件。第二套,操作权限校验:比如只有客服角色能调派单接口,只有业主角色能提交评价。
实现上我用了简单的 AOP 切面,自定义一个 @RequireRole(value = {1, 2}) 注解,标注在方法上,进入方法前先校验当前用户角色。这套东西比在每个方法里写 if 判断干净很多,也方便后续扩展角色。
4.3 容易被忽略的 Web 安全隐患与加固办法
这类系统最容易出的安全问题,我排一下:
- 密码明文保存:用 BCrypt,不要用 MD5。MD5 撞库太容易,BCrypt 自带盐值,暴力破解成本高得多。
- SQL 注入:用 MyBatis Plus 的 BaseMapper 基本都是预编译,问题不大,但自定义 XML 里如果有
${}拼接就要小心,优先用#{}。 - 图片上传漏洞:文件类型限制不能用后缀名判断,要读文件头判断真实类型,同时限制大小(例如单张不超过 5MB),文件路径用随机文件名,不要用用户上传的原名。
- 接口防刷:比如短信验证码接口、报修提交接口,加一个简单的计数器,同一个 IP 或用户短时间内请求次数超过阈值直接拒绝。用 Redis 的 INCR + EXPIRE 就能实现,不需要引入重型网关。
这些点在写代码的时候花不了多少时间,但演示时一旦被提出来就是致命伤,建议从一开始就按安全习惯来。
5. 前端集成与打包部署:把 Vue 页面装进 Spring Boot 的完整过程
很多人开发时前后端分离跑得很欢,一到部署就卡壳:前端一个地址,后端一个地址,总不能让人家运维开两个端口。办法很简单,把前端构建产物放进 Spring Boot 的静态资源目录,打成单 jar 运行。
5.1 开发环境的前后端分离配置与代理
开发阶段,前端跑在 Vite 默认的 5173 端口,后端跑在 8080。跨域问题前端用 Vite 代理解决,在 vite.config.js 里配置:
javascript复制server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
这样前端发请求写 /api/xxx 就行了,Vite 会自动把请求转发到后端 8080 端口。后端这边设置跨域允许时,allowedOriginPatterns 设为 *,但注意仅限开发环境,生产环境同一个端口其实不需要跨域配置。
开发时接口地址用一个环境变量区分,前端 .env.development 里 VITE_API_BASE='',请求路径写成 /api/xxx 即可。千万别把 http://localhost:8080 写死在前端代码里,不然后面部署到别的机器上改起来想哭。
5.2 生产打包:前端构建产物如何放进静态资源目录
生产部署前,先在前端项目执行构建:
bash复制npm run build
生成在 dist 目录下的 index.html、css、js 等文件,全部复制到后端项目的 src/main/resources/static 目录下。之后用 Maven 打包整个 Spring Boot 工程,生成的 jar 里就带了前端页面,直接启动就能访问。
这里有两个长期困扰新手的坑,我分别说下。
第一个坑:前端路由模式的问题。Vue Router 有 hash 模式和 history 模式两种。开发时通常用 history(地址好看),但部署到 Spring Boot 后刷新 /order/list 页面会 404,因为后端不认识这个前端路由。两个解决方案,要么把前端路由改成 hash 模式(地址变成 /#/order/list),要么在后端加一个转发规则,把非 /api 开头的路径全部转发到 index.html。我建议小系统直接用 hash 模式,零配置最省心。如果你想保留 history 模式,加一个 Controller 转发:
java复制@RequestMapping(value = {"/", "/order/**", "/user/**"})
public String forward() {
return "forward:/index.html";
}
第二个坑:静态资源缓存。前端打包后文件名带 hash 是好事,但 index.html 本身不要被浏览器缓存,否则用户永远加载旧页面。后端可以在静态资源路径上设置缓存策略,例如 index.html 不过期,其他带 hash 的 js/css 设置长缓存。
5.3 部署实测:端口、数据库连接与启动报错排查
部署到服务器上,最常遇到的启动报错我列一下,方便你照着排查:
| 报错关键字 | 原因 | 解决 |
|---|---|---|
| Access denied for user | 数据库账号密码错 | 检查 application.yml 里的 datasource 配置 |
| Communications link failure | 数据库地址或端口不通 | 确认 MySQL 是否启动、防火墙是否放行 3306 |
| Port 8080 was already in use | 端口被占 | lsof -i:8080 找到进程干掉,或改 server.port |
| Unknown database | 数据库不存在 | 先创建数据库,注意字符集选 utf8mb4 |
| The server time zone value | MySQL 时区问题 | JDBC URL 增加 serverTimezone=Asia/Shanghai |
部署我建议用生产配置文件区分:application-dev.yml 和 application-prod.yml,启动时用 spring.profiles.active=prod 指定。这样本地开发连本地 MySQL,服务器连服务器 MySQL,互不干扰。数据库连接池的初始连接和最大连接数按并发量设置,比如初始 5、最大 20 就够了。
如果系统需要演示,建议把数据库和 jar 包都放到同一台服务器,数据库用 Docker 拉起,启动脚本写在 start.sh 里,一键启动。演示的时候最怕手忙脚乱输命令,提前把环境编排好能省下大量现场补救的时间。
6. 如果继续往下做:从维修系统延伸到设备管理与消息通知
做完这套基本功能之后,我最大的体会是,维修服务系统的核心价值不在 CRUD,而在两个延伸方向:设备数据和消息触达。
6.1 扫码报修与设备档案:从修一次到管一台
你可以给每台设备生成一个二维码,贴在设备表面。业主扫码就能跳转到报修页,并且系统自动带出设备型号、购买时间、历史维修记录。这个功能实现起来不算难:设备表 device 里加一个 device_code 字段,报修表单里加一个隐藏字段,页面二维码就是 https://域名/repair?code=xxx。好处是用户不用手动选设备类型,报修准确率高,后台也能累计某台设备的维修频次,形成设备档案。对家庭场景来说,很多用户记不住设备型号,扫码直接带出,体验会好很多。
6.2 消息通知:从站内信到短信和模板消息
系统刚上线的时候,只有站内信和 Web 页面轮询,用户体验很一般。后来接入了第三方短信服务,在几个关键节点发通知:业主提交报修后通知客服、派单后通知师傅、状态变更后通知业主。对接第三方短信平台其实很简单,就是调一个 HTTP 接口,把模板参数传过去。需要注意两点:模板要先在平台审核通过,参数需要 URL 编码,还有短信发送不能阻塞主流程,用异步任务队列处理。
如果你觉得短信费用高,家庭维修场景还可以用企业微信或微信公众号模板消息推送。用户关注公众号后,报修进度直接通过模板消息推过去,成本比短信低很多,转化率也更好。
6.3 定价与在线支付:把“师傅说了算”变成“平台明码标价”
维修行业最容易扯皮的就是费用。付费闭环要做得细,可以把维修项目价格写成字典(上门费、检测费、空调加氟等标准价),师傅完工时只能从价格表里勾选项目,不能自己随口报价。再接入微信支付或支付宝,业主确认完工后在线上完成付款,费用直接进平台账户,平台抽佣后结算给师傅。这一步业务上更复杂,但整个系统就从信息撮合升级成了交易平台,离能独立运转的商业形态更近一步。
我在实际做这套系统时,最深的感受是:Spring Boot + Web 技术本身并不难,难的是把维修服务这个业务的边界想清楚。报修单从创建到完结,每一步由谁操作、状态如何流转、数据怎样落到表里,这些想明白了,代码只是翻译的过程。如果你也准备做类似的系统,我建议先画一张状态机草图,把角色和流转列清楚,再动手写项目,后面会顺很多。
