# ============================================================ # 紧急插单专用流(moduleId: domain-rush, OR-04 首切片,可重生 ✅) # 规则:evaluate 全程沙盒不碰主干;apply 写订单+试排草稿,P2 写前自动建档 # ============================================================ from __future__ import annotations import copy import uuid from typing import Any from server.aps_domain.orders import ( SCHEDULABLE_STATUSES, apply_order_action, find_order, find_product_by_hint, normalize_order_payload, ) from server.contracts import ScheduleResult, UIBlock from server.engines import get_engine from server.engines.base import EngineParams from server.timeutil import add_minutes, fmt_date, today0 World = dict[str, Any] def _sandbox_counter(): counters: dict[str, int] = {} def next_id(kind: str) -> int: counters[kind] = counters.get(kind, 0) + 1000000 return counters[kind] return next_id def _engine_params(world: World, strategy: str) -> EngineParams: from server.aps_domain.constraints import engine_constraint_flags from server.aps_domain.params import get_schedule_params sp = get_schedule_params(world) return EngineParams( orderIds=[], engineType="RULE", strategyTemplate=strategy, planningHorizonDays=int(sp.get("planningHorizonDays") or 14), startDate=fmt_date(add_minutes(today0(), 24 * 60)), constraints=engine_constraint_flags(world), ) def _promised_by_so(world: World) -> dict[int, str]: """销售订单 → 最晚计划完工日。""" out: dict[int, str] = {} for po in world.get("productionOrders", []): so_id = po.get("salesOrderId") end = po.get("plannedEndDate") if so_id is None or not end: continue prev = out.get(so_id) if prev is None or end > prev: out[int(so_id)] = end return out def _run_sandbox(world: World, strategy: str) -> tuple[ScheduleResult, dict[int, str]]: sandbox = copy.deepcopy(world) # 清空排程产物,避免基准版本残留干扰对比 sandbox["productionOrders"] = [] sandbox["workOrders"] = [] sandbox["conflicts"] = [] result = get_engine("RULE").solve(sandbox, _engine_params(sandbox, strategy), _sandbox_counter()) return result, _promised_by_so(sandbox) def resolve_rush_payload(world: World, payload: dict[str, Any]) -> dict[str, Any]: """规范化插单载荷:既有订单加急,或新建急单字段。""" order_no = str(payload.get("orderNo") or "").strip().upper() if order_no: existing = find_order(world, order_no=order_no) if existing is None: raise ValueError(f"订单 {order_no} 不存在") if existing["status"] not in SCHEDULABLE_STATUSES | {"SUBMITTED", "CHANGED", "DRAFT"}: raise ValueError(f"订单 {order_no} 状态 {existing['status']} 不可插单评估") first = (existing.get("items") or [{}])[0] return { "mode": "existing", "id": existing["id"], "orderNo": existing["orderNo"], "customerName": existing["customerName"], "customerLevel": existing.get("customerLevel") or "A", "productId": first.get("productId"), "productCode": first.get("productCode"), "productName": first.get("productName"), "quantity": first.get("quantity"), "deliveryDate": existing["deliveryDate"], "priority": min(int(existing.get("priority") or 5), 2), "status": "APPROVED", "isRush": True, } hint = str(payload.get("productCode") or payload.get("productName") or "").strip() product_id = payload.get("productId") if not product_id and hint: product = find_product_by_hint(world, hint) if product is None: raise ValueError(f"找不到成品:{hint}") product_id = product["id"] if not product_id: raise ValueError("请指定 productId / productCode,或给出已有 orderNo") raw = { "customerName": str(payload.get("customerName") or "紧急插单客户").strip() or "紧急插单客户", "customerLevel": str(payload.get("customerLevel") or "VIP").upper(), "deliveryDate": payload.get("deliveryDate") or fmt_date(add_minutes(today0(), 5 * 24 * 60)), "priority": int(payload.get("priority") or 1), "status": "APPROVED", "productId": int(product_id), "quantity": int(payload.get("quantity") or 100), "isRush": True, "specialRequirements": str(payload.get("specialRequirements") or "OR-04 紧急插单"), } p = normalize_order_payload(world, raw) return { "mode": "new", **p, "productCode": p.get("productCode"), "productName": p.get("productName"), } def inject_rush(world: World, rush: dict[str, Any]) -> dict[str, Any]: """把急单写入世界(沙盒或主干);返回订单对象。""" if rush["mode"] == "existing": order = find_order(world, order_id=rush["id"]) if order is None: raise ValueError("目标订单不存在") order["isRush"] = True order["rushStrategy"] = "STRATEGY_SHIFT" order["priority"] = min(int(order.get("priority") or 5), 2) if order["status"] not in SCHEDULABLE_STATUSES: order["status"] = "APPROVED" return order applied = apply_order_action(world, _sandbox_counter(), "order.upsert", { "customerName": rush["customerName"], "customerLevel": rush["customerLevel"], "deliveryDate": rush["deliveryDate"], "priority": rush["priority"], "status": "APPROVED", "productId": rush["productId"], "quantity": rush["quantity"], "isRush": True, "specialRequirements": rush.get("specialRequirements") or "OR-04 紧急插单", }) return applied["order"] def inject_rush_trunk(world: World, next_id, rush: dict[str, Any]) -> dict[str, Any]: """主干写入急单(使用真实发号器)。""" if rush["mode"] == "existing": order = find_order(world, order_id=rush["id"]) if order is None: raise ValueError("目标订单不存在") order["isRush"] = True order["rushStrategy"] = "STRATEGY_SHIFT" order["priority"] = min(int(order.get("priority") or 5), 2) if order["status"] not in SCHEDULABLE_STATUSES: order["status"] = "APPROVED" return order applied = apply_order_action(world, next_id, "order.upsert", { "customerName": rush["customerName"], "customerLevel": rush["customerLevel"], "deliveryDate": rush["deliveryDate"], "priority": rush["priority"], "status": "APPROVED", "productId": rush["productId"], "quantity": rush["quantity"], "isRush": True, "specialRequirements": rush.get("specialRequirements") or "OR-04 紧急插单", }) return applied["order"] def evaluate_rush(world: World, payload: dict[str, Any]) -> dict[str, Any]: """插单影响快评(P1):双沙盒对比,主干零接触。""" strategy = str(payload.get("strategy") or "DELIVERY_FIRST").upper() if strategy not in ("DELIVERY_FIRST", "CAPACITY_BALANCE", "COMPREHENSIVE"): strategy = "DELIVERY_FIRST" rush = resolve_rush_payload(world, payload) base_result, base_promised = _run_sandbox(world, strategy) rush_world = copy.deepcopy(world) rush_order = inject_rush(rush_world, rush) rush_result, rush_promised = _run_sandbox(rush_world, strategy) affected: list[dict[str, Any]] = [] for so in world.get("salesOrders", []): so_id = so["id"] if so_id == rush_order.get("id"): continue b, r = base_promised.get(so_id), rush_promised.get(so_id) if b and r and r > b: affected.append({ "orderNo": so["orderNo"], "customerName": so["customerName"], "beforeEnd": b, "afterEnd": r, }) affected.sort(key=lambda x: x["afterEnd"], reverse=True) delay_delta = round(rush_result.totalTardiness - base_result.totalTardiness, 1) conflict_delta = rush_result.conflictCount - base_result.conflictCount impact = { "evalId": uuid.uuid4().hex[:10], "strategy": strategy, "mode": rush["mode"], "rushOrder": { "orderNo": rush_order.get("orderNo"), "id": rush_order.get("id"), "customerName": rush_order.get("customerName"), "productCode": (rush_order.get("items") or [{}])[0].get("productCode") or rush.get("productCode"), "productName": (rush_order.get("items") or [{}])[0].get("productName") or rush.get("productName"), "quantity": (rush_order.get("items") or [{}])[0].get("quantity") or rush.get("quantity"), "deliveryDate": rush_order.get("deliveryDate"), "isRush": True, "promisedEnd": rush_promised.get(rush_order["id"]), }, "baseline": { "orderCount": base_result.orderCount, "conflictCount": base_result.conflictCount, "totalTardiness": round(base_result.totalTardiness, 1), "avgUtilization": round(base_result.avgUtilization, 3), }, "after": { "orderCount": rush_result.orderCount, "conflictCount": rush_result.conflictCount, "totalTardiness": round(rush_result.totalTardiness, 1), "avgUtilization": round(rush_result.avgUtilization, 3), }, "affectedOrderCount": len(affected), "delayDelta": delay_delta, "conflictDelta": conflict_delta, "affectedOrders": affected[:12], "payload": rush, # apply 复用 } return impact def impact_to_block(impact: dict[str, Any]) -> UIBlock: """对话侧影响摘要块(采用走口令「采用插单」或订单面板确认)。""" rush = impact["rushOrder"] return UIBlock( blockId=f"rush-{impact['evalId']}", type="text", props={ "kind": "rush-impact", "impact": impact, "title": "插单影响快评", "text": ( f"急单:{rush.get('orderNo') or '(新建)'} · {rush.get('productName')} × {rush.get('quantity')}\n" f"受影响订单 {impact['affectedOrderCount']} 条 · 延迟变化 {impact['delayDelta']:+}h · 冲突变化 {impact['conflictDelta']:+}\n" f"策略:{impact['strategy']}(沙盒,未改主干)\n" "确认后请说「采用插单」。" ), }, actions=[], ) def format_impact_text(impact: dict[str, Any]) -> str: rush = impact["rushOrder"] lines = [ f"插单快评完成(沙盒,未改主干)· 策略 {_strategy_cn(impact['strategy'])}", f"急单:{rush.get('customerName')} · {rush.get('productName')} × {rush.get('quantity')} · 交期 {rush.get('deliveryDate')}" + (f" · 预计完工 {rush.get('promisedEnd')}" if rush.get("promisedEnd") else ""), f"影响:受波及订单 {impact['affectedOrderCount']} 条 · 总延迟 {impact['delayDelta']:+}h · 冲突 {impact['conflictDelta']:+}", f"基准 → 插单后:订单 {impact['baseline']['orderCount']}→{impact['after']['orderCount']} · " f"延迟 {impact['baseline']['totalTardiness']}→{impact['after']['totalTardiness']}h · " f"冲突 {impact['baseline']['conflictCount']}→{impact['after']['conflictCount']}", ] if impact["affectedOrders"]: sample = "、".join(a["orderNo"] for a in impact["affectedOrders"][:5]) lines.append(f"延期样例:{sample}" + ("…" if len(impact["affectedOrders"]) > 5 else "")) lines.append("确认无误后说「采用插单」或点「采用此插单」(P2,写前自动建档)。") return "\n".join(lines) def _strategy_cn(s: str) -> str: return { "DELIVERY_FIRST": "交期优先", "CAPACITY_BALANCE": "产能均衡", "COMPREHENSIVE": "综合优化", }.get(s, s) def apply_rush(world: World, next_id, payload: dict[str, Any]) -> dict[str, Any]: """采用插单:写主干订单池 + 生成 DRAFT 排产版本。调用方负责 checkpoint/审计。""" rush = resolve_rush_payload(world, payload.get("payload") or payload) strategy = str(payload.get("strategy") or "DELIVERY_FIRST").upper() order = inject_rush_trunk(world, next_id, rush) # 正式试排写草稿版本 from server.aps_domain.constraints import engine_constraint_flags from server.aps_domain.params import get_schedule_params sp = get_schedule_params(world) params = EngineParams( orderIds=[], engineType="RULE", strategyTemplate=strategy if strategy in ("DELIVERY_FIRST", "CAPACITY_BALANCE", "COMPREHENSIVE") else "DELIVERY_FIRST", planningHorizonDays=int(sp.get("planningHorizonDays") or 14), startDate=fmt_date(add_minutes(today0(), 24 * 60)), constraints=engine_constraint_flags(world), ) result = get_engine("RULE").solve(world, params, next_id) return {"order": order, "result": result, "strategy": params.strategyTemplate}