设备管理系统实战:易点易动如何实现特种设备检验周期的自动提醒与调度

这块功能我们前后改过三轮,第一版上线一个月就被客户投诉"提醒太吵"。下面把周期怎么算、任务怎么调、提醒怎么发这几件事按我们实际的顺序写一遍,包括中间返工的地方。

起因是一次停产

客户是一家做汽车零部件的工厂,厂区里 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 上管,或者已经因为过期被查过,这套逻辑在易点易动里是开箱就有的,不用自己从定时任务写起。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值