214 lines
8.1 KiB
Markdown
214 lines
8.1 KiB
Markdown
# 07 · 步骤六:AI协同后端API构建与验证
|
||
|
||
> 目标:AI参与API开发,逐个验证——快速交付能用的代码,不追求完美架构。 `[Delta主导,AI为主]`
|
||
|
||
## 1. 指标分层设计
|
||
|
||
```
|
||
L0: 原始数据层 (bill_records, salary_detail_records, attendance_records)
|
||
↓
|
||
L1: 预聚合层 (物化视图 mv_*)
|
||
↓
|
||
L2: API层 (后端路由,SQL查询 + 业务逻辑)
|
||
↓
|
||
L3: 展示层 (前端页面,图表 + 表格)
|
||
```
|
||
|
||
## 2. 页面与API总览
|
||
|
||
| 页面 | 文件 | 调用的 API |
|
||
|------|------|-----------|
|
||
| 总部驾驶舱 | DashboardPage.tsx | /overview, /overview/daily, /stores/risk, /stores/priority, /tasks/loop-health, /stores/quadrant, /platform/economics, /situational-awareness/alerts, /store-expense/overview, /overview/yoy, /overview/mom, /overview/trend, /overview/profit-trend |
|
||
| 老板驾驶舱 | BossPage.tsx | /overview, /overview/daily, /store-expense/overview, /overview/profit-waterfall, /stores/risk, /stores/priority, /cost-analysis/store-overview, /store-expense/expense-structure, /overview/store-profit-ranking, /overview/profit-opportunity |
|
||
| 门店工作台 | StorePage.tsx | /stores/risk, /stores/priority, /tasks/stores/:code/daily-card, /tasks, /stores/:code/daily, /tasks/followup, /sku/attach, /sku/abc, /stores/:code, /situational-awareness/health-score, /stores/:code/meal-period, /stores/:code/category-mix, /stores/:code/cost, /stores/:code/member, /stores/:code/anomalies, /stores/:code/staffing |
|
||
| 成本分析 | CostPage.tsx | /cost/comparison, /cost/inventory, /cost/category-benchmark |
|
||
| 成本分析(Tab) | cost-analysis/*.tsx | /cost-analysis/overview, /category-comparison, /margin-deviation, /variance-top, /menu-engineering, /profitability, /pricing, /store-overview, /store-ranking, /bom-*, /packaging-*, /material-*, /data-quality, /unmatched-* |
|
||
| 费用分析(Tab) | store-expense/*.tsx | /store-expense/overview, /expense-structure, /store-ranking, /store-contribution, /break-even, /loss-diagnosis, /delivery-commission, /rent-risk, /fixed-variable, /efficiency, /store-evaluation |
|
||
| 风险监控 | RiskPage.tsx | /risk/anomaly, /risk/zero-received, /risk/cashier |
|
||
| 态势感知 | SituationalAwarenessPage.tsx | /situational-awareness/health-score, /alerts, /correlation, /forecast |
|
||
| 会员复购 | MemberPage.tsx | /member/comparison, /member/repeat |
|
||
| SKU分析 | SKUPage.tsx | /sku/abc, /sku/category |
|
||
| 菜单工程 | MenuEngineeringPage.tsx | /analytics-enhanced/menu-engineering/actions |
|
||
| 产品生命周期 | ProductLifecyclePage.tsx | /product/products, /product/reviews/overview, /product/reviews |
|
||
| BOM穿透 | BomPenetrationPage.tsx | /central-kitchen/bom-penetration |
|
||
| 分销对账 | DistributionReconciliationPage.tsx | /distribution/reconciliation |
|
||
| 生产计划 | ProductionPlanPage.tsx | /sales-driven/production-plan |
|
||
| 任务闭环 | TasksPage.tsx | /tasks |
|
||
| 数据导入 | DataImportPage.tsx | /product/import-logs, /product/quality-rules |
|
||
| 本体浏览 | OntologyPage.tsx | /tasks/ontology/* |
|
||
|
||
> 详细的逐页面→API→SQL→数据源字段映射,见 [08-数据溯源与API全量清单.md](08-数据溯源与API全量清单.md)
|
||
|
||
## 3. 后端API规范
|
||
|
||
### 3.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天对齐。
|