feat: 统一SearchSelect下拉组件 + 修复营销方案数据 + 平台经济性增强
- 创建SearchSelect搜索下拉组件,选项≤8自动隐藏搜索框 - 全站替换原生select为SearchSelect(13处下拉) - 修复/revenue/store-ranking优惠率字段名不匹配(discount_rate_pct→avg_discount_rate_pct) - RevenuePage门店营收明细改用FilterableTable支持筛选排序分页 - 修复/marketing/plans按门店分组改为按marketing_plan分组 - 营销方案分析补充消费额、优惠额列和Top5概览卡片 - PlatformPage门店平台经济性增加门店搜索、经济性优列(平台分色) - 平台成本率算法说明显示在标题下方
This commit is contained in:
+83
-125
@@ -117,20 +117,22 @@ router.get('/stores/:code', async (req: AuthRequest, res) => {
|
||||
const month = parseMonth(req)
|
||||
const [scorecard, risk, platform, benchmark, action] = await Promise.all([
|
||||
query(`
|
||||
SELECT store_code, store_name,
|
||||
SELECT c002 AS store_code, c003 AS store_name,
|
||||
count(*) AS bill_count,
|
||||
count(DISTINCT closed_at::date) AS active_days,
|
||||
sum(received_total) AS received,
|
||||
round(sum(received_total) / NULLIF(count(DISTINCT closed_at::date), 0), 2) AS avg_daily_received,
|
||||
round(sum(received_total) / count(*), 2) AS avg_bill_value,
|
||||
round(sum(received_total) / NULLIF(sum(guest_count), 0), 2) AS avg_guest_value,
|
||||
round(sum(discount_total) / NULLIF(sum(consumption), 0) * 100, 2) AS discount_rate_pct,
|
||||
round(sum(theoretical_profit) / NULLIF(sum(received_total), 0) * 100, 2) AS theoretical_margin_pct,
|
||||
round(count(*) FILTER (WHERE member_id IS NOT NULL)::numeric / count(*) * 100, 2) AS member_bill_share_pct
|
||||
FROM analytics.fact_bill
|
||||
WHERE store_code = $1
|
||||
AND closed_at >= $2::date AND closed_at < ($2::date + interval '1 month')
|
||||
GROUP BY store_code, store_name
|
||||
count(DISTINCT c176::date) AS active_days,
|
||||
round(sum(COALESCE(NULLIF(c114,'')::numeric,0)), 2) AS received,
|
||||
round(sum(COALESCE(NULLIF(c114,'')::numeric,0)) / NULLIF(count(DISTINCT c176::date), 0), 2) AS avg_daily_received,
|
||||
round(sum(COALESCE(NULLIF(c114,'')::numeric,0)) / count(*), 2) AS avg_bill_value,
|
||||
round(sum(COALESCE(NULLIF(c114,'')::numeric,0)) / NULLIF(sum(COALESCE(NULLIF(c178,'')::numeric,0)), 0), 2) AS avg_guest_value,
|
||||
round(sum(COALESCE(NULLIF(c068,'')::numeric,0)) / NULLIF(sum(COALESCE(NULLIF(c009,'')::numeric,0)), 0) * 100, 2) AS discount_rate_pct,
|
||||
round(sum(COALESCE(NULLIF(c181,'')::numeric,0)) / NULLIF(sum(COALESCE(NULLIF(c114,'')::numeric,0)), 0) * 100, 2) AS theoretical_margin_pct,
|
||||
round(count(*) FILTER (WHERE NULLIF(c185,'') IS NOT NULL)::numeric / count(*) * 100, 2) AS member_bill_share_pct
|
||||
FROM bill_records
|
||||
WHERE c002 = $1
|
||||
AND c176 IS NOT NULL AND c176 != ''
|
||||
AND c176::timestamp >= $2::date
|
||||
AND c176::timestamp < ($2::date + interval '1 month')
|
||||
GROUP BY c002, c003
|
||||
`, [code, month]),
|
||||
query(`SELECT * FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 AND store_code = $2`, [month, code]),
|
||||
query(`SELECT * FROM analytics.mv_store_platform_economics_monthly WHERE month_start = $2 AND store_code = $1`, [code, month]),
|
||||
@@ -183,20 +185,23 @@ router.get('/stores/:code/daily', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const code = req.params.code
|
||||
const month = parseMonth(req)
|
||||
const storeInfo = await query(`SELECT store_name FROM analytics.mv_store_risk_rating_monthly WHERE store_code = $1 AND month_start = $2::date LIMIT 1`, [code, month])
|
||||
if (storeInfo.rows.length === 0) { sendSuccess(res, []); return }
|
||||
const storeName = storeInfo.rows[0].store_name
|
||||
const result = await query(`
|
||||
SELECT closed_at::date AS business_date,
|
||||
SELECT c176::date AS business_date,
|
||||
count(*) AS bill_count,
|
||||
round(sum(received_total), 2) AS received,
|
||||
round(sum(received_total) / count(*), 2) AS avg_bill_value,
|
||||
round(sum(discount_total) / nullif(sum(consumption), 0) * 100, 2) AS discount_rate_pct
|
||||
FROM analytics.fact_bill
|
||||
WHERE store_code = $1
|
||||
AND closed_at >= $2::date
|
||||
AND closed_at < ($2::date + interval '1 month')
|
||||
AND closed_at IS NOT NULL
|
||||
GROUP BY closed_at::date
|
||||
round(sum(${RECEIVED_COLS}), 2) AS received,
|
||||
round(sum(${RECEIVED_COLS}) / count(*), 2) AS avg_bill_value,
|
||||
round(sum(COALESCE(c068::numeric,0)) / nullif(sum(c009::numeric), 0) * 100, 2) AS discount_rate_pct
|
||||
FROM bill_records
|
||||
WHERE c003 = $1
|
||||
AND c176 IS NOT NULL AND c176 != ''
|
||||
AND c176::timestamp >= $2::date
|
||||
AND c176::timestamp < ($2::date + interval '1 month')
|
||||
GROUP BY c176::date
|
||||
ORDER BY business_date
|
||||
`, [code, month])
|
||||
`, [storeName, month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -291,7 +296,7 @@ router.get('/member/comparison', async (req: AuthRequest, res) => {
|
||||
round(avg(discount_total), 2) AS avg_discount,
|
||||
round(sum(discount_total) / NULLIF(sum(consumption), 0) * 100, 2) AS discount_rate_pct,
|
||||
round(sum(theoretical_profit) / NULLIF(sum(received_total), 0) * 100, 2) AS theoretical_margin_pct
|
||||
FROM analytics.fact_bill
|
||||
FROM analytics.bill_fact
|
||||
WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
|
||||
GROUP BY CASE WHEN member_id IS NULL THEN '非会员' ELSE '会员' END
|
||||
ORDER BY customer_type
|
||||
@@ -351,44 +356,32 @@ router.get('/risk/anomaly', async (req: AuthRequest, res) => {
|
||||
const reason = req.query.reason as string
|
||||
const cashier = req.query.cashier as string
|
||||
|
||||
const conditions: string[] = [
|
||||
`c175 IS NOT NULL AND c175 != ''`,
|
||||
`c175::timestamp >= $1::date AND c175::timestamp < ($1::date + interval '1 month')`,
|
||||
`((c009::numeric > 0 AND (${RECEIVED_COLS}) = 0) OR (c068::numeric > c009::numeric) OR (abs(c009::numeric - COALESCE(c068::numeric,0) - (${RECEIVED_COLS})) > 0.05))`,
|
||||
]
|
||||
const conditions: string[] = [`month = to_char($1::date, 'YYYY-MM')`]
|
||||
const params: any[] = [month]
|
||||
let paramIdx = 2
|
||||
if (storeName) {
|
||||
conditions.push(`c003 = $${paramIdx}`)
|
||||
conditions.push(`store_name = $${paramIdx}`)
|
||||
params.push(storeName)
|
||||
paramIdx++
|
||||
}
|
||||
if (reason) {
|
||||
if (reason === '有消费无实收') conditions.push(`c009::numeric > 0 AND (${RECEIVED_COLS}) = 0`)
|
||||
else if (reason === '优惠大于消费') conditions.push(`c068::numeric > c009::numeric`)
|
||||
else if (reason === '消费-优惠与实收不平') conditions.push(`abs(c009::numeric - COALESCE(c068::numeric,0) - (${RECEIVED_COLS})) > 0.05 AND NOT (c009::numeric > 0 AND (${RECEIVED_COLS}) = 0) AND NOT (c068::numeric > c009::numeric)`)
|
||||
conditions.push(`anomaly_reason = $${paramIdx}`)
|
||||
params.push(reason)
|
||||
paramIdx++
|
||||
}
|
||||
if (cashier) {
|
||||
conditions.push(`c191 ILIKE $${paramIdx}`)
|
||||
conditions.push(`cashier ILIKE $${paramIdx}`)
|
||||
params.push(`%${cashier}%`)
|
||||
paramIdx++
|
||||
}
|
||||
const whereClause = conditions.join(' AND ')
|
||||
|
||||
const countResult = await query(`SELECT count(*) AS total, round(sum(c009::numeric),2) AS sum_consumption, round(sum(COALESCE(c068::numeric,0)),2) AS sum_discount, round(sum(${RECEIVED_COLS}),2) AS sum_received FROM bill_records WHERE ${whereClause}`, params)
|
||||
const countResult = await query(`SELECT count(*) AS total, round(sum(consumption),2) AS sum_consumption, round(sum(discount_total),2) AS sum_discount, round(sum(received_total),2) AS sum_received FROM mv_risk_anomaly WHERE ${whereClause}`, params)
|
||||
const result = await query(`
|
||||
SELECT c003 AS store_name, c005 AS bill_no, c004 AS meal_period,
|
||||
c009::numeric AS consumption, COALESCE(c068::numeric,0) AS discount_total,
|
||||
(${RECEIVED_COLS}) AS received_total,
|
||||
c191 AS cashier, c176 AS closed_at,
|
||||
CASE
|
||||
WHEN c009::numeric > 0 AND (${RECEIVED_COLS}) = 0 THEN '有消费无实收'
|
||||
WHEN c068::numeric > c009::numeric THEN '优惠大于消费'
|
||||
WHEN abs(c009::numeric - COALESCE(c068::numeric,0) - (${RECEIVED_COLS})) > 0.05 THEN '消费-优惠与实收不平'
|
||||
END AS anomaly_reason
|
||||
FROM bill_records
|
||||
SELECT store_name, bill_no, meal_period, consumption, discount_total, received_total, cashier, closed_at, anomaly_reason
|
||||
FROM mv_risk_anomaly
|
||||
WHERE ${whereClause}
|
||||
ORDER BY c176 DESC
|
||||
ORDER BY closed_at DESC
|
||||
LIMIT $${paramIdx} OFFSET $${paramIdx + 1}
|
||||
`, [...params, pageSize, offset])
|
||||
sendSuccess(res, result.rows, { total: parseInt(countResult.rows[0].total), page, page_size: pageSize, sum_consumption: countResult.rows[0].sum_consumption, sum_discount: countResult.rows[0].sum_discount, sum_received: countResult.rows[0].sum_received })
|
||||
@@ -402,26 +395,17 @@ router.get('/risk/zero-received', async (req: AuthRequest, res) => {
|
||||
const month = parseMonth(req)
|
||||
const storeName = req.query.store as string
|
||||
let sql = `
|
||||
SELECT c003 AS store_name, c005 AS bill_no, c004 AS meal_period,
|
||||
c009::numeric AS consumption, COALESCE(c068::numeric,0) AS discount_total,
|
||||
0 AS received_total,
|
||||
c191 AS cashier, c176 AS closed_at,
|
||||
CASE
|
||||
WHEN c068::numeric >= c009::numeric AND c009::numeric > 0 THEN '全额优惠'
|
||||
WHEN c009::numeric = 0 OR c009 IS NULL OR c009 = '' THEN '零消费零实收'
|
||||
ELSE '有消费无实收'
|
||||
END AS zero_received_type
|
||||
FROM bill_records
|
||||
WHERE c175 IS NOT NULL AND c175 != ''
|
||||
AND c175::timestamp >= $1::date AND c175::timestamp < ($1::date + interval '1 month')
|
||||
AND c009::numeric > 0 AND (${RECEIVED_COLS}) = 0
|
||||
SELECT store_name, bill_no, meal_period, consumption, discount_total,
|
||||
0 AS received_total, cashier, closed_at, zero_received_type
|
||||
FROM mv_risk_zero
|
||||
WHERE month = to_char($1::date, 'YYYY-MM')
|
||||
`
|
||||
const params: any[] = [month]
|
||||
if (storeName) {
|
||||
sql += ` AND c003 = $2`
|
||||
sql += ` AND store_name = $2`
|
||||
params.push(storeName)
|
||||
}
|
||||
sql += ` ORDER BY c009::numeric DESC LIMIT 200`
|
||||
sql += ` ORDER BY consumption DESC LIMIT 200`
|
||||
const result = await query(sql, params)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
@@ -433,18 +417,14 @@ router.get('/risk/cashier', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`
|
||||
SELECT c003 AS store_name, c191 AS cashier,
|
||||
count(*) AS bill_count,
|
||||
round(sum(${RECEIVED_COLS})::numeric, 2) AS received,
|
||||
count(*) FILTER (WHERE c009::numeric > 0 AND (${RECEIVED_COLS}) = 0) AS anomaly_bills,
|
||||
round(count(*) FILTER (WHERE c009::numeric > 0 AND (${RECEIVED_COLS}) = 0)::numeric / count(*) * 100, 2) AS anomaly_rate_pct,
|
||||
round(sum(c009::numeric) FILTER (WHERE c009::numeric > 0 AND (${RECEIVED_COLS}) = 0)::numeric, 2) AS anomaly_consumption
|
||||
FROM bill_records
|
||||
WHERE c175 IS NOT NULL AND c175 != ''
|
||||
AND c175::timestamp >= $1::date AND c175::timestamp < ($1::date + interval '1 month')
|
||||
AND c191 IS NOT NULL AND c191 != ''
|
||||
GROUP BY c003, c191
|
||||
ORDER BY anomaly_rate_pct DESC NULLS LAST
|
||||
SELECT store_name, cashier, bill_count,
|
||||
received,
|
||||
(anomaly_no_received + anomaly_discount_over + anomaly_unbalanced) AS anomaly_bills,
|
||||
round((anomaly_no_received + anomaly_discount_over + anomaly_unbalanced)::numeric / bill_count * 100, 2) AS anomaly_rate_pct,
|
||||
consumption_total
|
||||
FROM mv_risk_cashier
|
||||
WHERE month = to_char($1::date, 'YYYY-MM') AND cashier IS NOT NULL AND cashier != ''
|
||||
ORDER BY (anomaly_no_received + anomaly_discount_over + anomaly_unbalanced) DESC NULLS LAST
|
||||
`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
@@ -456,17 +436,18 @@ router.get('/marketing/plans', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`
|
||||
SELECT store_code, store_name,
|
||||
SELECT marketing_plan,
|
||||
count(*) AS bill_count,
|
||||
sum(received_total) AS received,
|
||||
sum(discount_total) AS discount_amount,
|
||||
round(sum(consumption)::numeric, 2) AS consumption,
|
||||
round(sum(discount_total)::numeric, 2) AS discounts,
|
||||
round(sum(received_total)::numeric, 2) AS received,
|
||||
round(avg(received_total), 2) AS avg_bill_value,
|
||||
round(sum(discount_total) / NULLIF(sum(consumption), 0) * 100, 2) AS discount_rate_pct,
|
||||
round(sum(third_party_discount) / NULLIF(sum(received_total), 0) * 100, 2) AS third_party_rate_pct,
|
||||
count(*) FILTER (WHERE marketing_plan IS NOT NULL AND marketing_plan != '') AS plan_bills,
|
||||
round(count(*) FILTER (WHERE marketing_plan IS NOT NULL AND marketing_plan != '')::numeric / count(*) * 100, 2) AS plan_coverage_pct
|
||||
FROM analytics.fact_bill
|
||||
WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
|
||||
GROUP BY store_code, store_name
|
||||
round(sum(theoretical_profit) / NULLIF(sum(received_total), 0) * 100, 2) AS theoretical_margin_pct
|
||||
FROM analytics.bill_fact
|
||||
WHERE marketing_plan IS NOT NULL AND marketing_plan != ''
|
||||
AND closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
|
||||
GROUP BY marketing_plan
|
||||
ORDER BY received DESC
|
||||
`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
@@ -489,18 +470,10 @@ router.get('/time/weekday', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`
|
||||
SELECT EXTRACT(isodow FROM closed_at)::int AS weekday_no,
|
||||
CASE EXTRACT(isodow FROM closed_at)::int
|
||||
WHEN 1 THEN '周一' WHEN 2 THEN '周二' WHEN 3 THEN '周三'
|
||||
WHEN 4 THEN '周四' WHEN 5 THEN '周五' WHEN 6 THEN '周六' ELSE '周日'
|
||||
END AS weekday,
|
||||
count(*) AS bill_count,
|
||||
sum(received_total) AS received,
|
||||
round(sum(received_total) / count(*), 2) AS avg_bill_value,
|
||||
round(avg(duration_minutes), 2) AS avg_duration_minutes
|
||||
FROM analytics.fact_bill
|
||||
WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
|
||||
GROUP BY 1, 2 ORDER BY 1
|
||||
SELECT weekday_no, weekday, bill_count, received, avg_bill_value, avg_duration_minutes
|
||||
FROM mv_time_weekday
|
||||
WHERE month = to_char($1::date, 'YYYY-MM')
|
||||
ORDER BY weekday_no
|
||||
`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
@@ -512,15 +485,10 @@ router.get('/time/hourly', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`
|
||||
SELECT EXTRACT(hour FROM closed_at)::int AS closing_hour,
|
||||
count(*) AS bill_count,
|
||||
sum(received_total) AS received,
|
||||
round(sum(received_total) / count(*), 2) AS avg_bill_value,
|
||||
round(avg(duration_minutes), 2) AS avg_duration_minutes
|
||||
FROM analytics.fact_bill
|
||||
WHERE closed_at IS NOT NULL
|
||||
AND closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
|
||||
GROUP BY 1 ORDER BY 1
|
||||
SELECT closing_hour, bill_count, received, avg_bill_value, avg_duration_minutes
|
||||
FROM mv_time_hourly
|
||||
WHERE month = to_char($1::date, 'YYYY-MM')
|
||||
ORDER BY closing_hour
|
||||
`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
@@ -532,20 +500,10 @@ router.get('/channel', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`
|
||||
SELECT closed_at::date AS business_date,
|
||||
sum(cash_received) AS cash,
|
||||
sum(alipay_received) AS alipay,
|
||||
sum(wechat_received) AS wechat,
|
||||
sum(meituan_received) AS meituan,
|
||||
sum(unionpay_received) AS unionpay,
|
||||
sum(douyin_received) AS douyin,
|
||||
sum(credit_received) AS credit,
|
||||
sum(jd_delivery_received) AS jd_delivery,
|
||||
sum(meituan_delivery_received) AS meituan_delivery,
|
||||
sum(taobao_delivery_received) AS taobao_delivery
|
||||
FROM analytics.fact_bill
|
||||
WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
|
||||
GROUP BY 1 ORDER BY 1
|
||||
SELECT business_date, cash, alipay, wechat, meituan, unionpay, douyin, credit, jd_delivery, meituan_delivery, taobao_delivery
|
||||
FROM mv_channel_daily
|
||||
WHERE month = to_char($1::date, 'YYYY-MM')
|
||||
ORDER BY business_date
|
||||
`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
@@ -567,7 +525,7 @@ router.get('/data-quality', async (req: AuthRequest, res) => {
|
||||
count(DISTINCT store_code) AS store_count,
|
||||
min(closed_at)::text AS min_date,
|
||||
max(closed_at)::text AS max_date
|
||||
FROM analytics.fact_bill
|
||||
FROM analytics.bill_fact
|
||||
`)
|
||||
const dishStats = await query(`
|
||||
SELECT
|
||||
@@ -604,7 +562,7 @@ router.get('/stores/:code/meal-period', async (req: AuthRequest, res) => {
|
||||
SELECT meal_period,
|
||||
count(*) AS network_bills,
|
||||
(sum(received_total) / NULLIF(count(*), 0)) AS network_avg_bill
|
||||
FROM analytics.fact_bill
|
||||
FROM analytics.bill_fact
|
||||
WHERE meal_period IS NOT NULL
|
||||
AND closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
|
||||
GROUP BY meal_period
|
||||
@@ -613,7 +571,7 @@ router.get('/stores/:code/meal-period', async (req: AuthRequest, res) => {
|
||||
count(*) AS bill_count,
|
||||
sum(received_total) AS received,
|
||||
(sum(received_total) / NULLIF(count(*), 0)) AS avg_bill
|
||||
FROM analytics.fact_bill
|
||||
FROM analytics.bill_fact
|
||||
WHERE meal_period IS NOT NULL
|
||||
AND store_code = $2
|
||||
AND closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
|
||||
@@ -712,8 +670,8 @@ router.get('/stores/:code/anomalies', async (req: AuthRequest, res) => {
|
||||
const { page, pageSize, offset } = parsePagination(req)
|
||||
const month = parseMonth(req)
|
||||
const code = req.params.code
|
||||
const countResult = await query(`SELECT count(*) AS total FROM analytics.fact_bill WHERE store_code = $1 AND is_anomaly = true AND closed_at >= $2::date AND closed_at < ($2::date + interval '1 month')`, [code, month])
|
||||
const result = await query(`SELECT * FROM analytics.fact_bill WHERE store_code = $1 AND is_anomaly = true AND closed_at >= $2::date AND closed_at < ($2::date + interval '1 month') ORDER BY closed_at DESC LIMIT $3 OFFSET $4`, [code, month, pageSize, offset])
|
||||
const countResult = await query(`SELECT count(*) AS total FROM mv_risk_anomaly WHERE store_name = (SELECT store_name FROM analytics.dim_store WHERE store_code = $1) AND month = to_char($2::date, 'YYYY-MM')`, [code, month])
|
||||
const result = await query(`SELECT * FROM mv_risk_anomaly WHERE store_name = (SELECT store_name FROM analytics.dim_store WHERE store_code = $1) AND month = to_char($2::date, 'YYYY-MM') ORDER BY closed_at DESC LIMIT $3 OFFSET $4`, [code, month, pageSize, offset])
|
||||
sendSuccess(res, result.rows, { total: parseInt(countResult.rows[0].total), page, page_size: pageSize })
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
@@ -734,7 +692,7 @@ router.get('/region/summary', async (req: AuthRequest, res) => {
|
||||
count(DISTINCT CASE WHEN r.risk_level = '红色' THEN b.store_code END) AS red_count,
|
||||
count(DISTINCT CASE WHEN r.risk_level = '黄色' THEN b.store_code END) AS yellow_count,
|
||||
count(DISTINCT CASE WHEN r.risk_level = '绿色' THEN b.store_code END) AS green_count
|
||||
FROM analytics.fact_bill b
|
||||
FROM analytics.bill_fact b
|
||||
JOIN analytics.dim_store d ON d.store_code = b.store_code
|
||||
LEFT JOIN analytics.mv_store_risk_rating_monthly r ON r.store_code = b.store_code AND r.month_start = $1::date
|
||||
WHERE d.region IS NOT NULL AND d.region <> ''
|
||||
@@ -1267,7 +1225,7 @@ router.get('/revenue/store-ranking', async (req: AuthRequest, res) => {
|
||||
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(discounts) / nullif(sum(received) + sum(discounts), 0) * 100, 2) AS avg_discount_rate_pct,
|
||||
round(sum(received) / nullif(sum(bill_count), 0), 2) AS avg_bill_value,
|
||||
round(sum(guests)::numeric, 0) AS guests,
|
||||
round(avg(theoretical_margin_pct)::numeric, 2) AS avg_theoretical_margin_pct
|
||||
|
||||
@@ -113,8 +113,8 @@ router.get('/alerts', async (req: AuthRequest, res) => {
|
||||
SELECT closed_at::date AS business_date,
|
||||
count(*) AS bill_count,
|
||||
sum(received_total) AS received
|
||||
FROM analytics.fact_bill
|
||||
WHERE closed_at >= (SELECT max(closed_at)::date - interval '30 days' FROM analytics.fact_bill)
|
||||
FROM analytics.bill_fact
|
||||
WHERE closed_at >= (SELECT max(closed_at)::date - interval '30 days' FROM analytics.bill_fact)
|
||||
AND closed_at IS NOT NULL
|
||||
GROUP BY closed_at::date
|
||||
ORDER BY business_date
|
||||
|
||||
+32
-23
@@ -5,6 +5,8 @@ import type { AuthRequest } from '../middleware/auth.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
const RECEIVED_COLS = `COALESCE(c143::numeric,0)+COALESCE(c144::numeric,0)+COALESCE(c145::numeric,0)+COALESCE(c146::numeric,0)+COALESCE(c147::numeric,0)+COALESCE(c148::numeric,0)+COALESCE(c149::numeric,0)+COALESCE(c150::numeric,0)+COALESCE(c151::numeric,0)+COALESCE(c152::numeric,0)+COALESCE(c153::numeric,0)+COALESCE(c155::numeric,0)+COALESCE(c160::numeric,0)+COALESCE(c161::numeric,0)+COALESCE(c164::numeric,0)+COALESCE(c166::numeric,0)+COALESCE(c169::numeric,0)+COALESCE(c173::numeric,0)+COALESCE(c174::numeric,0)+COALESCE(c118::numeric,0)+COALESCE(c119::numeric,0)+COALESCE(c120::numeric,0)+COALESCE(c121::numeric,0)+COALESCE(c139::numeric,0)+COALESCE(c141::numeric,0)+COALESCE(c142::numeric,0)`
|
||||
|
||||
// ============================================================
|
||||
// 固定路径路由(必须在 /:id 之前定义)
|
||||
// ============================================================
|
||||
@@ -287,47 +289,54 @@ router.get('/stores/:code/daily-card', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const code = req.params.code
|
||||
const month = req.query.month as string | undefined
|
||||
let dateFilter: string
|
||||
let params: any[] = [code]
|
||||
let storeNameFilter: string
|
||||
let params: any[] = []
|
||||
if (month) {
|
||||
const monthDate = month + '-01'
|
||||
dateFilter = `closed_at::date = (SELECT max(closed_at)::date FROM analytics.fact_bill WHERE closed_at >= $2::date AND closed_at < ($2::date + interval '1 month') AND closed_at IS NOT NULL)`
|
||||
params.push(monthDate)
|
||||
const storeInfo = await query(`SELECT store_name FROM analytics.mv_store_risk_rating_monthly WHERE store_code = $1 AND month_start = $2::date LIMIT 1`, [code, monthDate])
|
||||
if (storeInfo.rows.length === 0) { sendSuccess(res, { anomalies: [], todos: [] }); return }
|
||||
const storeName = storeInfo.rows[0].store_name
|
||||
params = [storeName, monthDate]
|
||||
storeNameFilter = `c003 = $1 AND c176 IS NOT NULL AND c176 != '' AND c176::timestamp >= $2::date AND c176::timestamp < ($2::date + interval '1 month')`
|
||||
} else {
|
||||
dateFilter = `closed_at::date = (SELECT max(closed_at)::date FROM analytics.fact_bill WHERE closed_at IS NOT NULL)`
|
||||
const storeInfo = await query(`SELECT store_name FROM analytics.mv_store_risk_rating_monthly WHERE store_code = $1 LIMIT 1`, [code])
|
||||
if (storeInfo.rows.length === 0) { sendSuccess(res, { anomalies: [], todos: [] }); return }
|
||||
const storeName = storeInfo.rows[0].store_name
|
||||
params = [storeName]
|
||||
storeNameFilter = `c003 = $1 AND c176 IS NOT NULL AND c176 != ''`
|
||||
}
|
||||
const result = await query(`
|
||||
WITH target_day AS (
|
||||
SELECT max(closed_at)::date AS business_date
|
||||
FROM analytics.fact_bill
|
||||
WHERE ${dateFilter.replace('closed_at', 'fact_bill.closed_at')}
|
||||
SELECT max(c176::date) AS business_date
|
||||
FROM bill_records
|
||||
WHERE ${storeNameFilter}
|
||||
),
|
||||
today_stats AS (
|
||||
SELECT bf.store_code, bf.store_name,
|
||||
round(sum(bf.received_total), 2) AS received,
|
||||
SELECT c003 AS store_name,
|
||||
round(sum(${RECEIVED_COLS}), 2) AS received,
|
||||
count(*) AS bill_count,
|
||||
round(sum(bf.received_total) / count(*), 2) AS avg_bill
|
||||
FROM analytics.fact_bill bf, target_day td
|
||||
WHERE bf.store_code = $1 AND bf.closed_at::date = td.business_date
|
||||
GROUP BY bf.store_code, bf.store_name
|
||||
round(sum(${RECEIVED_COLS}) / count(*), 2) AS avg_bill
|
||||
FROM bill_records bf, target_day td
|
||||
WHERE ${storeNameFilter.replace('$1', '$1')} AND bf.c176::date = td.business_date
|
||||
GROUP BY c003
|
||||
),
|
||||
week_ago_stats AS (
|
||||
SELECT bf.store_code,
|
||||
round(sum(bf.received_total), 2) AS received,
|
||||
SELECT
|
||||
round(sum(${RECEIVED_COLS}), 2) AS received,
|
||||
count(*) AS bill_count,
|
||||
round(sum(bf.received_total) / count(*), 2) AS avg_bill
|
||||
FROM analytics.fact_bill bf, target_day td
|
||||
WHERE bf.store_code = $1 AND bf.closed_at::date = td.business_date - interval '7 days'
|
||||
GROUP BY bf.store_code
|
||||
round(sum(${RECEIVED_COLS}) / count(*), 2) AS avg_bill
|
||||
FROM bill_records bf, target_day td
|
||||
WHERE ${storeNameFilter.replace('$1', '$1')} AND bf.c176::date = td.business_date - interval '7 days'
|
||||
GROUP BY c003
|
||||
)
|
||||
SELECT t.store_code, t.store_name, '收入' AS module,
|
||||
SELECT t.store_name, '收入' AS module,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('metric', '实收', 'value', t.received, 'baseline', COALESCE(w.received, 0), 'is_anomaly', t.received < (COALESCE(w.received, 0) * 0.8)),
|
||||
jsonb_build_object('metric', '账单数', 'value', t.bill_count, 'baseline', COALESCE(w.bill_count, 0), 'is_anomaly', t.bill_count::numeric < (COALESCE(w.bill_count, 0) * 0.8)),
|
||||
jsonb_build_object('metric', '客单价', 'value', t.avg_bill, 'baseline', COALESCE(w.avg_bill, 0), 'is_anomaly', t.avg_bill < (COALESCE(w.avg_bill, 0) * 0.9))
|
||||
) AS anomalies
|
||||
FROM today_stats t
|
||||
LEFT JOIN week_ago_stats w ON t.store_code = w.store_code
|
||||
LEFT JOIN week_ago_stats w ON true
|
||||
`, params)
|
||||
const tasks = await query(`
|
||||
SELECT t.* FROM analytics.store_task t
|
||||
@@ -666,7 +675,7 @@ router.get('/monthly-review/activity-list', async (req: AuthRequest, res) => {
|
||||
round(avg(received_total), 2) AS avg_bill_value,
|
||||
round(sum(discount_total) / NULLIF(sum(consumption), 0) * 100, 2) AS discount_rate_pct,
|
||||
round(sum(theoretical_profit) / NULLIF(sum(received_total), 0) * 100, 2) AS theoretical_margin_pct
|
||||
FROM analytics.fact_bill
|
||||
FROM analytics.bill_fact
|
||||
WHERE marketing_plan IS NOT NULL
|
||||
AND closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
|
||||
GROUP BY marketing_plan
|
||||
|
||||
Reference in New Issue
Block a user