系统升级方案:从"4月单月分析"到"逐月+时间段汇总+同比环比"
文档日期:2026-08-01
状态:方案设计
关联文档:docs/2026年4月运营分析指标与展现审查及利润提升整改方案.md
一、升级目标
| 能力 |
当前状态 |
目标状态 |
| 月份选择 |
固定2026年4月 |
任意月份可选,默认最新有数据月份 |
| 时间段汇总 |
不支持 |
季度/半年/全年/自定义区间汇总 |
| 同比(YoY) |
不支持 |
任意月份 vs 去年同月,展示变化率 |
| 环比(MoM) |
不支持 |
任意月份 vs 上月,展示变化率 |
| 趋势分析 |
不支持 |
月度趋势线、移动平均、同比叠加 |
| 前端选择器 |
无 |
全局月份选择器 + 时间范围切换 |
| 数据管道 |
硬编码4月 |
参数化导入,支持多月数据 |
二、现状诊断
1. 硬编码 _april 的数据库对象(32个)
这些视图/表的 SQL 内部硬编码了 WHERE report_month = DATE '2026-04-01' 或直接从4月数据源取数,无法切换月份。
核实方法: pg_matviews 查物化视图9个 + information_schema.tables 查基表6个和普通视图17个 = 合计32个。
1.1 物化视图(9个,pg_matviews)
| 对象名 |
用途 |
依赖月份字段 |
mv_store_theoretical_actual_cost_april |
门店理论/实际成本对比 |
report_month |
mv_store_action_priority_deep_april |
门店行动优先级深度分析 |
report_month |
dish_store_summary_april |
菜品门店汇总 |
report_month |
dish_store_sku_april |
菜品门店SKU汇总 |
report_month |
dish_basket_april |
菜品购物篮分析 |
report_month |
dish_category_summary_april |
菜品品类汇总 |
report_month |
dish_member_sku_april |
菜品会员SKU分析 |
report_month |
dish_sales_april |
菜品销售汇总 |
report_month |
dish_sku_summary_april |
菜品SKU汇总 |
report_month |
1.2 普通视图(17个,information_schema.tables WHERE table_type='VIEW')
| 对象名 |
用途 |
依赖月份字段 |
v_dish_sku_abc_april |
SKU ABC分类分析 |
report_month |
v_inventory_cost_classified_april |
库存成本分类 |
report_month |
v_inventory_abnormal_items_april |
库存异常货品 |
report_month |
v_inventory_finance_category_april |
库存财务分类 |
report_month |
v_c_sku_governance_april |
SKU治理分析 |
report_month |
v_dish_member_repeat_april |
菜品会员复购 |
report_month |
v_store_action_priority_deep_april |
门店行动优先级 |
report_month |
v_store_area_efficiency_april |
门店面积效率 |
report_month |
v_store_deep_diagnosis_april |
门店深度诊断 |
report_month |
v_store_site_profile_april |
门店选址画像 |
report_month |
v_store_site_replication_score_april |
选址复制评分 |
report_month |
v_store_location_overlap_risk_april |
选址重叠风险 |
report_month |
v_store_nearest_neighbor_april |
最近邻分析 |
report_month |
v_store_spatial_pairs_april |
门店空间对分析 |
report_month |
v_store_theoretical_actual_cost_april |
门店理论/实际成本(视图版) |
report_month |
v_district_site_benchmark_april |
区域选址基准(视图版) |
report_month |
v_site_segment_benchmark_april |
分段基准(视图版) |
report_month |
1.3 基表(6个,information_schema.tables WHERE table_type='BASE TABLE',不含物化视图)
| 对象名 |
用途 |
dish_pair_summary_april |
菜品搭售分析 |
mv_district_site_benchmark_april |
区域选址基准(表) |
mv_site_segment_benchmark_april |
分段基准(表) |
mv_store_location_overlap_risk_april |
选址重叠风险(表) |
mv_store_site_profile_april |
门店选址画像(表) |
mv_store_site_replication_score_april |
选址复制评分(表) |
注意: v_store_category_cost_benchmark_april 和 v_store_inventory_efficiency_april 在 data.ts 中被引用,但数据库中不存在这两个对象(可能已删除或从未创建),需在改造时排查。
注意: 部分对象有同名 v_(视图)和 mv_(基表)两个版本,如 v_store_site_profile_april 和 mv_store_site_profile_april,改造时需统一处理。
2. 硬编码 DATE '2026-04-01' 或 _april 表名的 API 接口(45处)
核实方法: grep -rn "DATE '2026-04-01'\|_april" server/src/routes/*.ts 逐行统计。
2.1 server/src/routes/store-expense.ts(11处 DATE '2026-04-01')
| 行号 |
接口 |
硬编码内容 |
| 36 |
/store-expense/overview |
e.report_month = DATE '2026-04-01' |
| 88 |
/store-expense/expense-structure |
report_month = DATE '2026-04-01' |
| 107 |
/store-expense/store-ranking |
WHERE report_month = DATE '2026-04-01' |
| 161 |
/store-expense/store-contribution |
WHERE report_month = DATE '2026-04-01' |
| 211 |
/store-expense/rent-risk |
WHERE report_month = DATE '2026-04-01' AND rent_expense IS NOT NULL |
| 276 |
/store-expense/delivery-commission |
WHERE report_month = DATE '2026-04-01' AND delivery_received > 0 |
| 330 |
/store-expense/efficiency |
WHERE report_month = DATE '2026-04-01' AND received > 0 AND area_sqm IS NOT NULL |
| 386 |
/store-expense/fixed-variable |
WHERE report_month = DATE '2026-04-01' AND received > 0 |
| 436 |
/store-expense/break-even |
WHERE report_month = DATE '2026-04-01' AND received > 0 |
| 499 |
/store-expense/loss-diagnosis |
WHERE report_month = DATE '2026-04-01' AND actual_store_contribution <= 0 AND received > 0 |
| 725 |
/store-expense/store-evaluation |
WHERE report_month = DATE '2026-04-01' AND received > 0 |
2.2 server/src/routes/data.ts(26处:9处 DATE '2026-04-01' + 17处 _april 表名)
| 行号 |
接口 |
硬编码内容 |
| 530 |
/overview/profit-waterfall |
e.report_month = DATE '2026-04-01' |
| 568 |
/overview/store-profit-ranking |
e.report_month = DATE '2026-04-01' |
| 589 |
/overview/profit-opportunity |
e.report_month = DATE '2026-04-01' |
| 661 |
/overview/profit-opportunity |
report_month = DATE '2026-04-01'(platform CTE) |
| 668 |
/overview/profit-opportunity |
e.report_month = DATE '2026-04-01'(platform_stores CTE) |
| 890 |
/bank/report |
month = DATE '2026-04-01'(overview) |
| 891 |
/bank/report |
month = DATE '2026-04-01'(daily) |
| 898 |
/bank/report |
e.report_month = DATE '2026-04-01'(waterfall) |
| 909 |
/bank/report |
business_date >= DATE '2026-04-01' AND < DATE '2026-05-01'(channel) |
| 117 |
/stores/:code |
mv_store_action_priority_deep_april(表名硬编码) |
| 176 |
/cost/comparison |
mv_store_theoretical_actual_cost_april(表名硬编码) |
| 185 |
/cost/category-benchmark |
v_store_category_cost_benchmark_april(表名硬编码) |
| 194 |
/cost/inventory |
v_store_inventory_efficiency_april(表名硬编码) |
| 230 |
/sku/abc |
v_dish_sku_abc_april(表名硬编码) |
| 248 |
/sku/attach |
dish_pair_summary_april(表名硬编码) |
| 364 |
/data-quality |
v_inventory_cost_classified_april(表名硬编码) |
| 406 |
/stores/:code/cost |
mv_store_theoretical_actual_cost_april(表名硬编码) |
| 412 |
/stores/:code/cost |
v_inventory_cost_classified_april(表名硬编码) |
| 420 |
/stores/:code/cost |
v_inventory_cost_classified_april(表名硬编码) |
| 474 |
/site-selection/profile |
mv_store_site_profile_april(表名硬编码) |
| 482 |
/site-selection/segment-benchmark |
mv_site_segment_benchmark_april(表名硬编码) |
| 490 |
/site-selection/replication |
mv_store_site_replication_score_april(表名硬编码) |
| 498 |
/site-selection/overlap-risk |
mv_store_location_overlap_risk_april(表名硬编码) |
| 506 |
/site-selection/district-benchmark |
mv_district_site_benchmark_april(表名硬编码) |
| 676 |
/overview/profit-opportunity |
v_dish_sku_abc_april(表名硬编码,sku CTE) |
| 680 |
/overview/profit-opportunity |
v_dish_sku_abc_april(表名硬编码,sku_samples CTE) |
2.3 server/src/routes/cost-analysis.ts(4处)
| 行号 |
接口 |
硬编码内容 |
| 736 |
/cost-analysis/store-overview |
mv_store_theoretical_actual_cost_april(表名硬编码) |
| 753 |
/cost-analysis/store-map |
mv_store_theoretical_actual_cost_april(表名硬编码) |
| 780 |
/cost-analysis/store-ranking |
mv_store_theoretical_actual_cost_april(表名硬编码) |
| 791 |
/cost-analysis/store-ranking |
mv_store_theoretical_actual_cost_april(表名硬编码) |
2.4 server/src/routes/situational-awareness.ts(2处)
| 行号 |
接口 |
硬编码内容 |
| 23 |
/situational-awareness/health-score |
mv_store_theoretical_actual_cost_april(表名硬编码) |
| 296 |
/situational-awareness/correlation |
mv_store_theoretical_actual_cost_april(表名硬编码) |
2.5 server/src/routes/tasks.ts(2处)
| 行号 |
接口 |
硬编码内容 |
| 622 |
/tasks/monthly-review/sku-governance |
v_dish_sku_abc_april(表名硬编码) |
| 629 |
/tasks/monthly-review/sku-governance |
v_dish_sku_abc_april(表名硬编码) |
3. 已支持月度参数的接口(少数,可作参考模式)
| 接口 |
文件 |
参数化方式 |
/overview |
data.ts:9-17 |
parseMonth(req) → mv_overview_monthly WHERE month = $1 |
/overview/daily |
data.ts:19-27 |
parseMonth(req) → mv_overview_daily WHERE month = $1 |
/revenue/channel |
data.ts:789-813 |
parseMonth(req) → business_date >= $1 AND < $1 + INTERVAL '1 month' |
/revenue/meal-period |
data.ts:816-834 |
parseMonth(req) → 同上 |
/revenue/store-ranking |
data.ts:837-860 |
parseMonth(req) → 同上 |
/revenue/daily-summary |
data.ts:863-884 |
parseMonth(req) → 同上 |
4. 底层数据结构
| 数据对象 |
类型 |
日期字段 |
支持多月 |
说明 |
bill_fact |
物化视图 |
closed_at(timestamptz) |
✅ |
账单事实表,通过 closed_at::date 派生 business_date |
v_store_daily |
视图 |
business_date(派生) |
✅ |
从 bill_fact.closed_at::date 派生 |
v_channel_daily |
视图 |
business_date |
✅ |
渠道日度数据 |
mv_overview_monthly |
物化视图 |
month |
✅ |
月度总览,已有 month 字段 |
mv_overview_daily |
物化视图 |
month + business_date |
✅ |
日度总览 |
mv_store_operating_expense_monthly |
物化视图 |
report_month |
✅ |
门店费用月度,已有 report_month 字段 |
mv_store_risk_rating |
物化视图 |
无显式月份 |
❌ |
门店风险评级,当前只反映4月 |
v_operating_expense_account_monthly |
视图 |
report_month |
✅ |
费用科目月度 |
32个 _april 对象 |
混合 |
硬编码4月 |
❌ |
需改造(9物化视图 + 17普通视图 + 6基表) |
5. 数据导入脚本硬编码
| 文件 |
硬编码内容 |
db/import_salary_attendance.py:57 |
report_month = '2026-04-01' |
db/import_salary_attendance.py:159 |
report_month = '2026-04-01' |
sql/etl_fact_inventory.sql:32 |
WHERE i.report_month = DATE '2026-04-01' |
db/ontology_standard.sql:758-772 |
15处 '2026-04-01'::date 作为指标 effective_date(元数据,非查询过滤器) |
6. 前端硬编码月份参数(10处)
核实方法: grep -rn "2026-0[3-5]" client/src/pages/*.tsx 逐行统计。
| 文件 |
行号 |
硬编码内容 |
BomPenetrationPage.tsx |
23 |
useState('2026-04') |
CentralKitchenPage.tsx |
25 |
useState('2026-04') |
DistributionReconciliationPage.tsx |
28 |
useState('2026-04') |
MonthlyReviewPage.tsx |
27 |
useState('2026-05') |
ProductionPlanPage.tsx |
26 |
useState('2026-04') |
RegionalPage.tsx |
32 |
month: '2026-05'(API调用参数) |
StoreDetailPage.tsx |
45 |
month: '2026-05'(API调用参数) |
StorePage.tsx |
33 |
month: '2026-05'(API调用参数) |
StorePage.tsx |
38 |
month: '2026-04'(API调用参数) |
TasksPage.tsx |
14 |
useState('2026-05') |
注意: 这些前端硬编码月份需要替换为全局 MonthPicker 组件的选择值,通过 React Query 的 queryKey 联动。
7. parseMonth 函数现状
已有参数解析基础设施,但默认值硬编码为4月,且大部分接口未使用。
三、改造方案
第1层:数据库 — 去硬编码,参数化视图
方案选择:参数化函数 + 通用视图
将32个 _april 视图/表改为接受 p_month DATE 参数的函数,同时创建同名通用视图供过渡期使用。
1.1 核心利润链改造(第1批,9个对象)
示例:mv_store_theoretical_actual_cost_april → 函数
需改造的第1批对象:
| 原始对象名 |
新函数名 |
新通用视图名 |
依赖链 |
mv_store_theoretical_actual_cost_april |
fn_store_cost_comparison(p_month) |
v_store_cost_comparison |
依赖 v_inventory_cost_classified |
v_inventory_cost_classified_april |
fn_inventory_cost_classified(p_month) |
v_inventory_cost_classified |
依赖 v_inventory_finance_category |
v_inventory_finance_category_april |
fn_inventory_finance_category(p_month) |
v_inventory_finance_category |
依赖 inventory_cost_records |
v_inventory_abnormal_items_april |
fn_inventory_abnormal_items(p_month) |
v_inventory_abnormal_items |
依赖 inventory_cost_records |
v_store_action_priority_deep_april |
fn_store_action_priority(p_month) |
v_store_action_priority |
依赖 mv_store_risk_rating |
mv_store_action_priority_deep_april |
与 v_ 版本统一为 fn_store_action_priority |
同上 |
物化视图,与v_版本共享函数 |
v_store_area_efficiency_april |
fn_store_area_efficiency(p_month) |
v_store_area_efficiency |
依赖 mv_store_operating_expense_monthly |
v_store_deep_diagnosis_april |
fn_store_deep_diagnosis(p_month) |
v_store_deep_diagnosis |
依赖多个视图 |
v_c_sku_governance_april |
fn_sku_governance(p_month) |
v_sku_governance |
依赖 v_dish_sku_abc |
1.2 SKU/菜品分析改造(第2批,9个对象)
| 原始对象名 |
新函数名 |
新通用视图名 |
v_dish_sku_abc_april |
fn_dish_sku_abc(p_month) |
v_dish_sku_abc |
dish_store_summary_april |
fn_dish_store_summary(p_month) |
v_dish_store_summary |
dish_basket_april |
fn_dish_basket(p_month) |
v_dish_basket |
dish_category_summary_april |
fn_dish_category_summary(p_month) |
v_dish_category_summary |
dish_member_sku_april |
fn_dish_member_sku(p_month) |
v_dish_member_sku |
dish_pair_summary_april |
fn_dish_pair_summary(p_month) |
v_dish_pair_summary |
dish_store_sku_april |
fn_dish_store_sku(p_month) |
v_dish_store_sku |
dish_sales_april |
fn_dish_sales(p_month) |
v_dish_sales |
dish_sku_summary_april |
fn_dish_sku_summary(p_month) |
v_dish_sku_summary |
1.3 选址/空间分析改造(第3批,14个对象,含v_/mv_双版本)
| 原始对象名 |
新函数名 |
新通用视图名 |
v_store_site_profile_april |
fn_store_site_profile(p_month) |
v_store_site_profile |
mv_store_site_profile_april |
物化视图按月刷新 |
v_store_site_profile |
v_store_site_replication_score_april |
fn_store_site_replication(p_month) |
v_store_site_replication |
mv_store_site_replication_score_april |
物化视图按月刷新 |
同上 |
v_store_location_overlap_risk_april |
fn_store_overlap_risk(p_month) |
v_store_overlap_risk |
mv_store_location_overlap_risk_april |
物化视图按月刷新 |
同上 |
v_store_nearest_neighbor_april |
fn_store_nearest_neighbor(p_month) |
v_store_nearest_neighbor |
v_store_spatial_pairs_april |
fn_store_spatial_pairs(p_month) |
v_store_spatial_pairs |
v_dish_member_repeat_april |
fn_dish_member_repeat(p_month) |
v_dish_member_repeat |
v_store_theoretical_actual_cost_april |
与 mv_ 版本统一为 fn_store_cost_comparison |
同上 |
v_district_site_benchmark_april |
fn_district_site_benchmark(p_month) |
v_district_site_benchmark |
v_site_segment_benchmark_april |
fn_site_segment_benchmark(p_month) |
v_site_segment_benchmark |
mv_site_segment_benchmark_april |
与 v_ 版本统一 |
同上 |
mv_district_site_benchmark_april |
与 v_ 版本统一为 fn_district_site_benchmark |
同上 |
1.4 mv_store_risk_rating 改造
当前 mv_store_risk_rating 无月份字段,只反映4月数据。需改为按月生成:
第2层:后端 API — 统一月度参数
2.1 改造 parseMonth 函数
2.2 改造现有接口(去硬编码 DATE '2026-04-01')
模式:将所有 DATE '2026-04-01' 替换为 $N::date 参数
改造清单(45处):
| 文件 |
接口 |
改造方式 |
store-expense.ts:36 |
/store-expense/overview |
e.report_month = $1::date |
store-expense.ts:88 |
/store-expense/expense-structure |
report_month = $1::date |
store-expense.ts:107 |
/store-expense/store-ranking |
WHERE report_month = $1::date |
store-expense.ts:161 |
/store-expense/store-contribution |
WHERE report_month = $1::date |
store-expense.ts:211 |
/store-expense/rent-risk |
WHERE report_month = $1::date |
store-expense.ts:276 |
/store-expense/delivery-commission |
WHERE report_month = $1::date |
store-expense.ts:330 |
/store-expense/efficiency |
WHERE report_month = $1::date |
store-expense.ts:386 |
/store-expense/fixed-variable |
WHERE report_month = $1::date |
store-expense.ts:436 |
/store-expense/break-even |
WHERE report_month = $1::date |
store-expense.ts:499 |
/store-expense/loss-diagnosis |
WHERE report_month = $1::date |
store-expense.ts:725 |
/store-expense/store-evaluation |
WHERE report_month = $1::date |
data.ts:530 |
/overview/profit-waterfall |
e.report_month = $1::date |
data.ts:568 |
/overview/store-profit-ranking |
e.report_month = $1::date |
data.ts:589 |
/overview/profit-opportunity |
e.report_month = $1::date(3处 DATE) + 2处 v_dish_sku_abc_april 表名 |
data.ts:890-909 |
/bank/report |
4处 DATE '2026-04-01' + 1处 DATE '2026-05-01' → $N::date |
data.ts:117 |
/stores/:code |
表名 → fn_store_action_priority($1) |
data.ts:176 |
/cost/comparison |
表名 → fn_store_cost_comparison($1) |
data.ts:185 |
/cost/category-benchmark |
表名 → fn_store_category_cost_benchmark($1) |
data.ts:194 |
/cost/inventory |
表名 → fn_store_inventory_efficiency($1) |
data.ts:230 |
/sku/abc |
表名 → fn_dish_sku_abc($1) |
data.ts:248 |
/sku/attach |
表名 → fn_dish_pair_summary($1) |
data.ts:364 |
/data-quality |
表名 → fn_inventory_cost_classified($1) |
data.ts:406-420 |
/stores/:code/cost |
表名 → fn_store_cost_comparison($1) + fn_inventory_cost_classified($1)(3处) |
data.ts:474-506 |
/site-selection/* |
5个表名 → 对应函数(含 district-benchmark) |
cost-analysis.ts:736-791 |
/cost-analysis/store-* |
4处表名 → fn_store_cost_comparison($1) |
situational-awareness.ts:23,296 |
/situational-awareness/health-score, /correlation |
2处表名 → fn_store_cost_comparison($1) |
tasks.ts:622,629 |
/tasks/monthly-review/sku-governance |
2处表名 → fn_dish_sku_abc($1) |
2.3 新增同比/环比接口
// 同比:当前月 vs 去年同月
router.get('/overview/yoy', async (req: AuthRequest, res) => {
try {
const month = parseMonth(req)
const prevYear = prevYearMonth(month)
const [current, previous] = await Promise.all([
query(`SELECT bill_count, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bills, member_share_pct FROM analytics.mv_overview_monthly WHERE month = $1::date`, [month]),
query(`SELECT bill_count, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bills, member_share_pct FROM analytics.mv_overview_monthly WHERE month = $1::date`, [prevYear])
])
const c = current.rows[0] as any
const p = previous.rows[0] as any
if (!c) { sendSuccess(res, null); return }
const calcChange = (cur: number, prev: number | null) => {
if (prev === null || prev === 0) return null
return round(((cur - prev) / Math.abs(prev)) * 100, 2)
}
sendSuccess(res, {
current: c,
previous: p,
yoy: p ? {
received_change_pct: calcChange(parseFloat(c.received), parseFloat(p.received)),
bill_count_change_pct: calcChange(parseInt(c.bill_count), parseInt(p.bill_count)),
avg_bill_value_change_pct: calcChange(parseFloat(c.avg_bill_value), parseFloat(p.avg_bill_value)),
discount_rate_change: p ? round(parseFloat(c.discount_rate_pct) - parseFloat(p.discount_rate_pct), 2) : null,
} : null
})
} catch (err: any) { sendError(res, err.message) }
})
// 环比:当前月 vs 上月
router.get('/overview/mom', async (req: AuthRequest, res) => {
try {
const month = parseMonth(req)
const prevMo = prevMonth(month)
// 同上结构,prevYear → prevMo
} catch (err: any) { sendError(res, err.message) }
})
// 月度趋势
router.get('/overview/trend', async (req: AuthRequest, res) => {
try {
const endMonth = parseMonth(req)
const months = parseInt(req.query.months as string) || 12
const startDate = new Date(endMonth)
startDate.setMonth(startDate.getMonth() - months + 1)
const startMonth = startDate.toISOString().slice(0, 10)
const result = await query(`
SELECT month, bill_count, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bills, member_share_pct
FROM analytics.mv_overview_monthly
WHERE month >= $1::date AND month <= $2::date
ORDER BY month
`, [startMonth, endMonth])
sendSuccess(res, result.rows)
} catch (err: any) { sendError(res, err.message) }
})
// 利润趋势(多月瀑布汇总)
router.get('/overview/profit-trend', async (req: AuthRequest, res) => {
try {
const endMonth = parseMonth(req)
const months = parseInt(req.query.months as string) || 6
const startDate = new Date(endMonth)
startDate.setMonth(startDate.getMonth() - months + 1)
const startMonth = startDate.toISOString().slice(0, 10)
// 逐月查询利润瀑布数据
const result = await query(`
WITH monthly AS (
SELECT e.report_month,
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
FROM analytics.mv_store_risk_rating r
JOIN analytics.mv_store_operating_expense_monthly e
ON r.store_code = e.sales_store_code
WHERE e.report_month >= $1::date AND e.report_month <= $2::date
AND e.operating_expense IS NOT NULL AND r.received > 0
GROUP BY e.report_month
ORDER BY e.report_month
)
SELECT *,
round(store_contribution / nullif(received, 0) * 100, 2) AS contribution_margin_pct
FROM monthly
`, [startMonth, endMonth])
sendSuccess(res, result.rows)
} catch (err: any) { sendError(res, err.message) }
})
// 时间段汇总
router.get('/overview/period', async (req: AuthRequest, res) => {
try {
const { start, end, mode, label } = parseDateRange(req)
const result = await query(`
SELECT
sum(bill_count)::bigint AS bill_count,
round(sum(received)::numeric, 2) AS received,
round(sum(discounts)::numeric, 2) AS discounts,
round(sum(discounts) / nullif(sum(received) + sum(discounts), 0) * 100, 2) AS discount_rate_pct,
round(sum(received) / nullif(sum(bill_count), 0), 2) AS avg_bill_value,
round(sum(guests)::numeric, 0) AS guests
FROM analytics.v_store_daily
WHERE business_date >= $1::date AND business_date < $2::date
`, [start, end])
sendSuccess(res, { ...result.rows[0], range_mode: mode, range_label: label, start_date: start, end_date: end })
} catch (err: any) { sendError(res, err.message) }
})
2.4 利润瀑布/利润机会池支持时间段汇总
第3层:前端 — 月份选择器 + 时间范围切换
3.1 全局月份选择器组件
3.2 时间范围切换器
3.3 同比/环比展示组件
3.4 指标卡集成同比环比
3.5 趋势图组件
3.6 React Query 参数联动
3.7 页面布局调整
在以下页面顶部统一添加:
| 页面 |
调用的硬编码API |
| BossPage |
/overview/profit-waterfall, /overview/profit-opportunity, /store-expense/overview, /cost/comparison |
| RevenuePage |
/revenue/*(已参数化,需加选择器统一体验) |
| CostPage |
/cost/comparison(mv_store_theoretical_actual_cost_april) |
| StoreExpensePage |
/store-expense/*(11处 DATE '2026-04-01',含 /overview, /expense-structure, /store-ranking, /store-contribution, /rent-risk, /delivery-commission, /efficiency, /fixed-variable, /break-even, /loss-diagnosis, /store-evaluation) |
| DataQualityPage |
/data-quality(v_inventory_cost_classified_april) |
| CostAnalysisPage |
/cost-analysis/store-overview, /cost-analysis/store-ranking(mv_store_theoretical_actual_cost_april) |
| SiteSelectionPage |
/site-selection/*(5个 _april 表名) |
| SituationalAwarenessPage |
/situational-awareness/health-score, /correlation(mv_store_theoretical_actual_cost_april) |
| BankPage |
/bank/report(5处 DATE '2026-04-01') |
| DashboardPage |
/store-expense/overview(DATE '2026-04-01') |
| SKUPage |
/sku/abc(v_dish_sku_abc_april) |
| StoreDetailPage |
/stores/:code, /stores/:code/cost(多个 _april 表名)+ 前端硬编码 month: '2026-05' |
| StorePage |
/stores/:code/daily(前端硬编码 month: '2026-04')+ /tasks(前端硬编码 month: '2026-05') |
| MonthlyReviewPage |
/tasks/monthly-review/sku-governance(v_dish_sku_abc_april)+ 前端硬编码 month: '2026-05' |
| RegionalPage |
/stores/risk(mv_store_risk_rating 无月份字段)+ 前端硬编码 month: '2026-05' |
| TasksPage |
前端硬编码 month: '2026-05' |
| BomPenetrationPage |
前端硬编码 month: '2026-04' |
| CentralKitchenPage |
前端硬编码 month: '2026-04' |
| DistributionReconciliationPage |
前端硬编码 month: '2026-04' |
| ProductionPlanPage |
前端硬编码 month: '2026-04' |
第4层:数据管道 — 多月数据导入
4.1 数据导入脚本参数化
4.2 物化视图按月刷新策略
4.3 可用月份查询接口
四、实施路线图
| 阶段 |
时间 |
内容 |
优先级 |
涉及文件 |
| Phase 1 |
1-2天 |
后端 API 去硬编码 DATE '2026-04-01',统一用 parseMonth() |
P0 |
store-expense.ts, data.ts, cost-analysis.ts, situational-awareness.ts, tasks.ts |
| Phase 2 |
2-3天 |
数据库视图去 _april 后缀,改为参数化函数(第1批:核心利润链9个) |
P0 |
新建 SQL 迁移脚本 |
| Phase 3 |
1-2天 |
新增同比/环比/趋势/时间段汇总接口 |
P1 |
data.ts 新增接口 |
| Phase 4 |
2-3天 |
前端月份选择器 + 范围切换 + 同比环比展示 + 趋势图 |
P1 |
新建组件 + 改造各页面 |
| Phase 5 |
1-2天 |
数据库视图参数化(第2批:SKU/菜品9个) |
P2 |
新建 SQL 迁移脚本 |
| Phase 6 |
1-2天 |
数据库视图参数化(第3批:选址/空间14个) |
P2 |
新建 SQL 迁移脚本 |
| Phase 7 |
1天 |
数据导入脚本参数化 + 多月数据导入测试 |
P2 |
import_salary_attendance.py, etl_fact_inventory.sql |
| Phase 8 |
1天 |
mv_store_risk_rating 改为参数化函数 |
P2 |
SQL 迁移脚本 + 相关 API |
总工期:约 10-16 天
五、风险评估与注意事项
1. 数据覆盖
- 当前只有2026年4月数据,同比/环比功能需要至少2个月数据才能展示
- 需先导入多月数据(至少5月数据)才能验证环比功能
- 同比需要2025年数据,当前完全没有
2. 物化视图刷新
- 参数化后物化视图无法直接带参数,需改为函数或按月刷新策略
REFRESH MATERIALIZED VIEW 是全量刷新,数据量大时耗时较长
- 建议采用
CONCURRENTLY 选项避免锁表
3. 性能
- 时间段汇总查询(如全年)可能较慢,需考虑:
- 预聚合表:按月汇总后再按时间段聚合
- 查询缓存:Redis 或前端 React Query 缓存
- 分页加载:趋势图先加载近6月,可展开查看更多
4. 前端兼容
- 改造期间需保持4月数据正常展示
MonthPicker 默认值为 2026-04
- API 参数
month 默认值保持 2026-04,确保旧请求不报错
5. API 向后兼容
- 所有改造接口保持向后兼容:不传
month 参数时默认返回4月数据
- 新增接口(同比/环比/趋势)为独立路径,不影响现有接口
- 表名替换为函数调用时,保留旧视图作为别名(过渡期)
6. 测试策略
- 每个 Phase 完成后需验证:
- 不传
month 参数时,4月数据与改造前完全一致
- 传
month=2026-04 时,结果与不传参数一致
- 传
month=2026-05 时(如有数据),返回5月数据
- 回归测试:所有页面的4月数据展示不受影响
六、完整性检查清单
数据库层
后端 API 层
前端层
数据管道层
测试验证
七、预期成果
改造完成后系统将具备:
- 任意月份分析:用户可选择任意有数据的月份进行分析,不再局限于4月
- 时间段汇总:支持季度/半年/全年/自定义区间的汇总分析
- 同比环比:每个核心指标都展示同比和环比变化率,一眼看出趋势
- 月度趋势图:可视化展示多月趋势,支持移动平均和同比叠加
- 数据管道参数化:导入脚本支持任意月份,不再需要修改代码
- 向后兼容:不传参数时默认返回4月数据,现有功能不受影响