# ============================================================ # MES 集成桩(moduleId: integ-mes-stub, 可重生 ✅) # EX-05/09 最小切片:进程内 Mock MES,无真实车间接口。 # 下发:创建外部工单(幂等);报工:进度/完工回写镜像。 # ============================================================ from __future__ import annotations import json from datetime import datetime from pathlib import Path from typing import Any _MIRROR_PATH = Path(__file__).resolve().parents[1] / "data" / "mes_mirror.json" def default_mirror() -> dict[str, Any]: return { "system": "MES-MOCK", "plant": "CNWH", "updatedAt": datetime.now().strftime("%Y-%m-%d %H:%M"), "workOrders": [], # 外部工单 "reports": [], # 报工流水 "idempotency": {}, # key → externalWoId } class MockMesClient: """进程内 Mock MES:读写本地镜像,模拟下发与报工。""" def __init__(self, path: Path | None = None): self.path = path or _MIRROR_PATH self.path.parent.mkdir(parents=True, exist_ok=True) if not self.path.exists(): self._save(default_mirror()) def _load(self) -> dict: try: return json.loads(self.path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): data = default_mirror() self._save(data) return data def _save(self, data: dict) -> None: data["updatedAt"] = datetime.now().strftime("%Y-%m-%d %H:%M") self.path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") def status(self) -> dict: m = self._load() wos = m.get("workOrders") or [] return { "connected": True, "mode": "stub", "system": m.get("system"), "plant": m.get("plant"), "updatedAt": m.get("updatedAt"), "woCount": len(wos), "openCount": sum(1 for w in wos if w.get("status") != "COMPLETED"), "reportCount": len(m.get("reports") or []), } def create_work_order(self, payload: dict, idem_key: str) -> dict: """幂等下发:相同 idem_key 返回原外部工单。""" m = self._load() existing = (m.get("idempotency") or {}).get(idem_key) if existing: wo = next((w for w in m.get("workOrders", []) if w["id"] == existing), None) return {"duplicate": True, "externalWo": wo or {"id": existing, "idemKey": idem_key}} eid = f"MES-WO-{len(m.get('workOrders') or []) + 1:04d}" external = { "id": eid, "idemKey": idem_key, "plant": m.get("plant"), "at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "status": "RELEASED", "qtyDone": 0, "progressPct": 0, **payload, } m.setdefault("workOrders", []).append(external) m.setdefault("idempotency", {})[idem_key] = eid self._save(m) return {"duplicate": False, "externalWo": external} def post_report(self, external_wo_id: str, payload: dict) -> dict: """写入报工流水并更新外部工单进度。""" m = self._load() wo = next((w for w in m.get("workOrders", []) if w["id"] == external_wo_id), None) if not wo: raise ValueError(f"MES 无外部工单 {external_wo_id}") rid = f"MES-RPT-{len(m.get('reports') or []) + 1:04d}" report = { "id": rid, "externalWoId": external_wo_id, "at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), **payload, } m.setdefault("reports", []).append(report) if "qtyDone" in payload: wo["qtyDone"] = payload["qtyDone"] if "progressPct" in payload: wo["progressPct"] = payload["progressPct"] if payload.get("status"): wo["status"] = payload["status"] self._save(m) return {"report": report, "externalWo": wo} def reset(self) -> dict: data = default_mirror() self._save(data) return self.status() _client: MockMesClient | None = None def get_mes_client() -> MockMesClient: global _client if _client is None: _client = MockMesClient() return _client def reset_mes_client(path: Path | None = None) -> MockMesClient: global _client _client = MockMesClient(path) _client.reset() return _client