ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue校园报修系统实战:状态流转与权限控制全解析

SpringBoot+Vue校园报修系统实战:状态流转与权限控制全解析 简介本资源是一套完整的校园报修系统毕业设计实现方案面向计算机类本科生及初阶Java全栈开发者解决高校后勤维修流程数字化、信息化管理的实际需求。压缩包共694个文件含317个Java后端核心代码、104个Vue前端组件、78个JS交互逻辑、40个XML配置及1个SQL建库脚本辅以SVG图标、SCSS样式与多环境部署配置development/staging/production整体仅2.15MB轻量易部署。资源已获49人学习下载配套《基于SpringBootVue报修管理系统的设计与实现部署》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.0JDK 版本 1.8。这套组合是当前课程设计和中小型项目的主流选型资料多、坑少社区里遇到问题基本都能搜到解决方案。3.1 项目分包和公共返回结构分包结构是我比较坚持的部分按controller/service/mapper/entity/dto/common分包。很多人做这种项目把业务逻辑直接写在 Controller 里图省事但后续维护很痛苦。我自己是按这样的结构来的com.example.repair ├── controller // 接收请求参数校验 ├── service // 业务逻辑 ├── mapper // MyBatis-Plus 数据访问 ├── entity // 数据库实体 ├── dto // 前端入参/出参对象 ├── common // 统一返回、异常处理、常量 └── config // 配置类所有接口统一返回ResultT结构包含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.activeprod我在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.proxydevServer: { 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 时区问题数据库连接串里一定要加上serverTimezoneAsia/Shanghai不然 MySQL 返回的时间会比北京时间少 8 小时。这个问题出现的频率极高排查过程也很耗时间。我现在的习惯是建任何新项目都直接在 JDBC 连接串里带上这个参数从根上避免。7. 从“能跑”到“好用”的细节打磨课程设计答辩时老师常问的一个问题是“你这个系统和传统报修方式比优势到底在哪”很多同学的答案只停留在“在线提交、无需跑腿”。如果想让项目真正有亮点可以从下面几个方向做差异化。第一个是超时未处理的提醒机制。报修单在“待派单”状态超过 24 小时没有派单时系统自动在这个单子的列表中标记为红色同时给管理员推送一条提醒。这个功能实现起来不复杂后端用定时任务扫表即可但答辩时能直接说明你考虑了业务的闭环。第二个是简单的数据统计看板。管理员首页展示本周报修总数、平均处理时长、各分类占比用 ECharts 画柱状图和饼图。数据来源就是报修单表的聚合查询不需要额外的统计表。这个看板能让项目演示时有更多展示维度视觉效果也容易引起老师注意。第三个是图片压缩。校园网环境下一张 5MB 的照片上传到服务器可能要几十秒体验很差。给上传接口加上压缩逻辑把图片压到 1MB 以内再存储前端展示时加载速度会明显提升。我本人最推荐的还是第一个——超时提醒。理由是这个功能把系统从“记录工具”升级成了“管理工具”它切入的是报修业务真正的痛点无人响应和流程卡住。最后说一点个人感受。这类管理系统项目技术难点其实有限真正拉开差距的是“有没有认真想过业务闭环”。报修单提交后谁负责多久没处理会怎样用户怎么知道进展这些问题在数据库字段和接口设计里都有对应答案你想得越清楚代码写起来就越顺答辩时老师问起来你也能理直气壮地讲出设计依据。如果你正在做类似的系统重点去啃状态流转和权限控制这两块项目质量会立刻提升一个档次。本文还有配套的精品资源点击获取
返回列表