
做校园设备报修系统这个选题大概率是两类人一类是计算机相关专业的毕业生拿它当毕业设计另一类是学校信息化部门的老师或外包开发者想找一个能落地的后勤报修方案。不管你是哪一类SpringBoot 校园设备报修这两个关键词组合在一起本身就是一套非常典型的管理信息系统练手项目——它涵盖了用户角色划分、流程状态流转、文件上传、数据统计这些高频需求同时又比电商、社交这类项目简单不少非常适合用来快速上手SpringBoot的完整开发链路。我今年上半年刚帮一所职业院校完整落地过一套类似的校园设备报修系统从需求梳理到部署上线前后花了三周左右中间踩了不少坑也攒了一些实战经验。这篇文章我就用这套系统的真实开发过程作为主线把SpringBoot在校园报修场景下的技术选型、核心模块设计、流程实现和部署运维整个链路完整拆开来讲希望能给准备做类似系统的同学一个可以直接参考的路线图。1. 系统整体设计与技术选型思路1.1 核心需求解析校园报修系统到底在管什么校园设备的报修维修和普通企业的工单系统有一个本质区别它的服务对象是流动性极强的师生群体报修设备种类杂多媒体教室的投影仪、实验室的电脑、宿舍的空调、食堂的设备而且维修响应速度直接影响到教学活动能不能正常进行。所以这套系统的核心需求可以拆成四句话师生能快速提交报修单不需要装任何客户端浏览器打开就能用。维修工能收到派单提醒处理完要回填维修结果和耗材使用情况。管理员能掌握全局数据比如哪个区域报修最多、哪个维修工处理最慢、哪些设备故障率最高。整个报修流程要有状态可查师生能随时看到自己的单子处理到哪一步了。这四句话翻译成技术语言就是三个核心模块角色权限管理模块、报修流程状态机模块、数据统计与可视化模块。配合上通知提醒一般是短信或微信模板消息也可以用站内信整个系统的骨架就出来了。1.2 为什么选SpringBoot MyBatis-Plus这套组合关于技术选型我看过不少同学的毕设代码有人用SpringBoot JPA有人用SpringBoot MyBatis原生还有人非要上微服务那套把简单的报修系统拆成五六个服务。我的建议非常明确单体应用 SpringBoot MyBatis-Plus这是最稳的组合没有之一。理由也很实在。校园报修系统的并发量撑死了也就几百人同时在线单体应用完全扛得住。SpringBoot最大的价值在于它把Spring繁琐的XML配置全部干掉你只需要在application.yml里写几行配置就能把一个可运行的Web服务跑起来——这对新手极其友好。而MyBatis-Plus则解决了MyBatis最让人头疼的问题CRUD还要自己写SQL。它内置的BaseMapper接口让你连selectById()这种方法都不用写直接调用就行。我举个实际例子你要实现分页查询所有待派单的报修记录按提交时间倒序用MyBatis-Plus只需要这样写// ServiceImpl中的代码 Override public IPageRepairRecord getPendingPage(int current, int size) { LambdaQueryWrapperRepairRecord wrapper new LambdaQueryWrapper(); wrapper.eq(RepairRecord::getStatus, PENDING) .orderByDesc(RepairRecord::getCreateTime); return this.page(new Page(current, size), wrapper); }如果换成原生MyBatis你要写XML映射文件、写SQL语句、写PageHelper的分页插件配置工作量至少多出三倍。对于校园报修这种CRUD占大头、业务逻辑并不复杂的项目MyBatis-Plus是性价比最高的选择。1.3 角色权限模型三种身份的边界划分整个系统的权限模型我设计为三角色学生报修人、维修工、管理员。这里有一个很关键的设计决策不要自己硬编码角色判断而是用SpringSecurity JWT来做统一的认证和授权。曾有同学图省事直接在每个Controller里加if(user.getRole() 1)这样的判断后患无穷。一旦你要加一个新角色比如楼栋管理员你得把所有if全翻一遍。我采用的是基于注解的权限控制RestController RequestMapping(/api/repair) public class RepairController { PreAuthorize(hasRole(STUDENT)) PostMapping(/submit) public Result submit(RequestBody RepairSubmitDTO dto) { // 学生提交报修单 } PreAuthorize(hasRole(WORKER)) PostMapping(/handle/{id}) public Result handle(PathVariable Long id, RequestBody HandleDTO dto) { // 维修工处理报修单 } PreAuthorize(hasRole(ADMIN)) GetMapping(/stats/category) public Result categoryStats() { // 管理员查看分类统计 } }这样做的最大好处是权限规则集中在注解上一眼就能看出接口谁能访问。而且JWT无状态认证天然适合前后端分离的场景——Vue前端拿到token存到localStorage里每次请求通过Authorization头带过来后端通过过滤器校验并解析出当前用户信息整个过程不需要Session。需要注意的是hasRole(STUDENT)和hasAuthority(student)的细微区别。Spring Security的hasRole会自动加上ROLE_前缀所以你在代码里写hasRole(STUDENT)那么在授权表或角色表里存的角色标识就是ROLE_STUDENT。这个细节搞混了权限怎么调都调不通。2. 核心流程与数据库设计实战2.1 报修流程状态机从提交到完成的完整链路校园报修最核心的业务逻辑是流程状态流转。我在设计时画了一条明确的状态链待派单(PENDING) - 已派单(DISPATCHED) - 维修中(PROCESSING) - 已完成(COMPLETED) - 已驳回(REJECTED)为什么要有待派单和维修中这两个状态分开管理因为校园场景下报修单往往会先经过管理员统一分派再通知具体维修工。如果让学生直接选维修工很容易出现某个维修工忙死、另一个闲死的情况。所以我在表设计里让assignee_id和status联动只有管理员或者系统自动分派完成状态才从PENDING变成DISPATCHED维修工点开始处理后变成PROCESSING处理完回填结果后变COMPLETED。在实际代码里我封装了一个状态流转的核心方法避免各业务层随意修改状态值public boolean transition(Long recordId, String expectedFrom, String targetTo) { LambdaUpdateWrapperRepairRecord wrapper new LambdaUpdateWrapper(); wrapper.eq(RepairRecord::getId, recordId) .eq(RepairRecord::getStatus, expectedFrom); // 乐观锁思想 RepairRecord update new RepairRecord(); update.setStatus(targetTo); return this.update(update, wrapper); }注意这句.eq(RepairRecord::getStatus, expectedFrom)——它利用数据库层面的行锁效果确保状态只能从指定的前置状态流转过来。如果两个人同时操作同一张单只有一个能成功另一个更新行数为0直接被接口层拦截并提示状态已变更请刷新。这就是最朴素的乐观锁思路对付报修单这种低频操作绰绰有余没必要引入专门的分布式锁。2.2 数据库表设计九张表的完整拆解这套系统的数据库我刚建的时候只设计了三张表——用户表、报修表、评论表结果做到权限模块就发现不对劲角色和菜单没法关联维修工和维修区域没法绑定。后来我完整敲定了九张表这里列出来给大家做个参考表名作用关键字段user所有用户学生/维修工/管理员id, username, password, role, real_name, phonerepair_record报修单主表id, user_id, device_type, location, description, photo_url, status, assignee_id, create_timerepair_assign_log派单日志id, record_id, assigner_id, assignee_id, assign_time, noterepair_handle_detail维修处理明细id, record_id, handler_id, handle_result, part_used, cost, handle_timerepair_evaluation师生评价id, record_id, user_id, rating, content, create_timedevice_category设备分类id, name, parent_id, sort_orderbuilding_info楼栋教室信息id, building_name, room_name, area_tagnotification通知消息id, user_id, title, content, is_read, create_timesys_menu / user_role权限辅助表视权限方案而定核心表之间的关联逻辑repair_record表的user_id关联提交人assignee_id关联维修工device_type关联设备分类location可以直接存楼栋加教室的文本也可以关联building_info表做规范化。repair_handle_detail和repair_record是一对一关系但单独拆表会让日志记录更清晰以后要查某个维修工总共换了多少耗材直接对repair_handle_detail做聚合就行完全不用碰主表。有个细节特别容易被忽略图片存储字段。报修时让学生拍照上传这个photo_url字段不要直接存磁盘绝对路径因为部署后路径会变。我建议统一存相对路径配合配置项在读取时拼接完整访问地址这样系统迁移时不会有历史数据全部失效的问题。2.3 自动派单策略一个极其实用的算法校园报修系统比企业工单系统多一个闲时没人管的问题——维修工不可能24小时候着学生晚上报修的单子经常积压到第二天早上。我实际做的时候给系统加了一个简单的自动派单算法public Long autoAssign(RepairRecord record) { // 找到该设备类型擅长且当前待处理单量最少的维修工 ListWorkerLoadDTO candidates workerMapper.findAvailableWorkers(); // 按待处理单量升序排序返回第一个 return candidates.stream() .filter(w - w.getSkillTags().contains(record.getDeviceType())) .sorted(Comparator.comparingInt(WorkerLoadDTO::getPendingCount)) .map(WorkerLoadDTO::getWorkerId) .findFirst() .orElse(null); }逻辑很简单就是擅长优先 负载均衡。先筛出懂这个设备类型的维修工再按他们当前待处理单量排序把单子给最闲的人。如果所有人都没有空或者是下班时间就退回给管理员手动派单。这个小功能实测下来非常有用能减少管理员80%的派单工作量。3. 核心功能模块实现与实操要点3.1 报修单提交表单设计、图片上传与防重复提交报修单提交是整个系统用户体验的起点也是信息质量的关键所在。我见过很多系统的报修表单就几个字段姓名、电话、问题描述。这样的表单交上来之后维修工还得打电话问到底哪个教室哪个设备坏了沟通成本极高。我落地的表单是这样设计的楼栋下拉选择、教室号下拉或手动输入、设备类型级联选择、故障描述必填不少于10个字、故障图片可选拍照、预约时间可选。关键点在设备类型我用了两级级联比如多媒体设备 - 投影仪、空调 - 挂机、网络 - 有线网络不通这样维修工接到单子后基本不用再确认哪坏了。图片上传我用的是本地存储 Nginx静态映射方案没有引入OSS。对于学校内部系统几百GB的图片存储需求本地磁盘完全扛得住。上传接口的核心代码PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(请选择文件); } // 校验文件类型只允许jpg/png限制大小2MB String originalName file.getOriginalFilename(); String ext originalName.substring(originalName.lastIndexOf(.)).toLowerCase(); if (!Arrays.asList(.jpg, .jpeg, .png).contains(ext)) { return Result.error(仅支持jpg/png格式); } // 用UUID重命名避免文件名冲突 String newName UUID.randomUUID().toString().replace(-, ) ext; String datePath LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy/MM/dd)); File dir new File(uploadPath / datePath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, newName)); return Result.success(/upload/ datePath / newName); }这里注意两点第一文件名必须重命名否则班级群里大家传的都是IMG_20240601_123456.jpg第二次上传直接覆盖同名文件。第二按日期建子目录能避免单个目录文件过多导致访问变慢。防重复提交是我踩过的一个坑。有个学生手快连点了三下提交按钮生成了三个一模一样的报修单。解决办法有两种最简单的是前端在点击后把按钮置灰、加loading状态更稳妥的是后端对这同一个用户在短时间内比如10秒内的重复提交做拦截。我用了后者的思路基于Redis的简单计数// 伪代码 String redisKey repair_submit_lock: userId; Boolean isFirst redisTemplate.opsForValue() .setIfAbsent(redisKey, 1, Duration.ofSeconds(10)); if (!Boolean.TRUE.equals(isFirst)) { return Result.error(请不要重复提交); }3.2 维修工工作台待办列表、抢单与处理回填维修工角色的核心页面是工作台——一个待办列表加一个处理详情弹窗。这个模块实现起来不难但有几个交互细节处理不好就很别扭。一是待办列表的刷新问题。维修工不可能一直刷新页面看新单子我这里用了最轻量的轮询方案前端每30秒调一次接口拿未处理的数量有变化就提示您有新的维修任务。当时我也考虑过WebSocket实时推送但对这个场景来说属于过度设计——报修单不是实时聊天30秒的延迟完全在可接受范围内。二是处理结果回填的字段设计。维修工完成维修后需要填写维修方式现场维修/带回维修/更换配件、处理结果说明、使用的耗材及数量、是否产生费用。耗材字段要设计成动态添加行一个维修工可能同时换了电容和电源线不能只给一个文本输入框。这里有实操心得维修工的文化水平差异很大有些人打字很慢。处理结果说明最好给几个常用模板选项比如已恢复正常使用配件已更换目前无异常设备需报废处理建议采购新机然后再挂一个可选的补充说明输入框。这样既能保证回复速度又能兼顾个性化信息。3.3 管理员后台审核、派单、数据统计与人员管理管理员的职责相比前两个角色要重得多我把后台功能分成四块报修单管理、维修工管理、设备与位置管理、数据看板。报修单管理的关键是列表支持多条件组合筛选。我做了这样一组筛选条件状态全部/待派单/已派单/维修中/已完成/已驳回、设备类型、楼栋、提交日期范围、是否紧急。筛选条件在MyBatis-Plus里用LambdaQueryWrapper动态拼接非常方便public IPageRepairRecord pageWithCondition(RepairQueryDTO query, int current, int size) { LambdaQueryWrapperRepairRecord wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(query.getStatus()), RepairRecord::getStatus, query.getStatus()) .eq(query.getDeviceTypeId() ! null, RepairRecord::getDeviceType, query.getDeviceTypeId()) .eq(StringUtils.hasText(query.getBuilding()), RepairRecord::getLocation, query.getBuilding()) .ge(query.getStartDate() ! null, RepairRecord::getCreateTime, query.getStartDate()) .le(query.getEndDate() ! null, RepairRecord::getCreateTime, query.getEndDate()); return this.page(new Page(current, size), wrapper); }注意eq方法的第一个参数是boolean类型当条件为false时这个条件会被自动忽略。这个写法比我之前习惯的if判断wrapper.eq拼接干净得多强烈推荐。数据看板部分我用ECharts做了三个图近7日报修量趋势柱状图、设备类型占比饼图、维修工工作量排行横向条形图。统计数据直接写SQL做聚合比在Java里循环统计快得多。这里贴一条一周趋势的统计SQLSELECT DATE(create_time) as day, COUNT(*) as cnt FROM repair_record WHERE create_time DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(create_time) ORDER BY day;对应ServiceImpl里直接用Mapper注解方法执行Select(SELECT DATE(create_time) as day, COUNT(*) as cnt FROM repair_record WHERE create_time DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(create_time) ORDER BY day) ListDailyCountVO selectLastSevenDaysCount();注意COUNT(*)查询返回的字段别名必须和VO里的属性名对应否则MyBatis映射会有问题。3.4 通知模块站内信与微信模板消息报修进度发生变化时怎么通知到学生最直接的方式是站内信就是notification表存记录学生登录系统后在右上角小红点看到未读消息。这个实现很简单但有个问题——学生在教室打开电脑才看得到手机上根本不会主动打开系统。我在实际部署时接入了学校已有的微信公众号平台服务通过模板消息推送进度通知。学生提交报修单时如果愿意绑定微信那么每个状态变化都会推送一条类似这样的消息【校园报修进度提醒】 您申报的[多媒体教室A101投影仪]维修单当前状态维修中 维修工张师傅 预计完成时间2024-06-01 18:00模板消息的接入在服务端其实就是调微信接口加上access_tokenSpringBoot里的调用方式是用RestTemplate或OkHttp发一个POST请求。不过这里有个大坑微信公众号的模板消息有严格的行业模板审核报修这种内容大概率审核不通过。更省事的方式是让学校提供微信企业号的应用消息接口或者直接用校园通的站内推送这个要跟学校信息中心确认他们有什么渠道。技术本身不难难的是确定哪种渠道可用——这块一定要第一时间跟校方沟通我当初因为这个接口确定晚了项目延期了三天。4. 开发部署全流程与常见问题排查4.1 从Maven工程到可运行Jar包开发环境我建议统一用JDK 8 SpringBoot 2.7.x这套组合除非学校服务器明确要求更高的版本。JDK 8的兼容性最稳SpringBoot 2.7.x也是2.x系列的最后一个稳定分支各种第三方库的适配都很成熟。我见过有人直接上SpringBoot 3.0 JDK 17结果javax.*包名全变成了jakarta.*老教程里的代码大面积报错白白浪费好几天去适配——完全没有必要。工程结构方面我采用的是标准的Maven多模块划分也可以做单模块但多模块结构更清晰适合毕设展示和后续扩展campus-repair-system/ ├── pom.xml ├── campus-common/ -- 通用工具类、统一结果封装、异常处理 ├── campus-framework/ -- SpringSecurity、JWT、配置类 ├── campus-system/ -- 用户、角色、菜单、日志模块 ├── campus-repair/ -- 报修单、派单、处理、评价模块 └── campus-admin/ -- 启动入口、Controller层、定时任务模块间的依赖关系是campus-repair依赖campus-systemcampus-admin依赖所有模块最终打包时只需要在campus-admin的pom.xml里配置SpringBoot打包插件build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId version2.7.18/version /plugin /plugins /build打包命令就一行mvn clean package -DskipTests。成功后会在campus-admin/target目录生成campus-admin.jar。注意用这个插件打出来的Jar是可执行的胖Jar包里面包含了Tomcat内嵌服务器直接java -jar campus-admin.jar就能跑起来这是SpringBoot最爽的地方。4.2 生产环境部署Java环境、外部配置与开机自启部署服务器我推荐2核4G的Linux云主机装好JDK 8和MySQL 5.7或8.0就够用了。部署前最需要提前定好的事情是外部配置分离。把application.yml里所有可能随环境变化的配置——数据库地址、Redis地址、JWT密钥、文件上传路径——放到启动参数上例如java -jar campus-admin.jar \ --spring.datasource.urljdbc:mysql://localhost:3306/campus_repair?useUnicodetruecharacterEncodingutf8 \ --spring.datasource.usernamerepair_user \ --spring.datasource.passwordYourPass123 \ --custom.upload-path/data/campus-repair/upload这样就不用每次环境变化都重新打包Jar只要写好启动脚本切换测试环境或者迁移生产环境只需要改参数非常省事。为了让服务在断电重启后能自动恢复我用Linux的systemd写了一个服务文件[Unit] DescriptionCampus Repair System Afternetwork.target mysql.service [Service] Typesimple Userdeploy ExecStart/usr/bin/java -jar /opt/campus-repair/campus-admin.jar --spring.profiles.activeprod Restarton-failure RestartSec10 [Install] WantedBymulti-user.target配置完成后systemctl daemon-reload systemctl enable campus-repair就能实现开机自启、崩溃自动重启。这一步虽然不属于写代码的部分但运维稳定性在真实环境里没它不行。4.3 高频Bug实录JWT过期、中文乱码与文件路径丢失整个开发部署过程中我整理了三个高频问题这里单独拎出来说比你自己瞎琢磨快得多。问题一JWT Token过期后前端不跳转登录页。表现是用户操作着突然接口报401但页面还停留在当前页过一会儿才弹出登录过期。排查思路要先明确JWT无状态后端只能返回401状态码跳转逻辑必须由前端全局拦截器处理。我后来在Vue项目的axios拦截器里做了统一处理response.status 401时清空localStorage并window.location.href /login。看起来是个纯前端问题但后端接口的返回结构需要统一成{ code: 401, msg: 登录已过期 }否则前端没法辨别。问题二MySQL插入中文数据变问号。这个基本可以锁定是连接字符串缺少characterEncodingutf8参数。我在application.yml里配置如下之后问题解决spring: datasource: url: jdbc:mysql://localhost:3306/campus_repair?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai同时需要确认两件事数据库表本身的CHARACTER SET是utf8mb4utf8mb4才是完整的UTF-8utf8在MySQL里实际是utf8mb3连emoji和生僻字都存不了以及Linux服务器的LANG环境变量不为空。这三层任何一层掉链子中文都会乱码。问题三上传的图片访问404。我排查过自己遇到过的情况上传成功后photo_url存了/upload/2024/06/01/uuid.jpg浏览器访问却返回404。原因是我前面的Nginx只配置了location /api的反向代理/upload路径没有映射到磁盘目录。正确的Nginx配置应该是server { listen 80; server_name campus.example.edu.cn; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /upload/ { alias /data/campus-repair/upload/; } }alias后面最后一级目录名要和URL路径对应写错了同样404。调试时可以先在服务器上curl http://127.0.0.1:8080/upload/xxx.jpg确认后端能不能访问到文件如果能问题就出在Nginx配置上逐步缩小排查范围比盲目改配置高效得多。4.4 核心接口压测与性能优化记录很多人做完系统就交付了从没打过压力测试。我这次在部署前用JMeter做了两轮简单的接口压测发现一个问题报修单列表页的接口在300并发下平均响应时间从80ms飙升到1200ms基本处于不可用状态。查了一下SQL日志发现列表接口默认JOIN了用户表、设备分类表、楼栋信息表而且没有加索引。repair_record表在status和create_time字段上建立联合索引之后响应时间降到了200ms以下。如果你的项目数据量不大几十万条以内这个级别的优化就足够了。如果数据量再大可以加一层Redis做热点数据的缓存比如楼栋列表、设备分类这类极少变化的字典数据第一次查询后写入Redis后续直接走缓存。注意只缓存读多写少的数据报修单本身别用Redis缓存因为有状态流转缓存一致性处理不好反而出bug。从我做校园报修系统踩过的坑来看整个项目最难的根本不是某个技术难点而是把用户角色、流程状态、通知反馈这些业务细节想清楚、做完整。SpringBoot在这类管理系统中确实省力它让开发者把精力放在业务逻辑而不是配置和框架本身。你按我上面这个路线把九张表、四个模块、两条状态链路报修流转和用户权限落地整个系统就已经很完备了。做这套系统后期我额外做的一个小扩展是导出了维修工月度工作量Excel报表用EasyExcel实现管理员后台点一下导出本月汇总就能生成一张带合并单元格的统计表。后续如果你想让它更亮眼也可以在这个方向上加把劲——从报修数据分析出设备老化趋势反推下一年的采购预算建议这个功能的展示效果比单纯做增删改查好得多。