# 03 · 指标体系与API开发 ## 1. 指标分层设计 ``` L0: 原始数据层 (bill_records, salary_detail_records, attendance_records) ↓ L1: 预聚合层 (物化视图 mv_*) ↓ L2: API层 (后端路由,SQL查询 + 业务逻辑) ↓ L3: 展示层 (前端页面,图表 + 表格) ``` ## 2. 后端API规范 ### 2.1 路由组织 ``` server/src/routes/ ├── data.ts # 核心数据API(概览、门店、风险明细等) ├── situational-awareness.ts # 态势感知(健康度、预警、关联分析) ├── smart-scheduling.ts # 智能排班 ├── cost-analysis.ts # 成本分析 ├── store-expense.ts # 门店费用 ├── analytics-enhanced.ts # 增强分析 ├── admin.ts # 管理 └── tts.ts # 语音 ``` ### 2.2 通用模式 ```typescript // 月份参数解析 const month = parseMonth(req) // 从 query 参数获取,默认 '2026-04' // 分页 const { page, pageSize, offset } = parsePagination(req) // 数据权限 const scope = getDataScope(req) // 返回 store_names 数组或 null(全部) // 响应格式 sendSuccess(res, data, meta?) // { success: true, data, meta } sendError(res, message) // { success: false, error: message } ``` ### 2.3 SQL查询模式 **安全参数化**: ```typescript // 正确:参数化查询 const result = await query(`SELECT * FROM mv_risk_anomaly WHERE month = to_char($1::date, 'YYYY-MM')`, [month]) // 错误:字符串拼接 const result = await query(`SELECT * FROM mv_risk_anomaly WHERE month = '${month}'`) ``` **动态条件构建**: ```typescript const conditions: string[] = [`month = to_char($1::date, 'YYYY-MM')`] const params: any[] = [month] let paramIdx = 2 if (storeName) { conditions.push(`store_name = $${paramIdx}`) params.push(storeName) paramIdx++ } const whereClause = conditions.join(' AND ') ``` **聚合汇总+分页**: ```typescript // 先查总数和汇总 const countResult = await query(`SELECT count(*) AS total, sum(consumption) AS sum_consumption FROM mv_risk_anomaly WHERE ${whereClause}`, params) // 再查明细 const result = await query(`SELECT * FROM mv_risk_anomaly WHERE ${whereClause} ORDER BY ${sortCol} ${order} LIMIT $${paramIdx} OFFSET $${paramIdx + 1}`, [...params, pageSize, offset]) ``` ## 3. 常见SQL陷阱与修复 ### 3.1 Schema前缀错误 **症状**:`relation "analytics.mv_channel_daily" does not exist` **排查**: ```sql SELECT schemaname, matviewname FROM pg_matviews WHERE matviewname LIKE '%channel%'; ``` **修复**:确认实际schema,移除或修正前缀 ```typescript // 错误:FROM analytics.mv_channel_daily // 正确:FROM mv_channel_daily (实际在public schema) ``` ### 3.2 列名不存在 **症状**:`column "risk_score" does not exist` **排查**: ```sql SELECT column_name FROM information_schema.columns WHERE table_name = 'mv_store_risk_rating_monthly'; ``` **修复策略**: - 策略A:改用 `SELECT *`(当不需要特定列名时) - 策略B:用 `0 AS column_name` 替代不存在的列(当前端期望该字段时) - 策略C:修正为正确的列名 ### 3.3 日期格式不匹配 **症状**:薪资相关查询返回0条 **排查**: ```sql SELECT DISTINCT salary_period FROM salary_detail_records ORDER BY 1 DESC LIMIT 5; -- 结果:2026年4月(非2026-04) ``` **修复**: ```typescript // 错误:s.salary_month = to_char($1::date, 'YYYY-MM') // 正确:s.salary_period = to_char($1::date, 'YYYY"年"FMMM"月"') ``` ### 3.4 物化视图stale **症状**:API返回数据与直接查原始表不一致 **排查**:对比物化视图和原始表的count ```sql SELECT count(*) FROM mv_risk_anomaly WHERE month = '2026-04'; SELECT count(*) FROM bill_records WHERE to_char(c176::timestamp, 'YYYY-MM') = '2026-04' AND ...; ``` **修复**:重建物化视图 ```sql DROP MATERIALIZED VIEW mv_risk_anomaly; CREATE MATERIALIZED VIEW mv_risk_anomaly AS ...; CREATE INDEX idx_mv_risk_anomaly_month ON mv_risk_anomaly(month); ``` ## 4. 考勤打卡数据解析 ### 4.1 department路径解析门店名 ``` 北京西部马华餐饮有限公司/西部马华品牌门店/胡庆鹏区/HQP2区/双安店/前厅/服务组 ``` **提取逻辑**(从后往前找以"店"结尾的层级): ```typescript function extractStore(dept: string): string { const parts = dept.split('/') for (let i = parts.length - 1; i >= 0; i--) { if (parts[i].endsWith('店')) return parts[i] } return '' } ``` ### 4.2 打卡时间解析 格式:`08:43(考勤机:指纹)\n21:23(考勤机:指纹)` ```typescript function parseClockTimes(raw: string): { start: number; end: number } | null { const times = raw.match(/(\d{1,2}):(\d{2})/g) if (!times || times.length < 2) return null const startHour = parseInt(times[0].match(/(\d{1,2}):/)[1]) let endHour = parseInt(times[times.length - 1].match(/(\d{1,2}):/)[1]) if (endHour < startHour) endHour = 23 // 跨天 return { start: startHour, end: endHour } } ``` ### 4.3 日均在岗人数估算 ```typescript // 每个员工每天打卡算1人在岗,按小时累计 // 月度汇总后除以30天得到日均同时在岗人数 staffSummary[store][hour] = Math.round(totalPersonHours[store][hour] / 30) ``` **关键点**:客流数据 `mv_bill_hourly.bills` 也是月度汇总,展示时需除以30天对齐。