fix: 利润机会池改为门店级计算,修复整体平均掩盖问题门店

问题:利润机会池6项中有4项返回0,总额仅15.3万
原因:用整体平均费率判断是否超标,整体已达标则机会=0,
      掩盖了个别门店严重超标的问题

修复:
1. 食材成本差异回收:改用 mv_store_theoretical_actual_cost_monthly
   (门店级理论vs实际成本),只对正差异(超耗)门店求和 × 30%
   修复前 0 → 修复后 1,898,587
2. 标准店人工优化:改为门店级,只对费率>24%的门店计算机会
   修复前 0 → 修复后 1,561,133(58家超标)
3. 标准店能源优化:改为门店级,只对费率>5%的门店计算机会
   修复前 0 → 修复后 466,609(49家超标)
4. 平台佣金优化:改为门店级,只对佣金率>20%的门店计算机会
   修复前 0 → 修复后 3,430(1家超标)
5. 高优惠门店治理和SKU复杂度压缩逻辑不变

总额:152,995 → 4,082,755(约408万)

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-13 14:45:43 +08:00
parent 85b58c53b1
commit 8fd74fd78c
+66 -59
View File
@@ -1144,6 +1144,7 @@ router.get('/overview/store-profit-ranking', async (req: AuthRequest, res) => {
}) })
// 利润机会池 // 利润机会池
// 门店级计算:只对超标门店计算机会金额,避免整体平均掩盖问题门店
router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => { router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
try { try {
const month = parseMonth(req) const month = parseMonth(req)
@@ -1154,64 +1155,67 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
sku_abc AS ( sku_abc AS (
SELECT * FROM analytics.mv_dish_sku_abc_monthly WHERE month_start = $1 SELECT * FROM analytics.mv_dish_sku_abc_monthly WHERE month_start = $1
), ),
standard_stores AS ( -- 食材成本差异:改用 mv_store_theoretical_actual_cost_monthly(门店级理论vs实际成本)
SELECT r.store_code, r.store_name, r.received,
COALESCE(e.actual_food_cost, 0) AS actual_food_cost,
COALESCE(e.operating_expense, 0) AS operating_expense,
COALESCE(e.wage_expense, 0) AS wage_expense,
COALESCE(e.utility_expense, 0) AS utility_expense,
r.theoretical_margin_pct, r.discount_rate_pct
FROM risk r
LEFT JOIN analytics.mv_store_operating_expense_monthly e
ON r.store_code = e.sales_store_code AND e.report_month = $1::date
WHERE r.received IS NOT NULL AND r.received > 0
),
cost_diff AS ( cost_diff AS (
SELECT SELECT
round(sum(actual_food_cost)::numeric, 2) AS actual_cost, count(*) FILTER (WHERE food_cost_variance > 0) AS over_cost_stores,
round(sum(received * (1 - COALESCE(theoretical_margin_pct, 0) / 100))::numeric, 2) AS theoretical_cost round(sum(food_cost_variance) FILTER (WHERE food_cost_variance > 0)::numeric, 2) AS total_positive_variance,
FROM standard_stores round(sum(theoretical_cost)::numeric, 2) AS total_theoretical_cost,
round(sum(actual_food_cost)::numeric, 2) AS total_actual_cost,
round(sum(sales_received)::numeric, 2) AS total_received
FROM analytics.mv_store_theoretical_actual_cost_monthly
WHERE month_start = $1
), ),
cost_diff_stores AS ( cost_diff_stores AS (
SELECT store_name, received, SELECT store_name, sales_received AS received,
round(actual_food_cost::numeric, 2) AS actual_food_cost, round(food_cost_variance::numeric, 2) AS diff_amount,
round((received * (1 - COALESCE(theoretical_margin_pct, 0) / 100))::numeric, 2) AS theoretical_cost, round(actual_food_cost_rate_pct::numeric, 2) AS actual_cost_rate,
round((actual_food_cost - received * (1 - COALESCE(theoretical_margin_pct, 0) / 100))::numeric, 2) AS diff_amount, round(theoretical_cost_rate_pct::numeric, 2) AS theoretical_cost_rate
round((actual_food_cost / nullif(received, 0) * 100)::numeric, 2) AS actual_cost_rate, FROM analytics.mv_store_theoretical_actual_cost_monthly
round((received * (1 - COALESCE(theoretical_margin_pct, 0) / 100) / nullif(received, 0) * 100)::numeric, 2) AS theoretical_cost_rate WHERE month_start = $1 AND food_cost_variance > 0
FROM standard_stores ORDER BY food_cost_variance DESC
WHERE actual_food_cost IS NOT NULL
ORDER BY (actual_food_cost - received * (1 - COALESCE(theoretical_margin_pct, 0) / 100)) DESC
LIMIT 5 LIMIT 5
), ),
-- 人工优化:门店级,只对费率>24%的门店计算机会
labor AS ( labor AS (
SELECT SELECT
count(*) FILTER (WHERE wage_rate_pct > 24) AS over_wage_stores,
round(sum(wage_expense) FILTER (WHERE wage_rate_pct > 24)::numeric, 2) AS over_wage_total,
round(sum(wage_expense * GREATEST(1 - 24.0 / nullif(wage_rate_pct, 0), 0)) FILTER (WHERE wage_rate_pct > 24)::numeric, 2) AS opportunity,
round(sum(wage_expense)::numeric, 2) AS total_wage, round(sum(wage_expense)::numeric, 2) AS total_wage,
round(sum(received)::numeric, 2) AS total_received round(sum(received)::numeric, 2) AS total_received
FROM standard_stores FROM analytics.mv_store_operating_expense_monthly
WHERE report_month = $1::date AND wage_expense IS NOT NULL AND wage_expense > 0 AND received > 0
), ),
labor_stores AS ( labor_stores AS (
SELECT store_name, received, SELECT sales_store_name AS store_name, received,
round(wage_expense::numeric, 2) AS wage, round(wage_expense::numeric, 2) AS wage,
round((wage_expense / nullif(received, 0) * 100)::numeric, 2) AS wage_rate round(wage_rate_pct::numeric, 2) AS wage_rate
FROM standard_stores FROM analytics.mv_store_operating_expense_monthly
WHERE wage_expense IS NOT NULL WHERE report_month = $1::date AND wage_expense IS NOT NULL AND wage_expense > 0 AND received > 0
ORDER BY wage_expense / nullif(received, 0) DESC AND wage_rate_pct > 24
ORDER BY wage_rate_pct DESC
LIMIT 5 LIMIT 5
), ),
-- 能源优化:门店级,只对费率>5%的门店计算机会
energy AS ( energy AS (
SELECT SELECT
count(*) FILTER (WHERE utility_rate_pct > 5) AS over_utility_stores,
round(sum(utility_expense) FILTER (WHERE utility_rate_pct > 5)::numeric, 2) AS over_utility_total,
round(sum(utility_expense * GREATEST(1 - 5.0 / nullif(utility_rate_pct, 0), 0)) FILTER (WHERE utility_rate_pct > 5)::numeric, 2) AS opportunity,
round(sum(utility_expense)::numeric, 2) AS total_utility, round(sum(utility_expense)::numeric, 2) AS total_utility,
round(sum(received)::numeric, 2) AS total_received round(sum(received)::numeric, 2) AS total_received
FROM standard_stores FROM analytics.mv_store_operating_expense_monthly
WHERE report_month = $1::date AND utility_expense IS NOT NULL AND utility_expense > 0 AND received > 0
), ),
energy_stores AS ( energy_stores AS (
SELECT store_name, received, SELECT sales_store_name AS store_name, received,
round(utility_expense::numeric, 2) AS utility, round(utility_expense::numeric, 2) AS utility,
round((utility_expense / nullif(received, 0) * 100)::numeric, 2) AS utility_rate round(utility_rate_pct::numeric, 2) AS utility_rate
FROM standard_stores FROM analytics.mv_store_operating_expense_monthly
WHERE utility_expense IS NOT NULL WHERE report_month = $1::date AND utility_expense IS NOT NULL AND utility_expense > 0 AND received > 0
ORDER BY utility_expense / nullif(received, 0) DESC AND utility_rate_pct > 5
ORDER BY utility_rate_pct DESC
LIMIT 5 LIMIT 5
), ),
discount AS ( discount AS (
@@ -1230,19 +1234,22 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
ORDER BY discount_rate_pct DESC ORDER BY discount_rate_pct DESC
LIMIT 5 LIMIT 5
), ),
-- 平台佣金优化:门店级,只对佣金率>20%的门店计算机会
platform AS ( platform AS (
SELECT SELECT
count(*) FILTER (WHERE delivery_commission_expense > 0 AND delivery_commission_expense / nullif(received, 0) * 100 > 20) AS over_commission_stores,
round(sum(delivery_commission_expense)::numeric, 2) AS total_commission, round(sum(delivery_commission_expense)::numeric, 2) AS total_commission,
round(sum(received)::numeric, 2) AS platform_received round(sum(received)::numeric, 2) AS platform_received,
round(sum(delivery_commission_expense * GREATEST(1 - 20.0 / nullif(delivery_commission_expense / nullif(received, 0) * 100, 0), 0)) FILTER (WHERE delivery_commission_expense > 0 AND delivery_commission_expense / nullif(received, 0) * 100 > 20)::numeric, 2) AS opportunity
FROM analytics.mv_store_operating_expense_monthly FROM analytics.mv_store_operating_expense_monthly
WHERE report_month = $1::date AND delivery_commission_expense IS NOT NULL WHERE report_month = $1::date AND delivery_commission_expense IS NOT NULL AND received > 0
), ),
platform_stores AS ( platform_stores AS (
SELECT e.sales_store_code AS store_code, e.received, SELECT e.sales_store_code AS store_code, e.received,
round(e.delivery_commission_expense::numeric, 2) AS commission, round(e.delivery_commission_expense::numeric, 2) AS commission,
round((e.delivery_commission_expense / nullif(e.received, 0) * 100)::numeric, 2) AS commission_rate round((e.delivery_commission_expense / nullif(e.received, 0) * 100)::numeric, 2) AS commission_rate
FROM analytics.mv_store_operating_expense_monthly e FROM analytics.mv_store_operating_expense_monthly e
WHERE e.report_month = $1::date AND e.delivery_commission_expense IS NOT NULL WHERE e.report_month = $1::date AND e.delivery_commission_expense IS NOT NULL AND e.received > 0
ORDER BY e.delivery_commission_expense / nullif(e.received, 0) DESC ORDER BY e.delivery_commission_expense / nullif(e.received, 0) DESC
LIMIT 5 LIMIT 5
), ),
@@ -1264,47 +1271,47 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
'items', json_build_array( 'items', json_build_array(
json_build_object( json_build_object(
'category', '食材成本差异回收', 'category', '食材成本差异回收',
'baseline', (SELECT round((actual_cost - theoretical_cost)::numeric, 2) FROM cost_diff), 'baseline', (SELECT total_positive_variance FROM cost_diff),
'target_pct', 30, 'target_pct', 30,
'opportunity', GREATEST(round((SELECT (actual_cost - theoretical_cost) FROM cost_diff) * 0.30, 2), 0), 'opportunity', round((SELECT total_positive_variance FROM cost_diff) * 0.30, 2),
'confidence', '中高', 'confidence', '中高',
'owner', '商品/供应链/门店', 'owner', '商品/供应链/门店',
'evidence', '采购价差、用量差、盘点差、报损差', 'evidence', '采购价差、用量差、盘点差、报损差',
'detail', (SELECT 'detail', (SELECT
'实际食材成本' || (SELECT actual_cost FROM cost_diff) || '元 vs 理论成本' || (SELECT theoretical_cost FROM cost_diff) || '元,差异' || round((SELECT actual_cost - theoretical_cost FROM cost_diff)::numeric, 2) || '元(成本率' || round((SELECT actual_cost FROM cost_diff) / nullif((SELECT sum(received) FROM standard_stores), 0) * 100, 2) || '% vs 理论' || round((SELECT theoretical_cost FROM cost_diff) / nullif((SELECT sum(received) FROM standard_stores), 0) * 100, 2) || '%\n\n' || '超耗门店' || (SELECT over_cost_stores FROM cost_diff) || '家,实际食材成本' || (SELECT total_actual_cost FROM cost_diff) || '元 vs 理论成本' || (SELECT total_theoretical_cost FROM cost_diff) || '元,正差异合计' || (SELECT total_positive_variance FROM cost_diff) || '\n\n' ||
'差异TOP5门店(需优先排查):\n' || '超耗TOP5门店(需优先排查):\n' ||
string_agg(store_name || ':差异' || diff_amount || '元(实际成本率' || actual_cost_rate || '% vs 理论' || theoretical_cost_rate || '%,实收' || received || '元)', '\n') || string_agg(store_name || ':差异' || diff_amount || '元(实际成本率' || actual_cost_rate || '% vs 理论' || theoretical_cost_rate || '%,实收' || received || '元)', '\n') ||
'\n\n行动指向:\n1)上述5家门店实际成本率均远超理论值,需逐店排查采购单价与BOM标准价差异;\n2)盘点差——核查月末盘点与系统库存一致性,差异>3%需复盘;\n3)报损差——对比报损记录与行业基准,报损率>2%的门店需检查存储和加工流程;\n430天目标:将TOP5门店成本率降低5个百分点,预计回收' || round((SELECT (actual_cost - theoretical_cost) FROM cost_diff) * 0.30, 2) || '元。' '\n\n行动指向:\n1)上述5家门店实际成本率均远超理论值,需逐店排查采购单价与BOM标准价差异;\n2)盘点差——核查月末盘点与系统库存一致性,差异>3%需复盘;\n3)报损差——对比报损记录与行业基准,报损率>2%的门店需检查存储和加工流程;\n430天目标:回收正差异的30%,预计节约' || round((SELECT total_positive_variance FROM cost_diff) * 0.30, 2) || '元。'
FROM cost_diff_stores) FROM cost_diff_stores)
), ),
json_build_object( json_build_object(
'category', '标准店人工优化', 'category', '标准店人工优化',
'baseline', (SELECT round(total_wage / total_received * 100, 2) FROM labor), 'baseline', (SELECT round(total_wage / nullif(total_received, 0) * 100, 2) FROM labor),
'target_pct', 100, 'target_pct', 100,
'opportunity', GREATEST(round((SELECT total_wage * (1 - 24.0 / nullif(total_wage / total_received * 100, 0)) FROM labor), 2), 0), 'opportunity', (SELECT opportunity FROM labor),
'confidence', '中', 'confidence', '中',
'owner', '运营/人力', 'owner', '运营/人力',
'evidence', '工时、工资、餐段销售与服务质量', 'evidence', '工时、工资、餐段销售与服务质量',
'detail', (SELECT 'detail', (SELECT
'标准店人工合计' || (SELECT total_wage FROM labor) || '元,费率' || round((SELECT total_wage FROM labor) / nullif((SELECT total_received FROM labor), 0) * 100, 2) || '%,目标降至24%\n\n' || '人工费率超24%的门店共' || (SELECT over_wage_stores FROM labor) || '家,超标门店人工合计' || (SELECT over_wage_total FROM labor) || '元,理论可优化' || (SELECT opportunity FROM labor) || '元\n\n' ||
'人工费率TOP5门店(需重点督导):\n' || '人工费率TOP5超标门店(需重点督导):\n' ||
string_agg(store_name || ':人工' || wage || '元(费率' || wage_rate || '%,实收' || received || '元)', '\n') || COALESCE(string_agg(store_name || ':人工' || wage || '元(费率' || wage_rate || '%,实收' || received || '元)', '\n'), '无超标门店') ||
'\n\n行动指向:\n1)上述门店人工费率远超24%目标,需核查排班与实际打卡工时,识别冗余工时;\n2)低峰时段用小时工替代月薪员工,预计可降费率2-3个百分点;\n3)对费率>30%的门店启动人效专项督导,要求店长提交排班优化方案;\n430天目标:TOP5门店人工费率平均降低2个百分点。' '\n\n行动指向:\n1)上述门店人工费率远超24%目标,需核查排班与实际打卡工时,识别冗余工时;\n2)低峰时段用小时工替代月薪员工,预计可降费率2-3个百分点;\n3)对费率>30%的门店启动人效专项督导,要求店长提交排班优化方案;\n430天目标:将超标门店人工费率降至24%以内。'
FROM labor_stores) FROM labor_stores)
), ),
json_build_object( json_build_object(
'category', '标准店能源优化', 'category', '标准店能源优化',
'baseline', (SELECT round(total_utility / total_received * 100, 2) FROM energy), 'baseline', (SELECT round(total_utility / nullif(total_received, 0) * 100, 2) FROM energy),
'target_pct', 100, 'target_pct', 100,
'opportunity', GREATEST(round((SELECT total_utility * (1 - 5.0 / nullif(total_utility / total_received * 100, 0)) FROM energy), 2), 0), 'opportunity', (SELECT opportunity FROM energy),
'confidence', '中', 'confidence', '中',
'owner', '工程/门店', 'owner', '工程/门店',
'evidence', '账单/抄表、面积、营业时长', 'evidence', '账单/抄表、面积、营业时长',
'detail', (SELECT 'detail', (SELECT
'标准店水电合计' || (SELECT total_utility FROM energy) || '元,费率' || round((SELECT total_utility FROM energy) / nullif((SELECT total_received FROM energy), 0) * 100, 2) || '%,目标降至5%\n\n' || '水电费率超5%的门店共' || (SELECT over_utility_stores FROM energy) || '家,超标门店水电合计' || (SELECT over_utility_total FROM energy) || '元,理论可优化' || (SELECT opportunity FROM energy) || '元\n\n' ||
'水电费率TOP5门店(需排查设备):\n' || '水电费率TOP5超标门店(需排查设备):\n' ||
string_agg(store_name || ':水电' || utility || '元(费率' || utility_rate || '%,实收' || received || '元)', '\n') || COALESCE(string_agg(store_name || ':水电' || utility || '元(费率' || utility_rate || '%,实收' || received || '元)', '\n'), '无超标门店') ||
'\n\n行动指向:\n1)上述门店水电费率远超5%目标,需排查是否存在设备老化、管道泄漏或空调空转;\n2)对比近3个月水电账单,波动>20%的门店需现场检查;\n3)缩短非营业时段的照明和空调,预计可降费率0.3-0.5个百分点;\n430天目标:TOP5门店水电费率平均降低0.5个百分点。' '\n\n行动指向:\n1)上述门店水电费率远超5%目标,需排查是否存在设备老化、管道泄漏或空调空转;\n2)对比近3个月水电账单,波动>20%的门店需现场检查;\n3)缩短非营业时段的照明和空调,预计可降费率0.3-0.5个百分点;\n430天目标:将超标门店水电费率降至5%以内。'
FROM energy_stores) FROM energy_stores)
), ),
json_build_object( json_build_object(
@@ -1326,15 +1333,15 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
'category', '平台佣金优化', 'category', '平台佣金优化',
'baseline', (SELECT round(total_commission / nullif(platform_received, 0) * 100, 2) FROM platform), 'baseline', (SELECT round(total_commission / nullif(platform_received, 0) * 100, 2) FROM platform),
'target_pct', 100, 'target_pct', 100,
'opportunity', GREATEST(round((SELECT total_commission * (1 - 20.0 / nullif(total_commission / nullif(platform_received, 0) * 100, 0)) FROM platform), 2), 0), 'opportunity', (SELECT opportunity FROM platform),
'confidence', '中低', 'confidence', '中低',
'owner', '外卖/采购', 'owner', '外卖/采购',
'evidence', '平台结算单与订单对账', 'evidence', '平台结算单与订单对账',
'detail', (SELECT 'detail', (SELECT
'平台佣金合计' || (SELECT total_commission FROM platform) || '元,佣金率' || round((SELECT total_commission FROM platform) / nullif((SELECT platform_received FROM platform), 0) * 100, 2) || '%。\n\n' || '平台佣金合计' || (SELECT total_commission FROM platform) || '元,整体佣金率' || round((SELECT total_commission FROM platform) / nullif((SELECT platform_received FROM platform), 0) * 100, 2) || '%,佣金率超20%的门店' || (SELECT over_commission_stores FROM platform) || '家\n\n' ||
'佣金费率TOP5门店(需对账核查):\n' || '佣金费率TOP5门店(需对账核查):\n' ||
COALESCE(string_agg(store_code || ':佣金' || commission || '元(费率' || commission_rate || '%,实收' || received || '元)', '\n'), '无数据') || COALESCE(string_agg(store_code || ':佣金' || commission || '元(费率' || commission_rate || '%,实收' || received || '元)', '\n'), '无数据') ||
'\n\n行动指向:\n1当前整体佣金率6.08%已低于20%目标,暂无大幅优化空间;\n2)上述门店佣金费率偏高,需逐月核对平台结算单与订单明细,识别多扣佣金;\n3)平台活动费与佣金应分离核算,避免活动费被计入佣金;\n4)提升自配送比例,降低对平台配送依赖;\n5)30天目标:完成TOP5门店平台结算单对账。' '\n\n行动指向:\n1佣金率超20%的门店需逐月核对平台结算单与订单明细,识别多扣佣金;\n2)平台活动费与佣金应分离核算,避免活动费被计入佣金;\n3)提升自配送比例,降低对平台配送依赖;\n4)30天目标:完成TOP5门店平台结算单对账。'
FROM platform_stores) FROM platform_stores)
), ),
json_build_object( json_build_object(