每年开题季,总有同学拿着“基于SpringBoot的XX管理系统”这个模板来问我,说到底选什么题目才能既好写、又不被答辩老师一句“你这和课程设计有什么区别”给问住。我今天想认真聊的,是一套基于SpringBoot的智慧生产安全巡检系统——后端用SpringBoot,数据落到MySQL,业务上带二维码扫码打卡、隐患上报整改、台账统计,最终把源码、数据库脚本、调试记录、文档整理成一套直接能跑起来、能讲清楚逻辑的完整交付物。如果你正卡在选题阶段,或者已经选了安全巡检、隐患排查、设备点检这类方向但不知道往哪儿深入,这篇文章基本能帮你把整个项目的骨架、核心代码思路和答辩时可能踩的雷一次说透。
1. 为什么选“安全巡检系统”当毕设:比普通管理系统多出的三层价值
1.1 业务上有天然闭环,答辩好讲
大部分学生毕设喜欢做“员工管理”“商品管理”“班级管理”,这类题目的通病是:功能只有增删改查,业务上没有一条完整的逻辑链,答辩时老师问“你这个系统解决了什么实际问题”,答案很容易变成“就是把纸质记录电子化”。
安全巡检系统的天然优势在于,它自带一个完整的管理闭环:计划制定 → 任务生成 → 现场巡检 → 隐患上报 → 整改复查 → 销号归档 → 统计报表。你不需要刻意编造业务亮点,只要把这条链路跑通,系统本身就已经是一个“能解决实际问题”的作品。答辩的时候顺着这条链路讲,老师很容易跟上你的思路,提问也会集中在业务和技术细节上,而不是质疑题目有没有意义。
1.2 技术点覆盖刚好卡在本科毕设的“满分区间”
我见过很多同学为了显得厉害,一上来就上微服务、Redis、MQ、分布式锁,结果毕设周期一大半耗在环境搭建和踩坑上,最后项目跑不起来。安全巡检系统这个题目很聪明的地方在于:它用单体的SpringBoot就能实现绝大部分功能,但又能自然引出足够多的技术话题。
比如:
- 巡检任务定时生成,可以用
Spring Schedule或Quartz,这就涉及定时任务、cron表达式、任务幂等; - 扫码巡检记录位置,可以用
zxing生成二维码,再写一个带定位或者拍照水印的移动端H5页面,这就涉及工具集成和移动端适配; - 隐患整改的状态流转,可以用状态机建模,配合
MyBatis-Plus的乐观锁防重复提交; - 报表统计可视化,用
ECharts把隐患台账按区域、按类型、按时间维度展示。
这些技术点没有一个是“为了用而用”,全都长在业务上。答辩老师听完你的功能演示,再翻一翻核心代码,很容易给出“工作量充实、逻辑完整”的评价。
1.3 题目怎么包装才不算“炒冷饭”
“安全巡检”这类题目每年都有人做,但很多同学做出来的东西就是一个普通CRUD换了个皮,所以题目包装很重要。标题里提到的“智慧生产安全系统”,实际是一个更大的平台概念,安全巡检系统是其中承上启下的核心子模块。我在和学弟确认需求的时候,给他的系统定位是:
- 基础层:统一用户 / 角色 / 权限(RBAC);
- 业务层:巡检点管理、巡检计划、任务生成、巡检执行、隐患登记;
- 闭环层:隐患整改、复查销号、超期预警;
- 展示层:大屏统计、区域隐患排名、巡检完成率趋势。
这样设计之后,你答辩介绍系统架构时,画出来的图是一张分层清晰的平台架构图,而不是一张“用户表、订单表、商品表”的课程设计图。同样是“SpringBoot + MySQL”的选题,讲出来的格局完全不一样。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从任务生成到隐患销号:核心功能模块怎么拆
2.1 巡检计划与任务下发
安全巡检的第一步,是把“什么时候巡检、巡哪里、查什么、谁去查”这些规则定下来。我的建议是不要做成简单的“手动创建任务”,而是把“计划”和“任务”拆成两张表。
- 巡检计划表:配置巡检区域、巡检点范围、巡检周期(每日/每周/每月)、执行时间、负责人、检查项模板;
- 巡检任务表:某一天实际生成的具体任务,包含计划ID、计划执行日期、指派的巡检人、任务状态(待巡检/已完成/已逾期)。
这样设计的好处,是系统可以每天定时扫描计划表,自动为“今天该巡检”的计划生成对应的巡检任务。举个例子:某车间配置了“每工作日上午9点巡检设备区”,公共服务类模块在每天早上的调度任务里,查一下计划表里周期是“每日”且执行时间等于当前时间段的计划,查出巡检点和检查项,批量生成当天的一条巡检任务。
我实际写的时候用的是Spring自带的 @Scheduled,因为它对单体项目来说最简单,不需要额外引入中间件:
java复制@Component
public class PatrolTaskGenerateJob {
private final PatrolPlanMapper patrolPlanMapper;
private final PatrolTaskService patrolTaskService;
@Scheduled(cron = "0 0 8 * * ?")
public void generateDailyTasks() {
List<PatrolPlan> plans = patrolPlanMapper.selectDailyPlans(LocalDate.now());
for (PatrolPlan plan : plans) {
patrolTaskService.generateTaskByPlan(plan, LocalDate.now());
}
}
}
这里有几个细节要提前想好:一是“防止重复生成”,调度任务必须加幂等判断,生成任务前先查一下当天是否已经有该计划产生的任务;二是“周期类型”,不要把周期做成死枚举,建议用字典表维护,后续扩展“双班制”“节假日顺延”都容易。
2.2 现场打卡、检查项填报、隐患上报
这是整个系统最接近“智慧”二字的模块。我先讲最简版本,再讲加分项。
最简版本是:每个巡检点生成一个唯一编码,系统用 zxing 生成二维码图片,打印后贴在设备或区域现场。巡检员在手机端打开H5页面,扫码后进入巡检点详情,看到该巡检点对应的检查项列表,逐项选择“正常/异常”,异常项必须填写描述并拍照上传,提交后自动生成隐患台账。
加分项我在后文会详细展开,包括:
- 扫码时获取地理位置,或者调用手机摄像头拍一张带水印的现场照片,防止“人没到现场、扫码打卡”;
- 二维码里面不直接放巡检点ID,而是放一个短码(如
PD-2024-0001),服务端再做一次映射,以防止别人随便改ID。
核心的二维码信息解析逻辑大概是:
java复制@GetMapping("/patrol/scan")
public Result scanPatrolPoint(@RequestParam("code") String code) {
PatrolPoint point = patrolPointMapper.selectByPointCode(code);
if (point == null) {
return Result.fail("巡检点不存在或二维码无效");
}
// 返回该巡检点绑定的检查项清单
List<PatrolItem> items = patrolItemMapper.selectByPointId(point.getId());
return Result.ok(items);
}
巡检员提交的时候,后端要一次性判断两件事:一是检查项是否都填报了,二是所有异常项是否都填了隐患描述。这两条校验逻辑写到Service层做事务控制,不能散落在Controller里。
2.3 隐患整改流程与台账统计
隐患一旦上报,就进入整改流程。我的状态机设计是:
text复制待整改 → 整改中 → 待复查 → 已销号
↘ 已驳回 ↙
也就是隐患上报后默认“待整改”,整改人接单后改成“整改中”,完成整改提交复查申请后进入“待复查”,复查人现场确认没问题后“销号”;如果复查不通过,驳回重新整改。
这个流程的好处是,每个状态变更都对应了一个操作人和操作时间,答辩老师说“把你的权限设计讲一下”的时候,你可以很自然地说:隐患流程里不同角色天然对应不同操作,巡检员只有上报权限,整改人只有处理自己名下隐患的权限,复查人只能看到“待复查”状态的隐患。
台账统计这块,推荐用ECharts做四个基础图:
- 隐患类型分布饼图;
- 各区域隐患数量柱状图;
- 近30天新增/整改趋势折线图;
- 责任部门隐患整改进度条。
统计SQL如果直接用 select * 再在Java里分组,数据量一大就会很慢。建议用聚合查询,比如:
sql复制SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day,
COUNT(*) AS total
FROM hidden_danger
WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d')
3. 数据库表设计里的关键取舍
3.1 权限部分直接复用成熟RBAC五表
很多同学一上来就自己设计一张用户表,里面放 role 字段,然后用 if ("admin".equals(role)) 去判断权限。这种设计方案在小demo里没问题,但安全隐患很大,答辩也容易被追问到“多角色、多权限怎么办”。
成熟的做法是经典RBAC模型,一共五张表:
| 表名 | 作用 |
|---|---|
| sys_user | 用户表,存账号、密码、昵称、部门 |
| sys_role | 角色表,如巡检员、整改责任人、安全管理员、系统管理员 |
| sys_user_role | 用户-角色关联表 |
| sys_menu | 菜单/权限表,存页面按钮和接口编码 |
| sys_role_menu | 角色-权限关联表 |
对应到业务里就是:一个安全管理员可以查看全部巡检任务和隐患台账;一个巡检员只能看到自己的任务、上报隐患;一个整改责任人只能看到分配给自己的整改单。权限数据用Spring Security + JWT来控制,接口上写 @PreAuthorize("hasAuthority('patrol:task:list')") 就行。
3.2 巡检域的主表与明细表如何拆分
巡检业务里最容易犯的错,是把“一次巡检”当成一条记录,所有检查项结果塞进一个 remark 字段里。正确做法是拆主表和明细表:
- patrol_task:一次巡检任务,记录巡检人、巡检点、计划日期、状态;
- patrol_task_item:本次任务对应的检查项清单,每条记录关联一个检查项和巡检结果。
为什么必须拆?因为以后要统计“哪个检查项异常率最高”,或者“某厂区上周所有设备电机温度检查结果”,如果结果都堆在一个文本字段里,SQL根本没法统计。拆表之后,统计就是一次简单的 group by。
巡检点单独建一张表 patrol_point,字段包括:点位名称、所属区域、设备编号、二维码编码、GPS经纬度、启用状态。检查项模板也独立出来,这样计划或者任务只存检查项ID列表,而不是复制检查项文本,避免一个检查项改名后所有历史任务跟着乱。
3.3 隐患表的状态机设计
隐患表 hidden_danger 除了基础字段(上报人、巡检任务ID、巡检点ID、隐患类型、隐患描述、现场照片URL)之外,必须加几个关键字段:
status:当前状态,对应上面说的“待整改/整改中/待复查/已销号/已驳回”;assignee_id:整改责任人;deadline:整改截止时间;version:乐观锁版本号,防止两个人同时打开一条隐患、同时提交不同的整改结果。
这里特别说一下 version 字段。整改人员在手机上接单,往往会把页面挂很久,另一台电脑上管理员可能已经把这单指派给别人了。如果不做乐观锁,后提交的人直接覆盖前一个人的操作,数据就乱了。
在代码里的体现是:
java复制@Update("UPDATE hidden_danger SET status = #{newStatus}, " +
"assignee_id = #{assigneeId}, version = version + 1 " +
"WHERE id = #{id} AND version = #{version}")
int updateWithVersion(HiddenDanger danger);
更新返回0说明version不匹配,直接提示“数据已被他人修改,请刷新”。
4. SpringBoot落地细节:定时任务、扫码、图片上传与权限
4.1 定时任务生成巡检任务:别用cron硬编码
我见过不少项目把所有定时逻辑写死成 0 0 8 * * ?,每天固定八点生成任务,然后就没有然后了。这样做的结果是:一旦生产环境想改成每天两次巡检,或者某个车间周五不巡检,就得改代码重启。
我的做法是:定时任务只负责“触发”,具体生成哪些计划的任务由 PatrolPlanMapper 根据参数查出来。比如:
java复制@Scheduled(cron = "${patrol.job.cron:0 0 8 * * ?}")
public void generateTask() {
List<PatrolPlan> plans = patrolPlanService.findPlansDueToday(LocalDate.now());
for (PatrolPlan plan : plans) {
patrolTaskService.generateTaskIfAbsent(plan, LocalDate.now());
}
}
这样cron表达式放进配置文件,计划周期靠表里的字段控制,哪天想加一个“每两小时巡检一次”的规则,只需要在计划表里加一条记录,不用动代码。
4.2 扫码打卡:二维码绑定巡检点而不是直接暴露ID
二维码内容如果直接用巡检点的自增ID,一旦被懂技术的用户猜到规律,扫个 ?id=3 就能提交别人的巡检任务,这个漏洞在答辩演示时如果被老师点出来,是很致命的。
我在项目里给每个巡检点生成一个独立的 point_code,规则是“区域编码+序号”,比如 AREA01-P0001。二维码内容就是这个编码,服务端根据编码查巡检点。同时二维码图片用zxing生成并保存到本地目录,系统里提供“批量导出二维码”功能,用户在管理端勾选几个巡检点,一次性打包下载所有二维码图片,方便打印张贴。
4.3 安全与文件上传的边界问题
文件上传必踩的坑有两个,我在开发文档里特意写了备注:
第一个是SpringBoot默认上传大小限制,老版本是1MB,新版虽然高一些但也不够传现场照片。要在配置文件里显式放开:
yaml复制spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB
第二个是文件存储路径不能写死在代码里。很多人写项目喜欢 File("D:/upload"),换一台电脑就报错。正确做法是把上传根目录配到 application.yml,然后用配置类封装一个路径常量。更稳妥的方案是:数据库里只存 /upload/2024/06/xx.jpg 这样的相对路径,本地通过自定义静态资源映射把它映射到磁盘目录:
yaml复制spring:
web:
resources:
static-locations: file:${upload.dir}/
这样打包部署后,只需要在启动命令里指定 upload.dir,或者运维改一下配置,图片就能正常访问。
4.4 Spring Security + JWT的“够用就好”策略
本科毕设里,我不建议上OAuth2、Spring Security的完整过滤器链配置到变态复杂的程度,够用就好。我的策略是:
- 登录接口放行,其他接口统一走JWT认证;
- 所有接口默认拒绝,显式配置放行路径(比如登录、swagger文档);
- 权限控制分两层:接口URL级别用
@PreAuthorize控制角色,按钮级别用菜单权限列表控制前端显隐; - Redis不是必须的,JWT的无状态特性本来就不依赖Redis。如果非要加“退出登录失效token”的功能,再引入Redis也算合理加项。
这么做的好处是代码量不大,但能讲清楚“认证”和“授权”两个概念,答辩时候老师问“你这个系统怎么做权限控制”,你可以从JWT的签发、拦截器校验、注解鉴权一路讲下去,非常流畅。
5. 开发调试期间踩过的坑(都给你记好了)
5.1 日期边界导致当日任务漏检
第一次做“今日任务”查询时,我直接用 where task_date = #{today},结果一切正常。后来测试的同学反馈:每天晚上11点多跑的巡检记录,第二天早上一查就不见了。
排了半天才发现是数据类型问题。MySQL的 DATE 字段传值时,如果用 Date 类型的 today 变量,Java侧的 Date 其实带时间,比如 2024-06-01 23:40:00,数据库比较 task_date = '2024-06-01 23:40:00' 必然匹配不上 2024-06-01。
排查链路是这样的:
- 先确认控制台打印的SQL,发现参数值后面带了时分秒;
- 再确认数据库字段类型是
date不是datetime; - 最后把查询条件改为
task_date >= ? AND task_date < DATE_ADD(?, INTERVAL 1 DAY),或者直接用LocalDate.now()传参。
这里我想多说一句:调试Bug的时候,第一步永远是看控制台打印的SQL,而不是猜代码逻辑。我见过太多同学对着Service层看半天,最后发现SQL根本没按预想执行。把SQL日志打开,能省掉一半排查时间。
5.2 并发改造同一隐患的重复提交
隐患整改这个场景,必须考虑并发。测试组当时开了两个浏览器窗口,同时打开同一条“待整改”隐患,一个提交“已整改”一个提交“复查通过”,结果后提交的人直接把前面的状态覆盖了。
修复方案就是我前面提到的乐观锁。这里补充一个细节:升级乐观锁之后,MyBatis-Plus 的 updateById 默认不会自动携带 version 字段,必须在实体类上加上 @Version 注解,并且配置乐观锁拦截器,否则version字段根本不会生效。
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
如果你用的不是MyBatis-Plus,而是原生MyBatis,那就按我前面写的手动 where version = #{version} 的方式做。两种方案效果一样,核心是让并发提交的同一条记录只有一个能成功更新。
5.3 打包部署后文件路径失效
这个坑太经典了。开发环境跑得好好的,图片上传到 D:/upload 正常显示,一打包成jar放到服务器上,控制台直接报“系统找不到指定的路径”。
原因很简单:Windows环境下 D:/upload 存在;Linux服务器上这个路径不存在,也没有权限创建。而且jar包内部路径是只读的,所以绝对不能把上传目录放到jar包同级的相对路径下面,试图通过 new File("upload/test.jpg") 访问。
我的最终方案是:
- 配置文件里加一个
upload.dir,生产环境设置为/home/app/upload; - 上传时先判断目录是否存在,不存在就
Files.createDirectories; - 静态资源映射到该目录;
- 启动脚本里用
--upload.dir=/home/app/upload --server.port=8080传参。
这样开发、测试、部署三种环境都用同一种逻辑,只是路径不同,不会出现“本地能用线上不能”的灵异事件。
6. 交付物整理与答辩准备:源码、MySQL脚本和文档怎么拿得出手
6.1 源码结构和数据库脚本的规范
一个让答辩老师印象好的项目,往往从“拿到压缩包后的第一印象”就赢了。我整理交付物时有一条硬规矩:任何人拿到源码,按README操作,15分钟内必须能把系统跑起来。
具体做了这么几件事:
- 源码模块按
controller / service / mapper / entity / config分包,禁止把几百行代码堆在一个类里; - SQL脚本用
init.sql+data.sql分开,init.sql建库建表,data.sql导入基础数据(管理员账号、演示角色、巡检点、检查项模板); - 每个业务的Mapper接口都写一行注释说明用途,比如
PatrolTaskItemMapper.java:巡检任务明细查询,异常项统计专用; - README里写清楚:JDK版本、MySQL版本、启动步骤、默认账号密码、演示流程。
数据库脚本这块我特别想强调:一定不要只导出一个空的表结构。答辩老师问“系统能跑吗”,你现场演示用的登录账号、巡检点数据、历史隐患数据,都得在 data.sql 里。没有演示数据的项目,演示效果差一大截。
6.2 演示数据与答辩演示流程
我建议准备一套“剧本式”的演示数据和演示流程,而不是现场临时造数据:
- 用管理员账号登录,打开“巡检计划列表”,展示今天自动生成的三条巡检任务;
- 切到巡检员账号,手机端H5扫码打开某个巡检点,逐项填报,其中一个检查项故意选异常,上传一张测试图片,提交后生成隐患;
- 切到管理员视角,打开“隐患台账”,能看到刚才的隐患状态是“待整改”,指派整改人;
- 切到整改人账号,接单、提交整改说明;
- 再回到管理员账号进行复查销号;
- 最后打开“统计报表”,展示刚才这条链路在趋势图上的变化。
这套流程演示完,不到五分钟,但把系统所有核心功能全部过了一遍。答辩老师看下来,对项目工作量的认可度会非常高。
6.3 常见答辩追问和应答思路
最后整理几个这个项目几乎必被问到的问题,每一题我都给了应答角度:
-
问:你这个系统相比传统纸质巡检,优势到底在哪儿?
答:一是任务自动化生成,减少了人为漏排;二是巡检记录可追溯,每个点位每次巡检的检查项数据都在库里;三是隐患流程闭环,从发现到销号全程留痕。 -
问:SpringBoot相比传统SSM好处是什么?
答:自动配置减少了大量xml配置;内置Tomcat可以独立运行;生态成熟,集成MyBatis-Plus、Spring Security都很简单;开发效率高,毕设周期短但功能完整。 -
问:如果巡检员不到现场,远程扫码怎么办?
答:二维码可以绑定固定点位,但我也预留了两种防作弊方式:一是扫码时获取GPS定位,和巡检点预设的位置做距离校验;二是拍照必须调用摄像头现场拍摄,并在照片上叠加时间水印和地点。可以做,但作为扩展点,因为要考虑部分车间无GPS信号的情况。 -
问:数据量大了之后,统计报表会变慢怎么办?
答:目前基于MySQL加索引已经能满足毕业设计场景。如果数据量真的很大,可以给巡检记录表按月分表,或者把统计数据通过定时任务提前汇总到统计表中,查询直接从统计表读取,不走原始明细表。 -
问:为什么用MySQL不用Oracle/PostgreSQL?
答:MySQL轻量、开源、资料多,对这个体量的系统完全够用。5.7或8.0版本的索引能力、JSON支持、窗口函数已能满足需求。选型一定是够用就好,不是为了秀技术而增加复杂度。
这套系统从选题、表结构、后端逻辑到演示流程,完整做下来大概需要三到四周的密集开发时间。大部分时间其实花在“业务闭环的细节打磨”上,而不是代码本身。如果你已经决定做类似题目,我给你的建议是先花两天把业务闭环和表结构理清楚,再开始写代码。这个顺序一旦搞反,后面八成要推倒重来。
