import { Router } from 'express' import { query } from '../config/database.js' import { sendSuccess, sendError, parseMonth, parsePagination } from '../middleware/error.js' import type { AuthRequest } from '../middleware/auth.js' const router = Router() router.get('/overview', async (req: AuthRequest, res) => { try { const month = parseMonth(req) const sql = ` SELECT 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, round(sum(theoretical_profit) / nullif(sum(received_total), 0) * 100, 2) AS theoretical_margin_pct, count(*) FILTER (WHERE member_id IS NOT NULL) AS member_bills, round(count(*) FILTER (WHERE member_id IS NOT NULL)::numeric / count(*) * 100, 2) AS member_share_pct FROM analytics.bill_fact WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month') AND closed_at IS NOT NULL ` const result = await query(sql, [month]) sendSuccess(res, result.rows[0]) } catch (err: any) { sendError(res, err.message) } }) router.get('/overview/daily', async (req: AuthRequest, res) => { try { const month = parseMonth(req) const sql = ` SELECT closed_at::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.bill_fact WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month') AND closed_at IS NOT NULL GROUP BY closed_at::date ORDER BY business_date ` const result = await query(sql, [month]) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/stores', async (req: AuthRequest, res) => { try { const { page, pageSize, offset } = parsePagination(req) const riskLevel = req.query.risk_level as string const quadrant = req.query.quadrant as string let sql = `SELECT * FROM analytics.v_store_scorecard` const params: any[] = [] const conditions: string[] = [] if (riskLevel) { sql = `SELECT s.* FROM analytics.v_store_scorecard s JOIN analytics.v_store_risk_rating r ON s.store_code = r.store_code WHERE r.risk_level = $1` params.push(riskLevel) } if (quadrant) { if (params.length > 0) { conditions.push(`b.management_quadrant = $${params.length + 1}`) sql = sql.replace('FROM analytics.v_store_scorecard s', 'FROM analytics.v_store_scorecard s JOIN analytics.v_store_benchmark b ON s.store_code = b.store_code') } else { sql = `SELECT s.* FROM analytics.v_store_scorecard s JOIN analytics.v_store_benchmark b ON s.store_code = b.store_code WHERE b.management_quadrant = $1` params.push(quadrant) } } sql += ` ORDER BY received DESC NULLS LAST` const countResult = await query(`SELECT count(*) AS total FROM (${sql}) t`, params) sql += ` LIMIT $${params.length + 1} OFFSET $${params.length + 2}` params.push(pageSize, offset) const result = await query(sql, params) sendSuccess(res, result.rows, { total: parseInt(countResult.rows[0].total), page, page_size: pageSize }) } catch (err: any) { sendError(res, err.message) } }) router.get('/stores/risk', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_store_risk_rating ORDER BY risk_level, received DESC`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/stores/priority', async (req: AuthRequest, res) => { try { const result = await query(` SELECT store_code, store_name, action_priority, problem_count, problem_combination, received, scale_tier, business_type FROM analytics.cache_store_priority ORDER BY CASE action_priority WHEN 'P0-修复数据口径' THEN 1 WHEN 'P0-综合专项整改' THEN 2 WHEN 'P1-重点整改' THEN 3 WHEN 'P2-单项改善' THEN 4 WHEN '标杆候选' THEN 5 ELSE 6 END, received DESC `) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/stores/quadrant', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_store_benchmark ORDER BY management_quadrant, avg_daily_received DESC`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/stores/:code', async (req: AuthRequest, res) => { try { const code = req.params.code const scorecard = await query(`SELECT * FROM analytics.v_store_scorecard WHERE store_code = $1`, [code]) const risk = await query(`SELECT * FROM analytics.v_store_risk_rating WHERE store_code = $1`, [code]) const platform = await query(`SELECT * FROM analytics.v_store_platform_economics WHERE store_code = $1`, [code]) const benchmark = await query(`SELECT * FROM analytics.v_store_benchmark WHERE store_code = $1`, [code]) const action = await query(`SELECT * FROM analytics.v_store_action_list WHERE store_code = $1`, [code]) const execution = await query(`SELECT * FROM analytics.v_store_execution_priority WHERE store_code = $1`, [code]) if (scorecard.rows.length === 0) { return sendError(res, 'Store not found', 404) } sendSuccess(res, { scorecard: scorecard.rows[0], risk: risk.rows[0], platform: platform.rows[0], benchmark: benchmark.rows[0], action: action.rows[0], execution: execution.rows[0], }) } catch (err: any) { sendError(res, err.message) } }) router.get('/stores/:code/daily', async (req: AuthRequest, res) => { try { const code = req.params.code const month = parseMonth(req) const result = await query(` SELECT closed_at::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.bill_fact 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 ORDER BY business_date `, [code, month]) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/cost/comparison', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_store_theoretical_actual_cost_april ORDER BY variance_to_theoretical_pct DESC NULLS LAST`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/cost/category-benchmark', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_store_category_cost_benchmark_april ORDER BY store_code`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/cost/inventory', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_store_inventory_efficiency_april ORDER BY estimated_inventory_days DESC NULLS LAST`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/platform/economics', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_store_platform_economics ORDER BY meituan_received DESC NULLS LAST`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/member/comparison', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_member_comparison`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/member/repeat', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_store_repeat_summary_monthly ORDER BY repeat_rate_pct DESC NULLS LAST`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/sku/abc', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_dish_sku_abc_april ORDER BY cumulative_revenue_share`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/sku/category', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.category_summary ORDER BY amount DESC`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/sku/attach', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.dish_pair_summary_april ORDER BY pair_count DESC LIMIT 50`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/risk/anomaly', async (req: AuthRequest, res) => { try { const { page, pageSize, offset } = parsePagination(req) const countResult = await query(`SELECT count(*) AS total FROM analytics.v_anomaly_bills`) const result = await query(`SELECT * FROM analytics.v_anomaly_bills ORDER BY closed_at DESC LIMIT $1 OFFSET $2`, [pageSize, offset]) sendSuccess(res, result.rows, { total: parseInt(countResult.rows[0].total), page, page_size: pageSize }) } catch (err: any) { sendError(res, err.message) } }) router.get('/risk/zero-received', async (req: AuthRequest, res) => { try { const storeCode = req.query.store_code as string let sql = `SELECT * FROM analytics.v_zero_received_detail` const params: any[] = [] if (storeCode) { sql += ` WHERE store_code = $1` params.push(storeCode) } sql += ` ORDER BY closed_at DESC LIMIT 200` const result = await query(sql, params) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/risk/cashier', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_cashier_risk ORDER BY anomaly_rate_pct DESC NULLS LAST`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/marketing/plans', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_marketing_plan_summary ORDER BY received DESC`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/benchmark/composite', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_store_benchmark_composite ORDER BY benchmark_score DESC`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/time/weekday', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_weekday_summary ORDER BY weekday_no`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/time/hourly', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_hourly_summary ORDER BY closing_hour`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/channel', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_channel_daily ORDER BY business_date`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/data-quality', async (req: AuthRequest, res) => { try { const billStats = await query(` SELECT count(*) AS total_bills, count(*) FILTER (WHERE bill_no IS NULL OR bill_no = '') AS missing_bill_no, count(*) FILTER (WHERE store_code IS NULL OR store_code = '') AS missing_store_code, count(*) FILTER (WHERE consumption = 0 OR consumption IS NULL) AS zero_consumption, count(*) FILTER (WHERE received_total < 0) AS negative_received, count(DISTINCT store_code) AS store_count, min(closed_at)::text AS min_date, max(closed_at)::text AS max_date FROM analytics.bill_fact `) const dishStats = await query(` SELECT count(*) AS total_dish_records, count(*) FILTER (WHERE store_code IS NULL OR store_code = '') AS dish_missing_store, count(*) FILTER (WHERE dish_name IS NULL OR dish_name = '') AS dish_missing_dish FROM public.dish_sales_details `) const result = { ...billStats.rows[0], ...dishStats.rows[0] } sendSuccess(res, result) } catch (err: any) { sendError(res, err.message) } }) // ============================================================ // 门店详情页扩展 API (Phase 9) // ============================================================ router.get('/stores/:code/meal-period', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_store_meal_opportunity WHERE store_code = $1 ORDER BY bill_count DESC`, [req.params.code]) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) router.get('/stores/:code/category-mix', async (req: AuthRequest, res) => { try { const result = await query(`SELECT * FROM analytics.v_store_category_mix WHERE store_code = $1`, [req.params.code]) const companyResult = await query(` SELECT SUM(consumption) as total_consumption, SUM(lanzhou_noodle) as total_noodle, SUM(western_staple) as total_western, SUM(delivery_package) as total_delivery, SUM(cold_dishes) as total_cold, SUM(silk_road_food) as total_silk FROM analytics.v_store_category_mix `) sendSuccess(res, { store: result.rows[0], company: companyResult.rows[0] }) } catch (err: any) { sendError(res, err.message) } }) router.get('/stores/:code/cost', async (req: AuthRequest, res) => { try { const costResult = await query(`SELECT * FROM analytics.v_store_theoretical_actual_cost_april WHERE store_code = $1`, [req.params.code]) const catResult = await query(`SELECT * FROM analytics.v_store_category_cost_benchmark_april WHERE store_code = $1`, [req.params.code]) sendSuccess(res, { cost: costResult.rows[0], categories: catResult.rows }) } catch (err: any) { sendError(res, err.message) } }) router.get('/stores/:code/member', async (req: AuthRequest, res) => { try { const oppResult = await query(`SELECT * FROM analytics.v_store_member_opportunity WHERE store_code = $1`, [req.params.code]) const repeatResult = await query(`SELECT * FROM analytics.v_store_repeat_summary_monthly WHERE store_code = $1`, [req.params.code]) const monthlyResult = await query(` SELECT store_code, store_name, count(*) as member_count, sum(orders) as total_orders, sum(received) as total_received, avg(orders) as avg_orders, avg(received) as avg_received FROM analytics.v_store_member_monthly_activity WHERE store_code = $1 GROUP BY store_code, store_name `, [req.params.code]) sendSuccess(res, { opportunity: oppResult.rows[0], repeat: repeatResult.rows[0], monthly: monthlyResult.rows[0] }) } catch (err: any) { sendError(res, err.message) } }) router.get('/stores/:code/anomalies', async (req: AuthRequest, res) => { try { const { page, pageSize, offset } = parsePagination(req) const countResult = await query(`SELECT count(*) AS total FROM analytics.v_anomaly_bills WHERE store_code = $1`, [req.params.code]) const result = await query(`SELECT * FROM analytics.v_anomaly_bills WHERE store_code = $1 ORDER BY closed_at DESC LIMIT $2 OFFSET $3`, [req.params.code, pageSize, offset]) sendSuccess(res, result.rows, { total: parseInt(countResult.rows[0].total), page, page_size: pageSize }) } catch (err: any) { sendError(res, err.message) } }) // 区域汇总 router.get('/region/summary', async (req, res) => { try { const result = await query(`SELECT * FROM analytics.v_region_summary ORDER BY total_received DESC`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) export default router