Spring Boot+Vue实现隐患排查闭环管理系统:从设计到部署全流程复盘
2026/9/1 18:25:36 网站建设 项目流程

简介:本资源是一套面向高校计算机专业本科生的毕业设计级安全生产隐患排查与整治管理信息系统,基于Spring Boot 2与Vue双技术栈构建,聚焦企业安全监管数字化场景,解决隐患信息采集、多维统计分析及闭环整改跟踪等核心业务问题。系统采用前后端分离架构,含SpringBoot后端(8088端口)、Vue采集端与vue-admin管理端三部分,配套MySQL数据库脚本及Redis缓存支持,技术组合覆盖MyBatis-Plus、Element UI、Vant UI等主流组件。压缩包共310个文件,含102个Java后端逻辑类、59个JS交互脚本、46个Vue页面组件、18个XML配置与SQL建库文件,整体仅2.16MB,结构紧凑、模块清晰,便于快速部署与二次开发。目前已有78人学习下载,提供完整可运行源码、标准化数据库初始化脚本及典型配置文件(如yml、bat启动脚本),适合作为毕设选题参考、全栈开发实战训练或安全信息化项目原型基础。 每年到毕业设计季,“XX管理系统”类题目几乎占半壁江山,其中“安全生产隐患排查与整治管理信息系统”出镜率尤其高。但说实话,我见过太多版本把它做成了“隐患登记的增删改查”——登记一条隐患、列表查一下、然后就没了。真正做过项目的人会告诉你,这个题目最大的价值不在CRUD,而在“排查—整改—复查—归档”的业务闭环和状态流转。这篇博文我就用一套能完整跑起来的springboot + vue前后端分离项目做一次全链路复盘:先说怎么拆业务,再说数据库怎么设计,然后把手写后端接口、前端页面、部署踩坑、答辩高频问题全部过一遍。不管你是手里已经有一份参考源码和数据库脚本想看懂逻辑,还是打算从零把系统做完整,这篇文章都能给你一条清晰的落地路径。

1. 先想清楚这是一套“闭环业务系统”,不是普通增删改查

1.1 排查—整改—复查—归档:业务闭环才是这个题目的核心价值

很多同学看到“管理系统”三个字就默认它是一个数据维护页面,但实际上安全生产隐患排查业务是一条完整的“工单流”。你可以把它理解成一个带状态流转的工单系统:安全员在现场发现了隐患,拍照、记录、提交;负责人审核这条隐患是否属实、定级;定级后派给具体责任人去整改;责任人整改完要上传整改后的照片和措施;最后安全管理员复查确认问题真正解决,隐患才算闭环归档。

如果只是做一个表单向导加一个列表页,那可能两天就写完了,但答辩老师一眼就能看出你没有深入理解业务。这个题目真正的加分点在于:一条隐患从发现到销项,每一步都在系统里留下可追踪的记录,整个过程是可回溯的。这正好是企业安全管理的核心要求——“一患一档”,一条隐患一个档案,从登记到复查全流程留痕。

1.2 角色权限拆解:四类用户各自关心什么

在动手写代码之前,建议先把角色和权限理清楚。我参考了许多企业和学校实际项目的做法,将用户划分为四类角色:

角色核心操作关注的业务数据
系统管理员用户管理、部门管理、角色分配、隐患分类维护系统的基础数据稳定、账号权限正确
安全排查员登记隐患、上传现场照片、查看自己登记的隐患进度“我发现了哪些隐患、整改进度如何”
安全负责人隐患审核、隐患定级、派发整改任务、查看超期隐患隐患是否真实、该派给谁、有没有人超期不整改
整改责任人接收整改任务、上报整改措施和照片我名下有哪些待整改任务、还剩多少时间
安全管理员/复查员复查整改结果、通过或退回、统计报表隐患是否真正闭环、各区域/部门隐患统计数据

实际编码里,我不建议做太细粒度权限,菜单权限加按钮权限就够了。后端在每个需要控制的接口上做角色校验,前端根据当前用户角色控制按钮的显示隐藏,这样既不用引入复杂的权限框架,又能把“权限控制”这件事在答辩时讲清楚。

1.3 隐患状态机:让“整改闭环”可追踪、可统计

状态机是这个项目的灵魂。我最初做的时候偷懒,只在表里放一个“状态”字段,后来发现根本没法做到位:隐患被驳回之后应该回到哪里?超期的任务怎么标记?复查不通过是退回整改还是直接作废?这些问题必须靠设计一套明确的状态流转规则来解决。

我整理出来的核心状态定义是:0待审核、1审核通过待派发、2整改中、3待复查、4已闭环、5已驳回、6已逾期。流转路径如下:

  • 排查员登记:0 待审核
  • 负责人审核通过:0 → 1 审核通过/待派发;审核不通过:0 → 5 已驳回
  • 负责人派发给整改人:1 → 2 整改中
  • 整改人提交整改结果:2 → 3 待复查
  • 复查员复查通过:3 → 4 已闭环;复查不通过:3 → 2 退回整改,并要求重新整改
  • 超过整改截止时间未完成:2 → 6 已逾期(逾期后仍然可以继续整改,但整改进度页要醒目提示)

这个状态机一旦定下来,后面的接口设计、前端按钮渲染、列表筛选全部围绕它展开,开发效率会高很多。更重要的是,答辩时老师问“系统的核心业务难点是什么”,你直接讲这套状态流转和全流程留痕,已经能拿到大半分了。

2. 技术选型复盘:为什么是Spring Boot + Vue,版本怎么定

2.1 单体 + 前后端分离:毕业设计最稳的组合

很多同学纠结要不要上微服务、要不要用Redis、要不要搞消息队列。我的建议是:别贪多,毕业设计最核心的目标是完整可用、逻辑清楚、能讲明白。微服务拆出来五六个子服务,演示的时候启动都要花半天,反而是负担。

Spring Boot作为后端框架的优势在于自动配置、内嵌Tomcat,打包成jar之后一键启动;Vue作为前端框架的优势在于组件化和数据驱动,列表、弹窗、表单这类交互写起来比JSP和模板引擎顺手太多。这个组合还有一个隐藏好处:前后端分离的项目结构本身就是目前企业开发的主流形态,你在简历和毕业设计说明里写出来,面试官看了不会觉得是“过时的教学项目”。

2.2 版本选型的现实考量:JDK8 + Spring Boot 2.7 + MyBatis-Plus

这里是曾经最容易翻车的地方。搜索引擎里“springboot版本太高”这个热词,八成都是我这种过来人贡献的搜索量。我明确说一下我的选择:

组件版本选择理由
JDK1.8学校机房和实验室最常见,兼容性最稳,资料最多
Spring Boot2.7.x配JDK8最省心,javax包不会被替换
MyBatis-Plus3.5.x内置分页插件、条件构造器,写起来高效
MySQL5.7 / 8.0两个版本语法差异不大,本文SQL两者都兼容
前端Vue 3 + Vite + Element Plus主流方案;如果你只会Vue2,改成Vue2 + ElementUI思路完全一样
认证方案JWT + HandlerInterceptor轻量、易解释,不引入Spring Security

为什么不用Spring Boot 3?因为Spring Boot 3要求JDK17,而且原来的javax.servlet换成了jakarta.servlet,很多老教程直接抄会报红。如果不巧你下载了一个Spring Boot 3.x的初始化项目,在学校电脑的JDK8环境下根本启动不了,这就是“版本太高”的典型翻车现场。当然,如果你的机器装了JDK17,用Spring Boot 3也没问题,本文所有业务逻辑一样适用,只是导入包时留意一下命名空间。

为什么不用Spring Security做登录安全?我是刻意不用的。Spring Security的学习曲线对毕设来说偏陡,配置起来容易卡住,而且你很难在答辩现场用五分钟把过滤器链讲清楚。我用JWT加自定义拦截器,代码量少、逻辑直观:登录成功后发一个token,前端带上token访问接口,拦截器里校验token并解析出当前用户和角色。这一套逻辑非常适合答辩展示。

2.3 项目结构规划:后端分层 + 前端目录

拿到一个项目先别急着敲代码,先把目录结构定好。我的后端包结构是这样设计的:

com.safety ├── config // 跨域配置、MyBatis-Plus分页配置、静态资源映射 ├── controller // 接口层,只做参数接收和结果返回 ├── service // 业务层,核心逻辑都在这层 ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 数据库表对应的实体类 ├── dto // 接收前端参数的传输对象 ├── vo // 返回给前端的结果对象 ├── common // 统一返回结构、错误码、全局异常 ├── interceptor // JWT登录拦截器 └── utils // 编号生成等工具类

前端按照Vue工程的常规方式组织:api目录封装接口请求,views目录放页面组件,router配置路由,store管理用户状态,utils放axios实例和工具函数。把目录结构定清楚,后面写代码就变成“往对应位置填代码”了,越到后期越轻松。

3. 数据库设计:一患一档 + 全流程留痕

3.1 核心表拆解:从隐患登记表到状态流转日志

先搞清楚需要哪些表。我做的时候没有堆砌大量字典表,保证够用又不冗余:

  • sys_user用户表:包含账号、密码、姓名、手机号、角色ID、部门ID
  • sys_role角色表:管理员、排查员、负责人、整改人、复查员
  • sys_dept部门表:不是必须,但部门会决定“这个隐患归哪个区域/车间管”,建议保留
  • danger_type隐患分类表:用电安全、消防安全、设备设施、作业行为、危化品等
  • hidden_danger隐患主表:整个系统的核心,一条记录代表一条隐患
  • danger_log隐患操作日志表:登记、审核、派发、整改、复查每一步都写入

这些表之间的关联关系很简单:一个用户属于一个角色、一个部门;一条隐患有发现人、整改责任人、复查人;一条隐患有多条操作日志。如果你在IDEA里通过Database面板生成数据库脚本,可以直接导出建表语句,很方便。

3.2 隐患编号规则与常用字段设计细节

隐患编号是一个容易被忽略但很重要的细节。数据库主键id是自增的,但在真实业务场景中,用户和监管方需要一条可读的隐患编号。我参考实际管理系统的做法,生成规则如下:

YH + 日期 + 四位序号 示例:YH202501170001

后端生成逻辑是这样的:查询当天已登记的最大编号,取后四位加1后拼上日期前缀。这个编号规则用文字说一句就行,代码实现也不复杂,但它能让人一眼看出隐患的登记时间,演示录屏时非常有“真实系统”的感觉。

隐患主表的关键字段如下,我把容易踩坑的字段设计也标出来了:

字段名类型说明
danger_codevarchar(20)隐患编号,唯一索引
danger_titlevarchar(200)隐患标题,一句话概括
danger_leveltinyint隐患等级:1一般、2较大、3重大
danger_type_idint关联隐患分类表
descriptiontext隐患详细描述
locationvarchar(200)隐患所在位置/区域
discoverer_idint发现人ID
statustinyint状态(对应前面说的状态机)
assignee_idint当前整改责任人ID
deadlinedatetime整改截止时间
rectify_measuretext整改措施描述
rectify_photovarchar(500)整改后照片路径,JSON数组
review_commentvarchar(500)复查意见
create_timedatetime登记时间
update_timedatetime最后更新时间

有一个细节要注意:status在MySQL里不是绝对保留字,但个别版本的驱动或某些查询语句中还是可能冲突,稳妥起见我建表时统一用反引号包一下字段名。类似这样的还有orderdesc,尽量不要用来当列名,能省很多莫名其妙的问题。

3.3 建表SQL示例与索引设计

下面这个hidden_danger建表语句是我调通之后稳定使用的版本,你可以直接参考:

CREATE TABLE `hidden_danger` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `danger_code` varchar(20) NOT NULL COMMENT '隐患编号', `danger_title` varchar(200) NOT NULL COMMENT '隐患标题', `danger_level` tinyint(4) DEFAULT NULL COMMENT '隐患等级 1一般 2较大 3重大', `danger_type_id` bigint(20) DEFAULT NULL COMMENT '隐患分类ID', `description` text COMMENT '隐患描述', `location` varchar(200) DEFAULT NULL COMMENT '隐患位置', `discoverer_id` bigint(20) DEFAULT NULL COMMENT '发现人ID', `assignee_id` bigint(20) DEFAULT NULL COMMENT '整改责任人ID', `status` tinyint(4) DEFAULT '0' COMMENT '状态 0待审核 1待派发 2整改中 3待复查 4已闭环 5已驳回 6已逾期', `deadline` datetime DEFAULT NULL COMMENT '整改截止时间', `rectify_measure` text COMMENT '整改措施', `rectify_photo` varchar(1000) DEFAULT NULL COMMENT '整改照片路径', `review_comment` varchar(500) DEFAULT NULL COMMENT '复查意见', `create_time` datetime DEFAULT NULL COMMENT '登记时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_danger_code` (`danger_code`), KEY `idx_status` (`status`), KEY `idx_discoverer` (`discoverer_id`), KEY `idx_assignee` (`assignee_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='隐患主表';

索引设计上,我建议在statusdiscoverer_idassignee_id上建索引。为什么?因为系统里最高频的查询就是这三个条件:按状态查待办、查我发现的、查派给我的。这几个索引建好,数据量到几万条时查询也基本没有压力。至于danger_code,做唯一索引是为了保证编号不会重复。

操作日志表就简单很多:

CREATE TABLE `danger_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `danger_id` bigint(20) NOT NULL COMMENT '隐患ID', `operator_id` bigint(20) DEFAULT NULL COMMENT '操作人ID', `operation` varchar(20) DEFAULT NULL COMMENT '操作类型', `content` varchar(500) DEFAULT NULL COMMENT '操作内容', `create_time` datetime DEFAULT NULL COMMENT '操作时间', PRIMARY KEY (`id`), KEY `idx_danger_id` (`danger_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='隐患操作日志表';

别小看这张日志表,它是“全流程留痕”的关键支撑,也是答辩时展示业务理解深度的好素材。

4. 后端实现:接口、鉴权、统一处理与核心业务代码

4.1 统一返回结构与全局异常

后端接口我一开始就开始规范设计,因为如果每个接口返回不同结构,前端封装axios会想骂人。我先定义了一个统一的Result返回类:

@Data public class Result<T> { private Integer code; // 200成功,500失败 private String message; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static <T> Result<T> error(String message) { Result<T> r = new Result<>(); r.setCode(500); r.setMessage(message); return r; } }

配套的还需要一个全局异常处理器,用@RestControllerAdvice捕获业务异常和运行时异常,统一转成Result.error(...)返回。这样前端接到任何响应都能用同一套逻辑判断成功失败,不会出现“这个接口data在result里、那个接口报错在message里”的混乱。全局异常处理属于后端工程化的基本功,写了它在答辩时能加分,项目也会显得更规范。

4.2 JWT登录鉴权与拦截器:为什么不用Spring Security

登录接口的设计思路是:用户传用户名密码,后端校验通过后,用JWT生成一个token,token里包含userId和userName,然后返回给前端。前端把它存到localStorage,之后每次请求在请求头带上Authorization: Bearer <token>。后端写一个TokenInterceptor拦截器,统一校验请求头里的token,解析出用户信息后放到ThreadLocal或用请求参数传递。

拦截器的核心逻辑像下面这样:

public class TokenInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); String userId = JWTUtil.parseToken(token).getPayload("userId").toString(); request.setAttribute("userId", userId); return true; } response.setStatus(401); return false; } }

这里有几个细节必须注意:一是登录接口、文件上传接口等需要放行,在WebMvcConfigurer配置拦截器时用excludePathPatterns排掉;二是前后端分离项目一定会遇到跨域,所以要写一个CorsFilter或实现WebMvcConfigureraddCorsMappings,否则前端请求会被浏览器拦截。这两点我在学校机房调试时经常看到别人卡半天,提前处理掉会顺利很多。

不用Spring Security的理由我在前面讲过,再补充一句:Security太重,你不仅要理解过滤器链,还要处理登录成功/失败处理器、密码加密器、会话管理,对毕设来说成本和收益不成正比。JWT加拦截器,两节课就能讲明白,演示也好复现。

4.3 隐患登记的完整后端流程:事务与日志

隐患登记不是简单插一条记录,它同时要做两件事:往hidden_danger表插入隐患数据,往danger_log表写一条“登记”日志。这两步必须在一个事务里,要么都成功,要么都失败。我直接在service方法上加了@Transactional

登记接口的Service代码如下:

@Transactional(rollbackFor = Exception.class) public Long addDanger(DangerAddDTO dto, Long userId) { HiddenDanger danger = new HiddenDanger(); danger.setDangerCode(generateDangerCode()); danger.setDangerTitle(dto.getDangerTitle()); danger.setDangerLevel(dto.getDangerLevel()); danger.setDangerTypeId(dto.getDangerTypeId()); danger.setDescription(dto.getDescription()); danger.setLocation(dto.getLocation()); danger.setDiscovererId(userId); danger.setStatus(0); // 待审核 danger.setCreateTime(LocalDateTime.now()); danger.setUpdateTime(LocalDateTime.now()); dangerMapper.insert(danger); DangerLog log = new DangerLog(); log.setDangerId(danger.getId()); log.setOperatorId(userId); log.setOperation("登记"); log.setContent("登记隐患:" + dto.getDangerTitle()); log.setCreateTime(LocalDateTime.now()); dangerLogMapper.insert(log); return danger.getId(); }

generateDangerCode()的实现也不复杂:先查当天最早的一条或最大编号,然后拼接序列号。一个小技巧是,在自定义工具类里用DateTimeFormatter.ofPattern("yyyyMMdd")格式化日期,再配合String.format("%04d", seq)补零,这样生成的编号始终是固定长度,排序也好看。

事务这块还有一个常见坑:如果同一个类里的方法A调用了带@Transactional的方法B,因为走的是this调用而不是Spring代理对象,事务会失效。所以如果哪天发现数据只插入了一半而没有报错,优先去查是不是“自调用”导致的。这个问题我帮好几个同学排查过,每次都是这种低级但隐蔽的原因。

4.4 隐患整改、复查、驳回的状态流转实现

状态流转如果写成一堆if-else嵌在Controller里,后面一定会改到崩溃。我建议把“更新状态”收敛成一个统一方法,入参是隐患ID、操作人、期望的旧状态、新状态、日志内容,在方法里做状态校验:

@Transactional(rollbackFor = Exception.class) public boolean changeStatus(Long dangerId, Long operatorId, Integer expectStatus, Integer targetStatus, String operation, String content) { LambdaUpdateWrapper<HiddenDanger> wrapper = Wrappers.lambdaUpdate(); wrapper.eq(HiddenDanger::getId, dangerId) .eq(HiddenDanger::getStatus, expectStatus) // 期望状态,防止并发误操作 .set(HiddenDanger::getStatus, targetStatus) .set(HiddenDanger::getUpdateTime, LocalDateTime.now()); boolean updated = dangerMapper.update(null, wrapper) > 0; if (!updated) { throw new BusinessException("状态已发生变化,请刷新后重试"); } // 写日志 DangerLog log = new DangerLog(); log.setDangerId(dangerId); log.setOperatorId(operatorId); log.setOperation(operation); log.setContent(content); log.setCreateTime(LocalDateTime.now()); dangerLogMapper.insert(log); return true; }

这样设计的好处是,状态流转的中心化校验逻辑全部收口到一处,接口层只要“意图明确”,比如“复查通过”就调用changeStatus(id, operatorId, 3, 4, "复查", "复查通过,隐患闭环")。如果状态不匹配(比如隐患还在待审核就被复查),方法会直接抛异常,不会产生脏状态。

“复查不通过”这个动作也要考虑清楚。我的处理是3 → 2,即退回整改中,同时把复查意见写入review_comment字段,让整改人知道为什么被退回来。注意这里不是让状态回到待派发,因为整改任务已经派发过,问题出在整改结果不达标,直接退回整改中更符合真实业务。

5. 前端实现:页面怎么分级,交互怎么与状态联动

5.1 前端技术栈与项目结构

前端我用的是Vue 3 + Vite + Element Plus。创建项目的命令很简单:

npm create vite@latest safety-web -- --template vue cd safety-web npm install npm install element-plus axios vue-router pinia

开发时用Vite的代理解决跨域,打包后部署到Nginx或直接让后端统一托管。src目录下我建议把页面按模块分组:

src/views ├── login // 登录页 ├── dashboard // 首页统计看板 ├── danger │ ├── list.vue // 隐患列表(核心页面) │ ├── add.vue // 隐患登记 │ ├── detail.vue // 隐患详情与流转记录 │ ├── rectify.vue // 整改上报 │ └── recheck.vue // 复查验收 ├── system │ ├── user.vue // 用户管理 │ ├── role.vue // 角色管理 │ └── type.vue // 隐患分类管理

页面尽量拆小:列表页只负责筛选和表格,详情页负责展示时间线和操作按钮,新增和整改用独立页面或弹窗。Vue组件化开发的好处就在这里,拆好之后每个页面代码量都不大,维护起来舒服很多。

5.2 axios封装、路由守卫与登录态

axios实例我只封装一次,放在utils/request.js里。核心作用是统一带上token、统一处理响应错误:

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '../router' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res } ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') localStorage.removeItem('userInfo') router.push('/login') } ElMessage.error('网络异常,请稍后重试') return Promise.reject(error) } ) export default request

路由守卫的逻辑很简单:进入页面前判断有没有token,没有就跳登录页。白名单包括登录页和首页,其余路由都设置为需要登录。这样即使用户手动输入URL,也会被拦回登录页,前端层面的登录态就闭环了。

5.3 隐患列表页:状态驱动按钮渲染

隐患列表页是整个系统前端最核心的页面,它的设计原则是“按状态和角色动态渲染操作按钮”。比如:

  • 当前登录人是负责人,该行状态是0待审核,操作列显示“审核”按钮
  • 当前登录人是整改人,该行状态是2整改中,操作列显示“提交整改”按钮
  • 当前登录人是复查员,该行状态是3待复查,操作列显示“复查”按钮
  • 已闭环、已驳回的记录只显示“详情”

Element Plus的表格列里直接写条件判断就可以:

<el-table-column label="操作" width="220" fixed="right"> <template #default="{ row }"> <el-button v-if="canReview(row)" type="primary" link @click="openRecheck(row)"> 复查 </el-button> <el-button v-if="canRectify(row)" type="warning" link @click="openRectify(row)"> 提交整改 </el-button> <el-button type="info" link @click="openDetail(row)"> 详情 </el-button> </template> </el-table-column>

canReviewcanRectify这类方法在Vue 3里直接用computed或普通方法写判断即可,核心就是“当前用户的角色 + 当前行的状态”两个条件同时满足才显示按钮。列表顶部再放几个筛选条件:按隐患等级、按状态、按关键字模糊搜索,配合后端分页接口,一套完整的列表查询就出来了。

5.4 隐患登记与整改表单:图片上传与校验

隐患登记页面要填写标题、类型、等级、位置、描述,同时支持上传现场照片。图片上传我直接用Element Plus的el-upload组件,上传地址指向后端的/api/file/upload,请求头带上token:

<el-upload action="/api/file/upload" :headers="uploadHeaders" :on-success="handleUploadSuccess" list-type="picture-card" multiple> <el-icon><Plus /></el-icon> </el-upload>

后端上传接口接收MultipartFile,把文件保存到一个指定的上传目录,然后把访问路径作为字符串返回,前端把路径塞进表单一起提交。这个方案简洁、可控,也不用引入OSS或七牛云。对于隐患管理这种内部系统,本地存储完全够用。

整改表单是另一个需要仔细校验的页面。整改人除了填写整改措施外,最好强制上传整改后的照片,不然复查员很难闭环确认。表单提交时做一个前端必填校验,后端接口里也做一重判空,双重保障。

6. 踩坑实录:从开发到部署,最常见的7个坑

6.1 Spring Boot版本太高导致JDK不兼容

我一开始用的是最新版Spring Boot 3.x,项目建好之后在本地怎么都启动不起来,报错提示UnsupportedClassVersionError,点击错误信息才发现是JDK版本不匹配。后来把所有依赖降到2.7.x,又顺手把javax.servlet的包名全部确认了一遍,问题才解决。

这个坑在网上常年有人问,核心原因是Spring Boot 3.x强制要求JDK17,而很多学校电脑、老师演示用的机器装的还是JDK8。如果你只是从某个项目模板拷贝了依赖坐标,没有注意版本对应关系,大概率会踩中。建议直接锁定Spring Boot 2.7.x + JDK8,这是毕业设计最稳的组合。如果确实想用Spring Boot 3,那务必确认你的环境是JDK17以上,并且在配置Maven时统一好编译器版本。

6.2 MyBatis-Plus分页不生效

MyBatis-Plus虽然自带分页插件,但它不会自动开启,需要手动写一个配置类:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

如果不加这个配置,调用selectPage方法时不会真正执行LIMIT,而是把全表数据都查出来,只是前端看着像分页。这个问题很隐蔽,因为小数据量下根本感知不出来。答辩前检查一下系统里有没有这个配置,能避免一个很大的尴尬。

6.3 LocalDateTime序列化不一致

从Java 8开始大家都在用LocalDateTime,但如果你在实体类里直接放这个类型,前端拿到的可能是2025-01-17T10:30:00这种带T的格式,或者直接变成一长串时间戳,非常不友好。解决方式是在application.yml里统一配置:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

加了之后,后端返回的所有LocalDateTime都会统一格式化成yyyy-MM-dd HH:mm:ss,前端展示省了很多事。这个配置尽早加,越早效果越好。

6.4 文件上传的路径和静态资源映射

本地开发时图片上传成功,但在线演示时却访问不到。原因通常是上传的文件保存到了本地绝对路径,而后端启动后没有把这个目录暴露成静态资源。解决方法是在WebMvcConfigurer里加资源映射:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath = System.getProperty("user.dir") + "/upload/"; registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath); }

这里有个细节:如果项目打成jar包在服务器上运行,user.dir是jar所在目录,上传目录一定要放在jar包外面,否则每次重新部署文件就丢了。我当时就是这么处理的:jar包外建了一个upload目录,所有上传文件都存那里,部署时只需要把新jar包放进去重启,老文件不受影响。

6.5 Vue路由刷新404

本地开发时一切正常,打包部署到Nginx后发现刷新页面就404。原因是用createWebHistory路由模式时,前端路由是浏览器端的URL,服务器上并没有对应的物理路径,刷新时服务器找不到资源就返回404。

解决方案有两类。第一类最简单,路由改用createWebHashHistory模式,URL里会带个#,刷新不会再404,缺点是URL不好看。第二类是给Nginx加try_files兜底:

location / { try_files $uri $uri/ /index.html; }

我实际部署时用的是第二种,因为URL干净。如果你用Spring Boot内嵌Tomcat直接托管前端打包后的文件,也可以在资源映射里加一个index.html的forward规则,让所有非接口路径都指向index.html。这些小配置平时不起眼,但演示前没处理好的话,现场刷新一下页面就白屏,那种尴尬真的不想经历第二次。

6.6 数据库字段名与保留字冲突

建表时我给某张表起了order字段,结果查询时直接报SQL语法错误,排查了半天才发现是MySQL保留字惹的祸。后来我统一规定,所有表字段用下划线命名,并且尽量避开保留字,比如usersys_userordersort_nodescdescription。用MyBatis-Plus时,实体类的@TableField@TableName注解把映射关系写明,再加auto-result-map配置,基本能规避大部分隐藏问题。

6.7 跨域配置和端口冲突

前后端分离开发时,前端跑在5173端口,后端跑在8080端口,它们之间通信一定会有跨域。我在后端写了一个全局CORS配置类,允许所有来源访问开发接口。同时要注意,如果有其他服务占用了8080端口,启动会直接报“Port already in use”。排查占用端口的成本很小,但几乎每个学期都有同学被这个问题卡住,记一次lsof -i:8080(Windows就是netstat -ano | findstr 8080)就能解决。

7. 答辩不被问倒:这几个高频问题提前准备好

7.1 高频问题与回答思路

我整理了毕业设计答辩时关于这个项目最常被问的几类问题,这里给出参考回答思路:

问题一:你这个系统解决的核心业务痛点是什么?

回答思路:企业安全生产最怕“有隐患不整改、整改不彻底、复查无记录”。系统把隐患排查整治流程拉通,从发现登记到整改复查全流程线上化,每一步都有时间和操作人记录,形成“一患一档”的闭环档案,方便安全管理部门随时查看整改进度和排查

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询