# ============================================================ # MySQL/Navicat SQL 数据包解析(moduleId: importer-sql-pack, 可重生 ✅) # 流式读取超大 dump(如松岳 mesdb_pro.sql ~1GB),抽取排产相关表 → flex* # ============================================================ from __future__ import annotations import os import re from datetime import datetime from typing import Any, Iterable # 表 → 角色(松岳 MES / 通用命名) _TABLE_KIND: dict[str, str] = { "pl_order": "orders", "r_plan_order": "orders", "pl_order_bom": "bom", "md_material": "materials", "md_item": "materials", "md_equipment": "equipment", "em_devices": "equipment", "r_production_craftl": "routing", "c_project_process_route": "routing", "md_craftl": "craft", "r_production_procedure": "op_equip", "pl_work_order": "work_orders", } _CREATE_RE = re.compile(r"CREATE TABLE\s+`([^`]+)`", re.I) _INSERT_RE = re.compile(r"INSERT INTO\s+`([^`]+)`\s+VALUES\s*", re.I) # 单表预览/导入上限(防 1GB 全量撑爆内存) _DEFAULT_LIMITS = { "orders": 800, "materials": 4000, "bom": 8000, "routing": 8000, # 其它工艺上限 "routing_prefer": 25000, # 订单相关工艺路线优先配额 "equipment": 800, "craft": 2000, "op_equip": 4000, "work_orders": 3000, } def _split_sql_values(blob: str) -> list[tuple[str, bool]]: """拆分 INSERT VALUES 元组内的字段 → (文本, 是否引号字符串)。""" out: list[tuple[str, bool]] = [] cur: list[str] = [] i = 0 in_str = False quoted = False n = len(blob) while i < n: ch = blob[i] if in_str: if ch == "\\" and i + 1 < n: cur.append(blob[i + 1]) i += 2 continue if ch == "'": if i + 1 < n and blob[i + 1] == "'": cur.append("'") i += 2 continue in_str = False i += 1 continue cur.append(ch) i += 1 continue if ch == "'": in_str = True quoted = True i += 1 continue if ch == ",": out.append(("".join(cur).strip(), quoted)) cur = [] quoted = False i += 1 continue cur.append(ch) i += 1 if cur or blob.endswith(","): out.append(("".join(cur).strip(), quoted)) return out def _parse_value(raw: str, *, quoted: bool = False) -> Any: s = (raw or "").strip() if not s or s.upper() == "NULL": return None if quoted: return s # 引号字段保持字符串(料号 030129001 不能丢前导 0) if (s.startswith("'") and s.endswith("'")) or (s.startswith('"') and s.endswith('"')): return s[1:-1] try: if "." in s: return float(s) return int(s) except ValueError: return s def _iter_insert_tuples(line: str) -> Iterable[list[Any]]: """从一行或多行 INSERT 语句产出行值列表。""" m = _INSERT_RE.search(line) if not m: return rest = line[m.end():].strip() if rest.endswith(";"): rest = rest[:-1] # 多个 (...),(...) i = 0 n = len(rest) while i < n: while i < n and rest[i] in " \t\r\n,": i += 1 if i >= n or rest[i] != "(": break depth = 0 in_str = False j = i while j < n: ch = rest[j] if in_str: if ch == "\\" and j + 1 < n: j += 2 continue if ch == "'": if j + 1 < n and rest[j + 1] == "'": j += 2 continue in_str = False j += 1 continue if ch == "'": in_str = True j += 1 continue if ch == "(": depth += 1 elif ch == ")": depth -= 1 if depth == 0: inner = rest[i + 1:j] yield [_parse_value(x, quoted=q) for x, q in _split_sql_values(inner)] i = j + 1 break j += 1 else: break def _parse_create_columns(block: str) -> list[str]: """解析 CREATE TABLE 列名;支持一行多列(测试/精简 dump)与 Navicat 一行一列。""" cols: list[str] = [] for line in block.splitlines(): s = line.strip().upper() if s.startswith(("PRIMARY ", "UNIQUE ", "KEY ", "INDEX ", "CONSTRAINT ")): continue if s.startswith(")"): break for m in re.finditer(r"`([^`]+)`\s+[a-zA-Z]", line): name = m.group(1) if name.upper() in ("PRIMARY", "UNIQUE", "KEY", "INDEX"): continue cols.append(name) return cols def _due_str(v: Any) -> str: if v is None: return "" if isinstance(v, datetime): return v.strftime("%Y-%m-%d") s = str(v).strip() m = re.match(r"(\d{4}-\d{2}-\d{2})", s) return m.group(1) if m else s[:10] def _row_dict(cols: list[str], vals: list[Any]) -> dict[str, Any]: d: dict[str, Any] = {} for i, c in enumerate(cols): if i < len(vals): d[c] = vals[i] return d def scan_sql_pack( path: str, *, limits: dict[str, int] | None = None, ) -> dict[str, Any]: """流式扫描 SQL dump,返回按 kind 分组的行 + 表统计。""" lim = {**_DEFAULT_LIMITS, **(limits or {})} schemas: dict[str, list[str]] = {} rows_by_table: dict[str, list[dict[str, Any]]] = {} counts: dict[str, int] = {} creating: str | None = None create_buf: list[str] = [] size = os.path.getsize(path) if os.path.isfile(path) else 0 try: from server.agent_core.progress import emit_thinking except Exception: def emit_thinking(*_a, **_k): # type: ignore return None emit_thinking("读取 SQL 数据包", f"{os.path.basename(path)} · {size / (1024 ** 2):.0f} MB", pct=1) lines_read = 0 bytes_read = 0 last_emit = 0 # 订单上的工艺路线编码:dump 里 pl_order 早于 r_production_craftl,可优先抽取 prefer_craftl: set[str] = set() routing_pref: list[dict[str, Any]] = [] routing_other: list[dict[str, Any]] = [] routing_table = "r_production_craftl" with open(path, "r", encoding="utf-8", errors="replace") as f: for line in f: lines_read += 1 bytes_read += len(line.encode("utf-8", errors="ignore")) if creating is not None: create_buf.append(line) if "ENGINE" in line.upper() and line.strip().endswith(";"): schemas[creating] = _parse_create_columns("".join(create_buf)) creating = None create_buf = [] continue cm = _CREATE_RE.search(line) if cm: name = cm.group(1) if name in _TABLE_KIND: creating = name create_buf = [line] emit_thinking("解析表结构", name, pct=min(40, 5 + len(schemas))) continue if "INSERT INTO" not in line.upper(): if lines_read - last_emit >= 400_000: last_emit = lines_read pct = min(85.0, 10 + (bytes_read / max(size, 1)) * 70) emit_thinking("扫描 SQL 全文", f"已读 {lines_read // 10000 / 10:.1f} 万行", pct=pct) continue im = _INSERT_RE.search(line) if not im: continue table = im.group(1) kind = _TABLE_KIND.get(table) if not kind: continue cols = schemas.get(table) or [] if not cols: continue # 支持 INSERT 跨行:VALUES 在下一行,直到分号结束 insert_blob = line while ";" not in insert_blob: nxt = f.readline() if not nxt: break lines_read += 1 bytes_read += len(nxt.encode("utf-8", errors="ignore")) insert_blob += nxt before = counts.get(table, 0) for vals in _iter_insert_tuples(insert_blob): counts[table] = counts.get(table, 0) + 1 row = _row_dict(cols, vals) if kind == "orders": cc = str(row.get("craftl_code") or "").strip() if cc: prefer_craftl.add(cc) bucket = rows_by_table.setdefault(table, []) if len(bucket) < lim.get("orders", 800): bucket.append(row) elif kind == "routing": cc = str(row.get("craftl_code") or row.get("process_id") or "").strip() if cc and cc in prefer_craftl: if len(routing_pref) < lim.get("routing_prefer", 25000): routing_pref.append(row) elif len(routing_other) < lim.get("routing", 8000): routing_other.append(row) else: bucket = rows_by_table.setdefault(table, []) cap = lim.get(kind, 1000) if len(bucket) < cap: bucket.append(row) if counts.get(table, 0) and before == 0: emit_thinking("抽取业务表", f"{table} → {kind}", pct=min(90, 20 + len(counts) * 5)) if lines_read - last_emit >= 400_000: last_emit = lines_read pct = min(90.0, 10 + (bytes_read / max(size, 1)) * 75) emit_thinking( "扫描 SQL 全文", f"已读 {lines_read // 10000 / 10:.1f} 万行 · 订单工艺 {len(prefer_craftl)} 条", pct=pct, ) if routing_pref or routing_other: rows_by_table[routing_table] = routing_pref + routing_other emit_thinking( "工艺优先装载", f"订单相关 {len(routing_pref)} 步 · 其它 {len(routing_other)} 步 · 覆盖工艺编码 {len(prefer_craftl)}", status="done", pct=91, ) emit_thinking("SQL 扫描完成", f"命中 {len(counts)} 张相关表", status="done", pct=92) return { "path": path, "size": size, "schemas": {k: v for k, v in schemas.items()}, "counts": counts, "rowsByTable": rows_by_table, "preferCraftl": sorted(prefer_craftl), "kinds": sorted({_TABLE_KIND[t] for t in counts}), } def _map_orders(rows: list[dict]) -> list[dict]: out = [] for r in rows: if str(r.get("is_delete") or "0") not in ("0", "0.0", "", "None"): continue ono = str(r.get("code") or r.get("order_code") or "").strip() pc = str(r.get("material_code") or r.get("product_code") or "").strip() if not ono or not pc: continue qty = float(r.get("quantity") or r.get("quatity") or r.get("all_quantity") or 0) if qty <= 0: qty = 1 due = _due_str(r.get("planned_end_time") or r.get("end_time") or r.get("actual_end_time")) if not due: due = datetime.now().strftime("%Y-%m-%d") out.append({ "orderNo": ono, "productCode": pc, "productName": str(r.get("material_name") or r.get("product_name") or pc), "quantity": int(qty) if qty == int(qty) else qty, "dueDate": due, "priority": int(float(r.get("level") or 5) or 5), "customerName": str(r.get("customer_business_code") or r.get("dept") or "松岳现场"), "status": "RELEASED", "drawingNo": str(r.get("drawing_code") or r.get("figure_no") or ""), "machineModel": str(r.get("machine_model") or r.get("model") or ""), "craftlCode": str(r.get("craftl_code") or "").strip(), }) return out def _map_materials(rows: list[dict]) -> list[dict]: out = [] seen: set[str] = set() for r in rows: if str(r.get("is_delete") or "0") not in ("0", "0.0", "", "None"): continue code = str(r.get("code") or "").strip() name = str(r.get("name") or code).strip() if not code or code in seen: continue seen.add(code) raw_type = str(r.get("type") or r.get("classify_name") or "").upper() type_cn = str(r.get("type") or "") if "毛坯" in name or "原料" in type_cn or raw_type in ("RM", "RAW"): mtype = "RAW_MATERIAL" elif "半" in type_cn or raw_type in ("SF", "SEMI"): mtype = "SEMI_FINISHED" elif "成品" in type_cn or raw_type in ("FG", "FINISHED"): mtype = "FINISHED_PRODUCT" else: # 松岳料号习惯:01* 毛坯/原料,其余先按原料,有工艺时再升为半成品 mtype = "RAW_MATERIAL" if code.startswith("01") else "SEMI_FINISHED" sourcing = "BUY" if mtype == "RAW_MATERIAL" or "毛坯" in name else "MAKE" out.append({ "code": code, "name": name, "type": mtype, "sourcingType": sourcing, "unit": str(r.get("unit") or "件").strip('"') or "件", "stock": float(r.get("stock") or 0), "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0, "spec": str(r.get("specification_type") or r.get("figure_no") or ""), }) return out def _map_equipment(rows: list[dict]) -> list[dict]: out = [] seen: set[str] = set() for r in rows: if str(r.get("is_delete") or r.get("delete_flag") or "0") not in ("0", "0.0", "", "None"): continue code = str(r.get("code") or r.get("devices_code") or "").strip() name = str(r.get("name") or r.get("devices_name") or code).strip() if not code or code in seen: continue seen.add(code) out.append({ "code": code, "name": name, "capabilities": ["GENERAL"], "opStdTime": {"GENERAL": 1.0}, "movable": False, "moveTimeMin": 0, "zone": "ZONE-A", "adaptableMolds": [], "availabilityRate": 0.95, "status": "RUNNING", }) return out def _map_routing(rows: list[dict], craft_product: dict[str, str]) -> list[dict]: """工艺步骤挂到成品:优先 craftl_code→订单/工艺主数据料号,禁止用中间件料号冒充成品。""" out = [] for r in rows: if str(r.get("is_delete") or r.get("del_flag") or "0") not in ("0", "0.0", "", "None"): continue op = str(r.get("procedure_code") or r.get("process_code") or "").strip() if not op: continue craft = str(r.get("craftl_code") or r.get("process_id") or "").strip() out_mat = str(r.get("output_material_code") or r.get("product_code") or "").strip() # 成品优先:工艺编码映射 > 输出料号(仅当映射缺失) pc = craft_product.get(craft) or out_mat or craft if not pc: continue try: seq = int(float(r.get("sort_no") or r.get("process_sort") or r.get("serial_number") or 10)) except (TypeError, ValueError): seq = 10 if seq <= 0: seq = 10 std = r.get("working_hours") or r.get("process_shift_output") try: std_f = float(std) if std not in (None, "") else None except (TypeError, ValueError): std_f = None # 工时若像小时则转分钟 if std_f is not None and std_f > 0 and std_f < 24: std_f = round(std_f * 60, 2) if std_f is None or std_f <= 0: std_f = 1.0 src = "推断" else: src = "实测" pname = str(r.get("craftl_name") or r.get("output_material_name") or pc) op_name = str(r.get("procedure_name") or r.get("process_name") or op) from server.aps_domain.sourcing import infer_op_sourcing sourcing = infer_op_sourcing(op, op_name) out.append({ "productCode": pc, "productName": pname, "seq": seq, "operationCode": op, "operationName": op_name, "requireMold": False, "stdTimePerUnit": std_f, "stdTimeSource": src, "craftlCode": craft, "outputMaterialCode": out_mat, "sourcingType": sourcing, "isExternal": sourcing == "OUTSOURCE", }) return out def _expand_routing_for_orders( orders: list[dict], steps: list[dict], ) -> list[dict]: """同一 craftl 下的步骤,按订单成品料号再挂一份(多订单共用工艺)。""" by_craft: dict[str, list[dict]] = {} for s in steps: cc = str(s.get("craftlCode") or "").strip() if cc: by_craft.setdefault(cc, []).append(s) out: list[dict] = [] seen: set[tuple] = set() for o in orders: pc = str(o.get("productCode") or "").strip() cc = str(o.get("craftlCode") or "").strip() if not pc: continue src = by_craft.get(cc) or [s for s in steps if s.get("productCode") == pc] for s in src: key = (pc, s.get("seq"), s.get("operationCode")) if key in seen: continue seen.add(key) row = dict(s) row["productCode"] = pc row["productName"] = o.get("productName") or pc out.append(row) # 保留未能挂到订单的步骤(其它产品) for s in steps: key = (s.get("productCode"), s.get("seq"), s.get("operationCode")) if key not in seen: out.append(s) seen.add(key) return out def _map_bom(rows: list[dict], order_product: dict[str, str]) -> list[dict]: out = [] for r in rows: if str(r.get("is_delete") or "0") not in ("0", "0.0", "", "None"): continue ono = str(r.get("order_code") or "").strip() mc = str(r.get("material_code") or "").strip() pc = order_product.get(ono) or str(r.get("machine_model") or "").strip() qty = float(r.get("composition_quantity") or r.get("consumption_quantity") or r.get("quantity") or 0) if not pc or not mc or qty <= 0: continue out.append({ "productCode": pc, "materialCode": mc, "quantity": qty, "consumeOp": str(r.get("procedure_code") or ""), "isKey": qty >= 1, "materialName": str(r.get("material_name") or mc), }) return out def _apply_op_equip(equip: list[dict], rows: list[dict]) -> None: """用工序-设备关系补全 capabilities。""" by_code = {e["code"]: e for e in equip} for r in rows: if str(r.get("is_delete") or "0") not in ("0", "0.0", "", "None"): continue ec = str(r.get("equipment_code") or "").strip() op = str(r.get("procedure_code") or "").strip() if not ec or not op or ec not in by_code: continue e = by_code[ec] caps = list(e.get("capabilities") or []) if "GENERAL" in caps and len(caps) == 1: caps = [] if op not in caps: caps.append(op) e["capabilities"] = caps or ["GENERAL"] std = dict(e.get("opStdTime") or {}) if op not in std: std[op] = 1.0 if "GENERAL" in std and op != "GENERAL" and len(std) > 1: std.pop("GENERAL", None) e["opStdTime"] = std def sql_pack_to_flex(scan: dict[str, Any]) -> dict[str, Any]: """把 scan 结果映射为可写入 world 的 flex* 批次。""" rbt = scan.get("rowsByTable") or {} craft_product: dict[str, str] = {} for r in rbt.get("md_craftl") or []: code = str(r.get("code") or "").strip() pc = str(r.get("product_code") or "").strip() if code and pc: craft_product[code] = pc orders = _map_orders((rbt.get("pl_order") or []) + (rbt.get("r_plan_order") or [])) # 订单上的 craftl→成品料号优先(现场口径,覆盖 md_craftl 偏差) for o in orders: cc = str(o.get("craftlCode") or "").strip() pc = str(o.get("productCode") or "").strip() if cc and pc: craft_product[cc] = pc order_product = {o["orderNo"]: o["productCode"] for o in orders} # 订单产品补进物料 materials = _map_materials((rbt.get("md_material") or []) + (rbt.get("md_item") or [])) mat_codes = {m["code"] for m in materials} for o in orders: if o["productCode"] not in mat_codes: materials.append({ "code": o["productCode"], "name": o.get("productName") or o["productCode"], "type": "FINISHED_PRODUCT", "unit": "件", "stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0, "spec": "", }) mat_codes.add(o["productCode"]) equipment = _map_equipment((rbt.get("md_equipment") or []) + (rbt.get("em_devices") or [])) _apply_op_equip(equipment, rbt.get("r_production_procedure") or []) routing_raw = _map_routing( (rbt.get("r_production_craftl") or []) + (rbt.get("c_project_process_route") or []), craft_product, ) routing = _expand_routing_for_orders(orders, routing_raw) # 设备能力:把订单工艺工序挂到设备(MES 设备表常无工序能力列) ops_needed = sorted({str(s.get("operationCode") or "") for s in routing if s.get("operationCode")}) if ops_needed and equipment: for e in equipment: caps = [c for c in (e.get("capabilities") or []) if c and c != "GENERAL"] if not caps: # MES 设备台账常无工序能力列:按已挂工艺放开,否则齐备度永远卡 NO_CAPABLE e["capabilities"] = list(ops_needed) e["opStdTime"] = {op: float((e.get("opStdTime") or {}).get(op) or 1.0) for op in ops_needed} bom = _map_bom(rbt.get("pl_order_bom") or [], order_product) pcs_ord = {o["productCode"] for o in orders} pcs_rt = {s["productCode"] for s in routing} return { "orders": orders, "materials": materials, "equipment": equipment, "routing": routing, "bom": bom, "stats": { "orders": len(orders), "materials": len(materials), "equipment": len(equipment), "routing": len(routing), "bom": len(bom), "orderProducts": len(pcs_ord), "routedProducts": len(pcs_ord & pcs_rt), "missingRouting": len(pcs_ord - pcs_rt), "rawCounts": scan.get("counts") or {}, }, } def sync_flex_to_classic_master(world: dict[str, Any]) -> dict[str, int]: """把 flex* 投影到主数据页读的经典表(materials/routings/…),避免工艺模型仍显示演示厂。""" summary: dict[str, int] = {} def _next(table: str) -> int: items = world.get(table) or [] return max((x.get("id", 0) for x in items if isinstance(x.get("id"), int)), default=0) + 1 # 物料 mats = world.setdefault("materials", []) code_to_id: dict[str, int] = {str(m.get("code")): int(m["id"]) for m in mats if m.get("id") is not None} for row in world.get("flexMaterials") or []: code = str(row.get("code") or "").strip() if not code: continue payload = { "code": code, "name": row.get("name") or code, "spec": row.get("spec") or "", "type": row.get("type") or "FINISHED_PRODUCT", "unit": row.get("unit") or "件", "productFamily": row.get("productFamily") or "", "safetyStock": float(row.get("safetyStock") or 0), "procurementLeadTime": float(row.get("procurementLeadTime") or 0), "stock": float(row.get("stock") or 0), "inTransit": float(row.get("inTransit") or 0), "expectedArrivalDate": row.get("expectedArrivalDate") or "", "sourcingType": row.get("sourcingType") or "", "sourcingTypeSource": row.get("sourcingTypeSource") or ("IMPORT" if row.get("sourcingType") else ""), "status": "ACTIVE", } if code in code_to_id: ex = next(m for m in mats if m.get("code") == code) ex.update(payload) else: mid = _next("materials") mats.append({"id": mid, **payload}) code_to_id[code] = mid summary["materials"] = summary.get("materials", 0) + 1 # 工序库 ops = world.setdefault("operations", []) op_code_to_id: dict[str, int] = {str(o.get("code")): int(o["id"]) for o in ops if o.get("id") is not None} for row in world.get("flexOperations") or []: code = str(row.get("code") or "").strip() if not code: continue if code in op_code_to_id: continue oid = _next("operations") from server.aps_domain.sourcing import infer_op_sourcing sourcing = infer_op_sourcing(code, row.get("name")) ops.append({ "id": oid, "code": code, "name": row.get("name") or code, "type": "EXTERNAL" if sourcing == "OUTSOURCE" else "INTERNAL", "sourcingType": sourcing, "standardTime": float((row.get("changeoverMin") or 0) or 0) / 60.0 or 0.5, }) op_code_to_id[code] = oid summary["operations"] = summary.get("operations", 0) + 1 # 工艺路线 + 步骤(按产品) routings = world.setdefault("routings", []) steps = world.setdefault("routingSteps", []) live_pcs = {str(r.get("productCode") or "") for r in (world.get("flexRoutings") or [])} if live_pcs: drop_pids = {code_to_id[pc] for pc in live_pcs if pc in code_to_id} drop_rids = {r["id"] for r in routings if r.get("productId") in drop_pids} if drop_rids: world["routings"] = [r for r in routings if r["id"] not in drop_rids] world["routingSteps"] = [s for s in steps if s.get("routingId") not in drop_rids] routings = world["routings"] steps = world["routingSteps"] by_pc: dict[str, list[dict]] = {} for row in world.get("flexRoutings") or []: pc = str(row.get("productCode") or "").strip() if pc: by_pc.setdefault(pc, []).append(row) for pc, rows in by_pc.items(): pid = code_to_id.get(pc) if not pid: continue rid = _next("routings") routings.append({ "id": rid, "productId": pid, "version": "V1", "versionName": f"{pc}工艺", "isDefault": True, "status": "ACTIVE", }) rows_sorted = sorted(rows, key=lambda r: int(r.get("seq") or 0)) prev = None for i, row in enumerate(rows_sorted): op = str(row.get("operationCode") or "").strip() if op and op not in op_code_to_id: oid = _next("operations") ops.append({ "id": oid, "code": op, "name": row.get("operationName") or op, "type": "EXTERNAL" if ( row.get("isExternal") or str(row.get("sourcingType") or "").upper() == "OUTSOURCE" or str(op).upper().startswith("WZ") ) else "INTERNAL", "sourcingType": row.get("sourcingType") or ( "OUTSOURCE" if str(op).upper().startswith("WZ") else "MAKE" ), "standardTime": float(row.get("stdTimePerUnit") or 1) / 60.0, }) op_code_to_id[op] = oid oid = op_code_to_id.get(op) if not oid: continue sid = _next("routingSteps") std = float(row.get("stdTimePerUnit") or 1.0) from server.aps_domain.sourcing import infer_op_sourcing sourcing = row.get("sourcingType") or infer_op_sourcing(op, row.get("operationName")) is_ext = bool(row.get("isExternal")) or sourcing == "OUTSOURCE" # 同步工序主数据 type for o in ops: if o.get("id") == oid: o["type"] = "EXTERNAL" if is_ext else o.get("type") or "INTERNAL" o["sourcingType"] = sourcing break steps.append({ "id": sid, "routingId": rid, "operationId": oid, "sequenceNo": i + 1, "prevStepId": prev, "setupTime": 10, "runTimePerUnit": std, "waitTime": 0, "transferTime": 0, "isExternal": is_ext, "sourcingType": sourcing, }) prev = sid summary["routingSteps"] = summary.get("routingSteps", 0) + 1 if is_ext: summary["externalSteps"] = summary.get("externalSteps", 0) + 1 summary["routings"] = summary.get("routings", 0) + 1 # BOM boms = world.setdefault("boms", []) bom_items = world.setdefault("bomItems", []) bom_by_pc: dict[str, list[dict]] = {} for row in world.get("flexBom") or []: pc = str(row.get("productCode") or "").strip() if pc: bom_by_pc.setdefault(pc, []).append(row) for pc, rows in bom_by_pc.items(): pid = code_to_id.get(pc) if not pid: continue # 清旧 old = [b["id"] for b in boms if b.get("productId") == pid] if old: world["boms"] = [b for b in boms if b["id"] not in old] world["bomItems"] = [bi for bi in bom_items if bi.get("bomId") not in old] boms = world["boms"] bom_items = world["bomItems"] bid = _next("boms") boms.append({ "id": bid, "productId": pid, "version": "V1", "versionName": f"{pc}BOM", "isDefault": True, "status": "ACTIVE", }) for row in rows: mc = str(row.get("materialCode") or "").strip() if not mc: continue if mc not in code_to_id: mid = _next("materials") mats.append({ "id": mid, "code": mc, "name": row.get("materialName") or mc, "spec": "", "type": "RAW_MATERIAL", "unit": row.get("unit") or "件", "productFamily": "", "safetyStock": 0, "procurementLeadTime": 0, "stock": 0, "inTransit": 0, "status": "ACTIVE", }) code_to_id[mc] = mid bom_items.append({ "id": _next("bomItems"), "bomId": bid, "materialId": code_to_id[mc], "quantity": float(row.get("quantity") or 1), "operationId": None, "isKeyMaterial": False, }) summary["bomItems"] = summary.get("bomItems", 0) + 1 summary["boms"] = summary.get("boms", 0) + 1 # 资源树:无工厂时挂一个「现场」占位,设备挂到柔性资源页即可 if (world.get("flexEquipment") or []) and not (world.get("factories") or []): world["factories"] = [{ "id": 1, "code": "SITE", "name": "现场工厂", "timezone": "Asia/Shanghai", "address": "", "status": "ACTIVE", }] world["workshops"] = [{ "id": 1, "factoryId": 1, "code": "WS1", "name": "生产车间", "status": "ACTIVE", }] world["lines"] = [{ "id": 1, "workshopId": 1, "code": "FLEX", "name": "柔性能力池", "capacityPerDay": 9999, "taktTime": 1, "efficiencyFactor": 1.0, "status": "ACTIVE", "alternativeLineIds": [], }] summary["factories"] = 1 params = world.setdefault("flexParams", {}) params["demoDataCleared"] = True params["siteProfile"] = params.get("siteProfile") or "sql-pack" return summary def apply_sql_pack_to_world(world: dict[str, Any], flex: dict[str, Any], *, replace: bool = True) -> dict[str, int]: """将 SQL 映射结果写入 world flex*,并投影到主数据页经典表。""" from server.state.seed import ensure_flex_seed, purge_demo_residue # 先清演示残留,再保证键存在(不会回填假数据) purge_demo_residue(world) world.setdefault("flexParams", {})["demoDataCleared"] = True ensure_flex_seed(world) summary: dict[str, int] = {} def _next(table: str) -> int: items = world.get(table) or [] return max((x.get("id", 0) for x in items if isinstance(x.get("id"), int)), default=0) + 1 if replace: for k in ("flexOrders", "flexMaterials", "flexEquipment", "flexRoutings", "flexBom"): world[k] = [] # 清掉演示厂班组,否则现场工序(NZ*/WZ*)会被 NO_TEAM 卡死 world["flexTeams"] = [] # 经典主数据也整表换新,禁止智能控制器与现场料号并存 for k in ( "materials", "boms", "bomItems", "operations", "routings", "routingSteps", "lineProducts", "workstationOperations", "changeoverMatrix", "factories", "workshops", "lines", "workstations", "equipment", ): world[k] = [] # materials first fm = world.setdefault("flexMaterials", []) for row in flex.get("materials") or []: ex = next((m for m in fm if m.get("code") == row["code"]), None) if ex: ex.update(row) else: fm.append({"id": _next("flexMaterials"), **row}) summary["materials"] = summary.get("materials", 0) + 1 fo = world.setdefault("flexOrders", []) for row in flex.get("orders") or []: ex = next((o for o in fo if o.get("orderNo") == row["orderNo"]), None) if ex: ex.update(row) else: fo.append({"id": _next("flexOrders"), **row}) summary["orders"] = summary.get("orders", 0) + 1 fe = world.setdefault("flexEquipment", []) for row in flex.get("equipment") or []: ex = next((e for e in fe if e.get("code") == row["code"]), None) if ex: ex.update(row) else: fe.append({"id": _next("flexEquipment"), **row}) summary["equipment"] = summary.get("equipment", 0) + 1 fr = world.setdefault("flexRoutings", []) for row in flex.get("routing") or []: fr[:] = [s for s in fr if not ( s.get("productCode") == row["productCode"] and s.get("seq") == row["seq"] and s.get("operationCode") == row["operationCode"] )] fr.append(dict(row)) summary["routing"] = summary.get("routing", 0) + 1 fb = world.setdefault("flexBom", []) for row in flex.get("bom") or []: fb[:] = [s for s in fb if not ( s.get("productCode") == row["productCode"] and s.get("materialCode") == row["materialCode"] )] fb.append(dict(row)) summary["bom"] = summary.get("bom", 0) + 1 # 默认班次 if not world.get("flexCalendar"): world["flexCalendar"] = [{ "shiftCode": "D", "startTime": "08:00", "endTime": "17:00", "breaks": [{"start": "12:00", "end": "13:00"}], "workdays": [1, 2, 3, 4, 5], }] summary["calendar"] = 1 # 工序库 ops = world.setdefault("flexOperations", []) seen_op = {o.get("code") for o in ops} all_ops: list[str] = [] for row in flex.get("routing") or []: code = row.get("operationCode") if code and code not in seen_op: ops.append({"code": code, "name": row.get("operationName") or code, "isBottleneck": False, "changeoverMin": 10}) seen_op.add(code) summary["operations"] = summary.get("operations", 0) + 1 if code and code not in all_ops: all_ops.append(code) # 现场综合班组:覆盖 SQL 工艺全部工序,避免演示厂班组资格卡死 if all_ops: teams = world.setdefault("flexTeams", []) plant = next((t for t in teams if t.get("code") == "T-PLANT"), None) if plant is None: plant = { "code": "T-PLANT", "name": "现场综合班组", "memberCount": max(8, min(40, len(world.get("flexEquipment") or []) or 8)), "supportOps": [], "skillLevel": "L3", } teams.append(plant) summary["teams"] = 1 for code in all_ops: if code not in plant["supportOps"]: plant["supportOps"].append(code) # 投影到主数据「工艺模型」页(经典 materials/routings) classic = sync_flex_to_classic_master(world) for k, v in classic.items(): summary[f"classic_{k}"] = v # 投影到订单管理页(salesOrders) try: from server.aps_domain.orders import sync_flex_orders_to_sales n = sync_flex_orders_to_sales(world) if n: summary["salesOrders"] = n except Exception: pass # 工序/物料供应类型标注(WZ*=委外,毛坯=采购) try: from server.aps_domain.sourcing import annotate_world_sourcing summary["sourcing"] = annotate_world_sourcing(world) except Exception: pass return summary def preview_sql_file(path: str) -> dict[str, Any]: """给 folder_pack / 分析用的轻量预览。""" scan = scan_sql_pack(path) flex = sql_pack_to_flex(scan) st = flex["stats"] return { "filename": os.path.basename(path), "kind": "sql-pack", "kindCn": "SQL 数据包", "size": scan["size"], "okCount": st["orders"] + st["materials"] + st["routing"] + st["equipment"], "errorCount": 0, "tables": [ {"table": t, "kind": _TABLE_KIND.get(t, ""), "rawCount": c, "loaded": len((scan.get("rowsByTable") or {}).get(t) or [])} for t, c in sorted((scan.get("counts") or {}).items()) ], "fieldMapText": ( f"订单{st['orders']} · 物料{st['materials']} · 工艺{st['routing']} · " f"设备{st['equipment']} · BOM{st['bom']}" ), "samples": [ f"订单 {o['orderNo']}:{o['productCode']}×{o['quantity']} 交期{o['dueDate']}" for o in (flex.get("orders") or [])[:3] ], "flex": flex, "canSchedule": st["orders"] > 0 and (st["routing"] > 0 or st["equipment"] > 0), }