aps-agent/server/db/sync.py

270 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================
# 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",
"calendarTemplates", "calendarHolidays", "masterdataVersions",
# 订单(销售/预测属于可维护数据;生产/工单是排产产物不入库)
"salesOrders", "forecastOrders",
# 柔性轨主数据
"flexZones", "flexOperations", "flexEquipment", "flexMolds",
"flexMaterials", "flexBom", "flexRoutings", "flexTeams", "flexCalendar",
"flexOrders",
"flexCalendarOverrides", "flexFactoryResources", "flexPersonnel", "flexWip",
"flexMaintenance", "flexSandboxOrders", "flexPartners", "flexScenarios", "intakeSources",
)
# world 主数据键(字典型)→ project_settings
MASTER_DICT_KEYS: tuple[str, ...] = ("scheduleParams", "constraintProfile", "flexParams", "planningContext")
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:
# Empty is authoritative too: switching projects must not retain the
# previous project's calendar, people, WIP, or editable master rows.
world[key] = by_key.get(key, [])
if key in by_key:
counts[key] = len(world[key])
for key in MASTER_DICT_KEYS:
world[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()