简介:这是一套基于Java与Shell的企业危险化学品双重预防机制数字化管理系统源码,面向Java开发者和企业安全管理人员,用于解决危险化学品库存、监测及隐患排查的数字化管控问题。资源包共504个文件、总大小2.19MB,以416个Java源文件为主体,覆盖业务逻辑与数据模型,另有44个XML配置文件、26个VM模板、4个YML配置以及SQL、BAT、Shell等脚本,支撑环境配置、构建部署与数据库初始化,并附使用手册与许可证文件。已有338人学习下载,适合作为企业级管理系统的设计参考。开发者可从中理解危化品双重预防机制的业务建模与风险监测流程,学习若依框架下的模块化开发方式,参考多环境参数配置、批处理运维脚本以及数据库脚本的组织方法,为同类安全生产数字化系统的二次开发提供完整代码基础。
1. 双重预防机制数字化,为什么 Java + Shell 是危化场景最搭的组合
危化品企业的双重预防机制说白了就是“风险分级管控 + 隐患排查治理”:先把罐区、仓库、装卸台这些风险点按 LEC 法或风险矩阵分出红橙黄蓝,再把巡检发现的隐患一条条走完整改、验收、闭环。业务逻辑用 Java 生态做,部署、巡检、定时任务交给 Shell,是我在化工企业落这类系统时最顺手的分工。这篇笔记不贴大而全的项目介绍,直接讲数据库怎么建模、Shell 脚本怎么写、Java 状态机怎么落地,以及最容易翻车的五个细节。适合正在做危化品双防数字化或准备改造现有源码的后端和运维同学参考。
2. 双重预防机制的业务数据模型:风险分级和隐患闭环如何落库
普通的企业管理系统可以先画页面再补数据表,双重预防机制反而不行。原因是这套业务的合规属性很强,风险等级、隐患排查这些数据要能回溯、能审计。建模阶段如果把“风险点”和“隐患”两张表做成各自独立,后面做风险联动、超期提醒、月度统计都会很别扭。我的习惯是先画状态流转图再画 ER 图:把两条主线的状态都画清楚,再定主表、子表和关联关系。
2.1 LEC 风险矩阵:三张表把风险分级管控制度化
先澄清 LEC 法和风险矩阵不是一回事。LEC 用 D = L × E × C 三个因子算分,L 是事故发生的可能性,E 是人员在危险环境的暴露频次,C 是后果严重度,D 超过 320 定红色重大风险,160~320 定橙色较大风险,70~160 定黄色一般风险,低于 70 定蓝色低风险。风险矩阵则是在发生可能性和后果严重度二维表上查交点。多套方法并存,表结构就要把每次评估过程留下来,不能只存一个等级结果,否则下次复评时看不到打分依据,审计也说不清楚。
第一张主表是危化品档案表,存产品名称、CAS 号、MSDS 编号、储存方式、最大储量、重大危险源临界量级别。第二张主表是风险点表,一个危化品档案下挂多个风险点(比如一个液氨储罐区和一个装卸区),记录位置、作业类型、当前等级和复核截止日期。第三张是历次评估记录表,把每次评估使用的方法、三个因子的值、计算总分、评估人和评估时间都存下来。
CREATE TABLE dpm_risk_point ( id BIGINT AUTO_INCREMENT PRIMARY KEY, hazard_id BIGINT NOT NULL COMMENT '关联危化品档案表 dpm_hazard', point_name VARCHAR(64) NOT NULL, location_desc VARCHAR(255) NOT NULL COMMENT '例如:液氨储罐区B-07号罐', current_level VARCHAR(8) NOT NULL COMMENT 'RED/ORANGE/YELLOW/BLUE', evaluate_date DATE NOT NULL, expiry_date DATE NOT NULL COMMENT '风险复评截止日期', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_hazard (hazard_id), KEY idx_expiry (expiry_date), KEY idx_level (current_level) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='风险点主表'; CREATE TABLE dpm_risk_assessment ( id BIGINT AUTO_INCREMENT PRIMARY KEY, risk_point_id BIGINT NOT NULL, assess_method VARCHAR(10) NOT NULL COMMENT 'LEC / MATRIX / JHA', factor_l DECIMAL(4,1) COMMENT 'LEC 的 L 值,可取 0.5~10', factor_e DECIMAL(4,1) COMMENT 'LEC 的 E 值', factor_c DECIMAL(4,1) COMMENT 'LEC 的 C 值', score_total DECIMAL(8,1) NOT NULL COMMENT 'D 值', risk_level VARCHAR(8) NOT NULL, assess_user VARCHAR(32) NOT NULL, assess_time DATETIME NOT NULL, remark VARCHAR(255), KEY idx_point (risk_point_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='历次风险评分记录';这里的两个设计选择值得展开。第一个是 current_level 做了冗余且加了索引。风险点等级会被大屏、统计报表、权限策略频繁过滤,每次从评估记录里取最新一条开销大;但冗余字段要求 Java 服务在保存评估记录时,必须用同一个事务同步更新 dpm_risk_point.current_level。要用 @Transactional 把两个写操作包起来,不然会出现“评估记录写了、列表等级没变”这种让人摸不着头脑的数据不一致。第二个是 expiry_date 加索引,因为每天的 Shell 定时任务都会扫描这个字段,拿当前日期去匹配未来 30 天内到期的风险点,没有索引,数据量过万后查询就会拖垮业务库。
2.2 隐患工单状态机:从发现到闭环的状态流转设计
隐患排查治理这条线,核心是“发现—整改—验收—闭环”的闭环。很多初版系统把隐患状态设计得过粗,只有“未整改 / 已整改”,导致验收驳回、超期变更、重新整改这些动作无处安放。我建议把状态机至少拆成五态:待指派、整改中、待验收、已闭环、验收驳回,再加一张状态流转记录表把每一步谁做的、什么时间、流转依据都留下来。这既满足业务日常操作,也方便后面做数据一致性校验。
CREATE TABLE dpm_hazard_rectify ( id BIGINT AUTO_INCREMENT PRIMARY KEY, risk_point_id BIGINT NOT NULL, source_type VARCHAR(20) NOT NULL COMMENT '日常检查/综合检查/专项检查/上级督查', hazard_desc VARCHAR(500) NOT NULL, discover_user VARCHAR(32) NOT NULL, discover_time DATETIME NOT NULL, rectify_user VARCHAR(32) COMMENT '整改责任人,待指派时为空', rectify_deadline DATETIME COMMENT '整改完成截止时间', rectify_content VARCHAR(1000) COMMENT '整改过程说明,验收时必填', rectify_time DATETIME, verify_user VARCHAR(32) COMMENT '验收人', verify_comment VARCHAR(255), verify_time DATETIME, rectify_status TINYINT NOT NULL DEFAULT 0 COMMENT '0待指派/1整改中/2待验收/3已闭环/4验收驳回', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_point (risk_point_id), KEY idx_status (rectify_status), KEY idx_deadline (rectify_deadline) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='隐患整改主表'; CREATE TABLE dpm_rectify_flow ( id BIGINT AUTO_INCREMENT PRIMARY KEY, rectify_id BIGINT NOT NULL, from_state TINYINT NOT NULL, to_state TINYINT NOT NULL, oper_user VARCHAR(32) NOT NULL, oper_time DATETIME NOT NULL, remark VARCHAR(255), KEY idx_rectify (rectify_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='状态流转记录';状态流转记录表是典型的审计数据,只写不删。业务上最大的价值是月底统计整改时长时,不需要去主表里猜状态切换时间点,直接从 dpm_rectify_flow 里取某条工单从“整改中”到“待验收”的首跳时间差。还有一个细节:主表不要把所有历史状态都覆盖保存,当前状态永远只存一个值,历史全部流向流转表。这样 SQL 简单、索引可控,也避免了靠 JSON 数组管理状态机的那种玄学设计。
2.3 危化品特征字段与数据一致性:避免“改了一处漏三处”
危化品场景和普通机械制造企业的差异,集中体现在几个特征字段上:MSDS 文件版本、GHS 危险性分类、储存临界量、安全标签有效期。这些字段和风险等级、复评周期是联动的,不是单纯的信息登记。比如储罐的最大储量原本是 25 吨,改扩建后变成 60 吨,跨过重大危险源临界量,风险等级很可能从黄色直接跳到红色,复评周期也要从 12 个月压缩到 3 个月。如果把储量只做一个普通字段,那无论业务怎么改,系统里都不会有任何反应,这才是双重预防机制数字化最容易失守的地方。
我的做法是把评审业务规则写在 Java Service 层:危化品档案的储量、临界量、储存方式任一项变更,必触发一次重新评估,生成一条新的风险评分记录并按结果刷新风险点等级。同时把安全标签的到期提醒字段放到危化品档案表,新增、变更时统一计算到期提醒日期,Shell 侧定时任务到点推送。数据一致性的保障就一句话:同一业务动作的所有写操作,要么全成功要么全回滚,用 MySQL 事务加 Java @Transactional 兜底,不靠人为调整顺序。
3. 用 Shell 把部署、巡检与定时任务一键化
Java 后端做的是业务规则,Shell 干的是“脏活”:初始化建库、启停服务、清理日志、跑定时任务。危化品企业的生产网环境一般不开放外网,几台 Linux 服务器就是全部家当,这种条件下用 Shell 脚本反而比高上大的容器编排更贴地气。脚本不用多,三类就能覆盖绝大多数场景:环境初始化、服务启停、定时任务。
3.1 环境初始化脚本:建库建表灌数一条命令跑完
初始化脚本最怕的就是不可重复执行。我见过同事写的初始化脚本,第一次跑失败后数据库里建了半张表,第二次跑直接报“表已存在”退出。所以要在一开始就确定幂等策略:库用 CREATE DATABASE IF NOT EXISTS,表结构放在 01_schema.sql 里用 CREATE TABLE IF NOT EXISTS 兜底,初始化数据单独放 02_risk_library.sql,不要在同一个文件里既建表又灌数。
#!/usr/bin/env bash # init_dpm_system.sh - 初始化双重预防系统(数据库与环境) set -euo pipefail DB_HOST="${DB_HOST:-127.0.0.1}" DB_USER="${DB_USER:-root}" DB_PASS="${DB_PASS:-}" DB_NAME="dpm_db" SQL_DIR="./sql" while getopts "h:u:p:" opt; do case "$opt" in h) DB_HOST="$OPTARG" ;; u) DB_USER="$OPTARG" ;; p) DB_PASS="$OPTARG" ;; *) echo "用法: $0 [-h 数据库地址] [-u 用户] [-p 密码]" >&2; exit 1 ;; esac done shift $((OPTIND - 1))# 续上文:建库、建表、导入初始风险库 echo "==> 创建数据库 ${DB_NAME}" mysql -h"${DB_HOST}" -u"${DB_USER}" -p"${DB_PASS}" \ -e "CREATE DATABASE IF NOT EXISTS ${DB_NAME} DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;" echo "==> 导入表结构与初始风险库" mysql -h"${DB_HOST}" -u"${DB_USER}" -p"${DB_PASS}" "${DB_NAME}" < "${SQL_DIR}/01_schema.sql" mysql -h"${DB_HOST}" -u"${DB_USER}" -p"${DB_PASS}" "${DB_NAME}" < "${SQL_DIR}/02_risk_library.sql" echo "==> 初始化完成,当前风险点数量:" mysql -h"${DB_HOST}" -u"${DB_USER}" -p"${DB_PASS}" "${DB_NAME}" \ -N -e "SELECT COUNT(*) FROM dpm_risk_point;"说明一下这段脚本里几个值得养成的习惯:
- getopts 负责解析 -h -u -p,处理完之后用 shift $((OPTIND - 1)) 把已经消费掉的选项参数从位置参数里移走,后面如果再传文件路径之类参数就不会串台。在 shell 命令行传参里,shift 是把位置参数一个个往前挪的命令,配合 OPTIND 可以清掉选项残留。
- set -euo pipefail 三件套强烈建议默认开启。u 表示变量未定义直接报错,e 表示任何一条命令非零退出就停,pipefail 保证管道只要有一段失败整体就失败。写初始化脚本最怕中间失败还继续跑,前半段的错到后半段才爆出来。
- 密码通过环境变量传入而不是写死在脚本里,同时注意不要在进程列表里明文暴露。命令行上直接 -p"${DB_PASS}" 会出现在 ps 输出里,生产环境更稳妥的做法是读 ~/.my.cnf,这里为了演示直观写成环境变量方式。
生产环境还有一种常见诉求:危化品基础数据是业务部门从 Excel 整理出来的,不能直接塞 SQL 文件。我一般会让 Shell 脚本循环读 CSV 逐行转 insert,同时跳过表头、去掉前后空格。写 for 循环时注意把 IFS 设成逗号,不然 CSV 字段会被空格切碎,这是 Shell 脚本入门阶段最容易踩的坑。
3.2 启动停止脚本与 JVM 参数:给线上留后路
Java 服务的启动脚本,核心需求是“启动可重复、停止可等待、重启可回滚”。启动前先检查进程是否已在运行,用 pgrep 而不是 ps grep,避免脚本自匹配到自己的命令行。停止时先发 TERM 信号,给 Spring Boot 里的数据库连接池、事务留出收尾时间,等 30 秒还没退出再强制 kill -9,这套逻辑比直接 pkill -9 靠谱得多。
#!/usr/bin/env bash # dpm_service.sh - 启动/停止/重启 APP_JAR="/opt/dpm/lib/dpm-system.jar" APP_NAME="dpm-system" LOG_DIR="/opt/dpm/logs" start() { if pgrep -f "${APP_NAME}.jar" >/dev/null 2>&1; then echo "服务已在运行" return 0 fi nohup java -Xms512m -Xmx1024m -XX:+UseG1GC \ -Djava.security.egd=file:/dev/./urandom \ -jar "${APP_JAR}" >> "${LOG_DIR}/app.log" 2>&1 & echo "启动中,PID: $!" } stop() { pid=$(pgrep -f "${APP_NAME}.jar" || true) [ -z "${pid}" ] && { echo "服务未运行"; return 0; } kill "${pid}" for i in $(seq 1 30); do if ! pgrep -f "${APP_NAME}.jar" >/dev/null 2>&1; then echo "已优雅退出" return 0 fi sleep 1 done echo "30 秒未退出,强制 kill -9" kill -9 "${pid}" }启动参数里那个 -Djava.security.egd=file:/dev/./urandom 很容易被忽略,但很关键。Java 在某些 Linux 发行版上默认读取 /dev/random 作为 SecureRandom 的种子源,高并发或低熵环境下会阻塞。双重预防系统每天会生成大量隐患编号和 UUID,如果不在 JVM 参数里换成 urandom,容易出现应用启动卡死或首次生成编号时明显延迟,这类问题看日志根本看不出端倪,属于典型的“黑匣子”故障。Xms 和 Xmx 建议在 2GB 以内的服务器上配成一致,避免运行中动态扩容引发停顿,Xmx 超过物理内存一半时反而要警惕容器内存超卖。
3.3 cron 定时任务:风险到期提醒与隐患超时升级
双重预防机制的“闭环”有很大一部分靠时间维度兜着。风险点复核到期前 30 天要提醒安全员,隐患整改超时要升级给部门负责人。这些任务在 Shell 里用一个挨一个的 curl 调 Java 接口即可,不需要单独部署一个调度中心。cron 只管“什么时候跑”,具体逻辑放在 Java 接口里,Shell 脚本只做“调接口 + 判断返回值 + 记错误日志”三件事。
#!/usr/bin/env bash # notify_daily_tasks.sh - 风险到期与标签到期提醒 set -euo pipefail API_BASE="http://127.0.0.1:8080/dpm/api" API_TOKEN="${DPM_TOKEN:-dev-token}" TODAY=$(date +%Y%m%d) LOG_FILE="/opt/dpm/logs/task_error.log" for ep in "notify/expiring-risks" "notify/label-expiry"; do curl -s -X POST "${API_BASE}/${ep}" \ -H "Authorization: Bearer ${API_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"queryDate\":\"${TODAY}\",\"windowDays\":30}" \ -o /dev/null if [ $? -ne 0 ]; then echo "$(date '+%F %T') 接口 ${ep} 调用失败" >> "${LOG_FILE}" fi done对应的 crontab 配置是:
0 8 * * * /opt/dpm/bin/notify_daily_tasks.sh >> /opt/dpm/logs/cron.log 2>&1 0 9 * * * /opt/dpm/bin/escalate_overdue_tasks.sh >> /opt/dpm/logs/cron.log 2>&1参数说明:windowDays 从外部传而不是硬编码在 Java 里,意味着年底大检查前安全员可以把 30 天临时改成 45 天,不用改代码重新编译。TODAY 用 date +%Y%m%d 生成数字串,这个写法 GNU date 和 BSD date 行为一致,跨环境友好;但如果要在 macOS 上跑,-d 参数语义不同,脚本开头可以用 uname 分支,或干脆统一在 Linux 服务器上跑定时任务,省去环境适配的麻烦。判断 curl 是否成功用 $? 是入门做法,更严谨的是配合 --fail --connect-timeout 让 curl 在 4xx/5xx 时非零退出,不然 HTTP 500 也会被当成成功。
4. Java 后端核心实现:规则引擎、状态流转与报表导出
Shell 把环境打理好了,就轮到 Java 写真正的业务逻辑。危化品双防系统的 Java 侧关键模块集中在三块:风险评估规则、隐患工单状态流转、外报文件导出。这三块做扎实,系统至少能扛住审计和月度核查。
4.1 风险值计算:把 LEC 阈值做成可配置规则
风险等级计算看起来只是一次乘法加一次比较,但直接写成if (score > 320)的硬编码,后患无穷。不同企业、不同评价方法对红橙黄蓝的阈值有微调,上级复查时甚至可能临时要求聚焦某类高风险项。把阈值抽到数据库配置表,让评估方法、阈值、等级三级联动,才是经得起推敲的做法。
@Service public class RiskAssessmentService { private final RuleConfigMapper ruleConfigMapper; @Transactional public RiskLevel assessAndRefresh(Long riskPointId, BigDecimal l, BigDecimal e, BigDecimal c, String assessUser) { BigDecimal score = l.multiply(e).multiply(c); RiskLevel level = resolveByScore(score, "LEC"); RiskAssessmentRecord record = new RiskAssessmentRecord(); record.setRiskPointId(riskPointId); record.setFactorL(l); record.setFactorE(e); record.setFactorC(c); record.setScoreTotal(score); record.setRiskLevel(level.name()); record.setAssessUser(assessUser); record.setAssessTime(new Date()); riskAssessmentRecordMapper.insert(record); // 同一个事务里刷新风险点当前等级 RiskPoint point = riskPointMapper.selectByPrimaryKey(riskPointId); point.setCurrentLevel(level.name()); point.setEvaluateDate(new Date()); point.setExpiryDate(calcExpiryDate(level)); riskPointMapper.updateByPrimaryKeySelective(point); return level; } private RiskLevel resolveByScore(BigDecimal score, String method) { List<RuleConfig> rules = ruleConfigMapper.findByMethod(method); for (RuleConfig rule : rules) { if (score.compareTo(rule.getThreshold()) >= 0) { return RiskLevel.valueOf(rule.getLevel()); } } return RiskLevel.BLUE; } }这段代码有几点要说清楚。BigDecimal 比较大小必须用 compareTo 而不是 equals,这一段在 java 基础面试题里经常被考,但实际生产里因为 equals 比较精度导致风险等级算错的案例并不少见。@Transactional 保证评估记录写入和风险点等级刷新要么同时成功,要么同时回滚,这是 2.1 节提到的数据一致性的落地。RuleConfig 表的行示例是 method=LEC、threshold=320、level=RED,threshold=160、level=ORANGE,threshold=70、level=YELLOW,查询时按 threshold 降序排,第一条命中的就是最终等级。
4.2 隐患工单状态机:用状态模式写不乱
隐患工单最容易写成大堆 if-else 的地方是“当前状态 + 目标状态 + 角色权限”三者的组合校验。状态枚举把可迁移规则内聚起来,比散落的逻辑判断可读性好得多。下面把五态迁移规则放到每个枚举常量里:
public enum RectifyStatus { PENDING(0, "待指派") { @Override public boolean canTransitTo(RectifyStatus target) { return target == ASSIGNED; } }, ASSIGNED(1, "整改中") { @Override public boolean canTransitTo(RectifyStatus target) { return target == DONE; } }, DONE(2, "待验收") { @Override public boolean canTransitTo(RectifyStatus target) { return target == VERIFIED || target == REJECTED; } }, VERIFIED(3, "已闭环") { @Override public boolean canTransitTo(RectifyStatus target) { return false; } }, REJECTED(4, "验收驳回") { @Override public boolean canTransitTo(RectifyStatus target) { return target == ASSIGNED; } }; private final int code; private final String desc; RectifyStatus(int code, String desc) { this.code = code; this.desc = desc; } public abstract boolean canTransitTo(RectifyStatus target); }@Service public class RectifyFlowService { public void transit(Long rectifyId, RectifyStatus target, String operator, String remark) { HazardRectify item = rectifyMapper.selectByPrimaryKey(rectifyId); RectifyStatus current = RectifyStatus.ofCode(item.getRectifyStatus()); if (!current.canTransitTo(target)) { throw new BusinessException("状态不允许流转: " + current + " -> " + target); } item.setRectifyStatus(target.getCode()); rectifyMapper.updateStatus(item); RectifyFlow flow = new RectifyFlow(); flow.setRectifyId(rectifyId); flow.setFromState(current.getCode()); flow.setToState(target.getCode()); flow.setOperUser(operator); flow.setRemark(remark); flow.setOperTime(new Date()); rectifyFlowMapper.insert(flow); } }这套写法的好处在验收驳回场景最明显:待验收的工单如果验收人填了“整改不到位”,只能流转回整改中,不能直接跳到已闭环,因为 DONE 的 canTransitTo 只接受 VERIFIED 和 REJECTED,REJECTED 又只能回 ASSIGNED。想要调整流程,只需改枚举里的方法体,不用去改 Controller 和前端下拉。前端的“下一步可点按钮”也可以由后端在详情接口里返回 allowedTransitions 推导,前后端共用同一套状态语义。这在 java 面试八股文里是老话题,但真正落到双防这类强流程系统里,价值非常直观。
4.3 报表导出:用 Apache POI 生成月度统计
监管平台每月要报风险分级和隐患整改进度,导出 Excel 是刚需。Apache POI 的 XSSFWorkbook 能处理 xlsx,重点不是“会不会写”,而是“怎么稳定地写”。常见翻车点是在 Controller 层直接同步生成几万行数据,内存直接爆掉。我一般导出走独立接口,先把数据查出来,再写到一个最小化的 XSSFWorkbook 里。下面是一个按月份导出隐患台账的例子:
public void exportMonthly(HazardQuery query, OutputStream out) throws IOException { try (XSSFWorkbook wb = new XSSFWorkbook()) { XSSFSheet sheet = wb.createSheet("月度隐患台账"); String[] headers = {"风险点", "等级", "隐患描述", "整改责任人", "状态", "整改时限"}; for (int i = 0; i < headers.length; i++) { sheet.createRow(0).createCell(i).setCellValue(headers[i]); } List<HazardRectify> rows = rectifyMapper.selectByMonthly(query); int rowIndex = 1; for (HazardRectify row : rows) { XSSFRow r = sheet.createRow(rowIndex++); r.createCell(0).setCellValue(row.getRiskPointName()); r.createCell(1).setCellValue(row.getRiskLevel()); r.createCell(2).setCellValue(row.getHazardDesc()); r.createCell(3).setCellValue(row.getRectifyUser()); r.createCell(4).setCellValue(row.getRectifyStatus().getDesc()); r.createCell(5).setCellValue( row.getRectifyDeadline() == null ? "" : row.getRectifyDeadline().toString()); } wb.write(out); } }这里没有做样式美化,是因为生成表单的首要目标是数据准确、打开不卡。监管上报一般要求 Excel 格式,加七八种颜色的单元格风格反而容易让文件体积膨胀。Apache POI 的 Word 部分不是不能生成图表,它确实可以,但生成参数繁琐、图表类型有限。如果需求是月报里带统计图,我通常的做法是 Java 侧算好序列,前端用 echarts 画图后转图片再填进 Word 模板,这样视觉效果可控,也避免了把图表生成的压力丢给后端线程池。
5. 危化品双重预防系统的避坑指南:五条高频踩坑记录
这一章写我在类似系统上踩过或见过的坑,每一条都按典型现象、原因、解决方法来记。前两条是业务逻辑层面的,后三条偏工程和运维,按优先级自己对照。
5.1 风险等级被人为改低,审计一查一个准
现象:月度汇总里某个重大危险源从红色变成黄色,经办人说是“重新评估过了”,但系统里找不到评估记录。
原因:某些页面为了操作方便,直接把风险点的当前等级字段做成了可编辑下拉框,业务人员手一抖就改低了,而评估记录表里没有对应的新记录。
解决:风险点的当前等级字段只允许由评估流程写入,任何页面都不能直接改。如果确有纠错需求,走“重新评估”入口,必须生成评估记录。审计时就要靠 dpm_risk_assessment 里的记录和 dpm_risk_point 里的当前等级一一对应,这是双重预防机制数字化系统最不能让步的一条设计底线。
5.2 隐患“假闭环”:只传照片不补整改证据链
现象:隐患状态显示已闭环,验收备注空着,附件只有两张模糊照片,上级复查时要求限期重改。
原因:验收动作只校验了“是否上传附件”,没校验附件数量和整改描述,业务人员用照片代替整改过程敷衍提交。
解决:验收接口增加规则校验:整改内容描述至少包含整改措施与结果两部分,关键隐患类型强制上传多张过程照片,且照片 EXIF 时间要在整改期限内。前端可以宽松一些,后端校验必须严格。这类“假闭环”问题在危化品场景里特别敏感,因为隐患描述里往往带着“泄漏”“超温”“静电接地失效”这类词,如果只看状态不看证据链,系统就变成台账造假工具了。
5.3 Shell 脚本跨环境翻车:CRLF、编码与路径三连坑
现象:脚本在开发机跑得好好的,到生产服务器就报一堆 command not found,连第一行 bash 都识别不出来。
原因:最常见的是 Windows 下编辑脚本留下了 CRLF 行尾,Linux 的 bash 把 \r 当成参数的一部分。utf-8 带 BOM 的脚本第一行同样会被识别失败,另一个是脚本里的中文注释在非 UTF-8 locale 下显示乱码甚至解析报错。
解决:脚本文件统一用 LF 行尾、UTF-8 无 BOM 保存。传输前执行 dos2unix 或 sed -i 's/\r$//' 处理。路径里的变量要带双引号,尤其遇到“路径带空格”这种情况,不带引号的话 cp、mv 命令会直接拆成两个参数。这些都是 shell 中常见坑,我习惯在脚本目录放一个 precheck.sh,上线前自动检查换行符、BOM、脚本语法和关键路径是否存在。
5.4 安全标签到期提醒做成“一次性任务”
现象:安全标签到期后只提醒了当月,下个月系统不再出现这条提醒,业务后面直接漏管。
原因:定时任务没有把“已提醒”和“已过期”的状态区分开,提醒一次就把记录标记成结束,整个生命周期就断了。
解决:提醒脚本里额外判断状态:到期前 30 天提醒一次,到期当天再提醒一次,过期未处理继续每天提醒,直到台账里标签有效期已更新。双重预防机制要的是持续闭环,一次性提醒没有意义。这条和风险点复评过期联动起来写:过期风险点自动置为“待复评”,不完成复评不允许新增相关隐患变更,用状态卡住流程而不是靠人记。
5.5 JDK 与 JDBC 驱动版本错配,半夜连接池报错
现象:凌晨跑数据迁移任务时日志报 Could not create connection to database server,白天手工连接完全正常。
原因:JDK 8 项目里用了过旧的 MySQL JDBC 驱动,或反过来 JDK 17 下还用 mysql-connector-java 5.x,时间协商和 SSL 握手在高并发下偶发失败,白天请求少看不出问题。
解决:统一驱动大版本,JDK 8 对应 mysql-connector-j 8.0.x,JDK 17 对应 8.3 以上,并在连接串上显式关闭不必要的 SSL:useSSL=false、serverTimezone=Asia/Shanghai。这类问题日志模型往往很怪,先查驱动版本与 JDK 兼容性,比改连接池参数要有效。我在一次配合排查中就遇到过类似情况,改完驱动版本后连接池恢复,之前调了一周的 hikari 参数全白搭。
6. 进阶:上线前的数据验证与离线容灾习惯
写到这里,最后落一个进阶检查。系统上线前可以跑一遍“反常数据巡检”,把两种最典型的问题提前捞出来:第一种是高风险点被偷偷降级,第二种是隐患工单状态与流转记录对不上。我用 SQL 就能查,不用等审计来翻车:
-- 找所有状态为 YELLOW/BLUE,但最近一次评估记录是 RED/ORANGE 的风险点 SELECT p.id, p.point_name, p.current_level, a.score_total, a.risk_level AS last_assess_level, a.assess_time FROM dpm_risk_point p JOIN dpm_risk_assessment a ON a.id = ( SELECT MAX(a2.id) FROM dpm_risk_assessment a2 WHERE a2.risk_point_id = p.id ) WHERE p.current_level IN ('YELLOW','BLUE') AND a.risk_level IN ('RED','ORANGE');这类 SQL 看着简单,但胜在能把“账面等级”和“评估依据”强行对齐。另一个我坚持的习惯是每天凌晨对 dpm_risk_point、dpm_hazard_rectify、dpm_rectify_flow 三张表做增量导出,一周一次全量备份。双防系统的数据是安全审计的底气,哪怕应用出问题,靠导出文件也能快速重建台账。如果你接手的是老项目,先用上面这条 SQL 跑一遍,结果会告诉你历史数据里藏了多少“手动改等级”的痕迹,这也是最直接的源码改造切入口。希望帮到你。
本文还有配套的精品资源,点击获取