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 },