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:
@@ -0,0 +1,71 @@
|
||||
# 01 · 项目概述与架构
|
||||
|
||||
## 1. 项目背景
|
||||
|
||||
为连锁餐饮企业(西部马华,100+门店)构建数据分析智脑平台,覆盖:
|
||||
- **老板驾驶舱**:门店风险评级、营收概览、平台经济性
|
||||
- **门店分级**:红/黄/绿三色风险评级与明细
|
||||
- **态势感知**:健康度评分、自动化预警、跨模块关联分析、趋势预测
|
||||
- **风险明细**:异常账单、零实收、收银员风险
|
||||
- **智能排班**:客流热力图、排班匹配度、人效对标、考勤预警
|
||||
- **成本分析**:理论成本vs实际成本、成本方差、菜品级分析
|
||||
- **会员复购**:会员占比、复购率、营销方案效果
|
||||
- **SKU分析**:ABC分类、菜品销售结构
|
||||
|
||||
## 2. 技术架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ 前端 (React + TypeScript + Vite) │
|
||||
│ - Recharts 图表库 │
|
||||
│ - @tanstack/react-query 数据请求 │
|
||||
│ - 自研组件: MetricCard / FilterableTable / │
|
||||
│ CollapsibleSection / MonthPicker │
|
||||
│ - 部署到 Nginx 静态文件 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ 后端 (Express + TypeScript) │
|
||||
│ - 路由按模块拆分: data.ts / situational- │
|
||||
│ awareness.ts / smart-scheduling.ts / │
|
||||
│ cost-analysis.ts / store-expense.ts │
|
||||
│ - pg 库直连 PostgreSQL │
|
||||
│ - JWT 认证 + 数据权限 (getDataScope) │
|
||||
│ - PM2 进程管理 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ 数据库 (PostgreSQL 15) │
|
||||
│ - bill_query: 业务数据 (原始表 + 物化视图) │
|
||||
│ - sbrain_admin: 平台管理 (租户/用户/配置) │
|
||||
│ - FRP 隧道: 本地5432 → 服务器15432 │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 3. 数据流
|
||||
|
||||
```
|
||||
原始Excel/CSV → 导入脚本 → bill_records / salary_detail_records / attendance_records
|
||||
↓
|
||||
物化视图层 (mv_*)
|
||||
┌─────────────┼─────────────┐
|
||||
↓ ↓ ↓
|
||||
mv_bill_hourly mv_risk_* mv_store_*_monthly
|
||||
↓ ↓ ↓
|
||||
后端API (SQL查询)
|
||||
↓
|
||||
前端Dashboard
|
||||
```
|
||||
|
||||
## 4. 部署拓扑
|
||||
|
||||
| 组件 | 位置 | 说明 |
|
||||
|------|------|------|
|
||||
| 前端 | 服务器 Nginx | `/var/www/dm.all8ai.top` 静态文件 |
|
||||
| 后端 | 服务器 PM2 | `sbrain-server` 进程,端口9333 |
|
||||
| 数据库 | 本地 PostgreSQL | 通过FRP隧道暴露到服务器15432 |
|
||||
| 域名 | dm.all8ai.top | Nginx反向代理 `/api/` → 后端 |
|
||||
|
||||
## 5. 关键设计决策
|
||||
|
||||
1. **物化视图而非实时查询**:原始表千万级,物化视图预聚合保证API响应速度
|
||||
2. **FRP隧道而非云数据库**:数据安全要求高,数据库留在本地,通过隧道供服务器访问
|
||||
3. **多租户SaaS架构**:`sbrain_admin` 管理租户配置,`tenant_configs` 表驱动数据源连接
|
||||
4. **数据权限分层**:总部看全部门店、区域经理看本区域、店长看本店
|
||||
5. **默认月份参数**:前端统一使用 `DEFAULT_MONTH` 常量,避免新月份无数据时空白
|
||||
@@ -0,0 +1,107 @@
|
||||
# 02 · 数据接入与治理
|
||||
|
||||
## 1. 原始数据导入
|
||||
|
||||
### 1.1 数据源
|
||||
|
||||
| 表名 | 数据来源 | 关键字段 | 数据量级 |
|
||||
|------|---------|---------|---------|
|
||||
| `bill_records` | 收银系统导出 | c003(门店) c005(账单号) c009(消费) c068(优惠) c114(实收) c175(下单时间) c176(结账时间) c191(收银员) | 千万级/月 |
|
||||
| `salary_detail_records` | 薪资系统导出 | org_level5(门店) salary_period(薪资周期) actual_attend(实际出勤) actual_hours(实际工时) | 万级/月 |
|
||||
| `attendance_records` | 考勤系统导出 | department(部门路径) position(岗位) day_01~day_31(每日打卡) | 万级/月 |
|
||||
|
||||
### 1.2 导入关键点
|
||||
|
||||
- **字段映射**:原始Excel列名(c001~c200)需建立映射文档,明确每列含义
|
||||
- **日期格式**:`salary_period` 格式为中文"2026年4月",非"2026-04",查询时需用 `to_char($1::date, 'YYYY"年"FMMM"月"')`
|
||||
- **空值处理**:实收字段 c114 可能为空字符串,需 `COALESCE(NULLIF(c114,'')::numeric, 0)`
|
||||
- **数据去重**:导入时按 record_id + import_id 去重
|
||||
|
||||
### 1.3 门店名映射
|
||||
|
||||
不同数据源门店名不一致,需维护 `store_name_mapping` 映射表:
|
||||
|
||||
```sql
|
||||
CREATE TABLE public.store_name_mapping (
|
||||
salary_name TEXT, -- 薪资/考勤系统中的名称
|
||||
bill_name TEXT -- 账单系统中的名称
|
||||
);
|
||||
```
|
||||
|
||||
**典型映射**:
|
||||
| salary_name | bill_name |
|
||||
|-------------|-----------|
|
||||
| 双安店 | 双安总店 |
|
||||
| 百子湾店 | 百子湾路店 |
|
||||
| 安宁庄快手店 | 安宁庄快手 |
|
||||
| 海淀大街店 | 海淀大街 |
|
||||
| 哈马尔罕大钟寺店 | 大钟寺店 |
|
||||
| 阿里疆(温泉路店) | 温泉店 |
|
||||
|
||||
**映射查找模式**(代码中复用):
|
||||
```typescript
|
||||
// 构建查找表:同时用原名和映射名
|
||||
const staffLookup: Record<string, any> = {}
|
||||
for (const [store, data] of Object.entries(staffSummary)) {
|
||||
staffLookup[store] = data // 原名
|
||||
const mapped = nameMap[store]
|
||||
if (mapped && mapped !== store) {
|
||||
staffLookup[mapped] = data // 映射名
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 物化视图体系
|
||||
|
||||
### 2.1 核心物化视图
|
||||
|
||||
| 视图名 | Schema | 用途 | 关键字段 |
|
||||
|--------|--------|------|---------|
|
||||
| `mv_store_risk_rating_monthly` | analytics | 门店风险评级 | store_code, store_name, risk_level, received, month_start |
|
||||
| `mv_store_platform_economics_monthly` | analytics | 平台经济性 | store_code, meituan_received, taobao_received, jd_received |
|
||||
| `mv_bill_hourly` | public | 小时客流 | store_name, hour, bills, avg_guests |
|
||||
| `mv_risk_anomaly` | public | 异常账单 | store_name, bill_no, consumption, anomaly_reason, month |
|
||||
| `mv_risk_zero` | public | 零实收 | store_name, bill_no, zero_received_type, month |
|
||||
| `mv_risk_cashier` | public | 收银员风险 | store_name, cashier, bill_count, anomaly_bills, month |
|
||||
| `mv_channel_daily` | public | 支付渠道 | business_date, cash, alipay, wechat, meituan, month |
|
||||
| `mv_time_hourly` | public | 时间维度汇总 | closing_hour, bill_count, received, month |
|
||||
|
||||
### 2.2 物化视图管理要点
|
||||
|
||||
1. **Schema前缀**:部分视图在 `analytics` schema,部分在 `public` schema,代码中需注意
|
||||
2. **刷新机制**:物化视图需手动刷新 `REFRESH MATERIALIZED VIEW`,不自动更新
|
||||
3. **索引**:重建物化视图后需重新创建索引
|
||||
4. **阈值调优**:异常判断阈值需根据业务调整(如"消费-优惠与实收不平"阈值从0.05元提高到1元)
|
||||
|
||||
### 2.3 异常账单阈值设计
|
||||
|
||||
```sql
|
||||
-- 异常类型判断(CASE WHEN顺序重要)
|
||||
CASE
|
||||
WHEN consumption > 0 AND received = 0 THEN '有消费无实收'
|
||||
WHEN discount > consumption THEN '优惠大于消费'
|
||||
WHEN abs(consumption - discount - received) > 1 THEN '消费-优惠与实收不平'
|
||||
ELSE NULL
|
||||
END
|
||||
```
|
||||
|
||||
**关键教训**:阈值0.05元太严格,会将舍入差异标为异常。建议初始阈值设为1元,后续根据数据分布调整。
|
||||
|
||||
## 3. 数据质量校验
|
||||
|
||||
### 3.1 校验流程
|
||||
|
||||
1. **数据范围**:`SELECT DISTINCT month FROM bill_records ORDER BY month` 确认最新月份
|
||||
2. **门店数**:`SELECT count(DISTINCT c003) FROM bill_records WHERE month = '2026-04'`
|
||||
3. **金额一致性**:`SELECT sum(c009), sum(c068), sum(c114)` 对比前后端
|
||||
4. **物化视图刷新状态**:对比物化视图和原始表的记录数
|
||||
|
||||
### 3.2 常见数据问题
|
||||
|
||||
| 问题 | 症状 | 排查方法 |
|
||||
|------|------|---------|
|
||||
| 物化视图stale | API数据与数据库不一致 | 直接查原始表对比物化视图 |
|
||||
| 月份格式不匹配 | 查询返回0条 | 检查 salary_period 实际格式 |
|
||||
| 门店名不一致 | 部分门店无数据 | 对比不同表的门店名列表 |
|
||||
| Schema前缀错误 | `relation does not exist` | 检查 `\dn` 和 `pg_matviews` |
|
||||
| 列不存在 | `column does not exist` | 检查 `information_schema.columns` |
|
||||
@@ -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天对齐。
|
||||
@@ -0,0 +1,135 @@
|
||||
# 04 · 前端页面开发
|
||||
|
||||
## 1. 技术栈
|
||||
|
||||
- React 18 + TypeScript
|
||||
- Vite 构建
|
||||
- @tanstack/react-query 数据请求
|
||||
- Recharts 图表
|
||||
- 自研UI组件库(无第三方UI框架)
|
||||
|
||||
## 2. 组件规范
|
||||
|
||||
### 2.1 核心组件
|
||||
|
||||
| 组件 | 用途 | 关键Props |
|
||||
|------|------|-----------|
|
||||
| `MetricCard` | 指标卡 | `label`, `value`, `format: 'number'\|'percent'\|'currency'` |
|
||||
| `FilterableTable` | 可筛选排序表格 | `columns`, `filterKey`, `sortOptions`, `data` |
|
||||
| `CollapsibleSection` | 可折叠区域 | `title`, `subtitle`, `defaultOpen` |
|
||||
| `MonthPicker` | 月份选择器 | `month`, `onChange` |
|
||||
| `LoadingSpinner` | 加载动画 | `text` |
|
||||
|
||||
### 2.2 页面结构模板
|
||||
|
||||
```tsx
|
||||
export default function XxxPage() {
|
||||
const { month, setMonth } = useMonthParam()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 1. 顶部标题 + MonthPicker */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">页面标题</h1>
|
||||
<MonthPicker month={month} onChange={setMonth} />
|
||||
</div>
|
||||
|
||||
{/* 2. MetricCard 概览行 */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<MetricCard label="指标1" value={data?.metric1} format="currency" />
|
||||
<MetricCard label="指标2" value={data?.metric2} format="number" />
|
||||
</div>
|
||||
|
||||
{/* 3. CollapsibleSection 包裹图表 */}
|
||||
<CollapsibleSection title="图表区" defaultOpen>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={data?.chart}>
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Bar dataKey="value" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* 4. FilterableTable 展示明细 */}
|
||||
<FilterableTable
|
||||
columns={columns}
|
||||
data={data?.list || []}
|
||||
filterKey="store_name"
|
||||
sortOptions={[{ key: 'received', label: '按实收' }]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 月份参数管理
|
||||
|
||||
### 3.1 统一Hook
|
||||
|
||||
```typescript
|
||||
// client/src/lib/useMonthParam.ts
|
||||
export const DEFAULT_MONTH = '2026-04'
|
||||
|
||||
export function useMonthParam() {
|
||||
const [month, setMonth] = useState(DEFAULT_MONTH)
|
||||
// 同步URL参数
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search)
|
||||
const m = params.get('month')
|
||||
if (m) setMonth(m)
|
||||
}, [])
|
||||
return { month, setMonth }
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 关键原则
|
||||
|
||||
- **所有页面统一使用 `useMonthParam`**,不各自 `new Date().toISOString().slice(0,7)`
|
||||
- **默认月份设为有数据的最新月份**,避免空白页面
|
||||
- 修改默认月份只需改 `DEFAULT_MONTH` 一处
|
||||
|
||||
## 4. 数据请求模式
|
||||
|
||||
```typescript
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['xxx-data', month],
|
||||
queryFn: () => api.get('/api/xxx', { params: { month } }),
|
||||
staleTime: 5 * 60 * 1000, // 5分钟缓存
|
||||
})
|
||||
```
|
||||
|
||||
## 5. 数据展示约定
|
||||
|
||||
### 5.1 日期格式
|
||||
|
||||
后端返回ISO格式 `2026-04-29T16:00:00.000Z`,前端必须截取:
|
||||
```typescript
|
||||
{new Date(item.business_date).toISOString().substring(0, 10)}
|
||||
```
|
||||
|
||||
### 5.2 金额格式化
|
||||
|
||||
```typescript
|
||||
formatCurrency(1234567.89) // ¥1,234,567.89
|
||||
formatNumber(12345) // 12,345
|
||||
formatPercent(85.5) // 85.5%
|
||||
```
|
||||
|
||||
### 5.3 空状态处理
|
||||
|
||||
```tsx
|
||||
{isLoading ? <LoadingSpinner text="加载中..." /> :
|
||||
data?.length === 0 ? <div className="text-center py-8 text-gray-400">暂无数据</div> :
|
||||
<FilterableTable data={data} ... />}
|
||||
```
|
||||
|
||||
## 6. 典型参考页面
|
||||
|
||||
| 页面 | 文件 | 特点 |
|
||||
|------|------|------|
|
||||
| 老板驾驶舱 | `BossPage.tsx` | 多Tab + MetricCard + 图表组合 |
|
||||
| 会员复购 | `MemberPage.tsx` | FilterableTable + BarChart + PieChart |
|
||||
| 店长工作台 | `StorePage.tsx` | 多Tab + FilterableTable + 图表 |
|
||||
| SKU分析 | `SKUPage.tsx` | ABC分类 + 可展开详情 |
|
||||
| 态势感知 | `SituationalAwarenessPage.tsx` | 多Tab + 预警卡片 + 关联分析 |
|
||||
@@ -0,0 +1,124 @@
|
||||
# 05 · 部署与运维
|
||||
|
||||
## 1. 部署脚本
|
||||
|
||||
### 1.1 deploy.sh
|
||||
|
||||
```bash
|
||||
# 后端部署
|
||||
bash deploy.sh server
|
||||
# → rsync 同步代码到服务器
|
||||
# → npm install
|
||||
# → pm2 restart sbrain-server
|
||||
|
||||
# 前端部署
|
||||
bash deploy.sh front
|
||||
# → npm run build
|
||||
# → scp dist/* 到服务器 /var/www/dm.all8ai.top
|
||||
```
|
||||
|
||||
### 1.2 服务器环境
|
||||
|
||||
| 配置项 | 值 |
|
||||
|--------|-----|
|
||||
| 服务器IP | 152.136.182.184 |
|
||||
| SSH用户 | ubuntu |
|
||||
| 后端目录 | /opt/sbrain-server |
|
||||
| 前端目录 | /var/www/dm.all8ai.top |
|
||||
| 后端端口 | 9333 |
|
||||
| PM2进程名 | sbrain-server |
|
||||
| Node版本 | 20+ |
|
||||
|
||||
## 2. 数据库连接
|
||||
|
||||
### 2.1 连接配置
|
||||
|
||||
| 用途 | 地址 | 端口 | 数据库 | 用户 | 密码 |
|
||||
|------|------|------|--------|------|------|
|
||||
| 业务数据 | 127.0.0.1 (FRP) | 15432 | bill_query | freedak | (空) |
|
||||
| 平台管理 | 127.0.0.1 | 5432 | sbrain_admin | sbrain_admin | sbrain2026 |
|
||||
|
||||
### 2.2 FRP隧道
|
||||
|
||||
```toml
|
||||
[[proxies]]
|
||||
name = "dm-db"
|
||||
type = "tcp"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 5432
|
||||
remotePort = 15432
|
||||
```
|
||||
|
||||
- macOS launchd 系统级服务,开机自启
|
||||
- 管理命令:
|
||||
- 启动:`launchctl load ~/Library/LaunchAgents/com.frp.client.plist`
|
||||
- 停止:`launchctl bootout gui/$(id -u)/com.frp.client`
|
||||
- 日志:`tail -f ~/Library/Logs/frpc.log`
|
||||
|
||||
### 2.3 远程数据库操作
|
||||
|
||||
```bash
|
||||
# 通过FRP隧道查询
|
||||
ssh ubuntu@152.136.182.184 "PGPASSWORD= psql -h 127.0.0.1 -p 15432 -U freedak -d bill_query -c \"SQL\""
|
||||
|
||||
# 本地直接查询
|
||||
psql -h 127.0.0.1 -p 5432 -U freedak -d bill_query -c "SQL"
|
||||
```
|
||||
|
||||
## 3. 数据库备份
|
||||
|
||||
```bash
|
||||
# 本地备份
|
||||
pg_dump -h 127.0.0.1 -p 5432 -U freedak bill_query > backups/bill_query_$(date +%Y%m%d).sql
|
||||
|
||||
# 恢复
|
||||
psql -h 127.0.0.1 -p 5432 -U freedak -d bill_query < backups/bill_query_YYYYMMDD.sql
|
||||
```
|
||||
|
||||
## 4. PM2 进程管理
|
||||
|
||||
```bash
|
||||
# 查看状态
|
||||
ssh ubuntu@152.136.182.184 "pm2 status"
|
||||
|
||||
# 查看日志
|
||||
ssh ubuntu@152.136.182.184 "pm2 logs sbrain-server --lines 50"
|
||||
|
||||
# 重启
|
||||
ssh ubuntu@152.136.182.184 "pm2 restart sbrain-server"
|
||||
|
||||
# 保存进程列表
|
||||
ssh ubuntu@152.136.182.184 "pm2 save"
|
||||
```
|
||||
|
||||
## 5. 部署前检查清单
|
||||
|
||||
- [ ] 数据库已备份
|
||||
- [ ] 代码已 commit + push
|
||||
- [ ] FRP隧道正常运行(`pgrep -l frpc`)
|
||||
- [ ] 后端本地可正常启动
|
||||
- [ ] 前端本地可正常构建
|
||||
- [ ] 部署后API健康检查(`curl -s https://dm.all8ai.top/api/health`)
|
||||
|
||||
## 6. 常用运维命令
|
||||
|
||||
```bash
|
||||
# API健康检查
|
||||
curl -s https://dm.all8ai.top/api/health
|
||||
|
||||
# 登录获取Token
|
||||
TOKEN=$(curl -s 'https://dm.all8ai.top/api/auth/login' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"总部管理员","password":"123"}' \
|
||||
| python3 -c "import sys,json;print(json.load(sys.stdin)['data']['token'])")
|
||||
|
||||
# 测试API
|
||||
curl -s "https://dm.all8ai.top/api/xxx?month=2026-04" \
|
||||
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
|
||||
|
||||
# 查看物化视图列表
|
||||
ssh ubuntu@152.136.182.184 "PGPASSWORD= psql -h 127.0.0.1 -p 15432 -U freedak -d bill_query -c \"SELECT schemaname, matviewname FROM pg_matviews ORDER BY 1,2\""
|
||||
|
||||
# 查看表结构
|
||||
ssh ubuntu@152.136.182.184 "PGPASSWORD= psql -h 127.0.0.1 -p 15432 -U freedak -d bill_query -c \"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'xxx' ORDER BY ordinal_position\""
|
||||
```
|
||||
@@ -0,0 +1,132 @@
|
||||
# 06 · 调试排查手册
|
||||
|
||||
## 1. 问题分类与排查流程
|
||||
|
||||
### 1.1 页面空白 / 无数据
|
||||
|
||||
```
|
||||
页面空白
|
||||
↓
|
||||
检查API返回 → curl测试
|
||||
↓
|
||||
API报错? → 查看error字段
|
||||
↓
|
||||
SQL报错? → 直查数据库验证
|
||||
↓
|
||||
数据不存在? → 检查月份/物化视图刷新状态
|
||||
```
|
||||
|
||||
### 1.2 API报错排查
|
||||
|
||||
| 错误信息 | 根因 | 修复方法 |
|
||||
|---------|------|---------|
|
||||
| `relation "xxx" does not exist` | Schema前缀错误 | 查 `pg_matviews` 确认实际schema |
|
||||
| `column "xxx" does not exist` | 列名不匹配 | 查 `information_schema.columns` |
|
||||
| `Route not found` | 路由未注册或文件路径错 | 检查 `index.ts` 中 `app.use` |
|
||||
| 查询返回0条 | 日期格式不匹配 | 检查实际数据格式(如中文日期) |
|
||||
| 数据量异常增大 | 物化视图stale | 重建物化视图 |
|
||||
|
||||
### 1.3 数据不一致排查
|
||||
|
||||
```
|
||||
前端显示 vs 数据库实际
|
||||
↓
|
||||
1. curl API 看返回值
|
||||
↓
|
||||
2. 直接 psql 查同一条件
|
||||
↓
|
||||
3. 不一致?→ 检查物化视图是否stale
|
||||
↓
|
||||
4. 一致但数值异常?→ 检查SQL逻辑(如月度汇总vs日均)
|
||||
```
|
||||
|
||||
## 2. 实际案例与修复记录
|
||||
|
||||
### 2.1 Dashboard空白(默认月份问题)
|
||||
|
||||
**症状**:部署后Dashboard显示"0家"
|
||||
**根因**:前端用 `new Date().toISOString().slice(0,7)` 获取当前月份(8月),但数据只到4月
|
||||
**修复**:统一使用 `DEFAULT_MONTH = '2026-04'`
|
||||
|
||||
### 2.2 /stores/risk 报错(列名不存在)
|
||||
|
||||
**症状**:`column "risk_score" does not exist`
|
||||
**根因**:SQL显式列名包含物化视图中不存在的列
|
||||
**修复**:改为 `SELECT *` 或用 `0 AS column_name` 替代
|
||||
|
||||
### 2.3 /channel 报错(Schema前缀)
|
||||
|
||||
**症状**:`relation "analytics.mv_channel_daily" does not exist`
|
||||
**根因**:`mv_channel_daily` 在 `public` schema,代码写了 `analytics.`
|
||||
**修复**:移除schema前缀
|
||||
|
||||
### 2.4 /situational-awareness/correlation 报错(列名错误)
|
||||
|
||||
**症状**:`column s.salary_month does not exist`
|
||||
**根因**:实际列名是 `salary_period`,代码写了 `salary_month`
|
||||
**修复**:改为 `s.salary_period`
|
||||
|
||||
### 2.5 考勤数据返回0条(日期格式)
|
||||
|
||||
**症状**:hr_revenue 为空
|
||||
**根因**:`salary_period` 格式是"2026年4月",代码用 `to_char($1, 'YYYY-MM')` 生成"2026-04"
|
||||
**修复**:改为 `to_char($1::date, 'YYYY"年"FMMM"月"')`
|
||||
|
||||
### 2.6 异常账单数据量暴增(物化视图stale)
|
||||
|
||||
**症状**:异常账单从3万变3.5万,实收从225万变461万
|
||||
**根因**:旧物化视图数据stale,重建后刷新到最新
|
||||
**附加修复**:异常阈值从0.05元提高到1元,过滤舍入差异
|
||||
|
||||
### 2.7 客流-人力匹配在岗人数不合理
|
||||
|
||||
**症状**:双安总店74人全天在岗不变
|
||||
**根因**:用月度总员工数作为每小时在岗人数
|
||||
**修复**:改用 `attendance_records` 打卡记录解析每小时实际在岗人数
|
||||
|
||||
### 2.8 客流-人力匹配人均产出过高
|
||||
|
||||
**症状**:人均产出300+单/小时
|
||||
**根因**:客流是月度汇总(3万+),在岗人数是日均(30人)
|
||||
**修复**:客流也除以30天对齐日均
|
||||
|
||||
## 3. 调试工具速查
|
||||
|
||||
```bash
|
||||
# 1. 获取Token
|
||||
TOKEN=$(curl -s 'https://dm.all8ai.top/api/auth/login' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"总部管理员","password":"123"}' \
|
||||
| python3 -c "import sys,json;print(json.load(sys.stdin)['data']['token'])")
|
||||
|
||||
# 2. 测试单个API
|
||||
curl -s "https://dm.all8ai.top/api/xxx?month=2026-04" \
|
||||
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
|
||||
|
||||
# 3. 批量测试API
|
||||
for api in "/api/a" "/api/b" "/api/c"; do
|
||||
echo -n "$api => "
|
||||
curl -s "https://dm.all8ai.top${api}?month=2026-04" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
| python3 -c "import sys,json;d=json.load(sys.stdin);print('OK' if d['success'] else 'ERROR: '+d.get('error','?'))"
|
||||
done
|
||||
|
||||
# 4. 直查数据库
|
||||
ssh ubuntu@152.136.182.184 "PGPASSWORD= psql -h 127.0.0.1 -p 15432 -U freedak -d bill_query -c \"SQL\""
|
||||
|
||||
# 5. 查看后端日志
|
||||
ssh ubuntu@152.136.182.184 "pm2 logs sbrain-server --lines 100"
|
||||
|
||||
# 6. 检查物化视图
|
||||
ssh ubuntu@152.136.182.184 "PGPASSWORD= psql -h 127.0.0.1 -p 15432 -U freedak -d bill_query -c \"SELECT schemaname, matviewname FROM pg_matviews ORDER BY 1,2\""
|
||||
|
||||
# 7. 检查表结构
|
||||
ssh ubuntu@152.136.182.184 "PGPASSWORD= psql -h 127.0.0.1 -p 15432 -U freedak -d bill_query -c \"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'xxx' ORDER BY ordinal_position\""
|
||||
```
|
||||
|
||||
## 4. 修复优先级
|
||||
|
||||
1. **P0 - 页面完全不可用**:API报错、路由不存在 → 立即修复
|
||||
2. **P1 - 数据不正确**:数值异常、数据不一致 → 验证后修复
|
||||
3. **P2 - 体验问题**:空状态、加载慢 → 优化处理
|
||||
4. **P3 - 增强功能**:新指标、新图表 → 按需开发
|
||||
@@ -0,0 +1,161 @@
|
||||
# 07 · 通用方法论
|
||||
|
||||
> 从智脑项目实践中提炼的核心原则和可复制方法论,适用于类似的连锁企业数据产品建设。
|
||||
|
||||
## 1. 核心原则
|
||||
|
||||
### 1.1 数据先行
|
||||
|
||||
- **先摸清数据再写代码**:在开发任何API前,先用psql直查数据库,了解表结构、数据范围、数据质量
|
||||
- **物化视图是核心**:千万级原始表不要直接查,必须通过物化视图预聚合
|
||||
- **验证闭环**:每个API开发完成后,必须用curl + psql双向验证数据一致性
|
||||
|
||||
### 1.2 渐进式修复
|
||||
|
||||
- **先跑通再优化**:遇到列名不存在,先用 `SELECT *` 跑通,再逐步修正
|
||||
- **最小改动原则**:只改出问题的那行,不重构无关代码
|
||||
- **保留容错**:不确定的查询用 try-catch 包裹,返回空数组而非报错
|
||||
|
||||
### 1.3 统一约定
|
||||
|
||||
- **默认参数集中管理**:`DEFAULT_MONTH` 一处定义,全局引用
|
||||
- **日期格式统一**:后端用 `to_char` 适配各种格式,前端用 `substring(0,10)` 处理ISO日期
|
||||
- **门店名映射集中管理**:维护 `store_name_mapping` 表,代码中统一查找模式
|
||||
|
||||
## 2. 项目实施阶段
|
||||
|
||||
### 阶段一:数据接入(1-2周)
|
||||
|
||||
```
|
||||
1. 接收原始数据文件(Excel/CSV)
|
||||
2. 设计导入脚本,写入原始表
|
||||
3. 建立字段映射文档(c001~c200 → 业务含义)
|
||||
4. 创建物化视图(预聚合)
|
||||
5. 数据质量校验(范围、完整性、一致性)
|
||||
6. 建立门店名映射表
|
||||
```
|
||||
|
||||
**关键交付物**:
|
||||
- 原始表 + 物化视图
|
||||
- 字段映射文档
|
||||
- 门店名映射表
|
||||
- 数据质量报告
|
||||
|
||||
### 阶段二:后端API(2-3周)
|
||||
|
||||
```
|
||||
1. 设计API路由结构(按业务模块拆分)
|
||||
2. 实现核心查询(SELECT * 先跑通)
|
||||
3. 添加分页、排序、筛选
|
||||
4. 添加数据权限(getDataScope)
|
||||
5. curl + psql 双向验证
|
||||
6. 修复SQL陷阱(schema前缀、列名、日期格式)
|
||||
```
|
||||
|
||||
**关键检查点**:
|
||||
- [ ] 每个API的 success=true
|
||||
- [ ] 返回数据条数与直查数据库一致
|
||||
- [ ] 聚合金额与数据库一致
|
||||
- [ ] 无数据时返回空数组而非报错
|
||||
|
||||
### 阶段三:前端页面(2-3周)
|
||||
|
||||
```
|
||||
1. 确定页面结构(参考典型页面模板)
|
||||
2. 实现 MetricCard 概览行
|
||||
3. 实现图表区域(Recharts)
|
||||
4. 实现 FilterableTable 明细
|
||||
5. 联调API
|
||||
6. 处理空状态和加载状态
|
||||
```
|
||||
|
||||
**关键检查点**:
|
||||
- [ ] 月份选择器工作正常
|
||||
- [ ] 图表数据与API返回一致
|
||||
- [ ] 表格筛选、排序正常
|
||||
- [ ] 空状态显示"暂无数据"
|
||||
|
||||
### 阶段四:部署上线(1天)
|
||||
|
||||
```
|
||||
1. 数据库备份
|
||||
2. Git commit + push
|
||||
3. 执行 deploy.sh
|
||||
4. 验证线上API
|
||||
5. 验证线上页面
|
||||
6. 修复线上特有问题
|
||||
```
|
||||
|
||||
## 3. 风险清单
|
||||
|
||||
| 风险 | 影响 | 预防措施 |
|
||||
|------|------|---------|
|
||||
| 物化视图stale | 数据不一致 | 定期刷新,重建后验证 |
|
||||
| 门店名不一致 | 部分门店无数据 | 维护映射表,代码中统一查找 |
|
||||
| 日期格式差异 | 查询返回0条 | 先查实际格式再写SQL |
|
||||
| Schema前缀混用 | API报错 | 统一查pg_matviews确认 |
|
||||
| 默认月份超出数据范围 | 页面空白 | DEFAULT_MONTH设为最新有数据月份 |
|
||||
| FRP隧道断开 | 线上API不可用 | launchd自启 + 监控 |
|
||||
| 月度vs日均未对齐 | 数值异常大 | 统一时间维度(都除以30天) |
|
||||
|
||||
## 4. 快速复制Checklist
|
||||
|
||||
### 4.1 新项目启动
|
||||
|
||||
- [ ] 确认数据源(Excel/CSV/API)
|
||||
- [ ] 确认数据库环境(本地/云)
|
||||
- [ ] 确认部署环境(服务器/域名)
|
||||
- [ ] 确认用户角色和权限
|
||||
|
||||
### 4.2 数据层
|
||||
|
||||
- [ ] 原始表创建 + 导入脚本
|
||||
- [ ] 字段映射文档
|
||||
- [ ] 物化视图创建
|
||||
- [ ] 门店名映射表
|
||||
- [ ] 数据质量校验报告
|
||||
|
||||
### 4.3 API层
|
||||
|
||||
- [ ] 路由文件按模块拆分
|
||||
- [ ] 通用工具函数(parseMonth, parsePagination, getDataScope)
|
||||
- [ ] 每个API curl验证通过
|
||||
- [ ] 错误处理(sendError返回明确信息)
|
||||
|
||||
### 4.4 前端层
|
||||
|
||||
- [ ] useMonthParam hook
|
||||
- [ ] 典型页面模板
|
||||
- [ ] 组件库(MetricCard, FilterableTable等)
|
||||
- [ ] 空状态处理
|
||||
|
||||
### 4.5 部署
|
||||
|
||||
- [ ] deploy.sh 脚本
|
||||
- [ ] FRP隧道配置(如需)
|
||||
- [ ] PM2进程配置
|
||||
- [ ] Nginx配置
|
||||
- [ ] 数据库备份脚本
|
||||
|
||||
## 5. 技术选型建议
|
||||
|
||||
| 层面 | 推荐 | 理由 |
|
||||
|------|------|------|
|
||||
| 数据库 | PostgreSQL | 物化视图、窗口函数、丰富的数据类型 |
|
||||
| 后端 | Express + TypeScript | 轻量、灵活、类型安全 |
|
||||
| 前端 | React + Vite | 生态成熟、构建快 |
|
||||
| 图表 | Recharts | React原生、API简洁 |
|
||||
| 数据请求 | @tanstack/react-query | 缓存、重试、loading状态 |
|
||||
| 进程管理 | PM2 | 自动重启、日志管理 |
|
||||
| 隧道 | FRP | 穿透内网、稳定可靠 |
|
||||
|
||||
## 6. 避坑指南
|
||||
|
||||
1. **不要硬编码列名**:用 `SELECT *` 或先查 `information_schema.columns` 确认
|
||||
2. **不要假设日期格式**:先 `SELECT DISTINCT` 看实际值
|
||||
3. **不要假设schema**:先查 `pg_matviews` 确认
|
||||
4. **不要用当前月份做默认值**:用有数据的最新月份
|
||||
5. **不要忽略物化视图刷新**:数据更新后必须 `REFRESH MATERIALIZED VIEW`
|
||||
6. **不要在代码中拼接SQL**:始终用参数化查询
|
||||
7. **不要引入新UI库**:复用现有组件,保持一致性
|
||||
8. **不要跳过数据库备份**:部署前必须备份
|
||||
@@ -0,0 +1,22 @@
|
||||
# 玄谋智脑 · 实施方法论与执行手册
|
||||
|
||||
> 本文档体系总结"连锁餐饮企业智脑"项目的全部工作步骤、关键决策点和通用方法论,目标是形成可复制的执行手册,对类似项目快速落地。
|
||||
|
||||
## 文档结构
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| [01-项目概述与架构.md](01-项目概述与架构.md) | 项目背景、技术架构、数据流、部署拓扑 |
|
||||
| [02-数据接入与治理.md](02-数据接入与治理.md) | 原始数据导入、物化视图体系、数据质量校验、映射表管理 |
|
||||
| [03-指标体系与API开发.md](03-指标体系与API开发.md) | 指标分层设计、后端API规范、常见SQL陷阱与修复模式 |
|
||||
| [04-前端页面开发.md](04-前端页面开发.md) | 页面模板、组件规范、月份参数管理、数据展示约定 |
|
||||
| [05-部署与运维.md](05-部署与运维.md) | 部署脚本、数据库备份、FRP隧道、PM2进程管理 |
|
||||
| [06-调试排查手册.md](06-调试排查手册.md) | 常见问题分类、排查流程、修复模式速查 |
|
||||
| [07-通用方法论.md](07-通用方法论.md) | 可复制的核心原则、风险清单、快速复制Checklist |
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 连锁餐饮/零售企业的数据分析平台建设
|
||||
- 基于PostgreSQL物化视图的BI系统
|
||||
- Express + React + Recharts 技术栈的Dashboard项目
|
||||
- 多租户SaaS架构的垂直行业数据产品
|
||||
Reference in New Issue
Block a user