aps-agent/server/state/seed.py

615 lines
40 KiB
Python
Raw Normal View History

2026-07-21 11:05:57 +08:00
# ============================================================
# 种子数据工厂(moduleId: state-seed, 可重生 ✅)
# 从 legacy aps-frontend/js/data.js 的 seed() 按行移植:
# - 数据键名保持 camelCase 与 JS 版一致(前端/存储直接复用)
# - 固定日期改为"相对今天"的偏移(黄金测试与任意运行日都稳定)
# - Math.random 改为固定种子 Random(42)(可复现,§9.5.2 deterministic)
# ============================================================
from __future__ import annotations # 前向类型引用
import random # 标准随机库(用固定种子保证确定性)
from typing import Any # 类型标注
from server.timeutil import add_minutes, fmt_date, fmt_dt, today0 # 日期工具(与 data.js $utils 对齐)
# 演示厂假料号(加载项目世界时强制清掉,禁止冒充客户主数据)
_DEMO_MATERIAL_CODES = frozenset({
"CTRL-A", "CTRL-B", "CTRL-C",
"PCB-001", "PCB-002", "RC-001", "CASE-001", "SCR-001", "WIRE-001", "LABEL-001",
"HV-HARNESS", "PDU-UNIT", "CHARGE-GUN", "HV-CONN",
})
_DEMO_ORDER_PREFIXES = ("FO-26", "SO-DEMO")
def _want_demo_seed() -> bool:
import os
return os.environ.get("APS_SEED_DEMO", "").strip().lower() in ("1", "true", "yes", "on")
def empty_world() -> dict[str, Any]:
"""空项目世界:仅骨架与默认班次,不含任何演示产品/产线/工艺。"""
data: dict[str, Any] = {
2026-07-21 11:05:57 +08:00
"factories": [], "workshops": [], "lines": [], "workstations": [],
"operations": [], "routings": [], "routingSteps": [], "materials": [],
"boms": [], "bomItems": [], "lineProducts": [], "workstationOperations": [],
"equipment": [], "teams": [], "shifts": [], "shiftCalendar": [],
"maintenance": [], "salesOrders": [], "productionOrders": [], "workOrders": [],
"purchaseOrders": [], "outsourceOrders": [],
"forecastOrders": [],
"changeoverMatrix": [],
2026-07-21 11:05:57 +08:00
"scheduleVersions": [], "conflicts": [], "logs": [], "auditEvents": [],
"scheduleParams": {
"defaultEngine": "RULE",
"planningHorizonDays": 14,
"timeGranularityMinutes": 15,
"deliveryBufferRatio": 0.95,
"weights": {"tardiness": 0.4, "cost": 0.3, "utilization": 0.2, "balance": 0.1},
"customerLevelWeights": {"VIP": 3.0, "A": 2.0, "B": 1.0, "C": 1.0},
"freezeWindowHours": 24,
2026-07-21 11:05:57 +08:00
},
"constraintProfile": {
"profileId": "default", "name": "默认约束剖面", "constraints": {},
},
"flexZones": [], "flexOperations": [], "flexEquipment": [], "flexMolds": [],
"flexMaterials": [], "flexRoutings": [], "flexBom": [], "flexOrders": [],
"flexTeams": [],
"flexCalendar": [{
"shiftCode": "D", "startTime": "08:00", "endTime": "17:00",
"breaks": [{"start": "12:00", "end": "13:00"}],
"workdays": [1, 2, 3, 4, 5],
}],
"flexParams": {
"sortMode": "BOTTLENECK",
"beforeDays": 0, "afterDays": 30,
"rollingWindows": {"realtime": "60m", "long": "7d", "mid": "2d", "short": "2h"},
"mrpControllerDays": 3,
"weights": {"tardiness": 0.4, "cost": 0.3, "utilization": 0.2, "balance": 0.1},
"demoDataCleared": True, # 禁止 ensure_flex_seed 回填假数据
},
"flexScheduleVersions": [], "flexVirtualLines": [],
"flexWorkOrders": [], "flexConflicts": [],
}
return data
def seed_world() -> dict[str, Any]:
"""世界播种入口(权力等级 P0:纯构造,不落盘)。
- 默认:空世界(以用户/工程目录真实数据为准,不造假)
- APS_SEED_DEMO=1:演示厂(仅测试/本地演示)
- APS_SEED_PACK:重放客户/行业数据包
"""
import os
pack_path = os.environ.get("APS_SEED_PACK")
if pack_path and os.path.exists(pack_path):
from server.state.packs import load_pack
world = load_pack(pack_path)
if world.get("materials") or world.get("flexOrders"):
return world
if _want_demo_seed():
return build_demo_world()
return empty_world()
def purge_demo_residue(data: dict[str, Any]) -> bool:
"""清掉世界里残留的演示厂主数据(智能控制器等)。
只动「经典主数据 / 纯演示柔性」;绝不因为有 CTRL-* 就清空现场 SQL 导入的 flex*。
"""
changed = False
params = data.setdefault("flexParams", {})
mat_codes = {m.get("code") for m in (data.get("materials") or [])}
has_demo_classic = bool(mat_codes & _DEMO_MATERIAL_CODES) or any(
(f.get("name") or "") == "演示工厂" for f in (data.get("factories") or [])
)
if has_demo_classic:
# 只剥演示经典表;现场 flex* 原样保留
for k in (
"factories", "workshops", "lines", "workstations", "equipment",
"operations", "routings", "routingSteps", "materials",
"boms", "bomItems", "lineProducts", "workstationOperations",
"changeoverMatrix", "teams", "shifts", "shiftCalendar", "maintenance",
"salesOrders", "productionOrders", "workOrders",
"purchaseOrders", "outsourceOrders", "forecastOrders",
"scheduleVersions", "conflicts",
):
if data.get(k):
data[k] = []
changed = True
params["demoDataCleared"] = True
# 柔性:仅当「全部」物料/订单都是演示种子时才清(子集判定,避免误伤现场库)
flex_codes = {m.get("code") for m in (data.get("flexMaterials") or []) if m.get("code")}
flex_orders = data.get("flexOrders") or []
pure_demo_flex = bool(flex_codes) and flex_codes <= _DEMO_MATERIAL_CODES
pure_demo_orders = bool(flex_orders) and all(
str(o.get("orderNo") or "").startswith(_DEMO_ORDER_PREFIXES) for o in flex_orders
)
if (pure_demo_flex or pure_demo_orders) and not params.get("siteProfile"):
# 若同时已有明显非演示订单号,跳过(混合世界保现场)
if flex_orders and not pure_demo_orders:
pass
elif flex_codes and not pure_demo_flex:
pass
else:
for k in (
"flexZones", "flexOperations", "flexEquipment", "flexMolds",
"flexMaterials", "flexRoutings", "flexBom", "flexOrders", "flexTeams",
"flexScheduleVersions", "flexVirtualLines", "flexWorkOrders", "flexConflicts",
):
if data.get(k):
data[k] = []
changed = True
params["demoDataCleared"] = True
if changed:
params["demoDataCleared"] = True
if not data.get("flexCalendar"):
data["flexCalendar"] = [{
"shiftCode": "D", "startTime": "08:00", "endTime": "17:00",
"breaks": [{"start": "12:00", "end": "13:00"}],
"workdays": [1, 2, 3, 4, 5],
}]
return changed
def build_demo_world() -> dict[str, Any]:
"""生成一套自洽的演示工厂数据(也是 packs/demo.json 数据包的生成源)。
仅 APS_SEED_DEMO=1 或显式调用时使用;生产默认走 empty_world。"""
rng = random.Random(42) # 固定种子:设备参数可复现
base = today0() # 相对基准:今天零点
data: dict[str, Any] = empty_world()
data["flexParams"] = {
"sortMode": "BOTTLENECK",
"beforeDays": 0, "afterDays": 30,
"rollingWindows": {"realtime": "60m", "long": "7d", "mid": "2d", "short": "2h"},
"mrpControllerDays": 3,
"weights": {"tardiness": 0.4, "cost": 0.3, "utilization": 0.2, "balance": 0.1},
"demoDataCleared": False,
2026-07-21 11:05:57 +08:00
}
# ---- 工厂与车间 ----
data["factories"].append({"id": 1, "code": "F001", "name": "演示工厂", "timezone": "Asia/Shanghai", "address": "演示园区", "status": "ACTIVE"}) # 单工厂(示例数据,非真实客户)
data["workshops"].append({"id": 1, "factoryId": 1, "code": "WS01", "name": "一车间", "status": "ACTIVE"}) # 车间 1
data["workshops"].append({"id": 2, "factoryId": 1, "code": "WS02", "name": "二车间", "status": "ACTIVE"}) # 车间 2
# ---- 产线(3 条,含效率因子与替代产线) ----
data["lines"].append({"id": 1, "workshopId": 1, "code": "L001", "name": "装配产线A", "capacityPerDay": 1000, "taktTime": 0.5, "efficiencyFactor": 1.0, "status": "ACTIVE", "alternativeLineIds": [3]})
data["lines"].append({"id": 2, "workshopId": 1, "code": "L002", "name": "装配产线B", "capacityPerDay": 800, "taktTime": 0.6, "efficiencyFactor": 0.95, "status": "ACTIVE", "alternativeLineIds": [1]})
data["lines"].append({"id": 3, "workshopId": 2, "code": "L003", "name": "装配产线C", "capacityPerDay": 600, "taktTime": 0.8, "efficiencyFactor": 0.9, "status": "ACTIVE", "alternativeLineIds": [1]})
# ---- 工位(13 个:产线, 编码, 名称, 顺序号) ----
ws_rows = [
(1, "WS001", "SMT-01", 1), (1, "WS002", "DIP-01", 2), (1, "WS003", "ASSY-01", 3), (1, "WS004", "TEST-01", 4), (1, "WS005", "PKG-01", 5),
(2, "WS006", "SMT-02", 1), (2, "WS007", "ASSY-02", 2), (2, "WS008", "TEST-02", 3), (2, "WS009", "PKG-02", 4),
(3, "WS010", "SMT-03", 1), (3, "WS011", "DIP-03", 2), (3, "WS012", "TEST-03", 3), (3, "WS013", "PKG-03", 4),
]
for i, (line_id, code, name, seq) in enumerate(ws_rows): # 逐行展开为工位记录
data["workstations"].append({"id": i + 1, "lineId": line_id, "code": code, "name": name, "sequenceNo": seq, "status": "ACTIVE"})
# ---- 工序(5 道) ----
data["operations"] += [
{"id": 1, "code": "OP10", "name": "SMT贴片", "type": "INTERNAL", "standardTime": 0.5},
{"id": 2, "code": "OP20", "name": "DIP插件", "type": "INTERNAL", "standardTime": 0.8},
{"id": 3, "code": "OP30", "name": "组装", "type": "INTERNAL", "standardTime": 1.0},
{"id": 4, "code": "OP40", "name": "测试", "type": "INTERNAL", "standardTime": 0.6},
{"id": 5, "code": "OP50", "name": "包装", "type": "INTERNAL", "standardTime": 0.3},
]
# ---- 工艺路线(3 个产品各一条默认路线) ----
data["routings"] += [
{"id": 1, "productId": 1, "version": "V1.0", "versionName": "控制器A工艺V1.0", "isDefault": True, "status": "ACTIVE"},
{"id": 2, "productId": 2, "version": "V1.0", "versionName": "控制器B工艺V1.0", "isDefault": True, "status": "ACTIVE"},
{"id": 3, "productId": 3, "version": "V1.0", "versionName": "控制器C工艺V1.0", "isDefault": True, "status": "ACTIVE"},
]
# ---- 工艺步骤(routingId, operationId, 顺序, 前序, 准备分钟, 单件分钟, 等待, 转移, 是否外协) ----
step_rows = [
(1, 1, 1, None, 30, 0.5, 0, 10, False), (1, 2, 2, 1, 30, 0.8, 0, 10, False), (1, 3, 3, 2, 20, 1.0, 0, 10, False), (1, 4, 4, 3, 20, 0.6, 0, 10, False), (1, 5, 5, 4, 10, 0.3, 0, 10, False),
(2, 1, 1, None, 30, 0.4, 0, 10, False), (2, 3, 2, 1, 20, 0.9, 0, 10, False), (2, 4, 3, 2, 20, 0.5, 0, 10, False), (2, 5, 4, 3, 10, 0.3, 0, 10, False),
(3, 1, 1, None, 30, 0.6, 0, 10, False), (3, 2, 2, 1, 30, 1.0, 0, 10, False), (3, 4, 3, 2, 20, 0.7, 0, 10, False), (3, 5, 4, 3, 10, 0.3, 0, 10, False),
]
for i, (rid, oid, seq, prev, setup, run, wait, transfer, ext) in enumerate(step_rows): # 展开为步骤记录
data["routingSteps"].append({"id": i + 1, "routingId": rid, "operationId": oid, "sequenceNo": seq, "prevStepId": prev,
"setupTime": setup, "runTimePerUnit": run, "waitTime": wait, "transferTime": transfer, "isExternal": ext})
# ---- 物料(3 成品 + 7 原材料,库存/在途决定齐套结果) ----
data["materials"] += [
{"id": 1, "code": "CTRL-A", "name": "智能控制器A型", "spec": "标准版", "type": "FINISHED_PRODUCT", "unit": "件", "productFamily": "CTRL-STD", "safetyStock": 50, "procurementLeadTime": 0, "stock": 20, "inTransit": 0},
{"id": 2, "code": "CTRL-B", "name": "智能控制器B型", "spec": "经济版", "type": "FINISHED_PRODUCT", "unit": "件", "productFamily": "CTRL-STD", "safetyStock": 50, "procurementLeadTime": 0, "stock": 30, "inTransit": 0},
{"id": 3, "code": "CTRL-C", "name": "智能控制器C型", "spec": "高频版", "type": "FINISHED_PRODUCT", "unit": "件", "productFamily": "CTRL-HF", "safetyStock": 30, "procurementLeadTime": 0, "stock": 10, "inTransit": 0},
2026-07-21 11:05:57 +08:00
{"id": 10, "code": "PCB-001", "name": "标准PCB板", "spec": "FR-4", "type": "RAW_MATERIAL", "unit": "块", "safetyStock": 500, "procurementLeadTime": 7, "stock": 1800, "inTransit": 500},
{"id": 11, "code": "RC-001", "name": "电阻电容包", "spec": "混合包", "type": "RAW_MATERIAL", "unit": "包", "safetyStock": 300, "procurementLeadTime": 5, "stock": 900, "inTransit": 300},
{"id": 12, "code": "CASE-001", "name": "塑料外壳", "spec": "白色", "type": "RAW_MATERIAL", "unit": "个", "safetyStock": 400, "procurementLeadTime": 10, "stock": 1200, "inTransit": 0},
{"id": 13, "code": "SCR-001", "name": "螺丝包", "spec": "M3", "type": "RAW_MATERIAL", "unit": "包", "safetyStock": 200, "procurementLeadTime": 3, "stock": 800, "inTransit": 0},
{"id": 14, "code": "WIRE-001", "name": "线材", "spec": "22AWG", "type": "RAW_MATERIAL", "unit": "米", "safetyStock": 1000, "procurementLeadTime": 4, "stock": 3500, "inTransit": 0},
{"id": 15, "code": "LABEL-001", "name": "标签", "spec": "防伪", "type": "RAW_MATERIAL", "unit": "张", "safetyStock": 500, "procurementLeadTime": 2, "stock": 2500, "inTransit": 0},
{"id": 16, "code": "PCB-002", "name": "高频PCB板", "spec": "高频", "type": "RAW_MATERIAL", "unit": "块", "safetyStock": 200, "procurementLeadTime": 10, "stock": 350, "inTransit": 200},
]
# ---- MD-06 换型矩阵(产品族 × 产品族,分钟)----
data["changeoverMatrix"] += [
{"id": 1, "fromFamily": "CTRL-STD", "toFamily": "CTRL-HF", "setupMinutes": 45, "note": "标准→高频:换贴片程序+治具"},
{"id": 2, "fromFamily": "CTRL-HF", "toFamily": "CTRL-STD", "setupMinutes": 40, "note": "高频→标准:回退程序"},
]
2026-07-21 11:05:57 +08:00
# ---- BOM(3 个产品各一版默认 BOM) ----
data["boms"] += [
{"id": 1, "productId": 1, "version": "V2.1", "versionName": "A型BOM V2.1", "isDefault": True, "status": "ACTIVE"},
{"id": 2, "productId": 2, "version": "V1.0", "versionName": "B型BOM V1.0", "isDefault": True, "status": "ACTIVE"},
{"id": 3, "productId": 3, "version": "V1.0", "versionName": "C型BOM V1.0", "isDefault": True, "status": "ACTIVE"},
]
# ---- BOM 明细(bomId, materialId, 单件用量, 消耗工序, 是否关键料) ----
bom_rows = [
(1, 10, 1, 1, False), (1, 11, 1, 1, False), (1, 12, 1, 3, False), (1, 13, 1, 3, False), (1, 14, 0.5, 3, False), (1, 15, 1, 5, False),
(2, 10, 1, 1, False), (2, 11, 1.2, 1, False), (2, 12, 1, 3, False), (2, 13, 1, 3, False), (2, 14, 0.6, 3, False), (2, 15, 1, 5, False),
(3, 16, 1, 1, True), (3, 11, 1.5, 1, False), (3, 12, 1, 3, False), (3, 13, 1, 3, False), (3, 14, 0.4, 3, False), (3, 15, 1, 5, False),
]
for i, (bid, mid, qty, oid, key) in enumerate(bom_rows): # 展开为 BOM 明细
data["bomItems"].append({"id": i + 1, "bomId": bid, "materialId": mid, "quantity": qty, "operationId": oid, "isKeyMaterial": key})
# ---- 产线-产品配置(选线依据:priority 越小越优先) ----
data["lineProducts"] += [
{"id": 1, "lineId": 1, "productId": 1, "standardCapacity": 1000, "priority": 1, "setupTime": 30},
{"id": 2, "lineId": 1, "productId": 3, "standardCapacity": 600, "priority": 2, "setupTime": 40},
{"id": 3, "lineId": 2, "productId": 2, "standardCapacity": 800, "priority": 1, "setupTime": 30},
{"id": 4, "lineId": 2, "productId": 1, "standardCapacity": 700, "priority": 2, "setupTime": 40},
{"id": 5, "lineId": 3, "productId": 1, "standardCapacity": 500, "priority": 3, "setupTime": 45},
{"id": 6, "lineId": 3, "productId": 3, "standardCapacity": 600, "priority": 1, "setupTime": 30},
]
# ---- 工位-工序配置(工位能做哪道工序) ----
wso_rows = [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 1), (7, 3), (8, 4), (9, 5), (10, 1), (11, 2), (12, 4), (13, 5)]
for i, (wsid, oid) in enumerate(wso_rows): # 展开为工位工序记录
data["workstationOperations"].append({"id": i + 1, "workstationId": wsid, "operationId": oid, "setupTime": 20, "runTimePerUnit": 0.5, "isPrimary": True})
# ---- 设备(每工位一台,参数用固定种子随机→确定性) ----
for i, ws in enumerate(data["workstations"]): # 遍历工位生成设备
data["equipment"].append({
"id": i + 1, "code": f"EQ{i + 1:03d}", "name": ws["name"] + "设备", "model": "MDL-" + ws["code"],
"workstationId": ws["id"], "capacityPerHour": 120,
"efficiencyFactor": round(0.95 + rng.random() * 0.05, 3), # 效率因子(确定性随机)
"availabilityRate": round(0.92 + rng.random() * 0.07, 3), # 可动率(确定性随机)
"status": "MAINTENANCE" if rng.random() > 0.9 else "RUNNING", # 少数设备处于保养态
})
# ---- 班组(6 个) ----
data["teams"] += [
{"id": 1, "code": "T001", "name": "贴片班组A", "workshopId": 1, "memberCount": 12, "skillLevel": "L3", "status": "ACTIVE"},
{"id": 2, "code": "T002", "name": "插件班组A", "workshopId": 1, "memberCount": 8, "skillLevel": "L2", "status": "ACTIVE"},
{"id": 3, "code": "T003", "name": "组装班组A", "workshopId": 1, "memberCount": 15, "skillLevel": "L3", "status": "ACTIVE"},
{"id": 4, "code": "T004", "name": "测试班组A", "workshopId": 1, "memberCount": 10, "skillLevel": "L4", "status": "ACTIVE"},
{"id": 5, "code": "T005", "name": "包装班组A", "workshopId": 1, "memberCount": 8, "skillLevel": "L2", "status": "ACTIVE"},
{"id": 6, "code": "T006", "name": "综合班组B", "workshopId": 1, "memberCount": 20, "skillLevel": "L3", "status": "ACTIVE"},
]
# ---- 班次(早班/中班,含休息段) ----
data["shifts"] += [
{"id": 1, "code": "D", "name": "早班", "startTime": "08:00", "endTime": "16:00", "breakPeriods": [{"start": "12:00", "end": "13:00"}], "isOvertime": False, "status": "ACTIVE"},
{"id": 2, "code": "N", "name": "中班", "startTime": "16:00", "endTime": "23:59", "breakPeriods": [{"start": "18:00", "end": "18:30"}], "isOvertime": False, "status": "ACTIVE"},
]
# ---- 班次日历(未来 30 天 × 产线 × 班次;周末不排班;覆盖引擎 30 天占槽窗口) ----
for i in range(30): # 未来 30 天
date = add_minutes(base, i * 24 * 60) # 第 i 天
date_str = fmt_date(date) # 日期字符串
is_weekend = date.weekday() >= 5 # 周六(5)/周日(6)
for line in data["lines"]: # 每条产线
for shift in data["shifts"]: # 每个班次
data["shiftCalendar"].append({
"id": len(data["shiftCalendar"]) + 1, # 顺序 ID
"lineId": line["id"], "date": date_str, "shiftId": shift["id"],
"isWorking": not is_weekend, # 周末停排
"teamId": 6 if line["id"] == 2 else (line["id"] if shift["id"] == 1 else 6), # 班组指派(演示口径)
"maxWorkers": 10, # 班次容量
})
# ---- 维保计划(第 3 天上午 ASSY-01 设备保养 → 制造设备冲突场景) ----
mnt_day = fmt_date(add_minutes(base, 2 * 24 * 60)) # 后天
data["maintenance"].append({"id": 1, "equipmentId": 3, "type": "MAINTENANCE",
"plannedStart": mnt_day + " 09:00", "plannedEnd": mnt_day + " 12:00",
"status": "PLANNED", "description": "ASSY-01 季度保养"})
# ---- 销售订单(7 张;日期相对今天偏移,保证任意运行日行为一致) ----
# 行结构:(客户, 等级, 下单偏移天, 交期偏移天, 优先级, 状态, 是否插单, 产品ID, 数量)
order_rows = [
("青岛科技", "A", -2, 9, 3, "APPROVED", False, 1, 500),
("海尔智家", "VIP", -3, 6, 1, "APPROVED", False, 2, 300),
("海信电子", "B", -2, 8, 5, "APPROVED", False, 1, 800),
("中车集团", "A", -4, 12, 4, "APPROVED", False, 3, 400),
("北汽新能源", "C", -1, 14, 8, "APPROVED", False, 2, 600),
("比亚迪", "VIP", -1, 4, 1, "APPROVED", True, 3, 200),
("青岛科技", "A", 0, 10, 3, "APPROVED", False, 2, 500),
2026-07-21 11:05:57 +08:00
]
for i, (cust, level, o_off, d_off, prio, status, rush, pid, qty) in enumerate(order_rows): # 展开为订单
order_date = fmt_date(add_minutes(base, o_off * 24 * 60)) # 下单日期(相对偏移)
delivery = fmt_date(add_minutes(base, d_off * 24 * 60)) # 交期(相对偏移)
product = next(m for m in data["materials"] if m["id"] == pid) # 关联产品
data["salesOrders"].append({
"id": i + 1,
"orderNo": "SO" + base.strftime("%Y%m%d") + f"{i + 1:03d}", # 订单号:SO+今天+序号
"customerId": f"CUST{i + 1:03d}", "customerName": cust, "customerLevel": level,
"orderDate": order_date, "deliveryDate": delivery,
"priority": prio, "manualPriority": None, "status": status,
"source": "MANUAL" if i % 2 == 0 else "API", # 来源交替(演示口径)
"specialRequirements": "需使用环保材料包装" if i == 0 else "",
"totalAmount": qty * (100 + (i * 37) % 200), # 金额(确定性伪随机)
"isRush": rush, "rushStrategy": "STRATEGY_SHIFT" if rush else None,
"changes": [], "createdBy": "admin",
"createdAt": order_date + " 09:00", "updatedAt": order_date + " 09:00",
"items": [{ # 单行订单项(M1 保持一单一项)
"id": i * 10 + 1, "orderId": i + 1, "lineNo": 1,
"productId": pid, "productName": product["name"], "productCode": product["code"],
"quantity": qty, "unit": "件", "bomVersion": "V1.0", "routingVersion": "V1.0",
"status": "PENDING", "note": "",
}],
})
# ---- OR-05 预测订单(长周期需求,默认不进正式排产)----
fc_rows = [
(1, 400, 14, 20, "WEEK", 0.75, "ACTIVE"),
(2, 300, 21, 27, "WEEK", 0.6, "ACTIVE"),
(3, 200, 30, 59, "MONTH", 0.5, "DRAFT"),
]
for i, (pid, qty, s_off, e_off, bucket, conf, st) in enumerate(fc_rows):
product = next(m for m in data["materials"] if m["id"] == pid)
ps = fmt_date(add_minutes(base, s_off * 24 * 60))
pe = fmt_date(add_minutes(base, e_off * 24 * 60))
data["forecastOrders"].append({
"id": i + 1,
"forecastNo": "FC" + base.strftime("%Y%m%d") + f"{i + 1:03d}",
"productId": pid, "productCode": product["code"], "productName": product["name"],
"quantity": qty, "unit": "件", "bucket": bucket,
"periodStart": ps, "periodEnd": pe, "dueDate": pe,
"confidence": conf, "status": st,
"customerName": "预测需求", "customerLevel": "C",
"note": "种子预测", "source": "SEED",
"createdAt": fmt_dt(base), "updatedAt": fmt_dt(base), "convertedOrderNo": None,
})
2026-07-21 11:05:57 +08:00
# ---- 初始日志(审计雏形 §3.6:系统初始化事件) ----
data["logs"].append({"id": 1, "type": "INFO", "category": "SCHEDULE", "operationType": "SYSTEM",
"targetType": "SCHEDULE", "targetId": 1, "action": "系统初始化",
"description": "种子数据生成完成", "operator": "system", "createdAt": fmt_dt(base)})
# ============================================================
# 柔性排产层(flex*):设备能力池 + 虚拟产线(吸收 demand/ 康尼芜湖方案)
# 与固定产线层并行、独立数据键;不进 RULE 引擎与既有黄金测试计数。
# 字段口径见 docs/product/demand-data-intake.md。案例形态,非绑定真实客户。
# ============================================================
_seed_flex(data, base)
2026-07-21 11:05:57 +08:00
return data # 返回完整世界状态
def ensure_flex_seed(data: dict[str, Any]) -> bool:
"""幂等补种:仅在显式演示模式下,给缺柔性层的旧世界补演示 flex*。
默认(空世界 / 现场项目)绝不回填 HV/PDU 等假数据。
"""
if data.get("flexOrders") or data.get("flexEquipment") or data.get("flexRoutings"):
return False
params = data.get("flexParams") or {}
if params.get("demoDataCleared") or params.get("siteProfile"):
return False
if not _want_demo_seed():
# 只保证键存在,不造业务数据
for k in (
"flexZones", "flexOperations", "flexEquipment", "flexMolds",
"flexMaterials", "flexRoutings", "flexBom", "flexOrders", "flexTeams",
"flexScheduleVersions", "flexVirtualLines", "flexWorkOrders", "flexConflicts",
):
data.setdefault(k, [])
if not data.get("flexCalendar"):
data["flexCalendar"] = [{
"shiftCode": "D", "startTime": "08:00", "endTime": "17:00",
"breaks": [{"start": "12:00", "end": "13:00"}],
"workdays": [1, 2, 3, 4, 5],
}]
data.setdefault("flexParams", {})["demoDataCleared"] = True
return False
_seed_flex(data, today0())
return True
def _seed_flex(data: dict[str, Any], base) -> None:
"""柔性资源模型演示数据(吸收 demand/ 康尼芜湖方案 V1.0)。
4 类共线产品(高压线束总成 / PDU 配电单元 / 充电枪总成 / 高压连接器),
7 个能力池(下线·压接·装配·测试·焊接·气密·包装),压接机 4 台可跨区调配。
案例形态,非绑定真实客户;字段口径见 docs/product/demand-data-intake.md。
"""
# ---- 布局区域(U 型线束区 / 直线 PDU 区 / ESD 充电枪区 / 公共测试包装区) ----
data["flexZones"] = [
{"code": "ZONE-A", "name": "线束区(U型)"},
{"code": "ZONE-B", "name": "PDU区(直线)"},
{"code": "ZONE-D", "name": "充电枪区(ESD)"},
{"code": "ZONE-C", "name": "公共测试与包装区"},
]
# ---- 工序(含瓶颈标记 + 换型时间;四类产品各自瓶颈见 demand §6.1) ----
data["flexOperations"] = [
{"code": "OP-IQC", "name": "来料检验", "isBottleneck": False, "changeoverMin": 0},
{"code": "OP-CUT", "name": "下线剥皮", "isBottleneck": False, "changeoverMin": 10},
{"code": "OP-CRIMP", "name": "压接端子", "isBottleneck": True, "changeoverMin": 20}, # 线束瓶颈
{"code": "OP-WELD", "name": "激光焊接", "isBottleneck": True, "changeoverMin": 25}, # PDU 瓶颈(全厂最紧)
{"code": "OP-LEAK", "name": "气密检漏", "isBottleneck": False, "changeoverMin": 10},
{"code": "OP-ESD", "name": "控制盒装配(ESD)", "isBottleneck": True, "changeoverMin": 15}, # 充电枪瓶颈
{"code": "OP-PULL", "name": "插拔力测试", "isBottleneck": True, "changeoverMin": 5}, # 连接器瓶颈
{"code": "OP-ASSY", "name": "装配", "isBottleneck": False, "changeoverMin": 15},
{"code": "OP-TEST", "name": "综合测试", "isBottleneck": False, "changeoverMin": 5},
{"code": "OP-PKG", "name": "贴标包装", "isBottleneck": False, "changeoverMin": 5},
]
# ---- 设备(能力集 + 可移动 + 当前区域 + 适配模具 + 可动率),对齐 demand §3.2 产能池 ----
# 行:(编码, 名称, 能力[工序:单件分钟...], 可移动, 移动耗时, 区域, 适配模具, 可动率)
eq_rows = [
# 线束区:2 下线机 + 4 压接机(可移动) + 2 装配台
("CUT-01", "全自动下线机#1", {"OP-CUT": 2}, False, 0, "ZONE-A", [], 0.95),
("CUT-02", "全自动下线机#2", {"OP-CUT": 2}, False, 0, "ZONE-A", [], 0.95),
("PRESS-01", "气动压接机#1", {"OP-CRIMP": 1}, True, 30, "ZONE-A", ["MOLD-HV-01", "MOLD-HV-02", "MOLD-HV-03", "MOLD-CG-01"], 0.94),
("PRESS-02", "气动压接机#2", {"OP-CRIMP": 1}, True, 30, "ZONE-A", ["MOLD-HV-01", "MOLD-HV-02", "MOLD-CG-01", "MOLD-CG-02"], 0.94),
("PRESS-03", "气动压接机#3", {"OP-CRIMP": 1}, True, 30, "ZONE-A", ["MOLD-HV-01", "MOLD-HV-03", "MOLD-CG-01"], 0.93),
("PRESS-04", "气动压接机#4", {"OP-CRIMP": 1}, True, 30, "ZONE-A", ["MOLD-HV-02", "MOLD-CG-01", "MOLD-CG-02"], 0.93),
("ASSY-01", "装配台#1", {"OP-ASSY": 5}, False, 0, "ZONE-A", [], 0.96),
("ASSY-02", "装配台#2", {"OP-ASSY": 5}, False, 0, "ZONE-A", [], 0.96),
# PDU 区:激光焊接机 ×1 + 氦质谱/气密检测 ×2 + 装配台 ×1
("WELD-01", "激光焊接机#1", {"OP-WELD": 5}, False, 0, "ZONE-B", [], 0.92),
("LEAK-01", "氦质谱检漏仪#1", {"OP-LEAK": 4}, False, 0, "ZONE-B", [], 0.95),
("LEAK-02", "气密检测仪#1", {"OP-LEAK": 4}, False, 0, "ZONE-B", [], 0.95),
("ASSY-03", "装配台#3", {"OP-ASSY": 5}, False, 0, "ZONE-B", [], 0.96),
# 充电枪区:ESD 控制盒装配台 ×2
("ESD-01", "ESD控制盒装配台#1", {"OP-ESD": 5}, False, 0, "ZONE-D", [], 0.95),
("ESD-02", "ESD控制盒装配台#2", {"OP-ESD": 5}, False, 0, "ZONE-D", [], 0.95),
# 公共测试与包装区
("IQC-01", "来料检验台#1", {"OP-IQC": 3}, False, 0, "ZONE-C", [], 0.98),
("TEST-01", "高压测试台#1", {"OP-TEST": 2}, False, 0, "ZONE-C", [], 0.97),
("TEST-02", "高压测试台#2", {"OP-TEST": 2}, False, 0, "ZONE-C", [], 0.97),
("TEST-03", "PDU测试台#1", {"OP-TEST": 3}, False, 0, "ZONE-C", [], 0.97),
("TEST-04", "充电枪测试台#1", {"OP-TEST": 3}, False, 0, "ZONE-C", [], 0.97),
("PULL-01", "插拔力测试机#1", {"OP-PULL": 2}, False, 0, "ZONE-C", [], 0.95),
("PKG-01", "包装台#1", {"OP-PKG": 1}, False, 0, "ZONE-C", [], 0.99),
("PKG-02", "包装台#2", {"OP-PKG": 1}, False, 0, "ZONE-C", [], 0.99),
]
data["flexEquipment"] = []
for i, (code, name, cap, movable, mv, zone, molds, avail) in enumerate(eq_rows):
data["flexEquipment"].append({
"id": i + 1, "code": code, "name": name,
"capabilities": list(cap.keys()), # 能力集(工序编码)
"opStdTime": cap, # 各工序单件工时(分钟)
"movable": movable, "moveTimeMin": mv, "zone": zone,
"adaptableMolds": molds, "availabilityRate": avail, "status": "RUNNING",
})
# ---- 工装/模具(压接模具家族;demand 提及 50+ 套,此处抽样 5 副代表) ----
mold_rows = [
("MOLD-HV-01", "高压端子模具A", "OP-CRIMP", ["PRESS-01", "PRESS-02", "PRESS-03"], 50000, 12000, "ZONE-A", 20),
("MOLD-HV-02", "高压端子模具B", "OP-CRIMP", ["PRESS-01", "PRESS-02", "PRESS-04"], 50000, 30000, "ZONE-A", 20),
("MOLD-HV-03", "高压端子模具C", "OP-CRIMP", ["PRESS-01", "PRESS-03"], 50000, 5000, "ZONE-A", 18),
("MOLD-CG-01", "充电枪端子模具A", "OP-CRIMP", ["PRESS-02", "PRESS-04"], 40000, 8000, "ZONE-A", 25),
("MOLD-CG-02", "充电枪端子模具B", "OP-CRIMP", ["PRESS-03", "PRESS-04"], 40000, 15000, "ZONE-A", 25),
]
data["flexMolds"] = []
for i, (code, name, op, eqs, total, used, zone, ch) in enumerate(mold_rows):
data["flexMolds"].append({
"id": i + 1, "code": code, "name": name, "operationCode": op,
"adaptableEquipment": eqs, "lifeTotal": total, "lifeUsed": used,
"zone": zone, "changeoverMin": ch, "status": "AVAILABLE",
})
# ---- 物料(4 成品 + 关键原料,含库存/在途/前置期) ----
data["flexMaterials"] = [
{"code": "HV-HARNESS", "name": "高压线束总成", "type": "FINISHED_PRODUCT", "unit": "套", "stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0},
{"code": "PDU-UNIT", "name": "PDU配电单元", "type": "FINISHED_PRODUCT", "unit": "套", "stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0},
{"code": "CHARGE-GUN", "name": "充电枪总成", "type": "FINISHED_PRODUCT", "unit": "套", "stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0},
{"code": "HV-CONN", "name": "高压连接器", "type": "FINISHED_PRODUCT", "unit": "个", "stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0},
{"code": "WIRE-HV", "name": "高压线材", "type": "RAW_MATERIAL", "unit": "米", "stock": 12000, "inTransit": 0, "safetyStock": 1000, "procurementLeadTime": 7},
{"code": "TERM-HV", "name": "高压端子", "type": "RAW_MATERIAL", "unit": "个", "stock": 6000, "inTransit": 2000, "safetyStock": 500, "procurementLeadTime": 10},
{"code": "BUSBAR", "name": "铜排", "type": "RAW_MATERIAL", "unit": "根", "stock": 1500, "inTransit": 400, "safetyStock": 200, "procurementLeadTime": 14},
{"code": "CONN-PDU", "name": "PDU连接器", "type": "RAW_MATERIAL", "unit": "个", "stock": 1600, "inTransit": 0, "safetyStock": 200, "procurementLeadTime": 12},
{"code": "CTRL-BOX", "name": "充电枪控制盒", "type": "RAW_MATERIAL", "unit": "个", "stock": 900, "inTransit": 0, "safetyStock": 100, "procurementLeadTime": 15},
{"code": "TERM-CG", "name": "充电枪端子", "type": "RAW_MATERIAL", "unit": "个", "stock": 4000, "inTransit": 0, "safetyStock": 300, "procurementLeadTime": 9},
{"code": "CONN-HOUSING", "name": "连接器壳体", "type": "RAW_MATERIAL", "unit": "个", "stock": 2000, "inTransit": 0, "safetyStock": 200, "procurementLeadTime": 8},
{"code": "PIN-HV", "name": "高压插针", "type": "RAW_MATERIAL", "unit": "个", "stock": 6000, "inTransit": 0, "safetyStock": 500, "procurementLeadTime": 6},
]
# ---- BOM(成品 → 物料,含消耗工序/关键料) ----
data["flexBom"] = [
{"productCode": "HV-HARNESS", "materialCode": "WIRE-HV", "quantity": 2.0, "consumeOp": "OP-CUT", "isKey": False},
{"productCode": "HV-HARNESS", "materialCode": "TERM-HV", "quantity": 4.0, "consumeOp": "OP-CRIMP", "isKey": True},
{"productCode": "PDU-UNIT", "materialCode": "BUSBAR", "quantity": 3.0, "consumeOp": "OP-WELD", "isKey": True},
{"productCode": "PDU-UNIT", "materialCode": "CONN-PDU", "quantity": 2.0, "consumeOp": "OP-ASSY", "isKey": False},
{"productCode": "CHARGE-GUN", "materialCode": "CTRL-BOX", "quantity": 1.0, "consumeOp": "OP-ESD", "isKey": True},
{"productCode": "CHARGE-GUN", "materialCode": "TERM-CG", "quantity": 3.0, "consumeOp": "OP-CRIMP", "isKey": False},
{"productCode": "HV-CONN", "materialCode": "CONN-HOUSING", "quantity": 1.0, "consumeOp": "OP-ASSY", "isKey": False},
{"productCode": "HV-CONN", "materialCode": "PIN-HV", "quantity": 6.0, "consumeOp": "OP-ASSY", "isKey": True},
]
# ---- 工艺路线(产品 → 有序工序;requireMold 决定是否需占模具) ----
# 行:(产品, 产品名, seq, 工序, 是否需模具, 单件工时覆盖 或 None)
rt_rows = [
# 高压线束总成:来料→下线→压接(模具)→装配→测试→包装
("HV-HARNESS", "高压线束总成", 10, "OP-IQC", False, None),
("HV-HARNESS", "高压线束总成", 20, "OP-CUT", False, None),
("HV-HARNESS", "高压线束总成", 30, "OP-CRIMP", True, None),
("HV-HARNESS", "高压线束总成", 40, "OP-ASSY", False, None),
("HV-HARNESS", "高压线束总成", 50, "OP-TEST", False, None),
("HV-HARNESS", "高压线束总成", 60, "OP-PKG", False, None),
# PDU 配电单元:来料→激光焊接→气密检漏→装配→测试→包装
("PDU-UNIT", "PDU配电单元", 10, "OP-IQC", False, None),
("PDU-UNIT", "PDU配电单元", 20, "OP-WELD", False, None),
("PDU-UNIT", "PDU配电单元", 30, "OP-LEAK", False, None),
("PDU-UNIT", "PDU配电单元", 40, "OP-ASSY", False, None),
("PDU-UNIT", "PDU配电单元", 50, "OP-TEST", False, None),
("PDU-UNIT", "PDU配电单元", 60, "OP-PKG", False, None),
# 充电枪总成:来料→压接(模具)→控制盒装配(ESD)→装配→测试→包装(与线束共用压接池)
("CHARGE-GUN", "充电枪总成", 10, "OP-IQC", False, None),
("CHARGE-GUN", "充电枪总成", 20, "OP-CRIMP", True, None),
("CHARGE-GUN", "充电枪总成", 30, "OP-ESD", False, None),
("CHARGE-GUN", "充电枪总成", 40, "OP-ASSY", False, None),
("CHARGE-GUN", "充电枪总成", 50, "OP-TEST", False, None),
("CHARGE-GUN", "充电枪总成", 60, "OP-PKG", False, None),
# 高压连接器:来料→装配→插拔力测试→包装
("HV-CONN", "高压连接器", 10, "OP-IQC", False, None),
("HV-CONN", "高压连接器", 20, "OP-ASSY", False, None),
("HV-CONN", "高压连接器", 30, "OP-PULL", False, None),
("HV-CONN", "高压连接器", 40, "OP-PKG", False, None),
]
data["flexRoutings"] = [
{"productCode": pc, "productName": pn, "seq": seq, "operationCode": op, "requireMold": rm, "stdTimePerUnit": st}
for (pc, pn, seq, op, rm, st) in rt_rows
]
# ---- 班组(人力资格;MVP 记录但排产先按设备约束) ----
data["flexTeams"] = [
{"code": "T-CUT", "name": "下线班组", "memberCount": 4, "supportOps": ["OP-CUT"], "skillLevel": "L2"},
{"code": "T-CRIMP", "name": "压接班组", "memberCount": 6, "supportOps": ["OP-CRIMP", "OP-ASSY"], "skillLevel": "L3"},
{"code": "T-WELD", "name": "焊接气密班组", "memberCount": 3, "supportOps": ["OP-WELD", "OP-LEAK"], "skillLevel": "L4"},
{"code": "T-ESD", "name": "充电枪装配班组", "memberCount": 4, "supportOps": ["OP-ESD", "OP-ASSY"], "skillLevel": "L3"},
{"code": "T-TEST", "name": "测试包装班组", "memberCount": 6, "supportOps": ["OP-TEST", "OP-PULL", "OP-PKG", "OP-IQC"], "skillLevel": "L3"},
]
# ---- 班次日历(单班 08:00-17:00,午休 1h;工作日周一~周五) ----
data["flexCalendar"] = [
{"shiftCode": "D", "startTime": "08:00", "endTime": "17:00",
"breaks": [{"start": "12:00", "end": "13:00"}], "workdays": [1, 2, 3, 4, 5]},
]
# ---- 订单(多品种小批量:4 产品 10 单,交期/优先级错落;含 WBS 与生产控制者隔离字段) ----
day = base
def _d(off: int) -> str:
return fmt_date(add_minutes(day, off * 24 * 60))
order_rows = [
("FO-2601", "HV-HARNESS", 100, _d(6), 2, "WBS-HV", "李强", "RELEASED"),
("FO-2602", "HV-HARNESS", 60, _d(4), 1, "WBS-HV", "李强", "RELEASED"),
("FO-2603", "PDU-UNIT", 80, _d(5), 1, "WBS-PDU", "王芳", "RELEASED"),
("FO-2604", "PDU-UNIT", 120, _d(8), 3, "WBS-PDU", "王芳", "RELEASED"),
("FO-2605", "CHARGE-GUN", 50, _d(5), 2, "WBS-CG", "陈涛", "RELEASED"),
("FO-2606", "CHARGE-GUN", 90, _d(9), 3, "WBS-CG", "陈涛", "RELEASED"),
("FO-2607", "HV-CONN", 200, _d(4), 1, "WBS-CONN", "赵敏", "RELEASED"),
("FO-2608", "HV-CONN", 150, _d(7), 2, "WBS-CONN", "赵敏", "RELEASED"),
("FO-2609", "HV-HARNESS", 200, _d(11), 4, "WBS-HV", "李强", "CREATED"),
("FO-2610", "PDU-UNIT", 60, _d(10), 4, "WBS-PDU", "王芳", "CREATED"),
]
data["flexOrders"] = [
{"id": i + 1, "orderNo": no, "productCode": pc, "quantity": qty, "dueDate": due,
"priority": prio, "wbs": wbs, "productionController": pcr, "status": st}
for i, (no, pc, qty, due, prio, wbs, pcr, st) in enumerate(order_rows)
]
# ---- 柔性排产参数(滚动时域 + 排产模式默认) ----
data["flexParams"] = {
"sortMode": "BOTTLENECK", # ASC 正排 / DESC 倒排 / BOTTLENECK 瓶颈锚
"beforeDays": 0, "afterDays": 30,
"rollingWindows": {"realtime": "60m", "long": "7d", "mid": "2d", "short": "2h"},
"mrpControllerDays": 3,
"weights": {"tardiness": 0.4, "cost": 0.3, "utilization": 0.2, "balance": 0.1},
}
# 柔性排产产物表(引擎写入;与固定产线层的 scheduleVersions 分离)
data["flexScheduleVersions"] = []
data["flexVirtualLines"] = []
data["flexWorkOrders"] = []
data["flexConflicts"] = []