
做毕设或者练手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、passwordBCrypt加密、real_name、phone、role0管理员/1用户/2维修工、avatar、status是否被封禁、create_time。索引建议加username和phone的唯一索引登录和搜索都用得到。第二张是设备类型表字段很简单id、name、parent_id支持多级比如“大家电”下面有“冰箱”“空调”、sort。把设备类型独立出来是因为工单需要根据设备类型匹配对应技能的维修工后期也可能按类型做统计。第三张是维修工扩展表因为维修工需要额外维护技能标签、工作年限、接单状态空闲/忙碌、累计服务次数和平均评分。跟用户表一对一用user_id关联。第四张、也是最重要的一张是维修工单表字段比较多字段名类型说明idbigint主键order_novarchar(32)业务单号格式如OX20250630123001user_idbigint报修用户IDworker_idbigint维修工ID可空device_type_idbigint设备类型IDfault_descvarchar(500)故障描述imagesvarchar(1000)图片路径逗号分隔addressvarchar(200)上门地址appoint_start/appoint_enddatetime预约时间段statustinyint0待派单 1已派单 2维修中 3已完成 4已评价 5已取消feedecimal(10,2)维修费用user_commentvarchar(500)用户评价内容user_ratingtinyint1-5星create_time/assign_time/start_time/finish_timedatetime各环节时间戳工单表是分页查询最频繁的表索引很重要。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 statusfromStatus这样即使并发请求也只可能有一个生效。另外每张工单的所有状态变迁都往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.servletMyBatis Plus的旧版本启动会直接报错所以切回2.7.x是最省事的方案。创建时我勾选了Spring Web、Thymeleaf、Lombok、MySQL Driver、Validation后面再手动加了MyBatis Plus和Redis依赖。最终pom.xml的核心依赖大概是这样的parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependencies如果依赖拉取慢在配置文件里加阿里云镜像repositories repository idaliyun/id urlhttps://maven.aliyun.com/repository/public//url /repository /repositories3.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层主要做参数校验和返回结果业务逻辑全部下沉到ServicePostMapping(/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的BeanConfiguration public class WebSocketConfig { Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }然后写一个WebSocket端点持有所有在线维修工的SessionComponent ServerEndpoint(/ws/worker/{workerId}) Slf4j public class WorkerWebSocket { private static MapLong, 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() { ListOrder overdue orderMapper.selectList(new LambdaQueryWrapperOrder() .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.9Redis客户端和连接池版本也要更新。4.2 中文乱码数据库URL必须带编码参数这个问题最容易在Windows上出现。创建表时如果数据库默认字符集是latin1插入中文全部变问号。解决两处一是建库时指定DEFAULT CHARACTER SET utf8mb4二是application.yml里数据库连接URL加上参数url: jdbc:mysql://localhost:3306/repair_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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 静态资源访问404SpringBoot默认把classpath:/static/,classpath:/public/,classpath:/resources/这几个目录作为静态资源根目录。如果你把前端文件放到了src/main/webappSpringBoot默认是不管的要添加额外配置。最简单的就是放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.port8081如果你用了Thymeleaf模板打包后模板在jar包里改模板就需要重新打包。追求灵活部署的话可以把模板、上传目录、配置全部外置通过spring.thymeleaf.prefixfile:/opt/repair/templates/指向外部目录但这样做复杂度高毕设阶段不需要。5.2 Docker部署自动化起来服务器上装好Docker后写一个DockerfileFROM 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部署能学到的东西不比做一个电商系统少。遇到问题就照着上面的排查清单过一遍多数坑都能绕过去。