门禁说走了,柜子说在,盘点说没有:资产"在位"状态的冲突收敛设计

举报
yd_242545912 发表于 2026/10/29 21:16:51 2026/10/29
【摘要】 季度盘点那天出了件很难解释的事。系统里三件涉密载体显示"在柜",管控柜的轮询也确实读到了它们的标签;但门禁日志里,其中一件在前一天下午被带出过一次,没有对应的外带审批单;另外两件,同事拿着手持机在保密室里来回扫了三遍,一次都没读到。三个人给了我三个答案。柜子的结论是在位,门禁的结论是出去了,手持机的结论是找不到。追问"它到底在不在",其实问错了——三个设备都没有说谎,它们只是在回答三个不同的...

季度盘点那天出了件很难解释的事。

系统里三件涉密载体显示"在柜",管控柜的轮询也确实读到了它们的标签;但门禁日志里,其中一件在前一天下午被带出过一次,没有对应的外带审批单;另外两件,同事拿着手持机在保密室里来回扫了三遍,一次都没读到。

三个人给了我三个答案。柜子的结论是在位,门禁的结论是出去了,手持机的结论是找不到。追问"它到底在不在",其实问错了——三个设备都没有说谎,它们只是在回答三个不同的问题。

这件事之后我们把"在位"这个字段从系统里拆掉了。不是因为难维护,是因为它本身就不成立。

一、"在位"不是一个布尔值

三种采集设备的观测语义完全不同。

柜内天线是持续驻留采样:每十秒轮询一轮,读到就是 present,读不到就是 absent。它回答的是"此刻这枚标签是否在柜内天线的覆盖范围内"。

通道门禁是穿越瞬时事件:标签经过读取面的那几百毫秒里被读到,配合前后天线的触发顺序给出 enter 或 leave。它回答的是"这枚标签刚刚穿过这扇门,往哪个方向"。

手持盘点是人为发起的时刻快照:同事走过哪些区域、天线朝哪个角度、有没有被金属文件柜挡住,都会影响结果。它回答的是"在人走过的这条路径上,这一刻读到了什么"。

一个布尔字段要同时承载"十秒粒度的驻留"、“毫秒粒度的穿越"和"不定期的抽样”,这本身就是设计错误。所以第一步不是修数据,是把状态位换掉,换成观测事实。

每一路只上报自己亲眼看到的事,不解释,不裁决。

二、把三路降维成同一种事件

统一成四元组:谁、在哪、什么动作、什么时候看到的。

from dataclasses import dataclass
from datetime import datetime

@dataclass(frozen=True)
class Observation:
    tag: str            # 标签 EPC
    source: str         # cabinet / gate / handheld
    node: str           # 天线编号或门禁编号
    phase: str          # enter / leave / present / absent
    event_time: datetime  # 设备本地采集时刻,不是上云时刻
    rssi: int

def edge_from_poll(tag, node, seen, ts, rssi):
    """柜内轮询结果转成边沿事件:只在状态翻转时产出"""
    prev = last_poll_state.get((tag, node))
    last_poll_state[(tag, node)] = seen
    if prev is None or prev == seen:
        return None                    # 无翻转,不产事件,避免刷屏
    return Observation(tag, "cabinet", node,
                       "present" if seen else "absent", ts, rssi)

两个细节值得说。

第一,柜内轮询必须做边沿化。十个柜子每十秒一轮,一天就是八万多条记录,其中百分之九十九点九是"还是那样"。只在上报值翻转时产事件,日事件量从八万降到几百,后面所有重排和消费的计算量跟着降两个数量级。

第二,event_time 必须是设备本地采集时刻,不能用云端收到消息的接收时刻。这条在断网场景下是致命的,下一节会看到。

三、方向要靠事件配对,不能靠单次读数

门禁读到一次标签,只能说明"它在这一刻出现在这扇门的读取面附近"。人抱着文件在门口站一会儿、标签贴在金属文件柜侧板上被反射读到,都可能产生单次读数。

所以方向不来自读数本身,来自配对:同一枚标签在同一次穿越里,先 a 侧天线读到、后 b 侧天线读到,中间间隔在 1.5 秒以内,才合成一个 leave(或 enter)。落单的读数不进事件流,只进调试日志。

柜内同理,present → absent 是离开座位,absent → present 是回到座位,两次翻转之间的时间差就是它在柜外的时长。

四、乱序和迟到:允许窗口,但别改写历史

边缘侧断网是常态。保密室那个 IoT Edge 节点上个月因为交换机重启断了四十分钟,恢复后一次性补传了两千多条事件。这些事件的 event_time 全部落在四十分钟之前,到达却是最后。

如果按到达时间消费,状态机会先看到"四十分钟后的 enter",再看到"四十分钟前的 leave",最后状态停在"已离开"——和事实完全相反。

处理办法是两条:窗口内重排,窗口外补偿。

ALLOW_LATE = timedelta(minutes=5)

def consume(obs, watermark):
    if obs.event_time >= watermark - ALLOW_LATE:
        buffer.append(obs)                 # 进重排缓冲,按 event_time 排序后消费
        buffer.sort(key=lambda o: o.event_time)
        while buffer and buffer[0].event_time <= watermark:
            state_machine.apply(buffer.pop(0))
    else:
        correction_log.append(obs)         # 超窗:不改历史,记一次补偿
        state_machine.correct(obs)         # 修正当前结论,并留下修正痕迹

关键在最后两行。超过迟到窗口的事件不去覆盖已经写下的历史结论,而是作为一条 correction 追加进去,状态机的当前值按它调整,但"当时系统认为是 X"这个事实保留。

审计的时候查的往往不是"现在是什么状态",而是"那天下午系统为什么判断它在柜"。改写历史会让这个问题永远答不出来。

五、冲突不裁决,降级成"待核"

状态机只有四个值:在柜、在途、在用、待核。

leave 且没有对应外带单,进在途并立刻告警;enter 回在柜;有领用单的进在用。

真正难判的是另一种:门禁说出了门,柜子却还在读到它。两个源冲突,谁对?

我们的选择是不裁决。落进"待核",同时把两边证据都附在记录上:门禁的穿越时间、天线编号、RSSI 序列,柜内最近三轮的轮询结果。等人工确认。

理由很实际。硬判丢失,误报率会让人不再相信告警;硬判在位,真出事时系统等于没说话。待核是一个诚实的中间态,它承认系统不知道,并且把"不知道"这件事本身变成一条待办。超过二十四小时没人处理,自动升级到部门负责人。

六、落到云上的那一段

设备侧在华为云 IoTDA 里定义一个标准物模型,属性只报三样:node_id、phase、event_time,RSSI 作为可选扩展字段。不上报任何业务含义,业务含义全部留给云端算——设备固件升级的周期比业务规则长得多,把判断逻辑放在端上,改一次规则就要动一批设备。

边缘节点用 IoT Edge 承接本地缓存,断网期间事件落本地库,链路恢复后按 event_time 升序批量上报。规则引擎把设备属性流转到 DIS,再由 FunctionGraph 消费,状态表落 GaussDB,用版本号做乐观锁,避免同一枚标签的并发事件互相覆盖。

对接第三方系统时,适配器统一在启动时装配:

SINKS = {
    "ledger": LedgerAdapter("首码资产管理系统"),
    "alarm":  AlarmAdapter(channel="app+mail"),
}

结语

做完这套之后,盘点再出现"三个答案不一致",我们不再急着修数据。

不一致本身就是信息。它说明某一路采集出了问题,或者某一次带出没有走审批。系统诚实地把三个答案并排放在一起,让人去看那个矛盾,比强行合成一个看似干净的状态位要有用得多。

台账的价值不在于给出一个确定的答案,而在于当答案不确定时,它还记得自己是怎么不确定的。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。