风险与内控管理优化: 异常账单服务端分页/筛选/收银员搜索/合计, 收银员TOP15按异常账单数排序, 双击跳转明细, MonthPicker靠右, 侧边栏底部间距

This commit is contained in:
freedakgmail
2026-08-01 21:28:15 +08:00
parent ac8e39e2f9
commit d724a565f5
60 changed files with 3154 additions and 2561 deletions
+71 -7
View File
@@ -123,8 +123,15 @@ router.get('/monthly-review', async (req: AuthRequest, res) => {
router.get('/followup', async (req: AuthRequest, res) => {
try {
const result = await query(`SELECT * FROM analytics.mv_store_monthly_followup ORDER BY priority, store_code`)
sendSuccess(res, result.rows)
const month = req.query.month as string | undefined
if (month) {
const monthDate = month + '-01'
const result = await query(`SELECT * FROM analytics.mv_store_monthly_followup WHERE plan_month = $1::date ORDER BY priority, store_code`, [monthDate])
sendSuccess(res, result.rows)
} else {
const result = await query(`SELECT * FROM analytics.mv_store_monthly_followup ORDER BY priority, store_code`)
sendSuccess(res, result.rows)
}
} catch (err: any) {
sendError(res, err.message)
}
@@ -132,7 +139,8 @@ router.get('/followup', async (req: AuthRequest, res) => {
router.get('/grade-change', async (req: AuthRequest, res) => {
try {
const result = await query(`SELECT * FROM analytics.store_grade_change ORDER BY change_month DESC, store_code`)
const month = parseMonth(req)
const result = await query(`SELECT * FROM analytics.store_grade_change WHERE change_month = $1::date ORDER BY change_month DESC, store_code`, [month])
sendSuccess(res, result.rows)
} catch (err: any) {
sendError(res, err.message)
@@ -278,7 +286,49 @@ router.post('/notifications/dispatch', async (req: AuthRequest, res) => {
router.get('/stores/:code/daily-card', async (req: AuthRequest, res) => {
try {
const code = req.params.code
const result = await query(`SELECT * FROM analytics.v_store_daily_card WHERE store_code = $1 ORDER BY module`, [code])
const month = req.query.month as string | undefined
let dateFilter: string
let params: any[] = [code]
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)
} else {
dateFilter = `closed_at::date = (SELECT max(closed_at)::date FROM analytics.fact_bill WHERE closed_at IS NOT NULL)`
}
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')}
),
today_stats AS (
SELECT bf.store_code, bf.store_name,
round(sum(bf.received_total), 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
),
week_ago_stats AS (
SELECT bf.store_code,
round(sum(bf.received_total), 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
)
SELECT t.store_code, 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
`, params)
const tasks = await query(`
SELECT t.* FROM analytics.store_task t
WHERE t.store_code = $1 AND t.status IN ('待启动', '进行中')
@@ -601,14 +651,28 @@ router.get('/monthly-review/completion', async (req: AuthRequest, res) => {
router.get('/monthly-review/activity-list', async (req: AuthRequest, res) => {
try {
const month = parseMonth(req)
const result = await query(`
SELECT marketing_plan, bill_count, consumption, discounts, received,
SELECT marketing_plan, bill_count, consumption, discounts, received,
avg_bill_value, discount_rate_pct, theoretical_margin_pct,
ROUND(discounts * 100.0 / NULLIF(consumption, 0), 2) as discount_cost_rate,
ROUND(received - discounts, 2) as net_contribution
FROM analytics.v_marketing_plan_summary
FROM (
SELECT marketing_plan,
count(*) AS bill_count,
sum(consumption) AS consumption,
sum(discount_total) AS discounts,
sum(received_total) 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(theoretical_profit) / NULLIF(sum(received_total), 0) * 100, 2) AS theoretical_margin_pct
FROM analytics.fact_bill
WHERE marketing_plan IS NOT NULL
AND closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
GROUP BY marketing_plan
) t
ORDER BY received DESC
`)
`, [month])
sendSuccess(res, result.rows)
} catch (err: any) { sendError(res, err.message) }
})