首页 / 资讯中心 / 文章详情

基于Java社区维修平台:Spring Boot状态机与权限控制实战

基于Java社区维修平台:Spring Boot状态机与权限控制实战 ★ FEATURED ARTICLE
简介这是一份基于Java的社区维修平台完整项目资源面向Java Web初学者、课程设计及毕业设计人群。系统采用EclipseMySQLJSP技术栈内置住户管理、维修工管理、维修订单、在线沟通、举报信息、留言板等模块完整覆盖社区维修服务从下单到完工的管理流程。资源包共811个文件包含java后端源码、vue/js前端组件、css/html页面、sql数据库初始化脚本、docx开发说明及配套论文与PPT压缩后约37MB目录结构便于按模块查阅。已有78人学习下载适合需要参考完整项目进行二次开发、撰写设计文档或快速部署验证的读者。随包还附有启动安装脚本、注意事项及Spring Boot开发说明能帮助降低环境搭建门槛是一份可运行、可拓展的实战型JavaWeb项目资料。1. 拿到一份基于java社区维修平台.zip先别急着解压从网盘下完这种带 .zip 的 Java 项目源码包大多数人第一件事就是解压、用 IDEA 打开、点运行然后被一堆红色报错劝退。这个标题看着像普通的 CRUD 课设但真正值钱的不是增删改查而是报修单从提交到验收的状态流转以及业主、维修工、管理员三种角色的权限边界。基于 java 社区维修平台本质是一个面向小区的维修协同系统业主提交报修管理员审核派单维修工接单并回填结果最后业主确认验收。它适合 java 基础已经入门、想完整走一遍 Spring Boot MyBatis MySQL 前后端联调的 java 学习者也适合正在选 java 课程设计案例源码的学生——这个题目业务闭环清晰规模可控不会像商城系统那样写着写着就膨胀。下面按“先设计、再跑通、后补坑”的顺序把这条路线完整讲一遍。2. 先画业务再写表社区维修平台的模型与数据库设计后端项目最怕边写边改表。我拿到维修平台这类题目第一件事是画流程而不是写 Controller。维修平台的核心不在“维修”两个字而在“状态”——一个报修单从业主提交到验收完成至少经历五个状态任何一步跳转错了用户看到的都是“卡住了”。2.1 三角色与一条主流程状态机先于代码定稿社区维修平台有三个角色业主、维修工、管理员。先列表格把每个角色能做什么定死后面接口才不会随便开洞。角色核心权限典型操作业主提交报修、查看进度、验收评价创建报修单、查看维修进度、确认完成维修工接单、回填处理结果查看待接单列表、更新维修状态、登记费用与材料管理员审核派单、改派、统计审核报修单、指派维修工、取消异常单、查看报表这个表定的不只是功能列表更是接口的 URL 前缀和权限边界。常见做法是业主端走/api/owner/**维修工走/api/worker/**管理员走/api/admin/**后续拦截器直接按路径前缀判断角色不用在每个业务方法里重复写if (user.getType() 1)。接着定义状态机这是整个平台最容易出错的地方。一个报修单从生到死我用六个状态码表示状态码含义允许的后续状态S0待派单S1 已派单、C 已取消S1已派单S2 维修中、C 已取消S2维修中S3 待验收S3待验收S4 已完成、S2 退回维修S4已完成终态C已取消终态提前把状态机定下来前端下拉框选项、后端枚举、数据库字段默认值都能对齐。更重要的是接单和派单这类操作必须校验“当前状态是否允许跳转”否则一个已取消的单子还能被维修工接走用户就会看到界面里闹鬼。我把状态统一写成S0到S4这样的短字符串而不是中文是为了数据库索引更友好也避免前端传“维修中”三个字时编码不一致导致查不到数据。2.2 六张核心表字段设计决定了后面少改多少代码一个能真正跑起来的社区维修平台最少需要六张表。课程设计里常见的做法是用户表、报修单主表、图片表、派单记录表、评价表和操作日志表。其中报修单主表和派单记录表能不能建好直接决定后面要写多少次JOIN。表名职责关键字段user账号主体type 区分角色id、name、phone、password、typerepair_order报修单主表id、user_id、repair_type、description、address、status、version、worker_id、appointment_timeorder_image报修图片id、order_id、url、upload_timedispatch_log派单记录id、order_id、worker_id、operator_id、create_timeorder_comment验收评价id、order_id、worker_id、score、content、create_timeoperation_log操作审计id、order_id、operator_id、from_status、to_status、create_timerepair_order的建表 SQL 我一般写成下面这样这是很多可运行源码包里最常见的一种结构没有过度设计也留了并发控制的位置CREATE TABLE repair_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, user_id BIGINT NOT NULL COMMENT 报修业主ID, repair_type VARCHAR(32) NOT NULL COMMENT 维修类型水电/木工/家电等, description VARCHAR(500) COMMENT 问题描述, address VARCHAR(128) NOT NULL COMMENT 业主填写的小区地址, status VARCHAR(8) NOT NULL DEFAULT S0 COMMENT 状态机编码, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, worker_id BIGINT DEFAULT NULL COMMENT 接单维修工ID, appointment_time DATETIME DEFAULT NULL COMMENT 预约上门时间, completed_time DATETIME DEFAULT NULL COMMENT 实际完成时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, KEY idx_user (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报修单主表;几个字段的用途必须提前说明version经常被当成冗余忽略但它就是解决两个维修工同时抢单的关键更新时带上version 0作为条件只有影响行数为 1 的请求才算成功。status建索引很重要列表页基本都是按状态过滤查没有索引数据量到几万条点一次“待派单”就能明显感觉到慢。appointment_time用DATETIME而不是VARCHAR是为了后面统计报表能按时间段分组。字符集必须用utf8mb4业主描述里带 Emoji 时utf8mb3会直接保存失败这个学费不用交。2.3 授权模型一个登录态如何撑起三种入口很多 java 初学者在 user 表里放一个 type 字段然后每个接口自己查一遍用户角色。这种做法能跑但每加一个接口都要重复贴一段判断代码项目一膨胀就乱。我一般会在登录成功之后把用户对象放进 Session再通过一个拦截器统一处理Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { HttpSession session request.getSession(false); Object loginUser session null ? null : session.getAttribute(loginUser); if (loginUser null) { response.setStatus(401); return false; } request.setAttribute(currentUser, loginUser); return true; } }提示这里用getSession(false)而不是getSession()否则未登录请求也会被创建出一个空 Session白白占内存。这段代码做的事很直接所有受保护的接口必须先经过拦截器Session 里没有loginUser就返回 401有就放进 request 作用域。后面 Controller 里用CurrentUser.get()取当前用户不用每个方法都从参数里抠 HttpServletRequest。再把拦截器注册进配置Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/api/owner/**, /api/worker/**, /api/admin/**) .excludePathPatterns(/api/auth/login); }这里有个容易忽略的细节excludePathPatterns只放行了登录接口但图片上传、静态资源这些路径如果放在/api之外就不受拦截。所以把上传接口写在/api/owner/repair/upload下面更安全既受登录保护又能按角色控制谁能传图。登录后的角色判断我建议在拦截器里做一次路径前缀匹配例如/api/worker/**只允许type2的维修工访问这样 user 表的 type 字段才真正形成了访问边界。3. 把最小可运行版本跑起来骨架、接口与联调配置选型先定。课程设计最常见、也最能打 java 基础薄弱点的组合是 Spring Boot 2.7 JDK 8 MySQL 8 MyBatis。Spring Boot 3 需要 JDK 17如果本机 java 环境变量配置还停在 1.8就别为追新给自己挖坑。3.1 骨架与依赖JDK 8 还是 JDK 17先检查环境java -version看到 1.8老老实实用 Spring Boot 2.7看到 17 再用 3.x。无论哪个版本编译器版本、IDEA SDK 和 pom 里的java.version必须三者一致这是新手最容易当场卡住的一关。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.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这个组合的选型理由很简单Spring Boot 2.7 内置 Tomcat不需要单独装 java 容器MyBatis starter 自动装配 SqlSessionFactory省去一堆 XML 配置mysql-connector-j 是 MySQL 官方驱动的新名字老项目里那个mysql-connector-java在 8.0.31 之后已经改版别被 IDEA 的自动提示带偏。Lombok 用来省掉实体类的 getter/setter纯属课设体验优化不装也能写。然后是 application.ymlserver: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/repair?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 10MB mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true三个参数最容易漏serverTimezoneAsia/Shanghai不加MySQL 8 连上后时间字段会差 8 小时插入报修单的时间直接错位max-file-size不调成 10MB手机拍下来的报修图动辄 3MB默认 1MB 会直接上传失败map-underscore-to-camel-case不打开order_image映射到orderImage实体必翻车后面所有对象查出来都是 null。3.2 报修单创建与状态流转Controller-Service-Mapper 三层怎么写报修单创建是平台第一条核心链路。Controller 只负责收参数和回结果不能写业务判断。下面这个接口是业主端创建报修单的标准写法RestController RequestMapping(/api/owner/repair) public class RepairOrderController { PostMapping(/create) public Result create(RequestBody RepairOrderCreateDTO dto) { Long userId CurrentUser.get().getId(); Long orderId repairOrderService.create(userId, dto); return Result.ok(orderId); } }RepairOrderCreateDTO里一般就是repairType、description、address、appointmentTime四个字段用RequestBody接收 JSON。这里要注意 DTO 必须有无参构造和 setterJackson 反序列化全靠它们。CurrentUser.get()是拦截器里放进 request 的那个对象线程内有效比从 Controller 参数里一层层往下传干净。Service 层的 create 方法长这样关键点全在注解和状态初值上Transactional public Long create(Long userId, RepairOrderCreateDTO dto) { RepairOrder order new RepairOrder(); order.setUserId(userId); order.setRepairType(dto.getRepairType()); order.setDescription(dto.getDescription()); order.setAddress(dto.getAddress()); order.setStatus(S0); order.setVersion(0); repairOrderMapper.insert(order); return order.getId(); }这段逻辑里有两个隐藏约定第一状态初值必须由服务端写死成S0不能信任前端传来的任何 status第二insert后能直接拿到order.getId()是因为 mapper 里配置了useGeneratedKeystrue keyPropertyid没配的话这里返回的是 null后续图片记录、派单日志都没法关联。状态流转不能直接写 Mapper 的 update 裸 SQL而是要包一层“状态校验 乐观锁”。这是接单接口里最关键的片段update idupdateStatusWithVersion update repair_order set status #{targetStatus}, version version 1, worker_id #{workerId} where id #{id} and status #{expectStatus} and version #{expectVersion} /update这个 SQL 一次完成两件事把S1改成S2同时要求数据库当前状态必须是S1、版本号必须是传入的expectVersion。Java 侧调用后判断影响行数等于 1 说明抢单成功等于 0 说明订单已经被别人改过了直接返回“该订单已被处理”。没有这个where条件两个维修工同时提交后提交的人会覆盖前一个人的worker_id这就是典型的并发脏写。3.3 图片上传与静态资源映射路由总出错的第三件事报修单一定要带图不带图管理员没法判断该派谁。图片存储的常见做法是本地磁盘保存 数据库存相对路径不上传服务器、不引第三方 OSS课设和练习完全够用。上传接口PostMapping(/upload) public String upload(RequestParam(file) MultipartFile file) throws IOException { String dir /data/repair/images/; String ext StringUtils.getFilenameExtension(file.getOriginalFilename()); String filename UUID.randomUUID() . ext; file.transferTo(new File(dir filename)); return /images/ filename; }返回给前端的一定是/images/xxx.jpg这种相对路径绝不能把/data/repair/images/整段返回去。浏览器里输绝对路径当然能打开但前端部署到另一台机器时图片路径就全废了。相对路径要能访问还得加一层静态资源映射Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceLocations(file:/data/repair/images/); }这段配置的意义是把 URL 里的/images/**映射到磁盘物理目录。file:前缀不能丢Windows 下写file:D:/repair/images/Linux 下写file:/data/repair/images/斜杠方向不一样跨平台部署时是最容易翻车的地方。目录路径应该从application.yml里读不要像我上面这样硬编码在 Java 代码里——这个坑后面避坑章专门讲。3.4 前后端联调时间格式与跨域先改好前端传2025-01-06 14:30这种字符串后端用LocalDateTime接必须在实体 VO 上统一声明格式否则会报 Jackson 反序列化错误或者序列化出来变成一串带T的 ISO 字符串public class RepairOrderVO { JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime appointmentTime; }timezone GMT8这半句不要删不写的话 Jackson 会用服务器默认时区部署在 Linux 云主机上时容易出现“数据库时间对接口返回差 8 小时”的玄学。前端的预约时间控件、列表展示格式和这个 pattern 保持一致就少一次联调时的字段对齐。跨域问题在前后端分离时必现。前端跑在 8081 端口后端跑在 8080浏览器直接拦截。往上一个 WebMvcConfigurer 里追加Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); }这里的选择有讲究allowedOriginPatterns(*)配上allowCredentials(true)是当前版本支持的标准写法直接写allowedOrigins(*)会和allowCredentials冲突启动时直接报非法参数。maxAge(3600)是浏览器预检请求的缓存时间设成 3600 秒能减少一半以上的 OPTIONS 请求联调时明显感觉接口响应变快。4. 避坑社区维修平台最常见的五个翻车现场这个方向做得多了常见的坑基本就那几种。下面按出现阶段从环境到业务、再到联调部署逐条写现象、原因和解决。4.1 环境阶段项目一打开就编译失败现象IDEA 一编译就报警告“java: 警告: 源发行版 17 需要目标发行版 17”或者直接“无效的目标发行版: 17”。原因IDEA 的 Project SDK、Java Compiler 的字节码版本、Maven 的 compiler 配置三个地方不一致。最常见的场景是 pom 里写着 17本机装的是 JDK 8或者反过来本机是 17项目却按 JDK 8 的模板生成。解决先把 pom 里的版本统一锁死properties java.version8/java.version maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties然后打开 IDEA 的 Project Structure把 Project SDK 切到本机那个 JDK再把 Settings 里 Java Compiler 的 Target bytecode version 改成 8。改完跑一次mvn clean compile重新加载 Maven 项目。这条是翻车重灾区也是 java 面试基础题里“编译期版本和运行期版本区别”的活教材。4.2 MyBatis 查询结果全是 null现象selectById返回的对象不是 null但业务字段全是 null只有主键有值。原因数据库列名是下划线风格repair_type实体属性是驼峰repairTypeMyBatis 默认不会自动映射下划线到驼峰。解决在application.yml里把开关打开mybatis: configuration: map-underscore-to-camel-case: true如果用的还是老式 XML 配置可以在每个查询列里手写AS repairType但那样太啰嗦。打开全局开关全项目一次性生效是最省事的做法。4.3 两个维修工同时抢单状态被覆盖现象同一张报修单两个维修工都显示可以接后提交的人把前一个人的worker_id覆盖掉甚至出现已取消的单被接走。原因update 语句只写了set status S2没有在 where 里带状态条件。两个请求同时读到S1同时提交数据库串行执行后都成功但逻辑上是错误的。解决用第 3.2 节那个乐观锁 updatewhere id #{id} and status #{expectStatus} and version #{expectVersion}。Java 侧根据影响行数判断返回 0 就提示“该订单刚被其他维修工接走”不要强行刷新界面。这是数据一致性最典型的场景也是 java 面试里“你怎么保证数据一致性”这道题的标准答案素材。4.4 事务回滚不生效现象报修单插入成功但插入图片记录时报错结果报修单还留在数据库里。原因事务方法被同类内部直接调用Transactional注解失效或者方法不是public或者异常被try-catch吞掉了。解决把事务方法拆到另一个 Service 类里Controller 调用的是外层 Service外层再调内层的事务方法事务方法必须public不要在 catch 块里只打印日志不抛出要throw new RuntimeException(e)让 Spring 感知到异常并回滚。记住一个判断标准事务要生效调用链里必须有 Spring 管理的 Bean 作为边界。4.5 图片上传成功页面回显 404现象上传接口返回了路径浏览器打开图片链接 404但文件确实存在磁盘上。原因Controller 把磁盘绝对路径直接返回给了前端前端拿这个路径当然访问不到还有一种原因是保存路径和addResourceHandlers映射的目录不一致比如上传写到/data/repair/images/映射配成file:/data/repair/images/但末尾少了斜杠。解决统一用“相对路径返回 静态资源映射”这个组合。磁盘物理路径放进application.ymlrepair: upload-dir: /data/repair/images/Controller 里读取配置拼路径返回/images/ filename前端拿到的始终是 URL 路径而不是磁盘路径。上传和映射用同一个配置来源就不会出现两边各写各的目录。5. 再往上走一步把课设级的维修平台做成能演示的系统5.1 用 Redis 给派单列表降温做完完整 CRUD 后第一个值得加的东西是缓存。社区维修平台里查得最频繁的是管理员和维修工反复刷的“待派单/待接单”列表每次刷新都全表扫一遍where status S0纯属浪费。常见做法是用 Spring Cache 套一层 30 秒的读缓存Cacheable(cacheNames repair:pending, key list) public ListRepairOrderVO pendingList() { return repairOrderMapper.selectByStatus(S0); }加了EnableCaching后这个注解就能生效。但要注意缓存的是只读列表状态一变更必须立刻失效缓存否则维修工看到的还是已经被别人接走的单。CacheEvict要放在状态变更的 Service 方法上和事务在同一个边界内保证读写一致。5.2 用 WebSocket 把进度推到业主页面上轮询也能做进度通知但体验太差。报修单状态落到 S3 时业主页面能立刻弹一条“维修完成请验收”观感比 F5 强很多。Spring Boot 里用消息模板一句就能推送messagingTemplate.convertAndSend(/queue/order/ orderId, newStatus);前提是 pom 里加spring-boot-starter-websocket再注册一个/queue前缀的 Broker 端点前端通过 STOMP 订阅对应目的地。这个功能我通常放在最后一个下午接因为它不影响主流程纯粹是答辩现场的加分项。5.3 自己验收按这份清单过一遍才算真正做完检查项通过标准登录权限隔离业主 Token 访问管理端接口返回 401/403状态流转同一张单从 S0 到 S4 每一步有记录非法跳转被拒并发抢单两个浏览器同时点接单只有一个人成功数据一致性报修单和图片记录要么都成功要么都回滚图片访问上传后前端能直接打开图片 URL刷新不失效我做这类项目有个习惯把它当成 java 面试八股文的实战考场。每跑一遍验收清单就等于亲手回答了“java 怎么保证数据一致性”“事务传播行为在项目里怎么用”这一类必考题。先跑通再精修别让表结构挡住上线——这是我在课设这条路上最深的教训。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站