校园里最容易让人心烦的不是课业压力而是“东西坏了不知道找谁报修”比如宿舍空调漏水、教室投影仪黑屏、实验室设备突然罢工。传统的报修方式不是靠微信群吼就是靠手写登记表流程长、进度不可见、责任不清晰还容易漏单。而一个基于SpringBoot的校园设备维护报修系统能够把“报修—审核—指派—维修—反馈—归档”整条链路搬到线上让所有角色在系统里协作彻底告别纸质工单和消息刷屏。我最近刚完成一个这样的系统从需求梳理、数据库设计到核心业务实现踩了不少坑也沉淀了一些比较实用的经验。这篇内容适合正在做课程设计或毕业设计的在校生也适合第一次接触SpringBoot全栈开发、想把一个多角色业务系统跑通的新手开发者。这里有闭环的报修流程设计、三端角色的权限模型、状态机的状态流转实现还有真实项目中很容易踩到的时间、图片上传、依赖管理雷区。照着走能帮你避开弯路。1. 先理清报修系统的需求边界再做技术选型很多人在起步阶段就走偏了——一上来就建项目、写Controller、画表结构结果做了一半发现角色权限分不清、流程走不通。做任何业务系统第一步一定不是写代码而是把“谁在什么场景下要做什么事情”梳理清楚技术选型才有依据。1.1 三类角色和三条主流程校园报修系统最常见的角色有三类报修人学生/老师提交报修单、查看处理进度、确认验收、对服务作评价。维修工接单、填写处理结果、补充维修费用和备件信息。管理员总务/后勤审核报修单、指派维修工、处理超时工单、管理账号和数据统计。这三个角色对应的三条主流程我建议画成三条独立的泳道来梳理报修流程报修人创建工单 → 管理员审核 → 管理员指派维修工 → 维修工接单 → 维修工维修并填写结果 → 报修人确认 → 闭单归档。这条链路中每个环节的负责人必须唯一比如“指派”必须落在管理员头上不能让维修工自己抢单虽然抢单也是一种模式但在校园场景下按区域/技能指派更合理管理员统一调度才能平衡工作量。1.2 技术栈选型的真实考量技术选型不要追新要追求“稳定上手、生态成熟、团队熟悉”。我这次用的是后端框架SpringBoot 2.7.x基于JDK 8这个组合的坑比较少网上资料也最多遇到问题几乎都能搜到现成方案。SpringBoot 3.0虽然也不错但如果你还在用JDK 8且部分老依赖需要兼容那2.7是更稳的选择。持久层MyBatis-Plus因为它内置了分页插件、代码生成器、逻辑删除和自动填充写报修单这类增删改查居多的业务能省下大量重复代码。如果你更喜欢Hibernate/JPA那套对象映射也行但如果团队不熟请不要在这里增加学习成本。数据库MySQL 8.0重点是支持JSON字段和窗口函数后续做统计报表会方便很多。前端这里要诚实地分情况。如果是毕业设计/课设Bootstrap Thymeleaf这套服务端渲染方案效率首选——不用处理跨域、不用单独部署前端、模板改完刷新就能看效果。如果是为了项目简历好看展示前后端分离能力Vue3 Element Plus Axios是更合适的组合。我这次因为要快速验证业务逻辑选了Thymeleaf Bootstrap把精力集中在后端设计上。关于ID生成我强烈建议不要用自增ID作为对外展示的单号。报修单号建议用时间戳随机数拼接比如“BX20240517153022001”既方便用户口头报单号又避免自增ID泄露业务量。这个细节虽然小但真正投入使用才会发现它的价值。2. 数据库表设计决定了这个项目能走多远业务系统的核心在表结构。报修系统的表设计如果只想着“能存数据”那做出来充其量是个登记表谈不上管理。我按“基础用户表 工单主表 流程记录表 支撑维表”的思路设计了10张表这里重点说说其中花费最多心思的4张。2.1 用户表sys_user——一张表容纳三种角色角色我采用字符串存储理由是不需要引入独立的关系表对于三个固定角色来说一张用户表加一个role字段就够了。CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT BCrypt加密密码, real_name varchar(30) DEFAULT NULL COMMENT 姓名, role varchar(20) NOT NULL COMMENT ROLE_STUDENT / ROLE_WORKER / ROLE_ADMIN, phone varchar(20) DEFAULT NULL, building varchar(50) DEFAULT NULL COMMENT 所在楼宇报修人时常需要, status tinyint DEFAULT 1 COMMENT 1启用 0禁用, create_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;密码我统一用BCrypt加密千万不要用MD5。MD5加不加盐都已经能被彩虹表秒破让学生或维修工的密码明文暴露是典型的低级失误。Spring Security自带BCryptPasswordEncoder几行代码就能接入。2.2 报修单主表repair_order——核心中的核心报修单表是这个系统的灵魂字段设计直接影响后面流程能走多顺。我先列出核心字段再说明几个容易被忽略但实际很重要的字段CREATE TABLE repair_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 报修单号展示用, title varchar(100) NOT NULL COMMENT 报修标题, description text COMMENT 详细描述, image_urls varchar(500) DEFAULT NULL COMMENT 图片逗号分隔, priority tinyint DEFAULT 2 COMMENT 1紧急 2普通 3低, category varchar(30) DEFAULT NULL COMMENT 水暖/电力/设备/网络/其他, location varchar(100) NOT NULL COMMENT 具体位置, report_user_id bigint NOT NULL COMMENT 报修人, assign_user_id bigint DEFAULT NULL COMMENT 当前维修工, handle_user_id bigint DEFAULT NULL COMMENT 实际处理人, status tinyint NOT NULL DEFAULT 1 COMMENT 状态由代码层控制流转, report_time datetime NOT NULL, assign_time datetime DEFAULT NULL, finish_time datetime DEFAULT NULL, confirm_time datetime DEFAULT NULL, cost decimal(10,2) DEFAULT NULL COMMENT 维修费用, worker_comment varchar(255) DEFAULT NULL COMMENT 维修工备注, user_comment varchar(255) DEFAULT NULL COMMENT 用户评价, del_flag tinyint DEFAULT 0, PRIMARY KEY (id), KEY idx_status (status), KEY idx_report_user (report_user_id), KEY idx_assign_user (assign_user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报修订单表;哪些字段容易忽略第一个是assign_user_id和handle_user_id分开的意义。分派给谁和实际处理人未必一致比如管理员指派了张三张三临时不舒服李四去帮忙处理这时候记录实际处理人非常关键月末统计工作量要以handle_user_id为准而不是指派字段。第二个是priority。有的人喜欢用字符串存“紧急/普通”但实际排序、筛选的时候字符串极其难用用tinyint存储再用字典表翻译查询和排序都高效得多。第三个是单独的report_time、assign_time、finish_time、confirm_time等时间字段。不要只用一个 update_time 来推断流程节点否则想统计“平均维修时长”时你会发现无从下手。建这么多时间字段报表需求来了能直接聚合不用改表。2.3 操作流水表repair_log——每一步都有迹可循校园报修系统虽然业务不复杂但“这个单子现在卡在谁手里了”是个高频追问。所以每一次状态变更都要落一条日志包括操作人、操作时间、从什么状态到什么状态、备注内容。CREATE TABLE repair_log ( id bigint NOT NULL AUTO_INCREMENT, order_id bigint NOT NULL, operator_id bigint NOT NULL, from_status tinyint DEFAULT NULL, to_status tinyint DEFAULT NULL, remark varchar(255) DEFAULT NULL, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报修操作日志表;流水表除了让管理员能追溯操作还有一层重要作用给“超时工单”判断依据。比如状态停在“待指派”超过24小时就说明管理员漏审了可以通过定时任务扫描流水表把超时单捞起来提醒。2.4 支撑维表building category——别忽视楼宇表和故障分类表看似可有可无但在真实使用中非常必要。楼宇表能支撑“这个宿舍楼有哪些待处理工单”这种查询故障分类表能支撑“灯具类平均耗时多少天”的统计。维表让数据能够被切片分析而不是永远只能看全量列表。如果一开始没建后续补数据和代码改动的工作量会成倍增加。3. 报修单的生命周期状态机设计是核心中的核心状态管理是报修系统最容易写乱的部分。成熟的思路只有一个不要把状态流转逻辑散落在业务代码的各个角落而是收敛成一张状态机表由代码统一校验。3.1 定义状态和允许的动作我给报修单定义了6个状态、7个动作状态含义允许执行的动作1待审核用户提交通过、驳回2已驳回重新提交3待派工审核已通过指派维修工4维修中维修工已接单完成维修、转派5待确认维修完成确认闭单、回退6已关闭重新打开如果问题复发我实际实现中不写大段的if-else而是维护一个配置结构当前状态 操作动作定位到唯一的目标状态和对应处理器。不合法流转直接在入口处抛出异常通知前端“这个操作不被允许”。这样做的好处有两个第一是前端无论如何操作都不可能把工单状态搞乱第二是新增一个状态或动作时只需要改配置不需要翻遍整个项目找散落的逻辑。3.2 为什么服务端必须再做一次状态校验很多前端做了按钮隐藏比如待审核状态下不给显示“完成维修”按钮就以为安全了。但前端校验只是用户体验层面的事情接口安全性必须靠后端。如果服务端不做状态机校验攻击者直接调用/api/order/complete接口传单号就能把任意单据伪造成完成状态。而状态机校验的代码逻辑很简单入口统一在RepairOrderService.updateStatus(orderId, action, operator)方法里所有状态变更都必须经过这个方法不接受业务方直接通过Mapper去update状态字段。3.3 状态流的核心代码简化示例public class OrderStatusMachine { private static final MapInteger, MapString, Integer STATE_ACTION_MAP new HashMap(); static { // 状态1(待审核) 动作approve - 状态3(待派工) addTransition(1, approve, 3); addTransition(1, reject, 2); addTransition(2, resubmit, 1); addTransition(3, assign, 4); addTransition(4, complete, 5); addTransition(4, transfer, 4); addTransition(5, confirm, 6); addTransition(5, rollback, 4); addTransition(6, reopen, 4); } public static Integer next(Integer currentStatus, String action) { MapString, Integer map STATE_ACTION_MAP.get(currentStatus); if (map null) { throw new BusinessException(当前状态不支持任何操作); } Integer nextStatus map.get(action); if (nextStatus null) { throw new BusinessException(当前状态下不允许执行该操作); } return nextStatus; } }这块代码是业务稳健的关键。我这个系统测试到后期大量问题都源于状态机校验没覆盖到位——比如已关闭的单子被重复评价、待审核的单子被直接派工统统靠这个配置类上面的校验拦了下来。4. 权限设计三端接口如何防止越权报修系统有三个角色权限模型如果用粗暴的角色判断也能实现但容易写成“到处写if判断用户角色”。我在这个项目里使用了Spring Security 自定义注解的方式把权限控制收敛到注解层。4.1 接口权限矩阵先明确哪些接口哪些角色能访问总结下来是这样一张矩阵接口功能学生维修工管理员提交报修单是否否查看自己的报修单是是处理中的是全部审核报修单否否是指派维修工否否是接单/完成维修否是是确认闭单是否是数据统计否否是这样做的好处是权限规则集中成一张表评审答辩的时候拿出来非常清楚。实际代码里用PreAuthorize(hasRole(ADMIN))就能把接口锁死额外的好处是后续如果要加一个“院系管理员”角色只需要在资源配置里增加一个映射不需要改动业务接口。4.2 数据权限行级越权防范角色控制只解决“谁能访问这个接口”真正难的是行级越权——一个学生登录后调用查看接口把orderId改成别人的单号能不能看到别人的信息这是做系统时必须默认攻击者会做的事。解决思路也很直接所有对报修单的查询都必须附带“数据归属条件”。学生查单时会自动拼接and report_user_id 当前登录用户id维修工查单时会拼接and (assign_user_id 当前用户 or handle_user_id 当前用户)管理员不做行级限制。这个逻辑在MyBatis-Plus中可以用自定义拦截器实现也可以在Service层强制拼条件。我倾向于在Service层拼因为更直观、更好调试。开发中最常犯的错误就是写完通用查询接口后忘记验证不同角色传入不同orderId时的数据隔离效果。我建议正式测试前专门建两个测试账号互相查对方单号这是最容易暴露行级越权的测试方法。5. 核心业务实现不只是增删改查既然叫“系统”而不是“表单”就必然要有比CRUD更完善的业务逻辑。这块也是容易拉开项目档次的地方。5.1 报修单提交图片上传的妥善处理学生报修时图片能帮助维修工快速定位问题。图片上传我这里存的是本地磁盘路径没有用对象存储——考虑到是校园项目用OSS反而增加配置和费用。实现上我把文件保存到服务器的一个独立目录数据库里存的是访问用的相对路径。但需要注意本地存储要考虑重启后路径失效的问题所以建议把上传目录配置在application.yml里并通过配置文件注入千万不要写死在代码中。upload: dir: /data/repair/uploads/ access-path-prefix: /upload/**对于图片数量限制建议不超过3张超过后即使功能允许我也会在前端做限制。原因是维修工视角要快速浏览工单信息而非翻图库过多的图片会降低处理效率。5.2 指派维修工用楼宇区域自动筛选候选管理员指派时如果候选维修工是全部列表在小范围如20人没问题但校园往往有多个校区、多栋楼宇手工找合适的维修工很累。我给每个维修工增加了一个service_buildings字段逗号分隔的楼宇ID指派页面可以根据报修单的location自动筛选出服务范围覆盖该楼宇的维修工管理员只需在候选列表里做最终确认。这个功能逻辑简单但实际使用中好评率最高因为大大降低了手工筛选成本。5.3 分页查询的性能细节报修单列表在工单量大之后如果不加索引分页会越来越慢。我给经常查询的status、report_user_id、assign_user_id都建了索引查询时统一用MyBatis-Plus的分页插件具体分页参数用前端传入的current和pageSize默认pageSize10。这里还有一个容易踩的小坑前端传的页码必须从1开始而后端在真正SQL层面要转换为offset (current - 1) * pageSize这个逻辑MyBatis-Plus内部已经处理但如果自己写LIMIT语句千万留意减1的细节。我见过不少同学自己拼SQL时第一页和第二页数据重复原因就在这里。5.4 定时任务超时工单自动提醒报修系统如果没有超时监测就很容易出现工单被遗忘。我实现了两个定时任务用Spring自带的Scheduled完成。scanPendingOrder每30分钟扫描一次“待审核超过12小时”的单子自动给管理员发送站内信提醒标记为“待审核超时”。scanProcessingOrder每60分钟扫描一次“待维修超过48小时”的单子给维修工和管理员发送提醒同时在列表页给该单打上“超时”标红标签。定时任务表达式建议做成可配置的放在配置文件中方便现场调试时把扫描间隔调短。比如cron.pending0 */30 * * * ?这样上线后调整不需要重新打包。6. 从开发到部署那些实测才发现的坑这一节全部来自我实际调试中的真实问题。每个坑都不深但如果不写出来照着网上的教程做很容易卡住很久。6.1 时区问题数据库时间和日志时间差8小时这是老生常谈但依然高频翻车的坑。MySQL连接串上如果没有加serverTimezoneAsia/Shanghai或者应用服务器和数据库服务器时区不一致会发现列表页显示的时间和实际时间差8小时。解决方案是在JDBC连接串上显式指定时区同时存储时间统一用datetime不要用timestamp——因为timestamp会随会话时区改变而改变而datetime就是存进去什么显示什么更可控。6.2 MyBatis-Plus自动填充失效问题我给create_time和update_time配置了字段自动填充但第一次实际测试时发现数据没写入时间。原因是没有在字段上指定TableField(fill FieldFill.INSERT)和TableField(fill FieldFill.INSERT_UPDATE)且自定义的MetaObjectHandler没有生效。自查方法很简单确认MetaObjectHandler实现类被Spring扫描到了吗字段注解写对了吗两个都对时间才会正确填充。6.3 跨域问题开发环境通过Vue访问后端接口如果你是前后端分离的方案解决跨域别只在后端配CrossOrigin完事。生产环境就要通过Nginx把/api/前缀反向代理到后端服务保持前端域名与接口域名一致彻底消除跨域的可能。这样线上才更稳定也不会出现浏览器拦截请求的情况。开发环境可以用后端全局CORS配置临时解决Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意这里addAllowedOriginPattern(*)和addAllowedOrigin(*)的区别。后者在SpringBoot新版中和Credentials携带Cookie一起用时会被拒绝必须用AllowedOriginPattern这也是我实际调试中踩过的一个隐蔽点。6.4 逻辑删除让唯一索引失效的问题我给报修单增加了del_flag字段实现逻辑删除但随后发现一个严重问题如果对order_no建了唯一索引逻辑删除后再次提交相同单号时会因为逻辑数据还在表里而插入失败。解决思路有几种放弃对order_no的唯一约束因为每次生成的单号本身冲突概率极低或把del_flag纳入唯一索引做联合唯一约束。我采用的是前者因为保留唯一索引的意义不大生成规则本身已经保证了唯一性。7. 进阶能力让这个系统提升一个档次如果你希望这个项目在简历或答辩中更有说服力基础CRUD之外建议增加以下扩展功能难度不高但效果很显著。7.1 WebSocket实时进度通知学生提交报修后最关心的就是“什么时候有人来处理”。我用WebSocket实现了实时推送状态变化时系统自动给学生端推送进度通知。具体是前端建立WebSocket连接后端在状态流转成功后通过SimpMessagingTemplate推送消息。启动后效果非常直观——提交工单、刷新页面、管理员一审核学生端立刻弹出一条消息“你的报修单已审核通过等待分配维修工”。这种体验感能大幅度提升项目完成度。7.2 维修评价与满意度统计闭单后可以让学生填写评价包括评分1-5分和文字备注。把评价作为独立的数据指标管理员端查看统计时能看到各维修工的月度平均分。这个功能做完整个系统就有了闭环反馈的逻辑不单单是一张流水线工单表。7.3 备件和费用登记维修过程中如果涉及更换零件或费用最好允许维修工记录备件名称、数量和费用。管理员在月末统计时可以看到每个工单的成本对后勤预算管理是有实际价值的。这个功能的表结构很简单一张repair_part表关联repair_order加一个cost字段就能支撑“本季度维修支出分析”这样的报表需求。7.4 常见故障知识库维修工在接单时如果发现是常见问题比如某型号空调经常报错代码E4可以直接在工单详情页关联到知识库条目学生和管理员也能看到。这个小功能本质是一张内容表加一个关联字段实现成本低但答辩或者写报告时能体现你考虑了“效率复用”这一层还挺加分的。8. 这套系统跑通后的真实体会做一个校园报修系统技术难度不算高但它是一个标准的“多角色、多状态、强流程”业务系统做好它的关键在于清晰的需求梳理和严谨的状态管理。在我做完这套系统之后最深的体会有两点。第一点是状态机设计越早越好。不要等代码写了一半再回头整理状态那时候散落在各个方法里的状态判断会让你改一处挂三处。先花半天时间用表格整理完整的状态流转图后面编码能省出好几天的调试时间。第二点是权限设计必须从第一天就考虑。如果觉得“先把功能跑通再说权限”后面补权限时你会发现每一个查询方法都要回头改条件代码侵入性特别大。正确做法是在构造查询条件的时候就默认拼接数据归属条件哪怕当前只有一个角色也要把条件封装成可以扩展的形式。另外关于部署有条件的话建议直接在服务器上用Docker跑MySQL和SpringBoot应用配置也一键化。直接用spring-boot-maven-plugin打成jar包用java -jar跑也行但要记得给JVM设置内存参数避免默认堆内存过大导致服务器卡死java -Xms256m -Xmx512m -jar repair-system.jar --spring.profiles.activeprod如果走Docker路线一个简单的docker-compose.yml就能把MySQL和应用编排起来version: 3 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: repair_db ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql app: build: . ports: - 8080:8080 depends_on: - mysql environment: SPRING_PROFILES_ACTIVE: prod volumes: mysql_data:最后想分享的是这类校园项目真正拉开差距的不是用了多新的技术而是业务闭环是否完整、边界情况是否考虑周全。如果你能把一个报修系统的状态流转、权限控制、超时提醒、数据统计这些环节都理顺那你在简历上写“熟练使用SpringBoot开发多角色业务系统”就完全有底气了。希望这套经验能帮你的项目少走弯路。
阅读完成 · 觉得有帮助?