perf: 全量物化视图优化 — 按月预计算所有慢函数,API响应从106秒降至0.09秒
- 创建12个按月物化视图替代实时函数调用 - phase10: mv_dish_sku_summary_monthly, mv_dish_sku_abc_monthly - phase11: mv_store_risk_rating/platform_economics/category_mix/member_opportunity/benchmark_composite/deep_diagnosis/dish_basket/dish_store_summary/theoretical_actual_cost (全部_monthly) - 后端所有路由从 fn_xxx() 改为 mv_xxx_monthly WHERE month_start = - 新增 analytics.refresh_all_monthly_views() 刷新函数 - 优化 fn_dish_sku_summary: CTE物化避免重复调用 - 优化 fn_dish_sku_abc: CTE物化 fn_dish_sku_summary 只调用一次 - 优化 fn_dish_sales: 添加 dish_name IS NOT NULL 提前过滤
This commit is contained in:
@@ -734,7 +734,8 @@ router.get('/store-overview', async (req: AuthRequest, res) => {
|
||||
count(*) FILTER (WHERE variance_level = '绿色-基本正常') AS green_count,
|
||||
count(*) FILTER (WHERE variance_level = '灰色-口径异常') AS gray_count,
|
||||
round(sum(food_cost_variance)::numeric, 2) AS total_variance
|
||||
FROM analytics.fn_store_theoretical_actual_cost($1)
|
||||
FROM analytics.mv_store_theoretical_actual_cost_monthly
|
||||
WHERE month_start = $1
|
||||
`, [month])
|
||||
sendSuccess(res, result.rows[0])
|
||||
} catch (err: any) {
|
||||
@@ -752,9 +753,9 @@ router.get('/store-map', async (req: AuthRequest, res) => {
|
||||
round(c.theoretical_cost_rate_pct::numeric, 2) AS theo_cost_rate,
|
||||
round(c.actual_food_cost_rate_pct::numeric, 2) AS actual_cost_rate,
|
||||
l.latitude_gcj02, l.longitude_gcj02
|
||||
FROM analytics.fn_store_theoretical_actual_cost($1) c
|
||||
FROM analytics.mv_store_theoretical_actual_cost_monthly c
|
||||
LEFT JOIN analytics.v_store_location_operating l ON l.store_code = c.store_code
|
||||
WHERE l.latitude_gcj02 IS NOT NULL
|
||||
WHERE c.month_start = $1 AND l.latitude_gcj02 IS NOT NULL
|
||||
`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
@@ -780,7 +781,7 @@ router.get('/store-ranking', async (req: AuthRequest, res) => {
|
||||
const sortCol = validSorts.includes(sort) ? sort : 'food_cost_variance'
|
||||
|
||||
const month = parseMonth(req)
|
||||
const countResult = await query(`SELECT count(*) FROM analytics.fn_store_theoretical_actual_cost($1) ${where}`, [month])
|
||||
const countResult = await query(`SELECT count(*) FROM analytics.mv_store_theoretical_actual_cost_monthly WHERE month_start = $1 ${where}`, [month])
|
||||
const total = countResult.rows[0].count
|
||||
|
||||
const result = await query(`
|
||||
@@ -791,7 +792,8 @@ router.get('/store-ranking', async (req: AuthRequest, res) => {
|
||||
round(food_cost_variance::numeric, 2) AS variance_amount,
|
||||
negative_item_lines,
|
||||
variance_level
|
||||
FROM analytics.fn_store_theoretical_actual_cost($1)
|
||||
FROM analytics.mv_store_theoretical_actual_cost_monthly
|
||||
WHERE month_start = $1
|
||||
${where}
|
||||
ORDER BY ${sortCol} ${order} NULLS LAST LIMIT $2 OFFSET $3
|
||||
`, [month, pageSize, offset])
|
||||
|
||||
+33
-27
@@ -39,8 +39,8 @@ router.get('/stores', async (req: AuthRequest, res) => {
|
||||
if (riskLevel) {
|
||||
const m = parseMonth(req)
|
||||
sql = `SELECT s.* FROM analytics.v_store_scorecard s
|
||||
JOIN analytics.fn_store_risk_rating($${params.length + 1}) r ON s.store_code = r.store_code
|
||||
WHERE r.risk_level = $${params.length + 2}`
|
||||
JOIN analytics.mv_store_risk_rating_monthly r ON s.store_code = r.store_code
|
||||
WHERE r.month_start = $${params.length + 1} AND r.risk_level = $${params.length + 2}`
|
||||
params.push(m, riskLevel)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ router.get('/stores', async (req: AuthRequest, res) => {
|
||||
router.get('/stores/risk', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`SELECT * FROM analytics.fn_store_risk_rating($1) ORDER BY risk_level, received DESC`, [month])
|
||||
const result = await query(`SELECT * FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 ORDER BY risk_level, received DESC`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -113,9 +113,9 @@ router.get('/stores/:code', async (req: AuthRequest, res) => {
|
||||
const code = req.params.code
|
||||
const [scorecard, risk, platform, benchmark, action] = await Promise.all([
|
||||
query(`SELECT * FROM analytics.mv_store_scorecard WHERE store_code = $1`, [code]),
|
||||
query(`SELECT * FROM analytics.fn_store_risk_rating($1) WHERE store_code = $2`, [parseMonth(req), code]),
|
||||
query(`SELECT * FROM analytics.fn_store_platform_economics($2) WHERE store_code = $1`, [code, parseMonth(req)]),
|
||||
query(`SELECT * FROM analytics.fn_store_benchmark_composite($2) WHERE store_code = $1`, [code, parseMonth(req)]),
|
||||
query(`SELECT * FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 AND store_code = $2`, [parseMonth(req), code]),
|
||||
query(`SELECT * FROM analytics.mv_store_platform_economics_monthly WHERE month_start = $2 AND store_code = $1`, [code, parseMonth(req)]),
|
||||
query(`SELECT * FROM analytics.mv_store_benchmark_composite_monthly WHERE month_start = $2 AND store_code = $1`, [code, parseMonth(req)]),
|
||||
query(`SELECT * FROM analytics.fn_store_action_priority_deep($1) WHERE store_code = $2`, [parseMonth(req), code]),
|
||||
])
|
||||
|
||||
@@ -176,7 +176,7 @@ router.get('/stores/:code/daily', async (req: AuthRequest, res) => {
|
||||
router.get('/cost/comparison', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`SELECT * FROM analytics.fn_store_theoretical_actual_cost($1) ORDER BY variance_to_theoretical_pct DESC NULLS LAST`, [month])
|
||||
const result = await query(`SELECT * FROM analytics.mv_store_theoretical_actual_cost_monthly WHERE month_start = $1 ORDER BY variance_to_theoretical_pct DESC NULLS LAST`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -206,7 +206,7 @@ router.get('/cost/inventory', async (req: AuthRequest, res) => {
|
||||
router.get('/platform/economics', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`SELECT * FROM analytics.fn_store_platform_economics($1) ORDER BY meituan_received DESC NULLS LAST`, [month])
|
||||
const result = await query(`SELECT * FROM analytics.mv_store_platform_economics_monthly WHERE month_start = $1 ORDER BY meituan_received DESC NULLS LAST`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -235,7 +235,7 @@ router.get('/member/repeat', async (req: AuthRequest, res) => {
|
||||
router.get('/sku/abc', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`SELECT * FROM analytics.fn_dish_sku_abc($1) ORDER BY cumulative_revenue_share`, [month])
|
||||
const result = await query(`SELECT * FROM analytics.mv_dish_sku_abc_monthly WHERE month_start = $1 ORDER BY cumulative_revenue_share`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -310,7 +310,7 @@ router.get('/marketing/plans', async (req: AuthRequest, res) => {
|
||||
router.get('/benchmark/composite', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`SELECT * FROM analytics.fn_store_benchmark_composite($1) ORDER BY benchmark_score DESC`, [month])
|
||||
const result = await query(`SELECT * FROM analytics.mv_store_benchmark_composite_monthly WHERE month_start = $1 ORDER BY benchmark_score DESC`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -396,7 +396,7 @@ router.get('/stores/:code/category-mix', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const [result, companyResult] = await Promise.all([
|
||||
query(`SELECT * FROM analytics.fn_store_category_mix($1) WHERE store_code = $2`, [month, req.params.code]),
|
||||
query(`SELECT * FROM analytics.mv_store_category_mix_monthly WHERE month_start = $1 AND store_code = $2`, [month, req.params.code]),
|
||||
query(`
|
||||
SELECT
|
||||
SUM(consumption) as total_consumption,
|
||||
@@ -405,7 +405,7 @@ router.get('/stores/:code/category-mix', async (req: AuthRequest, res) => {
|
||||
SUM(delivery_package) as total_delivery,
|
||||
SUM(cold_dishes) as total_cold,
|
||||
SUM(silk_road_food) as total_silk
|
||||
FROM analytics.fn_store_category_mix($1)
|
||||
FROM analytics.mv_store_category_mix_monthly WHERE month_start = $1
|
||||
`, [month]),
|
||||
])
|
||||
sendSuccess(res, { store: result.rows[0], company: companyResult.rows[0] })
|
||||
@@ -416,7 +416,7 @@ router.get('/stores/:code/cost', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const [costResult, catResult] = await Promise.all([
|
||||
query(`SELECT * FROM analytics.fn_store_theoretical_actual_cost($1) WHERE store_code = $2`, [month, req.params.code]),
|
||||
query(`SELECT * FROM analytics.mv_store_theoretical_actual_cost_monthly WHERE month_start = $1 AND store_code = $2`, [month, req.params.code]),
|
||||
query(`
|
||||
WITH store_cat AS (
|
||||
SELECT finance_category AS category_name,
|
||||
@@ -450,7 +450,7 @@ router.get('/stores/:code/member', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const [oppResult, repeatResult, monthlyResult] = await Promise.all([
|
||||
query(`SELECT * FROM analytics.fn_store_member_opportunity($2) WHERE store_code = $1`, [req.params.code, month]),
|
||||
query(`SELECT * FROM analytics.mv_store_member_opportunity_monthly WHERE month_start = $2 AND store_code = $1`, [req.params.code, month]),
|
||||
query(`SELECT * FROM analytics.mv_store_repeat_summary_monthly WHERE store_code = $1 AND month_start = $2::date`, [req.params.code, month]),
|
||||
query(`
|
||||
SELECT store_code, store_name, count(*) as member_count,
|
||||
@@ -545,10 +545,10 @@ router.get('/overview/profit-waterfall', async (req: AuthRequest, res) => {
|
||||
e.repair_clean_expense,
|
||||
e.operating_expense,
|
||||
CASE WHEN e.operating_expense IS NOT NULL THEN true ELSE false END AS has_expense
|
||||
FROM analytics.fn_store_risk_rating($1) r
|
||||
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.received IS NOT NULL AND e.operating_expense IS NOT NULL
|
||||
WHERE r.month_start = $1 AND r.received IS NOT NULL AND e.operating_expense IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
round(sum(received)::numeric, 2) AS received,
|
||||
@@ -584,10 +584,10 @@ router.get('/overview/store-profit-ranking', async (req: AuthRequest, res) => {
|
||||
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,
|
||||
r.risk_level
|
||||
FROM analytics.fn_store_risk_rating($1) r
|
||||
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.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 AND e.operating_expense IS NOT NULL
|
||||
ORDER BY store_contribution DESC
|
||||
`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
@@ -601,12 +601,18 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`
|
||||
WITH standard_stores AS (
|
||||
WITH risk AS (
|
||||
SELECT * FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1
|
||||
),
|
||||
sku_abc AS (
|
||||
SELECT * FROM analytics.mv_dish_sku_abc_monthly WHERE month_start = $1
|
||||
),
|
||||
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,
|
||||
r.theoretical_margin_pct, r.discount_rate_pct
|
||||
FROM analytics.fn_store_risk_rating($1) r
|
||||
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
|
||||
@@ -663,14 +669,14 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
|
||||
SELECT
|
||||
count(*) FILTER (WHERE discount_rate_pct > 25) AS high_discount_stores,
|
||||
round(sum(CASE WHEN discount_rate_pct > 25 THEN received * discount_rate_pct / nullif(100 - discount_rate_pct, 0) * (discount_rate_pct - 25) / nullif(discount_rate_pct, 0) ELSE 0 END)::numeric, 2) AS theoretical_saving
|
||||
FROM analytics.fn_store_risk_rating($1)
|
||||
FROM risk
|
||||
WHERE received IS NOT NULL AND received > 0
|
||||
),
|
||||
discount_stores AS (
|
||||
SELECT store_name, received,
|
||||
round(discount_rate_pct::numeric, 2) AS discount_rate,
|
||||
round((received * discount_rate_pct / nullif(100 - discount_rate_pct, 0) * (discount_rate_pct - 25) / nullif(discount_rate_pct, 0))::numeric, 2) AS potential_saving
|
||||
FROM analytics.fn_store_risk_rating($1)
|
||||
FROM risk
|
||||
WHERE received IS NOT NULL AND received > 0 AND discount_rate_pct > 25
|
||||
ORDER BY discount_rate_pct DESC
|
||||
LIMIT 5
|
||||
@@ -695,11 +701,11 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
|
||||
SELECT
|
||||
count(*) FILTER (WHERE received_amount < 1000 AND bill_count < 30) AS low_sku_count,
|
||||
round(sum(received_amount) FILTER (WHERE received_amount < 1000 AND bill_count < 30)::numeric, 2) AS low_sku_revenue
|
||||
FROM analytics.fn_dish_sku_abc($1)
|
||||
FROM sku_abc
|
||||
),
|
||||
sku_samples AS (
|
||||
SELECT dish_name, category_level1, received_amount, bill_count, abc_class
|
||||
FROM analytics.fn_dish_sku_abc($1)
|
||||
FROM sku_abc
|
||||
WHERE received_amount < 1000 AND bill_count < 30
|
||||
ORDER BY received_amount ASC
|
||||
LIMIT 10
|
||||
@@ -1051,7 +1057,7 @@ router.get('/bank/report', async (req: AuthRequest, res) => {
|
||||
SELECT r.received, e.actual_food_cost AS food_cost, e.wage_expense AS wage, e.rent_expense AS rent,
|
||||
e.utility_expense AS utility, e.dorm_expense AS dorm, e.delivery_commission_expense AS commission,
|
||||
e.card_fee_expense AS card_fee, e.repair_clean_expense AS repair, e.operating_expense AS operating_expense
|
||||
FROM analytics.fn_store_risk_rating($1) r
|
||||
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.received IS NOT NULL AND e.operating_expense IS NOT NULL
|
||||
)
|
||||
@@ -1062,9 +1068,9 @@ router.get('/bank/report', async (req: AuthRequest, res) => {
|
||||
round(sum(received)-sum(food_cost)-sum(operating_expense),2) AS store_contribution
|
||||
FROM full_scope
|
||||
`, [month]),
|
||||
query(`SELECT risk_level, count(*) AS store_count, round(sum(received)::numeric,2) AS total_received, round(avg(theoretical_margin_pct)::numeric,2) AS avg_margin_pct, round(avg(discount_rate_pct)::numeric,2) AS avg_discount_pct FROM analytics.fn_store_risk_rating($1) WHERE received IS NOT NULL GROUP BY risk_level ORDER BY risk_level`, [month]),
|
||||
query(`SELECT risk_level, count(*) AS store_count, round(sum(received)::numeric,2) AS total_received, round(avg(theoretical_margin_pct)::numeric,2) AS avg_margin_pct, round(avg(discount_rate_pct)::numeric,2) AS avg_discount_pct FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 AND received IS NOT NULL GROUP BY risk_level ORDER BY risk_level`, [month]),
|
||||
query(`SELECT round(sum(cash)::numeric,2) AS cash, round(sum(alipay)::numeric,2) AS alipay, round(sum(wechat)::numeric,2) AS wechat, round(sum(meituan)::numeric,2) AS meituan, round(sum(unionpay)::numeric,2) AS unionpay, round(sum(douyin)::numeric,2) AS douyin, round(sum(credit)::numeric,2) AS credit, round(sum(jd_delivery)::numeric,2) AS jd_delivery, round(sum(meituan_delivery)::numeric,2) AS meituan_delivery, round(sum(taobao_delivery)::numeric,2) AS taobao_delivery FROM analytics.v_channel_daily WHERE business_date >= $1::date AND business_date < $2::date`, [month, nextMonth]),
|
||||
query(`SELECT store_code, store_name, round(received::numeric,2) AS received, bill_count, round(avg_bill_value::numeric,2) AS avg_bill_value, round(discount_rate_pct::numeric,2) AS discount_rate_pct, round(theoretical_margin_pct::numeric,2) AS theoretical_margin_pct, risk_level FROM analytics.fn_store_risk_rating($1) WHERE received IS NOT NULL ORDER BY received DESC`, [month]),
|
||||
query(`SELECT store_code, store_name, round(received::numeric,2) AS received, bill_count, round(avg_bill_value::numeric,2) AS avg_bill_value, round(discount_rate_pct::numeric,2) AS discount_rate_pct, round(theoretical_margin_pct::numeric,2) AS theoretical_margin_pct, risk_level FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 AND received IS NOT NULL ORDER BY received DESC`, [month]),
|
||||
])
|
||||
|
||||
const ov = overview.rows[0] as any
|
||||
|
||||
@@ -15,14 +15,14 @@ router.get('/health-score', async (req: AuthRequest, res) => {
|
||||
SELECT store_code, store_name, received, bill_count,
|
||||
avg_bill_value, avg_daily_received, theoretical_margin_pct,
|
||||
member_bill_share_pct, risk_level
|
||||
FROM analytics.fn_store_risk_rating($1)
|
||||
WHERE received IS NOT NULL
|
||||
FROM analytics.mv_store_risk_rating_monthly
|
||||
WHERE month_start = $1 AND received IS NOT NULL
|
||||
),
|
||||
cost AS (
|
||||
SELECT store_code,
|
||||
round(avg(LEAST(variance_to_theoretical_pct, 50))::numeric, 1) AS avg_cost_variance_pct
|
||||
FROM analytics.fn_store_theoretical_actual_cost($1)
|
||||
WHERE variance_to_theoretical_pct IS NOT NULL
|
||||
FROM analytics.mv_store_theoretical_actual_cost_monthly
|
||||
WHERE month_start = $1 AND variance_to_theoretical_pct IS NOT NULL
|
||||
GROUP BY store_code
|
||||
),
|
||||
member AS (
|
||||
@@ -184,9 +184,9 @@ router.get('/alerts', async (req: AuthRequest, res) => {
|
||||
round(p.taobao_cost_rate_pct::numeric, 1) AS taobao_rate,
|
||||
round(p.jd_cost_rate_pct::numeric, 1) AS jd_rate,
|
||||
round((COALESCE(p.meituan_received, 0) + COALESCE(p.taobao_received, 0) + COALESCE(p.jd_received, 0)) / nullif(sc.received, 0) * 100, 1) AS platform_share
|
||||
FROM analytics.fn_store_platform_economics($1) p
|
||||
FROM analytics.mv_store_platform_economics_monthly p
|
||||
JOIN analytics.v_store_scorecard sc ON p.store_code = sc.store_code
|
||||
WHERE (COALESCE(p.meituan_received, 0) + COALESCE(p.taobao_received, 0) + COALESCE(p.jd_received, 0)) / nullif(sc.received, 0) > 0.4
|
||||
WHERE p.month_start = $1 AND (COALESCE(p.meituan_received, 0) + COALESCE(p.taobao_received, 0) + COALESCE(p.jd_received, 0)) / nullif(sc.received, 0) > 0.4
|
||||
ORDER BY platform_share DESC
|
||||
LIMIT 10
|
||||
`, [month])
|
||||
@@ -293,12 +293,12 @@ router.get('/correlation', async (req: AuthRequest, res) => {
|
||||
WHEN COALESCE(c.avg_cost_variance_pct, 20) > 30 THEN '成本失控'
|
||||
ELSE '正常'
|
||||
END AS correlation_status
|
||||
FROM analytics.fn_store_risk_rating($1) r
|
||||
FROM analytics.mv_store_risk_rating_monthly r
|
||||
LEFT JOIN (
|
||||
SELECT store_code,
|
||||
round(avg(LEAST(variance_to_theoretical_pct, 50))::numeric, 1) AS avg_cost_variance_pct
|
||||
FROM analytics.fn_store_theoretical_actual_cost($1)
|
||||
WHERE variance_to_theoretical_pct IS NOT NULL
|
||||
FROM analytics.mv_store_theoretical_actual_cost_monthly
|
||||
WHERE month_start = $1 AND variance_to_theoretical_pct IS NOT NULL
|
||||
GROUP BY store_code
|
||||
) c ON r.store_code = c.store_code
|
||||
WHERE r.received IS NOT NULL
|
||||
@@ -323,12 +323,13 @@ router.get('/correlation', async (req: AuthRequest, res) => {
|
||||
WHEN COALESCE(r.member_bill_share_pct, 0) > 50 THEN '会员驱动型'
|
||||
ELSE '均衡型'
|
||||
END AS channel_status
|
||||
FROM analytics.fn_store_risk_rating($1) r
|
||||
FROM analytics.mv_store_risk_rating_monthly r
|
||||
LEFT JOIN (
|
||||
SELECT pe.store_code,
|
||||
round((COALESCE(pe.meituan_received, 0) + COALESCE(pe.taobao_received, 0) + COALESCE(pe.jd_received, 0)) / nullif(r2.received, 0) * 100, 1) AS platform_share
|
||||
FROM analytics.fn_store_platform_economics($1) pe
|
||||
JOIN analytics.fn_store_risk_rating($1) r2 ON pe.store_code = r2.store_code
|
||||
FROM analytics.mv_store_platform_economics_monthly pe
|
||||
JOIN analytics.mv_store_risk_rating_monthly r2 ON pe.store_code = r2.store_code AND r2.month_start = pe.month_start
|
||||
WHERE pe.month_start = $1
|
||||
) p ON r.store_code = p.store_code
|
||||
LEFT JOIN analytics.mv_store_repeat_summary_monthly m ON r.store_code = m.store_code AND m.month_start = $1::date
|
||||
WHERE r.received IS NOT NULL
|
||||
|
||||
@@ -32,10 +32,10 @@ router.get('/overview', async (req: AuthRequest, res) => {
|
||||
CASE WHEN e.operating_expense IS NOT NULL THEN (r.received - e.actual_food_cost - e.operating_expense) ELSE NULL END AS actual_store_contribution,
|
||||
CASE WHEN e.operating_expense IS NOT NULL THEN (r.received - r.received * (1 - COALESCE(r.theoretical_margin_pct, 0) / 100) - e.operating_expense) ELSE NULL END AS theoretical_store_contribution,
|
||||
e.area_sqm
|
||||
FROM analytics.fn_store_risk_rating($1) r
|
||||
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.received IS NOT NULL
|
||||
WHERE r.month_start = $1 AND r.received IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
count(*) AS total_stores,
|
||||
|
||||
@@ -620,15 +620,16 @@ router.get('/monthly-review/sku-governance', async (req: AuthRequest, res) => {
|
||||
SELECT abc_class, count(*) as sku_count,
|
||||
ROUND(sum(revenue_share_pct), 1) as total_revenue_share,
|
||||
ROUND(avg(revenue_share_pct), 2) as avg_revenue_share
|
||||
FROM analytics.fn_dish_sku_abc($1)
|
||||
FROM analytics.mv_dish_sku_abc_monthly
|
||||
WHERE month_start = $1
|
||||
GROUP BY abc_class
|
||||
ORDER BY abc_class
|
||||
`, [month])
|
||||
const longtailResult = await query(`
|
||||
SELECT dish_name, category_level1, bill_count, sales_quantity,
|
||||
received_amount, revenue_share_pct, cumulative_revenue_share
|
||||
FROM analytics.fn_dish_sku_abc($1)
|
||||
WHERE abc_class = 'C-长尾'
|
||||
FROM analytics.mv_dish_sku_abc_monthly
|
||||
WHERE month_start = $1 AND abc_class = 'C-长尾'
|
||||
ORDER BY received_amount ASC
|
||||
LIMIT 50
|
||||
`, [month])
|
||||
|
||||
Reference in New Issue
Block a user