270 lines
11 KiB
Python
270 lines
11 KiB
Python
# ============================================================
|
||
# 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()
|