fix: 修复API数据正确性问题

- 移除3个API的operating_expense IS NOT NULL过滤,改用COALESCE
  - /overview/store-profit-ranking: 93家门店全部纳入排名
  - /overview/profit-opportunity: 所有有营收门店纳入机会计算
  - /overview/profit-trend: LEFT JOIN替代INNER JOIN
- 重建v_store_operating_expense_monthly视图,去除硬编码_april视图
  - sales CTE改用mv_store_site_profile_monthly
  - food CTE改用mv_store_theoretical_actual_cost_monthly
  - 支持任意月份查询
- 修复人工优化和能源优化opportunity为负数问题
- 新增scripts/refresh_materialized_views.sh自动化刷新脚本
- 新增本体.md/API.md/原始数据级导入处理.md/物化视图更新.md/API数据检查报告.md
This commit is contained in:
freedakgmail
2026-08-11 19:25:49 +08:00
parent 3f1c291486
commit a090fc7c19
7 changed files with 1677 additions and 19 deletions
+21 -19
View File
@@ -1093,15 +1093,15 @@ router.get('/overview/store-profit-ranking', async (req: AuthRequest, res) => {
r.store_code,
r.store_name,
round(r.received::numeric, 2) AS received,
round(e.actual_food_cost::numeric, 2) AS food_cost,
round(e.operating_expense::numeric, 2) AS expense,
round((r.received - e.actual_food_cost - e.operating_expense)::numeric, 2) AS store_contribution,
round((r.received - e.actual_food_cost - e.operating_expense) / nullif(r.received, 0) * 100, 2) AS contribution_margin_pct,
round(COALESCE(e.actual_food_cost, 0)::numeric, 2) AS food_cost,
round(COALESCE(e.operating_expense, 0)::numeric, 2) AS expense,
round((r.received - COALESCE(e.actual_food_cost, 0) - COALESCE(e.operating_expense, 0))::numeric, 2) AS store_contribution,
round((r.received - COALESCE(e.actual_food_cost, 0) - COALESCE(e.operating_expense, 0)) / nullif(r.received, 0) * 100, 2) AS contribution_margin_pct,
r.risk_level
FROM analytics.mv_store_risk_rating_monthly 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.month_start = $1 AND r.received IS NOT NULL AND r.received > 0 AND e.operating_expense IS NOT NULL
WHERE r.month_start = $1 AND r.received IS NOT NULL AND r.received > 0
ORDER BY store_contribution DESC
`, [month])
sendSuccess(res, result.rows)
@@ -1123,13 +1123,15 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
),
standard_stores AS (
SELECT r.store_code, r.store_name, r.received,
e.actual_food_cost, e.operating_expense,
e.wage_expense, e.utility_expense,
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 AND e.operating_expense IS NOT NULL
WHERE r.received IS NOT NULL AND r.received > 0
),
cost_diff AS (
SELECT
@@ -1246,7 +1248,7 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
'category', '标准店人工优化',
'baseline', (SELECT round(total_wage / total_received * 100, 2) FROM labor),
'target_pct', 100,
'opportunity', round((SELECT total_wage * (1 - 24.0 / nullif(total_wage / total_received * 100, 0)) FROM labor), 2),
'opportunity', GREATEST(round((SELECT total_wage * (1 - 24.0 / nullif(total_wage / total_received * 100, 0)) FROM labor), 2), 0),
'confidence', '中',
'owner', '运营/人力',
'evidence', '工时、工资、餐段销售与服务质量',
@@ -1261,7 +1263,7 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
'category', '标准店能源优化',
'baseline', (SELECT round(total_utility / total_received * 100, 2) FROM energy),
'target_pct', 100,
'opportunity', round((SELECT total_utility * (1 - 5.0 / nullif(total_utility / total_received * 100, 0)) FROM energy), 2),
'opportunity', GREATEST(round((SELECT total_utility * (1 - 5.0 / nullif(total_utility / total_received * 100, 0)) FROM energy), 2), 0),
'confidence', '中',
'owner', '工程/门店',
'evidence', '账单/抄表、面积、营业时长',
@@ -1423,21 +1425,21 @@ router.get('/overview/profit-trend', async (req: AuthRequest, res) => {
),
monthly AS (
SELECT e.report_month,
round(sum(e.consumption)::numeric, 2) AS consumption,
round(sum(COALESCE(e.consumption, 0))::numeric, 2) AS consumption,
round(max(bm.discount)::numeric, 2) AS discount,
round(sum(r.received)::numeric, 2) AS received,
round(sum(e.actual_food_cost)::numeric, 2) AS food_cost,
round(sum(e.wage_expense)::numeric, 2) AS wage,
round(sum(e.rent_expense)::numeric, 2) AS rent,
round(sum(e.utility_expense)::numeric, 2) AS utility,
round(sum(e.operating_expense)::numeric, 2) AS total_expense,
round(sum(r.received) - sum(e.actual_food_cost) - sum(e.operating_expense), 2) AS store_contribution
round(sum(COALESCE(e.actual_food_cost, 0))::numeric, 2) AS food_cost,
round(sum(COALESCE(e.wage_expense, 0))::numeric, 2) AS wage,
round(sum(COALESCE(e.rent_expense, 0))::numeric, 2) AS rent,
round(sum(COALESCE(e.utility_expense, 0))::numeric, 2) AS utility,
round(sum(COALESCE(e.operating_expense, 0))::numeric, 2) AS total_expense,
round(sum(r.received) - sum(COALESCE(e.actual_food_cost, 0)) - sum(COALESCE(e.operating_expense, 0)), 2) AS store_contribution
FROM analytics.v_store_scorecard r
JOIN analytics.mv_store_operating_expense_monthly e
LEFT JOIN analytics.mv_store_operating_expense_monthly e
ON r.store_code = e.sales_store_code
LEFT JOIN bill_monthly bm ON bm.report_month = e.report_month
WHERE e.report_month >= $1::date AND e.report_month <= $2::date
AND e.operating_expense IS NOT NULL AND r.received > 0
AND r.received > 0
GROUP BY e.report_month
ORDER BY e.report_month
)