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:
freedakgmail
2026-08-01 23:08:34 +08:00
parent d724a565f5
commit ed7beb7e47
20 changed files with 1035 additions and 286 deletions
+32 -23
View File
@@ -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