docs: 新增智脑实施方法论文档体系(7篇) + fix: 态势感知关联分析修复

文档:
- 01-项目概述与架构: 技术架构、数据流、部署拓扑
- 02-数据接入与治理: 原始数据导入、物化视图、门店名映射
- 03-指标体系与API开发: 指标分层、SQL模式、常见陷阱
- 04-前端页面开发: 组件规范、页面模板、月份参数管理
- 05-部署与运维: 部署脚本、FRP隧道、PM2、备份
- 06-调试排查手册: 问题分类、8个实际案例、工具速查
- 07-通用方法论: 核心原则、实施阶段、快速复制Checklist

修复:
- situational-awareness.ts: salary_month→salary_period, 日期格式改中文
- 客流-人力匹配: 改用attendance_records打卡数据解析在岗人数
- 客流数据除以30天对齐日均
This commit is contained in:
freedakgmail
2026-08-12 11:00:54 +08:00
parent 2c5b03e41b
commit c2a9e27e49
9 changed files with 1066 additions and 30 deletions
@@ -0,0 +1,186 @@
# 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天对齐。