做毕设或者练手SpringBoot的兄弟们,别再死磕电商系统了,我最近把一个“基于SpringBoot的家庭设备维修服务系统”从需求到上线完整跑通了一遍,今天把技术选型、数据库设计、核心代码和踩过的坑一次性整理出来。说白了,这个系统就是在Web端把电话报修、手写登记这些老旧流程,变成用户在线提交故障单、管理员派单、维修工接单处理、用户评价的完整闭环。无论你是准备拿来当毕业设计,还是想系统过一遍SpringBoot开发流程,这篇文章都能直接套用。
整个项目我用的是SpringBoot + Thymeleaf + MyBatis Plus + MySQL这套组合,前端没有单独拆出工程,而是用模板渲染加一点Vue语法,部署时一个jar包搞定。下面从设计思路开始讲,一步步拆到部署上线,每部分都会讲清“为什么这么选”和“实际会踩什么坑”。
1. 项目整体设计与技术选型思路
1.1 需求分析:家庭设备维修到底要解决什么问题
做任何系统之前,先把业务理顺。家庭设备维修服务,你可以把它想成一个简化版的外卖平台,只不过送的不是餐,而是维修工。用户是业主,服务方是维修师傅,中间需要一个调度中心。
传统线下报修的痛点是:电话报修经常说不清设备型号和故障现象;维修师傅到了家里才发现缺工具、缺零件,白跑一趟;维修过程不透明,业主不知道师傅几点来;修完之后没有评价机制,服务质量全靠运气。这个系统就是把这些痛点搬到Web上,让每个环节都有记录、可追踪、可评价。
核心角色有三类:
| 角色 | 核心操作 | 关注点 |
|---|---|---|
| 普通用户 | 填写报修单、查看进度、评价、在线支付(可选) | 操作简单、进度透明 |
| 维修工 | 接单、上门维修、填写处理结果 | 派单合理、路径清晰 |
| 管理员/调度员 | 审核工单、派单、管理师傅、查询统计 | 调度高效、数据准确 |
从需求上抽象出来的业务链路就是:用户创建报修单 → 管理员审核并派单 → 维修工接单 → 上门维修 → 用户验收评价。整个系统的一切功能都是围绕这条链路展开的。
1.2 技术栈选型:为什么用SpringBoot,前端怎么做
选型原则只有一个:在满足功能的前提下,用最熟悉、最稳定的方式解决问题。后端我选了SpringBoot,理由很简单,它能让你用最小的配置搭起一个可运行的Web服务。SpringBoot的自动装配帮你处理了大部分样板配置,内嵌Tomcat免去了单独部署服务器的麻烦。如果你面试被问到自动装配原理,可以这样理解:SpringBoot启动时会根据classpath下的jar包和你的配置,自动创建并配置好需要的Bean,比如你引入spring-boot-starter-web后,DispatcherServlet、Tomcat、JSON转换器这些就都自动配好了。
前端这块,我见过不少人纠结要不要用Vue做前后端分离。我这个系统最终选了Thymeleaf + Bootstrap + jQuery,因为页面大多是以表单提交和列表展示为主,用服务端渲染写起来最快,模板里可以直接用SpringBoot的模型数据,不用处理跨域,也省去了一整个Node.js构建流程。当然,如果你们的毕业设计老师要求前后端分离,也可以用Vue写前端,然后npm run build打包,把dist目录扔到SpringBoot的static目录下,一样能跑,这个后面部署章节再细说。
持久层选择MyBatis Plus,主要是看中它的条件构造器和分页插件,写CRUD能省一半代码。数据库用MySQL,会话和缓存用Redis,这个组合在社区里最主流,遇到问题随便搜都能找到答案。
1.3 功能模块拆解与系统设计原则
功能模块按角色划分:
- 用户端:注册登录、个人信息维护、提交维修工单(设备类型、故障描述、上传照片、预约时间段)、工单列表与进度查询、取消申请、维修完成后评价打分。
- 维修工端:任务大厅或待接单列表、接单/拒单、开始维修、提交维修结果(项目、费用、耗时)、个人服务记录与评分查看。
- 管理端:工单审核、手动或智能派单、维修工账号管理、设备类型维护、工单统计报表、公告发布。
设计时我把状态管理单独抽了出来,没有到处写if/else判断状态是否允许操作。工单一共有六个状态:待派单、已派单、维修中、已完成、已评价、已取消。每个状态规定了允许的后续操作和角色,例如用户取消只允许在“待派单”状态,维修工接单只能在“已派单”状态且只能操作指派给自己的工单。这样一个状态机既防止了越权操作,也让后续扩展日志追溯变得简单。
2. 核心细节解析与数据库设计
2.1 数据库表结构:从用户到工单的一张关联网
数据库设计是整个系统的基础,表设计不好,后面写接口全是眼泪。我按业务边界拆了几张核心表,这里把关键字段和设计思路列一下。
第一张是用户表,无论是居民还是维修工都放这一张表,用role字段区分,好处是登录逻辑统一。关键字段有:id、username、password(BCrypt加密)、real_name、phone、role(0管理员/1用户/2维修工)、avatar、status(是否被封禁)、create_time。索引建议加username和phone的唯一索引,登录和搜索都用得到。
第二张是设备类型表,字段很简单:id、name、parent_id(支持多级,比如“大家电”下面有“冰箱”“空调”)、sort。把设备类型独立出来是因为工单需要根据设备类型匹配对应技能的维修工,后期也可能按类型做统计。
第三张是维修工扩展表,因为维修工需要额外维护技能标签、工作年限、接单状态(空闲/忙碌)、累计服务次数和平均评分。跟用户表一对一,用user_id关联。
第四张、也是最重要的一张是维修工单表,字段比较多:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 业务单号,格式如OX20250630123001 |
| user_id | bigint | 报修用户ID |
| worker_id | bigint | 维修工ID,可空 |
| device_type_id | bigint | 设备类型ID |
| fault_desc | varchar(500) | 故障描述 |
| images | varchar(1000) | 图片路径,逗号分隔 |
| address | varchar(200) | 上门地址 |
| appoint_start/appoint_end | datetime | 预约时间段 |
| status | tinyint | 0待派单 1已派单 2维修中 3已完成 4已评价 5已取消 |
| fee | decimal(10,2) | 维修费用 |
| user_comment | varchar(500) | 用户评价内容 |
| user_rating | tinyint | 1-5星 |
| create_time/assign_time/start_time/finish_time | datetime | 各环节时间戳 |
工单表是分页查询最频繁的表,索引很重要。status要建索引,因为列表页经常按状态筛选;user_id和worker_id也要建索引,对应各自角色的“我的工单”查询。order_no用业务单号而不是自增ID展示给用户,避免别人通过ID遍历你的数据。
2.2 权限与登录设计:拦截器还是Security
这个系统我用了Session + 拦截器方案,没有上Spring Security。原因很简单,只有三类角色,拦截器三个注解就搞定了,代码量少,也容易讲清楚。核心是在SpringMVC拦截器中判断当前登录用户的role是否在允许列表里。
我实际实现是搞了一个@RequireRole注解,放在Controller方法或类上,拦截器拿到注解value数组,再对比session里的用户角色。比如管理员的派单接口标注@RequireRole({"0"}),用户提交工单接口标注@RequireRole({"1"}),维修工接单标注@RequireRole({"2"})。这样权限规则直接写在接口旁边,醒目且维护方便。
如果你们项目要求真正意义上的RBAC,可以引入Spring Security,把登录认证改成JWT形式,前端每次请求Header带token,后端用SecurityFilterChain配置放行和拦截路径。但坦白讲,对于这类管理后台,Spring Security的复杂度和收益不成正比,初学阶段用拦截器反而能把权限控制理解得更透彻。
2.3 工单状态机:如何避免状态错乱和数据不一致
刚才提到的状态流转,设计时最好画一个状态图,不过我这里直接用表格列出允许的迁移:
| 当前状态 | 允许操作 | 目标状态 | 操作角色 |
|---|---|---|---|
| 待派单 | 用户取消 | 已取消 | 用户 |
| 待派单 | 管理员派单 | 已派单 | 管理员 |
| 已派单 | 维修工接单 | 维修中 | 维修工 |
| 已派单 | 管理员改派 | 已派单(另一人) | 管理员 |
| 维修中 | 维修工提交结果 | 已完成 | 维修工 |
| 已完成 | 用户评价 | 已评价 | 用户 |
我这个校验写在了Service层,而不是在Controller里。因为接口调用入口可能有两个:比如维修工接单之后,系统还得给用户发通知,如果这些都放在Controller里,很容易漏掉事务。在Service中做一个transition(order, fromStatus, toStatus, operator)方法,每次变更状态前先查一次当前status,然后加条件更新update where status=fromStatus,这样即使并发请求也只可能有一个生效。
另外每张工单的所有状态变迁都往order_log表写一条记录,包含操作人ID、旧状态、新状态、操作备注。这个表后面查问题、做审计都非常有用。之前做项目时跳过这步,结果线上出现“工单莫名其妙被完成”的情况,半天都查不到操作痕迹,后来补上日志才定位到是用错了账号登录。
3. 实操过程与核心环节实现
3.1 项目初始化:IDEA创建SpringBoot工程与版本避坑
我用的是IDEA 2024,直接在Spring Initializr插件里建项目。这里有一坑,Spring Initializr默认给的SpringBoot版本往往会选到最新的3.x,而3.x要求JDK17且部分第三方依赖还没跟上。我这个系统用的是JDK8 + SpringBoot 2.7.18,不想折腾。如果你已经建了3.x项目,会发现原来的javax.servlet包变成了jakarta.servlet,MyBatis Plus的旧版本启动会直接报错,所以切回2.7.x是最省事的方案。
创建时我勾选了Spring Web、Thymeleaf、Lombok、MySQL Driver、Validation,后面再手动加了MyBatis Plus和Redis依赖。最终pom.xml的核心依赖大概是这样的:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>如果依赖拉取慢,在配置文件里加阿里云镜像:
<repositories> <repository> <id>aliyun</id> <url>https://maven.aliyun.com/repository/public/</url> </repository> </repositories>3.2 核心功能一:用户提交维修工单,含图片上传
用户提交报修单是系统中被调用最频繁的写接口,也是流程起点。先定义一个VO类接收前端参数:
public class OrderCreateRequest { @NotNull(message = "设备类型不能为空") private Long deviceTypeId; @NotBlank(message = "故障描述不能为空") private String faultDesc; private MultipartFile[] images; @NotBlank(message = "联系地址不能为空") private String address; @NotNull(message = "预约开始时间不能为空") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime appointStart; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime appointEnd; }Controller层主要做参数校验和返回结果,业务逻辑全部下沉到Service:
@PostMapping("/order/create") @RequireRole({"1"}) @ResponseBody public Result create(@Valid OrderCreateRequest request, @RequestParam(value = "images", required = false) MultipartFile[] images, HttpSession session) { User user = (User) session.getAttribute("loginUser"); Long orderId = orderService.createOrder(user.getId(), request, images); return Result.success(orderId); }Service中关键逻辑有这么几点。生成order_no时我用“OX + 年月日 + 随机数”,随机数要防止重复,我用的是Redis自增+日期做后缀,例如RedissonClient.getAtomicLong或RedisTemplate.opsForValue().increment。图片存储用本地磁盘,上传路径不要放在项目目录下,而是配置一个外部路径,比如/home/upload/,然后把图片按日期分目录存放,文件名用UUID,避免中文名和重复名导致的各种问题。
文件保存的代码:
public String saveImage(MultipartFile file) { String dateDir = LocalDate.now().toString(); File dir = new File(uploadPath + "/" + dateDir); if (!dir.exists()) dir.mkdirs(); String ext = StringUtils.getFilenameExtension(file.getOriginalFilename()); String filename = UUID.randomUUID() + "." + ext; file.transferTo(new File(dir, filename)); return "/upload/" + dateDir + "/" + filename; }注意file.transferTo要求上传临时文件大小不能超过SpringBoot默认的1MB,生产环境必须调大配置。事务也不可少,因为保存工单主表和更新用户状态(如送积分)要同时成功,如果图片没保存成功,数据库里不能有残废工单。我在方法上加@Transactional,但图片操作依赖真实文件系统,事务回滚不会删文件,所以我的做法是先保存图片到临时路径,等工单主记录插入成功后再把文件挪到正式目录。实际中如果图片保存失败,直接抛业务异常提示“图片上传失败”,前端也不会进入成功逻辑。
3.3 核心功能二:派单后实时通知维修工
管理员在后台把工单派给某个维修工后,维修工页面需要第一时间出现新任务提示,这时候用WebSocket比轮询好。SpringBoot整合WebSocket我用的是原生@ServerEndpoint方案,跟SpringMVC体系松耦合,配置简单。
先在配置类里注册一个ServerEndpointExporter的Bean:
@Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }然后写一个WebSocket端点,持有所有在线维修工的Session:
@Component @ServerEndpoint("/ws/worker/{workerId}") @Slf4j public class WorkerWebSocket { private static Map<Long, Session> sessions = new ConcurrentHashMap<>(); @OnOpen public void onOpen(@PathParam("workerId") Long workerId, Session session) { sessions.put(workerId, session); } @OnClose public void onClose(@PathParam("workerId") Long workerId, Session session) { sessions.remove(workerId); } public static void sendToWorker(Long workerId, String message) { Session session = sessions.get(workerId); if (session != null && session.isOpen()) { session.getBasicRemote().sendText(message); } } }前端页面在维修工登录后创建WebSocket连接,指向/ws/worker/{userId}。管理员派单时,Service里除了更新工单表,还调用WorkerWebSocket.sendToWorker(workerId, "你有新工单"+orderNo)。要注意WebSocket端点是由JVM管理的,和Spring的IoC容器隔离,所以@ServerEndpoint类里面不能直接注入Spring的Service,需要静态工厂或者从SpringContextUtil里取Bean。我当时就是忘记处理这一步,导致推送一直空指针,后来改成在类里定义一个ApplicationContextHolder,从里面拿Service才解决。
3.4 核心功能三:图片访问映射与MinIO存储选型
如果图片存储用本地磁盘,SpringBoot默认静态资源不会映射到外部目录,直接访问/upload/...会404。所以需要实现一个WebMvcConfigurer,添加资源映射:
@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${upload.path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("/upload/**") .addResourceMapping("/upload/**") .addResourceLocations("file:" + uploadPath + "/"); } }注意Windows和Linux的末尾斜杠都要带,不然路径拼接会出问题。
如果你后续想把图片迁移到对象存储,热词里提到的MinIO是很好的选择。MinIO支持S3协议,部署一个服务端,SpringBoot里引入minio依赖,然后配置Endpoint、AccessKey、SecretKey和Bucket,上传时直接用minioClient.putObject(...)。本地存储适合学习演示和单机部署,MinIO适合生产环境,两者代码结构上只需要在FileStorageService接口下做不同实现,切换成本不高。
3.5 核心功能四:定时任务处理超时工单
维修工长时间不接单,或者用户预约时间临近还没指派,系统应该自动提醒。我用@Scheduled定时任务实现,开启方法是在启动类加@EnableScheduling,然后写一个定时调度Service。
@Component public class OrderReminderTask { @Autowired private OrderMapper orderMapper; @Autowired private NotifyService notifyService; @Scheduled(cron = "0 */30 * * * ?") public void remindUnassignedOrders() { List<Order> overdue = orderMapper.selectList(new LambdaQueryWrapper<Order>() .eq(Order::getStatus, 0) .lt(Order::getCreateTime, LocalDateTime.now().minusHours(2))); for (Order order : overdue) { notifyService.sendNoticeToAdmin(order.getOrderNo()); } } }这里的cron是每30分钟执行一次,扫描创建超过2小时且还是“待派单”的工单,给管理员发站内信或者短信提醒。如果你做的是用户端提醒,每次触发都别忘了用Redis或者数据库记录上次提醒时间,避免重复骚扰。
4. 常见问题与排查技巧实录
这部分是我实打实踩过的坑,每一个都能对应到具体报错场景,比看文档直接得多。
4.1 SpringBoot版本太高:javax包一切崩
之前说过,SpringBoot 3.x把javax.servlet换成了jakarta.servlet,如果项目是JDK8,启动直接报“NoClassDefFoundError: javax/servlet/http/HttpServlet”。另外热词里提到“springboot版本太高”应该就是这类问题。我的建议是:学习项目和毕业设计一律用SpringBoot 2.7.x,等你对SpringBoot足够熟悉了,再迁移3.x不迟。如果你非要上3.x,记得检查每个依赖版本都支持Jakarta命名空间,Mybatis Plus要切换到3.5.9+,Redis客户端和连接池版本也要更新。
4.2 中文乱码:数据库URL必须带编码参数
这个问题最容易在Windows上出现。创建表时如果数据库默认字符集是latin1,插入中文全部变问号。解决两处:一是建库时指定DEFAULT CHARACTER SET utf8mb4,二是application.yml里数据库连接URL加上参数:
url: jdbc:mysql://localhost:3306/repair_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai另外页面乱码通常是模板编码问题,SpringBoot的Thymeleaf默认是UTF-8,但IDEA里文件编码如果是GBK,也会乱,直接把IDEA的Global Encoding、Project Encoding、Properties Files全改成UTF-8,一劳永逸。
4.3 Maven构建失败和依赖下载慢
IDEA创建SpringBoot项目后,第一次maven build经常卡死,这是网速问题。除了前面说的配置阿里云镜像,还要注意IDEA里Maven的settings.xml是否指向了自带的镜像地址。我习惯复制一份settings.xml放到本地目录,显式配置本地仓库和镜像,这样换电脑也不受影响。构建时如果报Package xxx does not exist,先执行mvn clean再重新导入依赖,多数是编译缓存问题。
4.4 图片文件上传大小限制
SpringBoot默认单文件最大1MB,请求体最大10MB,随便传个手机照片就超了。在application.yml里调大:
spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB注意请求大小要比单文件大,因为一次可能传多张。接口层建议限制数量(比如最多5张),否则用户一次传20张,带宽和存储都遭殃。
4.5 Thymeleaf热更新不生效
调试页面时改完模板要重启才能看到效果,太浪费时间了。配置两步实现模板热更新:
spring: thymeleaf: cache: false然后加上spring-boot-devtools依赖,IDEA里再设置自动编译。注意devtools会带来类加载器变化,遇到奇怪的Bean注入问题先停下试试。实在不行就手动重启一次,别纠结。
4.6 静态资源访问404
SpringBoot默认把classpath:/static/,classpath:/public/,classpath:/resources/这几个目录作为静态资源根目录。如果你把前端文件放到了src/main/webapp,SpringBoot默认是不管的,要添加额外配置。最简单的就是放static目录。前面说的外部上传目录映射404,按3.4节的配置检查一下路径末尾斜杠。
4.7 会话和登录状态丢失
如果用httpSession.setAttribute("loginUser", user),要确认没有在拦截器里重新创建Session。最常见的问题是部署时用了多个实例,Session不共享,用户登录后下次请求跑到另一台机器就没登录状态。单机部署没这个问题,如果是分布式部署,需要把Session放到Redis里,SpringBoot集成spring-session-data-redis即可,几行配置搞定。
5. 打包部署与后续扩展
5.1 打包成可执行jar
项目开发完成后,用Maven打包:
mvn clean package -DskipTeststarget目录下生成xxx.jar,在服务器上执行:
java -jar repair-system.jar默认端口8080,指定端口就用:
java -jar repair-system.jar --server.port=8081如果你用了Thymeleaf模板,打包后模板在jar包里,改模板就需要重新打包。追求灵活部署的话,可以把模板、上传目录、配置全部外置,通过spring.thymeleaf.prefix=file:/opt/repair/templates/指向外部目录,但这样做复杂度高,毕设阶段不需要。
5.2 Docker部署,自动化起来
服务器上装好Docker后,写一个Dockerfile:
FROM openjdk:8-jre-alpine COPY repair-system.jar /app/repair-system.jar EXPOSE 8080 ENTRYPOINT ["java","-jar","/app/repair-system.jar"]构建并启动:
docker build -t repair-system . docker run -d -p 8080:8080 -v /home/upload:/home/upload --name repair repair-system注意挂载上传目录,否则容器重建后图片就丢了。数据库和Redis建议也用Docker起,但千万别直接用官方默认账号密码,生产环境密码要复杂。
5.3 结合热搜词,未来可以这样扩展
我做完这个系统后,还留了几个扩展点,正好对应到近期搜索里比较热的几个方向。一是把通知模块改成ActiveMQ异步消息,派单、超时提醒、系统公告都可以通过MQ发消息,削峰解耦;二是Web端实时视频这块,给维修工加一个“一键开工直播”或者“完成后上传视频”,集成摄像头推流(比如WebRTC或播放器插件),用户就能远程看到维修过程;三是Spring AI大模型,很多人在搜“springai web”,你可以加一个智能问答助手,让用户先描述故障现象,系统通过大模型自动猜测故障原因并推荐维修方案,顺便把工单里的故障描述自动规范化。这些扩展方向都能让你的项目在答辩时多几个亮点。
最后再分享一个我这套系统的体会:别迷信复杂的技术栈,先把业务闭环做扎实,再花点心思把状态机、日志、权限这些“内功”练好。你把这个家庭设备维修系统完整跟一遍,SpringBoot从自动装配到WebSocket、从定时任务到Docker部署,能学到的东西不比做一个电商系统少。遇到问题就照着上面的排查清单过一遍,多数坑都能绕过去。