简介这是一套面向高校计算机专业毕业设计与课程设计场景的完整项目源码基于SpringBoot框架开发采用B/S架构与MySQL数据库适合正在准备Java方向毕设或需要练手企业级管理系统的学生参考。项目围绕大型商场应急预案管理展开管理员端涵盖个人中心、员工管理、预案信息与预案类型管理、事件类型管理、预案类型统计、事件类型统计及应急预案管理等模块员工端可查看各类预案信息业务逻辑贴近真实商场安全管理需求具备一定实用性。压缩包共388个文件约32.08MB包含98个Java源文件、40个Vue组件、16个JS脚本、13个XML配置及SQL建表脚本、MP4演示视频与说明文档等前端页面、后端接口与数据库脚本齐全目录结构清晰。目前已有76人学习下载适合希望快速理解SpringBoot与Vue前后端分离开发流程、对照源码完成毕设搭建与功能扩展的读者。1. 大型商场应急预案管理系统从“纸面预案”到“可执行流程”的落地拆解商场这种人流密集场所最怕的不是没有预案而是预案锁在文件柜里、真出事时没人翻得动。我见过一个十万平的购物中心消防演练时值班经理拿着三十页 Word 打印稿满场跑对讲机里问“B 区扶梯困人第一步找谁”没人答得上来。这就是传统应急预案的典型翻车现场文档静态、流程割裂、责任人不明确、演练记录靠手写。基于 Springboot 的大型商场应急预案管理系统要解决的正是这件事——把预案结构化成可查询、可触发、可追踪的数据让“启动一级响应”变成系统里点一下就能派单的动作。这套东西适合谁做 Java 毕业设计、需要真实业务场景撑起技术栈的同学以及商场安全岗想自己搭一套轻量工具的一线人员。它不追求大而全的应急指挥平台核心是把预案、事件、资源、演练四块数据管起来用 Springboot 把 CRUD 和流程串明白。下面我按“数据怎么建模 → 接口怎么写 → 流程怎么跑 → 坑在哪”的顺序把可复现的路径讲清楚。2. 预案结构化建模把三十页文档拆成能查的表2.1 为什么不能直接存 Word 附件很多同学第一反应是“预案上传个文件出事下载看就行”。这个思路在演示阶段能跑通但业务上站不住。原因有三一是检索不到领导要查“所有涉及电梯困人的预案条款”附件里搜不了二是版本混乱商场每年修订预案附件覆盖后旧版本追溯不了三是无法和事件联动事件触发时系统不知道该调哪条预案、通知哪个岗位。所以核心设计是把预案拆成三层预案主表plan记录预案名称、类型、适用区域、版本号、生效状态预案步骤表plan_step记录每一步的序号、动作描述、责任岗位、时限要求、所需资源预案资源表plan_resource记录该预案关联的物资、设备、联系人。这样“启动预案”就变成按 plan_id 拉出有序步骤逐条派发。2.2 核心表结构与字段说明下面是我一般会用的建表 SQL字段做了精简但保留了业务关键项。注意plan_type用字典值区分消防、治安、设备故障、公共卫生等step_timeout单位是分钟用于后续超时告警。-- 预案主表 CREATE TABLE emergency_plan ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plan_name VARCHAR(128) NOT NULL COMMENT 预案名称, plan_type VARCHAR(32) NOT NULL COMMENT 预案类型: FIRE/SECURITY/DEVICE/HEALTH, area_scope VARCHAR(256) COMMENT 适用区域, 如 B1-B3 停车场, version VARCHAR(16) NOT NULL DEFAULT V1.0, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1生效 2停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT 应急预案主表; -- 预案步骤表 CREATE TABLE plan_step ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plan_id BIGINT NOT NULL, step_order INT NOT NULL COMMENT 步骤序号, 从1开始, action_desc VARCHAR(512) NOT NULL COMMENT 动作描述, duty_post VARCHAR(64) NOT NULL COMMENT 责任岗位, step_timeout INT DEFAULT 10 COMMENT 时限(分钟), resource_ids VARCHAR(256) COMMENT 关联资源ID, 逗号分隔, KEY idx_plan (plan_id, step_order) ) COMMENT 预案步骤表; -- 预案资源表 CREATE TABLE plan_resource ( id BIGINT PRIMARY KEY AUTO_INCREMENT, resource_name VARCHAR(128) NOT NULL, resource_type VARCHAR(32) COMMENT EQUIPMENT/MATERIAL/CONTACT, location VARCHAR(256), contact_phone VARCHAR(32), stock_num INT DEFAULT 0 ) COMMENT 应急资源表;逻辑说明三张表通过 plan_id 关联步骤表用step_order保证执行顺序resource_ids做轻量关联避免多对多中间表在毕设阶段过度设计。参数上step_timeout建议按商场实际演练数据填消防疏散类步骤一般 3-5 分钟设备抢修类可放宽到 15-30 分钟。status字段一定要有草稿态允许编辑生效态锁定这是版本管理的基础。2.3 用 Springboot 暴露预案查询接口建完表就是写接口。我一般用 MyBatis-Plus 减少样板代码Controller 层保持薄Service 层做业务组装。下面这个接口做的是“按类型和区域查生效预案并带出步骤列表”是事件触发时最先调用的。RestController RequestMapping(/api/plan) public class PlanController { Autowired private PlanService planService; /** * 按类型区域查询生效预案详情(含步骤) * param type 预案类型 FIRE/SECURITY/DEVICE/HEALTH * param area 区域关键字, 可为空 */ GetMapping(/detail) public ResultPlanDetailVO detail(RequestParam String type, RequestParam(required false) String area) { // 只查 status1 的生效预案, 草稿和停用不返回 PlanDetailVO vo planService.getActivePlan(type, area); if (vo null) { return Result.fail(未找到匹配的生效预案); } return Result.ok(vo); } }逻辑说明接口只做参数接收和结果包装真正的过滤逻辑在 Service。type必传是因为事件触发时一定知道事件类型area可选是为了兼容“全商场通用预案”。返回的PlanDetailVO里包含预案主信息和按step_order排好序的步骤列表前端拿到就能渲染成执行清单。参数校验上建议在 Service 里对 type 做白名单校验防止传入非法值导致查空。这里没写分页是因为预案详情本身数据量小但列表查询接口一定要加分页商场预案几十条不分页前端会卡。3. 事件触发与流程派发让预案真正“跑起来”3.1 事件表设计与状态机预案查出来只是第一步关键是事件发生时能自动关联预案并生成执行任务。事件表emergency_event要记录事件编号、类型、发生位置、上报人、发生时间、关联预案 ID、当前状态。状态机我一般设计成待确认 → 已启动 → 处理中 → 已完结 → 已归档。其中“已启动”这个动作会触发预案步骤的实例化——把 plan_step 复制成 event_task每条任务带上责任岗位和截止时间。这样做的价值是预案是模板事件任务是实例两者分离预案改版不影响历史事件记录。CREATE TABLE emergency_event ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_no VARCHAR(32) NOT NULL UNIQUE COMMENT 事件编号, event_type VARCHAR(32) NOT NULL, location VARCHAR(256) NOT NULL, reporter VARCHAR(64), occur_time DATETIME NOT NULL, plan_id BIGINT COMMENT 关联预案ID, status TINYINT DEFAULT 0 COMMENT 0待确认 1已启动 2处理中 3已完结 4已归档, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 应急事件表; CREATE TABLE event_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_id BIGINT NOT NULL, step_order INT NOT NULL, action_desc VARCHAR(512) NOT NULL, duty_post VARCHAR(64) NOT NULL, deadline DATETIME COMMENT 截止时间启动时间step_timeout, task_status TINYINT DEFAULT 0 COMMENT 0待执行 1执行中 2已完成 3已超时, finish_time DATETIME, KEY idx_event (event_id, step_order) ) COMMENT 事件执行任务表;参数说明event_no建议用“日期类型序号”生成比如 20240520-FIRE-001方便人工对讲机里报编号。deadline在启动时计算是超时告警的依据。task_status的“已超时”状态由定时任务扫描更新不是人工改的。3.2 启动预案的 Service 实现这是整个系统最核心的一段逻辑事件状态从“待确认”变“已启动”时根据 plan_id 拉出步骤批量生成 event_task并计算每条任务的截止时间。Service public class EventService { Autowired private EventMapper eventMapper; Autowired private PlanStepMapper planStepMapper; Autowired private EventTaskMapper eventTaskMapper; /** * 启动预案: 事件状态流转 任务实例化 * param eventId 事件ID */ Transactional(rollbackFor Exception.class) public void startPlan(Long eventId) { EmergencyEvent event eventMapper.selectById(eventId); if (event null || event.getPlanId() null) { throw new BizException(事件不存在或未关联预案); } if (event.getStatus() ! 0) { throw new BizException(仅待确认状态可启动预案); } // 1. 更新事件状态为已启动 event.setStatus(1); eventMapper.updateById(event); // 2. 拉取预案步骤, 按序号排序 ListPlanStep steps planStepMapper.selectByPlanId(event.getPlanId()); LocalDateTime startTime LocalDateTime.now(); // 3. 逐条生成任务, 计算截止时间 for (PlanStep step : steps) { EventTask task new EventTask(); task.setEventId(eventId); task.setStepOrder(step.getStepOrder()); task.setActionDesc(step.getActionDesc()); task.setDutyPost(step.getDutyPost()); // 截止时间 启动时间 步骤时限(分钟) task.setDeadline(startTime.plusMinutes(step.getStepTimeout())); task.setTaskStatus(0); eventTaskMapper.insert(task); } } }逻辑说明Transactional保证事件状态更新和任务生成要么全成功要么全回滚避免出现“事件已启动但任务没生成”的黑匣子状态。startTime统一取一次保证同一事件下所有任务的基准时间一致。参数上step.getStepTimeout()如果为空要有默认值兜底我一般设 10 分钟。这段代码的边界在于如果预案步骤为空会生成零条任务事件卡在“已启动”无法推进所以启动前要校验步骤数大于零。3.3 超时任务的定时扫描任务生成后超时判断不能靠前端轮询要用后端定时任务。Springboot 里用Scheduled最简单每分钟扫一次deadline now且状态为待执行或执行中的任务标记为超时并触发通知。Component public class TaskTimeoutJob { Autowired private EventTaskMapper eventTaskMapper; /** * 每分钟扫描一次超时任务 * cron: 每分钟第0秒执行 */ Scheduled(cron 0 * * * * ?) public void scanTimeout() { ListEventTask timeoutTasks eventTaskMapper.selectTimeoutTasks(LocalDateTime.now()); for (EventTask task : timeoutTasks) { task.setTaskStatus(3); // 3已超时 eventTaskMapper.updateById(task); // 此处可扩展: 推送站内信或短信给责任岗位 } } }逻辑说明cron 表达式0 * * * * ?表示每分钟执行一次商场场景下这个频率足够。selectTimeoutTasks的 SQL 条件是task_status IN (0,1) AND deadline #{now}只扫未完成的任务。参数上要注意时区问题服务器和数据库时区不一致会导致 deadline 比较错乱建议统一用Asia/Shanghai并在连接串里显式指定。扩展点在于通知渠道毕设阶段用站内信表记录即可不必真接短信网关。4. 演练记录与资源调度数据闭环怎么补全4.1 演练记录表与预案有效性验证预案写完不是终点得通过演练验证它是否可执行。演练记录表drill_record记录演练时间、参与预案、参与人数、实际耗时、发现问题。关键字段是actual_duration和issue_desc前者和预案步骤的step_timeout总和对比能看出预案时限设定是否合理后者是预案修订的输入。我一般会在演练结束后生成一份对比预案预计总时长 vs 实际总时长偏差超过 30% 就提示修订。CREATE TABLE drill_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plan_id BIGINT NOT NULL, drill_time DATETIME NOT NULL, join_num INT DEFAULT 0 COMMENT 参与人数, actual_duration INT COMMENT 实际耗时(分钟), issue_desc VARCHAR(1024) COMMENT 发现问题, score INT COMMENT 演练评分 0-100, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 应急演练记录表;参数说明score是可选字段如果商场有考核要求就填没有就留空。issue_desc建议前端做成多行文本鼓励填写具体问题而不是“整体良好”这种废话。这张表的数据积累起来后可以做“预案修订建议”的统计查询比如按 plan_id 聚合平均分和常见问题这是系统从工具变成决策辅助的关键。4.2 资源库存与调度记录应急资源不只是登记还要管“用了多少、还剩多少”。资源调度表resource_dispatch记录每次事件或演练中某资源的使用数量、使用时间、归还状态。这样事件结束后能自动扣减库存避免“灭火器登记了 50 个实际只剩 30 个”的账实不符。Service public class ResourceService { Autowired private ResourceMapper resourceMapper; Autowired private DispatchMapper dispatchMapper; /** * 资源出库: 校验库存 扣减 记录调度 * param resourceId 资源ID * param num 使用数量 * param eventId 关联事件ID */ Transactional(rollbackFor Exception.class) public void dispatch(Long resourceId, Integer num, Long eventId) { PlanResource resource resourceMapper.selectById(resourceId); if (resource null || resource.getStockNum() num) { throw new BizException(库存不足, 当前库存: (resource null ? 0 : resource.getStockNum())); } // 扣减库存 resource.setStockNum(resource.getStockNum() - num); resourceMapper.updateById(resource); // 记录调度 ResourceDispatch dispatch new ResourceDispatch(); dispatch.setResourceId(resourceId); dispatch.setEventId(eventId); dispatch.setUseNum(num); dispatch.setDispatchTime(LocalDateTime.now()); dispatch.setReturnStatus(0); // 0未归还 dispatchMapper.insert(dispatch); } }逻辑说明库存校验和扣减必须在同一个事务里否则并发下会出现超卖。return_status字段用于追踪归还事件完结时如果有未归还资源系统应提示。参数上num要做正整数校验防止传负数导致库存反增。这段代码的边界是并发场景毕设阶段用数据库行锁select ... for update就能兜住不必上分布式锁。4.3 用状态字段串起事件全生命周期把事件、任务、资源、演练四块数据串起来的主线是状态字段。事件从待确认到归档任务从待执行到完成或超时资源从出库到归还演练从计划到评分。我一般会在事件详情页做一个时间轴按时间倒序展示所有关联动作谁在什么时候启动了预案、生成了哪些任务、调用了哪些资源、任务完成情况如何。这个时间轴的数据来源就是各表的 create_time 和 update_time不需要额外建日志表。对商场安全岗来说这个时间轴就是事后复盘的全部依据比翻纸质记录快得多。5. 避坑与排查这套系统最容易翻车的五个地方5.1 预案步骤顺序错乱现象事件启动后任务列表里“疏散人群”排在“报警”前面执行顺序完全反了。原因plan_step表查询时没加ORDER BY step_orderMySQL 默认按主键或存储顺序返回插入顺序和业务顺序不一致时就乱。解决所有查步骤的 SQL 必须显式ORDER BY step_order ASC并且在 Service 层生成任务时再排一次序做双保险。我一般会在 Mapper 方法名上就体现比如selectByPlanIdOrderByStep让后来人一眼看到。5.2 事件状态并发修改现象两个值班员同时点“启动预案”系统生成了两套任务事件状态被改了两次。原因startPlan方法里先查状态再更新查和更新之间没有锁并发下都通过了状态校验。解决在更新事件状态时加条件更新UPDATE emergency_event SET status1 WHERE id? AND status0根据影响行数判断是否抢到启动权影响行数为 0 就抛异常。这是乐观锁的轻量实现比synchronized更适合多实例部署。5.3 超时任务重复标记现象定时任务每分钟扫一次同一条超时任务被反复更新日志里刷了几百条。原因扫描条件只判断了deadline now没排除已经是超时状态的任务。解决SQL 条件加上task_status IN (0,1)只扫未完成的。另外可以在更新时加AND task_status ! 3做二次防护。这个坑很典型本质是状态机没闭环任何状态流转都要想清楚“从哪些状态来、到哪些状态去”。5.4 资源库存扣成负数现象演练时多人同时领用同一批物资库存显示 -5。原因库存校验和扣减分了两步中间没有事务和锁。解决把校验和扣减放进同一个Transactional方法并且用UPDATE plan_resource SET stock_num stock_num - ? WHERE id ? AND stock_num ?这种带条件的原子更新影响行数为 0 就说明库存不够。这样即使并发也不会扣穿。5.5 预案版本追溯断档现象商场修订了消防预案但上个月的事件记录里关联的步骤描述也跟着变了复盘时对不上。原因事件任务表存的是 plan_step 的引用还是快照没想清楚。解决event_task表里冗余存action_desc和duty_post的快照不要只存 step_id 去关联查。预案改版只影响新事件历史事件的任务描述保持原样。这个设计决策要在建表时就定后期改代价很大。6. 进阶技巧用预案覆盖率统计反推管理漏洞系统跑起来之后最有价值的不是单个事件的处理记录而是聚合统计。我一般会加一个“预案覆盖率”看板核心指标有三个一是区域覆盖率商场所有楼层和重点部位中有多少个区域有生效预案关联二是类型覆盖率消防、治安、设备、公共卫生四类事件中每类有多少条生效预案三是演练覆盖率过去 12 个月内每个预案被演练过几次。这三个指标能直接暴露管理漏洞——比如 B2 停车场没有设备故障预案或者某条预案两年没演练过。实现上不复杂用几条聚合 SQL 就能出数。下面这条查的是“有事件发生但无预案关联”的区域这是最危险的盲区。-- 查出近一年发生过事件但当前无生效预案覆盖的区域 SELECT e.location, COUNT(*) AS event_count FROM emergency_event e WHERE e.occur_time DATE_SUB(NOW(), INTERVAL 1 YEAR) AND NOT EXISTS ( SELECT 1 FROM emergency_plan p WHERE p.status 1 AND p.area_scope LIKE CONCAT(%, e.location, %) ) GROUP BY e.location ORDER BY event_count DESC;逻辑说明NOT EXISTS子查询判断该区域是否有生效预案覆盖LIKE做模糊匹配是因为area_scope存的是范围描述。参数上时间窗口设一年是平衡数据量和参考价值太短看不出规律太长数据陈旧。这条查询的结果应该定期推送给安全负责人作为预案修订的优先级依据。另一个进阶点是任务执行的时效分析。把event_task的finish_time和deadline对比能算出每个岗位的平均响应时长和超时率。我一般按duty_post分组统计超时率高的岗位要么是人员配置不足要么是流程设计不合理。这个数据比任何主观评价都有说服力。-- 按岗位统计任务超时率 SELECT duty_post, COUNT(*) AS total, SUM(CASE WHEN task_status 3 THEN 1 ELSE 0 END) AS timeout_num, ROUND(SUM(CASE WHEN task_status 3 THEN 1 ELSE 0 END) / COUNT(*) * 100, 1) AS timeout_rate FROM event_task GROUP BY duty_post HAVING total 5 ORDER BY timeout_rate DESC;HAVING total 5是为了过滤样本太少的岗位避免一两次偶然超时被放大。这个查询建议做成月度报表连续三个月超时率上升的岗位就要约谈或调整流程。最后说个我自己的习惯这套系统做完后别急着加功能先拿商场最近一次真实演练的数据跑一遍全流程——从事件录入、预案启动、任务派发到资源调度、演练评分走通一遍比写十个接口都有用。我踩过的最大坑就是功能都写了但流程串不起来数据在每张表里都好看连起来就是断的。希望帮到你。本文还有配套的精品资源点击获取
