From a68b2bbca0d01b619231591192249a8e1715979f Mon Sep 17 00:00:00 2001 From: duxingchen Date: Tue, 22 Sep 2026 13:09:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(webhook):=20=E9=83=A8=E9=97=A8=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=20=E2=80=94=E2=80=94=20=E5=8F=AA=E8=AE=A4=20company?= =?UTF-8?q?=5Fname=20=3D=3D=20"LICA"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MOM 现在同时对接 IRIS 与 LICA 两个 Track 实例,按载荷里的 company_name 分流。 本实例采取**严格白名单**: · company_name == "LICA"(strip 后)→ 正常处理 · 空白 / 缺失 / "IRIS" / 未知值 → 忽略 ⚠️ 与 IRIS 实例的策略**刻意相反**,别"顺手统一成一样": IRIS 对空白值要放行 —— MOM 判定不出公司时会回落到指向 IRIS 的扁平配置, 不收就彻底丢了。 LICA 没有兜底角色,空白值只可能来自「MOM 没判定出公司」,那本就该由 IRIS 兜。 **宁可漏,不可误收** —— 误收会把别的部门的设备状态改掉,那是数据污染, 比漏一条通知严重得多。 实现: · MomInboundPayload / MomOutboundPayload 补 company_name 字段 (不补的话会被 Pydantic 静默丢弃,校验无从谈起) · 新增 _belongs_to_this_org(),在**鉴权之后、匹配产品之前**拦截 · 拦截时返回 200 + matched=False + reason=org_mismatch —— 与「未命中」保持 同一契约,避免 MOM 侧把它当成故障去重试 · 顺带补上 outbound 一直在发、但此前被丢弃的 outbound_type 字段 实测: · 7 种 company_name 取值全部符合预期 ("LICA" / "LICA " 通过;"IRIS" / "" / 缺失 / null / "UNKNOWN" 全部忽略) · 无 X-API-Key 仍返回 401(鉴权没有被绕过) · 真实闭环:LICA 入库回调 → 产品「待仓库收货」→「已入库」✅ · 对照:对同一产品发 IRIS 的回调 → 状态纹丝不动 ✅ · 测试数据已还原为原始值 --- backend/app/api/v1/endpoints/webhooks.py | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/backend/app/api/v1/endpoints/webhooks.py b/backend/app/api/v1/endpoints/webhooks.py index 649e0e8..6d42b1b 100644 --- a/backend/app/api/v1/endpoints/webhooks.py +++ b/backend/app/api/v1/endpoints/webhooks.py @@ -6,6 +6,17 @@ MOM 仓储系统确认接收产品入库后,回调本接口,将 Track 中该 同一条入站通道还承担【撤回出库】的强制回滚:MOM 把误点出库的设备物理 回滚到仓库时,Track 必须被动跟随 MOM 的权威物理状态(详见 _mom_inbound_revoke 上方的特权通道说明)。 + +═══ 部门校验(本实例 = LICA)═══ + +MOM 现在同时对接 IRIS 与 LICA 两个 Track 实例,按载荷里的 `company_name` +分流。本实例采取**严格白名单**:只有 `company_name == "LICA"` 才处理, +空白 / 缺失 / "IRIS" / 未知值一律忽略。 + +这与 IRIS 实例的策略**刻意相反** —— IRIS 对空白值要放行,因为 MOM 判定不出 +公司时会回落到指向 IRIS 的扁平配置,不收就彻底丢了;而 LICA 没有兜底角色, +空白值本就不该由它承担。**宁可漏,不可误收**:误收会把别的部门的设备 +状态改掉,那是数据污染,比漏一条通知严重得多。 """ from __future__ import annotations @@ -36,6 +47,24 @@ class MomInboundPayload(BaseModel): event: str | None = None # 事件名,如 inbound.created / outbound.revoked action: str | None = None # 显式动作指令,如 revoke_outbound source_table: str | None = None # stock_product / stock_semi + # ↓ 2026-09 新增:MOM 用它区分这条业务属于哪个部门,本实例只认 "LICA"。 + # 不加这个字段的话会被 Pydantic 静默丢弃,部门校验就无从谈起。 + company_name: str | None = None # IRIS / LICA + + +def _belongs_to_this_org(company_name: str | None) -> bool: + """这条回调是不是发给本部门(LICA)的 —— **严格白名单**。 + + 只有明确写着 "LICA" 才返回 True;空白 / 缺失 / "IRIS" / 未知部门名 + 一律 False(忽略)。 + + ⚠️ 与 IRIS 实例的策略刻意相反,别"顺手统一": + IRIS 对空白值要放行 —— MOM 判定不出公司时会回落到指向 IRIS 的扁平 + 配置,不收就彻底丢了。 + LICA 没有兜底角色,空白值只可能来自"MOM 没判定出公司",那本就该由 + IRIS 兜。**宁可漏,不可误收**:误收会把别的部门的设备状态改掉。 + """ + return (company_name or "").strip() == settings.ORG_DEPARTMENT # 「撤回出库」信号词 —— 只在 action / event 里做子串匹配。 @@ -142,6 +171,14 @@ async def mom_inbound_webhook( if not settings.TRACK_WEBHOOK_KEY or x_api_key != settings.TRACK_WEBHOOK_KEY: raise HTTPException(status_code=401, detail="Unauthorized: invalid X-API-Key") + # ── 部门校验(纵深防御)── + # MOM 侧已按 company_name 分流,这里再挡一道:万一 MOM 路由写错、或有人拿 + # 旧配置直接打这个接口,也不会把别的部门的设备状态改掉。 + # 返回 200 + matched=False 而非 4xx —— 与「未命中」保持同一契约, + # 避免 MOM 侧把它当成故障去重试。 + if not _belongs_to_this_org(payload.company_name): + return {"ok": True, "matched": False, "reason": "org_mismatch"} + explicit_revoke = _is_outbound_revoke(payload) # ── 匹配产品 ── @@ -329,6 +366,9 @@ class MomOutboundPayload(BaseModel): sku: str | None = None # 规格型号 spec_model(serial 缺失时的兜底匹配) operator: str | None = None # 出库操作人(写入 task_logs.operator_id) outbound_time: datetime | None = None # 出库时间 + # ↓ 2026-09 新增:部门路由键,本实例只认 "LICA"(见 _belongs_to_this_org) + company_name: str | None = None # IRIS / LICA + outbound_type: str | None = None # SALES / PRODUCTION(MOM 一直在发,此前被丢弃) @router.post("/mom-outbound") @@ -348,6 +388,10 @@ async def mom_outbound_webhook( if not settings.TRACK_WEBHOOK_KEY or x_api_key != settings.TRACK_WEBHOOK_KEY: raise HTTPException(status_code=401, detail="Unauthorized: invalid X-API-Key") + # ── 部门校验(纵深防御)—— 同 mom_inbound,理由见该处注释 + if not _belongs_to_this_org(payload.company_name): + return {"ok": True, "matched": False, "reason": "org_mismatch"} + # ── 按 serial_number(优先)或 sku 匹配"在仓库/已入库"的产品 ── product = None where_cond = or_(