From e330545d5921844a6630139b34a276d584af9463 Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Wed, 26 Aug 2026 22:38:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(P1-3):=20=E7=9B=B8=E5=85=B3=E6=88=90?= =?UTF-8?q?=E6=9C=AC=E5=86=B3=E7=AD=96=E6=A8=A1=E5=9D=97=20=E2=80=94=20?= =?UTF-8?q?=E5=AE=98=E6=96=B925%=E6=9D=83=E9=87=8D'=E5=95=86=E4=B8=9A?= =?UTF-8?q?=E5=86=B3=E7=AD=96=E5=88=86=E6=9E=90'=E6=A0=B8=E5=BF=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 /predict/relevant-decision API + 前端Tab(3场景): 1. 自制vs外购: 相关成本比较+无差别点(增量成本视角) 2. 特殊订单: 剩余产能下价格>变动成本即接受(增量利润) 3. 产品组合(约束理论): 单位约束资源边际贡献排序 验证: 自制850k vs 外购900k→自制; 特殊订单单位贡献25→接受; 产品组合B产品单位约束贡献50最高→优先 --- backend/app/api/predict.py | 91 ++++++++++++++++ frontend/src/api/index.ts | 1 + frontend/src/views/PredictDashboard.vue | 135 ++++++++++++++++++++++++ 3 files changed, 227 insertions(+) diff --git a/backend/app/api/predict.py b/backend/app/api/predict.py index ce005f8e..badf7ea4 100644 --- a/backend/app/api/predict.py +++ b/backend/app/api/predict.py @@ -161,6 +161,97 @@ def api_cvp_detailed(data: dict): raise HTTPException(400, f"CVP详细分析失败: {str(e)}") +# ── 相关成本决策(CMA P2商业决策分析25%权重核心) ────────────────── + + +@router.post("/relevant-decision") +def api_relevant_decision(data: dict): + """相关成本决策分析(CMA P2 商业决策分析核心内容) + 场景: make-or-buy自制外购 / special-order特殊订单 / product-mix产品组合 + """ + try: + decision_type = data.get("type", "make_or_buy") + + if decision_type == "make_or_buy": + # 自制vs外购决策 + # 相关成本 = 增量成本(只有随决策变化的成本才是相关的) + make_var_cost = float(data.get("make_variable_cost", 0)) # 自制单位变动成本 + make_fixed = float(data.get("make_fixed_cost", 0)) # 自制新增固定成本 + buy_price = float(data.get("buy_price", 0)) # 外购单价 + demand = float(data.get("demand", 0)) # 需求量 + existing_fixed = float(data.get("existing_fixed_cost", 0)) # 现有固定成本(无关成本,自制不增加则忽略) + + make_total = make_var_cost * demand + make_fixed + buy_total = buy_price * demand + diff = buy_total - make_total # >0自制省钱 + + return { + "type": "自制vs外购", + "make_total_cost": round(make_total, 2), + "buy_total_cost": round(buy_total, 2), + "difference": round(diff, 2), + "recommendation": "自制" if diff > 0 else "外购", + "reason": f"自制总成本{make_total:.2f} vs 外购总成本{buy_total:.2f},{'自制节省' + str(round(diff,2)) if diff > 0 else '外购节省' + str(round(-diff,2))}", + "unit_make_cost": round(make_var_cost + (make_fixed / demand if demand else 0), 2), + "unit_buy_price": buy_price, + "indifferent_point": round(make_fixed / (buy_price - make_var_cost), 2) if buy_price > make_var_cost else None, + "notes": "仅考虑相关成本(增量成本);现有固定成本若不受决策影响则无关", + } + + elif decision_type == "special_order": + # 特殊订单决策(有剩余产能时,只要价格>单位变动成本即接受) + normal_price = float(data.get("normal_price", 0)) + special_price = float(data.get("special_price", 0)) + var_cost = float(data.get("variable_cost", 0)) + order_qty = float(data.get("order_qty", 0)) + capacity_used = float(data.get("capacity_used", 0)) # 特殊订单占用产能% + extra_fixed = float(data.get("extra_fixed_cost", 0)) # 一次性额外固定成本 + + contribution_per_unit = special_price - var_cost + total_contribution = contribution_per_unit * order_qty - extra_fixed + accept = total_contribution > 0 and capacity_used <= 100 + + return { + "type": "特殊订单", + "unit_contribution": round(contribution_per_unit, 2), + "total_contribution": round(total_contribution, 2), + "extra_fixed_cost": extra_fixed, + "capacity_used_pct": capacity_used, + "recommendation": "接受" if accept else "拒绝", + "reason": f"单价{special_price} - 变动成本{var_cost} = 单位贡献{contribution_per_unit:.2f}" + + (f",共{total_contribution:.2f} > 0 且产能{capacity_used}%够用 → 接受(增量利润)" if accept else + f",总贡献{total_contribution:.2f} ≤ 0 或产能不足 → 拒绝"), + "notes": "有剩余产能时,只要价格>变动成本且不冲击正常市场即可接受;固定成本无关", + } + + elif decision_type == "product_mix": + # 产品组合决策(约束理论:单位约束资源的边际贡献最大者优先) + products = data.get("products", []) # [{name, price, var_cost, constraint_usage, demand}] + results = [] + for p in products: + cm_per_unit = float(p.get("price", 0)) - float(p.get("var_cost", 0)) + cm_per_constraint = cm_per_unit / float(p.get("constraint_usage", 1)) + results.append({ + "name": p.get("name", ""), + "unit_contribution": round(cm_per_unit, 2), + "constraint_usage": float(p.get("constraint_usage", 1)), + "contribution_per_constraint": round(cm_per_constraint, 2), + "demand": float(p.get("demand", 0)), + }) + # 按单位约束资源贡献排序(约束理论优先) + results.sort(key=lambda x: x["contribution_per_constraint"], reverse=True) + return { + "type": "产品组合(约束理论)", + "ranking": results, + "recommendation": f"优先生产「{results[0]['name']}」(单位约束贡献{results[0]['contribution_per_constraint']}最高)", + "notes": "瓶颈资源下,按单位约束资源的边际贡献排序,而非单位边际贡献", + } + + raise HTTPException(400, "未知决策类型: " + str(decision_type)) + except Exception as e: + raise HTTPException(400, f"相关成本决策失败: {str(e)}") + + # ── 现金流预测(AI事前预警) ──────────────────────────────────── diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 2764a3a4..e4dca5b4 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -218,6 +218,7 @@ export const predictApi = { investment: (data: any) => api.post('/predict/investment', data), sensitivity: (data: any) => api.post('/predict/sensitivity', data), scenario: (data: any) => api.post('/predict/scenario', data), + relevantDecision: (data: any) => api.post('/predict/relevant-decision', data), growthQuality: (data: any) => api.post('/predict/growth-quality', data), // AI事前预警 cashForecast: (data: any) => api.post('/predict/cash-forecast', data), diff --git a/frontend/src/views/PredictDashboard.vue b/frontend/src/views/PredictDashboard.vue index a72923e2..107b398c 100644 --- a/frontend/src/views/PredictDashboard.vue +++ b/frontend/src/views/PredictDashboard.vue @@ -217,6 +217,112 @@ + + + + + + 自制 vs 外购 + 特殊订单 + 产品组合(约束理论) + + + + + + + + + + + + + + + 计算决策 + + + + + + + + + {{ formatRelNum(relResult.make_total_cost) }} + {{ formatRelNum(relResult.buy_total_cost) }} + {{ formatRelNum(relResult.unit_make_cost) }} + {{ formatRelNum(relResult.unit_buy_price) }} + {{ relResult.indifferent_point != null ? formatRelNum(relResult.indifferent_point) + '件' : '—' }} + + {{ relResult.notes }} + + + + + + + + + + + + + + + + + 计算决策 + + + + + + + + + {{ formatRelNum(relResult.unit_contribution) }} + {{ formatRelNum(relResult.total_contribution) }} + {{ relResult.capacity_used_pct }}% + {{ formatRelNum(relResult.extra_fixed_cost) }} + + {{ relResult.notes }} + + + + + + + + + +
+ + + + + +
+ + 添加产品 + 计算决策 +
+
+ + + + + + + + + + + + {{ relResult.notes }} + + +
+
+ @@ -455,6 +561,35 @@ const scenarioForm = ref({ }) const scenarioResult = ref({}) +// ── 相关成本决策(CMA P2商业决策分析) ── +const relDecisionType = ref('make_or_buy') +const relResult = ref(null) +const makeBuy = ref({ demand: 10000, make_variable_cost: 80, make_fixed_cost: 50000, buy_price: 90 }) +const specialOrder = ref({ normal_price: 150, special_price: 120, variable_cost: 95, order_qty: 1000, capacity_used: 60, extra_fixed_cost: 0 }) +const productMix = ref({ products: [ + { name: 'A产品', price: 100, var_cost: 60, constraint_usage: 2, demand: 1000 }, + { name: 'B产品', price: 150, var_cost: 100, constraint_usage: 1, demand: 800 }, + { name: 'C产品', price: 200, var_cost: 150, constraint_usage: 3, demand: 500 }, +]}) + +function formatRelNum(v: any) { + if (v === null || v === undefined) return '--' + return Number(v).toLocaleString('zh-CN', { maximumFractionDigits: 2 }) +} + +async function runRelevantDecision() { + try { + let payload: any = { type: relDecisionType.value } + if (relDecisionType.value === 'make_or_buy') payload = { ...payload, ...makeBuy.value } + else if (relDecisionType.value === 'special_order') payload = { ...payload, ...specialOrder.value } + else payload = { ...payload, products: productMix.value.products } + relResult.value = await predictApi.relevantDecision(payload) as any + } catch (e: any) { + relResult.value = null + ElMessage.error(e?.response?.data?.detail || e?.message || '决策计算失败') + } +} + function fillScenarioExample() { scenarioForm.value = { optimistic: { revenue: 130, cost: 90 },