aps-agent/server/state/seed.py

615 lines
40 KiB
Python
Raw 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.

# ============================================================
# 种子数据工厂(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] = {
"factories": [], "workshops": [], "lines": [], "workstations": [],
"operations": [], "routings": [], "routingSteps": [], "materials": [],
"boms": [], "bomItems": [], "lineProducts": [], "workstationOperations": [],
"equipment": [], "teams": [], "shifts": [], "shiftCalendar": [],
"maintenance": [], "salesOrders": [], "productionOrders": [], "workOrders": [],
"purchaseOrders": [], "outsourceOrders": [],
"forecastOrders": [],
"changeoverMatrix": [],
"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,
},
"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,
}
# ---- 工厂与车间 ----
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},
{"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": "高频→标准:回退程序"},
]
# ---- 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),
]
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,
})
# ---- 初始日志(审计雏形 §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)
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"] = []