SpringBoot+Vue校园报修系统实战:状态流转与权限控制全解析
简介:本资源是一套完整的校园报修系统毕业设计实现方案,面向计算机类本科生及初阶Java全栈开发者,解决高校后勤维修流程数字化、信息化管理的实际需求。压缩包共694个文件,含317个Java后端核心代码、104个Vue前端组件、78个JS交互逻辑、40个XML配置及1个SQL建库脚本,辅以SVG图标、SCSS样式与多环境部署配置(development/staging/production),整体仅2.15MB,轻量易部署。资源已获49人学习下载,配套《基于SpringBoot+Vue报修管理系统的设计与实现(部署)》Word文档,涵盖系统架构图、E-R图、用例图等专业建模成果,并提供run.bat、package.bat等一键启停与打包脚本,显著降低本地运行门槛。读者可直接获取可运行的前后端分离工程、规范数据库设计及完整模块化源码,覆盖用户管理、报修提交、维修派单、进度跟踪、服务评价等全流程业务逻辑。 做课程设计拿到“校园报修系统”这个题目时,我第一反应是“这不就是一套增删改查吗”。但真正动手之后才发现,这套系统最磨人的地方根本不在 CRUD,而是状态怎么流转、权限怎么控制、图片传上来存哪里、部署到服务器之后前端刷新为什么 404。如果你也在做类似的全栈项目,或者正准备用 SpringBoot + Vue 从零搭一套带完整流程的管理系统,这篇文章应该能帮你少走不少弯路。
我会直接按当时的实操顺序来写:先拆业务流程,再设计数据库,然后分别讲后端和前端的核心实现,最后说部署。每个环节都会给出我实际用过的代码、SQL 和配置,也会把踩过的坑单独拎出来说。
1. 报修流程的角色拆解与功能边界
校园报修和一般的企业工单系统有个明显区别:用户群体是学生和老师,他们不关心内部流转逻辑,只想知道“我报的这个问题什么时候有人来处理”。所以系统设计的第一原则是——把内部流转藏起来,把状态变化亮出来。
1.1 三个角色的诉求完全不同
| 角色 | 核心诉求 | 关键操作 |
|---|---|---|
| 学生/教职工 | 提交方便、进度可见、不用反复打电话 | 提交报修、查看进度、确认完工 |
| 维修工 | 任务明确、能记录处理过程 | 接单、更新维修状态、填写处理结果 |
| 管理员 | 工单分配合理、数据能统计 | 派单、用户管理、分类管理、数据看板 |
这三种角色对应的是三种完全不同的页面视角。我当时在设计时最纠结的是:维修工要不要自己抢单?还是必须由管理员派单?调研下来校园场景还是以“管理员派单”为主,因为维修工的技能方向不同,修水管的去修电不现实,派单制可以保证工单分配给对的人。
1.2 状态流转是整个系统的核心命脉
报修单的状态我最终定为六个:待派单、待接单、维修中、待验收、已完成、已取消。学生提交报修后进入“待派单”,管理员指派维修工后变成“待接单”,维修工接单开始处理进入“维修中”,处理完提交成果进入“待验收”,学生确认满意后“已完成”。整个链路里有两处要特别注意:
- 状态不能乱跳,比如“待接单”不能直接变“已完成”,必须经过“维修中”和“待验收”,这是我在后端做状态校验的硬性规则。
- 每个状态变化都要留下日志。为什么要留日志?因为一旦学生投诉“我的问题一直没处理”,管理员能直接查到工单卡在哪个节点、卡了多久,避免扯皮。
这套状态设计逻辑其实就是从用户视角倒推出来的——学生只关心“什么时候能修好”,维修工只关心“我的任务列表”,管理员只关心“有没有工单卡住”。系统里的每一张表、每一个接口,都是服务于这三句话的。
2. 数据库设计:五张表如何撑起完整闭环
数据库设计阶段花了我最多时间。不是说表有多复杂,而是字段的取舍直接影响后端的代码量。我最终设计了五张核心表,分别是用户表、维修分类表、报修单表、处理日志表、通知表。这五张表互相之间关系清晰,既不会过度设计也不会缺字段。
2.1 用户表:角色字段用数字还是字符串
用户表结构如下:
CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) NOT NULL, phone VARCHAR(20), avatar VARCHAR(255), role TINYINT NOT NULL DEFAULT 2 COMMENT '0-管理员 1-维修工 2-学生', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );角色字段我用了 TINYINT 而不是直接存字符串。原因是数字可以写进枚举类里统一管理,避免因为手滑把“维修工”写成“维修员”导致判断失效。后端定义一个枚举或者常量类即可,配合@EnumValue注解使用更顺手。这里踩过一个坑:如果直接用字符串,前端传参时大小写不一致就会导致角色校验失败,排查起来很费劲。
2.2 报修单表:状态字段和图片字段是最关键的设计
报修单表是整张数据库的核心,字段比较多,我贴出关键部分:
CREATE TABLE repair_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, user_id BIGINT NOT NULL, category_id BIGINT NOT NULL, title VARCHAR(100) NOT NULL, description TEXT, images VARCHAR(1000) COMMENT '图片地址, 英文逗号分隔', address VARCHAR(200) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0-待派单 1-待接单 2-维修中 3-待验收 4-已完成 5-已取消', assignee_id BIGINT COMMENT '维修工ID', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, accept_time DATETIME, finish_time DATETIME, cancel_reason VARCHAR(200), INDEX idx_status (status), INDEX idx_user (user_id), INDEX idx_assignee (assignee_id) );几个设计细节:
order_no为什么要单独建一个业务单号,而不是直接用自增 ID?因为学生报修时希望能报出一串单号方便查询,自增 ID 太容易被猜到,也显得不专业。我生成单号的规则是“当前时间戳 + 随机三位数”,保证唯一即可。images字段用逗号分隔存储多张图片地址,是一对多的妥协方案。如果单独建一张报修图片表,查询时要多关联一次,而且图片生命周期完全依附于报修单,冗余存储完全可控。前端拿到字符串后 split 一下就能回显。assignee_id就是维修工的sys_user.id,通过这个字段建立报修单和维修工的关联。这里没有建外键约束,因为业务上维修工被删除时不应该影响历史工单,用逻辑关联更安全。
2.3 处理日志表:给每一个状态变化留下证据
CREATE TABLE repair_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL, operator_id BIGINT NOT NULL, action VARCHAR(50) NOT NULL COMMENT '提交/派单/接单/维修完成/验收/取消', remark VARCHAR(255), create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );这张表看起来简单,但它解决了三个实际问题:管理员看工单详情时能直接看到完整的操作时间线;学生端“进度跟踪”页面可以直接查这张表渲染;发生纠纷时有据可查。我在管理员端做了一个时间线组件,效果就是类似电商物流跟踪那样一条竖线从上到下展开,数据来源就是这张表。
通知表结构比较简单:id, user_id, content, is_read, create_time,用户登录后从接口拉取未读消息即可。报修状态变化时往这张表插入一条记录。这里要注意的一点是:通知的读取状态用is_read的0/1表示,不要在业务代码里用true/false去匹配数据库的tinyint(1),MySQL 的布尔类型在 JDBC 里映射偶尔会有兼容性问题,直接用0/1判断最稳。
3. 后端 SpringBoot 实现:分层架构与核心业务逻辑
后端我用的是 SpringBoot 2.7 + MyBatis-Plus + MySQL 8.0,JDK 版本 1.8。这套组合是当前课程设计和中小型项目的主流选型,资料多、坑少,社区里遇到问题基本都能搜到解决方案。
3.1 项目分包和公共返回结构
分包结构是我比较坚持的部分,按controller/service/mapper/entity/dto/common分包。很多人做这种项目把业务逻辑直接写在 Controller 里,图省事,但后续维护很痛苦。我自己是按这样的结构来的:
com.example.repair ├── controller // 接收请求,参数校验 ├── service // 业务逻辑 ├── mapper // MyBatis-Plus 数据访问 ├── entity // 数据库实体 ├── dto // 前端入参/出参对象 ├── common // 统一返回、异常处理、常量 └── config // 配置类所有接口统一返回Result<T>结构,包含code、message、data三个字段。这个看起来只是一个约定,但前后端联调时特别省心,前端 axios 拦截器只需要处理code === 200的情况,其他状态统一弹错误消息,不用每个接口单独判断。
另外必须做全局异常处理,用@RestControllerAdvice捕获业务异常和MethodArgumentNotValidException。不然前端传参数少了一个,后端直接抛出一大段英文栈信息,前端很难解析。
3.2 报修单提交的完整链路
提交报修是学生端最核心的接口,我贴一下 service 层的关键逻辑:
@Transactional(rollbackFor = Exception.class) public void submitRepair(RepairSubmitDTO dto, Long userId) { // 1. 参数校验 RepairOrder order = new RepairOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setCategoryId(dto.getCategoryId()); order.setTitle(dto.getTitle()); order.setDescription(dto.getDescription()); order.setImages(dto.getImages()); // 前端传英文逗号拼接的字符串 order.setAddress(dto.getAddress()); order.setStatus(0); // 待派单 repairOrderMapper.insert(order); // 2. 记录日志 repairLogMapper.insert(new RepairLog(order.getId(), userId, "提交报修", dto.getDescription())); // 3. 通知管理员 notificationMapper.insert(new Notification(ADMIN_ID, "您有新的报修单 " + order.getOrderNo() + " 待派单", 0)); }我特意在方法上加上了@Transactional。为什么要加事务?因为要保证报修单插入和日志插入要么同时成功要么同时失败。如果日志表插入失败而报修单插入成功,学生端显示“已提交”但管理员端查不到工单,这种数据不一致非常难排查。
generateOrderNo()我实现为yyyyMMddHHmmss + 3位随机数,并发场景下虽然理论上可能重复,但一个校园系统的提交频率完全不会撞上。如果你追求更严谨,可以用 Redis 生成自增序列,但考虑到课程设计场景,没必要增加复杂度。
3.3 状态流转校验的两种实现方式
状态变更涉及多个接口。我在基础的方法里实现了一套“状态机预校验”:先查当前状态,判断是否允许变化到目标状态,不允许直接抛业务异常,并给出提示信息。
private void validateStatusChange(RepairOrder order, int expected, int target, String action) { if (order.getStatus() != expected) { throw new BusinessException("当前状态不允许" + action); } }这种方式代码写得重复,但直白易懂。想要统一管理的话,可以定义一个 HashMap 保存每个状态的合法后继状态,后续扩展状态时只改配置不改业务代码。我用的是前者,因为报修流程的状态只有六种,提前设计和过度设计之间我会毫不犹豫选简单方案。
重点要说的是“派单”这个操作。派单不是简单地改assignee_id,而是要在系统里做两件事:工单状态从“待派单”改为“待接单”;通知被指派的维修工。维修工端通过轮询或者刷新列表看到新工单。这里不需要 WebSocket 或者消息推送,校园场景的用户量还没到需要实时推送的程度,轮询间隔 10 秒完全够用。
3.4 图片上传的目录规划与大小限制
报修时学生要上传现场照片,这一步如果处理不好会出很多幺蛾子。我选择直接把图片存到服务器本地磁盘,而不是数据库的blob字段,也不是存对象存储。原因很简单:项目部署在一台云服务器上,本地上传实现成本最低,不需要额外引入云服务 SDK。
上传接口返回的是可访问的 URL,然后把 URL 拼到报修单的images字段里。需要注意spring.servlet.multipart.max-file-size和max-request-size这两个配置,不设置的话默认只有 1MB,手机拍的照片轻松超限。我设置为 10MB,并且在代码里再校验一次文件类型,避免有人传个.exe上来。
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB图片的磁盘路径我配置成可修改的,通过application.yml里的file.upload-path指定。这样部署到服务器时可以统一挂载到/data/repair/upload/目录,也方便备份。
3.5 登录认证与权限拦截
登录认证我用的是 JWT。学生、维修工、管理员登录成功后返回一个 token,前端存到 localStorage,后续请求头带上Authorization: Bearer <token>。后端写一个拦截器解析 token,把用户信息放到ThreadLocal里,这样 Controller 里直接UserContext.getUserId()就能拿到当前登录人。
权限控制这里有一个关键点:所有操作都要校验数据归属。学生只能看自己的报修单,维修工只能看分派给自己的工单,管理员可以看全部。这个校验如果漏掉,接口就会变成越权漏洞。比如学生把自己的orderId改成别人的单号,直接调“确认完工”就能把别人的工单结束掉,这是非常严重的逻辑漏洞。
我的做法是在 service 层统一加入归属校验:
if (!order.getUserId().equals(currentUserId) && !isAdmin(currentUserId)) { throw new BusinessException("无权操作该工单"); }4. 前端 Vue 实现:页面结构与接口联调经验
前端我用的是 Vue 2 + Element UI,因为当时项目要求相对传统,Vue 2 生态更稳定,Element UI 组件库开箱即用,不需要自己去拼表格、对话框这些基础组件。如果你想用 Vue 3 + Element Plus 也可以,整体思路是相通的。
4.1 路由权限控制
系统分三种角色,前端路由用动态路由方案:登录成功后根据角色过滤路由表,只挂载当前角色能访问的页面。基础路由只有 login 一个页面,其他页面都在asyncRoutes里配置了roles字段。Vue Router 的beforeEach钩子判断用户角色后决定放行还是重定向。
前端权限只能带来好的体验,不能作为安全防线。真正防越权还得靠后端的归属校验。我之前做过一个项目前端隐藏了按钮就被认为安全了,结果通过接口直接调后端,闹出过安全问题。所以这里要记住一句话:前端权限是用户体验的一部分,后端校验才是安全底线。
4.2 学生端报修页面的组件设计
学生端页面拆成三个核心组件:
- 报修表单组件:分类下拉、标题、描述、地址、图片上传。
- 报修列表组件:卡片式展示报修单,每个卡片显示单号、标题、状态、时间。
- 进度详情组件:通过处理日志渲染时间线,展示状态流转过程。
图片上传组件我没有用现成的 Element Upload,而是自定义了一个。因为默认的action属性是写死上传地址的,但我希望上传成功后把返回的 URL 保存到一个fileList数组里,提交报修时拼接成字符串传给后端。自定义上传逻辑反而更灵活,代码只有几十行。
4.3 axios 封装与 token 过期处理
axios 统一封装是 Vue 项目的标配。我在 interceptor 中统一处理请求头和响应状态码:
service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( res => { const { code, message, data } = res.data if (code === 200) { return data } else { Message.error(message) return Promise.reject(new Error(message)) } }, err => { if (err.response && err.response.status === 401) { localStorage.removeItem('token') router.push('/login') } Message.error('网络异常,请稍后重试') return Promise.reject(err) } )这个封装看起来简单,但是解决了一个关键痛点:后端返回的业务错误(比如“当前状态不允许操作”)和网络错误在调用方视角是一致的。组件里只需要处理成功返回的数据,出错的提示全部由拦截器统一负责。
5. 部署上线:从 jar 包到 Nginx 反向代理
部署阶段是这个项目的最后一公里,也是很多人栽跟头的地方。我当时采用前后端分离部署:后端打包成 jar 包跑在服务器上,前端构建成静态文件交给 Nginx 托管,Nginx 再把/api开头的请求转发给后端 jar 包的端口。
5.1 后端打包与启动
后端打包用 Maven,先执行mvn clean package -DskipTests,然后得到target/repair-system-1.0.0.jar。在服务器上直接执行:
java -jar repair-system-1.0.0.jar --spring.profiles.active=prod我在application-prod.yml里单独配置了数据库连接、上传路径等环境相关参数。和本地开发环境彻底隔离,避免部署时改代码。这个习惯强烈建议养成,用一个 profile 文件管理部署环境,多台服务器部署时只需改一份配置。
5.2 Nginx 配置的两个关键点
Nginx 配置如下:
server { listen 80; server_name yourdomain.com; root /data/repair/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }第一关键点是proxy_pass http://127.0.0.1:8080/;最后这个斜杠。有斜杠意味着把/api前缀去掉再转发,比如/api/repair/list会被转发到http://127.0.0.1:8080/repair/list,后端接口就不需要额外处理/api前缀。没有斜杠则会保留完整路径。这个细节很多新手第一次都没搞明白,白白在代码里做字符串替换。
第二关键点是location /块里的try_files $uri $uri/ /index.html;。这句话解决的是 Vue 路由刷新 404 的问题。Vue 默认使用 history 模式,路由地址没有#号,但刷新时浏览器会向服务器请求该路径,Nginx 找不到对应文件就会 404。try_files指令的意思是:先试原始路径,找不到文件就回退到index.html,交给 Vue Router 自己处理。
5.3 数据库脚本导入
数据库我用的 MySQL 8.0。从 IDEA 的 Database 面板里可以右键导出整个库的 schema 和 data 为.sql文件,服务器上执行source xxx.sql完成导入。这里提醒两点:
- 导出的 SQL 文件里如果有
DROP TABLE IF EXISTS,在生产数据库上要谨慎执行,避免误删已有数据。 - 检查 SQL 文件里的字符集设置,确保是
utf8mb4,不然后端往数据库里写入 emoji 表情时会出现乱码报错。
6. 联调与部署阶段踩过的典型问题
这部分是我最想分享的,很多问题是网上教程不会讲的。
6.1 跨域问题
本地开发时前端跑在 Vue 的默认地址http://localhost:8080,后端跑在http://localhost:9090,前端请求后端接口必然跨域。我解决了两次,第一次用后端配置全局 CORS,第二次用 Vue 的 devServer 代理。
推荐做法是开发环境用devServer.proxy:
devServer: { proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true, pathRewrite: { '^/api': '' } } } }这样本地请求/api/repair/list会被代理到http://localhost:9090/repair/list,前端 axios 的 baseURL 写/api就行。部署到生产环境后,Nginx 又帮我们把/api反向代理到后端,前后端的接口代码完全不用改,只改环境配置就能切换联调模式和上线模式。这个方案理解之后,跨域问题就直接被架构性地解决了,而不是靠加一堆 CORS 配置来掩盖问题。
6.2 状态字段的魔法值问题
报修单的status字段在后端有六个取值,前端也要渲染对应的状态标签。最蠢的做法是前端写死:status === 0 -> 待派单,后端又单独写一个if (status == 0)。中间一旦某个人改了枚举值,整个系统就对不上了。
我的方案是后端在登录接口或数据字典接口里把状态枚举列表返回给前端,前端动态渲染。如果嫌麻烦不想加这个接口,至少也要把枚举放在前后端两个项目里同步维护,并且命名保持一致。这个魔法值问题在课程设计中还算隐蔽,因为一般只有一两个人在写代码,但一旦多人协作,这个问题会直接引发线上故障。
6.3 图片回显路径问题
上传图片时如果存的是http://localhost:9090/upload/xxx.jpg,本地测试没问题,但部署到服务器后这个地址就指向自己电脑了。我改成只存相对路径/upload/xxx.jpg,前端展示时通过Vue.prototype.$baseUrl = 'http://服务器IP:端口'拼接。这样换环境时只需要改一个变量,所有图片地址自动生效。
6.4 时区问题
数据库连接串里一定要加上serverTimezone=Asia/Shanghai,不然 MySQL 返回的时间会比北京时间少 8 小时。这个问题出现的频率极高,排查过程也很耗时间。我现在的习惯是建任何新项目都直接在 JDBC 连接串里带上这个参数,从根上避免。
7. 从“能跑”到“好用”的细节打磨
课程设计答辩时老师常问的一个问题是:“你这个系统和传统报修方式比,优势到底在哪?”很多同学的答案只停留在“在线提交、无需跑腿”。如果想让项目真正有亮点,可以从下面几个方向做差异化。
第一个是超时未处理的提醒机制。报修单在“待派单”状态超过 24 小时没有派单时,系统自动在这个单子的列表中标记为红色,同时给管理员推送一条提醒。这个功能实现起来不复杂,后端用定时任务扫表即可,但答辩时能直接说明你考虑了业务的闭环。第二个是简单的数据统计看板。管理员首页展示本周报修总数、平均处理时长、各分类占比,用 ECharts 画柱状图和饼图。数据来源就是报修单表的聚合查询,不需要额外的统计表。这个看板能让项目演示时有更多展示维度,视觉效果也容易引起老师注意。第三个是图片压缩。校园网环境下,一张 5MB 的照片上传到服务器可能要几十秒,体验很差。给上传接口加上压缩逻辑,把图片压到 1MB 以内再存储,前端展示时加载速度会明显提升。
我本人最推荐的还是第一个——超时提醒。理由是这个功能把系统从“记录工具”升级成了“管理工具”,它切入的是报修业务真正的痛点:无人响应和流程卡住。
最后说一点个人感受。这类管理系统项目,技术难点其实有限,真正拉开差距的是“有没有认真想过业务闭环”。报修单提交后谁负责?多久没处理会怎样?用户怎么知道进展?这些问题在数据库字段和接口设计里都有对应答案,你想得越清楚,代码写起来就越顺,答辩时老师问起来你也能理直气壮地讲出设计依据。如果你正在做类似的系统,重点去啃状态流转和权限控制这两块,项目质量会立刻提升一个档次。
本文还有配套的精品资源,点击获取
