aps-agent/server/db/sync.py

263 lines
10 KiB
Python
Raw Normal View History

# ============================================================
# world ↔ SQLite 同步层(moduleId: db-sync, 可重生 ✅)
# 职责:
# 1. world_to_db:把 world 主数据键写入当前项目(替换式,主数据事实源=DB)
# 2. db_to_world:把当前项目主数据投影回 world dict(引擎零改动)
# 3. 项目管理:get/set_active_project(康尼、演示厂……多项目隔离)
# 排产运行态(版本/工单/冲突/日志/检查点)不入库,仍留 world.json。
# ============================================================
from __future__ import annotations
import json
from datetime import datetime
from typing import Any
from sqlalchemy import delete, select
from server.db.database import get_session
from server.auth.context import get_identity
# world 主数据键(列表型)——固定轨 + 柔性轨
MASTER_LIST_KEYS: tuple[str, ...] = (
# 固定轨主数据
"factories", "workshops", "lines", "workstations", "equipment",
"operations", "routings", "routingSteps", "materials", "boms", "bomItems",
"lineProducts", "workstationOperations", "teams", "shifts", "shiftCalendar",
"maintenance", "changeoverMatrix",
# 订单(销售/预测属于可维护数据;生产/工单是排产产物不入库)
"salesOrders", "forecastOrders",
# 柔性轨主数据
"flexZones", "flexOperations", "flexEquipment", "flexMolds",
"flexMaterials", "flexBom", "flexRoutings", "flexTeams", "flexCalendar",
"flexOrders",
)
# world 主数据键(字典型)→ project_settings
MASTER_DICT_KEYS: tuple[str, ...] = ("scheduleParams", "constraintProfile", "flexParams")
DEFAULT_PROJECT = "demo"
# ---------------- 项目管理 ----------------
def _ensure_project(session, code: str, name: str = "", industry: str = "machining"):
from server.db.models import Project
identity = get_identity()
proj = session.execute(select(Project).where(
Project.tenant_uuid == identity.tenant_uuid,
Project.code == code,
Project.deleted == 0,
)).scalar_one_or_none()
if proj is None:
proj = Project(
tenant_uuid=identity.tenant_uuid,
owner_user_id=identity.user_id or None,
creator_id=identity.user_id or None,
code=code,
name=name or code,
industry=industry,
active=False,
created_at=datetime.now().strftime("%Y-%m-%d %H:%M"),
)
session.add(proj)
session.flush()
return proj
def _workspace_state(session):
from server.db.models import UserWorkspace
identity = get_identity()
state = session.execute(select(UserWorkspace).where(
UserWorkspace.tenant_uuid == identity.tenant_uuid,
UserWorkspace.user_id == identity.user_id,
UserWorkspace.deleted == 0,
)).scalar_one_or_none()
if state is None:
state = UserWorkspace(
tenant_uuid=identity.tenant_uuid,
user_id=identity.user_id,
creator_id=identity.user_id or None,
active_project_id=DEFAULT_PROJECT,
created_at=datetime.now().strftime("%Y-%m-%d %H:%M"),
)
session.add(state)
session.flush()
return state
def get_active_project() -> dict[str, Any]:
"""返回激活项目(无则创建并激活 demo)。"""
from server.db.models import Project
identity = get_identity()
with get_session() as session:
state = _workspace_state(session)
code = state.active_project_id
if code in ("", "__personal__"):
code = DEFAULT_PROJECT
proj = session.execute(select(Project).where(
Project.tenant_uuid == identity.tenant_uuid,
Project.code == code,
Project.deleted == 0,
)).scalar_one_or_none()
if proj is None:
proj = _ensure_project(session, DEFAULT_PROJECT, "演示项目")
proj.active = True
state.active_project_id = proj.code
session.commit()
return {"id": proj.id, "code": proj.code, "name": proj.name, "industry": proj.industry}
def set_active_project(code: str, name: str = "", industry: str = "machining") -> dict[str, Any]:
"""切换/创建激活项目(导入现场数据时把康尼建成独立项目)。"""
from server.db.models import Project
identity = get_identity()
with get_session() as session:
proj = _ensure_project(session, code, name, industry)
for p in session.execute(select(Project).where(
Project.tenant_uuid == identity.tenant_uuid,
Project.deleted == 0,
)).scalars():
p.active = (p.id == proj.id)
_workspace_state(session).active_project_id = code
session.commit()
return {"id": proj.id, "code": proj.code, "name": proj.name, "industry": proj.industry}
def list_projects() -> list[dict[str, Any]]:
from server.db.models import Project
identity = get_identity()
with get_session() as session:
active = _workspace_state(session).active_project_id
rows = session.execute(select(Project).where(
Project.tenant_uuid == identity.tenant_uuid,
Project.deleted == 0,
).order_by(Project.id)).scalars().all()
return [{"id": p.id, "code": p.code, "name": p.name,
"industry": p.industry, "active": p.code == active} for p in rows]
# ---------------- world → DB ----------------
def world_to_db(world: dict[str, Any], project_code: str | None = None) -> dict[str, int]:
"""把 world 主数据替换式写入项目(返回各表行数)。"""
from server.db.models import MasterRecord, ProjectSetting
identity = get_identity()
proj = get_active_project() if project_code is None else set_active_project(project_code)
counts: dict[str, int] = {}
with get_session() as session:
session.execute(delete(MasterRecord).where(
MasterRecord.tenant_uuid == identity.tenant_uuid,
MasterRecord.project_id == proj["id"],
))
session.execute(delete(ProjectSetting).where(
ProjectSetting.tenant_uuid == identity.tenant_uuid,
ProjectSetting.project_id == proj["id"],
))
for key in MASTER_LIST_KEYS:
rows = world.get(key) or []
if not isinstance(rows, list):
continue
for i, rec in enumerate(rows):
if not isinstance(rec, dict):
continue
session.add(MasterRecord(
tenant_uuid=identity.tenant_uuid,
creator_id=identity.user_id or None,
project_id=proj["id"], table_key=key, seq=i,
rec_id=str(rec.get("id") or rec.get("code") or i),
code=str(rec.get("code") or rec.get("orderNo") or ""),
name=str(rec.get("name") or rec.get("productName") or ""),
payload=rec,
))
counts[key] = len(rows)
for key in MASTER_DICT_KEYS:
val = world.get(key)
if isinstance(val, dict) and val:
session.add(ProjectSetting(
tenant_uuid=identity.tenant_uuid,
creator_id=identity.user_id or None,
project_id=proj["id"], key=key, payload=val,
))
counts[key] = 1
session.commit()
return counts
# ---------------- DB → world 投影 ----------------
def db_has_master(project_code: str | None = None) -> bool:
"""当前(或指定)项目是否已有主数据。"""
from server.db.models import MasterRecord, Project
identity = get_identity()
with get_session() as session:
if project_code:
proj = session.execute(select(Project).where(
Project.tenant_uuid == identity.tenant_uuid,
Project.code == project_code,
Project.deleted == 0,
)).scalar_one_or_none()
if proj is None:
return False
pid = proj.id
else:
pid = get_active_project()["id"]
row = session.execute(
select(MasterRecord.id).where(
MasterRecord.tenant_uuid == identity.tenant_uuid,
MasterRecord.project_id == pid,
MasterRecord.deleted == 0,
).limit(1)
).first()
return row is not None
def db_to_world(world: dict[str, Any], project_code: str | None = None) -> dict[str, int]:
"""把项目主数据投影进 world dict(覆盖主数据键,保留运行态键)。"""
from server.db.models import MasterRecord, ProjectSetting, Project
identity = get_identity()
with get_session() as session:
if project_code:
proj_row = session.execute(select(Project).where(
Project.tenant_uuid == identity.tenant_uuid,
Project.code == project_code,
Project.deleted == 0,
)).scalar_one_or_none()
if proj_row is None:
return {}
pid = proj_row.id
else:
pid = get_active_project()["id"]
counts: dict[str, int] = {}
rows = session.execute(
select(MasterRecord).where(
MasterRecord.tenant_uuid == identity.tenant_uuid,
MasterRecord.project_id == pid,
MasterRecord.deleted == 0,
).order_by(MasterRecord.table_key, MasterRecord.seq)
).scalars().all()
by_key: dict[str, list] = {}
for r in rows:
by_key.setdefault(r.table_key, []).append(r.payload)
for key in MASTER_LIST_KEYS:
if key in by_key:
world[key] = by_key[key]
counts[key] = len(by_key[key])
for s in session.execute(select(ProjectSetting).where(
ProjectSetting.tenant_uuid == identity.tenant_uuid,
ProjectSetting.project_id == pid,
ProjectSetting.deleted == 0,
)).scalars():
if s.key in MASTER_DICT_KEYS and isinstance(s.payload, dict):
world[s.key] = s.payload
counts[s.key] = 1
return counts
def master_fingerprint(world: dict[str, Any]) -> str:
"""主数据指纹(store.save 判断是否需要回写 DB)。"""
import hashlib
h = hashlib.sha256()
for key in MASTER_LIST_KEYS + MASTER_DICT_KEYS:
val = world.get(key)
if val:
h.update(key.encode())
h.update(json.dumps(val, ensure_ascii=False, sort_keys=True, default=str).encode())
return h.hexdigest()