feat(P1-3): 相关成本决策模块 — 官方25%权重'商业决策分析'核心

新增 /predict/relevant-decision API + 前端Tab(3场景):
1. 自制vs外购: 相关成本比较+无差别点(增量成本视角)
2. 特殊订单: 剩余产能下价格>变动成本即接受(增量利润)
3. 产品组合(约束理论): 单位约束资源边际贡献排序

验证: 自制850k vs 外购900k→自制; 特殊订单单位贡献25→接受;
      产品组合B产品单位约束贡献50最高→优先
This commit is contained in:
Hermes CI Fix
2026-08-26 22:38:37 +08:00
parent 35974514da
commit e330545d59
3 changed files with 227 additions and 0 deletions
+1
View File
@@ -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),
+135
View File
@@ -217,6 +217,112 @@
</el-row>
</el-tab-pane>
<!-- Tab: 相关成本决策CMA P2商业决策分析 -->
<el-tab-pane label="相关成本决策" name="relevantDecision">
<el-row :gutter="16">
<el-col :span="24">
<el-radio-group v-model="relDecisionType" style="margin-bottom:16px;">
<el-radio-button value="make_or_buy">自制 vs 外购</el-radio-button>
<el-radio-button value="special_order">特殊订单</el-radio-button>
<el-radio-button value="product_mix">产品组合(约束理论)</el-radio-button>
</el-radio-group>
</el-col>
</el-row>
<!-- 自制vs外购 -->
<el-row v-if="relDecisionType === 'make_or_buy'" :gutter="16">
<el-col :span="10">
<el-card>
<template #header>输入参数</template>
<el-form label-width="140px" size="small">
<el-form-item label="需求量(件)"><el-input-number v-model="makeBuy.demand" :min="0" style="width:200px;" /></el-form-item>
<el-form-item label="自制单位变动成本"><el-input-number v-model="makeBuy.make_variable_cost" :min="0" style="width:200px;" /></el-form-item>
<el-form-item label="自制新增固定成本"><el-input-number v-model="makeBuy.make_fixed_cost" :min="0" style="width:200px;" /></el-form-item>
<el-form-item label="外购单价"><el-input-number v-model="makeBuy.buy_price" :min="0" style="width:200px;" /></el-form-item>
<el-button type="primary" size="small" @click="runRelevantDecision">计算决策</el-button>
</el-form>
</el-card>
</el-col>
<el-col :span="14">
<el-card v-if="relResult">
<template #header>决策结果</template>
<el-alert :title="'建议:' + relResult.recommendation" :type="relResult.recommendation === '自制' ? 'primary' : 'warning'" :description="relResult.reason" show-icon :closable="false" style="margin-bottom:12px;" />
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="自制总成本">{{ formatRelNum(relResult.make_total_cost) }}</el-descriptions-item>
<el-descriptions-item label="外购总成本">{{ formatRelNum(relResult.buy_total_cost) }}</el-descriptions-item>
<el-descriptions-item label="单位自制成本">{{ formatRelNum(relResult.unit_make_cost) }}</el-descriptions-item>
<el-descriptions-item label="外购单价">{{ formatRelNum(relResult.unit_buy_price) }}</el-descriptions-item>
<el-descriptions-item label="无差别点" :span="2">{{ relResult.indifferent_point != null ? formatRelNum(relResult.indifferent_point) + '件' : '—' }}</el-descriptions-item>
</el-descriptions>
<el-text size="small" type="info" style="margin-top:8px;display:block;">{{ relResult.notes }}</el-text>
</el-card>
</el-col>
</el-row>
<!-- 特殊订单 -->
<el-row v-if="relDecisionType === 'special_order'" :gutter="16">
<el-col :span="10">
<el-card>
<template #header>输入参数</template>
<el-form label-width="140px" size="small">
<el-form-item label="正常售价"><el-input-number v-model="specialOrder.normal_price" :min="0" style="width:200px;" /></el-form-item>
<el-form-item label="特殊订单价"><el-input-number v-model="specialOrder.special_price" :min="0" style="width:200px;" /></el-form-item>
<el-form-item label="单位变动成本"><el-input-number v-model="specialOrder.variable_cost" :min="0" style="width:200px;" /></el-form-item>
<el-form-item label="订单数量"><el-input-number v-model="specialOrder.order_qty" :min="0" style="width:200px;" /></el-form-item>
<el-form-item label="产能占用%"><el-input-number v-model="specialOrder.capacity_used" :min="0" :max="100" style="width:200px;" /></el-form-item>
<el-form-item label="额外固定成本"><el-input-number v-model="specialOrder.extra_fixed_cost" :min="0" style="width:200px;" /></el-form-item>
<el-button type="primary" size="small" @click="runRelevantDecision">计算决策</el-button>
</el-form>
</el-card>
</el-col>
<el-col :span="14">
<el-card v-if="relResult">
<template #header>决策结果</template>
<el-alert :title="'建议:' + relResult.recommendation" :type="relResult.recommendation === '接受' ? 'success' : 'danger'" :description="relResult.reason" show-icon :closable="false" style="margin-bottom:12px;" />
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="单位贡献">{{ formatRelNum(relResult.unit_contribution) }}</el-descriptions-item>
<el-descriptions-item label="总贡献">{{ formatRelNum(relResult.total_contribution) }}</el-descriptions-item>
<el-descriptions-item label="产能占用">{{ relResult.capacity_used_pct }}%</el-descriptions-item>
<el-descriptions-item label="额外固定成本">{{ formatRelNum(relResult.extra_fixed_cost) }}</el-descriptions-item>
</el-descriptions>
<el-text size="small" type="info" style="margin-top:8px;display:block;">{{ relResult.notes }}</el-text>
</el-card>
</el-col>
</el-row>
<!-- 产品组合 -->
<el-row v-if="relDecisionType === 'product_mix'" :gutter="16">
<el-col :span="12">
<el-card>
<template #header>产品参数价格/变动成本/约束资源消耗/需求</template>
<div v-for="(p, idx) in productMix.products" :key="idx" style="display:flex;gap:8px;margin-bottom:8px;align-items:center;">
<el-input v-model="p.name" placeholder="产品名" style="width:100px;" size="small" />
<el-input-number v-model="p.price" :min="0" placeholder="价格" style="width:110px;" size="small" />
<el-input-number v-model="p.var_cost" :min="0" placeholder="变动成本" style="width:110px;" size="small" />
<el-input-number v-model="p.constraint_usage" :min="0.1" placeholder="约束消耗" style="width:100px;" size="small" />
<el-input-number v-model="p.demand" :min="0" placeholder="需求" style="width:90px;" size="small" />
</div>
<el-button size="small" @click="productMix.products.push({ name: '新产品', price: 100, var_cost: 60, constraint_usage: 1, demand: 100 })">+ 添加产品</el-button>
<el-button size="small" type="primary" @click="runRelevantDecision" style="margin-left:8px;">计算决策</el-button>
</el-card>
</el-col>
<el-col :span="12">
<el-card v-if="relResult">
<template #header>决策结果</template>
<el-alert :title="relResult.recommendation" type="success" :closable="false" style="margin-bottom:12px;" />
<el-table :data="relResult.ranking" border stripe size="small">
<el-table-column prop="name" label="产品" />
<el-table-column prop="unit_contribution" label="单位贡献" />
<el-table-column prop="constraint_usage" label="约束消耗" />
<el-table-column prop="contribution_per_constraint" label="单位约束贡献" />
<el-table-column prop="demand" label="需求" />
</el-table>
<el-text size="small" type="info" style="margin-top:8px;display:block;">{{ relResult.notes }}</el-text>
</el-card>
</el-col>
</el-row>
</el-tab-pane>
<!-- Tab 5: 现金流预测AI事前预警 -->
<el-tab-pane label="现金流预测" name="cashFlow">
<el-row :gutter="16">
@@ -455,6 +561,35 @@ const scenarioForm = ref({
})
const scenarioResult = ref<any>({})
// ── 相关成本决策(CMA P2商业决策分析) ──
const relDecisionType = ref('make_or_buy')
const relResult = ref<any>(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 },