diff --git a/server/src/routes/data.ts b/server/src/routes/data.ts index 1980e3a..7b01c2e 100644 --- a/server/src/routes/data.ts +++ b/server/src/routes/data.ts @@ -1068,9 +1068,9 @@ router.get('/overview/profit-waterfall', async (req: AuthRequest, res) => { WHERE r.month_start = $1 AND r.received IS NOT NULL ) SELECT - round(sum(consumption)::numeric, 2) AS consumption, - round(sum(discount)::numeric, 2) AS discount, - round(sum(received)::numeric, 2) AS received, + round(sum(consumption) FILTER (WHERE has_expense)::numeric, 2) AS consumption, + round(sum(discount) FILTER (WHERE has_expense)::numeric, 2) AS discount, + round(sum(received) FILTER (WHERE has_expense)::numeric, 2) AS received, round(sum(actual_food_cost)::numeric, 2) AS food_cost, round(sum(wage_expense)::numeric, 2) AS wage, round(sum(rent_expense)::numeric, 2) AS rent, @@ -1079,7 +1079,7 @@ router.get('/overview/profit-waterfall', async (req: AuthRequest, res) => { round(sum(delivery_commission_expense)::numeric, 2) AS commission, round(sum(card_fee_expense + repair_clean_expense)::numeric, 2) AS other_expense, round(sum(operating_expense)::numeric, 2) AS total_expense, - round(sum(received) - sum(actual_food_cost) - sum(operating_expense), 2) AS store_contribution, + round(sum(received) FILTER (WHERE has_expense) - sum(actual_food_cost) - sum(operating_expense), 2) AS store_contribution, count(*) AS covered_stores, count(*) FILTER (WHERE has_expense) AS stores_with_expense FROM full_scope diff --git a/数据溯源审计.md b/数据溯源审计.md new file mode 100644 index 0000000..d8fc523 --- /dev/null +++ b/数据溯源审计.md @@ -0,0 +1,308 @@ +# 全页面数据溯源审计报告 + +> 目标:逐页面、逐指标追溯显示名称 → API → SQL → 数据源表字段,标出跨页面重复指标口径是否一致。 + +## 一、页面与 API 总览 + +| 页面 | 文件 | 调用的 API | +|------|------|-----------| +| 总部驾驶舱 | DashboardPage.tsx | /overview, /overview/daily, /stores/risk, /stores/priority, /tasks/loop-health, /stores/quadrant, /platform/economics, /situational-awareness/alerts, /store-expense/overview, /overview/yoy, /overview/mom, /overview/trend, /overview/profit-trend | +| 老板驾驶舱 | BossPage.tsx | /overview, /overview/daily, /store-expense/overview, /overview/profit-waterfall, /stores/risk, /stores/priority, /cost-analysis/store-overview, /store-expense/expense-structure, /overview/store-profit-ranking, /overview/profit-opportunity | +| 门店工作台 | StorePage.tsx | /stores/risk, /stores/priority, /tasks/stores/:code/daily-card, /tasks, /stores/:code/daily, /tasks/followup, /sku/attach, /sku/abc, /stores/:code, /situational-awareness/health-score, /stores/:code/meal-period, /stores/:code/category-mix, /stores/:code/cost, /stores/:code/member, /stores/:code/anomalies, /stores/:code/staffing | +| 成本分析 | CostPage.tsx | /cost/comparison, /cost/inventory, /cost/category-benchmark | +| 成本分析(Tab) | cost-analysis/*.tsx | /cost-analysis/overview, /category-comparison, /margin-deviation, /variance-top, /menu-engineering, /profitability, /pricing, /store-overview, /store-ranking, /bom-*, /packaging-*, /material-*, /data-quality, /unmatched-* | +| 费用分析(Tab) | store-expense/*.tsx | /store-expense/overview, /expense-structure, /store-ranking, /store-contribution, /break-even, /loss-diagnosis, /delivery-commission, /rent-risk, /fixed-variable, /efficiency, /store-evaluation | +| 风险监控 | RiskPage.tsx | /risk/anomaly, /risk/zero-received, /risk/cashier | +| 会员复购 | MemberPage.tsx | /member/comparison, /member/repeat | +| SKU分析 | SKUPage.tsx | /sku/abc, /sku/category | +| 菜单工程 | MenuEngineeringPage.tsx | /analytics-enhanced/menu-engineering/actions | +| 产品生命周期 | ProductLifecyclePage.tsx | /product/products, /product/reviews/overview, /product/reviews | +| BOM穿透 | BomPenetrationPage.tsx | /central-kitchen/bom-penetration | +| 分销对账 | DistributionReconciliationPage.tsx | /distribution/reconciliation | +| 生产计划 | ProductionPlanPage.tsx | /sales-driven/production-plan | +| 任务闭环 | TasksPage.tsx | /tasks | +| 数据导入 | DataImportPage.tsx | /product/import-logs, /product/quality-rules | +| 本体浏览 | OntologyPage.tsx | /tasks/ontology/* | + +--- + +## 二、核心指标溯源与口径对比 + +### 2.1 营业收入(消费金额) + +| 页面 | 前端变量 | API | API字段 | SQL来源 | 数据源表.字段 | +|------|---------|-----|---------|---------|-------------| +| 总部驾驶舱 | `od?.consumption` | /overview | `consumption` | `sum(consumption)` from bill_fact | analytics.bill_fact.consumption | +| 老板驾驶舱 | `od?.consumption` | /overview | `consumption` | 同上 | 同上 | +| 费用总览Tab | `ov.total_consumption` | /store-expense/overview | `total_consumption` | `sum(consumption)` from bill_fact CTE | analytics.bill_fact.consumption | + +**口径:一致 ✅** — 三个页面均来自 `analytics.bill_fact.consumption` 的月度汇总。 + +### 2.2 优惠 + +| 页面 | 前端变量 | API | API字段 | SQL来源 | 数据源表.字段 | +|------|---------|-----|---------|---------|-------------| +| 总部驾驶舱 | `od?.discount` | /overview | `discount` | `sum(discount_total)` from bill_fact | analytics.bill_fact.discount_total | +| 老板驾驶舱 | `od?.discount` | /overview | `discount` | 同上 | 同上 | +| 费用总览Tab | `ov.total_discount` | /store-expense/overview | `total_discount` | `sum(discount_total)` from bill_fact CTE | analytics.bill_fact.discount_total | + +**口径:一致 ✅**(已修复,BossPage 之前用 `ex?.total_discount` 也一致) + +### 2.3 实收 + +| 页面 | 前端变量 | API | API字段 | SQL来源 | 数据源表.字段 | +|------|---------|-----|---------|---------|-------------| +| 总部驾驶舱 | `od?.received` | /overview | `received` | `sum(received_total)` from bill_fact | analytics.bill_fact.received_total | +| 老板驾驶舱 | `od?.received` | /overview | `received` | 同上 | 同上 | +| 费用总览Tab | `ov.total_received` | /store-expense/overview | `total_received` | `sum(r.received)` from mv_store_risk_rating_monthly | analytics.mv_store_risk_rating_monthly.received | +| 利润瀑布 | `wf.received` | /overview/profit-waterfall | `received` | `sum(r.received)` from mv_store_risk_rating_monthly | analytics.mv_store_risk_rating_monthly.received | + +**口径:一致 ✅**(已修复)— `bill_fact.received_total` 汇总和 `mv_store_risk_rating_monthly.received` 汇总结果相同(均为 64,409,339.09),因为后者物化视图就来自前者。 + +### 2.4 账单数 + +| 页面 | 前端变量 | API | API字段 | SQL来源 | 数据源表.字段 | +|------|---------|-----|---------|---------|-------------| +| 总部驾驶舱 | `od?.bill_count` | /overview | `bill_count` | `count(*)` from bill_fact | analytics.bill_fact | +| 老板驾驶舱 | `ex?.total_bills` | /store-expense/overview | `total_bills` | `sum(bill_count)` from mv_store_risk_rating_monthly | analytics.mv_store_risk_rating_monthly.bill_count | + +**口径:一致 ✅** — 值均为 1,682,360。 + +### 2.5 客单价 + +| 页面 | 前端变量 | API | API字段 | 计算方式 | +|------|---------|-----|---------|---------| +| 总部驾驶舱 | `od?.avg_bill_value` | /overview | `avg_bill_value` | `avg(received_total)` from bill_fact = 38.29 | +| 老板驾驶舱 | `ex?.avg_bill_value` | /store-expense/overview | `avg_bill_value` | `sum(received)/sum(bill_count)` = 38.29 | + +**口径:一致 ✅** — 数学结果相同。 + +### 2.6 优惠率 + +| 页面 | 前端变量 | API | API字段 | 计算方式 | +|------|---------|-----|---------|---------| +| 总部驾驶舱 | `od?.discount_rate_pct` | /overview | `discount_rate_pct` | `avg(discount_total/consumption*100)` from bill_fact = 19.52% | +| 老板驾驶舱 | 无(不显示此指标) | - | - | - | + +**口径:N/A** — 仅总部驾驶舱显示。 + +### 2.7 理论毛利率 + +| 页面 | 前端变量 | API | API字段 | 计算方式 | +|------|---------|-----|---------|---------| +| 总部驾驶舱 | `od?.theoretical_margin_pct` | /overview | `theoretical_margin_pct` | `sum(theoretical_profit)/sum(received)*100` from bill_fact = 61.84% | + +**口径:N/A** — 仅总部驾驶舱显示。(已修复,之前硬编码为0) + +### 2.8 会员占比 + +| 页面 | 前端变量 | API | API字段 | 计算方式 | +|------|---------|-----|---------|---------| +| 总部驾驶舱 | `od?.member_share_pct` | /overview | `member_share_pct` | `sum(received_total) FILTER(member_id非空)/sum(received_total)*100` = 14.25% | + +**口径:N/A** — 仅总部驾驶舱显示。(已修复,之前硬编码为0) + +### 2.9 门店贡献利润(实际) + +| 页面 | 前端变量 | API | API字段 | 计算方式 | 值 | +|------|---------|-----|---------|---------|-----| +| 总部驾驶舱 | `ex?.actual_net_profit` | /store-expense/overview | `actual_net_profit` | `sum(received) FILTER(费用匹配) - sum(food_cost) - sum(operating_expense)` | 6,982,110.42 | +| 老板驾驶舱(指标卡) | `ex?.actual_net_profit` | /store-expense/overview | `actual_net_profit` | 同上 | 6,982,110.42 | +| 老板驾驶舱(瀑布图) | `wf.store_contribution` | /overview/profit-waterfall | `store_contribution` | `sum(received) - sum(COALESCE(food_cost,0)) - sum(COALESCE(operating_expense,0))` | **7,703,472.16** | + +**口径:不一致 ❌** — 瀑布图用 COALESCE(...,0) 包含2家无费用门店,导致利润虚高72万。需修复 profit-waterfall API。 + +### 2.10 贡献率(实际) + +| 页面 | 前端变量 | API | API字段 | 计算方式 | 值 | +|------|---------|-----|---------|---------|-----| +| 总部驾驶舱 | `ex?.actual_net_margin_pct` | /store-expense/overview | `actual_net_margin_pct` | `(matched_received - food_cost - operating_expense)/matched_received*100` | 10.96% | +| 老板驾驶舱(指标卡) | `ex?.actual_net_margin_pct` | /store-expense/overview | `actual_net_margin_pct` | 同上 | 10.96% | +| 老板驾驶舱(瀑布图) | 前端计算 `wf.store_contribution/wf.received*100` | /overview/profit-waterfall | 前端除法 | `7703472/64409339*100` | **11.96%** | + +**口径:不一致 ❌** — 同 2.9,瀑布图口径不同。 + +### 2.11 食材成本 + +| 页面 | 前端变量 | API | API字段 | 计算方式 | +|------|---------|-----|---------|---------| +| 总部驾驶舱 | `ex?.total_food_cost` | /store-expense/overview | `total_food_cost` | `sum(actual_food_cost)` from mv_store_operating_expense_monthly | +| 老板驾驶舱(瀑布图) | `wf.food_cost` | /overview/profit-waterfall | `food_cost` | `sum(COALESCE(actual_food_cost,0))` — 含2家0值门店 | +| 费用总览Tab | `ov.total_food_cost` | /store-expense/overview | `total_food_cost` | 同总部 | + +**口径:一致 ✅** — 2家无费用门店的 food_cost 为 NULL,`sum(NULL)` 忽略,`sum(COALESCE(NULL,0))` 加0,结果相同。 + +### 2.12 食材成本率 + +| 页面 | 前端变量 | API | API字段 | 计算方式 | 值 | +|------|---------|-----|---------|---------|-----| +| 总部驾驶舱 | `ex?.actual_food_cost_rate_pct` | /store-expense/overview | `actual_food_cost_rate_pct` | `sum(food_cost)/matched_received*100` | 35.26% | +| 老板驾驶舱(瀑布图) | 前端计算 `wf.food_cost/wf.received*100` | /overview/profit-waterfall | 前端除法 | `22459074/64409339*100` | **34.87%** | + +**口径:不一致 ❌** — 分母不同(matched_received 6369万 vs all_received 6441万)。 + +### 2.13 经营费用 + +| 页面 | 前端变量 | API | API字段 | 计算方式 | +|------|---------|-----|---------|---------| +| 总部驾驶舱 | `ex?.total_expense` | /store-expense/overview | `total_expense` | `sum(operating_expense)` — NULL被sum忽略 | +| 老板驾驶舱(瀑布图) | `wf.total_expense` | /overview/profit-waterfall | `total_expense` | `sum(COALESCE(operating_expense,0))` — 2家为0 | +| 费用总览Tab | `ov.total_expense` | /store-expense/overview | `total_expense` | 同总部 | + +**口径:一致 ✅** — sum忽略NULL vs COALESCE加0,结果相同。 + +### 2.14 费用率 + +| 页面 | 前端变量 | API | API字段 | 计算方式 | 值 | +|------|---------|-----|---------|---------|-----| +| 总部驾驶舱 | `ex?.overall_expense_rate_pct` | /store-expense/overview | `overall_expense_rate_pct` | `sum(operating_expense)/matched_received*100` | 53.77% | +| 老板驾驶舱(瀑布图) | 前端计算 `wf.total_expense/wf.received*100` | /overview/profit-waterfall | 前端除法 | `34246793/64409339*100` | **53.17%** | +| 费用总览Tab | `ov.overall_expense_rate_pct` | /store-expense/overview | `overall_expense_rate_pct` | 同总部 | 53.77% | + +**口径:不一致 ❌** — 瀑布图分母含全部94家。 + +### 2.15 人工费用 / 人工费率 + +| 页面 | 前端变量 | API | API字段 | 计算方式 | +|------|---------|-----|---------|---------| +| 总部驾驶舱 | `ex?.total_wage` / `ex?.overall_wage_rate_pct` | /store-expense/overview | `total_wage` / `overall_wage_rate_pct` | `sum(wage_expense)` / `sum(wage)/matched_received*100` | +| 老板驾驶舱(瀑布图) | `wf.wage` | /overview/profit-waterfall | `wage` | `sum(COALESCE(wage_expense,0))` | +| 费用总览Tab | `ov.total_wage` / `ov.overall_wage_rate_pct` | /store-expense/overview | 同总部 | 同总部 | + +**金额口径:一致 ✅** — sum忽略NULL vs COALESCE加0结果相同。 +**费率口径:瀑布图不一致 ❌** — 前端用 `wf.wage/wf.received*100`(分母含94家)vs store-expense用 `matched_received`(92家)。 + +### 2.16 房租费用 / 房租费率 + +同 2.15,金额一致,费率瀑布图分母不一致。 + +### 2.17 水电费用 / 水电费率 + +同 2.15,金额一致,费率瀑布图分母不一致。 + +### 2.18 外卖佣金 + +| 页面 | 前端变量 | API | API字段 | 计算方式 | +|------|---------|-----|---------|---------| +| 总部驾驶舱 | `ex?.total_commission` | /store-expense/overview | `total_commission` | `sum(delivery_commission_expense)` | +| 老板驾驶舱(瀑布图) | `wf.commission` | /overview/profit-waterfall | `commission` | `sum(COALESCE(delivery_commission_expense,0))` | + +**口径:一致 ✅** — 金额相同。 + +### 2.19 门店数 + +| 页面 | 前端变量 | API | API字段 | 计算方式 | +|------|---------|-----|---------|---------| +| 总部驾驶舱 | `riskRows.length` | /stores/risk | count of rows | `count(*)` from mv_store_risk_rating_monthly | +| 老板驾驶舱 | `ex?.total_stores` | /store-expense/overview | `total_stores` | `count(*)` from full_scope (mv_store_risk_rating_monthly) | + +**口径:一致 ✅** — 均为94家。 + +### 2.20 盈利/亏损门店数 + +| 页面 | 前端变量 | API | API字段 | 计算方式 | +|------|---------|-----|---------|---------| +| 总部驾驶舱 | `ex?.profitable_stores` / `ex?.loss_stores` | /store-expense/overview | `profitable_stores` / `loss_stores` | `count(*) FILTER(actual_store_contribution>0)` / `count(*) FILTER(<=0 AND received>0)` | +| 老板驾驶舱 | 同上 | 同上 | 同上 | 同上 | + +**口径:一致 ✅** — 盈利78家/亏损13家。 + +### 2.21 门店风险分布 + +| 页面 | 前端变量 | API | 数据源 | +|------|---------|-----|--------| +| 总部驾驶舱 | `riskSummary` | /stores/risk | analytics.mv_store_risk_rating_monthly.risk_level | +| 老板驾驶舱 | `riskSummary` | /stores/risk | 同上 | + +**口径:一致 ✅** + +### 2.22 P0/P1 门店 + +| 页面 | 前端变量 | API | 数据源 | +|------|---------|-----|--------| +| 总部驾驶舱 | `priorityRows` | /stores/priority | analytics.mv_store_action_priority_deep_monthly | +| 老板驾驶舱 | `priorityRows` | /stores/priority | 同上 | + +**口径:一致 ✅** + +### 2.23 成本异常分布 + +| 页面 | 前端变量 | API | 数据源 | +|------|---------|-----|--------| +| 老板驾驶舱 | `costOv.red_count` 等 | /cost-analysis/store-overview | analytics.mv_store_theoretical_actual_cost_monthly | +| 成本分析Tab | 同上 | 同上 | 同上 | + +**口径:一致 ✅** + +### 2.24 利润机会池 + +| 页面 | 前端变量 | API | 数据源 | +|------|---------|-----|--------| +| 老板驾驶舱 | `profitOpp` | /overview/profit-opportunity | analytics.mv_store_risk_rating_monthly + mv_store_operating_expense_monthly | + +**口径:N/A** — 仅老板驾驶舱显示。 + +### 2.25 日度趋势 + +| 页面 | 前端变量 | API | 数据源 | +|------|---------|-----|--------| +| 总部驾驶舱 | `dailyRows` | /overview/daily | analytics.mv_overview_daily 或 bill_fact | +| 老板驾驶舱 | `dailyRows` | /overview/daily | 同上 | + +**口径:一致 ✅** + +--- + +## 三、发现的问题汇总 + +### 问题1:利润瀑布 store_contribution 口径不一致 ❌ + +**位置**:`/overview/profit-waterfall` API (`data.ts:1082`) + +**现状**: +```sql +round(sum(received) - sum(actual_food_cost) - sum(operating_expense), 2) AS store_contribution +``` +- `sum(received)` = 64,409,339(94家) +- `sum(actual_food_cost)` = 22,459,074(92家,NULL被忽略) +- `sum(operating_expense)` = 34,246,793(92家,NULL被忽略) +- 结果 = 7,703,472(含2家无费用门店的实收429万作为"利润") + +**正确值**(与 store-expense/overview 一致):6,982,110(仅92家匹配门店) + +**影响**:BossPage 利润瀑布图和占比条显示的"门店贡献利润"比指标卡多72万,贡献率高1个百分点。 + +### 问题2:利润瀑布各项费率分母不一致 ❌ + +**位置**:BossPage.tsx 前端计算 (line 296-318) + +**现状**:前端用 `wf.received`(64,409,339,94家)做分母计算食材成本率、费用率、贡献率,但 store-expense/overview 用 `matched_received`(63,687,977,92家)做分母。 + +| 指标 | 瀑布图口径 | store-expense口径 | 差异 | +|------|-----------|------------------|------| +| 食材成本率 | 34.87% | 35.26% | 0.39pp | +| 费用率 | 53.17% | 53.77% | 0.60pp | +| 贡献率 | 11.96% | 10.96% | 1.00pp | + +### 问题3:mv_overview_monthly 刷新机制不完整 ⚠️ + +**现状**:`mv_overview_monthly` 是普通表(非物化视图),之前刷新脚本未包含它的更新步骤,导致数据导入后该表不更新。 + +**已修复**:刷新脚本已加入 DELETE+INSERT 步骤。 + +--- + +## 四、修复方案 + +### 修复问题1+2:统一利润瀑布口径 + +将 `/overview/profit-waterfall` 的 `store_contribution` 和 `received` 改为仅计算有费用门店,与 `/store-expense/overview` 保持一致。 + +**修改文件**:`server/src/routes/data.ts` profit-waterfall API + +**修改内容**: +1. `received` → `sum(received) FILTER (WHERE has_expense)` +2. `store_contribution` → `sum(received) FILTER (WHERE has_expense) - sum(actual_food_cost) - sum(operating_expense)` +3. `consumption` 和 `discount` 也改为 `FILTER (WHERE has_expense)` 保持同口径 + +这样瀑布图的 received、food_cost、expense、contribution 全部基于92家匹配门店,与 store-expense/overview 完全一致。