fix: 修复费用总览API两个错误

1. 费用率公式反转: (1-expense/received)*100 → expense/matched_received*100
   修复前: 46.83%(实际是非费用率)→ 修复后: 53.77%(正确费用率)
2. 利润口径不一致: sum(received)含全部94家门店,但费用只含92家匹配门店
   修复: 利润和各项率指标分母改为仅匹配门店实收(matched_received)
   实际贡献利润: 7,703,472→6,982,110 贡献率: 11.96%→10.96%
This commit is contained in:
freedakgmail
2026-08-11 19:54:23 +08:00
parent a090fc7c19
commit 2a3805c44f
+12 -11
View File
@@ -57,6 +57,7 @@ router.get('/overview', async (req: AuthRequest, res) => {
round(sum(consumption)::numeric, 2) AS total_consumption, round(sum(consumption)::numeric, 2) AS total_consumption,
round(sum(discount)::numeric, 2) AS total_discount, round(sum(discount)::numeric, 2) AS total_discount,
round(sum(received)::numeric, 2) AS total_received, round(sum(received)::numeric, 2) AS total_received,
round(sum(received) FILTER (WHERE operating_expense IS NOT NULL)::numeric, 2) AS matched_received,
round(sum(received) / nullif(sum(bill_count), 0), 2) AS avg_bill_value, round(sum(received) / nullif(sum(bill_count), 0), 2) AS avg_bill_value,
round(sum(operating_expense)::numeric, 2) AS total_expense, round(sum(operating_expense)::numeric, 2) AS total_expense,
round(sum(wage_expense)::numeric, 2) AS total_wage, round(sum(wage_expense)::numeric, 2) AS total_wage,
@@ -70,18 +71,18 @@ router.get('/overview', async (req: AuthRequest, res) => {
round(sum(theoretical_cost)::numeric, 2) AS total_theoretical_cost, round(sum(theoretical_cost)::numeric, 2) AS total_theoretical_cost,
round(sum(actual_store_contribution)::numeric, 2) AS total_contribution, round(sum(actual_store_contribution)::numeric, 2) AS total_contribution,
round(sum(theoretical_store_contribution)::numeric, 2) AS total_theoretical_contribution, round(sum(theoretical_store_contribution)::numeric, 2) AS total_theoretical_contribution,
round(sum(received) - sum(theoretical_cost) - sum(operating_expense), 2) AS theoretical_net_profit, round(sum(received) FILTER (WHERE operating_expense IS NOT NULL) - sum(theoretical_cost) - sum(operating_expense), 2) AS theoretical_net_profit,
round(sum(received) - sum(actual_food_cost) - sum(operating_expense), 2) AS actual_net_profit, round(sum(received) FILTER (WHERE operating_expense IS NOT NULL) - sum(actual_food_cost) - sum(operating_expense), 2) AS actual_net_profit,
round(sum(area_sqm)::numeric, 2) AS total_area, round(sum(area_sqm)::numeric, 2) AS total_area,
round((1 - sum(operating_expense) / nullif(sum(received), 0)) * 100, 2) AS overall_expense_rate_pct, round(sum(operating_expense) / nullif(sum(received) FILTER (WHERE operating_expense IS NOT NULL), 0) * 100, 2) AS overall_expense_rate_pct,
round((1 - (sum(actual_food_cost) + sum(operating_expense)) / nullif(sum(received), 0)) * 100, 2) AS overall_contribution_rate_pct, round((sum(received) FILTER (WHERE operating_expense IS NOT NULL) - sum(actual_food_cost) - sum(operating_expense)) / nullif(sum(received) FILTER (WHERE operating_expense IS NOT NULL), 0) * 100, 2) AS overall_contribution_rate_pct,
round((sum(received) - sum(theoretical_cost) - sum(operating_expense)) / nullif(sum(received), 0) * 100, 2) AS theoretical_net_margin_pct, round((sum(received) FILTER (WHERE operating_expense IS NOT NULL) - sum(theoretical_cost) - sum(operating_expense)) / nullif(sum(received) FILTER (WHERE operating_expense IS NOT NULL), 0) * 100, 2) AS theoretical_net_margin_pct,
round((sum(received) - sum(actual_food_cost) - sum(operating_expense)) / nullif(sum(received), 0) * 100, 2) AS actual_net_margin_pct, round((sum(received) FILTER (WHERE operating_expense IS NOT NULL) - sum(actual_food_cost) - sum(operating_expense)) / nullif(sum(received) FILTER (WHERE operating_expense IS NOT NULL), 0) * 100, 2) AS actual_net_margin_pct,
round(sum(theoretical_cost) / nullif(sum(received), 0) * 100, 2) AS theoretical_food_cost_rate_pct, round(sum(theoretical_cost) / nullif(sum(received) FILTER (WHERE operating_expense IS NOT NULL), 0) * 100, 2) AS theoretical_food_cost_rate_pct,
round(sum(actual_food_cost) / nullif(sum(received), 0) * 100, 2) AS actual_food_cost_rate_pct, round(sum(actual_food_cost) / nullif(sum(received) FILTER (WHERE operating_expense IS NOT NULL), 0) * 100, 2) AS actual_food_cost_rate_pct,
round(sum(wage_expense) / nullif(sum(received), 0) * 100, 2) AS overall_wage_rate_pct, round(sum(wage_expense) / nullif(sum(received) FILTER (WHERE operating_expense IS NOT NULL), 0) * 100, 2) AS overall_wage_rate_pct,
round(sum(rent_expense) / nullif(sum(received), 0) * 100, 2) AS overall_rent_rate_pct, round(sum(rent_expense) / nullif(sum(received) FILTER (WHERE operating_expense IS NOT NULL), 0) * 100, 2) AS overall_rent_rate_pct,
round(sum(utility_expense) / nullif(sum(received), 0) * 100, 2) AS overall_utility_rate_pct round(sum(utility_expense) / nullif(sum(received) FILTER (WHERE operating_expense IS NOT NULL), 0) * 100, 2) AS overall_utility_rate_pct
FROM full_scope FROM full_scope
`, [month]) `, [month])
sendSuccess(res, result.rows[0]) sendSuccess(res, result.rows[0])