# ============================================================ # 种子数据工厂(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"] = []