110 lines
4.9 KiB
Markdown
110 lines
4.9 KiB
Markdown
# 03 · 步骤二:数据现状与本体层构建
|
||
|
||
> 目标:摸清数据家底,建立从原始数据到指标体系的映射——本体层是FDE双向沉淀的核心载体。 `[Echo+Delta]`
|
||
|
||
## 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` |
|