尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于SpringBoot+Vue的校园报修系统设计与实现

基于SpringBoot+Vue的校园报修系统设计与实现 简介本资源是一套面向计算机专业本科生的毕业设计级校园报修系统完整实现方案聚焦高校后勤服务数字化场景解决传统报修流程低效、信息不透明、反馈滞后等实际问题。压缩包共694个文件含317个Java后端核心代码、104个Vue前端组件、92个SVG图标资源、78个JS交互逻辑及40个XML配置文件辅以SQL建库脚本、YML环境配置、BAT一键部署脚本和Word版设计文档整体仅2.15MB轻量易部署。已有49人学习下载适合需快速完成毕设答辩、掌握前后端分离开发全流程的学生使用。资源提供从E-R图与用例图等软件工程设计图到系统管理、报修处理、维修记录、评价反馈等全模块可运行代码涵盖数据库设计、权限控制、前后端联调及本地部署实操具备完整闭环交付能力。1. 项目概述这套校园报修系统到底解决什么问题在学校里宿舍灯管坏了、教室投影仪失灵、实验室空调漏水——这些场景几乎每天都会发生。传统报修方式无外乎打电话、填纸质单子、或者在微信群里吼一声但这类方式的痛点非常明显报修信息容易遗漏、维修进度无法追踪、维修工单分配靠人工协调、事后统计更是无从谈起。这个基于SpringBootVue的校园报修系统本质上是把“报修”这件事做成了一条完整的信息化流转链路学生线上提交故障描述和照片→系统自动生成工单→维修人员接单处理→更新维修状态→学生查看进度→管理员统计报表。整个流程不再依赖任何人工传达所有的状态变化都有记录所有的工单都有据可查。作为同类项目里非常经典的一个全栈练手案例它最大的价值在于麻雀虽小但五脏俱全。这个系统天然覆盖了RBAC权限模型学生、维修工、管理员三种角色、文件上传、状态机流转、前后端分离联调、服务器部署等高频知识点。不管你是正在做毕业设计的学生还是刚学完SpringBoot和Vue想找个综合项目巩固的开发者这套系统的设计思路和实现细节都值得完整过一遍。我下面要拆解的版本包含完整的源码、数据库脚本和部署文档技术栈是SpringBoot 2.x Vue 2.x MySQL 5.7/8.0这也是当前网上流传最广、资料最全的一个组合。接下来我把整个项目的设计和实现一条条拆开讲清楚包括数据库怎么建、权限怎么控制、工单状态怎么流转、部署时哪些坑必须避开。2. 技术选型与架构设计2.1 为什么是SpringBoot Vue这个组合先聊后端。SpringBoot在当前Java生态里基本是事实标准了它把Spring那套繁琐的XML配置全部干掉内嵌了Tomcat打成一个jar包就能跑对于校园报修这种典型的管理信息系统来说非常合适。不需要自己做服务注册发现、不需要分布式事务、不需要消息队列——单机部署加一个MySQL库就已经覆盖了全部业务场景。再说前端。Vue的组件化开发方式天然适合“页面结构高度类似、但数据不同”的管理系统场景。报修列表、我的工单、维修任务这些页面其实都是表格搜索条件操作按钮的组合抽成公共组件后每个页面的维护成本都很低。Vue 2的生态成熟稳定Element UI的组件库开箱即用后端返回JSON数据前端绑定渲染开发效率非常高。在这个项目里前后端是通过RESTful API进行交互的数据格式统一为JSON后端不关心页面长什么样只负责把数据处理好交给前端前端不关心数据存哪里只负责把JSON渲染成好看好用的界面。这种松耦合的架构有几个实际好处排错方便用Postman单独测接口和页面完全独立、开发可以并行后端的接口定义好前端就能开工、将来如果要出小程序或者移动端版本后端代码几乎不用改动。2.2 项目的整体架构与数据流向整个系统的架构可以理解为三层再加一个数据库第一层是浏览器端的Vue单页应用负责用户交互和页面渲染。第二层是SpringBoot后端负责接收前端的HTTP请求、做参数校验、调用业务逻辑、访问数据库、返回JSON结果。第三层是MySQL数据库负责持久化存储所有业务数据。一次完整的报修请求会这样流转学生在页面填写报修表单包括报修类型水/电/木/设备、故障描述、联系方式和图片点击提交。Vue前端通过axios把数据POST到后端接口/repair/add。SpringBoot的Controller接住请求校验token确认学生身份把数据封装成实体对象。Service层处理业务逻辑比如从数据库读取当前登录用户的学生姓名和班级、给工单生成唯一编号、设置初始状态为“待受理”。Mapper层通过MyBatis Plus把数据插入数据库的repair_order表。后端返回“提交成功”和新的工单号给前端。Vue收到成功响应弹出提示刷新当前页面的工单列表。逆操作也是一样的路径——维修工点击“接单”按钮前端发出POST请求后端更新工单状态并把维修工ID写入工单记录前端再次刷新时看到的状态就变成了“维修中”。这种分层设计是最基础也是最推荐的写法。在实际开发时我建议严格遵循Controller-Service-Mapper的职责划分不要把业务逻辑写在Controller里。很多新手为了省事在Controller里直接操作数据库刚开始觉得很快但一旦业务复杂起来代码就会像一团乱麻排查问题和后续扩展都会很痛苦。2.3 RBAC权限模型的设计思路校园报修系统有三种角色权限等级和操作范围完全不同这正好是RBAC基于角色的访问控制模型的经典应用场景。学生只允许提交报修、查看自己提交的工单、确认完工并评价。维修工可以查看分配给自己的工单任务、接单、更新维修进度、填写处理结果。管理员拥有全部权限——管理用户、管理维修工、分配工单、删除违规记录、查看统计报表。在实现层面后端采用JWTJSON Web Token进行无状态认证。用户登录成功后后端签发一个包含用户ID和角色信息的token返回给前端前端把这个token存到localStorage里之后每次请求都在HTTP请求头里带上Authorization: Bearer token。后端通过拦截器统一校验token校验通过后把用户信息放到请求上下文里业务代码就能直接获取当前登录用户。这里有一个特别容易被忽视的细节拦截器只校验token是否合法还不够还得校验接口权限。比如维修工访问学生专属接口、学生试图调管理员的删除接口这种请求必须被拦截。我是通过自定义注解RequireRole(admin)加在Controller方法上然后在拦截器里解析token里的角色信息做比对校验来实现的。这样权限控制的粒度足够细且代码侵入性很小新加一个接口只需要在方法上加一行注解。对于毕业设计或者课程设计来说这个权限模型已经算是完整了。相比那些登录接口只把用户名密码比对一下就放行的做法JWT角色注解的方案能明显提升项目的完成度在现场答辩的时候也更有话可讲。3. 数据库设计如何用五张表撑起整个业务3.1 核心表结构与设计思路这套系统的数据库一共有五张核心表用户表、维修工表、报修单表、报修图片表、系统日志表。虽然表不多但每张表的字段设计都有明确的业务考量下面我分组讲。用户表sys_user是最基础的表存储账号密码和角色信息。关键字段包括id主键、username唯一登录名、password加密后的密码、real_name真实姓名、role角色标识student/admin、phone联系电话、class_name班级学生身份用、status账号状态1启用/0禁用、create_time创建时间。密码存储我这里强烈建议用BCrypt加密Spring Security自带这个工具不要用MD5——MD5是哈希算法而不是加密算法撞库风险极高在答辩时被问到安全问题也是个减分项。维修工表repair_worker单独建表是为了和用户表解耦。维修工也有登录账号但不能直接复用用户表因为维修工有自己独立的属性字段比如擅长类型电工/水工/木工/设备维修、当前状态空闲/忙碌中、接单数量、好评率等。如果强行塞进用户表会导致字段大量冗余且表格结构臃肿。维修工表通过user_id字段关联回用户表这样既保证了登录逻辑的统一又保留了维修工专属信息的扩展空间。报修单表repair_order是整套系统的绝对核心字段设计直接影响后续功能开发。我的设计如下order_no工单编号唯一索引格式为BX年月日四位随机数比如BX202501150023。为什么不用自增id直接展示给用户因为自增id会暴露系统业务量而且用户截图反馈时16位的数字串可读性太差。title故障标题比如“宿舍302灯管不亮”。description故障详细描述支持文字和标点。type报修类型0水电/1木工/2设备/3其他用int存储页面上用字典翻译成中文。status工单状态0待受理/1待维修/2维修中/3已完成/4已取消/5已验收这是整套系统的灵魂字段。student_id报修学生ID。worker_id接单维修工ID。location故障地点比如“3号宿舍楼502室”。create_time / accept_time / complete_time三个时间字段分别记录提交、接单、完成的时间点。rating / feedback评价分数和反馈文字工单完成后学生填写。这里状态的实现我用的是整型数字而不是字符串。好处有三存储空间更小、查询排序更高效、在代码里用枚举类做状态流转控制更不容易出错。3.2 状态机的定义与流转规则工单状态是整个系统的核心这里我要单独拎出来讲。这个项目的业务逻辑并不复杂但状态流转如果不提前定义清楚后期开发时一定会出现“逻辑到处都是、bug修都修不完”的情况。我定义的状态机流转规则如下当前状态可执行操作目标状态操作人待受理(0)管理员派单待维修(1)管理员待受理(0)学生取消已取消(4)学生待维修(1)维修工接单维修中(2)维修工待维修(1)管理员改派待维修(1)管理员维修中(2)维修工提交完工已完成(3)维修工已完成(3)学生确认验收评价已验收(5)学生在代码实现时我建议把状态流转写成一个独立的枚举类比如OrderStatusEnum然后在Service层做一个统一的changeOrderStatus(orderId, targetStatus, operator)方法。这个方法里先根据当前状态判断是否允许转移到目标状态允许就更新数据库并写日志不允许就抛出业务异常提示“当前状态不允许执行该操作”。这个设计的好处是把所有可能的状态组合集中到了一处管理。比如学生想取消一个“维修中”的工单前端按钮在状态为2时根本不显示就算有人绕过前端直接调后端接口后端的状态机校验也会把他拦住——这就是在架构层面做安全兜底而不是靠前端“自觉”。3.3 数据库初始化脚本编写要点数据库脚本看似简单但里面有几个细节值得注意你在部署时如果遇到问题多半出在这里。字符集我建议统一设置为utf8mb4而不是UTF8因为utf8mb4是UTF8的超集支持emoji表情和生僻字。学生故障描述里经常会出现emoji用utf8会导致插入数据时报错用utf8mb4则完全没有问题。排序规则推荐utf8mb4_general_ci兼顾性能和准确性。表之间需要设置外键吗我的实践建议是逻辑外键而非物理外键。也就是在表设计时通过字段关联比如student_id但不真的在数据库层面创建FOREIGN KEY约束。原因很实际物理外键会在插入、删除、更新时带来额外的约束检查开销校园报修系统数据量有限这些开销虽不明显但物理外键在后续做分库分表或数据迁移时极其痛苦必须先删外键才能操作。代码层面控制好引用关系的一致性就够了。索引方面除了主键索引之外我建议给两个字段加索引status状态和student_id学生ID。这两个字段是列表查询最常用的WHERE条件加索引后查询性能提升明显。但如果表的数据量还没到十万级不建索引影响也不大。完整的建表SQL脚本我在项目部署包的/docs/sql/init.sql里有提供里面包含了全部建表语句和三条预设账号admin管理员、student01学生、worker01维修工数据库导入后就能直接登录测试。4. 后端SpringBoot核心模块实现4.1 项目初始化与目录结构SpringBoot的工程结构我推荐采用按功能分包的方式而不是按技术类型分包。很多教程里常见的是controller包、service包、mapper包这种按技术层分包的写法小项目里用起来还行但一旦功能模块变多找代码就得来回切包。按功能分包的结构是这样的com.campus.repair ├── config // 配置类跨域、拦截器、文件上传 ├── controller // 接口层接收请求返回结果 ├── service // 业务层核心逻辑 │ └── impl ├── mapper // MyBatis Plus的数据访问接口 ├── entity // 数据库实体类 ├── dto // 数据传输对象接收前端参数 ├── vo // 视图对象返回给前端的数据 ├── common // 公共组件统一返回结果、异常处理、常量 └── utils // 工具类JWT工具、时间处理等pom.xml里的核心依赖有这些spring-boot-starter-webWeb基础、mybatis-plus-boot-starter数据库操作、mysql-connector-javaMySQL驱动、jjwtJWT生成和解析、hutool-all工具类集合、lombok简化实体类代码、spring-boot-starter-validation参数校验。我特别推荐MyBatis Plus而不是原生MyBatis原因只有一个这种管理系统80%的SQL都是单表CRUD。MyBatis Plus内置了通用的增删改查方法你不需要手写XML和SQL直接用this.save(entity)、this.getById(id)就能完成绝大部分操作。只有多表关联查询时才需要自己写SQL比如“查询每个维修工的接单数量排名”这种统计语句。对于时间紧张的毕设项目来说这能帮你省下大量开发时间。4.2 登录认证与JWT的完整实现认证这块我展开讲讲因为这是面试官和答辩老师最喜欢问的部分。登录接口的流程是前端把用户名和密码POST到/api/auth/login后端先用username从数据库查出用户如果用户不存在直接返回“用户名或密码错误”。如果存在用BCrypt的matches方法比对明文密码和数据库中加密后的密文匹配成功就生成token返回给前端。token的生成代码如下String token Jwts.builder() .setSubject(user.getUsername()) .claim(userId, user.getId()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();这里我把有效期设成了7天也就是用户登录一次一周内不需要重新登录。secretKey是签名密钥我在配置文件里单独设置了生产环境必须通过环境变量注入不能硬编码在代码里。拦截器是认证的核心我实现了一个AuthInterceptor类实现HandlerInterceptor接口。在preHandle方法里执行三步先从请求头取token为空就返回401然后用JWT解析验证token是否有效无效也返回401最后把解析出来的用户信息放进ThreadLocal或请求Attribute里供后续代码使用。需要注意一个细节登录、注册这两个接口不能拦截否则用户连登录都进不来。我在配置类里用excludePathPatterns排除了这两个路径同时放行了静态资源路径。registry.addInterceptor(authInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/register, /api/file/**);权限控制方面我用自定义注解RequireRole({student, admin})标注在方法上。拦截器里在token校验通过后再检查当前用户角色是否满足注解要求不满足就返回403。这样学生只能操作自己的数据维修工只能处理分配给自己的工单管理员才能访问用户管理模块。4.3 报修工单的业务逻辑实现报修模块的核心接口有六个分别是学生提交报修、学生查询自己的工单列表、维修工查询待处理工单、维修工接单、维修工提交完工、学生确认验收。提交报修的Service方法我贴一段核心逻辑Transactional public RepairOrderVO submitRepair(RepairSubmitDTO dto) { RepairOrder order new RepairOrder(); order.setOrderNo(generateOrderNo()); order.setTitle(dto.getTitle()); order.setDescription(dto.getDescription()); order.setType(dto.getType()); order.setLocation(dto.getLocation()); order.setStatus(OrderStatusEnum.PENDING_ACCEPT.getCode()); order.setStudentId(currentUserId()); order.setCreateTime(new Date()); repairOrderMapper.insert(order); // 保存报修图片如果上传了的话 if (dto.getImageUrls() ! null) { for (String url : dto.getImageUrls()) { repairImageMapper.insert(new RepairImage(order.getId(), url)); } } return convertToVO(order); }Transactional注解在这里很重要——如果插入工单成功后插入图片时抛异常事务会回滚工单和图片会保持数据一致不会出现“工单存在但图片丢失”的脏数据。工单编号的生成逻辑里我加了一个小细节用当前日期加随机数。BX202501150023中的20250115是日期0023是当天第23个工单。实现的时候我用Redis的自增计数器来做但考虑到这个项目不一定部署Redis直接用new Random()生成四位随机数也能保证同一天的碰撞概率低到可以忽略。状态流转我在Service层做了一个统一处理方法private void checkAndUpdateStatus(Long orderId, Integer expectStatus, Integer targetStatus) { RepairOrder order repairOrderMapper.selectById(orderId); if (order null) { throw new BusinessException(工单不存在); } if (!order.getStatus().equals(expectStatus)) { throw new BusinessException(当前工单状态不支持该操作); } order.setStatus(targetStatus); repairOrderMapper.updateById(order); }这个方法基本上把业务规则都收口了。比如维修工接单时前提条件是这个工单状态必须是“待维修(1)”并且这个工单当前没有被其他维修工接走过。这行校验写得简单但避免了两个人同时点击接单导致的重复分配问题。4.4 文件上传与图片访问学生提交报修时上传故障现场照片这个功能涉及两个地方后端的文件存储和前端的图片预览。后端我用的方案是本地磁盘存储——上传的文件保存到服务器指定目录比如/data/upload/文件名用UUID重命名防止文件名冲突和路径穿越问题。数据库里只存储文件的相对路径比如/upload/2025/01/15/xxxx.jpg。文件上传的Controller方法PostMapping(/api/file/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件不能为空); } // 校验文件大小限制5MB if (file.getSize() 5 * 1024 * 1024) { return Result.error(文件大小不能超过5MB); } // 校验文件类型 String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); if (!Arrays.asList(.jpg, .jpeg, .png, .gif).contains(suffix.toLowerCase())) { return Result.error(仅支持图片文件); } String newFileName UUID.randomUUID().toString().replace(-, ) suffix; // 按日期分目录存储避免单个目录文件过多 String datePath new SimpleDateFormat(yyyy/MM/dd).format(new Date()); File dir new File(uploadPath / datePath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, newFileName)); return Result.success(/upload/ datePath / newFileName); }这里的目录按日期分层的设计非常建议保留。如果没有这一层一年后你的upload目录下会有几千个文件堆在一起文件检索和维护都是噩梦。按日期分目录后既方便做定期备份比如按月归档又可以给后续加CDN做缓存策略留了余地。在SpringBoot里要让用户能通过浏览器直接访问上传的图片需要配置静态资源映射。在application.yml里加上spring: servlet: multipart: max-file-size: 5MB max-request-size: 20MB web: resources: static-locations: classpath:/static/,file:/data/upload/映射配置好后浏览器访问http://localhost:8080/upload/2025/01/15/xxxx.jpg就能直接看到图片了。要注意的是文件上传目录的权限必须设置正确。在Linux服务器上部署时如果运行Java进程的用户对upload目录没有写权限上传就报错如果对目录没有读权限前端图片就加载不出来。我踩过这个坑Java进程用root启动在测试环境没事换到生产环境用普通用户启动后图片全部404排查了半天发现是目录权限问题。5. 前端Vue页面实现与交互逻辑5.1 Vue项目的初始化与工程结构前端的工程化部分我用Vue CLI创建的项目Node版本建议使用14.x或16.x避免版本过高导致webpack编译报错。项目的核心依赖有vue-router路由管理、vuex状态管理、axiosHTTP请求库、element-uiUI组件库、echarts管理员统计报表图表。整个前端的目录结构这样划分src ├── api // 所有接口请求封装按模块分文件 │ ├── auth.js // 登录登出接口 │ ├── repair.js // 报修接口 │ ├── user.js // 用户管理接口 │ └── statistics.js // 统计报表接口 ├── assets // 静态资源 ├── components // 公共组件 │ └── UploadImage.vue // 图片上传组件 ├── router // 路由配置 ├── store // Vuex状态管理 ├── views // 页面组件 │ ├── login/Login.vue │ ├── student/StudentRepair.vue // 学生提交报修页 │ ├── student/MyRepairList.vue // 我的报修记录页 │ ├── worker/WorkerTaskList.vue // 维修工任务列表 │ ├── admin/AdminDashboard.vue // 管理员统计面板 │ ├── admin/WorkerManage.vue // 维修工管理 │ └── common/Profile.vue // 个人中心 └── utils ├── request.js // axios封装统一拦截器 └── auth.js // token存取工具这里的核心设计思想是接口请求统一封装。我在api/目录下创建了按业务模块拆分的接口文件每一个后端接口在前端都有对应的封装函数比如// api/repair.js import request from /utils/request export function submitRepair(data) { return request({ url: /api/repair/add, method: post, data }) } export function getMyRepairList(params) { return request({ url: /api/repair/myList, method: get, params }) }这样的好处是页面组件里不需要直接写URL将来后端接口改了路径只需要改api目录下的一个文件所有调用方自动生效。5.2 登录页与路由守卫的实现登录页看起来简单但交互细节不少。表单需要做前端校验用户名不能为空、密码长度不能少于6位这些在提交前先用Element UI的rules规则做一轮校验减少无效的请求发送。登录成功后的处理顺序很关键async handleLogin() { this.$refs.loginForm.validate(async (valid) { if (!valid) return const res await login(this.loginForm) if (res.code 200) { // 1. 保存token localStorage.setItem(token, res.data.token) // 2. 保存用户信息 localStorage.setItem(userInfo, JSON.stringify(res.data.userInfo)) // 3. 根据角色跳转到不同的首页 const role res.data.userInfo.role const redirectPath role admin ? /admin : role worker ? /worker : /student this.$router.push(redirectPath) } else { this.$message.error(res.message) } }) }这里的角色判断跳转用到了Vue Router的动态路由。在router/index.js里我通过beforeEach导航守卫做全局的登录检查和权限控制router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token) { if (to.path /login) { next() } else { next(/login) } return } // 已登录但没去登录页放行 if (to.path /login) { next(/) return } // 检查路由的meta.roles配置 const roles to.meta.roles const userInfo JSON.parse(localStorage.getItem(userInfo) || {}) if (roles !roles.includes(userInfo.role)) { this.$message.error(没有权限访问该页面) next(/403) return } next() })这段代码解决了两个问题未登录用户任何页面都跳转登录页已登录用户访问没有权限的页面会被拦下来。在实践中最容易漏掉的是第二种情况——很多项目只做了登录校验没做角色校验学生直接访问管理员的URL就能看到后台页面这在答辩时是硬伤。5.3 axios请求封装与拦截器axios封装是前端质量的分水岭。在utils/request.js里统一做三件事请求携带token、响应统一处理业务错误、HTTP错误统一提示。const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) // 请求拦截器自动携带token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) // 响应拦截器统一处理业务码 service.interceptors.response.use( response { const res response.data if (res.code ! 200) { this.$message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) this.$message.error(登录已过期请重新登录) window.location.href /login } else { this.$message.error(error.message || 网络异常) } return Promise.reject(error) } )这里有两个值得注意的细节。第一baseURL是通过环境变量配置的所以在env.development里配http://localhost:8080在env.production里配线上后端地址。这样切换环境只需要改一个环境变量文件不需要改代码。第二401状态码的处理需要做全局跳转登录页。因为token过期后用户任何操作都会返回401如果没有这个全局处理用户在页面里点了半天没反应体验非常差。5.4 报修工单列表页的表格组件化工单列表是三个角色都会用到的页面虽然数据来源和展示字段不同但整体布局高度相似。我抽取了一个公共组件RepairOrderTable.vue通过props传入statusList和columns配置来适配不同场景页面组件只需要关注数据加载和操作回调。学生端“我的报修”页面的核心方法async loadMyRepairs() { const params { page: this.currentPage, size: this.pageSize, status: this.filterStatus } const res await getMyRepairList(params) this.tableData res.data.records this.total res.data.total }列表页的状态标签我用自定义函数处理成不同颜色的标签这样用户在页面上一眼就能看出哪些工单需要关注getStatusTagType(status) { const map { 0: info, // 待受理 - 灰色 1: warning, // 待维修 - 橙色 2: primary, // 维修中 - 蓝色 3: success, // 已完成 - 绿色 4: danger, // 已取消 - 红色 5: success // 已验收 - 深绿 } return map[status] || info }维修工任务列表有一个特殊功能显示“抢单”按钮。因为多个维修工可能同时刷新这个列表谁先点接单工单就被谁抢走。前端表现上点击接单后按钮立即变为禁用状态并且显示“已被xx接单”其实更严谨的做法是每次用户点击接单后后端更新成功才刷新列表如果后端返回工单已被其他人接走前端弹提示“手慢了工单已被接走”。这个交互细节非常真实我在代码里专门用了防重复提交处理避免用户手滑连续点击导致后端被刷多个请求。5.5 管理员统计报表的图表实现管理员的统计面板是我觉得最能提升项目“高级感”的模块。我用ECharts做了一个图表页包含三个核心图表近7天报修数量趋势折线图、不同报修类型占比饼图、维修工接单量排行柱状图。折线图的后端数据接口是统计近七天每天的报修量SELECT DATE(create_time) AS day, COUNT(*) AS cnt FROM repair_order WHERE create_time DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(create_time) ORDER BY day前端拿到数据后这样渲染const chart echarts.init(document.getElementById(trendChart)) chart.setOption({ xAxis: { type: category, data: days }, yAxis: { type: value }, series: [{ data: counts, type: line, smooth: true, areaStyle: {} }] })ECharts的官方文档里案例很多照着改数据就行难度不大。但有一个坑我得提醒图表容器必须有明确的高度。初始化ECharts时如果容器div的高度为0图表就什么都不显示。我的做法是在样式里给容器设置height: 400px或者在mounted里用setTimeout(() chart.resize(), 100)延迟初始化。6. 系统部署与上线实战6.1 后端打包与服务器部署后端部署前先把配置文件里的环境信息改好。application.yml里的数据库连接、文件上传路径等从开发环境切换到生产环境时都需要调整。我的经验是使用SpringBoot的多环境配置支持spring: profiles: active: profile.active然后在pom.xml里配置profileprofiles profile iddev/id propertiesprofile.activedev/profile.active/properties /profile profile idprod/id propertiesprofile.activeprod/profile.active/properties /profile /profiles打包命令对应开发环境是mvn clean package -Pdev -DskipTests生产环境是mvn clean package -Pprod -DskipTests。这样数据库地址、密钥等环境相关的配置都在各自的配置文件里互不干扰。打包完成后在服务器上执行java -jar campus-repair-system.jar --spring.profiles.activeprod如果想让jar包在后台运行用nohup命令nohup java -jar campus-repair-system.jar --spring.profiles.activeprod /var/log/campus-repair.log 21 这里强烈建议设置-Xms和-Xmx参数来指定JVM初始内存和最大内存比如-Xms256m -Xmx512m。对于这种体量的管理系统512MB内存完全够用但如果不设上限JVM默认会启动时分配很大内存小服务器上很容易因为内存不足被系统杀掉进程。6.2 前端打包与部署策略前端构建npm run build构建完成后项目根目录会生成dist文件夹里面是静态文件。部署有两种常见方案。方案一把dist里的文件拷贝到Nginx的静态资源目录Nginx同时负责反向代理转发API请求。这是最推荐的方案。方案二直接把dist目录放到SpringBoot的resources/static/下重新打包成jar一起部署。这个方案前期省事但每次前端改代码都要重新打jar包不推荐在正式项目中使用。这里我把Nginx的配置贴出来这部分是网上教程最容易含糊的地方server { listen 80; server_name your-domain.com; # 前端静态资源 root /var/www/campus-repair/dist; index index.html; # 解决Vue History模式刷新404问题 location / { try_files $uri $uri/ /index.html; } # API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传的图片访问 location /upload/ { alias /data/upload/; } }这里try_files那行非常关键它的作用是当用户访问/student/repair这类前端路由地址时Nginx发现磁盘上没有对应的文件就用index.html兜底返回由Vue Router接管路由渲染。如果不加这行用户直接刷新二级页面时会出现404。6.3 服务器环境准备与数据库初始化在服务器上部署前需要先装好JDK、MySQL、Nginx这三样东西。JDK安装建议用openjdk 8版本和SpringBoot 2.x兼容性最好。MySQL的初始化脚本导入用这个命令mysql -u root -p /usr/local/campus-repair/docs/sql/init.sql导入后验证一下表是否创建成功mysql -u root -p -e USE campus_repair; SHOW TABLES;这里需要注意数据库的时区问题。很多服务器默认时区是UTC会导致create_time时间字段在MySQL中显示的时间和本地时间差8小时。解决方法是启动MySQL时加上--default-time-zone08:00或者在MySQL配置文件的[mysqld]节点下添加default-time-zone08:00。6.4 部署后的安全加固措施系统上线后不能裸奔有几个安全动作必须做第一修改MySQL的root密码并创建一个最小权限的专用账号。不要用root连接业务数据库给应用创建一个只允许操作campus_repair库的账号。CREATE USER repair_applocalhost IDENTIFIED BY StrongPassword123!; GRANT SELECT, INSERT, UPDATE, DELETE ON campus_repair.* TO repair_applocalhost; FLUSH PRIVILEGES;第二SpringBoot的配置文件里不要用明文密码。可以把数据库密码通过环境变量注入password: ${DB_PASSWORD}在启动时设定环境变量。防止配置文件泄露后数据库直接失守。第三Nginx层加基础的安全响应头。在server配置中添加add_header X-Frame-Options SAMEORIGIN; add_header X-XSS-Protection 1; modeblock; add_header X-Content-Type-Options nosniff;这些响应头能防御一部分常见Web攻击。对于校园报修系统来说做到这个程度已经足够了不需要上WAF或者更复杂的防护。7. 开发与部署中的常见问题排查7.1 跨域请求报错的定位与解决前后端分离项目最常见的第一个问题就是跨域。前端访问后端接口时浏览器控制台报Access-Control-Allow-Origin错误。跨域的根源在于前端运行在http://localhost:8081后端运行在http://localhost:8080两者端口不同触发了浏览器的同源策略。解决方案有两种后端加跨域配置或前端用代理。后端加全局跨域配置最省事在SpringBoot里写一个CorsConfigConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }前端开发环境下也可以用vue.config.js里的devServer代理来规避跨域但生产环境跨域最终还是得靠后端配置或者Nginx代理解决。如果你在部署后发现跨域问题优先检查Nginx的反向代理配置是否正确以及后端CORS配置的allowedOrigins是否包含了你的前端域名。7.2 图片上传后前端无法访问这个问题我前面提到过但作为高频问题值得再拎出来强调一次。表象是上传成功了数据库里也有路径但浏览器访问图片URL返回404。排查步骤按顺序来检查服务器上该路径的文件是否存在ls /data/upload/2025/01/15/。如果文件不存在说明Java进程的启动用户对upload目录没有写权限用chown改目录属主。如果文件存在但404检查SpringBoot静态资源映射配置是否生效。如果本地正常但服务器异常大概率是Nginx的/upload/location配置写错了。alias路径要写全比如alias /data/upload/;alias最后面的斜杠不能丢。7.3 工单状态异常接单后发现工单已被别人接走这个问题的本质是并发场景下的超卖问题。两个维修工同时点击接单后端都查到了状态为“待维修”的工单两个请求都认为可以接单结果工单被分配给了两个人。解决方案是加一层数据库层面的乐观锁在repair_order表加一个version字段更新时加上WHERE version ?判断。MyBatis Plus支持Version注解配置好之后自动实现乐观锁Version private Integer version;或者更直接的方案是接单时用一条原子SQL完成状态变更UPDATE repair_order SET worker_id #{workerId}, status 2, accept_time NOW() WHERE id #{orderId} AND status 1一条SQL解决了“查询更新”两步合并的问题如果数据库返回影响行数为0说明工单已经被别人接走后端返回“该工单已被接单”即可。这种方案简单粗暴且完全有效。7.4 打包后配置文件不生效有些同学本地开发时一切正常把项目打包部署到服务器后发现数据库连不上检查半天发现配置文件的改动没有生效。这是Maven资源过滤导致的坑。SpringBoot打包时application.yml会被处理但如果你在src/main/resources下放了application-prod.yml在打生产包时可能没有被正确复制到jar包中。解决方式是在pom.xml中显式配置资源目录resources resource directorysrc/main/resources/directory filteringtrue/filtering /resource /resources打包后用这个命令验证jar包里的配置文件是否正确jar tf campus-repair-system.jar | grep application7.5 部署运维常用命令速查最后整理一份我常用的部署运维命令都是实际使用频率最高的操作命令查看后端日志tail -f /var/log/campus-repair.log重启后端服务pkill -f campus-repair-system.jar nohup java -jar ... 查看端口占用netstat -tlnp | grep 8080查看磁盘空间df -h数据库备份mysqldump -u root -p campus_repair backup_$(date %Y%m%d).sql数据库恢复mysql -u root -p campus_repair backup_20250115.sqlNginx重载配置nginx -s reloadNginx检查配置nginx -t数据库定时备份我建议直接上crontab每天凌晨全量备份一次保留最近7天0 2 * * * mysqldump -u root -pYourPassword campus_repair /data/backup/campus_repair_$(date \%Y\%m\%d).sql find /data/backup -mtime 7 -delete这样即使出现误删数据或服务器崩溃的情况最多丢失一天的数据而且可以从备份中恢复。8. 项目经验总结与扩展方向建议做完这套校园报修系统我对“一个完整项目”的认知比之前零散地学框架要清晰得多。在学校里学SpringBoot每个章节是一个独立的知识点JWT、MyBatis、文件上传、拦截器分开学感觉都会但真正把它们糅合到一个系统里时才发现很多知识之间有隐藏的依赖关系。比如JWT认证和拦截器如果只学了JWT的生成和解析不知道拦截器怎么注册、怎么排除不需要认证的路径登录功能就做不完整。再比如事务单表插入看起来很简单但两张表同时插入时如果不知道Transactional数据库就会出现脏数据。这些“连接点”的知识只有通过完整项目才能串联起来。这个项目的数据库表只有五张如果想让项目体验更完整后续可以从这几个方向扩展一是加入通知提醒功能。学生提交报修后通过WebSocket向维修工推送新工单提醒维修工提交完工后向学生推送您的工单已完成消息。WebSocket的推送机制在同一个服务器上实现并不复杂但会给系统增加非常自然的实时感。二是增加消息通知站内信表。维修工接单、完工、学生验收等关键节点都生成一条站内信用户在系统右上角能查看未读消息数。这个功能在答辩时很容易成为加分项因为它体现了对业务闭环完整性的思考。三是引入定时任务做服务保障。比如每天凌晨3点自动检查是否有待受理超过24小时的工单自动升级为“超时工单”并通知管理员。用SpringBoot自带的Scheduled注解就能实现。四是为移动端适配做准备。目前页面用的是Element UI的响应式布局在手机浏览器上能看但体验一般。下一步可以做一个Vue移动端版本或者直接套一个移动端的UI组件库比如Vant。由于后端接口都是RESTful风格前端换成移动端组件只是视图层的替换接口完全不用动。最后再分享一个我在实际部署中总结的体会很多开发者做完项目后以为“跑通了”就等于“做完了”其实部署上线这一步涉及的知识——环境配置、Nginx反向代理、JVM参数调整、数据库备份——恰恰是工作中最容易遇到的也是面试中面试官最倾向问的细节。这套校园报修系统只要按我上面写的流程完整走一遍部署整个全栈项目的技能树就算基本点亮了。如果你正在做类似的系统遇到具体的问题比如某个接口查询卡住、前端页面出不来、部署时各种报错可以顺着我上面排查的思路去定位。这类业务管理系统的技术难点其实不在某个单独的技术点上而在数据如何在页面、后端、数据库之间正确流转。把这条链路打通了你会觉得整个项目一下子变得简单了。本文还有配套的精品资源点击获取
返回列表