# 成品流转对账 · MQTT 接入约定 v1.0 状态:2026-09-11 接入约定。已在本机试运行版实现 MQTT 接收及独立采集适配;真实来源尚待现场联调。Topic 和报文字段在本文规定;示例为虚构数据。启动和配置见 README.md。 ## 1. 传输与 Topic MQTT 3.1.1/5 均可;载荷 UTF-8 JSON,schema_version 为 `1.0`。建议 QoS 1,持久会话、稳定 client_id、本地待发送队列。启用 TLS、各来源独立身份和发布 Topic 白名单;浏览器不保存采集端写入凭证。 | Topic | 发布者 → 接收者 | QoS / retain | 用途 | | --- | --- | --- | --- | | `fg/v1/{site_id}/{line_id}/events/{source}/{node_id}` | 各子采集 → Web | 1 / false | 逐台下线、扫码、漏扫、补扫、入库、借还、通用节点结果 | | `fg/v1/{site_id}/{line_id}/routes/{route_id}` | 指定路线配置发布端 → Web | 1 / true | 当前完整路线定义;历史版本由 Web 归档 | | `fg/v1/{site_id}/{line_id}/health/{source}` | 各子采集 → Web | 1 / true | 心跳、完整水位、回补状态,LWT 发布 offline | | `fg/v1/{site_id}/{line_id}/pauses/{source}` | 指定停线采集 → Web | 1 / false | 临时停线/恢复事件,与班次就餐表合并 | | `fg/v1/{site_id}/{line_id}/scan-summary/{source}` | 码垛采集 → Web | 1 / true | 源累计漏扫/补扫快照;独立核对,不与明细相加 | | `fg/v1/{site_id}/{line_id}/acks/{source}` | Web → 对应采集/发布端 | 1 / false | 持久化确认;路线发布端 source 使用 config | | `fg/v1/{site_id}/{line_id}/wms-node/all/request` | Web → WMS 节点 | 1 / false | Web 已有下线条码的扫码/入库查询,最多最近 7 天 | | `fg/v1/{site_id}/{line_id}/wms-node/{node_id}/gap-result` | WMS 节点 → Web | 1 / false | 前后区间原始记录核查,含两库查询完整性、NOREAD行号和时间 | 推荐来源代码:`mes`、`pallet`、`wms`、`oqc`;新增采集源使用稳定代码。site_id、line_id、node_id 使用稳定编码,禁用 `/`、`+`、`#` 和空字符串。名称允许中文。一个来源 Topic 不允许其他模块伪造事件。 WMS 请求报文使用 `message_type=wms.query`,携带 `schema_version`、`site_id`、`line_id`、`request_id`、`sns`(1~100 个)、`window_start`、`server_time`。Web 每 10 秒按产线轮转仍缺扫码或有效入库证据的已有 MES 下线条码;范围按北京时间今天及前 6 天,优先今天。节点按 LineCode、barcode、QueryTime 参数化查询,回包仍使用原 events/acks 协议。请求授权 60 秒后过期,未请求、超期、跨线和过早记录不发送;旧队列保留,后续请求可恢复原文补传。按条码查询不声明整库 `backfill_complete=true`。部署权限须允许 Web 发布 request、WMS 订阅 request 和 pallet/wms 回执;中央 WMS 节点按已有映射发布 pallet/PALLET 与 wms/WMS 事件。 Web 订阅示例(F01 下所有线): 超时核查请求可携带 `gaps`,每次最多10项,每项提供 `sn`、`before_sn`、`after_sn`、`missing_sns`。节点分别定位两库中前后参照记录并读取其rowid之间的记录;参照重复、时间乱序、查询失败、超过1000条均不能声明完整。响应 `message_type=wms.gap-result` 携带 `request_id`、`checked_at` 和两个 `sources` 查询结果,使用 `acks/wms` 哈希确认。Web只接收自己请求对应的核查结果,重放已保存的相同结果返回duplicate。唯一缺机且发现NOREAD按规则认定漏扫;两库完整且区间空才判疑似被人为拉走;多缺机或NOREAD复用保持待核查。 ```text fg/v1/F01/+/events/+/+ fg/v1/F01/+/routes/+ fg/v1/F01/+/health/+ fg/v1/F01/+/pauses/+ fg/v1/F01/+/scan-summary/+ ``` 运行状态不会根据 retained 历史心跳自动判健康,仍检查 observed_at、covered_through、backfill_complete。业务事件不能 retain,否则新订阅者只收到各 Topic 最后一条而缺失历史。 ## 2. 逐台业务事件 Topic:`fg/v1/F01/L01/events/mes/OFFLINE` ```json { "schema_version": "1.0", "event_id": "MES-OFF-781001", "source": "mes", "source_record_id": "MES-OFF-781001", "source_revision": 1, "site_id": "F01", "line_id": "L01", "line_name": "总装一线", "node_id": "OFFLINE", "node_name": "整机下线(成品下线)", "route_id": "MAIN", "route_version": 1, "sn": "AC260911000002", "work_order_no": "MO260911-001", "product_code": "FG-35001", "event_type": "offline.result", "result": "success", "occurred_at": "2026-09-11T10:01:00+08:00", "collected_at": "2026-09-11T10:01:01+08:00", "line_sequence": 2, "sequence_scope": "L01-20260911-day", "evidence_level": "source", "data": {"station_id": "OFF-01"} } ``` | 字段 | 规定 | | --- | --- | | schema_version / event_id / source | 必填;event_id 在同 source 内跨重启不变、全厂唯一(建议包含站点/线体前缀)。重发同事件保留原完整载荷 | | site_id / line_id / node_id | 必填,必须与 Topic 一致;跨线 SN 或身份冲突进入待核对,不静默覆盖归属 | | node_name | 必填;未知节点自动生成列,已知节点改名走路线版本定义 | | source_record_id / source_revision | 必填;凭证可追溯;源记录修订必须新 event_id,加 revision 与 supersedes_event_id 引用原事件 | | sn | 逐台事件必填字符串,保留前导零。全局唯一,不复用;多个读取结果不是多个产品 | | work_order_no / product_code | 两个键均保留,分别为工单号和产品编码;来源无字段时显式 null,不能互相替代。Web 可从同 SN 的 MES 起点补充展示并标来源;不改原报文 | | route_id / route_version | 必填;未知版本先存待匹配,不按当前最新路线强行推断历史 | | occurred_at / collected_at | 必填 ISO 8601 带时区;Web 另记录 received_at,统一 UTC 存储。回补沿用业务时间,不用接收时间覆盖 | | event_type / result | 按下表枚举校验,MQTT ACK 不属于业务 result | | line_sequence / sequence_scope | MES 下线顺序推测所需;可 null,缺失则不运行 ABC 推测。序号同产线、同作用域唯一且递增;跨班/重启重置时变更作用域 | | evidence_level | 采集事实使用 source;Web 推测另存 derived,禁止混进确认漏扫计数 | | data / reason_code / reason / operator_id | 扩展源字段;操作人仅在来源提供时填,不推测责任人 | 原始 SN 不做数值转换,不从条码加减得出“下一台”。source_revision 用于同单据修订;跨来源按业务时间与明确引用建因果顺序,同时间又缺少顺序证据的矛盾记录应标待核对,不能按消息到达顺序随意覆盖。 ## 3. 事件类型与结果 | 来源 / node_id | event_type | result | 额外必需字段 | | --- | --- | --- | --- | | mes / OFFLINE | offline.result | success / failed | 下线记录;成功为唯一统一起点 | | pallet / PALLET | scan.result | success / failed | 原扫码/校验记录,成功不等于 WMS 入库;明细【码垛扫码】列即此来源,可用 WDS 日志逐条对照 | | pallet / PALLET | scan.missed | confirmed / cancelled | 对应真实 SN;取消需新修订号及 supersedes_event_id,保留原历史 | | pallet / PALLET | scan.rescan | success / failed | miss_event_id;引用同来源、同 SN 的漏扫事件 | | wms / WMS | warehouse.result | success / failed / unknown / revoked | warehouse_document_id,撤销另含 reversal_of_event_id;unknown 可暂缺单据 | | oqc / OQC | oqc.borrow / oqc.return | success / failed | loan_id;归还配对同 SN、同 loan_id 的先前有效借机 | | wms_receipt / WMS | receipt.result | success / failed | warehouse_document_id(收货单号);KT_成品收货报表回采的实体收货明细,一行一个条码 | | 指定来源 / 新节点 | node.result | success / failed / unknown | 原节点凭证;不内含未经注册的业务推测 | 入库撤销只撤销所引用的入库记录,不能撤掉更晚的新有效入库单。来源更正也采用新事件+引用,不覆盖历史。重复正常扫码保留操作证据,但计台仍去重。来源明确纠正一条错误漏扫时,以新修订事件引用旧记录,重新计算有效统计并保存原发现/订正履历。 ### 3.1 上抛回执与 WMS 实体收货必须对照 `warehouse.result` 来自中央 MySQL `WmsUploadTable`(`isWmSuccess='Y'`),它只证明**上抛接口反馈成功**,不等于 WMS 真的收下了货;`receipt.result` 才是 WMS 收货报表回采的**到货凭证**。上抛接口偶发“反馈成功但 WMS 未接收”,因此两台数据必须逐条码对照: - 有 `warehouse.result=success`、宽限期(`rules.wmsAcceptGraceMinutes`,默认 240 分钟)内仍无 `receipt.result` → **【WMS上抛】列置「WMS接受异常」**、**【WMS确认入库】列置「WMS接受异常」**,对账状态「已正确上抛WMS,但WMS接受异常」,异常码 `warehouse_accept_failed`;该台不计入「WMS 确认入库」,计入「尚未入库」。 - 判定只在 WMS 收货明细**采集覆盖范围内**生效:覆盖起点取已采到的最早收货日零点,早于该日的上抛当时并未采过收货报表,不得判为异常;这类台在【WMS确认入库】列显示 `收货报表未覆盖期间`(黄),所属市场未接入收货报表时显示 `市场无收货报表覆盖`(黄)。 - 明细把单一 WMS 节点拆成两列(`WMS` = WMS上抛 / `WMS_CONFIRM` = WMS确认入库),**不新增 MQTT 报文、不动路线定义**:`node_id='WMS'` 仍是上抛回执的落点,【WMS确认入库】由投影层按 `receipt.result` / `oqc.return` 生成虚拟列。 - 收货明细回采到达后异常自动闭环(不再有该异常,案件随之恢复)。 ## 4. 漏扫与补扫成对示例 Topic:`fg/v1/F01/L01/events/pallet/PALLET`。以下两个完整报文先后到达;即便实际接收乱序也能按引用回补。 ```json { "schema_version": "1.0", "event_id": "PALLET-MISS-002", "source": "pallet", "source_record_id": "MISS-002", "source_revision": 1, "site_id": "F01", "line_id": "L01", "node_id": "PALLET", "node_name": "码垛扫码", "route_id": "MAIN", "route_version": 1, "sn": "AC260911000002", "work_order_no": "MO260911-001", "product_code": "FG-35001", "event_type": "scan.missed", "result": "confirmed", "occurred_at": "2026-09-11T10:10:00+08:00", "collected_at": "2026-09-11T10:10:01+08:00", "evidence_level": "source" } ``` ```json { "schema_version": "1.0", "event_id": "PALLET-RESCAN-002", "source": "pallet", "source_record_id": "RESCAN-002", "source_revision": 1, "site_id": "F01", "line_id": "L01", "node_id": "PALLET", "node_name": "码垛扫码", "route_id": "MAIN", "route_version": 1, "sn": "AC260911000002", "work_order_no": "MO260911-001", "product_code": "FG-35001", "event_type": "scan.rescan", "result": "success", "miss_event_id": "PALLET-MISS-002", "occurred_at": "2026-09-11T10:28:00+08:00", "collected_at": "2026-09-11T10:28:01+08:00", "evidence_level": "source" } ``` 收到漏扫后:累计漏扫 1、已补扫 0、待补扫 1。收到匹配成功补扫后:1、1、0。重复接收仍为 1、1、0。若只收补扫,先展示“补扫凭证已到、原漏扫待回补”,不虚构原漏扫;来源回补后配对。全部有效漏扫配对成功才把该 SN 从待补转到已补。 ## 5. 路线定义与新增节点 Topic:`fg/v1/F01/L01/routes/MAIN`,retain=true。以下在旧路线中插入 TRANSFER;必须连同 WMS 的前置关系一起发布。其他产线分别发布自己的路线定义。 ```json { "schema_version": "1.0", "message_type": "route.definition", "site_id": "F01", "line_id": "L01", "route_id": "MAIN", "route_version": 2, "effective_at": "2026-09-12T07:30:00+08:00", "nodes": [ {"node_id":"OFFLINE","node_name":"整机下线(成品下线)","predecessor_ids":[],"required":true,"source":"mes"}, {"node_id":"PALLET","node_name":"码垛扫码","predecessor_ids":["OFFLINE"],"required":true,"source":"pallet","timeout_seconds":1200}, {"node_id":"TRANSFER","node_name":"入库交接","predecessor_ids":["PALLET"],"required":true,"source":"transfer"}, {"node_id":"WMS","node_name":"WMS上抛","predecessor_ids":["TRANSFER"],"required":true,"source":"wms"}, {"node_id":"OQC","node_name":"OQC 借还机","predecessor_ids":["OFFLINE"],"required":false,"branch":true,"source":"oqc"} ] } ``` 前置关系生成主线/旁路线;required 表示适用路线内必经,branch 表示旁路。未提供阈值的普通节点可展示状态,不自动启用无记录超时。1200 秒仅为演示建议值,正式值按区段标定。 Web 原子校验:节点编码唯一、引用存在、无环、版本递增且生效时间合法。配置未完整到达不部分切换。历史版本永存,旧版本重发幂等且不能覆盖新版本。同 SN 继续走下线时绑定版本;版本更改若确需迁移在途产品,必须显式记录迁移依据。 ## 6. 停线与健康 Topic:`fg/v1/F01/L01/pauses/mes`;固定就餐由 Web 班次日历管理,实际停线事件来自指定子模块。以下是暂停报文;恢复报文沿用 pause_id、区段及来源,使用新 event_id、event_type=`line.resume`、实际恢复 occurred_at。 ```json { "schema_version":"1.0", "event_id":"PAUSE-L01-20260911-001", "source":"mes", "source_record_id":"STOP-001", "source_revision":1, "site_id":"F01", "line_id":"L01", "event_type":"line.pause", "pause_id":"L01-20260911-lunch", "edge_from":"OFFLINE", "edge_to":"PALLET", "reason":"就餐", "occurred_at":"2026-09-11T12:00:00+08:00", "collected_at":"2026-09-11T12:00:01+08:00" } ``` pause/resume 无 SN。恢复先到则暂存配对;相同 pause_id 的重复事件幂等,恢复时间早于暂停需待核对。重叠日历/实际停线按区间并集扣时,不能重复扣除。来源更新或日历变更按版本保存,标明重算依据。 Topic:`fg/v1/F01/L01/health/pallet`;建议每 30 秒一次,90 秒无新心跳标离线(均可配置)。 ```json { "schema_version":"1.0", "message_type":"connector.health", "site_id":"F01", "line_id":"L01", "source":"pallet", "status":"online", "observed_at":"2026-09-11T13:10:00+08:00", "last_success_at":"2026-09-11T13:09:59+08:00", "covered_through":"2026-09-11T13:09:58+08:00", "backfill_complete":true, "pending_outbox":0, "last_error":null } ``` covered_through 是**已完整查询、交付并可确认接收的业务时间水位**,不是“最近一台扫码的时间”。无生产也可推进完整扫描水位。跨 Topic 的心跳不能证明业务消息已落库:Web 结合自己的持久化检查点/回补清单确认完整性,不能只信到来的健康消息。来源时钟偏差过大同样标不完整。 v0.3 接收实现要求在线心跳另外带 manifest(本批业务/停线/汇总消息 hash 数组)和 previous_checkpoint(上一已确认心跳 hash,没有则 null)。Web 递归检查当前及前置批次已持久化,再认可完整性;缺 manifest 的旧格式心跳可显示连接情况,但不用于无记录推测。hash 为 Topic + 换行 + 按键名排序的规范化 JSON 的 SHA256。Web 发布 persist.ack,status 为 accepted/duplicate/rejected,采集收到后才更新本地队列状态。完整格式见 README.md。 ## 7. 只有累计数量的码垛来源 Topic:`fg/v1/F01/L01/scan-summary/pallet`。覆盖本 Topic 最新快照;历史班次快照由 Web 归档,断线期间历史需另行回补。 ```json { "schema_version":"1.0", "message_type":"scan.summary", "site_id":"F01", "line_id":"L01", "source":"pallet", "snapshot_id":"PALLET-L01-20260911-DAY", "revision":3, "as_of":"2026-09-11T13:10:00+08:00", "window_start":"2026-09-11T07:30:00+08:00", "window_end":"2026-09-11T19:30:00+08:00", "business_date":"2026-09-11", "shift":"day", "count_basis":"source_offline_cohort_unique_sn", "missed_total":12, "rescanned_total":9, "pending_total":3, "sn_detail_complete":false } ``` 这里 12=9+3 的前提是源已按“同一下线批次、唯一 SN、已补清全部漏扫”统计。源只有操作次数时 count_basis 使用 `source_operation_count`,Web 显示“次数”并禁止当“台数”。无法说明口径则用 `source_unspecified` 展示“源汇总待核对”,不计算与逐台台账的差额。 同 snapshot_id 仅取最大 revision,不把每次轮询的 12 台相加;同 revision 不同内容进冲突记录。缺具体 SN 时只能展示未定位数量及疑似 SN,不把 source 汇总与已定位明细重复累计。跨班补扫按下线归属和补扫操作班次分别处理。 ## 8. 幂等、顺序与实施验收 1. 采集使用稳定源记录号+修订号构成 event_id。增量游标在本地 outbox 与查询结果一起持久化后推进,崩溃后可重发。PUBACK 只证明 Broker 收到,不能证明 Web 落库,更不能证明入库成功;持久会话外还需要保留源可回补窗口和定期核对。 2. Web 按 `(source,event_id)` 去重(此约定要求 event_id 全厂唯一);同键相同业务载荷幂等,同键不同内容隔离并告警,不静默选择一个。JSON 属性顺序不应影响载荷相等判定。 3. 消息按业务发生时间、源 revision、引用关系重建;缺漏扫/借机/路线定义的下游记录暂存未配对,后续到达自动重算。缺来源、非法时间、Topic/载荷不一致、未知主版本进入待核对队列。 4. 冻结日结快照与最新回溯结果分开;迟到补扫/入库不覆盖昨天的原始快照。历史异常有发现、证据变化、恢复及原因关闭记录。 5. 接收端可重建同一批次结果:成功下线=有效入库+未入库;确认漏扫=全部补清+仍待补。还机、补扫、MQTT 确认、上抛回执均不能替代 WMS 确认入库凭证(收货报表回采的 `receipt.result`)。 6. 联调必须覆盖:重复/乱序/丢包回补、来源重启、同键冲突、单据撤销与更正、B 缺口、只有汇总无 SN、补扫先到、OQC 未配对、一小时就餐、跨夜/换班、停线源断连、未知节点及路线切换。 后台 lib/protocol.js 执行本约定的字段、来源、时间、引用格式和路线结构校验;关联业务校验由投影阶段执行。离线 /demo 页面仍仅演示规则。正式接入前用各源真实脱敏样本核对字段、状态枚举和计数口径。 ## 9. 成品线中继(relay-node,2026-09-29 新增) 部署在整机下线与码垛扫码之间的输送线扫码点,用于划分车间(OFFLINE→RELAY)与仓库(RELAY→PALLET→WMS)责任段。节点程序与协议见 `源代码/relay-node/README.md`。 - Topic 前缀 `fg/v1/{site}/{line}/relay-node/`,与码垛节点(`pallet-node/`)互不相通:`{node}/scans`(节点→Web,origin 恒为 live)、`{node}/ack`(Web→节点)、`all/request`(Web→节点,仅授时心跳,sns 恒为空)、`{node}/status`(节点→Web)。全部 QoS 1、非 retained。 - 日报查询(2026-09-30 新增):节点额外订阅 `all/report-request`、发布 `{node}/report`,字段与 pallet/wms 日报协议一致,`report` 只接受 `relay`;数据源为本机 outbox 留档(Ack 只改状态、记录永不删除),原始扫码字面透传。详见 `docs/daily-report-protocol.md`。 - Web 收到中继扫码后先落盘(`relay_scans`),等同产线同条码的 MES 下线身份到达后,提升为**通用节点事件**:`events/relay/RELAY`,event_type=`node.result`、result=success,继承该产品下线时的路线版本、工单号与产品编码;同一产品 10 分钟窗口内被多把枪读到只提升一次,原始扫码全部保留。 - 中继事件是来源凭证,不是扫码证据:中继来源无权声明 `scan.result / scan.missed / scan.rescan`(协议层按 source 校验拒绝);「下线后长时间无中继记录」由 Web 按路线 `timeout_seconds` 做超时推测(`timeout_RELAY`),推测与证据分开。 - 中继来源不发布 connector.health(无回补水位语义);Web 侧就绪判定 = 任一中继节点状态报文 ≤120 秒且 scanner_online=true 且待发送队列为空。设备离线或扫码枪断开期间只显示「等待来源记录」,不产生超时推测,避免把设备故障当成产品失踪。 - 路线 v3 在 OFFLINE 与 PALLET 之间插入必经节点 RELAY(source=relay),PALLET 前置改为 RELAY;OQC 旁路不变。在途产品绑定旧路线版本不受追溯;就餐/停线区段今后按新边(OFFLINE→RELAY、RELAY→PALLET)配置。