180 lines
7.3 KiB
Python
180 lines
7.3 KiB
Python
|
|
# ============================================================
|
|||
|
|
# 排产参数(moduleId: domain-params, 可重生 ✅)
|
|||
|
|
# OR-02:客户等级权重 / 目标权重;变更走 P2,只影响后续新版本
|
|||
|
|
# ============================================================
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from copy import deepcopy
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
World = dict[str, Any]
|
|||
|
|
|
|||
|
|
DEFAULT_LEVEL_WEIGHTS = {"VIP": 3.0, "A": 2.0, "B": 1.0, "C": 1.0}
|
|||
|
|
DEFAULT_OBJECTIVE_WEIGHTS = {"tardiness": 0.4, "cost": 0.3, "utilization": 0.2, "balance": 0.1}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def default_schedule_params() -> dict[str, Any]:
|
|||
|
|
return {
|
|||
|
|
"defaultEngine": "RULE",
|
|||
|
|
"planningHorizonDays": 14,
|
|||
|
|
"timeGranularityMinutes": 15,
|
|||
|
|
"deliveryBufferRatio": 0.95,
|
|||
|
|
"weights": dict(DEFAULT_OBJECTIVE_WEIGHTS),
|
|||
|
|
"customerLevelWeights": dict(DEFAULT_LEVEL_WEIGHTS),
|
|||
|
|
"freezeWindowHours": 24,
|
|||
|
|
"cpTimeLimitSeconds": 8, # SC-03 CP-SAT 时限
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_schedule_params(world: World) -> dict[str, Any]:
|
|||
|
|
"""合并默认值后的排产参数投影(P0)。"""
|
|||
|
|
base = default_schedule_params()
|
|||
|
|
cur = world.get("scheduleParams") or {}
|
|||
|
|
out = {**base, **cur}
|
|||
|
|
out["weights"] = {**DEFAULT_OBJECTIVE_WEIGHTS, **(cur.get("weights") or {})}
|
|||
|
|
out["customerLevelWeights"] = {
|
|||
|
|
**DEFAULT_LEVEL_WEIGHTS,
|
|||
|
|
**(cur.get("customerLevelWeights") or {}),
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def level_weight(world: World, level: str | None) -> float:
|
|||
|
|
"""取客户等级排序权重(越高越优先排)。"""
|
|||
|
|
weights = get_schedule_params(world)["customerLevelWeights"]
|
|||
|
|
return float(weights.get(str(level or "C").upper(), 1.0))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def normalize_params_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|||
|
|
"""校验并规范化 params.update 载荷。"""
|
|||
|
|
out: dict[str, Any] = {}
|
|||
|
|
if payload.get("resetDefaults"):
|
|||
|
|
out["resetDefaults"] = True
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
if "customerLevelWeights" in payload and payload["customerLevelWeights"] is not None:
|
|||
|
|
raw = payload["customerLevelWeights"]
|
|||
|
|
if not isinstance(raw, dict):
|
|||
|
|
raise ValueError("customerLevelWeights 必须是对象")
|
|||
|
|
levels: dict[str, float] = {}
|
|||
|
|
for k, v in raw.items():
|
|||
|
|
key = str(k).upper()
|
|||
|
|
if key not in DEFAULT_LEVEL_WEIGHTS:
|
|||
|
|
raise ValueError(f"不支持的客户等级:{k}")
|
|||
|
|
try:
|
|||
|
|
val = float(v)
|
|||
|
|
except (TypeError, ValueError) as exc:
|
|||
|
|
raise ValueError(f"{key} 权重必须是数字") from exc
|
|||
|
|
if val < 0 or val > 100:
|
|||
|
|
raise ValueError(f"{key} 权重须在 0~100")
|
|||
|
|
levels[key] = val
|
|||
|
|
out["customerLevelWeights"] = levels
|
|||
|
|
|
|||
|
|
if "weights" in payload and payload["weights"] is not None:
|
|||
|
|
raw = payload["weights"]
|
|||
|
|
if not isinstance(raw, dict):
|
|||
|
|
raise ValueError("weights 必须是对象")
|
|||
|
|
obj: dict[str, float] = {}
|
|||
|
|
for k, v in raw.items():
|
|||
|
|
key = str(k)
|
|||
|
|
if key not in DEFAULT_OBJECTIVE_WEIGHTS:
|
|||
|
|
raise ValueError(f"不支持的目标权重键:{k}")
|
|||
|
|
try:
|
|||
|
|
val = float(v)
|
|||
|
|
except (TypeError, ValueError) as exc:
|
|||
|
|
raise ValueError(f"{key} 权重必须是数字") from exc
|
|||
|
|
if val < 0 or val > 1:
|
|||
|
|
raise ValueError(f"{key} 目标权重须在 0~1")
|
|||
|
|
obj[key] = val
|
|||
|
|
out["weights"] = obj
|
|||
|
|
|
|||
|
|
if "planningHorizonDays" in payload and payload["planningHorizonDays"] is not None:
|
|||
|
|
days = int(payload["planningHorizonDays"])
|
|||
|
|
if days < 1 or days > 90:
|
|||
|
|
raise ValueError("planningHorizonDays 须在 1~90")
|
|||
|
|
out["planningHorizonDays"] = days
|
|||
|
|
|
|||
|
|
if "defaultEngine" in payload and payload["defaultEngine"] is not None:
|
|||
|
|
eng = str(payload["defaultEngine"]).upper()
|
|||
|
|
if eng not in ("RULE", "CP", "GA", "HYBRID"):
|
|||
|
|
raise ValueError("defaultEngine 须为 RULE/CP/GA/HYBRID")
|
|||
|
|
out["defaultEngine"] = eng
|
|||
|
|
|
|||
|
|
if "cpTimeLimitSeconds" in payload and payload["cpTimeLimitSeconds"] is not None:
|
|||
|
|
sec = float(payload["cpTimeLimitSeconds"])
|
|||
|
|
if sec < 0.5 or sec > 120:
|
|||
|
|
raise ValueError("cpTimeLimitSeconds 须在 0.5~120")
|
|||
|
|
out["cpTimeLimitSeconds"] = sec
|
|||
|
|
|
|||
|
|
if not out:
|
|||
|
|
raise ValueError("未提供可更新的排产参数字段")
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def confirmation_for_params_update(world: World, payload: dict[str, Any]) -> tuple[str, list[str]]:
|
|||
|
|
"""生成 params.update 确认卡文案。"""
|
|||
|
|
norm = normalize_params_payload(payload)
|
|||
|
|
cur = get_schedule_params(world)
|
|||
|
|
if norm.get("resetDefaults"):
|
|||
|
|
return "恢复排产参数默认值", [
|
|||
|
|
"客户等级权重恢复 VIP=3 / A=2 / B=1 / C=1",
|
|||
|
|
"目标权重恢复交期/成本/利用率/均衡默认比",
|
|||
|
|
"只影响后续新排产版本,不回写历史版本",
|
|||
|
|
]
|
|||
|
|
lines: list[str] = []
|
|||
|
|
if "customerLevelWeights" in norm:
|
|||
|
|
before = cur["customerLevelWeights"]
|
|||
|
|
after = {**before, **norm["customerLevelWeights"]}
|
|||
|
|
parts = [f"{k}:{before.get(k)}→{after[k]}" for k in ("VIP", "A", "B", "C")]
|
|||
|
|
lines.append("客户等级权重:" + " / ".join(parts))
|
|||
|
|
if "weights" in norm:
|
|||
|
|
before = cur["weights"]
|
|||
|
|
after = {**before, **norm["weights"]}
|
|||
|
|
parts = [f"{k}:{before.get(k)}→{after[k]}" for k in after]
|
|||
|
|
lines.append("目标权重:" + " / ".join(parts))
|
|||
|
|
if "planningHorizonDays" in norm:
|
|||
|
|
lines.append(f"展望期:{cur['planningHorizonDays']}→{norm['planningHorizonDays']} 天")
|
|||
|
|
if "defaultEngine" in norm:
|
|||
|
|
lines.append(f"默认引擎:{cur.get('defaultEngine')}→{norm['defaultEngine']}")
|
|||
|
|
if "cpTimeLimitSeconds" in norm:
|
|||
|
|
lines.append(f"CP 时限:{cur.get('cpTimeLimitSeconds')}→{norm['cpTimeLimitSeconds']} 秒")
|
|||
|
|
lines.append("只影响后续新排产版本,不回写历史版本")
|
|||
|
|
return "更新排产参数", lines
|
|||
|
|
|
|||
|
|
|
|||
|
|
def apply_params_update(world: World, payload: dict[str, Any]) -> dict[str, Any]:
|
|||
|
|
"""写入 scheduleParams(P2 批准后调用)。"""
|
|||
|
|
norm = normalize_params_payload(payload)
|
|||
|
|
before = deepcopy(get_schedule_params(world))
|
|||
|
|
if norm.get("resetDefaults"):
|
|||
|
|
world["scheduleParams"] = default_schedule_params()
|
|||
|
|
else:
|
|||
|
|
sp = world.setdefault("scheduleParams", default_schedule_params())
|
|||
|
|
if "customerLevelWeights" in norm:
|
|||
|
|
sp["customerLevelWeights"] = {
|
|||
|
|
**DEFAULT_LEVEL_WEIGHTS,
|
|||
|
|
**(sp.get("customerLevelWeights") or {}),
|
|||
|
|
**norm["customerLevelWeights"],
|
|||
|
|
}
|
|||
|
|
if "weights" in norm:
|
|||
|
|
sp["weights"] = {
|
|||
|
|
**DEFAULT_OBJECTIVE_WEIGHTS,
|
|||
|
|
**(sp.get("weights") or {}),
|
|||
|
|
**norm["weights"],
|
|||
|
|
}
|
|||
|
|
if "planningHorizonDays" in norm:
|
|||
|
|
sp["planningHorizonDays"] = norm["planningHorizonDays"]
|
|||
|
|
if "defaultEngine" in norm:
|
|||
|
|
sp["defaultEngine"] = norm["defaultEngine"]
|
|||
|
|
if "cpTimeLimitSeconds" in norm:
|
|||
|
|
sp["cpTimeLimitSeconds"] = norm["cpTimeLimitSeconds"]
|
|||
|
|
after = get_schedule_params(world)
|
|||
|
|
return {
|
|||
|
|
"kind": "SCHEDULE_PARAMS",
|
|||
|
|
"id": "scheduleParams",
|
|||
|
|
"name": "排产参数",
|
|||
|
|
"before": before,
|
|||
|
|
"after": after,
|
|||
|
|
"reset": bool(norm.get("resetDefaults")),
|
|||
|
|
}
|