# ============================================================ # 世界状态存储器(moduleId: state-store, 可重生 ✅) # M1 形态:单文件 JSON + 原子写(tmp + os.replace),结构与 data.js 对齐 # 后续可整体替换为 PostgreSQL 实现而不影响上层(契约=方法签名) # ============================================================ from __future__ import annotations # 前向类型引用 import json # JSON 序列化 import os # 路径与原子替换 import tempfile # 原子写的临时文件 import threading # 进程内互斥锁 from typing import Any # 类型标注 from server.state.seed import seed_world # 种子数据工厂 class WorldStore: """世界状态存储器:加载/保存/重置/发号(进程内单例使用)。""" def __init__(self, path: str | None = None) -> None: """初始化:确定存储路径并加载(无文件则播种)。权力等级 P0(构造只读加载)。""" # 存储路径:优先环境变量,默认仓库内 server/data/world.json self.path = path or os.environ.get("APS_WORLD_PATH", "server/data/world.json") self._lock = threading.Lock() # 互斥锁:保护并发读写(FastAPI 多请求) self._counters: dict[str, int] = {} # ID 发号器(按类型独立计数) self.data: dict[str, Any] = self._load() # 加载世界状态(无文件时自动播种) self._reset_counters() # 依据现有数据校准发号器 # ---------------- 加载与持久化 ---------------- def _load(self) -> dict[str, Any]: """从磁盘加载世界状态;文件缺失/损坏时重新播种(P0 只读 + 自愈)。""" try: with open(self.path, "r", encoding="utf-8") as f: # 尝试读取现有文件 return json.load(f) # 解析 JSON except (FileNotFoundError, json.JSONDecodeError): # 缺失或损坏 data = seed_world() # 重新播种 self._write(data) # 立即落盘 return data # 返回新世界 def _write(self, data: dict[str, Any]) -> None: """原子写盘:先写临时文件再 os.replace(防止写一半崩溃损坏数据)。""" os.makedirs(os.path.dirname(self.path) or ".", exist_ok=True) # 确保目录存在 fd, tmp = tempfile.mkstemp(dir=os.path.dirname(self.path) or ".", suffix=".tmp") # 同目录临时文件(保证同盘 rename) try: with os.fdopen(fd, "w", encoding="utf-8") as f: # 打开临时文件 json.dump(data, f, ensure_ascii=False, indent=1) # 序列化(保留中文可读) os.replace(tmp, self.path) # 原子替换目标文件 except BaseException: # 任意失败 if os.path.exists(tmp): # 清理残留临时文件 os.unlink(tmp) raise # 上抛由调用方处理 def save(self) -> None: """持久化当前世界状态(权力等级 P2 动作的收尾必须调用)。""" with self._lock: # 串行化写盘 self._write(self.data) # 原子落盘 def reset(self) -> None: """重置为种子数据(危险操作,仅开发/演示用;调用方须走审计)。""" with self._lock: # 串行化 self.data = seed_world() # 重新播种 self._counters = {} # 清空发号器 self._write(self.data) # 落盘 self._reset_counters() # 重新校准发号器 def restore(self, world: dict[str, Any]) -> None: """整体替换世界状态(Checkpoint 回滚专用,P2 动作放行后调用,§4.3)。""" import copy # 局部导入(仅回滚用) with self._lock: # 串行化 self.data = copy.deepcopy(world) # 深拷贝隔离快照与运行态 self._write(self.data) # 落盘 self._reset_counters() # 按恢复后数据重新校准发号器(防重号) # ---------------- ID 发号(与 data.js resetCounters/nextId 对齐) ---------------- def _reset_counters(self) -> None: """扫描现有数据,把每类计数器校准到当前最大 ID(防重号)。""" scan_map = { # 类型 → 数据表 的映射 "salesOrder": "salesOrders", "productionOrder": "productionOrders", "workOrder": "workOrders", "scheduleVersion": "scheduleVersions", "conflict": "conflicts", "log": "logs", "audit": "auditEvents", "maintenance": "maintenance", # 维保计划(MD-03 主数据维护可新增) "purchaseOrder": "purchaseOrders", # 采购建议单(MRP 分解产物) "outsourceOrder": "outsourceOrders", # 委外建议单(MRP 分解产物) } for key, table in scan_map.items(): # 逐类型扫描 max_id = 0 # 当前最大 ID for item in self.data.get(table, []): # 遍历该表 if isinstance(item.get("id"), int): # 仅数值 ID 参与 max_id = max(max_id, item["id"]) # 更新最大值 self._counters[key] = max_id # 校准计数器 def next_id(self, kind: str) -> int: """取下一个 ID(进程内自增,类型隔离)。""" with self._lock: # 保护计数器 self._counters[kind] = self._counters.get(kind, 0) + 1 # 自增 return self._counters[kind] # 返回新号 # ---------------- 进程级单例(gateway 与引擎共用同一世界) ---------------- _store: WorldStore | None = None # 模块级单例槽 def get_store() -> WorldStore: """获取全局 WorldStore 单例(懒加载)。""" global _store # 引用模块级槽 if _store is None: # 首次调用 _store = WorldStore() # 创建实例(自动加载/播种) return _store # 返回单例