|
这块功能我们前后改过三轮,第一版上线一个月就被客户投诉"提醒太吵"。下面把周期怎么算、任务怎么调、提醒怎么发这几件事按我们实际的顺序写一遍,包括中间返工的地方。
起因是一次停产
客户是一家做汽车零部件的工厂,厂区里 47 台叉车、9 台桥式起重机、3 台电梯、2 台蒸汽锅炉。检验周期原来由设备科一位同事用 Excel 管着,表里有一列"上次检验日期",旁边一列手填的"下次检验日期",他每月初自己扫一遍。
这位同事休产假,表格交接了,"每月扫一遍"这个习惯没交接。三台叉车的检验证书过期四十多天,安监来查,产线停了半天。后来他们找我们的时候,第一句话是"能不能到期自动提醒我们一下"。
听起来是个很小的需求。真做下来,发现难的地方不在提醒,在提醒之前那一步:到期日本身算得对不对。手填的日期里我们抽查了 20 条,错了 6 条,大部分错在"检验合格之后周期从哪天起算"。
检验周期不是一个数字
如果只做一类设备,写死一个 12 个月也能跑。但特种设备八大类的周期差别很大,粗略看几个常见的:
| 设备类别 | 定期检验周期 | 麻烦在哪 |
|---|
| 电梯 | 1 年 | 维保周期另算,别混在一起 | | 起重机械 | 1~2 年 | 按类型和使用工况分 | | 场(厂)内专用机动车辆 | 1 年 | 叉车归这一类,客户常认不准 | | 压力容器 | 1~6 年不等 | 跟安全状况等级挂钩,最不规则 | | 锅炉 | 外部/内部/耐压各自计 | 一台设备三条周期线并行 |
上面只是示意,实际以现行法规和当地特种设备安全监督管理部门的要求为准。也正因为它会变,这些数字必须是库里的配置,不能是代码里的常量。我们第一版就是硬编码的,后来法规调了一次、又碰上客户内控比法定更严(法定一年,他们要求十个月),改一次发一次版本,很快就受不了了。
起算点也是个坑。到底按"上次检验日期 + 周期"推,还是按检验报告上印的下次检验日期?我们最后的规则是报告优先——报告上有就用报告的,那是法定依据,没有报告才用规则推算。这个优先级要写进模型,不能靠约定。
还有停用、封存、报废、正在大修的设备。这些不能跟在用设备一样提醒。第一版没做这层过滤,客户有一批封存了两年的旧设备照样天天报警,这也是"提醒太吵"投诉的一部分来源。
三张表
模型可以简化到三层:台账记"是什么",规则记"多久一次",记录记"做过什么"。下次检验日期不是用户填的字段,是后两者推出来的结果。
-- 设备台账(节选)
asset (
id, asset_no, name, category_id, -- 类别决定套用哪条检验规则
status, -- 在用/停用/封存/报废/维修中
org_id, location_id, owner_org_node, -- 注意:责任岗位,不是责任人
reg_code, install_date
)
-- 检验规则:按类别 + 检验类型配,支持租户级覆盖
inspection_rule (
id, category_id,
inspect_type, -- 定期检验/年度检验/内部检验/耐压试验...
interval_months,
reminder_offsets, -- 提前提醒天数,如 [60,30,7,1]
escalate_after_days,
enabled
)
-- 检验记录:每检一次一条,唯一事实来源
inspection_record (
id, asset_id, inspect_type,
inspect_date,
result, -- 合格/复检/不合格
next_due_date, -- 报告上印的下次检验日期,优先级最高
cert_no, report_file, org_name
)
inspect_type 这个维度值得单独说一句。锅炉的外部检验、内部检验、耐压试验是三条独立周期,同一台设备得能同时挂多条待检任务。我见过不少自研系统在设备表上放一个 next_inspect_date 字段就完事,后面接锅炉、接压力容器的时候怎么改都别扭,最后只能加表重构。
到期日的计算
LocalDate resolveNextDueDate(Asset asset, String inspectType) {
InspectionRecord last = recordRepo.findLatest(asset.id, inspectType);
InspectionRule rule = ruleRepo.match(asset.categoryId, inspectType);
// 从没检过:安装/启用日期 + 首检周期
if (last == null) {
return asset.installDate.plusMonths(rule.firstIntervalMonths());
}
// 报告上写了就用报告的
if (last.nextDueDate != null) {
return last.nextDueDate;
}
// 不合格的那次不能推进周期
if (last.result == Result.FAILED) {
return last.inspectDate; // 保持到期状态,另走整改流程
}
return last.inspectDate.plusMonths(rule.intervalMonths);
}
逻辑本身没什么,坑在旁边。result = 不合格 那一条如果照常推进周期,一次不合格反倒把到期日往后推了一年,性质就变了,这个 bug 我们线上出过。月末也要留意,1 月 31 日加一个月得落到 2 月 28/29 日,JDK 的 plusMonths 已经按就近有效日处理了,自己拿天数硬算反而容易错。
另外别在查询时实时算。列表页几千台设备每次翻页都算一遍,慢得很明显。我们的做法是每天调度的时候算好写进一张 asset_due_snapshot,检验记录一变就刷新对应那条。
调度:怎么保证一次都不漏
最早想得挺花:给每台设备每个提醒节点建一个延时任务,到点触发,很精确。做完发现维护成本高得离谱——台账改一次、规则调一次,几千个已排的任务全要重排,还得考虑排到一半失败怎么办。
后来退回到最土的办法:每天定时全量扫一遍。粗,但可重跑、跟历史状态无关,几万台设备的扫描秒级就完事。加上两个补丁之后它反而更可靠。
@Scheduled(cron = "0 30 6 * * ?")
void scanInspectionDue() {
for (Tenant t : tenantRepo.activeTenants()) {
LocalDate today = LocalDate.now(t.zoneId()); // 租户时区,别用服务器时区
List<DueSnapshot> hits = snapshotRepo.findHits(t.id, today);
for (DueSnapshot s : hits) {
long sc-camel-days-left = DAYS.between(today, s.nextDueDate);
Integer node = matchNode(s.offsets, daysLeft); // 60/30/7/1/逾期
if (node == null) continue;
// 幂等键:设备 + 检验类型 + 到期日 + 节点,只发一次
String key = s.assetId + ":" + s.inspectType
+ ":" + s.nextDueDate + ":" + node;
if (!reminderLogRepo.tryInsert(key)) continue; // 唯一索引兜底
pendingByOwner.add(s.ownerNode, s); // 先攒着,不立刻发
}
pendingByOwner.flushAsDigest(t); // 按人合并成一条
}
}
那个幂等键是整段代码里最要紧的一行,配合数据库唯一索引用。有了它,任务重跑、服务重启、集群里多个节点同时触发、运维手动补发,都不会重复轰炸。
第二个补丁是节点匹配不能用等号。假设昨天服务挂了,今天扫的时候 daysLeft 已经从 30 掉到 29,正好跳过 30 天这个节点。所以判断条件是"daysLeft ≤ 节点值 且这个节点还没发过就补发"。宁可晚一天,不能不发。
被投诉"太吵"之后改的东西
第一版技术上没问题,消息都发出去了,但两周后客户跟我们说没人看。原因很简单:一个管理员名下十几台设备同期到期,他一早上收到十几条几乎一样的消息,第三天就开始无视了。
改动有两处。一是按人聚合,同一责任人当天所有到期项合并成一条摘要,点进去是列表。二是把不同紧急度的提醒分开走:
| 距到期 | 通知谁 | 怎么发 |
|---|
| 60 天 | 设备管理员 | 站内消息,够他去约检验机构 | | 30 天 | 管理员 + 使用部门 | 企微/钉钉,同时自动开检验工单 | | 7 天 | 加抄上级主管 | 企微/钉钉 | | 1 天 / 已逾期 | EHS 负责人 | 短信,设备同时置为"禁用待检" |
最后那一行是客户主动要求加的,也是整个功能里最有效的一条。光提醒没有约束力,逾期之后把设备状态改成"禁用待检",领用、派工、报修流程里都拦住它,这时候提醒才算真的有分量。当然这个动作要能审批解除,不然赶产的时候会被人绕过去。
提醒之后那半截
如果提醒完还是靠微信协调,系统就只是个会响的闹钟。这条链在易点易动里是接起来的:
扫描命中→生成检验工单→排期、协调停机→现场扫码传报告→写入检验记录→到期日自动刷新
最后一步是自动的:检验记录落库就触发快照重算,同时解掉"禁用待检"。台账上没有任何需要手填的下次检验日期,Excel 那套流程到这里就退出了。
设备上贴的二维码比我们预想的有用。检验人员和设备管理员在现场扫一下就能看到这台设备的检验历史、证书 PDF、下次到期日,报告拍照直接传。省掉"回办公室再补录系统"这一步,记录的及时性才有保障——不然台账又开始滞后,前面算得再准也没意义。
还有几个具体的坑
责任人离职,提醒发进黑洞。通知对象一开始绑的是具体人,有位管理员离职之后,他名下两台设备的提醒空发了一个月才被发现。改成绑组织节点或岗位,人走了自动落到接任者头上。
时区。UTC 服务器上取 LocalDate.now(),在国内客户看来还是昨天,"明天到期"就会晚一天发。按租户时区取当天日期。
历史数据导入当天的雪崩。首次导入几千条台账,里面本来就有一批已经逾期的,扫描直接甩出上千条告警,客户当场就慌了。后来加了导入静默期:先让他们把历史数据在系统里清一遍,再开提醒开关。
复检和延期检验。这两种情况的日期规则各地要求不完全一样,别自己想,做成配置项,交给客户的安全管理人员定。
整件事回头看,技术含量最高的部分大概就是那个幂等键,其余都是把细节一个个补齐:周期可配、多检验类型并行、状态过滤、按人聚合、逾期有约束、记录驱动回写。哪一条缺了都会在某个月漏掉某台设备。
如果你们厂里的特种设备检验还在 Excel 上管,或者已经因为过期被查过,这套逻辑在易点易动里是开箱就有的,不用自己从定时任务写起。
|