在前后端开发联调前 的提交20260223

This commit is contained in:
Neo
2026-02-23 23:02:20 +08:00
parent 254ccb1e77
commit fafc95e64c
1142 changed files with 10366960 additions and 36957 deletions

View File

@@ -13,7 +13,7 @@
3. 前后端通过 JSON API 通信,实时日志通过 WebSocket 推送
4. 数据库查询限制为只读,防止误操作
5. **多门店隔离**:通过 `site_id` 贯穿全链路Operator 登录后绑定门店,所有 API 请求自动携带 site_id
6. **执行流程Flow分离**:完整保留现有 7 种 Flow 和 3 种处理模式,前端按 Flow 动态展示可选层和任务
6. **执行流程Flow分离**:完整保留现有 7 种 Flow 和 4 种处理模式,前端按 Flow 动态展示可选层和任务
## 架构
@@ -94,10 +94,11 @@ Web 管理后台的隔离策略:
| `dwd_dws_index` | DWD → DWS汇总 → DWS指数 | DWS, INDEX |
| `dwd_index` | DWD → DWS指数 | INDEX |
3 种处理模式:
4 种处理模式:
- `increment_only`:仅增量处理
- `verify_only`:校验并修复(可选"校验前从 API 获取"
- `increment_verify`:增量 + 校验并修复
- `full_window`:用 API 返回数据的实际时间范围处理全部层,无需校验
时间窗口模式:
- `lookback`:回溯 + 冗余lookback_hours + overlap_seconds
@@ -198,7 +199,7 @@ apps/admin-web/
| GET | `/api/tasks/registry` | 获取任务注册表(按业务域分组) | 2 |
| GET | `/api/tasks/dwd-tables` | 获取 DWD 表定义(按业务域分组) | 2 |
| POST | `/api/tasks/validate` | 验证 TaskConfig | 2, 11 |
| GET | `/api/tasks/flows` | 获取执行流程列表7 种 Flow + 3 种处理模式) | 2 |
| GET | `/api/tasks/flows` | 获取执行流程列表7 种 Flow + 4 种处理模式) | 2 |
| POST | `/api/execution/run` | 提交任务执行 | 3 |
| GET | `/api/execution/queue` | 获取当前队列 | 4 |
| POST | `/api/execution/queue` | 添加任务到队列 | 4 |

View File

@@ -45,13 +45,13 @@
- [x] 3.1 迁移 CLIBuilder 到后端(`apps/backend/app/services/cli_builder.py`
-`gui/utils/cli_builder.py` 迁移核心逻辑
- 适配新的 TaskConfigSchema自动注入 `--store-id` 参数
- 支持 7 种 Flow 和 3 种处理模式
- 支持 7 种 Flow 和 4 种处理模式
- _Requirements: 2.6_
- [x] 3.2 实现任务注册表 API`apps/backend/app/routers/tasks.py`
- `GET /api/tasks/registry`:返回按业务域分组的任务列表
- `GET /api/tasks/dwd-tables`:返回按业务域分组的 DWD 表定义
- `GET /api/tasks/flows`:返回 7 种 Flow 定义和 3 种处理模式定义
- `GET /api/tasks/flows`:返回 7 种 Flow 定义和 4 种处理模式定义
- `POST /api/tasks/validate`:验证 TaskConfig 并返回生成的 CLI 命令预览
- Pydantic schemas 在 `apps/backend/app/schemas/tasks.py`
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 11.1, 11.2_

View File

@@ -0,0 +1 @@
{"generationMode": "requirements-first"}

View File

@@ -0,0 +1,224 @@
# 设计文档助教废除Abolish全链路清理
## 概述
本设计描述如何安全地从 ETL 全链路中移除"助教废除"独立数据链路。
核心思路:**删除独立的废除链路API → ODS → DWD保留服务记录中已有的 `is_trash` 字段作为唯一废除判断源。**
清理范围覆盖:
- ETL 任务定义ODS 任务 + 注册表)
- DWD 加载映射FACT_MAPPINGS + TABLE_MAP
- DWS 聚合逻辑(死代码移除)
- DWD 验证器配置
- 数据库 DDL 和迁移脚本
- 属性测试
- 运维脚本
## 架构
### 清理前数据流
```mermaid
graph LR
API_Abolish["/AssistantPerformance/GetAbolitionAssistant"] --> ODS_ACR["ods.assistant_cancellation_records"]
ODS_ACR --> DWD_ATE["dwd.dwd_assistant_trash_event"]
ODS_ACR --> DWD_ATE_EX["dwd.dwd_assistant_trash_event_ex"]
DWD_ATE --> |"_extract_trash_records (死代码)"| DWS_DAILY["dws.dws_assistant_daily_detail"]
API_Service["/AssistantPerformance/GetAssistantServiceRecords"] --> ODS_ASR["ods.assistant_service_records"]
ODS_ASR --> DWD_SL_EX["dwd.dwd_assistant_service_log_ex"]
DWD_SL_EX --> |"is_trash 字段 (实际使用)"| DWS_DAILY
style API_Abolish fill:#f99,stroke:#c00
style ODS_ACR fill:#f99,stroke:#c00
style DWD_ATE fill:#f99,stroke:#c00
style DWD_ATE_EX fill:#f99,stroke:#c00
```
### 清理后数据流
```mermaid
graph LR
API_Service["/AssistantPerformance/GetAssistantServiceRecords"] --> ODS_ASR["ods.assistant_service_records"]
ODS_ASR --> DWD_SL_EX["dwd.dwd_assistant_service_log_ex"]
DWD_SL_EX --> |"is_trash 字段"| DWS_DAILY["dws.dws_assistant_daily_detail"]
style API_Service fill:#9f9,stroke:#090
style DWD_SL_EX fill:#9f9,stroke:#090
```
## 组件与接口
### 需要修改的文件清单
| 层级 | 文件 | 操作 | 需求 |
|------|------|------|------|
| ODS 任务 | `tasks/ods/ods_tasks.py` | 删除 `OdsTaskSpec` 条目 + 从默认序列移除 | 1.2, 1.3 |
| 任务注册 | `orchestration/task_registry.py` | 删除 `ODS_ASSISTANT_ABOLISH` 注册 | 1.1 |
| DWD 加载 | `tasks/dwd/dwd_load_task.py` | 删除 FACT_MAPPINGS 和 TABLE_MAP 条目 | 2.12.4 |
| DWS 日度 | `tasks/dws/assistant_daily_task.py` | 删除 `_extract_trash_records``_build_trash_index`,简化 `_aggregate_by_assistant_date` 签名 | 3.13.4 |
| DWD 验证 | `tasks/verification/dwd_verifier.py` | 删除废除表的 ID 和时间字段映射 | 4.14.4 |
| DDL | `db/etl_feiqiu/schemas/dwd.sql` | 删除 `dwd_assistant_trash_event` / `_ex` 的 CREATE TABLE + COMMENT | 6.16.2 |
| DDL | `db/etl_feiqiu/schemas/ods.sql` | 删除 `assistant_cancellation_records` 的 CREATE TABLE + COMMENT | 6.3 |
| DDL | `db/etl_feiqiu/schemas/schema_dwd_doc.sql` | 删除废除表的 CREATE TABLE + COMMENT | 6.4 |
| DDL | `db/etl_feiqiu/schemas/schema_ODS_doc.sql` | 删除废除表的 CREATE TABLE + COMMENT | 6.5 |
| DDL | `db/etl_feiqiu/schemas/dws.sql` | 更新 `dws_assistant_daily_detail` 注释 | 6.6 |
| DDL | `db/etl_feiqiu/schemas/schema_dws.sql` | 更新 `dws_assistant_daily_detail` 注释 | 6.7 |
| 迁移 | `db/etl_feiqiu/migrations/` | 新建 DROP TABLE 迁移脚本 | 5.15.5 |
| 属性测试 | `tests/test_property_1_fact_mappings.py` | 删除 `_REQ3_EXPECTED` 和相关引用 | 7.17.3 |
| 运维 | `scripts/ops/dataflow_analyzer.py` | 删除 `ODS_ASSISTANT_ABOLISH` spec 条目 | 8.1 |
| 运维 | `scripts/ops/gen_full_dataflow_doc.py` | 删除 `ODS_ASSISTANT_ABOLISH` spec 条目 | 8.1 |
| 运维 | `scripts/ops/etl_consistency_check.py` | 删除废除相关映射 | 8.18.2 |
| 运维 | `scripts/ops/blackbox_test_report.py` | 删除废除相关映射 | 8.18.4 |
| 运维 | `scripts/ops/field_audit.py` | 删除废除表审计条目 | 8.38.4 |
| 运维 | `scripts/ops/gen_field_review_doc.py` | 删除废除表字段定义 | 8.38.4 |
| 运维 | `scripts/ops/gen_api_field_mapping.py` | 从 ODS_TABLES 列表移除 | 8.3 |
| 运维 | `scripts/ops/export_dwd_field_review.py` | 删除废除表条目 | 8.4 |
| 运维 | `scripts/ops/check_ods_latest_indexes.py` | 删除废除表索引检查 | 8.3 |
### 不需要修改的文件(确认安全)
| 文件 | 原因 |
|------|------|
| `dwd_assistant_service_log_ex` 表 DDL | 保留 `is_trash` 等字段(需求 9 |
| `ods.assistant_service_records` 表 DDL | 保留 `is_trash` 等字段(需求 9 |
| `assistant_monthly_task.py` | 仅消费 `dws_assistant_daily_detail``trashed_seconds`/`trashed_count`,不直接引用废除表 |
| `assistant_salary_task.py` | 仅消费 `dws_assistant_monthly_summary`,不直接引用废除表 |
## 数据模型
### 被删除的表
```sql
-- ODS 层
ods.assistant_cancellation_records -- 78 条记录
-- DWD 层
dwd.dwd_assistant_trash_event -- 主表
dwd.dwd_assistant_trash_event_ex -- 扩展表
```
### 保留的废除相关字段
```sql
-- ods.assistant_service_records 中(保留)
is_trash INT -- 废除标记
trash_reason TEXT -- 废除原因
trash_applicant_id BIGINT -- 废除申请人 ID
trash_applicant_name TEXT -- 废除申请人姓名
-- dwd.dwd_assistant_service_log_ex 中(保留)
is_trash INTEGER -- 废除标记
trash_applicant_id BIGINT -- 废除申请人 ID
trash_applicant_name VARCHAR(64) -- 废除申请人姓名
trash_reason VARCHAR(255) -- 废除原因
```
### DWS 层字段(保留,数据来源变更说明)
```sql
-- dws.dws_assistant_daily_detail保留注释需更新
trashed_seconds INTEGER -- 数据来源dwd_assistant_service_log_ex.is_trash + income_seconds
trashed_count INTEGER -- 数据来源dwd_assistant_service_log_ex.is_trash 计数
-- dws.dws_assistant_monthly_summary保留
trashed_hours NUMERIC(10,2) -- 来自 daily_detail.trashed_seconds 汇总
```
## DWS 代码重构细节
### assistant_daily_task.py 变更
**删除方法:**
- `_extract_trash_records()` — 查询 `dwd.dwd_assistant_trash_event` 的 SQL已无消费者
- `_build_trash_index()` — 构建废除索引,已不参与判断逻辑
**修改方法:**
- `extract()` — 移除对 `_extract_trash_records` 的调用,移除 `trash_records` 变量
- `transform()` 或调用 `_aggregate_by_assistant_date` 的地方 — 移除 `trash_index` 参数传递
- `_aggregate_by_assistant_date()` — 从签名中移除 `trash_index` 参数;`is_trash` 判断逻辑保持不变
**不变逻辑:**
```python
# 这段逻辑保持不变——通过 is_trash 字段判断废除
is_trashed = bool(record.get('is_trash', 0))
if is_trashed:
agg['trashed_seconds'] += income_seconds
agg['trashed_count'] += 1
```
## 正确性属性
*正确性属性是一种在系统所有有效执行中都应成立的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
### Property 1废除聚合逻辑正确性is_trash 驱动)
*对任意*服务记录集合,其中每条记录包含 `is_trash` 标记和 `income_seconds` 值,聚合后:
- `trashed_seconds` 应等于所有 `is_trash=1` 记录的 `income_seconds` 之和
- `trashed_count` 应等于所有 `is_trash=1` 记录的数量
- `total_service_count` 应等于所有 `is_trash=0` 记录的数量
- `total_seconds` 应等于所有 `is_trash=0` 记录的 `income_seconds` 之和
**Validates: Requirements 3.4, 9.3**
### Property 2FACT_MAPPINGS 一致性(已有属性测试的回归验证)
*对任意* FACT_MAPPINGS 中的表名,该表名应在 TABLE_MAP 中有对应的 ODS 源表映射,且映射的每个 DWD 列名应为合法的 SQL 标识符。
**Validates: Requirements 2.12.4, 7.3**
> 注:此属性已由 `tests/test_property_1_fact_mappings.py` 实现。清理后需确保该测试仍然通过。
## 错误处理
### 迁移脚本安全性
- 所有 `DROP TABLE` 语句使用 `IF EXISTS`,确保幂等执行
- 迁移脚本在单个事务中执行,失败时自动回滚
- 迁移脚本包含注释说明移除原因,便于审计追溯
### 代码删除安全性
- 删除 `_extract_trash_records``_build_trash_index` 前,确认无其他调用者
- `_aggregate_by_assistant_date` 移除 `trash_index` 参数后,确认所有调用点已同步更新
- 保留 `is_trash` 判断逻辑不变,确保废除统计功能不受影响
### 回滚策略
- DDL 变更通过迁移脚本管理可通过反向迁移CREATE TABLE回滚
- 代码变更通过 Git 版本控制回滚
- ODS 表数据在删除前可选择性备份(数据量小,仅 78 条)
## 测试策略
### 属性测试
- 使用 `hypothesis` 库进行属性测试
- 每个属性测试至少运行 100 次迭代
- 每个测试用注释标注对应的设计属性编号
**Property 1 测试方案:**
- 生成随机服务记录列表(包含随机 `is_trash` 标记和 `income_seconds` 值)
- 调用 `_aggregate_by_assistant_date` 方法
- 验证 `trashed_seconds`/`trashed_count` 与手动计算的期望值一致
- 标签:`Feature: assistant-abolish-cleanup, Property 1: 废除聚合逻辑正确性`
**Property 2 测试方案:**
- 已由 `tests/test_property_1_fact_mappings.py` 覆盖
- 清理后运行 `pytest tests/ -v` 确认无回归
- 标签:`Feature: assistant-abolish-cleanup, Property 2: FACT_MAPPINGS 一致性`
### 单元测试
- 验证 `AssistantDailyTask` 不再有 `_extract_trash_records``_build_trash_index` 方法
- 验证 `_aggregate_by_assistant_date` 签名不包含 `trash_index` 参数
- 验证 FACT_MAPPINGS 不包含废除表条目
- 验证 TABLE_MAP 不包含废除表映射
- 验证 DwdVerifier 配置不包含废除表
### 集成验证
- 运行现有属性测试套件:`pytest tests/ -v`
- 运行 ETL 单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit`
- 确认所有测试通过,无回归

View File

@@ -0,0 +1,127 @@
# 需求文档助教废除Abolish全链路清理
## 简介
上游 SaaS 系统提供了一个独立的"助教废除记录"API`/AssistantPerformance/GetAbolitionAssistant`
ETL 系统为此建立了完整的 ODS → DWD → DWS 数据链路。但经排查发现:
1. **废除表 `dwd_assistant_trash_event` 无法与服务记录表 `dwd_assistant_service_log` 做 1:1 关联**——废除表没有 `assistant_service_id` 外键,两个 ID 不同源。
2. **DWS 层已改用 `dwd_assistant_service_log_ex.is_trash` 字段**(来自 `assistant_service_records` API直接判断服务是否被废除不再依赖废除表做跨表匹配。
3. 废除表的 `_extract_trash_records``_build_trash_index` 虽然仍被调用,但 `trash_index` 实际上不再参与废除判断逻辑(仅"备查"),属于死代码。
4. `trashed_seconds` / `trashed_count` 等 DWS 字段的数据来源已从废除表切换为服务记录自身的 `income_seconds`,废除表数据不再被消费。
因此,整条 abolish 独立链路API 抓取 → ODS 表 → DWD 表 → DWS 引用)可以安全移除,
同时保留 `assistant_service_records` 中已有的 `is_trash` / `trash_reason` / `trash_applicant_*` 字段作为废除判断的唯一数据源。
## 术语表
- **ETL_System**:飞球 ETL Connector`apps/etl/connectors/feiqiu/`
- **ODS_Layer**:原始数据层(`ods` schema存储从上游 API 抓取的原始记录
- **DWD_Layer**:明细数据层(`dwd` schema存储清洗后的事实表和维度表
- **DWS_Layer**:汇总数据层(`dws` schema存储按业务粒度聚合的汇总表
- **Abolish_Chain**:助教废除独立链路,包括 `ODS_ASSISTANT_ABOLISH` 任务、`ods.assistant_cancellation_records` 表、`dwd.dwd_assistant_trash_event` / `_ex` 表,以及 DWS 层中引用这些表的代码
- **Service_Trash_Fields**`assistant_service_records` API 中自带的废除标记字段(`is_trash``trash_reason``trash_applicant_id``trash_applicant_name`),已映射到 `dwd_assistant_service_log_ex`
- **FACT_MAPPINGS**`dwd_load_task.py` 中定义的 ODS → DWD 字段映射字典
- **Task_Registry**`orchestration/task_registry.py` 中的任务注册表
## 需求
### 需求 1移除 ODS 层废除任务
**用户故事:** 作为 ETL 维护者,我希望移除不再使用的 ODS 抓取任务,以减少无效 API 调用和维护负担。
#### 验收标准
1. WHEN ETL_System 执行调度时THE Task_Registry SHALL 不包含 `ODS_ASSISTANT_ABOLISH` 任务注册
2. WHEN ETL_System 加载 ODS 任务定义时THE ETL_System SHALL 不包含 `OdsAssistantAbolishTask``OdsTaskSpec` 定义
3. WHEN ETL_System 构建默认执行序列时THE ETL_System SHALL 不包含 `ODS_ASSISTANT_ABOLISH` 任务代码
### 需求 2移除 DWD 层废除表映射
**用户故事:** 作为 ETL 维护者,我希望移除废除表的 FACT_MAPPINGS 和 DWD 加载配置,以消除死代码。
#### 验收标准
1. WHEN DWD_Layer 执行装载时THE FACT_MAPPINGS SHALL 不包含 `dwd.dwd_assistant_trash_event` 的映射条目
2. WHEN DWD_Layer 执行装载时THE FACT_MAPPINGS SHALL 不包含 `dwd.dwd_assistant_trash_event_ex` 的映射条目
3. WHEN DWD_Layer 构建 ODS→DWD 表映射时THE ETL_System SHALL 不包含 `dwd.dwd_assistant_trash_event``ods.assistant_cancellation_records` 的映射关系
4. WHEN DWD_Layer 构建 ODS→DWD 表映射时THE ETL_System SHALL 不包含 `dwd.dwd_assistant_trash_event_ex``ods.assistant_cancellation_records` 的映射关系
### 需求 3清理 DWS 层废除表引用
**用户故事:** 作为 ETL 维护者,我希望移除 DWS 任务中对废除表的查询和索引构建代码,以消除死代码路径。
#### 验收标准
1. WHEN DWS_Layer 执行 `DWS_ASSISTANT_DAILY` 任务时THE AssistantDailyTask SHALL 不调用 `_extract_trash_records` 方法
2. WHEN DWS_Layer 执行 `DWS_ASSISTANT_DAILY` 任务时THE AssistantDailyTask SHALL 不调用 `_build_trash_index` 方法
3. WHEN DWS_Layer 执行 `DWS_ASSISTANT_DAILY` 任务时THE AssistantDailyTask SHALL 不向 `_aggregate_by_assistant_date` 传递 `trash_index` 参数
4. WHEN DWS_Layer 聚合服务记录时THE AssistantDailyTask SHALL 仅通过 `is_trash` 字段(来自 `dwd_assistant_service_log_ex` JOIN判断服务是否被废除
### 需求 4清理 DWD 验证器配置
**用户故事:** 作为 ETL 维护者,我希望移除验证器中对废除表的引用,以避免验证器尝试校验已不存在的表。
#### 验收标准
1. WHEN DWD_Layer 执行数据验证时THE DwdVerifier SHALL 不包含 `dwd_assistant_trash_event` 的 ID 映射配置
2. WHEN DWD_Layer 执行数据验证时THE DwdVerifier SHALL 不包含 `dwd_assistant_trash_event_ex` 的 ID 映射配置
3. WHEN DWD_Layer 执行数据验证时THE DwdVerifier SHALL 不包含 `dwd_assistant_trash_event` 的时间字段映射配置
4. WHEN DWD_Layer 执行数据验证时THE DwdVerifier SHALL 不包含 `dwd_assistant_trash_event_ex` 的时间字段映射配置
### 需求 5创建数据库迁移脚本
**用户故事:** 作为数据库管理员,我希望通过迁移脚本安全地移除废除相关的数据库对象,以保持 schema 整洁。
#### 验收标准
1. WHEN 迁移脚本执行时THE 迁移脚本 SHALL 删除 `dwd.dwd_assistant_trash_event`
2. WHEN 迁移脚本执行时THE 迁移脚本 SHALL 删除 `dwd.dwd_assistant_trash_event_ex`
3. WHEN 迁移脚本执行时THE 迁移脚本 SHALL 删除 `ods.assistant_cancellation_records`
4. WHEN 迁移脚本执行时THE 迁移脚本 SHALL 在 DROP 前使用 `IF EXISTS` 防止重复执行报错
5. WHEN 迁移脚本执行时THE 迁移脚本 SHALL 包含注释说明移除原因
### 需求 6同步更新 DDL 文档
**用户故事:** 作为 ETL 维护者,我希望 DDL schema 文件与实际数据库结构保持一致。
#### 验收标准
1. WHEN DDL 文件更新后THE `db/etl_feiqiu/schemas/dwd.sql` SHALL 不包含 `dwd_assistant_trash_event` 表的 CREATE TABLE 语句
2. WHEN DDL 文件更新后THE `db/etl_feiqiu/schemas/dwd.sql` SHALL 不包含 `dwd_assistant_trash_event_ex` 表的 CREATE TABLE 语句
3. WHEN DDL 文件更新后THE `db/etl_feiqiu/schemas/ods.sql` SHALL 不包含 `assistant_cancellation_records` 表的 CREATE TABLE 语句
4. WHEN DDL 文件更新后THE `db/etl_feiqiu/schemas/schema_dwd_doc.sql` SHALL 不包含 `dwd_assistant_trash_event` 相关的 CREATE TABLE 和 COMMENT 语句
5. WHEN DDL 文件更新后THE `db/etl_feiqiu/schemas/schema_ODS_doc.sql` SHALL 不包含 `assistant_cancellation_records` 相关的 CREATE TABLE 和 COMMENT 语句
6. WHEN DDL 文件更新后THE `db/etl_feiqiu/schemas/dws.sql``dws_assistant_daily_detail` 的注释 SHALL 不再引用 `dwd_assistant_trash_event` 作为数据来源
7. WHEN DDL 文件更新后THE `db/etl_feiqiu/schemas/schema_dws.sql``dws_assistant_daily_detail` 的注释 SHALL 不再引用 `dwd_assistant_trash_event` 作为数据来源
### 需求 7更新属性测试
**用户故事:** 作为开发者,我希望属性测试反映清理后的实际状态,以确保测试的准确性。
#### 验收标准
1. WHEN 属性测试执行时THE `test_property_1_fact_mappings.py` SHALL 不包含 `dwd.dwd_assistant_trash_event` 在 A 类表列表中
2. WHEN 属性测试执行时THE `test_property_1_fact_mappings.py` SHALL 不包含 `assistant_cancellation_records → dwd_assistant_trash_event` 的映射期望(`_REQ3_EXPECTED`
3. WHEN 属性测试执行后THE 所有现有属性测试 SHALL 通过(无回归)
### 需求 8更新运维脚本引用
**用户故事:** 作为运维人员,我希望运维脚本中不再引用已移除的表和任务,以避免脚本执行错误。
#### 验收标准
1. WHEN 运维脚本加载 ODS 任务映射时THE 脚本 SHALL 不包含 `ODS_ASSISTANT_ABOLISH``assistant_cancellation_records` 的映射
2. WHEN 运维脚本加载 DWD 表映射时THE 脚本 SHALL 不包含 `dwd.dwd_assistant_trash_event``ods.assistant_cancellation_records` 的映射
3. WHEN 运维脚本列举 ODS 表时THE 脚本 SHALL 不包含 `assistant_cancellation_records`
4. WHEN 运维脚本列举 DWD 表时THE 脚本 SHALL 不包含 `dwd_assistant_trash_event``dwd_assistant_trash_event_ex`
### 需求 9保留 Service_Trash_Fields 不受影响
**用户故事:** 作为 ETL 维护者,我希望确认清理操作不会影响 `assistant_service_records` 中已有的废除标记字段。
#### 验收标准
1. WHILE 清理操作执行期间THE `dwd_assistant_service_log_ex` 表 SHALL 保留 `is_trash``trash_reason``trash_applicant_id``trash_applicant_name` 字段不变
2. WHILE 清理操作执行期间THE `ods.assistant_service_records` 表 SHALL 保留 `is_trash``trash_reason``trash_applicant_id``trash_applicant_name` 字段不变
3. WHILE 清理操作执行期间THE AssistantDailyTask 中通过 `is_trash` 判断废除的逻辑 SHALL 保持正常工作

View File

@@ -0,0 +1,99 @@
# 实施计划助教废除Abolish全链路清理
## 概述
按 ETL 数据流的逆序DWS → DWD → ODS清理废除链路确保每一步都可验证。先清理代码引用再清理 DDL 和数据库对象,最后更新运维脚本和测试。
## 任务
- [x] 1. 清理 DWS 层死代码
- [x] 1.1 从 `assistant_daily_task.py` 中删除 `_extract_trash_records``_build_trash_index` 方法,从 `extract()` 中移除对 `_extract_trash_records` 的调用和 `trash_records` 变量,从 `_aggregate_by_assistant_date` 签名中移除 `trash_index` 参数,同步更新所有调用点。保留 `is_trash` 判断逻辑不变。
- 更新文件头部的 docstring移除对 `dwd_assistant_trash_event` 的数据来源引用
- _Requirements: 3.1, 3.2, 3.3, 3.4_
- [x] 1.2 编写属性测试验证废除聚合逻辑正确性
- **Property 1: 废除聚合逻辑正确性is_trash 驱动)**
- 生成随机服务记录列表,验证 `trashed_seconds`/`trashed_count``is_trash=1` 记录的手动计算一致
- **Validates: Requirements 3.4, 9.3**
- [x] 2. 清理 DWD 层映射和验证器
- [x] 2.1 从 `dwd_load_task.py``FACT_MAPPINGS` 中删除 `dwd.dwd_assistant_trash_event``dwd.dwd_assistant_trash_event_ex` 条目,从 `TABLE_MAP` 中删除对应的 ODS→DWD 映射
- _Requirements: 2.1, 2.2, 2.3, 2.4_
- [x] 2.2 从 `dwd_verifier.py` 中删除 `dwd_assistant_trash_event``dwd_assistant_trash_event_ex` 的 ID 映射和时间字段映射配置
- _Requirements: 4.1, 4.2, 4.3, 4.4_
- [x] 3. 清理 ODS 层任务定义
- [x] 3.1 从 `ods_tasks.py` 中删除 `ODS_ASSISTANT_ABOLISH``OdsTaskSpec` 定义,从默认执行序列中移除该任务代码
- _Requirements: 1.2, 1.3_
- [x] 3.2 从 `task_registry.py` 中删除 `ODS_ASSISTANT_ABOLISH` 的注册语句(如果存在独立注册)
- _Requirements: 1.1_
- [x] 4. Checkpoint — 确保 ETL 单元测试通过
- 运行 `cd apps/etl/connectors/feiqiu && pytest tests/unit`,确保所有测试通过,如有问题请询问用户。
- [x] 5. 更新属性测试
- [x] 5.1 从 `tests/test_property_1_fact_mappings.py` 中删除 `dwd.dwd_assistant_trash_event` 在 A 类表列表中的条目,删除 `_REQ3_EXPECTED` 映射期望及其在参数化测试中的引用
- _Requirements: 7.1, 7.2_
- [x] 6. 创建数据库迁移脚本
- [x] 6.1 在 `db/etl_feiqiu/migrations/` 下创建迁移脚本 `2026-02-22__drop_assistant_abolish_tables.sql`,包含 `DROP TABLE IF EXISTS` 语句删除 `ods.assistant_cancellation_records``dwd.dwd_assistant_trash_event``dwd.dwd_assistant_trash_event_ex`,以及删除相关索引(如 `idx_ods_assistant_cancellation_records_latest`),包含注释说明移除原因
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_
- [x] 7. 更新 DDL schema 文件
- [x] 7.1 从 `db/etl_feiqiu/schemas/dwd.sql` 中删除 `dwd_assistant_trash_event``dwd_assistant_trash_event_ex` 的 CREATE TABLE 和 COMMENT 语句
- _Requirements: 6.1, 6.2_
- [x] 7.2 从 `db/etl_feiqiu/schemas/ods.sql` 中删除 `assistant_cancellation_records` 的 CREATE TABLE 和 COMMENT 语句
- _Requirements: 6.3_
- [x] 7.3 从 `db/etl_feiqiu/schemas/schema_dwd_doc.sql` 中删除 `dwd_assistant_trash_event``dwd_assistant_trash_event_ex` 的 CREATE TABLE 和 COMMENT 语句
- _Requirements: 6.4_
- [x] 7.4 从 `db/etl_feiqiu/schemas/schema_ODS_doc.sql` 中删除 `assistant_cancellation_records` 的 CREATE TABLE 和 COMMENT 语句
- _Requirements: 6.5_
- [x] 7.5 更新 `db/etl_feiqiu/schemas/dws.sql``db/etl_feiqiu/schemas/schema_dws.sql``dws_assistant_daily_detail` 的注释,将数据来源从 `dwd_assistant_trash_event` 改为 `dwd_assistant_service_log_ex.is_trash`
- _Requirements: 6.6, 6.7_
- [x] 8. 更新运维脚本
- [x] 8.1 从 `scripts/ops/dataflow_analyzer.py` 中删除 `ODS_ASSISTANT_ABOLISH` spec 条目
- _Requirements: 8.1_
- [x] 8.2 从 `scripts/ops/gen_full_dataflow_doc.py` 中删除 `ODS_ASSISTANT_ABOLISH` spec 条目
- _Requirements: 8.1_
- [x] 8.3 从 `scripts/ops/etl_consistency_check.py` 中删除 `ODS_ASSISTANT_ABOLISH` 映射和 `dwd.dwd_assistant_trash_event` 映射
- _Requirements: 8.1, 8.2_
- [x] 8.4 从 `scripts/ops/blackbox_test_report.py` 中删除 `assistant_cancellation_records` 在 ODS_TABLES 列表中的条目、`ODS_ASSISTANT_ABOLISH` 映射、`dwd.dwd_assistant_trash_event` 映射
- _Requirements: 8.1, 8.2, 8.3, 8.4_
- [x] 8.5 从 `scripts/ops/field_audit.py` 中删除 `assistant_cancellation_records` 审计条目
- _Requirements: 8.3, 8.4_
- [x] 8.6 从 `scripts/ops/gen_field_review_doc.py` 中删除 `assistant_cancellation_records` 相关的字段定义块
- _Requirements: 8.3, 8.4_
- [x] 8.7 从 `scripts/ops/gen_api_field_mapping.py` 中删除 `assistant_cancellation_records` 在 ODS_TABLES 列表中的条目
- _Requirements: 8.3_
- [x] 8.8 从 `scripts/ops/export_dwd_field_review.py` 中删除 `dwd_assistant_trash_event``dwd_assistant_trash_event_ex` 条目
- _Requirements: 8.4_
- [x] 8.9 从 `scripts/ops/check_ods_latest_indexes.py` 中删除 `idx_ods_assistant_cancellation_records_latest` 索引检查
- _Requirements: 8.3_
- [x] 9. 最终 Checkpoint — 确保所有测试通过
- 运行 `cd apps/etl/connectors/feiqiu && pytest tests/unit``cd C:\NeoZQYY && pytest tests/ -v`
- 确认所有测试通过,无回归,如有问题请询问用户。
- _Requirements: 7.3, 9.1, 9.2, 9.3_
## 备注
- 标记 `*` 的任务为可选,可跳过以加速 MVP
- 每个任务引用了具体的需求编号,便于追溯
- Checkpoint 确保增量验证
- 属性测试验证通用正确性属性,单元测试验证具体示例和边界情况
- 本次清理涉及高风险路径(`tasks/``orchestration/``db/`),完成后需运行 `/audit`

View File

@@ -0,0 +1 @@
{"generationMode": "requirements-first"}

View File

@@ -0,0 +1,447 @@
# 设计文档:数据流字段补全与前后端联调
## 概述
本设计基于 `dataflow_2026-02-19_190440.md` 数据流分析报告,覆盖两大任务:
1. **字段补全**:对 11 张 ODS/DWD 表执行字段映射补全,包括 DDL 更新、ETL loader/task 代码同步、文档精化
2. **DWS 库存汇总**:在 DWS 层新建日/周/月三个粒度的库存汇总表,基于 DWD goods_stock_summary 数据构建
3. **前后端联调**:确保 admin-web 前端与 FastAPI 后端的 ETL 执行流程完整可用,含计时和黑盒测试
核心设计原则:
- **执行依据**:字段补全部分基于排查结论文档 `export/SYSTEM/REPORTS/field_audit/field_review_for_user.md`(由 `FIELD_AUDIT_ROOT` 环境变量配置路径)
- **先确认再新增**:对每个疑似缺失字段,必须先排查是否已存在(可能是命名差异、已映射到其他列、或已在 FACT_MAPPINGS 中以不同名称配置),确认确实缺失后才执行新增
- 所有字段映射变更通过 `DwdLoadTask.FACT_MAPPINGS` 声明式配置,不修改核心合并逻辑
- 新建 DWD 表遵循现有 main/ex 分表模式(核心字段 → main 表,扩展字段 → ex 表)
- DDL 变更通过迁移脚本(`db/etl_feiqiu/migrations/`)执行,同步更新 schema 文件
- 控制无效字段新增:仅在确认字段确实缺失且有业务价值时才新增
## 架构
### 现有 ETL 数据流架构
```mermaid
graph LR
API[上游 SaaS API] -->|JSON| ODS_Loader[GenericODSLoader]
ODS_Loader -->|UPSERT| ODS[(ODS 表)]
ODS -->|SELECT| DWD_Task[DwdLoadTask]
DWD_Task -->|SCD2 合并| DIM[(DWD 维度表)]
DWD_Task -->|增量插入| FACT[(DWD 事实表)]
```
### 字段映射机制
`DwdLoadTask` 使用两层映射策略:
1. **自动映射**ODS 列名与 DWD 列名相同时自动匹配
2. **显式映射**:通过 `FACT_MAPPINGS` 字典声明 `(dwd_col, ods_expr, cast_type)` 三元组
本次变更主要操作 `FACT_MAPPINGS``TABLE_MAP`,以及对应的 DDL。
### 前后端联调架构
```mermaid
graph LR
AdminWeb[Admin Web<br/>React + Ant Design] -->|HTTP/WS| Backend[FastAPI 后端]
Backend -->|subprocess| ETL[ETL CLI]
ETL -->|SQL| DB[(PostgreSQL)]
Backend -->|WebSocket| AdminWeb
```
## 字段排查结论(已完成)
排查工作已完成,详细结论见 `export/SYSTEM/REPORTS/field_audit/field_review_for_user.md`
排查方法包括:查 DWD 表现有列、查 FACT_MAPPINGS、查 ODS 表现有列、查自动映射、查 API JSON 样本、数据库实际数据验证。排查发现 4 个映射错误、21 个待新增字段、2 张需新建 DWD 表、6 个跳过字段。
## 组件与接口
### 任务 1字段补全涉及的组件
| 组件 | 文件路径 | 变更类型 |
|------|---------|---------|
| DWD 加载任务 | `tasks/dwd/dwd_load_task.py` | 修改 `FACT_MAPPINGS``TABLE_MAP` |
| ODS DDL | `db/etl_feiqiu/schemas/ods.sql` | 新增列store_goods_master 嵌套展开) |
| DWD DDL | `db/etl_feiqiu/schemas/dwd.sql` | 新增列、新建表 |
| 迁移脚本 | `db/etl_feiqiu/migrations/` | 新增 ALTER TABLE / CREATE TABLE |
| ODS 加载器 | `loaders/ods/generic.py` | 可能需要扩展 columns 列表 |
| BD_Manual 文档 | `docs/database/` | 更新字段说明 |
### 任务 2前后端联调涉及的组件
| 组件 | 文件路径 | 变更类型 |
|------|---------|---------|
| 执行 API | `apps/backend/app/routers/` | 调试/修复参数传递 |
| 执行页面 | `apps/admin-web/src/pages/TaskManager.tsx` | 调试/修复前端逻辑 |
| 计时模块 | `apps/etl/connectors/feiqiu/utils/` | 新增计时器工具 |
| 黑盒测试 | `apps/etl/connectors/feiqiu/quality/` | 新增数据一致性检查 |
## 数据模型
### 字段补全分类
根据 `field_review_for_user.md` 排查结论,将变更分为四类:
#### 🔴 映射错误修复(高优先级)
| 表 | 问题 | 修正方案 |
|----|------|---------|
| assistant_service_records | DWD `site_assistant_id` 错误映射自 ODS `order_assistant_id` | 修正映射源 + 新增 `order_assistant_id` 列 |
| store_goods_sales_records | DWD `discount_price` 实际映射自 ODS `discount_money`(列名误导) | 重命名 DWD 列 + 新增真正的 `discount_price` |
| store_goods_master | `batch_stock_qty` 映射自 `stock`(错误),`provisional_total_cost` 映射自 `total_purchase_cost`(错误) | 修正 FACT_MAPPINGS 源列 |
#### A 类:新增 DWD 列 + FACT_MAPPINGS
| 表 | 新增字段数 | DWD 目标 |
|----|----------|---------|
| assistant_accounts_master | 4 | dim_assistant_ex |
| assistant_service_records | 2 | dwd_assistant_service_log_ex |
| assistant_cancellation_records | 0仅更新映射 | dwd_assistant_trash_event |
| member_balance_changes | 1 | dwd_member_balance_change_ex |
| site_tables_master | 14 | dim_table_ex |
#### B 类:仅补 FACT_MAPPINGSDWD 列已存在)
| 表 | 说明 |
|----|------|
| recharge_settlements | 5 个字段DWD 列已存在ODS/DWD 两侧数据全为 0业务未启用 |
#### 跳过(无需变更)
| 表 | 原因 |
|----|------|
| tenant_goods_master | `commoditycode``commodity_code` 100% 冗余(花括号包裹格式),跳过 |
| store_goods_mastertime_slot_sale | ODS 列不存在,跳过 |
#### C 类:需新建 DWD 表
| 表 | ODS 字段数 | DWD 新表 | 备注 |
|----|----------|---------|------|
| goods_stock_summary | 14 | dwd_goods_stock_summary | 需先修改 ODS 配置 `requires_window=True` 并重新采集 |
| goods_stock_movements | 19 | dwd_goods_stock_movement | 事实表,按 createtime 增量加载 |
#### C 类:疑似需新建 DWD 表(需排查是否有替代方案)
| 表 | ODS 字段数 | 疑似新建 DWD 表 | 排查重点 |
|----|----------|---------------|---------|
| goods_stock_summary | 14 | dwd_goods_stock_summary | 确认是否有意不建 DWD 表(如数据直接在 ODS 层使用) |
| goods_stock_movements | 19 | dwd_goods_stock_movement | 同上 |
### 已确认的映射关系(排查结论)
以下映射关系已通过数据库实际数据验证确认:
| 字段 | 排查结论 | 所在表 |
|------|---------|-------|
| discount_price (store_goods_sales) | 🔴 DWD `discount_price` 实际映射自 ODS `discount_money`,需重命名 + 新增 | store_goods_sales_records |
| commoditycode (tenant_goods) | ⏭️ 与 `commodity_code` 100% 冗余,跳过 | tenant_goods_master |
| site_assistant_id (assistant_service) | 🔴 DWD 错误映射自 ODS `order_assistant_id`,需修正 | assistant_service_records |
| recharge 电费/券字段 | ✅ DWD 列已存在,仅需补 FACT_MAPPINGS数据全为 0 | recharge_settlements |
| batch_stock_qty (store_goods) | 🔴 错误映射自 `stock`,应映射自 `batch_stock_quantity` | store_goods_master |
| provisional_total_cost (store_goods) | 🔴 错误映射自 `total_purchase_cost`,应映射自 `provisional_total_cost` | store_goods_master |
### 新建 DWD 表设计
#### dwd_goods_stock_summary
```sql
CREATE TABLE dwd.dwd_goods_stock_summary (
site_goods_id bigint NOT NULL,
goods_name text,
goods_unit text,
goods_category_id bigint,
goods_category_second_id bigint,
category_name text,
range_start_stock numeric,
range_end_stock numeric,
range_in numeric,
range_out numeric,
range_sale numeric,
range_sale_money numeric(12,2),
range_inventory numeric,
current_stock numeric,
site_id bigint,
tenant_id bigint,
fetched_at timestamptz,
PRIMARY KEY (site_goods_id)
);
```
#### dwd_goods_stock_movement
```sql
CREATE TABLE dwd.dwd_goods_stock_movement (
site_goods_stock_id bigint NOT NULL,
tenant_id bigint,
site_id bigint,
site_goods_id bigint,
goods_name text,
goods_category_id bigint,
goods_second_category_id bigint,
unit text,
price numeric(12,2),
stock_type integer,
change_num numeric,
start_num numeric,
end_num numeric,
change_num_a numeric,
start_num_a numeric,
end_num_a numeric,
remark text,
operator_name text,
create_time timestamptz,
fetched_at timestamptz,
PRIMARY KEY (site_goods_stock_id)
);
```
### recharge_settlements 映射关系
ODS 列与 DWD 列的对应关系(命名转换):
| ODS 列(驼峰) | DWD 列(蛇形) |
|---------------|--------------|
| plcouponsaleamount | pl_coupon_sale_amount |
| mervousalesamount | mervou_sales_amount |
| electricitymoney | electricity_money |
| realelectricitymoney | real_electricity_money |
| electricityadjustmoney | electricity_adjust_money |
这 5 个字段在 `dwd_recharge_order` 中已有列定义但缺少 FACT_MAPPINGS 条目,需要补充映射。
### store_goods_master 映射修正
根据排查结论,该表存在两个映射错误(非新增字段):
| DWD 列 | 当前错误映射 ODS 列 | 正确 ODS 列 | 验证结果 |
|--------|-------------------|------------|---------|
| `batch_stock_qty` | `stock`(当前库存) | `batch_stock_quantity`(批次库存) | 仅 7.3% 行相等 |
| `provisional_total_cost` | `total_purchase_cost`(实际采购成本) | `provisional_total_cost`(暂估成本) | 93.5% 行相等但 113 行不同 |
`time_slot_sale` ODS 列不存在,跳过。`goodsStockWarningInfo` 嵌套展开不在本次范围内。
### DWS 库存汇总表设计(日/周/月)
基于 `field_review_for_user.md` 第 10 章发现goods_stock_summary API 支持 `startTime`/`endTime` 参数返回时间范围内的库存汇总数据。在 ODS 任务配置修改(`requires_window=True` + `time_fields=("startTime", "endTime")`并重新采集后DWD 层 `dwd_goods_stock_summary` 将拥有带时间范围的真实数据,可在此基础上构建 DWS 层汇总。
#### 三张 DWS 表
| 表名 | 粒度 | 任务代码 | stat_period |
|------|------|---------|-------------|
| `dws.dws_goods_stock_daily_summary` | 日 | `DWS_GOODS_STOCK_DAILY` | `'daily'` |
| `dws.dws_goods_stock_weekly_summary` | 周 | `DWS_GOODS_STOCK_WEEKLY` | `'weekly'` |
| `dws.dws_goods_stock_monthly_summary` | 月 | `DWS_GOODS_STOCK_MONTHLY` | `'monthly'` |
#### DDL 设计(三张表结构相同)
```sql
CREATE TABLE dws.dws_goods_stock_daily_summary (
site_id bigint NOT NULL,
tenant_id bigint,
stat_date date NOT NULL,
site_goods_id bigint NOT NULL,
goods_name text,
goods_unit text,
goods_category_id bigint,
goods_category_second_id bigint,
category_name text,
range_start_stock numeric,
range_end_stock numeric,
range_in numeric,
range_out numeric,
range_sale numeric,
range_sale_money numeric(12,2),
range_inventory numeric,
current_stock numeric,
stat_period text NOT NULL DEFAULT 'daily',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (site_id, stat_date, site_goods_id)
);
```
周度表和月度表结构相同,仅表名和 `stat_period` 默认值不同(`'weekly'` / `'monthly'`)。
#### 任务实现模式
继承 `BaseDwsTask`,实现 `extract` / `transform` / `load` 三阶段:
- **extract**:从 `dwd.dwd_goods_stock_summary` 按时间范围查询数据
- **transform**:按粒度(日/周/月)对 `stat_date` 进行分组聚合,计算各库存指标
- 日度:直接取 DWD 数据(`stat_date` = 采集日期)
- 周度:按 ISO 周分组,`stat_date` = 周一日期
- 月度:按自然月分组,`stat_date` = 月首日期
- **load**:使用 `upsert` 写入目标表,主键冲突时更新
#### 前置依赖
- 需求 7goods_stock_summary 新建 DWD 表)必须先完成
- ODS 任务配置修改(`requires_window=True`)必须先完成并重新采集数据
#### 文件位置
- DDL`db/etl_feiqiu/schemas/dws.sql`
- 迁移脚本:`db/etl_feiqiu/migrations/{date}__create_dws_goods_stock_summary.sql`
- 任务代码:`apps/etl/connectors/feiqiu/tasks/dws/goods_stock_daily_task.py``goods_stock_weekly_task.py``goods_stock_monthly_task.py`
### settlement_ticket_details 彻底移除设计
从项目中完整移除 `settlement_ticket_details`结账小票详情相关的所有代码、DDL、配置、文档和数据。
#### 需要移除的文件/代码位置
| 层级 | 文件路径 | 移除内容 |
|------|---------|---------|
| ETL 任务定义 | `tasks/ods/ods_tasks.py` | `OdsTaskSpec("ODS_SETTLEMENT_TICKET", ...)``OdsSettlementTicketTask` 类、`ENABLED_ODS_CODES` 中的条目、`ODS_TASK_CLASSES` 覆盖 |
| ETL 校验 | `tasks/verification/dwd_verifier.py` | `settlement_ticket_details` 主键映射条目 |
| ETL 校验 | `tasks/verification/ods_verifier.py` | 相关注释和特殊处理逻辑 |
| ETL 手动导入 | `tasks/utility/manual_ingest_task.py` | `settlement_ticket_details` 的表映射和配置 |
| JSON 存储 | `utils/json_store.py` | `/order/getordersettleticketnew` 的路径映射 |
| ODS 间隙检查 | `scripts/check/check_ods_gaps.py` | `_check_settlement_tickets` 函数及调用 |
| 黑盒调试 | `scripts/debug/debug_blackbox.py` | `ODS_SETTLEMENT_TICKET` 跳过逻辑 |
| DDL | `db/etl_feiqiu/schemas/ods.sql``schema_ODS_doc.sql` | `settlement_ticket_details` 建表语句和注释 |
| 种子数据 | `db/etl_feiqiu/seeds/seed_ods_tasks.sql` | `ODS_SETTLEMENT_TICKET` 条目 |
| 索引检查 | `scripts/ops/check_ods_latest_indexes.py` | `idx_ods_settlement_ticket_details_latest` |
| 分析脚本 | `scripts/ops/gen_full_dataflow_doc.py` | ODS spec 条目和特殊跳过逻辑 |
| 分析脚本 | `scripts/ops/gen_field_review_doc.py` | 第 12 章 settlement_ticket_details 配置 |
| 分析脚本 | `scripts/ops/gen_api_field_mapping.py` | 表名列表中的条目 |
| 分析脚本 | `scripts/ops/field_audit.py` | 排查配置和特殊处理 |
| 分析脚本 | `scripts/ops/export_dwd_field_review.py` | 字段列表配置 |
| 分析脚本 | `scripts/ops/dataflow_analyzer.py` | ODS spec 条目和跳过逻辑 |
| 文档 | `docs/database/etl_feiqiu_schema_migration.md` | 索引条目 |
| ETL 文档 | `apps/etl/connectors/feiqiu/docs/etl_tasks/` | 任务表格条目 |
| 单元测试 | `tests/unit/test_ods_tasks.py` | `test_ods_settlement_ticket_by_payment_relate_ids` |
#### 迁移脚本
```sql
-- 移除 settlement_ticket_details 表和索引
DROP INDEX IF EXISTS ods.idx_ods_settlement_ticket_details_latest;
DROP TABLE IF EXISTS ods.settlement_ticket_details;
-- 移除 meta.ods_task_registry 中的任务注册
DELETE FROM meta.ods_task_registry WHERE task_code = 'ODS_SETTLEMENT_TICKET';
```
#### 注意事项
- `export/` 下的报告文件(`field_audit_report.md``dataflow_api_ods_dwd.md` 等)为历史产物,不需要手动清理,下次重新生成时自然不再包含
- `docs/audit/` 下的审计日志为历史记录,保留不动
- `tmp/` 下的临时文件不需要处理
## 正确性属性
*正确性属性是一种在系统所有合法执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
### Property 1FACT_MAPPINGS 字段映射正确性
*对于任意* ODS 表行和任意已配置的 `FACT_MAPPINGS` 条目 `(dwd_col, ods_expr, cast_type)`,当 DWD 加载任务执行后DWD 目标行中 `dwd_col` 列的值应等于从 ODS 行中按 `ods_expr` 提取并按 `cast_type` 转换后的值。
**Validates: Requirements 1.1, 1.2, 2.1, 3.1, 4.1, 5.1, 6.1, 6.2, 7.2, 8.2, 9.1, 10.3, 11.1**
### Property 2FACT_MAPPINGS 引用完整性
*对于任意* `FACT_MAPPINGS` 中的映射条目,其 DWD 目标列名必须存在于对应 DWD 表的列定义中,其 ODS 源表达式引用的列名必须存在于对应 ODS 表的列定义中(或为合法的 SQL 表达式)。
**Validates: Requirements 6.3**
### Property 3TABLE_MAP 覆盖完整性
*对于任意*`TABLE_MAP` 中注册的 DWD 表,该表的所有非 SCD2 列要么在 `FACT_MAPPINGS` 中有显式映射,要么在对应 ODS 表中存在同名列(自动映射)。
**Validates: Requirements 7.2, 8.2**
### Property 4映射错误修正后数据一致性
*对于任意* 已修正映射的字段assistant_service_records.site_assistant_id、store_goods_sales_records.discount_price、store_goods_master.batch_stock_qty、store_goods_master.provisional_total_cost修正后 DWD 目标列的值应等于从正确的 ODS 源列提取的值,而非修正前的错误源列。
**Validates: Requirements 2.1, 4.1, 10.3**
### Property 5ETL 参数解析与 CLI 命令构建正确性
*对于任意* 合法的 ETL 执行参数组合门店列表、数据源模式、校验模式、时间范围、窗口切分、force-full 标志、任务选择Backend 构建的 CLI 命令字符串应包含所有指定参数,且参数值与输入一致。
**Validates: Requirements 14.1, 14.2**
### Property 6数据一致性检查正确性
*对于任意* ODS 行和对应的 DWD 行,黑盒测试检查器应能正确识别:(a) ODS 中存在但 DWD 中缺失的字段,(b) ODS 与 DWD 之间值不一致的字段。
**Validates: Requirements 16.2, 16.3**
### Property 7计时器记录完整性
*对于任意* ETL 步骤序列,计时器输出应包含每个步骤的名称、开始时间、结束时间和耗时,且耗时等于结束时间减去开始时间。
**Validates: Requirements 15.2**
### Property 8DWS 库存汇总粒度聚合正确性
*对于任意* DWD 库存汇总数据集和任意汇总粒度(日/周/月DWS 汇总任务的 transform 输出应满足:(a) 每条记录的 `stat_period` 与任务粒度一致,(b) 同一 `(site_id, stat_date, site_goods_id)` 组合不重复,(c) 日度汇总的记录数不少于周度和月度汇总的记录数。
**Validates: Requirements 12.2, 12.3, 12.4, 12.5, 12.6**
## 错误处理
### 字段补全错误处理
| 场景 | 处理方式 |
|------|---------|
| DDL 迁移失败 | 回滚事务,记录错误日志,不影响其他表 |
| ODS 列不存在 | 跳过该映射条目,记录 WARNING 日志 |
| 类型转换失败 | 使用 NULLIF + CAST 兜底,转换失败写入 NULL |
| 新建 DWD 表主键冲突 | 使用 ON CONFLICT DO UPDATE 策略 |
### DWS 库存汇总错误处理
| 场景 | 处理方式 |
|------|---------|
| DWD 源表无数据 | 跳过汇总,记录 WARNING 日志 |
| 跨周/跨月边界数据不完整 | 按已有数据汇总,不补零 |
| upsert 主键冲突 | 使用 ON CONFLICT DO UPDATE 更新已有记录 |
| DWD 表尚未创建(前置依赖未完成) | 抛出明确错误,提示需先完成需求 7 |
### 前后端联调错误处理
| 场景 | 处理方式 |
|------|---------|
| 参数校验失败 | 返回 422 状态码,附带详细错误信息 |
| ETL 子进程超时 | 设置超时阈值,超时后终止进程并返回错误 |
| WebSocket 断连 | 前端自动重连,后端缓存最近日志 |
| 黑盒测试发现不一致 | 记录差异明细到报告,不中断流程 |
## 测试策略
### 属性测试
使用 `hypothesis` 库进行属性测试,每个属性至少运行 100 次迭代。
- **Property 1-3**:通过 FakeDB 模拟 ODS/DWD 表结构,生成随机 ODS 行数据,验证 FACT_MAPPINGS 映射逻辑
- **Property 4**:对修正后的映射字段,验证 DWD 值来自正确的 ODS 源列
- **Property 5**:生成随机参数组合,验证 CLI 命令构建
- **Property 6**:生成随机 ODS/DWD 行对,验证一致性检查逻辑
- **Property 7**:生成随机步骤序列,验证计时器输出
- **Property 8**:生成随机 DWD 库存数据,验证日/周/月三个粒度的聚合逻辑正确性
测试标签格式:`Feature: dataflow-field-completion, Property N: {property_text}`
### 单元测试
- DDL 迁移脚本语法正确性SQL 解析)
- 各表 FACT_MAPPINGS 条目的具体映射值验证
- DWS 库存汇总任务的边界值测试(跨周/跨月数据、空数据集)
- 前端参数表单的边界值测试
- 计时器精度测试
### 集成测试
- 端到端 ETL 执行:从 API JSON 到 DWD 落库的完整流程
- 前后端联调:从 Admin Web 触发到 ETL 完成的完整流程
- 黑盒测试:全量数据一致性验证
### 测试工具
- ETL 单元测试使用 `tests/unit/task_test_utils.py` 提供的 FakeDB/FakeAPI
- 属性测试使用 `hypothesis`
- 后端测试使用 `pytest` + FastAPI TestClient

View File

@@ -0,0 +1,228 @@
# 需求文档:数据流字段补全与前后端联调
## 简介
本特性基于 `dataflow_2026-02-19_190440.md` 数据流分析报告,完成三大任务:
1. 补全 11 张 ODS/DWD 表中缺失的字段映射(含 DDL 更新、ETL loader/task 代码同步、文档精化)
2. 在 DWS 层新建库存汇总表,支持日/周/月三个粒度的库存数据汇总
3. 管理后台admin-web前后端联调确保 ETL 全流程可通过 Web 界面正确触发和执行
## 术语表
- **ETL_System**:飞球连接器 ETL 系统(`apps/etl/connectors/feiqiu/`),负责从上游 API 抽取数据并经 ODS→DWD→DWS 三层处理
- **ODS**Operational Data Store原始数据层保留 API 返回的原始字段
- **DWD**Data Warehouse Detail明细数据层经清洗、标准化后的业务字段
- **DDL**Data Definition Language数据库表结构定义位于 `db/etl_feiqiu/schemas/`
- **Loader**ETL 加载器(`loaders/`),负责将 ODS 数据清洗映射到 DWD 表
- **Task**ETL 任务(`tasks/`),编排 loader 的执行逻辑
- **Admin_Web**:管理后台(`apps/admin-web/`React + Vite + Ant Design 前端
- **Backend**FastAPI 后端(`apps/backend/`),提供 ETL 调度和数据查询 API
- **SCD2**:缓慢变化维度类型 2用于维度表历史版本追踪
- **BD_Manual**:业务数据字典文档(`docs/database/`),记录字段含义和映射关系
- **Field_Mapping**:字段映射关系,描述 API JSON → ODS 列 → DWD 列的对应关系
- **DWS**Data Warehouse Summary汇总数据层按业务维度聚合的统计数据
- **BaseDwsTask**DWS 任务基类(`tasks/dws/base_dws_task.py`),提供 extract/transform/load 三阶段框架
## 执行依据
本需求文档的字段补全部分基于以下排查结论文档:
- `export/SYSTEM/REPORTS/field_audit/field_review_for_user.md` — 逐表逐字段的排查结论与操作建议
## 需求
### 需求 1assistant_accounts_master 字段补全
**用户故事:** 作为数据工程师,我希望将助教账号档案表中 4 个未映射的 ODS 字段system_role_id、job_num、cx_unit_price、pd_unit_price补全到 DWD 层,以便下游分析可以使用完整的助教档案数据。
#### 验收标准
1. WHEN ETL_System 执行 assistant_accounts_master 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `system_role_id` 映射到 DWD 目标表 `dim_assistant_ex` 的对应列
2. WHEN ETL_System 执行 assistant_accounts_master 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `job_num``cx_unit_price``pd_unit_price` 映射到 DWD 目标表 `dim_assistant_ex` 的对应列
3. WHEN 新字段被添加到 DWD 表, THE DDL SHALL 在 `db/etl_feiqiu/schemas/dwd.sql` 中包含对应的 ALTER TABLE 或 CREATE 语句
4. WHEN 字段映射完成后, THE BD_Manual SHALL 更新对应的字段说明文档,消除"待补充""待分析"等模糊描述
### 需求 2assistant_service_records 字段补全
**用户故事:** 作为数据工程师,我希望将助教服务流水表中 3 个未映射的 ODS 字段site_assistant_id、operator_id、operator_name补全到 DWD 层,以便追踪服务操作员信息。
#### 验收标准
1. WHEN ETL_System 执行 assistant_service_records 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `site_assistant_id``operator_id``operator_name` 映射到 DWD 目标表 `dwd_assistant_service_log_ex` 的对应列
2. WHEN 新字段被添加到 DWD 表, THE DDL SHALL 在 `dwd.sql` 中包含对应的列定义
3. WHEN 字段映射完成后, THE BD_Manual SHALL 更新对应的字段说明文档
### 需求 3assistant_cancellation_records 字段补全
**用户故事:** 作为数据工程师,我希望将助教废除记录表中 1 个未映射的 ODS 字段assistanton补全到 DWD 层。
#### 验收标准
1. WHEN ETL_System 执行 assistant_cancellation_records 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `assistanton` 映射到 DWD 目标表 `dwd_assistant_trash_event_ex` 的对应列
2. WHEN 新字段被添加到 DWD 表, THE DDL SHALL 在 `dwd.sql` 中包含对应的列定义
3. WHEN 字段映射完成后, THE BD_Manual SHALL 对 `assistanton` 字段进行语义分析并补充精确说明
### 需求 4store_goods_sales_records 字段补全
**用户故事:** 作为数据工程师,我希望将门店商品销售流水表中 1 个未映射的 ODS 字段discount_price补全到 DWD 层,以便分析折后单价。
#### 验收标准
1. WHEN ETL_System 执行 store_goods_sales_records 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `discount_price` 映射到 DWD 目标表 `dwd_store_goods_sale_ex` 的对应列
2. WHEN 新字段被添加到 DWD 表, THE DDL SHALL 在 `dwd.sql` 中包含对应的列定义,类型为 `numeric`(金额精度)
3. WHEN 字段映射完成后, THE BD_Manual SHALL 更新对应的字段说明文档
### 需求 5member_balance_changes 字段补全
**用户故事:** 作为数据工程师,我希望将会员余额变动表中 1 个未映射的 ODS 字段relate_id补全到 DWD 层,以便关联充值记录或订单。
#### 验收标准
1. WHEN ETL_System 执行 member_balance_changes 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `relate_id` 映射到 DWD 目标表 `dwd_member_balance_change_ex` 的对应列
2. WHEN 新字段被添加到 DWD 表, THE DDL SHALL 在 `dwd.sql` 中包含对应的列定义
3. WHEN 字段映射完成后, THE BD_Manual SHALL 更新对应的字段说明文档
### 需求 6recharge_settlements 字段补全与映射建立
**用户故事:** 作为数据工程师,我希望将充值结算表中 5 个 ODS→DWD 未映射字段补全,并为 5 个 DWD 无 ODS 源字段建立正确的映射关系,以便电费和券销售额数据完整流转。
#### 验收标准
1. WHEN ETL_System 执行 recharge_settlements 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `electricityadjustmoney``electricitymoney``mervousalesamount``plcouponsaleamount``realelectricitymoney` 映射到 DWD 目标表 `dwd_recharge_order` 的对应列
2. WHEN DWD 表存在无 ODS 源的列(`pl_coupon_sale_amount``mervou_sales_amount``electricity_money``real_electricity_money``electricity_adjust_money`, THE Loader SHALL 建立从 ODS 对应列到这些 DWD 列的映射关系
3. WHEN 映射建立后, THE ETL_System SHALL 确保 ODS 列名(驼峰式)与 DWD 列名(蛇形式)之间的命名转换正确
4. WHEN 字段映射完成后, THE DDL SHALL 同步更新THE BD_Manual SHALL 更新对应的字段说明文档
### 需求 7goods_stock_summary 新建 DWD 表与字段映射
**用户故事:** 作为数据工程师,我希望为库存汇总表新建 DWD 目标表,并将 14 个 ODS 字段完整映射,以便库存数据可在 DWD 层使用。
#### 验收标准
1. WHEN ETL_System 需要加载 goods_stock_summary 数据到 DWD 层, THE DDL SHALL 在 `dwd.sql` 中创建新的 DWD 目标表(如 `dwd_goods_stock_summary`
2. WHEN DWD 目标表创建后, THE Loader SHALL 将全部 14 个 ODS 列sitegoodsid、goodsname、goodsunit、goodscategoryid、goodscategorysecondid、categoryname、rangestartstock、rangeendstock、rangein、rangeout、rangesale、rangesalemoney、rangeinventory、currentstock映射到 DWD 目标表
3. WHEN 新表创建后, THE ETL_System SHALL 创建对应的 DWD loader 和 task 代码
4. WHEN 新表创建后, THE BD_Manual SHALL 为新表编写完整的字段说明文档
### 需求 8goods_stock_movements 新建 DWD 表与字段映射
**用户故事:** 作为数据工程师,我希望为库存变化记录表新建 DWD 目标表,并将 19 个 ODS 字段完整映射,以便库存变动明细可在 DWD 层使用。
#### 验收标准
1. WHEN ETL_System 需要加载 goods_stock_movements 数据到 DWD 层, THE DDL SHALL 在 `dwd.sql` 中创建新的 DWD 目标表(如 `dwd_goods_stock_movement`
2. WHEN DWD 目标表创建后, THE Loader SHALL 将全部 19 个 ODS 列映射到 DWD 目标表
3. WHEN 新表创建后, THE ETL_System SHALL 创建对应的 DWD loader 和 task 代码
4. WHEN 新表创建后, THE BD_Manual SHALL 为新表编写完整的字段说明文档
### 需求 9site_tables_master 字段补全
**用户故事:** 作为数据工程师,我希望将台桌维表中 14 个未映射的 ODS 字段补全到 DWD 层,以便台桌配置信息完整可用。
#### 验收标准
1. WHEN ETL_System 执行 site_tables_master 的 DWD 加载任务, THE Loader SHALL 将 14 个 ODS 列sitename、appletQrCodeUrl、audit_status、charge_free、create_time、delay_lights_time、is_rest_area、light_status、only_allow_groupon、order_delay_time、self_table、tablestatusname、temporary_light_second、virtual_table映射到 DWD 目标表 `dim_table_ex` 的对应列
2. WHEN 新字段被添加到 DWD 表, THE DDL SHALL 在 `dwd.sql` 中包含对应的列定义
3. WHEN 字段映射完成后, THE BD_Manual SHALL 更新对应的字段说明文档,消除"待补充""待分析"等模糊描述
### 需求 10store_goods_master 字段补全与嵌套展开
**用户故事:** 作为数据工程师我希望将门店商品档案表中的平层未映射字段、嵌套对象字段、ODS→DWD 未映射字段全部补全,以便商品档案数据完整。
#### 验收标准
1. WHEN ETL_System 执行 store_goods_master 的 ODS 加载任务, THE Loader SHALL 将 API 平层字段 `time_slot_sale` 映射到 ODS 表的对应列
2. WHEN ETL_System 执行 store_goods_master 的 ODS 加载任务, THE Loader SHALL 将嵌套对象 `goodsStockWarningInfo` 的 4 个子字段site_goods_id、sales_day、warning_day_max、warning_day_min展开并映射到 ODS 表的对应列
3. WHEN ETL_System 执行 store_goods_master 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `batch_stock_quantity``provisional_total_cost` 以及展开后的库存预警字段映射到 DWD 目标表(根据字段用途自动分配到 `dim_store_goods``dim_store_goods_ex`
4. WHEN 新字段被添加, THE DDL SHALL 同步更新 `ods.sql``dwd.sql`
5. WHEN 字段映射完成后, THE BD_Manual SHALL 更新对应的字段说明文档
### 需求 11tenant_goods_master 字段补全
**用户故事:** 作为数据工程师,我希望将租户商品档案表中 1 个未映射的 ODS 字段commoditycode补全到 DWD 层。
#### 验收标准
1. WHEN ETL_System 执行 tenant_goods_master 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `commoditycode` 映射到 DWD 目标表 `dim_tenant_goods_ex` 的对应列
2. WHEN 新字段被添加到 DWD 表, THE DDL SHALL 在 `dwd.sql` 中包含对应的列定义
3. WHEN 字段映射完成后, THE BD_Manual SHALL 更新对应的字段说明文档
### 需求 12DWS 库存汇总(日/周/月)
**用户故事:** 作为数据分析师,我希望在 DWS 层拥有日度、周度、月度三个粒度的库存汇总表,以便按不同时间维度分析商品库存变化趋势。
#### 验收标准
1. WHEN 需求 7goods_stock_summary 新建 DWD 表)完成且 ODS 任务配置已修改(`requires_window=True` + `time_fields=("startTime", "endTime")`)并重新采集数据后, THE ETL_System SHALL 具备构建 DWS 库存汇总的数据基础
2. WHEN ETL_System 执行 DWS_GOODS_STOCK_DAILY 任务, THE ETL_System SHALL 从 DWD 层 `dwd_goods_stock_summary` 提取数据,按日粒度汇总并写入 `dws.dws_goods_stock_daily_summary`
3. WHEN ETL_System 执行 DWS_GOODS_STOCK_WEEKLY 任务, THE ETL_System SHALL 从 DWD 层提取数据,按周粒度汇总并写入 `dws.dws_goods_stock_weekly_summary`
4. WHEN ETL_System 执行 DWS_GOODS_STOCK_MONTHLY 任务, THE ETL_System SHALL 从 DWD 层提取数据,按月粒度汇总并写入 `dws.dws_goods_stock_monthly_summary`
5. THE DWS 库存汇总表 SHALL 包含以下字段site_id、tenant_id、stat_date汇总日期、site_goods_id、goods_name、goods_unit、goods_category_id、goods_category_second_id、category_name商品维度、range_start_stock、range_end_stock、range_in、range_out、range_sale、range_sale_money、range_inventory、current_stock库存指标、stat_period汇总粒度标识'daily'/'weekly'/'monthly'
6. THE DWS 库存汇总表 SHALL 以 `(site_id, stat_date, site_goods_id)` 为主键,支持按门店、日期、商品维度的唯一性约束
7. WHEN DWS 库存汇总任务执行时, THE ETL_System SHALL 继承 `BaseDwsTask`,实现 `extract` / `transform` / `load` 三阶段
8. WHEN DWS 库存汇总表创建后, THE DDL SHALL 在 `db/etl_feiqiu/schemas/dws.sql` 中包含建表语句,迁移脚本放在 `db/etl_feiqiu/migrations/`
9. WHEN DWS 库存汇总任务代码创建后, THE ETL_System SHALL 将任务代码放在 `apps/etl/connectors/feiqiu/tasks/dws/` 目录下
### 需求 17彻底移除 settlement_ticket_details
**用户故事:** 作为数据工程师,我希望从项目中彻底移除 settlement_ticket_details结账小票详情相关的所有代码、DDL、配置、文档和数据以便简化系统维护并消除无用的数据流。
#### 验收标准
1. WHEN 移除任务完成后, THE ETL_System SHALL 不再包含 `ODS_SETTLEMENT_TICKET` 任务代码(从 `ods_tasks.py``ENABLED_ODS_CODES``ODS_TASK_CLASSES``OdsSettlementTicketTask` 类中移除)
2. WHEN 移除任务完成后, THE DDL SHALL 不再包含 `ods.settlement_ticket_details` 表定义(从 `ods.sql` / `schema_ODS_doc.sql` 中移除建表语句和注释)
3. WHEN 移除任务完成后, THE ETL_System SHALL 从以下位置移除所有 settlement_ticket_details 引用:
- `tasks/ods/ods_tasks.py`OdsTaskSpec、OdsSettlementTicketTask 类、ENABLED_ODS_CODES
- `tasks/verification/dwd_verifier.py``tasks/verification/ods_verifier.py`
- `tasks/utility/manual_ingest_task.py`
- `utils/json_store.py`
- `scripts/check/check_ods_gaps.py`
- `scripts/debug/debug_blackbox.py`
4. WHEN 移除任务完成后, THE ETL_System SHALL 从 `db/etl_feiqiu/seeds/seed_ods_tasks.sql` 中移除 `ODS_SETTLEMENT_TICKET`
5. WHEN 移除任务完成后, THE BD_Manual SHALL 从 `docs/database/etl_feiqiu_schema_migration.md` 和 ETL 任务文档中移除相关条目
6. WHEN 移除任务完成后, THE ETL_System SHALL 编写迁移脚本 `DROP TABLE IF EXISTS ods.settlement_ticket_details``DROP INDEX IF EXISTS ods.idx_ods_settlement_ticket_details_latest`
7. WHEN 移除任务完成后, THE ETL_System SHALL 从 `scripts/ops/` 下的分析脚本(`gen_full_dataflow_doc.py``gen_field_review_doc.py``gen_api_field_mapping.py``field_audit.py``export_dwd_field_review.py``dataflow_analyzer.py``check_ods_latest_indexes.py`)中移除相关引用
8. WHEN 移除任务完成后, THE ETL_System SHALL 从单元测试 `tests/unit/test_ods_tasks.py` 中移除 `test_ods_settlement_ticket_by_payment_relate_ids` 测试
### 需求 13文档精化
**用户故事:** 作为数据工程师,我希望对所有涉及的 BD_Manual 文档进行精细化更新,消除所有模糊描述,以便团队成员可以准确理解每个字段的含义。
#### 验收标准
1. WHEN 文档精化任务执行时, THE BD_Manual SHALL 逐个文档、逐项排查所有"待补充""待处理""未确定""未定义"等缺失内容
2. WHEN 文档精化任务执行时, THE BD_Manual SHALL 将"金额字段""XX 相关""XXX 类"等粗略说明替换为精确的字段语义描述
3. WHEN 字段说明需要精化时, THE ETL_System SHALL 通过手动字段名称分析、上下文推测、遍历值/枚举值分析、代码取用情况分析来确定字段含义
4. WHEN 文档更新完成后, THE BD_Manual SHALL 确保每个字段说明包含:字段类型、业务含义、取值范围或枚举值、在代码中的使用位置
### 需求 14Admin-Web 前后端联调
**用户故事:** 作为系统管理员,我希望通过管理后台 Web 界面触发 ETL 全流程执行,以便可视化管理数据处理任务。
#### 验收标准
1. WHEN 管理员在 Admin_Web 中配置 ETL 参数全部门店、api_full、仅校验修复且校验前从 API 获取、自定义范围 2025-11-01 至 2026-02-20、窗口切分 10 天、force-full、全选常用功能, THE Backend SHALL 正确接收并解析这些参数
2. WHEN Backend 接收到 ETL 执行请求, THE Backend SHALL 将参数转换为 ETL_System 可识别的命令并触发执行
3. WHEN ETL 任务执行时, THE Admin_Web SHALL 实时展示任务执行状态和进度
4. WHEN 所有选中的任务执行完成后, THE ETL_System SHALL 确保数据处理结果正确(源数据与落库数据/字段一致)
### 需求 15ETL 执行计时机制
**用户故事:** 作为系统管理员,我希望 ETL 执行过程中有详细的计时记录,以便分析各步骤的性能瓶颈。
#### 验收标准
1. WHEN ETL 任务开始执行, THE ETL_System SHALL 启动计时器,记录每个步骤和分步骤的开始时间
2. WHEN 每个步骤完成时, THE ETL_System SHALL 记录该步骤的耗时(精确到毫秒)
3. WHEN 全部任务执行完成后, THE ETL_System SHALL 输出详细颗粒度的计时结果文档,包含每个步骤名称、开始时间、结束时间、耗时
### 需求 16黑盒测试机制
**用户故事:** 作为质量保证工程师,我希望在 ETL 全流程完成后执行黑盒测试,验证数据源与落库数据的一致性。
#### 验收标准
1. WHEN 所有 ETL 步骤顺利完成后, THE ETL_System SHALL 以黑盒测试者角度检查数据源和落库数据/字段情况是否一致
2. WHEN 黑盒测试执行时, THE ETL_System SHALL 对比 API 源数据与 ODS 落库数据的字段完整性
3. WHEN 黑盒测试执行时, THE ETL_System SHALL 对比 ODS 数据与 DWD 落库数据的映射正确性
4. WHEN 黑盒测试完成后, THE ETL_System SHALL 输出黑盒测试报告,包含每张表的检查结果、差异明细、通过/失败状态

View File

@@ -0,0 +1,304 @@
# 实现计划:数据流字段补全与前后端联调
## 概述
按"先排查确认 → 再 DDL 变更 → 再代码映射 → 再移除废弃表 → 再 DWS 汇总 → 再文档精化 → 最后联调"的顺序,分阶段推进。每个表的字段补全遵循"先确认再新增"原则。
执行依据:`export/SYSTEM/REPORTS/field_audit/field_review_for_user.md`(逐表逐字段排查结论)
## 任务
- [x] 1. 字段排查脚本与基础设施
- [x] 1.1 编写字段排查脚本 `scripts/ops/field_audit.py`
- 连接数据库,对每张目标表执行排查流程:查 DWD 现有列、查 FACT_MAPPINGS 现状、查 ODS 列、查自动映射
- 输出排查记录表markdown 格式),标注每个字段的排查结论和建议操作
- 覆盖 11 张表的所有疑似缺失字段
- _Requirements: 1.1-1.4, 2.1-2.3, 3.1-3.3, 4.1-4.3, 5.1-5.3, 6.1-6.4, 7.1-7.4, 8.1-8.4, 9.1-9.3, 10.1-10.5, 11.1-11.3_
- [x] 1.2 执行排查脚本,生成排查报告
- 运行脚本,审查输出结果
- _Requirements: 1.1-1.4_
- [x] 1.3 逐字段调查与推测,确认排查结论(由 Kiro 执行)
- 结论文档:`export/SYSTEM/REPORTS/field_audit/field_review_for_user.md`
- 脚本输出仅为线索,不能直接作为最终结论
- 对脚本标记为"缺失"或"对不齐"的每个字段Kiro 需逐一执行以下调查:
- 查阅 FACT_MAPPINGS 源码,确认是否已以其他名称/表达式映射
- 查阅 DWD DDL确认是否已有同语义列命名差异
- 查阅 ODS loader 代码,确认 ODS 列是否真实写入
- 结合字段命名规律、上下文语义、业务逻辑进行推测
- 必要时查询数据库实际数据SELECT DISTINCT / 采样)辅助判断
- 对每个字段标注最终决策:无需变更 / 仅补映射 / 新增列+映射 / 跳过(附理由)
- 将调查过程和推测依据记录到排查报告中,确保可追溯
- _Requirements: 1.1-1.4, 2.1-2.3, 3.1-3.3, 4.1-4.3, 5.1-5.3, 6.1-6.4, 7.1-7.4, 8.1-8.4, 9.1-9.3, 10.1-10.5, 11.1-11.3_
- [x] 2. Checkpoint - 排查结果确认
- 此项已完成,最终文档为:`export/SYSTEM/REPORTS/field_audit/field_review_for_user.md`
- [x] 3. 🔴 映射错误修复(高优先级)
- 依据:`field_review_for_user.md` 映射错误修复章节
- [x] 3.1 assistant_service_records — site_assistant_id 映射错误修正
- 当前问题DWD `dwd_assistant_service_log.site_assistant_id` 错误映射自 ODS `order_assistant_id`(订单级 ID应映射自 ODS `site_assistant_id`(助教档案 ID
- 修正 FACT_MAPPINGS将 DWD `site_assistant_id` 的 ODS 源从 `order_assistant_id` 改为 `site_assistant_id`
- 新增 DWD 列 `order_assistant_id`bigint`dwd_assistant_service_log`,映射 ODS `order_assistant_id`
- 编写迁移脚本
- 需重新加载历史数据
- _Requirements: 2.1, 2.2_
- [x] 3.2 store_goods_sales_records — discount_price 列名误导修正
- 当前问题DWD `discount_price` 实际映射自 ODS `discount_money`(折扣金额),而非 ODS `discount_price`(折后单价)
- 将 DWD 列名 `discount_price` 重命名为 `discount_money`
- 新增 DWD 列 `discount_price`numeric映射 ODS `discount_price`(折后单价)
- 编写迁移脚本
- _Requirements: 4.1, 4.2_
- [x] 3.3 store_goods_master — batch_stock_qty 和 provisional_total_cost 映射源修正
- `batch_stock_qty` 的 FACT_MAPPINGS 从 `stock`(当前库存)改为 `batch_stock_quantity`(批次库存)
- `provisional_total_cost` 的 FACT_MAPPINGS 从 `total_purchase_cost`(实际采购成本)改为 `provisional_total_cost`(暂估成本)
- 需重新加载历史数据
- _Requirements: 10.3_
- [x] 4. A 类表:新增 DWD 列 + FACT_MAPPINGS
- 依据:`field_review_for_user.md` 各表"待新增/补映射字段"
- [x] 4.1 assistant_accounts_master — 新增 4 个字段到 dim_assistant_ex
- 新增列:`system_role_id`bigint`job_num`text`cx_unit_price`numeric(18,2))、`pd_unit_price`numeric(18,2)
- 更新 `dwd.sql`,添加 FACT_MAPPINGS 条目
- 编写迁移脚本
- _Requirements: 1.1, 1.2, 1.3_
- [x] 4.2 assistant_service_records — 新增 2 个字段到 dwd_assistant_service_log_ex
- 新增列:`operator_id`bigint`operator_name`text
- 跳过 `siteprofile`jsonb 嵌套列)
- 更新 `dwd.sql`,添加 FACT_MAPPINGS 条目
- 编写迁移脚本
- _Requirements: 2.1, 2.2_
- [x] 4.3 assistant_cancellation_records — 更新 FACT_MAPPINGS
- 更新映射ODS `assistanton` → DWD `dwd_assistant_trash_event.assistant_no`
- 跳过 `siteprofile`jsonb 嵌套列)
- _Requirements: 3.1_
- [x] 4.4 member_balance_changes — 新增 1 个字段到 dwd_member_balance_change_ex
- 新增列:`relate_id`bigint— 关联业务单据 ID
- 更新 `dwd.sql`,添加 FACT_MAPPINGS 条目
- 编写迁移脚本
- _Requirements: 5.1, 5.2_
- [x] 4.5 site_tables_master — 新增 14 个字段到 dim_table_ex
- 新增列:`create_time`timestamptz`light_status`integer`tablestatusname`text`sitename`text`appletQrCodeUrl`text`audit_status`integer`charge_free`integer`delay_lights_time`integer`is_rest_area`integer`only_allow_groupon`integer`order_delay_time`integer`self_table`integer`temporary_light_second`integer`virtual_table`integer
- 更新 `dwd.sql`,添加 FACT_MAPPINGS 条目
- 编写迁移脚本
- _Requirements: 9.1, 9.2_
- [x] 4.6 tenant_goods_master — 无需变更(跳过)
- `commoditycode``commodity_code` 100% 冗余(花括号包裹格式),已确认跳过
- _Requirements: 11.1已确认无需操作_
- [x] 4.7 编写 A 类表字段映射属性测试
- **Property 1: FACT_MAPPINGS 字段映射正确性**
- **Validates: Requirements 1.1, 1.2, 2.1, 3.1, 4.1, 5.1, 9.1**
- [x] 5. B 类表:仅补 FACT_MAPPINGS / 修正映射
- 依据:`field_review_for_user.md` B 类表章节
- [x] 5.1 recharge_settlements — 补充 5 个 FACT_MAPPINGS 条目
- 仅补映射DWD 列已存在ODS/DWD 两侧数据全为 0业务未启用
- `plcouponsaleamount → pl_coupon_sale_amount`
- `mervousalesamount → mervou_sales_amount`
- `electricitymoney → electricity_money`
- `realelectricitymoney → real_electricity_money`
- `electricityadjustmoney → electricity_adjust_money`
- 无需 DDL 变更,无需迁移脚本
- _Requirements: 6.1, 6.2, 6.3_
- [x] 5.2 编写 B 类表属性测试
- **Property 2: FACT_MAPPINGS 引用完整性**
- **Validates: Requirements 6.3**
- [x] 5.5. Checkpoint - 映射修复与 A/B 类表完成确认
- 确保所有映射错误已修正、A/B 类表的 FACT_MAPPINGS、DDL、迁移脚本都已更新
- Ask the user if questions arise.
- [x] 6. C 类表:新建 DWD 表与完整映射
- 依据:`field_review_for_user.md` C 类表章节
- [x] 6.1 goods_stock_summary — 修改 ODS 配置 + 新建 DWD 表
- 步骤 1修改 ODS 任务配置 `requires_window=True` + `time_fields=("startTime", "endTime")`
- 步骤 2重新采集历史数据按时间窗口分批
- 步骤 3编写 DDL 创建 `dwd.dwd_goods_stock_summary`14 个字段)
- 步骤 4`TABLE_MAP` 中注册,在 `FACT_MAPPINGS` 中添加映射
- 步骤 5创建 DWD loader 和 task 代码
- 编写迁移脚本
- _Requirements: 7.1, 7.2, 7.3_
- [x] 6.2 goods_stock_movements — 新建 DWD 表
- 编写 DDL 创建 `dwd.dwd_goods_stock_movement`19 个字段,事实表,按 createtime 增量加载)
-`TABLE_MAP` 中注册,在 `FACT_MAPPINGS` 中添加映射(驼峰 → 蛇形命名)
- 创建 DWD loader 和 task 代码
- 编写迁移脚本
- _Requirements: 8.1, 8.2, 8.3_
- [x] 6.3 编写 C 类表属性测试
- **Property 3: TABLE_MAP 覆盖完整性**
- **Validates: Requirements 7.2, 8.2**
- [x] 7. Checkpoint - 全部字段补全完成
- 确保所有 11 张表的字段补全工作完成所有测试通过ask the user if questions arise.
- [x] 7.3. 彻底移除 settlement_ticket_details
- [x] 7.3.1 移除 ETL 核心代码中的 settlement_ticket_details
-`tasks/ods/ods_tasks.py` 中移除:`OdsTaskSpec("ODS_SETTLEMENT_TICKET", ...)``OdsSettlementTicketTask` 类、`ENABLED_ODS_CODES` 中的条目、`ODS_TASK_CLASSES` 覆盖
-`tasks/verification/dwd_verifier.py` 中移除主键映射条目
-`tasks/verification/ods_verifier.py` 中移除相关注释和特殊处理
-`tasks/utility/manual_ingest_task.py` 中移除表映射和配置
-`utils/json_store.py` 中移除 `/order/getordersettleticketnew` 路径映射
- _Requirements: 17.1, 17.3_
- [x] 7.3.2 移除 DDL、种子数据和迁移脚本
-`db/etl_feiqiu/schemas/ods.sql``schema_ODS_doc.sql` 中移除建表语句和注释
-`db/etl_feiqiu/seeds/seed_ods_tasks.sql` 中移除 `ODS_SETTLEMENT_TICKET`
- 编写迁移脚本:`DROP TABLE IF EXISTS ods.settlement_ticket_details``DROP INDEX``DELETE FROM meta.ods_task_registry`
- _Requirements: 17.2, 17.4, 17.6_
- [x] 7.3.3 移除分析脚本和工具中的引用
-`scripts/ops/` 下移除:`gen_full_dataflow_doc.py``gen_field_review_doc.py``gen_api_field_mapping.py``field_audit.py``export_dwd_field_review.py``dataflow_analyzer.py``check_ods_latest_indexes.py` 中的相关引用
-`scripts/check/check_ods_gaps.py` 中移除 `_check_settlement_tickets` 函数及调用
-`scripts/debug/debug_blackbox.py` 中移除跳过逻辑
- _Requirements: 17.7_
- [x] 7.3.4 移除文档和测试中的引用
-`docs/database/etl_feiqiu_schema_migration.md` 中移除索引条目
-`apps/etl/connectors/feiqiu/docs/etl_tasks/` 中移除任务表格条目
-`tests/unit/test_ods_tasks.py` 中移除 `test_ods_settlement_ticket_by_payment_relate_ids`
- _Requirements: 17.5, 17.8_
- [x] 7.5. DWS 库存汇总(日/周/月)
- 前置依赖:任务 6.1goods_stock_summary DWD 表完成、ODS 任务配置修改(`requires_window=True`)完成并重新采集数据
- [x] 7.5.1 编写 DWS 库存汇总 DDL 与迁移脚本
-`db/etl_feiqiu/schemas/dws.sql` 中添加三张表的建表语句:`dws_goods_stock_daily_summary``dws_goods_stock_weekly_summary``dws_goods_stock_monthly_summary`
- 编写迁移脚本 `db/etl_feiqiu/migrations/{date}__create_dws_goods_stock_summary.sql`
- 主键:`(site_id, stat_date, site_goods_id)`
- 字段site_id, tenant_id, stat_date, site_goods_id, goods_name, goods_unit, goods_category_id, goods_category_second_id, category_name, range_start_stock, range_end_stock, range_in, range_out, range_sale, range_sale_money, range_inventory, current_stock, stat_period, created_at, updated_at
- _Requirements: 12.5, 12.6, 12.8_
- [x] 7.5.2 实现 DWS_GOODS_STOCK_DAILY 任务
-`apps/etl/connectors/feiqiu/tasks/dws/` 下创建 `goods_stock_daily_task.py`
- 继承 `BaseDwsTask`,实现 `extract` / `transform` / `load` 三阶段
- extract`dwd.dwd_goods_stock_summary` 按时间范围查询
- transform按日粒度汇总`stat_period='daily'`
- loadupsert 写入 `dws.dws_goods_stock_daily_summary`
- _Requirements: 12.2, 12.7_
- [x] 7.5.3 实现 DWS_GOODS_STOCK_WEEKLY 任务
- 创建 `goods_stock_weekly_task.py`,按 ISO 周分组,`stat_date` = 周一日期,`stat_period='weekly'`
- _Requirements: 12.3, 12.7_
- [x] 7.5.4 实现 DWS_GOODS_STOCK_MONTHLY 任务
- 创建 `goods_stock_monthly_task.py`,按自然月分组,`stat_date` = 月首日期,`stat_period='monthly'`
- _Requirements: 12.4, 12.7_
- [x] 7.5.5 注册 DWS 库存汇总任务到任务调度
- 在任务注册表中添加 `DWS_GOODS_STOCK_DAILY``DWS_GOODS_STOCK_WEEKLY``DWS_GOODS_STOCK_MONTHLY`
- 确保任务依赖关系正确(依赖 DWD goods_stock_summary 加载完成)
- _Requirements: 12.9_
- [x] 7.5.6 编写 DWS 库存汇总属性测试
- **Property 8: DWS 库存汇总粒度聚合正确性**
- **Validates: Requirements 12.2, 12.3, 12.4, 12.5, 12.6**
- [x] 8. 文档精化
- [x] 8.1 精化 A/B/C 类表涉及的 BD_Manual 文档
- 逐个文档、逐项排查所有"待补充""待处理""未确定""未定义"等缺失内容
- 将"金额字段""XX 相关""XXX 类"等粗略说明替换为精确的字段语义描述
- 通过字段名称分析、上下文推测、遍历值/枚举值分析、代码取用情况分析确定字段含义
- 确保每个字段说明包含:字段类型、业务含义、取值范围或枚举值、在代码中的使用位置
- _Requirements: 13.1, 13.2, 13.3, 13.4_
- [x] 8.2 更新 dataflow 分析报告中涉及表的字段说明
- 同步更新 `docs/database/` 下对应的文档
- _Requirements: 1.4, 2.3, 3.3, 4.3, 5.3, 6.4, 7.4, 8.4, 9.3, 10.5, 11.3_
- [x] 9. Checkpoint - 文档精化完成
- 确保所有文档更新完毕,无遗留的"待补充"标记ask the user if questions arise.
- [x] 10. Admin-Web 前后端联调
- [x] 10.1 排查并修复后端 ETL 执行 API
- 检查 `apps/backend/app/routers/` 中 ETL 执行相关路由
- 确保参数解析正确全部门店、api_full、仅校验修复且校验前从 API 获取、自定义范围 2025-11-01 至 2026-02-20、窗口切分 10 天、force-full、全选常用功能
- 确保参数正确转换为 ETL CLI 命令
- _Requirements: 14.1, 14.2_
- [x] 10.2 排查并修复前端 TaskManager 页面
- 检查 `apps/admin-web/src/pages/TaskManager.tsx` 中的参数配置表单
- 确保所有参数选项可正确选择和提交
- 确保任务执行状态实时展示WebSocket 日志流)
- _Requirements: 14.3_
- [x] 10.3 实现 ETL 执行计时器模块
-`apps/etl/connectors/feiqiu/utils/` 中新增计时器工具
- 记录每个步骤和分步骤的开始时间、结束时间、耗时(精确到毫秒)
- 全部任务完成后输出计时结果文档
- _Requirements: 15.1, 15.2, 15.3_
- [x] 10.4 编写计时器属性测试
- **Property 7: 计时器记录完整性**
- **Validates: Requirements 15.2**
- [x] 10.5 编写 ETL 参数解析属性测试
- **Property 5: ETL 参数解析与 CLI 命令构建正确性**
- **Validates: Requirements 14.1, 14.2**
- [x] 11. 黑盒测试机制
- [x] 11.1 实现数据一致性检查器
-`apps/etl/connectors/feiqiu/quality/` 中新增一致性检查模块
- 实现 API 源数据 vs ODS 落库数据的字段完整性对比
- 实现 ODS 数据 vs DWD 落库数据的映射正确性对比
- 输出黑盒测试报告(每张表的检查结果、差异明细、通过/失败状态)
- _Requirements: 16.1, 16.2, 16.3, 16.4_
- [x] 11.2 编写数据一致性检查属性测试
- **Property 6: 数据一致性检查正确性**
- **Validates: Requirements 16.2, 16.3**
- [x] 12. 端到端联调验证
- [x] 12.1 执行完整 ETL 流程并验证数据正确性
-`EtlTimer` 集成到 `orchestration/flow_runner.py``FlowRunner.run()` 方法中
- 增量 ETL 和校验分支均包裹计时步骤(`INCREMENT_ETL``VERIFICATION``FETCH_BEFORE_VERIFY`
- `timer.finish(write_report=True)` 在 Flow 结束时自动输出计时报告到 `ETL_REPORT_ROOT`
- 产出物:`export/ETL-Connectors/feiqiu/REPORTS/etl_timing_*.md`
- _Requirements: 14.4, 15.3_
- [x] 12.2 执行黑盒测试并生成报告
-`ConsistencyChecker` 集成到 `FlowRunner.run()``_run_post_consistency_check()` 方法中
- ETL Flow 完成后自动运行一致性检查API vs ODS + ODS vs DWD输出报告到 `ETL_REPORT_ROOT`
- 独立验证脚本 `scripts/ops/run_post_etl_reports.py` 确认报告生成正常
- 实际执行结果API vs ODS 22/22 通过ODS vs DWD 38/42 通过
- 产出物:`export/ETL-Connectors/feiqiu/REPORTS/consistency_report_*.md`
- _Requirements: 16.1, 16.4_
- [x] 12.3 前后端浏览器联调验证2026-02-20
- 启动后端 `uvicorn app.main:app --reload`localhost:8000+ 前端 `pnpm dev`localhost:5173
- 浏览器登录管理后台,进入"任务配置"页面
- 配置Flow=ods_dwd, 处理模式=仅增量, dry-run=✓, 本地JSON=✓, 回溯24h
- 点击"直接执行"→ 自动跳转"任务管理 > 历史"tab
- 验证结果status=success, duration=22.5s, exit_code=0
- CLI 命令正确构建:`python -m cli.main --flow ods_dwd --processing-mode increment_only --tasks DWD_LOAD_FROM_ODS --lookback-hours 24 --overlap-seconds 600 --dry-run --data-source offline --store-id 2790685415443269`
- 执行日志实时推送到前端 ModalWebSocket /ws/logs/{id})✅
- 计时报告自动生成:`etl_timing_20260220_073610.md`2步骤总耗时20.78s)✅
- 一致性检查报告自动生成:`consistency_report_20260220_073610.md`
- _Requirements: 14.1, 14.2, 14.3, 14.4, 15.3, 16.4_
- [x] 13. Final checkpoint - 全部完成
- 确保所有字段补全、文档精化、前后端联调、黑盒测试均已完成ask the user if questions arise.
## 备注
- 标记 `*` 的任务为可选,可跳过以加速 MVP
- 每个任务引用具体需求编号以确保可追溯
- Checkpoint 确保增量验证
- 属性测试验证通用正确性属性,单元测试验证具体示例和边界情况
- 所有涉及 `loaders/``tasks/``db/` 的变更属于高风险路径,完成后需触发 `/audit`

View File

@@ -10,7 +10,7 @@
5. 将数据采集脚本任务化,支持 CLI 参数(日期范围、条数),落盘到 `SYSTEM_ANALYZE_ROOT` 目录
6. 全流程通过 Kiro Hook 手动触发Python 脚本负责机械性数据准备Kiro Agent 负责语义分析和报告编排
现有脚本输出样本参见 `docs/reports/dataflow_api_ods_dwd.md`
现有脚本输出样本参见 `export/SYSTEM/REPORTS/full_dataflow_doc/dataflow_api_ods_dwd.md`
## 术语表

View File

@@ -84,7 +84,7 @@
- 使用 `hypothesis` 生成随机列集合和窗口参数,通过 `FakeCursor` 捕获 SQL验证 INSERT INTO ... ON CONFLICT 结构与预期一致
- 文件:`tests/test_dwd_phase1_properties.py`
- [~] 7. 最终检查点 - 确保所有测试通过
- [x] 7. 最终检查点 - 确保所有测试通过
- 运行 `cd apps/etl/pipelines/feiqiu && pytest tests/unit``cd C:\NeoZQYY && pytest tests/ -v`,确保所有测试通过,如有问题请询问用户。
## 备注

View File

@@ -0,0 +1 @@
{"generationMode": "requirements-first"}

View File

@@ -0,0 +1,464 @@
# 设计文档
## 概述
本设计覆盖 v8 联调中 4 个"临时止血"修复的深度方案,按优先级排列:
1. **需求 DP0**`DwdLoadTask.load()` 返回值格式规范化
2. **需求 C1P1**:会员生日字段 ETL 链路补齐
3. **需求 BP1**:多门店会员查询支持
4. **需求 AP2**:助教月度聚合按档位分段统计
5. **需求 C2P2**:助教手动补录会员生日
设计原则:
- 每个需求独立可部署,按优先级逐步实施
- DDL 变更通过迁移脚本执行,支持回滚
- 保持现有 ETL 架构BaseTask E/T/L 模板)不变
## 架构
整体架构不变,变更集中在以下层面:
```mermaid
graph TD
subgraph "需求 D: 返回值规范化"
D1[DwdLoadTask.load] -->|errors: int| D2[BaseTask._accumulate_counts]
D2 -->|sum| D3[FlowRunner._safe_int]
end
subgraph "需求 A: 档位分段统计"
A1[dws_assistant_daily_detail] -->|GROUP BY level_code| A2[AssistantMonthlyTask]
A2 -->|多行/档位| A3[dws_assistant_monthly_summary]
A3 --> A4[AssistantSalaryTask 分段计算]
end
subgraph "需求 B: 多门店会员查询"
B1[dwd.事实表] -->|member_id IN| B2[dim_member]
B2 --> B3[DWS 任务]
end
subgraph "需求 C: 生日字段"
C1[ODS payload] -->|birthday 提取| C2[dim_member.birthday]
C3[后端 API] -->|UPSERT| C4[zqyy_app.member_birthday_manual]
C2 --> C5[DWS 任务: COALESCE]
C4 -->|FDW 只读| C5
end
```
## 组件与接口
### 需求 D返回值格式规范化
**变更文件:**
- `apps/etl/connectors/feiqiu/tasks/dwd/dwd_load_task.py`
- `apps/etl/connectors/feiqiu/tasks/base_task.py`
**DwdLoadTask.load() 返回值变更:**
```python
# 变更前
return {"tables": summary, "errors": errors}
# errors: list[dict],如 [{"table": "dim_assistant_ex", "error": "..."}]
# 变更后
return {
"tables": summary,
"errors": len(errors), # int — 与其他任务一致
"error_details": errors, # list[dict] — 保留详情供日志使用
}
```
**BaseTask._accumulate_counts() 防御层增强:**
```python
@staticmethod
def _accumulate_counts(total: dict, current: dict) -> dict:
for key, value in (current or {}).items():
if isinstance(value, (int, float)):
total[key] = (total.get(key) or 0) + value
elif isinstance(value, list):
# 防御层list 类型转为 len() 累加
total[key] = (total.get(key) or 0) + len(value)
else:
total.setdefault(key, value)
return total
```
**FlowRunner._safe_int() 保留不变**,作为最终防御层。
### 需求 A助教月度聚合按档位分段统计
**DDL 变更:**
```sql
-- 迁移脚本:删除旧唯一约束,创建新约束
ALTER TABLE dws.dws_assistant_monthly_summary
DROP CONSTRAINT IF EXISTS uk_dws_assistant_monthly;
ALTER TABLE dws.dws_assistant_monthly_summary
ADD CONSTRAINT uk_dws_assistant_monthly
UNIQUE (site_id, assistant_id, stat_month, assistant_level_code);
```
**AssistantMonthlyTask._extract_daily_aggregates() 变更:**
```sql
-- 变更前GROUP BY assistant_id, DATE_TRUNC('month', stat_date)
-- 变更后:加入 assistant_level_code 分组
SELECT
assistant_id,
assistant_level_code,
assistant_level_name,
-- nickname 取时间最后一条
(ARRAY_AGG(assistant_nickname ORDER BY stat_date DESC))[1] AS assistant_nickname,
DATE_TRUNC('month', stat_date)::DATE AS stat_month,
COUNT(DISTINCT stat_date) AS work_days,
SUM(total_service_count) AS total_service_count,
-- ... 其余聚合字段不变
FROM dws.dws_assistant_daily_detail
WHERE site_id = %s AND ({month_where})
GROUP BY assistant_id, assistant_level_code, assistant_level_name,
DATE_TRUNC('month', stat_date)
```
**AssistantSalaryTask 适配:**
- `_extract_monthly_summary()` 返回多行(同一助教不同档位)
- `transform()` 遍历每行分别计算工资,按档位使用对应的 `level_price``tier`
- 最终每个 `(assistant_id, stat_month, assistant_level_code)` 生成一条工资记录
**AssistantFinanceTask._extract_daily_revenue() nickname 修复:**
```sql
-- 变更前MAX(s.nickname) AS assistant_nickname
-- 变更后:
(ARRAY_AGG(s.nickname ORDER BY s.start_use_time DESC))[1] AS assistant_nickname
```
**AssistantCustomerTask._extract_service_pairs() nickname 修复:**
```sql
-- 变更前MAX(assistant_nickname) AS assistant_nickname
-- 变更后:
(ARRAY_AGG(assistant_nickname ORDER BY service_date DESC))[1] AS assistant_nickname
```
### 需求 B多门店会员查询支持
**变更模式:** 所有 `_extract_member_info(site_id)` 方法的 SQL 从:
```sql
WHERE register_site_id = %s AND scd2_is_current = 1
```
改为通过事实表反查:
```sql
WHERE member_id IN (
SELECT DISTINCT tenant_member_id
FROM dwd.{}
WHERE site_id = %s AND tenant_member_id IS NOT NULL AND tenant_member_id != 0
) AND scd2_is_current = 1
```
**受影响的任务和对应事实表:**
| 任务 | 方法 | 事实表 |
|------|------|--------|
| `member_visit_task.py` | `_extract_member_info` | `dwd_settlement_head` |
| `member_consumption_task.py` | `_extract_member_info` | `dwd_settlement_head` |
| `assistant_customer_task.py` | `_extract_member_info` | `dwd_assistant_service_log` |
**`dim_member_card_account` 的处理:**
- `member_consumption_task.py``finance_recharge_task.py` 中对 `dim_member_card_account` 的查询也使用 `register_site_id`
- 同样改为通过事实表的 `tenant_member_id` 反查:
```sql
WHERE tenant_member_id IN (
SELECT DISTINCT tenant_member_id
FROM dwd.{}
WHERE site_id = %s AND tenant_member_id IS NOT NULL AND tenant_member_id != 0
) AND scd2_is_current = 1
```
### 需求 C1会员生日字段 ETL 链路补齐
**DDL 变更:**
```sql
-- dim_member 加列
ALTER TABLE dwd.dim_member ADD COLUMN IF NOT EXISTS birthday DATE;
COMMENT ON COLUMN dwd.dim_member.birthday IS '会员生日来源ODS member_profiles payload 中的 birthday 字段';
```
**ODS → DWD 装载:**
- `DwdLoadTask` 的列映射是自动的(通过 `_get_columns()` 读取 DWD 表列名,与 ODS 列名匹配)
- ODS `member_profiles` 表没有 `birthday` 列,但 `payload` JSONB 中可能包含
- 需要在 `_build_column_mapping()``_fetch_source_rows()` 中增加从 `payload` 提取 `birthday` 的逻辑
- 方案:在 ODS 表也加 `birthday` 列(保持 ODS 与 API 字段对齐ODS 入库时从 JSON 提取
```sql
-- ODS member_profiles 加列
ALTER TABLE ods.member_profiles ADD COLUMN IF NOT EXISTS birthday DATE;
```
ODS 入库逻辑(`ods_tasks.py`)已有从 JSON 提取字段的机制,新增 `birthday` 字段映射即可。DwdLoadTask 的自动列匹配会自动将 `ods.member_profiles.birthday` 映射到 `dwd.dim_member.birthday`
**SCD2 处理:**
- `birthday` 作为 `dim_member` 的普通维度列SCD2 变化检测会自动包含
- 当 API 返回的 birthday 值变化时,会触发 SCD2 版本更新
**DWS 任务恢复 birthday 引用:**
- `member_visit_task.py``_extract_member_info()` SQL 中加入 `birthday`
- `member_consumption_task.py` 同理
### 需求 C2助教手动补录会员生日
**DDL`zqyy_app` / `test_zqyy_app` 业务库):**
```sql
CREATE TABLE IF NOT EXISTS member_birthday_manual (
id BIGSERIAL PRIMARY KEY,
member_id BIGINT NOT NULL,
birthday_value DATE NOT NULL,
recorded_by_assistant_id BIGINT,
recorded_by_name VARCHAR(50),
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
source VARCHAR(20) DEFAULT 'assistant',
CONSTRAINT uk_member_birthday_manual
UNIQUE (member_id, recorded_by_assistant_id)
);
COMMENT ON TABLE member_birthday_manual IS '助教手动补录的会员生日信息';
CREATE INDEX idx_mbd_member ON member_birthday_manual (member_id);
```
**FDW 映射ETL 库读取业务库数据):**
当前 FDW 方向是 `zqyy_app``etl_feiqiu`(业务库读 ETL 数据)。需求 C2 需要反向ETL DWS 任务读取业务库的手动补录表。
方案:在 `etl_feiqiu` 库中创建指向 `zqyy_app` 的 FDW 外部表:
```sql
-- 在 etl_feiqiu 中执行
CREATE SERVER IF NOT EXISTS zqyy_app_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', dbname 'zqyy_app', port '5432');
CREATE USER MAPPING IF NOT EXISTS FOR etl_user
SERVER zqyy_app_server
OPTIONS (user 'app_reader', password '***');
CREATE SCHEMA IF NOT EXISTS fdw_app;
CREATE FOREIGN TABLE fdw_app.member_birthday_manual (
id BIGINT,
member_id BIGINT,
birthday_value DATE,
recorded_by_assistant_id BIGINT,
recorded_by_name VARCHAR(50),
recorded_at TIMESTAMPTZ,
source VARCHAR(20)
) SERVER zqyy_app_server
OPTIONS (schema_name 'public', table_name 'member_birthday_manual');
```
**DWS 任务生日读取逻辑:**
```sql
-- 优先手动补录值,其次 API 值
COALESCE(
(SELECT birthday_value
FROM fdw_app.member_birthday_manual
WHERE member_id = m.member_id
ORDER BY recorded_at ASC -- 最早提交优先
LIMIT 1),
m.birthday
) AS member_birthday
```
**后端 API**
```python
# apps/backend/app/routers/member_birthday.py
@router.post("/member-birthday")
async def submit_member_birthday(
member_id: int,
birthday_value: date,
assistant_id: int,
assistant_name: str,
db=Depends(get_db),
):
"""助教提交会员生日UPSERT"""
sql = """
INSERT INTO member_birthday_manual
(member_id, birthday_value, recorded_by_assistant_id, recorded_by_name)
VALUES (%s, %s, %s, %s)
ON CONFLICT (member_id, recorded_by_assistant_id)
DO UPDATE SET
birthday_value = EXCLUDED.birthday_value,
recorded_at = NOW()
"""
db.execute(sql, (member_id, birthday_value, assistant_id, assistant_name))
return {"status": "ok"}
```
## 数据模型
### 变更汇总
| 库 | 表 | 变更类型 | 说明 |
|----|-----|---------|------|
| `etl_feiqiu` | `ods.member_profiles` | 加列 | `birthday DATE` |
| `etl_feiqiu` | `dwd.dim_member` | 加列 | `birthday DATE` |
| `etl_feiqiu` | `dws.dws_assistant_monthly_summary` | 改约束 | UK 加入 `assistant_level_code` |
| `zqyy_app` | `member_birthday_manual` | 新建表 | 手动补录生日 |
| `etl_feiqiu` | `fdw_app.member_birthday_manual` | 新建外部表 | FDW 映射 |
### 迁移脚本清单
按优先级排序,每个迁移脚本独立可执行:
1. `2026-02-22__D_dwd_load_return_format.sql` — 无 DDL纯代码变更
2. `2026-02-22__C1_dim_member_add_birthday.sql` — ODS/DWD 加列
3. `2026-02-22__B_no_ddl_code_only.sql` — 无 DDL纯代码变更
4. `2026-02-22__A_monthly_summary_uk_change.sql` — 唯一约束变更
5. `2026-02-22__C2_member_birthday_manual.sql` — 新建表 + FDW
## 正确性属性
*属性Property是系统在所有合法执行中都应保持为真的特征或行为——本质上是对"系统应该做什么"的形式化陈述。属性是连接人类可读规格说明与机器可验证正确性保证之间的桥梁。*
### Property 1: DwdLoadTask 返回值格式一致性
*对于任意* DwdLoadTask.load() 的执行结果,返回字典中 `errors` 键的值应为 `int` 类型,且等于 `error_details` 列表的长度。
**验证: 需求 1.1**
### Property 2: _accumulate_counts 类型安全累加
*对于任意* 包含 `int``float``list` 类型值的计数字典,`_accumulate_counts()` 应将 `int`/`float` 直接累加,将 `list` 转为 `len()` 后累加,且不抛出异常。
**验证: 需求 1.2**
### Property 3: 档位分段聚合正确性
*对于任意* 助教在同一月内存在 N 个不同 `assistant_level_code` 的日度数据,`_extract_daily_aggregates()` 应返回恰好 N 行记录,每行的业绩指标之和应等于该助教该月的总业绩。
**验证: 需求 2.1**
### Property 4: nickname 按时间倒序取值
*对于任意* 助教在聚合周期内有多条不同 nickname 的记录,聚合结果中的 nickname 应等于时间最晚的那条记录的 nickname。此属性适用于 AssistantMonthlyTask、AssistantFinanceTask、AssistantCustomerTask 三个任务。
**验证: 需求 2.3, 2.5, 2.6**
### Property 5: 工资按档位分段计算
*对于任意* 助教在同一月有多个档位的月度汇总记录AssistantSalaryTask 应为每个档位分别计算工资,每个档位使用对应的 `level_price``tier` 配置,且所有档位的工资记录数等于月度汇总的行数。
**验证: 需求 2.4**
### Property 6: 跨店会员可查
*对于任意* 在 A 店注册但在 B 店有消费记录的会员B 店的 DWS 任务通过事实表反查 `dim_member`应能获取到该会员的维度信息nickname、mobile 等)。
**验证: 需求 3.1, 3.2**
### Property 7: birthday ODS→DWD 装载正确性
*对于任意* ODS `member_profiles` 中包含 `birthday` 值的记录DwdLoadTask 装载后 `dwd.dim_member` 中对应记录的 `birthday` 应与 ODS 源值一致。
**验证: 需求 4.2**
### Property 8: birthday SCD2 变化检测
*对于任意* `dim_member` 现有记录,当 ODS 中同一会员的 `birthday` 值发生变化时SCD2 应关闭旧版本并创建新版本,新版本的 `birthday` 等于新值。
**验证: 需求 4.3**
### Property 9: 生日 UPSERT 幂等性
*对于任意* `(member_id, assistant_id)` 组合,连续两次提交不同的 `birthday_value``member_birthday_manual` 表中该组合应只有一条记录,且 `birthday_value` 等于最后一次提交的值。
**验证: 需求 5.2, 5.5**
### Property 10: 手动补录优先于 API 来源
*对于任意* 同时在 `dim_member.birthday``member_birthday_manual` 中有值的会员DWS 任务输出的 `member_birthday` 应等于手动补录表中的值。
**验证: 需求 5.4**
### Property 11: SCD2 更新不影响手动补录表
*对于任意*`member_birthday_manual` 中有记录的会员,执行 DwdLoadTask SCD2 更新 `dim_member.birthday` 后,`member_birthday_manual` 中的记录应保持不变。
**验证: 需求 5.6**
## 错误处理
### 需求 D返回值格式
- `DwdLoadTask.load()` 中单表装载失败时,错误信息追加到 `error_details` 列表,`errors` 计数递增
- `_accumulate_counts()` 遇到未知类型时使用 `setdefault` 保留原值(现有行为不变)
- `_safe_int()` 遇到非 int/list 类型时返回 0现有行为不变
### 需求 A档位分段
- 助教在某月无任何服务记录时,不生成月度汇总行(现有行为不变)
- `assistant_level_code` 为 NULL 时作为独立分组处理NULL 视为一个档位)
- 唯一约束变更后需要清理旧数据DELETE + 重新计算当月数据)
### 需求 B多门店查询
- 事实表中无该门店消费记录时,`_extract_member_info()` 返回空字典(现有行为不变)
- 子查询返回空集时,`WHERE member_id IN (空集)` 等价于 `WHERE FALSE`,不会报错
### 需求 C1生日字段
- ODS 中 `birthday` 为 NULL 或空字符串时DWD 中存为 NULL
- 无效日期格式时DwdLoadTask 的现有类型转换逻辑会将其置为 NULL
### 需求 C2手动补录
- `member_id` 不存在于 `dim_member` 时,仍允许提交(助教可能先于 ETL 发现新客户)
- `birthday_value` 格式校验由后端 API 的 Pydantic schema 处理
- FDW 连接失败时DWS 任务应 catch 异常并降级为仅使用 `dim_member.birthday`
## 测试策略
### 测试框架
- 属性测试:`hypothesis`Python每个属性测试最少 100 次迭代
- 单元测试:`pytest`
- 测试工具:`apps/etl/connectors/feiqiu/tests/unit/task_test_utils.py` 提供 FakeDB/FakeAPI
### 属性测试
每个正确性属性对应一个 hypothesis 属性测试,标注格式:
```python
# Feature: etl-aggregation-fix, Property N: {property_text}
@given(...)
def test_property_N_xxx(data):
...
```
属性测试放置位置:
- 需求 D 相关Property 1-2`apps/etl/connectors/feiqiu/tests/unit/test_return_format_properties.py`
- 需求 A 相关Property 3-5`apps/etl/connectors/feiqiu/tests/unit/test_monthly_aggregation_properties.py`
- 需求 B 相关Property 6`apps/etl/connectors/feiqiu/tests/unit/test_multi_store_properties.py`
- 需求 C 相关Property 7-11`apps/etl/connectors/feiqiu/tests/unit/test_birthday_properties.py`
### 单元测试
单元测试覆盖具体示例和边界情况:
- DDL 结构验证(唯一约束、列存在性)
- 空数据 / NULL 值边界
- 迁移脚本的回滚验证
### 测试环境
- 数据库:`test_etl_feiqiu` / `test_zqyy_app`(通过 `TEST_DB_DSN` 环境变量)
- 纯单元测试使用 FakeDB/FakeAPI不涉及真实数据库连接
- ETL 测试 cwd`apps/etl/connectors/feiqiu/`
- 后端测试 cwd`apps/backend/`

View File

@@ -0,0 +1,84 @@
# 需求文档
## 简介
v8 联调修复了 11 个 BUG其中 4 个的当前修复方式是"临时止血",需要更完整的方案。本 Spec 覆盖以下四个需求:
- **需求 A**:助教月度聚合按档位分段统计,替代 `MAX()` 聚合
- **需求 B**:多门店会员查询 `register_site_id` 已知限制标记 + 预留扩展方案
- **需求 C**会员生日字段全链路补齐C1: ETL 链路 / C2: 手动补录)
- **需求 D**`DwdLoadTask.load()` 返回值格式规范化
## 术语表
- **DwdLoadTask**DWD 层装载任务,负责将 ODS 原始数据清洗装载至 DWD 明细层
- **DWS 任务**:数据汇总层任务,从 DWD 层聚合生成业务报表数据
- **SCD2**:缓慢变化维度类型 2通过版本化记录维度属性的历史变化
- **BaseTask**ETL 任务基类,提供 Extract/Transform/Load 模板方法和计数累加逻辑
- **FlowRunner**ETL 流程编排器,按层级顺序执行任务并汇总计数
- **dim_member**DWD 层会员维度表,使用 SCD2 管理历史版本
- **dws_assistant_monthly_summary**DWS 层助教月度汇总表
- **dws_assistant_salary_calc**DWS 层助教工资计算表
- **register_site_id**:会员注册门店 ID当前 `dim_member` 中唯一的门店标识
- **assistant_level_code**:助教档位代码,助教月内可能因升级/降级而变化
- **dim_member_birthday_manual**:手动补录生日表(位于 `zqyy_app` 业务库),存储助教提交的会员生日信息,通过 FDW 只读映射供 ETL DWS 任务读取
- **_safe_int()**`flow_runner.py` 中的类型安全辅助函数,将 `int`/`list`/`None` 统一转为 `int`
- **_accumulate_counts()**`BaseTask` 中的计数累加方法,合并多段执行的统计结果
## 需求
### 需求 1DwdLoadTask 返回值格式规范化(需求 D
**用户故事:** 作为 ETL 开发者,我希望 DwdLoadTask.load() 的返回值格式与其他任务保持一致,以便 FlowRunner 能安全地汇总所有任务的计数。
#### 验收标准
1. WHEN DwdLoadTask.load() 执行完成, THE DwdLoadTask SHALL 返回包含 `errors` 键(值为 `int` 类型,等于错误列表长度)和 `error_details` 键(值为 `list[dict]` 类型,包含错误详情)的字典
2. WHEN BaseTask._accumulate_counts() 遇到值为 `list` 类型的计数项, THE BaseTask SHALL 将该值转换为 `len(list)` 后再累加(防御层)
3. WHEN FlowRunner 汇总所有任务计数, THE FlowRunner SHALL 保留 `_safe_int()` 作为最终防御层,确保 `sum()` 不会因类型不一致而崩溃
### 需求 2助教月度聚合按档位分段统计需求 A
**用户故事:** 作为运营管理者,我希望助教月度汇总按档位分段统计业绩,以便准确反映助教在不同档位期间的表现和工资计算。
#### 验收标准
1. WHEN 助教在同一月内存在多个 assistant_level_code, THE AssistantMonthlyTask SHALL 按 `(assistant_id, stat_month, assistant_level_code)` 分组生成多行记录,分别统计各档位的业绩指标
2. THE dws_assistant_monthly_summary 表 SHALL 使用 `(site_id, assistant_id, stat_month, assistant_level_code)` 作为唯一约束
3. WHEN AssistantMonthlyTask 需要取 nickname 值, THE AssistantMonthlyTask SHALL 按时间倒序取最后一条记录的 nickname而非使用 `MAX()` 聚合
4. WHEN AssistantSalaryTask 计算工资, THE AssistantSalaryTask SHALL 按档位分段计算抽成,适配新的多行月度汇总结构
5. WHEN AssistantFinanceTask 提取日度收入需要 nickname, THE AssistantFinanceTask SHALL 按时间倒序取最后一条记录的 nickname而非使用 `MAX()` 聚合
6. WHEN AssistantCustomerTask 提取服务对需要 nickname, THE AssistantCustomerTask SHALL 按时间倒序取最后一条记录的 nickname而非使用 `MAX()` 聚合
### 需求 3多门店会员查询支持需求 B
**用户故事:** 作为运营管理者,我希望 DWS 任务能正确查询跨店消费的会员信息,以便 B 店能看到在 A 店注册但在 B 店消费的会员维度数据。
#### 验收标准
1. WHEN DWS 任务需要查询会员信息, THE DWS 任务 SHALL 通过事实表中的 `member_id` 反查 `dim_member`,而非使用 `WHERE register_site_id = %s` 预筛选
2. WHEN 会员在 A 店注册并在 B 店消费, THE B 店的 DWS 任务 SHALL 能查询到该会员的昵称、手机号等维度信息
3. WHEN DWS 任务执行会员信息提取, THE DWS 任务 SHALL 使用 `WHERE member_id IN (SELECT DISTINCT member_id FROM dwd.事实表 WHERE site_id = %s)` 模式获取会员维度数据
### 需求 4会员生日字段 ETL 链路补齐(需求 C1
**用户故事:** 作为运营管理者,我希望会员生日信息能从上游 API 完整传递到 DWS 层,以便用于会员分析和销售线索。
#### 验收标准
1. THE dim_member 表 SHALL 包含 `birthday DATE`
2. WHEN DwdLoadTask 执行 ODS → DWD 装载, THE DwdLoadTask SHALL 在列映射中包含 `birthday` 字段,将 ODS 中的生日数据装载到 `dim_member.birthday`
3. WHEN SCD2 更新 dim_member 记录, THE DwdLoadTask SHALL 将 `birthday` 作为变化检测字段之一,正常处理生日值的变化
4. WHEN MemberVisitTask 等 DWS 任务提取会员信息, THE DWS 任务 SHALL 从 `dim_member.birthday` 读取生日字段并写入 DWS 目标表
### 需求 5助教手动补录会员生日需求 C2
**用户故事:** 作为助教,我希望能手动提交客户的生日信息,以便在上游 API 未提供生日数据时补充这一重要销售线索。
#### 验收标准
1. THE `zqyy_app` 业务库(开发/测试环境使用 `test_zqyy_app`SHALL 包含 `member_birthday_manual` 表,结构包含 `member_id``birthday_value``recorded_by_assistant_id``recorded_by_name``recorded_at``source` 字段,唯一约束为 `(member_id, recorded_by_assistant_id)`
2. WHEN 同一助教对同一会员重复提交生日, THE 系统 SHALL 更新该助教的已有记录UPSERT保留所有其他助教的提交记录
3. WHEN DWS 任务需要读取手动补录生日, THE DWS 任务 SHALL 通过 FDW 只读映射从 `zqyy_app.member_birthday_manual` 读取数据
4. WHEN dim_member.birthdayAPI 来源)和 member_birthday_manual手动来源同时存在, THE DWS 任务 SHALL 优先使用手动补录值
5. WHEN 助教通过后端 API 提交生日, THE 后端 SHALL 提供 POST 接口接收 `member_id``birthday_value``assistant_id``assistant_name` 参数,执行 UPSERT 写入 `zqyy_app.member_birthday_manual`
6. WHEN SCD2 更新 dim_member.birthday, THE DwdLoadTask SHALL 正常更新 API 来源的生日值,不影响业务库中手动补录表的数据

View File

@@ -0,0 +1,228 @@
# 实现计划ETL 聚合修复与生日字段补齐
## 概述
按优先级D → C1 → B → A → C2逐步实施每个需求独立可部署。代码变更集中在 ETL Connector 的 tasks 层和后端 APIDDL 变更通过迁移脚本执行。
## 任务
- [x] 1. 需求 DDwdLoadTask 返回值格式规范化
- [x] 1.1 修改 DwdLoadTask.load() 返回值格式
-`apps/etl/connectors/feiqiu/tasks/dwd/dwd_load_task.py``load()` 方法中,将 `return {"tables": summary, "errors": errors}` 改为 `return {"tables": summary, "errors": len(errors), "error_details": errors}`
- _需求: 1.1_
- [x] 1.2 增强 BaseTask._accumulate_counts() 防御层
-`apps/etl/connectors/feiqiu/tasks/base_task.py``_accumulate_counts()` 方法中,增加 `isinstance(value, list)` 分支,将 list 转为 `len()` 后累加
- _需求: 1.2_
- [x] 1.3 编写属性测试DwdLoadTask 返回值格式一致性
- **Property 1: DwdLoadTask 返回值格式一致性**
- **验证: 需求 1.1**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_return_format_properties.py`
- [x] 1.4 编写属性测试_accumulate_counts 类型安全累加
- **Property 2: _accumulate_counts 类型安全累加**
- **验证: 需求 1.2**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_return_format_properties.py`
- [x] 2. 检查点 — 需求 D 完成
- 确保所有测试通过ask the user if questions arise.
- [x] 3. 需求 C1会员生日字段 ETL 链路补齐
- [x] 3.1 编写迁移脚本ODS/DWD 加 birthday 列
- 创建 `db/etl_feiqiu/migrations/2026-02-22__C1_dim_member_add_birthday.sql`
- ODS: `ALTER TABLE ods.member_profiles ADD COLUMN IF NOT EXISTS birthday DATE;`
- DWD: `ALTER TABLE dwd.dim_member ADD COLUMN IF NOT EXISTS birthday DATE;`
- 包含回滚语句和验证 SQL
- _需求: 4.1_
- [-] 3.1a 在测试库执行迁移脚本 C1
-`test_etl_feiqiu` 上执行 `2026-02-22__C1_dim_member_add_birthday.sql`
- 执行验证 SQL 确认列已添加
- _需求: 4.1_
- [x] 3.2 更新 ODS 入库逻辑:提取 birthday 字段
- 在 ODS 入库任务中增加 `birthday` 字段的 JSON 提取映射
- 确认 `ods_tasks.py``member_profiles` 的字段列表包含 `birthday`
- _需求: 4.2_
- [x] 3.3 验证 DwdLoadTask 自动列映射包含 birthday
- DwdLoadTask 通过 `_get_columns()` 自动读取 DWD 表列名,确认 `birthday` 被自动包含在列映射中
- SCD2 变化检测自动包含所有非 SCD2 元数据列,确认 `birthday` 参与变化检测
- _需求: 4.2, 4.3_
- [x] 3.4 恢复 DWS 任务中的 birthday 引用
- 修改 `member_visit_task.py``_extract_member_info()` SQL加入 `birthday` 字段
- 修改 `member_consumption_task.py``_extract_member_info()` SQL加入 `birthday` 字段
- 修改 DWS 任务的 `transform()` 方法,将 `member_birthday` 写入输出记录
- _需求: 4.4_
- [x] 3.5 编写属性测试birthday ODS→DWD 装载正确性
- **Property 7: birthday ODS→DWD 装载正确性**
- **验证: 需求 4.2**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_birthday_properties.py`
- [x] 3.6 编写属性测试birthday SCD2 变化检测
- **Property 8: birthday SCD2 变化检测**
- **验证: 需求 4.3**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_birthday_properties.py`
- [x] 4. 检查点 — 需求 C1 完成
- 确保所有测试通过ask the user if questions arise.
- [x] 5. 需求 B多门店会员查询支持
- [x] 5.1 修改 member_visit_task.py 的 _extract_member_info()
-`WHERE register_site_id = %s` 改为通过 `dwd_settlement_head` 事实表的 `tenant_member_id` 反查
- _需求: 3.1, 3.2_
- [x] 5.2 修改 member_consumption_task.py 的 _extract_member_info()
-`WHERE register_site_id = %s` 改为通过 `dwd_settlement_head` 事实表的 `tenant_member_id` 反查
- 同时修改 `dim_member_card_account` 查询,改为通过事实表反查
- _需求: 3.1, 3.2_
- [x] 5.3 修改 assistant_customer_task.py 的 _extract_member_info()
-`WHERE register_site_id = %s` 改为通过 `dwd_assistant_service_log` 事实表的 `tenant_member_id` 反查
- _需求: 3.1, 3.2_
- [x] 5.4 修改 finance_recharge_task.py 的 dim_member_card_account 查询
-`WHERE register_site_id = %s` 改为通过事实表反查
- _需求: 3.1_
- [x] 5.5 编写属性测试:跨店会员可查
- **Property 6: 跨店会员可查**
- **验证: 需求 3.1, 3.2**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_multi_store_properties.py`
- [x] 6. 检查点 — 需求 B 完成
- 确保所有测试通过ask the user if questions arise.
- [x] 7. 需求 A助教月度聚合按档位分段统计
- [x] 7.1 编写迁移脚本:唯一约束变更
- 创建 `db/etl_feiqiu/migrations/2026-02-22__A_monthly_summary_uk_change.sql`
- DROP 旧约束 `uk_dws_assistant_monthly`ADD 新约束 `(site_id, assistant_id, stat_month, assistant_level_code)`
- 包含回滚语句和验证 SQL
- _需求: 2.2_
- [x] 7.1a 在测试库执行迁移脚本 A
-`test_etl_feiqiu` 上执行 `2026-02-22__A_monthly_summary_uk_change.sql`
- 执行验证 SQL 确认约束已变更(`SELECT conname FROM pg_constraint ...`
- _需求: 2.2_
- [x] 7.2 修改 AssistantMonthlyTask._extract_daily_aggregates()
- GROUP BY 加入 `assistant_level_code, assistant_level_name`
- nickname 改为 `(ARRAY_AGG(assistant_nickname ORDER BY stat_date DESC))[1]`
- _需求: 2.1, 2.3_
- [x] 7.3 修改 AssistantMonthlyTask._process_month() 适配多行
- 确认 `_process_month()` 能正确处理同一助教多个档位的聚合数据
- 每行使用自己的 `assistant_level_code` 进行档位匹配和排名计算
- _需求: 2.1_
- [x] 7.4 修改 AssistantSalaryTask 适配档位分段工资计算
- `_extract_monthly_summary()` 已能返回多行(同一助教不同档位)
- `transform()` 遍历每行分别计算工资,按档位使用对应的 `level_price``tier`
- _需求: 2.4_
- [x] 7.5 修改 AssistantFinanceTask._extract_daily_revenue() nickname 取值
-`MAX(s.nickname)` 改为 `(ARRAY_AGG(s.nickname ORDER BY s.start_use_time DESC))[1]`
- _需求: 2.5_
- [x] 7.6 修改 AssistantCustomerTask._extract_service_pairs() nickname 取值
-`MAX(assistant_nickname)` 改为 `(ARRAY_AGG(assistant_nickname ORDER BY service_date DESC))[1]`
- _需求: 2.6_
- [x] 7.7 编写属性测试:档位分段聚合正确性
- **Property 3: 档位分段聚合正确性**
- **验证: 需求 2.1**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_monthly_aggregation_properties.py`
- [x] 7.8 编写属性测试nickname 按时间倒序取值
- **Property 4: nickname 按时间倒序取值**
- **验证: 需求 2.3, 2.5, 2.6**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_monthly_aggregation_properties.py`
- [x] 7.9 编写属性测试:工资按档位分段计算
- **Property 5: 工资按档位分段计算**
- **验证: 需求 2.4**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_monthly_aggregation_properties.py`
- [x] 8. 检查点 — 需求 A 完成
- 确保所有测试通过ask the user if questions arise.
- [x] 9. 需求 C2助教手动补录会员生日
- [x] 9.1 编写迁移脚本:创建 member_birthday_manual 表
- 创建 `db/zqyy_app/migrations/2026-02-22__C2_member_birthday_manual.sql`
-`zqyy_app` / `test_zqyy_app` 中创建 `member_birthday_manual`
- 包含回滚语句和验证 SQL
- _需求: 5.1_
- [x] 9.1a 在测试库执行迁移脚本 C2
-`test_zqyy_app` 上执行 `2026-02-22__C2_member_birthday_manual.sql`
- 执行验证 SQL 确认表和约束已创建
- _需求: 5.1_
- [x] 9.2 编写 FDW 映射脚本ETL 库读取业务库
- 创建 `db/fdw/setup_fdw_reverse.sql`etl_feiqiu → zqyy_app 方向)
- 创建 `db/fdw/setup_fdw_reverse_test.sql`test 环境版本)
- 在 ETL 库中创建 `fdw_app.member_birthday_manual` 外部表
- _需求: 5.3_
- [x] 9.2a 在测试库执行 FDW 映射脚本
-`test_etl_feiqiu` 上执行 `setup_fdw_reverse_test.sql`
- 验证 `fdw_app.member_birthday_manual` 外部表可读取
- _需求: 5.3_
- [x] 9.3 修改 DWS 任务:生日读取优先级逻辑
-`member_visit_task.py``member_consumption_task.py``_extract_member_info()` 中,使用 `COALESCE(fdw_app.member_birthday_manual, dim_member.birthday)` 逻辑
- 增加 FDW 连接失败的降级处理
- _需求: 5.4_
- [x] 9.4 实现后端 API生日提交接口
- 创建 `apps/backend/app/routers/member_birthday.py`
- 实现 `POST /member-birthday` 接口,执行 UPSERT
- 创建 Pydantic schema `apps/backend/app/schemas/member_birthday.py`
-`apps/backend/app/main.py` 中注册路由
- _需求: 5.5_
- [x] 9.5 编写属性测试:生日 UPSERT 幂等性
- **Property 9: 生日 UPSERT 幂等性**
- **验证: 需求 5.2, 5.5**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_birthday_properties.py`
- [x] 9.6 编写属性测试:手动补录优先于 API 来源
- **Property 10: 手动补录优先于 API 来源**
- **验证: 需求 5.4**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_birthday_properties.py`
- [x] 9.7 编写属性测试SCD2 更新不影响手动补录表
- **Property 11: SCD2 更新不影响手动补录表**
- **验证: 需求 5.6**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_birthday_properties.py`
- [x] 10. 收尾:主 DDL 合并与文档更新
- [x] 10.1 合并迁移变更到主 DDL 文件
-`birthday` 列定义合并到 `db/etl_feiqiu/schemas/ods.sql``member_profiles` 表)
-`birthday` 列定义合并到 `db/etl_feiqiu/schemas/dwd.sql``dim_member` 表)
- 将新唯一约束合并到 `db/etl_feiqiu/schemas/dws.sql``dws_assistant_monthly_summary` 表)
-`member_birthday_manual` 表定义合并到 `db/zqyy_app/schemas/init.sql`
- 将 FDW 反向映射合并到 `db/fdw/setup_fdw.sql``db/fdw/setup_fdw_test.sql`
- [x] 10.2 更新数据库文档
-`docs/database/` 中新建或更新受影响表的文档:
- `dim_member`:新增 `birthday` 列说明
- `dws_assistant_monthly_summary`:唯一约束变更说明
- `member_birthday_manual`:新建表文档(含 FDW 映射说明)
- 遵循现有 `BD_Manual_*.md` 命名规范
- [x] 10.3 最终验证
- 确保所有测试通过
- 确认主 DDL 文件与测试库实际结构一致
- ask the user if questions arise
## 备注
- 标记 `*` 的任务为可选测试任务,可跳过以加速 MVP
- 每个需求独立可部署,检查点确保增量验证
- 迁移脚本需在 `test_etl_feiqiu` / `test_zqyy_app` 上先行验证
- 属性测试使用 hypothesis每个属性最少 100 次迭代
- 单元测试使用 FakeDB/FakeAPI不依赖真实数据库

View File

@@ -0,0 +1,126 @@
# 设计文档ETL 全流程前后端联调etl-fullstack-integration
## 概述
本 Spec 是一个运维联调任务,不涉及新功能开发。目标是验证 `admin-web-console` Spec 产出的前后端代码在真实环境下的端到端正确性,同时收集性能数据。
核心流程:
1. 启动后端 + 前端服务
2. 通过 API 登录获取 JWT
3. 提交全流程 ETL 任务api_full, full_window, force-full, 全选常用任务, 自定义窗口 2025-11-01~2026-02-20, 30天切分, 全部门店)
4. 实时监控执行过程,捕获错误/警告
5. 执行完成后生成综合报告
## 架构
```
联调脚本 (scripts/ops/)
├── 1. 启动服务
│ ├── uvicorn app.main:app (后端 :8000)
│ └── pnpm dev (前端 :5173)
├── 2. API 调用链
│ ├── POST /api/auth/login → JWT
│ ├── GET /api/tasks/registry → 任务列表
│ ├── GET /api/tasks/sync-check → 同步检查
│ ├── POST /api/tasks/validate → CLI 预览
│ └── POST /api/execution/run → 触发执行
├── 3. 监控循环
│ ├── GET /api/execution/queue → 状态轮询
│ ├── GET /api/execution/{id}/logs → 日志获取
│ └── 错误/警告检测
└── 4. 报告生成
└── 输出到 SYSTEM_LOG_ROOT
```
## 任务参数
根据用户需求,联调任务的具体参数:
```python
INTEGRATION_TASK_CONFIG = {
"flow": "api_full", # 全流程API → ODS → DWD → DWS → INDEX
"processing_mode": "full_window", # 全窗口处理
"window_mode": "custom", # 自定义时间范围
"window_start": "2025-11-01 00:00",
"window_end": "2026-02-20 00:00",
"window_split": "day", # 按天切分
"window_split_days": 30, # 30天一个切片
"force_full": True, # 强制全量
"dry_run": False,
"tasks": [ # 全选 is_common=True 的任务
# ODS 层22 个)
"ODS_ASSISTANT_ACCOUNT", "ODS_ASSISTANT_LEDGER", "ODS_ASSISTANT_ABOLISH",
"ODS_SETTLEMENT_RECORDS", "ODS_TABLE_USE", "ODS_TABLE_FEE_DISCOUNT",
"ODS_TABLES", "ODS_PAYMENT", "ODS_REFUND", "ODS_PLATFORM_COUPON",
"ODS_MEMBER", "ODS_MEMBER_CARD", "ODS_MEMBER_BALANCE", "ODS_RECHARGE_SETTLE",
"ODS_GROUP_PACKAGE", "ODS_GROUP_BUY_REDEMPTION",
"ODS_INVENTORY_STOCK", "ODS_INVENTORY_CHANGE",
"ODS_GOODS_CATEGORY", "ODS_STORE_GOODS", "ODS_STORE_GOODS_SALES", "ODS_TENANT_GOODS",
# DWD 层1 个常用)
"DWD_LOAD_FROM_ODS",
# DWS 层15 个常用,排除 DWS_MAINTENANCE
"DWS_BUILD_ORDER_SUMMARY", "DWS_ASSISTANT_DAILY", "DWS_ASSISTANT_MONTHLY",
"DWS_ASSISTANT_CUSTOMER", "DWS_ASSISTANT_SALARY", "DWS_ASSISTANT_FINANCE",
"DWS_MEMBER_CONSUMPTION", "DWS_MEMBER_VISIT",
"DWS_FINANCE_DAILY", "DWS_FINANCE_RECHARGE", "DWS_FINANCE_INCOME_STRUCTURE",
"DWS_FINANCE_DISCOUNT_DETAIL",
"DWS_GOODS_STOCK_DAILY", "DWS_GOODS_STOCK_WEEKLY", "DWS_GOODS_STOCK_MONTHLY",
# INDEX 层3 个常用,排除 DWS_ML_MANUAL_IMPORT
"DWS_WINBACK_INDEX", "DWS_NEWCONV_INDEX", "DWS_RELATION_INDEX",
],
# store_id 由后端从 JWT 注入(默认管理员 site_id=1
# 注意:用户要求"全部门店",但当前系统只有 site_id=1后续多门店需逐个执行
}
```
## 监控策略
- 轮询间隔30 秒
- 最长等待30 分钟(无新日志输出时)
- 错误检测:日志行匹配 `ERROR``CRITICAL``Traceback``Exception`
- 警告检测:日志行匹配 `WARNING``WARN`
- 计时解析:从日志中提取时间戳,计算阶段耗时
## 报告格式
报告输出为 Markdown 文件,路径:`{SYSTEM_LOG_ROOT}/{date}__etl_integration_report.md`
```markdown
# ETL 全流程联调报告
## 执行概要
- 任务参数:...
- 开始时间 / 结束时间 / 总时长
- 退出码 / 最终状态
## 性能报告
- 各窗口切片耗时对比表
- Top-5 耗时阶段
- 总体吞吐量估算
## DEBUG 报告(如有)
- 错误摘要
- 警告摘要
- 相关日志片段
- 可能的原因分析
```
## 正确性属性
本 Spec 为运维联调任务,不涉及新功能代码开发,因此不定义形式化的属性测试。验证通过以下方式进行:
- 服务健康检查通过
- 任务提交成功并开始执行
- 执行完成后退出码和日志符合预期
- 报告文件成功生成
## 测试策略
本 Spec 本身就是一次集成测试。不额外编写单元测试或属性测试。验证标准:
- 后端 API 响应正确
- ETL CLI 子进程正常启动和执行
- 日志正确捕获和推送
- 报告文件正确生成到 SYSTEM_LOG_ROOT

View File

@@ -0,0 +1,70 @@
# 需求文档ETL 全流程前后端联调etl-fullstack-integration
## 简介
基于已完成的 `admin-web-console` Spec 产出的前后端代码,进行一次完整的端到端联调验证。通过管理后台 API 提交 ETL 全流程任务api_full覆盖 ODS → DWD → DWS → INDEX 全链路,验证前后端协作、子进程执行、日志推送、错误处理等环节的正确性。同时收集精细计时数据,定位性能瓶颈。
## 术语表
- **联调**:将 admin-web 后端 API 与 feiqiu ETL CLI 串联,通过真实 API 调用触发 ETL 全流程执行
- **全选常用任务**:任务注册表中 `is_common=True` 的所有任务(排除工具类、手动导入、维护类等 `is_common=False` 的任务)
- **精细计时**:在 ETL 执行过程中,通过日志解析或 CLI 输出记录每个阶段ODS/DWD/DWS/INDEX和每个子任务的耗时
## 需求
### 需求 1服务启动与健康检查
**用户故事:** 作为开发者,我希望一键启动后端和前端服务,并确认服务健康可用,以便开始联调。
#### 验收标准
1. WHEN 启动后端服务, THE Backend_API SHALL 在 `http://localhost:8000` 上响应请求
2. WHEN 启动前端服务, THE Admin_Web SHALL 在 `http://localhost:5173` 上可访问
3. WHEN 调用 `POST /api/auth/login`, THE Backend_API SHALL 返回有效的 JWT 令牌
4. WHEN 调用 `GET /api/tasks/registry`, THE Backend_API SHALL 返回非空的任务注册表
5. WHEN 调用 `GET /api/tasks/sync-check`, THE Backend_API SHALL 确认后端任务注册表与 ETL 真实注册表同步
### 需求 2全流程任务提交与执行
**用户故事:** 作为开发者,我希望通过后端 API 提交一个覆盖全链路的 ETL 任务验证任务配置、CLI 构建、子进程执行的完整流程。
#### 验收标准
1. WHEN 提交 TaskConfigapi_full + full_window + 全选常用任务 + 自定义窗口 2025-11-01~2026-02-20 + 30天切分 + force-full, THE Backend_API SHALL 验证配置有效并返回 CLI 命令预览
2. WHEN 提交任务到执行队列, THE Backend_API SHALL 创建队列任务并自动开始执行
3. WHILE ETL 子进程运行中, THE Backend_API SHALL 通过 WebSocket 推送实时日志
4. WHEN ETL 子进程完成, THE Backend_API SHALL 记录退出码、执行时长、完整日志到 task_execution_log
### 需求 3执行监控与错误处理
**用户故事:** 作为开发者,我希望在任务执行过程中实时监控状态,对报错或警告及时发现并进行 DEBUG。
#### 验收标准
1. WHILE 任务执行中, THE 监控脚本 SHALL 每 30 秒轮询执行状态和日志
2. WHEN 日志中出现 ERROR 或 WARNING 级别信息, THE 监控脚本 SHALL 立即记录并标记
3. WHEN 任务执行完成(成功或失败), THE 监控脚本 SHALL 停止轮询并收集最终状态
4. IF 任务执行超过 30 分钟无新日志输出, THEN THE 监控脚本 SHALL 报告超时警告
5. IF 任务执行失败, THEN THE 监控脚本 SHALL 收集完整的 stderr 和错误上下文
### 需求 4性能计时与瓶颈分析
**用户故事:** 作为开发者,我希望获得精细粒度的执行计时数据,以便发现性能瓶颈。
#### 验收标准
1. WHEN 任务执行完成, THE 报告 SHALL 包含总执行时长
2. WHEN 日志中包含阶段性时间戳, THE 报告 SHALL 解析并展示每个窗口切片的耗时
3. THE 报告 SHALL 标注耗时最长的 Top-5 阶段/任务
4. THE 报告 SHALL 包含每个窗口切片30天的独立耗时对比
### 需求 5联调报告输出
**用户故事:** 作为开发者,我希望联调完成后获得一份综合报告,包含执行情况、性能数据和可能的 DEBUG 信息。
#### 验收标准
1. THE 报告 SHALL 包含:执行概要(任务参数、开始/结束时间、总时长、退出码)
2. THE 报告 SHALL 包含性能报告各阶段耗时、窗口切片耗时对比、Top-5 瓶颈)
3. IF 执行过程中出现错误或警告, THEN THE 报告 SHALL 包含 DEBUG 报告(错误摘要、相关日志片段、可能的原因分析)
4. THE 报告 SHALL 输出到 `SYSTEM_LOG_ROOT` 环境变量指定的目录

View File

@@ -0,0 +1,86 @@
# 实现计划ETL 全流程前后端联调etl-fullstack-integration
## 概述
基于 `admin-web-console` 已完成的前后端代码,进行端到端联调验证。通过后端 API 提交 api_full 全流程 ETL 任务(自定义窗口 2025-11-01~2026-02-2030天切分force-full全选常用任务实时监控执行过程收集性能数据最终生成综合报告。
## 任务
- [ ] 1. 服务启动与健康检查
- [ ] 1.1 启动后端服务(`apps/backend/`uvicorn :8000确认 API 可达
- 启动 `uvicorn app.main:app --host 0.0.0.0 --port 8000`cwd 为 `apps/backend/`
- 验证 `GET /api/tasks/flows` 返回 200
- _Requirements: 1.1_
- [ ] 1.2 启动前端服务(`apps/admin-web/`pnpm dev :5173确认页面可访问
- 启动 `pnpm dev`cwd 为 `apps/admin-web/`
- 验证 `http://localhost:5173` 可达
- _Requirements: 1.2_
- [ ] 1.3 API 健康检查:登录获取 JWT验证任务注册表执行 sync-check
- `POST /api/auth/login` 使用默认管理员账号admin / admin123获取 JWT
- `GET /api/tasks/registry` 确认返回非空任务列表
- `GET /api/tasks/sync-check` 确认 `in_sync=true`(后端注册表与 ETL 真实注册表一致)
- 如果 sync-check 不一致,记录差异并向用户报告
- _Requirements: 1.3, 1.4, 1.5_
- [ ] 2. 全流程任务提交
- [ ] 2.1 构建 TaskConfig 并调用 validate 预览 CLI 命令
- 构建 TaskConfigflow=api_full, processing_mode=full_window, window_mode=custom, window_start="2025-11-01 00:00", window_end="2026-02-20 00:00", window_split=day, window_split_days=30, force_full=True, tasks=全选 is_common=True 的任务
- 调用 `POST /api/tasks/validate` 验证配置有效
- 记录生成的 CLI 命令预览,确认参数完整(--flow api_full --processing-mode full_window --window-start ... --window-end ... --window-split day --window-split-days 30 --force-full --store-id 1 --tasks ...
- _Requirements: 2.1_
- [ ] 2.2 提交任务执行(`POST /api/execution/run`),记录 execution_id
- 调用 `POST /api/execution/run` 提交任务
- 记录返回的 execution_id
- 确认任务状态变为 running
- _Requirements: 2.2, 2.4_
- [ ] 3. 执行监控与 DEBUG
- [ ] 3.1 启动监控循环:每 30 秒轮询状态和日志,检测错误/警告,最长等待 30 分钟
- 轮询 `GET /api/execution/queue` 检查任务状态
- 轮询 `GET /api/execution/{id}/logs` 获取增量日志
- 检测日志中的 ERROR / CRITICAL / Traceback / Exception / WARNING
- 记录每次轮询的时间戳和日志增量行数
- 如果连续 30 分钟无新日志输出,报告超时警告
- 任务完成success/failed/cancelled时停止轮询
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
- [ ] 3.2 对执行过程中发现的错误/警告进行 DEBUG 分析
- 收集所有 ERROR 和 WARNING 日志行及其上下文(前后各 5 行)
- 分析错误类型API 超时、数据库连接、数据质量、配置问题等
- 如果任务失败,获取完整 stderr 并分析根因
- 记录 DEBUG 发现到报告中
- _Requirements: 3.2, 3.5_
- [ ] 4. 性能计时与报告生成
- [ ] 4.1 解析执行日志,提取精细计时数据
- 从日志中提取每个窗口切片30天的开始/结束时间
- 计算每个切片的耗时
- 识别 ODS / DWD / DWS / INDEX 各阶段的耗时
- 标注 Top-5 耗时最长的阶段/任务
- _Requirements: 4.1, 4.2, 4.3, 4.4_
- [ ] 4.2 生成综合联调报告,输出到 SYSTEM_LOG_ROOT
- 报告包含:执行概要(参数、时间、退出码)
- 报告包含性能报告各切片耗时对比、Top-5 瓶颈)
- 报告包含DEBUG 报告(如有错误/警告)
- 输出路径:`{SYSTEM_LOG_ROOT}/{date}__etl_integration_report.md`
- 路径通过 `SYSTEM_LOG_ROOT` 环境变量获取,缺失时报错
- _Requirements: 5.1, 5.2, 5.3, 5.4_
- [ ] 5. 服务清理
- [ ] 5.1 停止后端和前端服务,清理资源
- 停止 uvicorn 进程
- 停止 pnpm dev 进程
- 报告联调完成状态
## 说明
- 本 Spec 为运维联调任务,不涉及新功能代码开发
- 不编写属性测试或单元测试,联调本身即为集成验证
- 全选常用任务 = 任务注册表中 `is_common=True` 的所有任务(共 41 个)
- "全部门店":当前系统仅有 site_id=1默认管理员绑定如需多门店需逐个执行
- 监控允许空闲等待,最长 30 分钟无新日志才报超时
- 报告输出路径遵循 export-paths 规范,通过 SYSTEM_LOG_ROOT 环境变量获取

View File

@@ -0,0 +1 @@
{"generationMode": "requirements-first"}

View File

@@ -0,0 +1,354 @@
# 设计文档ETL 员工维度表staff_info
## 概述
为飞球 ETL 连接器新增员工维度表,从 `SearchSystemStaffInfo` API 抓取球房全体员工数据(店长、主管、教练、收银员、助教管理员等),经 ODS 落地后清洗装载至 DWD 层。员工表与现有助教表(`assistant_accounts_master`)是完全独立的实体。
## API 响应结构
```json
{
"data": {
"total": 15,
"staffProfiles": [
{
"id": 3020236636900101,
"cashierPointId": 2790685415443270,
"cashierPointName": "默认",
"job_num": "",
"staff_name": "葛芃",
"mobile": "13811638071",
"auth_code": "",
"avatar": "",
"create_time": "2025-12-24 00:03:37",
"entry_time": "2025-12-23 08:00:00",
"is_delete": 0,
"leave_status": 0,
"resign_time": "2225-12-24 00:03:37",
"site_id": 2790685415443269,
"staff_identity": 2,
"status": 1,
"system_role_id": 4,
"system_user_id": 3020236636293893,
"tenant_id": 2790683160709957,
"tenant_org_id": 2790685415443269,
"job": "店长",
"shop_name": "朗朗桌球",
"account_status": 1,
"is_reserve": 1,
"groupName": "",
"groupId": 0,
"alias_name": "葛芃",
"staff_profile_id": 0,
"site_label": "",
"rank_id": -1,
"ding_talk_synced": 1,
"new_rank_id": 0,
"new_staff_identity": 0,
"salary_grant_enabled": 2,
"rankName": "无职级",
"entry_type": 1,
"userRoles": [],
"entry_sign_status": 0,
"resign_sign_status": 0,
"criticism_status": 1,
"gender": 3
}
]
},
"code": 0
}
```
## 1. ODS 层设计
### 1.1 ODS 任务规格
```python
OdsTaskSpec(
code="ODS_STAFF_INFO",
class_name="OdsStaffInfoTask",
table_name="ods.staff_info_master",
endpoint="/PersonnelManagement/SearchSystemStaffInfo",
data_path=("data",),
list_key="staffProfiles",
pk_columns=(_int_col("id", "id", required=True),),
extra_params={
"workStatusEnum": 0,
"dingTalkSynced": 0,
"staffIdentity": 0,
"rankId": 0,
"criticismStatus": 0,
"signStatus": -1,
},
include_source_endpoint=False,
include_fetched_at=False,
include_record_index=True,
requires_window=False,
time_fields=None,
snapshot_mode=SnapshotMode.FULL_TABLE,
description="员工档案 ODSSearchSystemStaffInfo -> staffProfiles 原始 JSON",
)
```
### 1.2 ODS 表 DDL`ods.staff_info_master`
```sql
CREATE TABLE ods.staff_info_master (
id BIGINT NOT NULL,
tenant_id BIGINT,
site_id BIGINT,
tenant_org_id BIGINT,
system_user_id BIGINT,
staff_name TEXT,
alias_name TEXT,
mobile TEXT,
avatar TEXT,
gender INTEGER,
job TEXT,
job_num TEXT,
staff_identity INTEGER,
status INTEGER,
account_status INTEGER,
system_role_id INTEGER,
rank_id INTEGER,
rank_name TEXT,
new_rank_id INTEGER,
new_staff_identity INTEGER,
leave_status INTEGER,
entry_time TIMESTAMP WITHOUT TIME ZONE,
resign_time TIMESTAMP WITHOUT TIME ZONE,
create_time TIMESTAMP WITHOUT TIME ZONE,
is_delete INTEGER,
is_reserve INTEGER,
shop_name TEXT,
site_label TEXT,
cashier_point_id BIGINT,
cashier_point_name TEXT,
group_id BIGINT,
group_name TEXT,
staff_profile_id BIGINT,
auth_code TEXT,
auth_code_create TIMESTAMP WITHOUT TIME ZONE,
ding_talk_synced INTEGER,
salary_grant_enabled INTEGER,
entry_type INTEGER,
entry_sign_status INTEGER,
resign_sign_status INTEGER,
criticism_status INTEGER,
user_roles JSONB,
-- ETL 元数据
content_hash TEXT NOT NULL,
source_file TEXT,
fetched_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
payload JSONB NOT NULL
);
COMMENT ON TABLE ods.staff_info_master IS '员工档案主数据来源SearchSystemStaffInfo API';
```
### 1.3 ODS 列名映射说明
API 返回的驼峰字段在 ODS 层统一转为蛇形命名(由 BaseOdsTask 自动处理):
- `cashierPointId``cashier_point_id`
- `cashierPointName``cashier_point_name`
- `staffName` / `staff_name``staff_name`API 已是蛇形)
- `systemUserId` / `system_user_id``system_user_id`
- `tenantOrgId` / `tenant_org_id``tenant_org_id`
- `groupName``group_name`注意API 返回驼峰 `groupName`
- `groupId``group_id`API 返回驼峰 `groupId`
- `rankName``rank_name`API 返回驼峰 `rankName`
- `userRoles``user_roles`(数组,存为 JSONB
- `authCodeCreate` / `auth_code_create``auth_code_create`
## 2. DWD 层设计
### 2.1 主表 DDL`dwd.dim_staff`
核心业务字段,高频查询使用。
```sql
CREATE TABLE dwd.dim_staff (
staff_id BIGINT NOT NULL,
staff_name TEXT,
alias_name TEXT,
mobile TEXT,
gender INTEGER,
job TEXT,
tenant_id BIGINT,
site_id BIGINT,
system_role_id INTEGER,
staff_identity INTEGER,
status INTEGER,
leave_status INTEGER,
entry_time TIMESTAMP WITH TIME ZONE,
resign_time TIMESTAMP WITH TIME ZONE,
is_delete INTEGER,
-- SCD2
scd2_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
scd2_end_time TIMESTAMP WITH TIME ZONE,
scd2_is_current INTEGER,
scd2_version INTEGER,
PRIMARY KEY (staff_id, scd2_start_time)
);
COMMENT ON TABLE dwd.dim_staff IS '员工档案维度主表SCD2';
```
### 2.2 扩展表 DDL`dwd.dim_staff_ex`
次要/低频变更字段。
```sql
CREATE TABLE dwd.dim_staff_ex (
staff_id BIGINT NOT NULL,
avatar TEXT,
job_num TEXT,
account_status INTEGER,
rank_id INTEGER,
rank_name TEXT,
new_rank_id INTEGER,
new_staff_identity INTEGER,
is_reserve INTEGER,
shop_name TEXT,
site_label TEXT,
tenant_org_id BIGINT,
system_user_id BIGINT,
cashier_point_id BIGINT,
cashier_point_name TEXT,
group_id BIGINT,
group_name TEXT,
staff_profile_id BIGINT,
auth_code TEXT,
auth_code_create TIMESTAMP WITH TIME ZONE,
ding_talk_synced INTEGER,
salary_grant_enabled INTEGER,
entry_type INTEGER,
entry_sign_status INTEGER,
resign_sign_status INTEGER,
criticism_status INTEGER,
create_time TIMESTAMP WITH TIME ZONE,
user_roles JSONB,
-- SCD2
scd2_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
scd2_end_time TIMESTAMP WITH TIME ZONE,
scd2_is_current INTEGER,
scd2_version INTEGER,
PRIMARY KEY (staff_id, scd2_start_time)
);
COMMENT ON TABLE dwd.dim_staff_ex IS '员工档案维度扩展表SCD2';
```
### 2.3 TABLE_MAP 映射
```python
# 在 DwdLoadTask.TABLE_MAP 中新增:
"dwd.dim_staff": "ods.staff_info_master",
"dwd.dim_staff_ex": "ods.staff_info_master",
```
### 2.4 FACT_MAPPINGS 字段映射
```python
# dim_staff 主表映射
"dwd.dim_staff": [
("staff_id", "id", None),
("entry_time", "entry_time", "timestamptz"),
("resign_time", "resign_time", "timestamptz"),
],
# dim_staff_ex 扩展表映射
"dwd.dim_staff_ex": [
("staff_id", "id", None),
("rank_name", "rankname", None),
("cashier_point_id", "cashierpointid", "bigint"),
("cashier_point_name", "cashierpointname", None),
("group_id", "groupid", "bigint"),
("group_name", "groupname", None),
("system_user_id", "systemuserid", "bigint"),
("tenant_org_id", "tenantorgid", "bigint"),
("auth_code_create", "auth_code_create", "timestamptz"),
("create_time", "create_time", "timestamptz"),
("user_roles", "userroles", "jsonb"),
],
```
说明:
- ODS 层的列名由 BaseOdsTask 自动从 API 驼峰转为蛇形(如 `cashierPointId``cashierpointid`,注意 PG 列名全小写无下划线)
- DWD 主表中 `staff_name``alias_name``mobile` 等与 ODS 同名列自动映射,无需显式配置
- `staff_id` 映射自 ODS 的 `id`
## 3. 数据流概览
```
API: SearchSystemStaffInfo
↓ (POST, 分页, extra_params 筛选)
ODS: ods.staff_info_master
↓ (SCD2 合并, FULL_TABLE 快照)
DWD: dwd.dim_staff + dwd.dim_staff_ex
```
## 4. 测试框架
- 测试框架:`pytest` + `hypothesis`
- 单元测试使用 `FakeDB` / `FakeAPI``tests/unit/task_test_utils.py`
## 5. 正确性属性
### P1ODS 任务规格完整性
对于 `ODS_STAFF_INFO` 任务规格,以下属性必须成立:
- `code == "ODS_STAFF_INFO"`
- `table_name == "ods.staff_info_master"`
- `endpoint == "/PersonnelManagement/SearchSystemStaffInfo"`
- `list_key == "staffProfiles"`
- `snapshot_mode == SnapshotMode.FULL_TABLE`
- `requires_window == False`
- `time_fields is None`
- `"staffProfiles"` 存在于 `DEFAULT_LIST_KEYS`
- `"ODS_STAFF_INFO"` 存在于 `ENABLED_ODS_CODES`
验证方式:单元测试直接断言
### P2DWD 映射完整性
对于 DWD 装载配置,以下属性必须成立:
- `TABLE_MAP["dwd.dim_staff"] == "ods.staff_info_master"`
- `TABLE_MAP["dwd.dim_staff_ex"] == "ods.staff_info_master"`
- `FACT_MAPPINGS["dwd.dim_staff"]` 包含 `staff_id``id` 的映射
- `FACT_MAPPINGS["dwd.dim_staff_ex"]` 包含 `staff_id``id` 的映射
验证方式:单元测试直接断言
### P3ODS 列名提取一致性(属性测试)
对于任意 API 返回的员工记录(含驼峰和蛇形混合字段名),经 BaseOdsTask 处理后:
- 所有字段名转为小写蛇形
- `id` 字段不为空且为正整数
- `payload` 字段包含完整原始 JSON
验证方式hypothesis 属性测试,生成随机员工记录验证转换一致性
## 6. 文件变更清单
### 代码变更
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `apps/etl/connectors/feiqiu/api/client.py` | 修改 | `DEFAULT_LIST_KEYS` 添加 `"staffProfiles"` |
| `apps/etl/connectors/feiqiu/tasks/ods/ods_tasks.py` | 修改 | 新增 `ODS_STAFF_INFO` 任务规格 + 注册到 `ENABLED_ODS_CODES` |
| `apps/etl/connectors/feiqiu/tasks/dwd/dwd_load_task.py` | 修改 | `TABLE_MAP``FACT_MAPPINGS` 新增 dim_staff/dim_staff_ex 映射 |
### DDL / 迁移
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `db/etl_feiqiu/migrations/2026-02-22__add_staff_info_tables.sql` | 新增 | ODS + DWD 建表迁移脚本 |
| `docs/database/ddl/etl_feiqiu__ods.sql` | 修改 | 追加 `ods.staff_info_master` DDL |
| `docs/database/ddl/etl_feiqiu__dwd.sql` | 修改 | 追加 `dwd.dim_staff` + `dwd.dim_staff_ex` DDL |
### 文档
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `apps/etl/connectors/feiqiu/docs/database/ODS/mappings/mapping_SearchSystemStaffInfo_staff_info_master.md` | 新增 | API→ODS 字段映射文档 |
| `apps/etl/connectors/feiqiu/docs/database/ODS/main/BD_manual_staff_info_master.md` | 新增 | ODS 表 BD 手册 |
| `apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_dim_staff.md` | 新增 | DWD 主表 BD 手册 |
| `apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_dim_staff_ex.md` | 新增 | DWD 扩展表 BD 手册 |
| `apps/etl/connectors/feiqiu/docs/database/README.md` | 修改 | 增加员工表条目 |
| `apps/etl/connectors/feiqiu/docs/etl_tasks/ods_tasks.md` | 修改 | 增加 ODS_STAFF_INFO 任务说明 |
| `docs/database/README.md` | 修改 | 增加员工相关表条目 |

View File

@@ -0,0 +1,82 @@
# 需求文档ETL 员工维度表staff_info
## 概述
为飞球 ETL 连接器新增"员工"维度表,从上游 `SearchSystemStaffInfo` API 抓取球房员工数据,经 ODS 落地后清洗装载至 DWD 层,走 SCD2 缓慢变化维度。
## 用户故事
### US-1API 对接与 ODS 落地
作为 ETL 开发者,我需要将 `SearchSystemStaffInfo` API 融入现有 API 请求框架,并将原始数据落地到 ODS 层,以便后续清洗使用。
验收标准:
- 1.1 在 `ODS_TASK_SPECS` 中新增 `ODS_STAFF_INFO` 任务规格endpoint 为 `/PersonnelManagement/SearchSystemStaffInfo`
- 1.2 API 请求体包含必要的筛选参数(`workStatusEnum``staffIdentity` 等),使用现有 `APIClient.iter_paginated` 分页机制
- 1.3 ODS 表 `ods.staff_info_master` 包含 API 返回的所有业务字段 + ETL 元数据列(`content_hash``source_file``fetched_at``payload`
- 1.4 任务配置为 `snapshot_mode=FULL_TABLE`(全量快照,无时间窗口),`requires_window=False`
- 1.5 在 `DEFAULT_LIST_KEYS` 中添加 `staffProfiles`API 响应的列表键名)
- 1.6 在 `ENABLED_ODS_CODES` 中注册 `ODS_STAFF_INFO`
### US-2DWD 维度表设计与 SCD2 装载
作为数据分析师,我需要一张清洗后的员工维度表,以便在 DWS 汇总层关联员工信息。
验收标准:
- 2.1 创建 `dwd.dim_staff` 主表,包含核心业务字段(员工 ID、姓名、手机号、角色、门店、在职状态等+ SCD2 列
- 2.2 创建 `dwd.dim_staff_ex` 扩展表,包含次要/低频变更字段 + SCD2 列
- 2.3 在 `DwdLoadTask.TABLE_MAP` 中注册 `dwd.dim_staff``ods.staff_info_master``dwd.dim_staff_ex``ods.staff_info_master` 的映射
- 2.4 在 `DwdLoadTask.FACT_MAPPINGS` 中配置字段映射ODS 列名 → DWD 列名,含必要的类型转换)
- 2.5 DWD 装载走 SCD2 合并路径,变更检测正常工作
### US-3DDL 创建与归档
作为 DBA我需要 ODS 和 DWD 层的 DDL 被正确创建并归档到项目文档中。
验收标准:
- 3.1 编写 ODS 层 DDL`ods.staff_info_master`),在测试库执行
- 3.2 编写 DWD 层 DDL`dwd.dim_staff``dwd.dim_staff_ex`),在测试库执行
- 3.3 DDL 归档至 `docs/database/ddl/etl_feiqiu__ods.sql``docs/database/ddl/etl_feiqiu__dwd.sql`
- 3.4 编写迁移脚本至 `db/etl_feiqiu/migrations/`(日期前缀)
### US-4文档增补
作为团队成员,我需要所有相关文档同步更新,以便理解新增的数据流。
验收标准:
- 4.1 新增 ODS mapping 文档:`apps/etl/connectors/feiqiu/docs/database/ODS/mappings/mapping_SearchSystemStaffInfo_staff_info_master.md`
- 4.2 新增 ODS BD_manual 文档:`apps/etl/connectors/feiqiu/docs/database/ODS/main/BD_manual_staff_info_master.md`
- 4.3 新增 DWD BD_manual 文档:`apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_dim_staff.md`
- 4.4 更新 `apps/etl/connectors/feiqiu/docs/database/README.md`,增加员工表条目
- 4.5 更新 `apps/etl/connectors/feiqiu/docs/etl_tasks/ods_tasks.md`,增加 ODS_STAFF_INFO 任务说明
- 4.6 更新 `docs/database/README.md`,增加员工相关表的条目
- 4.7 新增 DWD BD_manual 扩展表文档:`apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_dim_staff_ex.md`(如有扩展表)
## API 信息
| 属性 | 值 |
|------|-----|
| URL | `https://pc.ficoo.vip/apiprod/admin/v1/PersonnelManagement/SearchSystemStaffInfo` |
| 方法 | POST |
| 端点路径 | `/PersonnelManagement/SearchSystemStaffInfo` |
| 认证 | Bearer Token标准飞球 API 认证) |
| 分页 | `page` + `limit`(与现有接口一致) |
### 请求体参数
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| workStatusEnum | int | 0 | 在职状态筛选0=全部) |
| dingTalkSynced | int | 0 | 钉钉同步状态0=全部) |
| staffIdentity | int | 0 | 员工身份筛选0=全部) |
| rankId | int | 0 | 职级筛选0=全部) |
| criticismStatus | int | 0 | 批评状态0=全部) |
| signStatus | int | -1 | 签约状态(-1=全部) |
| page | int | 1 | 页码 |
| limit | int | 50 | 每页条数 |
## 技术约束
- 员工表为维度表DWD 层走 SCD2
- ODS 使用 `SnapshotMode.FULL_TABLE`(全量快照软删除)
- 不需要时间窗口(`requires_window=False``time_fields=None`
- 主键为 `id`(员工 ID
- API 响应结构已确认:`data.staffProfiles` 为列表键,`data.total` 为总数
- 员工表与助教表(`assistant_accounts_master`)是完全独立的实体
- DWD 层拆分为主表(`dim_staff`+ 扩展表(`dim_staff_ex`

View File

@@ -0,0 +1,33 @@
# 任务列表ETL 员工维度表staff_info
## 任务
- [x] 1. DDL 创建与数据库执行
- [x] 1.1 编写迁移脚本 `db/etl_feiqiu/migrations/2026-02-22__add_staff_info_tables.sql`,包含 ODS + DWD 三张表的 CREATE TABLE 语句
- [x] 1.2 在测试库test_etl_feiqiu执行迁移脚本创建 `ods.staff_info_master``dwd.dim_staff``dwd.dim_staff_ex`
- [x] 1.3 将 DDL 归档追加至 `docs/database/ddl/etl_feiqiu__ods.sql``docs/database/ddl/etl_feiqiu__dwd.sql`
- [x] 2. ODS 任务注册
- [x] 2.1 在 `apps/etl/connectors/feiqiu/api/client.py``DEFAULT_LIST_KEYS` 中添加 `"staffProfiles"`
- [x] 2.2 在 `apps/etl/connectors/feiqiu/tasks/ods/ods_tasks.py``ODS_TASK_SPECS` 中新增 `ODS_STAFF_INFO` 任务规格
- [x] 2.3 在 `ENABLED_ODS_CODES` 集合中注册 `"ODS_STAFF_INFO"`
- [x] 3. DWD 映射注册
- [x] 3.1 在 `apps/etl/connectors/feiqiu/tasks/dwd/dwd_load_task.py``TABLE_MAP` 中新增 `dwd.dim_staff``dwd.dim_staff_ex` 的映射
- [x] 3.2 在 `FACT_MAPPINGS` 中新增 `dwd.dim_staff``dwd.dim_staff_ex` 的字段映射配置
- [x] 4. 单元测试
- [x] 4.1 编写 ODS 任务规格完整性测试(验证 P1 属性)
- [x] 4.2 编写 DWD 映射完整性测试(验证 P2 属性)
- [x] 5. 属性测试
- [x] 5.1 [PBT] 编写 ODS 列名提取一致性属性测试(验证 P3 属性):对于任意员工记录,字段名转换和 payload 保留正确
- [x] 6. 文档增补
- [x] 6.1 新增 ODS mapping 文档:`apps/etl/connectors/feiqiu/docs/database/ODS/mappings/mapping_SearchSystemStaffInfo_staff_info_master.md`
- [x] 6.2 新增 ODS BD_manual 文档:`apps/etl/connectors/feiqiu/docs/database/ODS/main/BD_manual_staff_info_master.md`
- [x] 6.3 新增 DWD BD_manual 主表文档:`apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_dim_staff.md`
- [x] 6.4 新增 DWD BD_manual 扩展表文档:`apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_dim_staff_ex.md`
- [x] 6.5 更新 `apps/etl/connectors/feiqiu/docs/database/README.md`,增加员工表条目
- [x] 6.6 更新 `apps/etl/connectors/feiqiu/docs/etl_tasks/ods_tasks.md`,增加 ODS_STAFF_INFO 任务说明
- [x] 6.7 更新 `docs/database/README.md`,增加员工相关表条目

View File

@@ -0,0 +1 @@
{"generationMode": "requirements-first"}

View File

@@ -0,0 +1,395 @@
# 设计文档SPI 消费力指数Spending Power Index
## 概述
SPI 是 NeoZQYY 指数体系的第 7 个指数(继 WBI/NCI/RS/OS/MS/ML 之后),粒度为 `(site_id, member_id)`,用于衡量会员在门店内的综合消费力层级。
SPI 采用"主分 + 子分"结构:
- Level消费水平基于消费金额和客单价的 log1p 压缩加权
- Speed消费速度基于绝对速度、相对速度、EWMA 速度的加权
- Stability消费稳定性基于近 90 天周覆盖率
SPI 不继承 `MemberIndexBaseTask`(该基类为 WBI/NCI 共享的会员分群逻辑SPI 不需要 NEW/OLD/STOP 分群),而是直接继承 `BaseIndexTask`,自行实现数据提取和评分逻辑。
### 设计决策
1. **继承 BaseIndexTask 而非 MemberIndexBaseTask**SPI 不需要会员分群NEW/OLD/STOP所有有消费记录的会员均参与计算。MemberIndexBaseTask 的 `_build_member_activity` 提取的特征intervals、t_v/t_r/t_a 等)与 SPI 需求不匹配,复用反而增加耦合。
2. **独立数据提取**SPI 需要按周聚合、日消费序列等 MemberIndexBaseTask 不提供的特征,因此自行编写 SQL 提取逻辑。
3. **金额压缩基数自动校准**:首次执行时从门店数据计算中位数作为基数,后续可通过 cfg_index_parameters 手动覆盖。
4. **子分独立映射**Level/Speed/Stability 各自独立做 batch_normalize_to_display使用不同的 index_type 后缀SPI_LEVEL/SPI_SPEED/SPI_STABILITY隔离分位历史。
## 架构
```mermaid
graph TD
subgraph 数据来源
A[dwd.dwd_settlement_head<br/>消费订单]
B[dwd.dwd_recharge_order<br/>充值订单]
end
subgraph SpendingPowerIndexTask
C[extract_spending_features<br/>提取基础特征]
D[calculate_level<br/>Level 子分]
E[calculate_speed<br/>Speed 子分]
F[calculate_stability<br/>Stability 子分]
G[calculate_spi_raw<br/>SPI 总分合成]
H[batch_normalize_to_display<br/>展示分映射]
end
subgraph 配置
I[cfg_index_parameters<br/>index_type='SPI']
end
subgraph 输出
J[dws.dws_member_spending_power_index]
K[dws.dws_index_percentile_history]
end
A --> C
B --> C
I --> C
I --> D
I --> E
I --> F
I --> G
C --> D
C --> E
C --> F
D --> G
E --> G
F --> G
G --> H
H --> J
H --> K
```
### 继承体系
```
BaseTask
└── BaseDwsTask
└── BaseIndexTask
├── MemberIndexBaseTask ← WBI / NCI不使用
├── RelationIndexTask ← RS/OS/MS/ML
├── MlManualImportTask ← ML 台账导入
└── SpendingPowerIndexTask ← SPI新增
```
## 组件与接口
### SpendingPowerIndexTask
继承 `BaseIndexTask`,实现以下接口:
```python
class SpendingPowerIndexTask(BaseIndexTask):
INDEX_TYPE = "SPI"
DEFAULT_PARAMS = {
# 窗口参数
'spend_window_short_days': 30,
'spend_window_long_days': 90,
'ewma_alpha_daily_spend': 0.3,
# 金额压缩基数(初始默认值,可被自动校准或配置表覆盖)
'amount_base_spend_30': 500.0,
'amount_base_spend_90': 1500.0,
'amount_base_ticket_90': 200.0,
'amount_base_recharge_90': 1000.0,
'amount_base_speed_abs': 100.0,
'amount_base_ewma_90': 50.0,
# Level 子分权重
'w_level_spend_30': 0.30,
'w_level_spend_90': 0.35,
'w_level_ticket_90': 0.20,
'w_level_recharge_90': 0.15,
# Speed 子分权重
'w_speed_abs': 0.50,
'w_speed_rel': 0.30,
'w_speed_ewma': 0.20,
# 总分权重
'weight_level': 0.60,
'weight_speed': 0.30,
'weight_stability': 0.10,
# 稳定性参数
'stability_window_days': 90,
'use_stability': 1,
# 映射与平滑
'percentile_lower': 5,
'percentile_upper': 95,
'compression_mode': 1, # log1p
'use_smoothing': 1,
'ewma_alpha': 0.2,
# 速度计算
'speed_epsilon': 1e-6,
}
# --- 必须实现的抽象方法 ---
def get_task_code(self) -> str: ...
def get_target_table(self) -> str: ...
def get_primary_keys(self) -> List[str]: ...
def get_index_type(self) -> str: ...
# --- 核心执行流程 ---
def execute(self, context=None) -> Dict[str, Any]: ...
# --- 数据提取 ---
def _extract_spending_features(self, site_id, params) -> Dict[int, SPIMemberFeatures]: ...
def _extract_recharge_features(self, site_id, params) -> Dict[int, RechargeFeatures]: ...
def _calibrate_amount_bases(self, features, params) -> Dict[str, float]: ...
# --- 子分计算(纯函数,可独立测试) ---
@staticmethod
def compute_level(features, params) -> float: ...
@staticmethod
def compute_speed(features, params) -> float: ...
@staticmethod
def compute_stability(features, params) -> float: ...
@staticmethod
def compute_spi_raw(level, speed, stability, params) -> float: ...
# --- 持久化 ---
def _save_spi_data(self, data_list, site_id) -> int: ...
```
### 关键设计:子分计算为静态方法
`compute_level``compute_speed``compute_stability``compute_spi_raw` 设计为 `@staticmethod`,不依赖数据库或任务实例状态。这使得属性测试可以直接调用这些纯函数,无需 mock 数据库连接。
### SPIMemberFeatures 数据类
```python
@dataclass
class SPIMemberFeatures:
"""SPI 计算所需的会员级特征"""
member_id: int
site_id: int
# 基础特征
spend_30: float = 0.0 # 近30天消费总额
spend_90: float = 0.0 # 近90天消费总额
recharge_90: float = 0.0 # 近90天充值总额
orders_30: int = 0 # 近30天消费笔数
orders_90: int = 0 # 近90天消费笔数
visit_days_30: int = 0 # 近30天消费日数按天去重
visit_days_90: int = 0 # 近90天消费日数按天去重
avg_ticket_90: float = 0.0 # 90天客单价
active_weeks_90: int = 0 # 近90天有消费的自然周数
daily_spend_ewma_90: float = 0.0 # 日消费 EWMA
# 子分
score_level_raw: float = 0.0
score_speed_raw: float = 0.0
score_stability_raw: float = 0.0
# 展示分(归一化后填充)
score_level_display: float = 0.0
score_speed_display: float = 0.0
score_stability_display: float = 0.0
# 总分
raw_score: float = 0.0
display_score: float = 0.0
```
## 数据模型
### dws.dws_member_spending_power_index 表结构
```sql
CREATE TABLE dws.dws_member_spending_power_index (
spi_id BIGSERIAL PRIMARY KEY,
site_id INTEGER NOT NULL,
member_id BIGINT NOT NULL,
-- 基础特征
spend_30 NUMERIC(14,2) DEFAULT 0,
spend_90 NUMERIC(14,2) DEFAULT 0,
recharge_90 NUMERIC(14,2) DEFAULT 0,
orders_30 INTEGER DEFAULT 0,
orders_90 INTEGER DEFAULT 0,
visit_days_30 INTEGER DEFAULT 0,
visit_days_90 INTEGER DEFAULT 0,
avg_ticket_90 NUMERIC(14,2) DEFAULT 0,
active_weeks_90 INTEGER DEFAULT 0,
daily_spend_ewma_90 NUMERIC(14,2) DEFAULT 0,
-- 子分Raw
score_level_raw NUMERIC(10,4) DEFAULT 0,
score_speed_raw NUMERIC(10,4) DEFAULT 0,
score_stability_raw NUMERIC(10,4) DEFAULT 0,
-- 子分Display 0-10
score_level_display NUMERIC(5,2) DEFAULT 0,
score_speed_display NUMERIC(5,2) DEFAULT 0,
score_stability_display NUMERIC(5,2) DEFAULT 0,
-- 总分
raw_score NUMERIC(10,4) DEFAULT 0,
display_score NUMERIC(5,2) DEFAULT 0,
-- 元数据
calc_time TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 唯一约束(业务主键)
CREATE UNIQUE INDEX idx_spi_site_member
ON dws.dws_member_spending_power_index (site_id, member_id);
-- 查询索引
CREATE INDEX idx_spi_display_score
ON dws.dws_member_spending_power_index (site_id, display_score DESC);
```
### cfg_index_parameters 新增种子数据
`db/etl_feiqiu/seeds/seed_index_parameters.sql` 中追加 `index_type='SPI'` 的参数行,格式与现有 WBI/NCI 参数一致。
### 执行流程
```mermaid
sequenceDiagram
participant Scheduler as 调度器
participant Task as SpendingPowerIndexTask
participant DB as PostgreSQL
participant Base as BaseIndexTask
Scheduler->>Task: execute(context)
Task->>DB: 获取 site_id
Task->>Base: load_index_parameters('SPI')
Base->>DB: SELECT FROM cfg_index_parameters
Base-->>Task: params dict
Task->>DB: 提取消费订单近90天
Task->>DB: 提取充值订单近90天
Task->>Task: 聚合会员级特征
Task->>Task: 校准金额压缩基数(如需)
loop 每个会员
Task->>Task: compute_level(features, params)
Task->>Task: compute_speed(features, params)
Task->>Task: compute_stability(features, params)
Task->>Task: compute_spi_raw(L, S, P, params)
end
Task->>Base: batch_normalize_to_display(SPI raw scores)
Task->>Base: batch_normalize_to_display(Level raw scores)
Task->>Base: batch_normalize_to_display(Speed raw scores)
Task->>Base: batch_normalize_to_display(Stability raw scores)
Task->>DB: DELETE FROM dws_member_spending_power_index WHERE site_id = %s
Task->>DB: INSERT INTO dws_member_spending_power_index (batch)
Task->>Base: save_percentile_history('SPI')
Task-->>Scheduler: {status, member_count, records_inserted}
```
## 正确性属性
*正确性属性Correctness Property是系统在所有合法执行路径上都应成立的行为特征——本质上是对"系统应该做什么"的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
以下属性基于需求文档中的验收标准推导,每个属性都是可通过 hypothesis 属性测试验证的全称量化命题。子分计算函数(`compute_level``compute_speed``compute_stability``compute_spi_raw`)设计为纯静态方法,不依赖数据库,可直接用于属性测试。
### Property 1: SPI 总分非负性
*For any* 非负的 Level、Speed、Stability 子分和非负的权重参数,`compute_spi_raw(L, S, P, params)` 的返回值应为非负。
推导:`SPI_raw = w_L × L + w_S × S + w_P × P`,当所有子分 ≥ 0 且所有权重 ≥ 0 时,加权和必然 ≥ 0。
**Validates: Requirements 6.1, 10.1**
### Property 2: Level 子分关于消费金额单调非递减
*For any* 非负的特征值和参数,若仅增加 `spend_30``spend_90`(其他特征不变),`compute_level` 的返回值不应减少。
推导:`L` 中每一项形如 `w × ln(1 + x/M)``ln(1 + x/M)` 关于 `x` 单调递增(`x ≥ 0, M > 0`),权重 `w ≥ 0`,因此增加任一消费金额项只会使 `L` 增加或不变。
**Validates: Requirements 3.1, 10.2**
### Property 3: Speed 子分关于 spend_30 单调非递减
*For any* 非负的特征值和参数,若仅增加 `spend_30`(其他特征不变),`compute_speed` 的返回值不应减少。
推导:
- `V_abs = ln(1 + spend_30 / (max(visit_days_30, 1) × V0))`:关于 spend_30 单调递增
- `V_rel = ln((spend_30/30 + ε) / (spend_90/90 + ε))`spend_30 增加使分子增大,`max(0, V_rel)` 不减
- `V_ewma`:不依赖 spend_30不变
- 三项加权和中前两项不减,第三项不变,总和不减
**Validates: Requirements 4.1, 4.4, 10.3**
### Property 4: Stability 子分取值范围 [0, 1]
*For any* `active_weeks_90` 在 [0, 13] 范围内,`compute_stability` 的返回值应在 [0, 1] 范围内。
推导:`P = active_weeks_90 / 13`,当 `active_weeks_90 ∈ {0, 1, ..., 13}` 时,`P ∈ [0, 1]`
**Validates: Requirements 5.2, 5.4, 10.4**
### Property 5: Display Score 取值范围 [0, 10]
*For any* 非空的 raw_score 列表(所有值非负),经 `batch_normalize_to_display` 映射后,所有 display_score 应在 [0.00, 10.00] 范围内。
推导:`batch_normalize_to_display` 内部先 Winsorize 到 [P5, P95],再 MinMax 映射到 [0, 10],最后 `max(0, min(10, score))` 截断。
**Validates: Requirements 6.6, 10.5**
## 错误处理
| 场景 | 处理方式 |
|------|----------|
| 门店无消费/充值数据 | 返回 `{'status': 'skipped', 'reason': 'no_data'}`,不写入任何记录 |
| cfg_index_parameters 中缺少 SPI 参数 | 使用 `DEFAULT_PARAMS` 字典中的默认值,日志 WARNING |
| 金额压缩基数为 0 或负数 | 使用 DEFAULT_PARAMS 中的默认基数,日志 WARNING |
| orders_90 = 0 导致除零 | `avg_ticket_90 = spend_90 / max(orders_90, 1)`,分母至少为 1 |
| visit_days_30 = 0 导致除零 | `V_abs` 公式中 `max(visit_days_30, 1)`,分母至少为 1 |
| v_30 和 v_90 均为 0 导致 V_rel 除零 | 使用 `ε`speed_epsilon默认 1e-6防除零 |
| 所有会员 raw_score 相同 | `batch_normalize_to_display``max - min < ε` 时返回 5.0 |
| 数据库写入失败 | 事务回滚,抛出异常由调度器处理 |
| EWMA 分位历史不存在(首次执行) | 不平滑,直接使用当前分位点 |
## 测试策略
### 属性测试hypothesis
属性测试位于 `tests/` 目录Monorepo 级),使用 `hypothesis` 库。
每个属性测试对应设计文档中的一个 Property最少运行 100 次迭代。
测试文件:`tests/test_spi_properties.py`
```python
# Feature: spi-spending-power-index, Property 1: SPI 总分非负性
@given(
level=st.floats(min_value=0, max_value=100),
speed=st.floats(min_value=0, max_value=100),
stability=st.floats(min_value=0, max_value=1),
)
@settings(max_examples=200)
def test_spi_raw_non_negative(level, speed, stability):
params = SpendingPowerIndexTask.DEFAULT_PARAMS
result = SpendingPowerIndexTask.compute_spi_raw(level, speed, stability, params)
assert result >= 0
```
属性测试库:`hypothesis`(已在项目依赖中)
### 单元测试
单元测试位于 `apps/etl/connectors/feiqiu/tests/unit/`,使用 FakeDB/FakeAPI 工具。
重点覆盖:
- 边界情况:全零输入、单一极大值输入
- 配置回退:参数缺失时使用默认值
- 任务注册:验证 task_registry 中 SPI 任务的注册信息
- use_stability=0 时稳定性子分不参与计算
### 测试配置
- 属性测试:`cd C:\NeoZQYY && pytest tests/test_spi_properties.py -v`
- 单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit/test_spi_task.py -v`
- 每个属性测试标注 `@settings(max_examples=200)`
- 每个属性测试注释引用设计文档 Property 编号

View File

@@ -0,0 +1,156 @@
# 需求文档SPI 消费力指数Spending Power Index
## 简介
SPISpending Power Index消费力指数是 NeoZQYY 指数体系的新增客户级指数用于评估会员在门店场景内的综合消费力层级。SPI 基于消费水平Level、消费速度Speed、消费稳定性Stability三个子分加权合成与现有 NCI/WBI/RS/OS/MS/ML 指数协同使用,为运营人员提供客户分层和资源分配依据。
## 术语表
- **SPI_Task**`SpendingPowerIndexTask`,负责计算 SPI 指数的 ETL 任务
- **BaseIndexTask**指数算法任务基类提供展示分映射Winsorize → 压缩 → MinMax 0-10 → EWMA 平滑)
- **cfg_index_parameters**`dws.cfg_index_parameters` 表,按 `index_type` + `param_name` 存储指数算法参数
- **dws_member_spending_power_index**SPI 指数结果表,存储会员级消费力评分
- **Raw_Score**:原始评分,由算法直接计算得出的未归一化分数
- **Display_Score**展示分Raw_Score 经 P5/P95 Winsorize → 可选压缩 → MinMax 映射到 [0, 10] 的归一化分数
- **Level_Sub_Score**:消费水平子分,衡量客户消费金额层级与客单水平
- **Speed_Sub_Score**:消费速度子分,衡量近期消费推进速度与节奏变化
- **Stability_Sub_Score**:消费稳定性子分,衡量消费行为的时间覆盖稳定性
- **Winsorize**:分位截断,将值限制在 [P5, P95] 范围内以消除极端值影响
- **EWMA**指数加权移动平均Exponential Weighted Moving Average用于平滑分位点避免批次间波动
- **log1p**`ln(1 + x)` 压缩变换,用于处理长尾分布
- **settle_type**结算类型1=台桌结账3=商城订单5=充值订单
- **task_registry**`orchestration/task_registry.py`ETL 任务注册表
- **delete-before-insert**:按门店全量刷新策略,先删除该门店所有旧记录再插入新记录
## 需求
### 需求 1SPI 结果表创建
**用户故事:** 作为 ETL 开发者,我需要创建 SPI 指数结果表,以便存储会员级消费力评分及中间特征。
#### 验收标准
1. THE SPI_Task SHALL 将结果写入 `dws.dws_member_spending_power_index` 表,主键为 `(site_id, member_id)`
2. THE dws_member_spending_power_index 表 SHALL 包含基础特征字段:`spend_30``spend_90``recharge_90``orders_30``orders_90``visit_days_30``visit_days_90``avg_ticket_90``active_weeks_90``daily_spend_ewma_90`
3. THE dws_member_spending_power_index 表 SHALL 包含子分字段:`score_level_raw``score_level_display``score_speed_raw``score_speed_display``score_stability_raw``score_stability_display`
4. THE dws_member_spending_power_index 表 SHALL 包含总分字段:`raw_score`SPI 原始分)和 `display_score`SPI 展示分numeric(5,2)
5. THE dws_member_spending_power_index 表 SHALL 包含元数据字段:`calc_time``created_at``updated_at`
6. THE 开发者 SHALL 编写迁移脚本 `db/etl_feiqiu/migrations/<日期>_create_dws_member_spending_power_index.sql`,在测试库 test_etl_feiqiu 中执行建表
7. WHEN DDL 在测试库执行成功后THE 开发者 SHALL 运行 `python scripts/ops/gen_consolidated_ddl.py` 从测试库导出最新 DDL自动合并到 `docs/database/ddl/etl_feiqiu__dws.sql`
### 需求 2SPI 基础特征提取
**用户故事:** 作为 ETL 开发者,我需要从 DWD 层提取会员消费和充值数据,以便计算 SPI 所需的基础特征。
#### 验收标准
1. THE SPI_Task SHALL 从 `dwd.dwd_settlement_head` 提取近 90 天消费订单settle_type IN (1, 3)),聚合为客户级特征
2. THE SPI_Task SHALL 从 `dwd.dwd_recharge_order` 提取近 90 天充值订单settle_type = 5聚合为客户级充值特征
3. THE SPI_Task SHALL 计算以下基础特征:`spend_30`近30天消费总额`spend_90`近90天消费总额`recharge_90`近90天充值总额`orders_30`近30天消费笔数`orders_90`近90天消费笔数`visit_days_30`近30天消费日数按天去重`visit_days_90`近90天消费日数按天去重
4. THE SPI_Task SHALL 计算 `avg_ticket_90 = spend_90 / max(orders_90, 1)`
5. THE SPI_Task SHALL 计算 `active_weeks_90`近90天有消费的自然周数最多13周
6. THE SPI_Task SHALL 对近90天日消费序列计算 EWMA 得到 `daily_spend_ewma_90`,平滑系数从 cfg_index_parameters 读取
### 需求 3Level 子分计算
**用户故事:** 作为 ETL 开发者我需要实现消费水平Level子分算法以便衡量客户的消费金额层级。
#### 验收标准
1. THE SPI_Task SHALL 按以下公式计算 Level 子分:`L = w_s30 × ln(1 + spend_30/M30) + w_s90 × ln(1 + spend_90/M90) + w_ticket × ln(1 + avg_ticket_90/T0) + w_r90 × ln(1 + recharge_90/R90)`
2. THE SPI_Task SHALL 从 cfg_index_parameters 读取 Level 子分的权重参数(`w_level_spend_30``w_level_spend_90``w_level_ticket_90``w_level_recharge_90`)和金额压缩基数(`amount_base_spend_30``amount_base_spend_90``amount_base_ticket_90``amount_base_recharge_90`
3. WHEN 所有消费和充值金额均为 0 时THE SPI_Task SHALL 将 Level 子分 Raw 设为 0.0
### 需求 4Speed 子分计算
**用户故事:** 作为 ETL 开发者我需要实现消费速度Speed子分算法以便衡量客户近期消费推进速度。
#### 验收标准
1. THE SPI_Task SHALL 计算绝对速度:`V_abs = ln(1 + spend_30 / (max(visit_days_30, 1) × V0))`
2. THE SPI_Task SHALL 计算相对速度:`V_rel = ln((v_30 + ε) / (v_90 + ε))`,其中 `v_30 = spend_30 / 30``v_90 = spend_90 / 90``ε` 为防除零小量
3. THE SPI_Task SHALL 计算 EWMA 速度:`V_ewma = ln(1 + daily_spend_ewma_90 / E0)`
4. THE SPI_Task SHALL 按以下公式合成 Speed 子分:`S = w_abs × V_abs + w_rel × max(0, V_rel) + w_ewma × V_ewma`
5. THE SPI_Task SHALL 仅对加速(`V_rel > 0`)加分,不对减速直接扣分(通过 `max(0, V_rel)` 实现)
### 需求 5Stability 子分计算
**用户故事:** 作为 ETL 开发者我需要实现消费稳定性Stability子分算法以便识别稳定高消费与偶发冲高。
#### 验收标准
1. THE SPI_Task SHALL 使用近 90 天数据计算稳定性,窗口固定为 90 天
2. THE SPI_Task SHALL 按周覆盖率计算稳定性:`P = active_weeks_90 / 13`近90天共约13个自然周
3. WHEN cfg_index_parameters 中 `use_stability = 0`THE SPI_Task SHALL 将 Stability 子分权重视为 0跳过稳定性计算
4. THE Stability_Sub_Score SHALL 的取值范围为 [0, 1]
### 需求 6SPI 总分合成与展示分映射
**用户故事:** 作为 ETL 开发者,我需要将三个子分加权合成 SPI 总分并映射为展示分,以便业务人员直观理解客户消费力层级。
#### 验收标准
1. THE SPI_Task SHALL 按以下公式计算 SPI 总分:`SPI_raw = w_L × L + w_S × S + w_P × P`,默认权重 `w_L=0.60``w_S=0.30``w_P=0.10`
2. THE SPI_Task SHALL 复用 BaseIndexTask 的 `batch_normalize_to_display` 方法将 Raw_Score 映射为 Display_Score0-10 分)
3. THE SPI_Task SHALL 对 Level、Speed、Stability 三个子分分别独立映射为展示分0-10 分)
4. THE SPI_Task SHALL 支持通过 cfg_index_parameters 配置压缩模式(`compression_mode`0=无压缩1=log1p2=asinh
5. THE SPI_Task SHALL 支持通过 cfg_index_parameters 配置 EWMA 分位平滑(`use_smoothing``ewma_alpha`
6. THE Display_Score SHALL 保留 2 位小数,取值范围为 [0.00, 10.00]
### 需求 7SPI 配置参数管理
**用户故事:** 作为 ETL 开发者,我需要在 cfg_index_parameters 中注册 SPI 的全部默认参数,以便算法参数可配置、可追溯。
#### 验收标准
1. THE 种子数据脚本 SHALL 在 cfg_index_parameters 中插入 `index_type='SPI'` 的全部参数,包括窗口参数、金额压缩基数、子分权重、总分权重、映射与平滑参数
2. THE SPI_Task SHALL 通过 BaseIndexTask 的 `load_index_parameters(index_type='SPI')` 加载参数
3. IF cfg_index_parameters 中缺少某个 SPI 参数THEN THE SPI_Task SHALL 使用 DEFAULT_PARAMS 字典中定义的默认值
4. THE 种子数据脚本 SHALL 追加到 `db/etl_feiqiu/seeds/seed_index_parameters.sql`
### 需求 8金额压缩基数校准
**用户故事:** 作为 ETL 开发者,我需要提供金额压缩基数的校准机制,以便各门店的 SPI 评分能适配不同的消费水平分布。
#### 验收标准
1. THE SPI_Task SHALL 在首次执行或参数缺失时,支持从门店历史数据自动计算金额压缩基数的建议值
2. THE SPI_Task SHALL 以近 90 天消费数据的中位数作为各金额压缩基数的默认校准值:`amount_base_spend_30` 取近30天消费中位数、`amount_base_spend_90` 取近90天消费中位数、`amount_base_ticket_90` 取90天客单中位数、`amount_base_recharge_90` 取90天充值中位数、`amount_base_speed_abs` 取每消费日平均消费中位数、`amount_base_ewma_90` 取日消费 EWMA 中位数
3. IF cfg_index_parameters 中已存在对应的金额压缩基数参数THEN THE SPI_Task SHALL 优先使用配置表中的值而非自动校准值
4. THE SPI_Task SHALL 在日志中输出实际使用的金额压缩基数值,便于运营人员审查和手动调优
5. THE 种子数据脚本 SHALL 为金额压缩基数提供合理的初始默认值(基于典型台球门店消费水平)
### 需求 9SPI 任务注册与执行
**用户故事:** 作为 ETL 开发者,我需要将 SPI 任务注册到 task_registry 并实现完整的执行流程,以便通过调度器触发计算。
#### 验收标准
1. THE SPI_Task SHALL 以任务代码 `DWS_SPENDING_POWER_INDEX` 注册到 task_registry`layer="INDEX"``requires_db_config=False`
2. THE SPI_Task SHALL 声明依赖 `depends_on=["DWS_MEMBER_CONSUMPTION"]`
3. THE SPI_Task SHALL 采用 delete-before-insert 策略:先按 `site_id` 删除旧记录,再批量插入新记录
4. WHEN 门店无任何消费或充值数据时THE SPI_Task SHALL 返回 `{'status': 'skipped', 'reason': 'no_data'}` 并跳过计算
5. THE SPI_Task SHALL 在执行完成后保存分位点历史到 `dws_index_percentile_history`index_type='SPI'
### 需求 10SPI 算法正确性测试
**用户故事:** 作为 ETL 开发者我需要通过属性测试hypothesis验证 SPI 算法的正确性,以便确保计算逻辑符合 PRD 定义。
#### 验收标准
1. THE 属性测试 SHALL 验证:对于任意非负消费/充值金额SPI_raw 为非负值
2. THE 属性测试 SHALL 验证:在其他条件不变时,增加 spend_30 或 spend_90 不会导致 Level 子分下降(单调性)
3. THE 属性测试 SHALL 验证:在其他条件不变时,增加 spend_30 不会导致 Speed 子分下降(单调性)
4. THE 属性测试 SHALL 验证Stability 子分取值范围为 [0, 1]
5. THE 属性测试 SHALL 验证Display_Score 取值范围为 [0.00, 10.00]
6. THE 属性测试 SHALL 验证SPI 总分权重 `w_L + w_S + w_P` 之和为 1.0(权重归一化)
### 需求 11文档更新
**用户故事:** 作为 ETL 开发者,我需要更新相关文档,以便团队成员了解 SPI 的表结构、算法逻辑和使用方式。
#### 验收标准
1. THE 开发者 SHALL 编写数据库手册文档 `docs/database/BD_Manual_dws_member_spending_power_index.md`,包含表结构、字段说明、索引、验证 SQL
2. THE 开发者 SHALL 更新 ETL 任务文档 `apps/etl/connectors/feiqiu/docs/etl_tasks/index_tasks.md`,新增 SPI 任务章节
3. THE 文档 SHALL 包含 SPI 算法公式、参数清单、数据来源、计算流程说明

View File

@@ -0,0 +1,131 @@
# 实现计划SPI 消费力指数Spending Power Index
## 概述
基于设计文档,将 SPI 指数建设拆分为 DDL 建表 → 核心算法实现 → 任务注册与执行流程 → 配置种子数据 → 属性测试 → 文档更新 六个阶段,每个阶段增量构建并可验证。
## 任务
- [x] 1. 创建 DDL 与数据库表
- [x] 1.1 编写迁移脚本 `db/etl_feiqiu/migrations/<日期>_create_dws_member_spending_power_index.sql`
- 创建 `dws.dws_member_spending_power_index` 表(含序列、唯一索引、查询索引)
- 字段定义参照设计文档数据模型章节
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
- [x] 1.2 在测试库 test_etl_feiqiu 执行迁移脚本建表
- 通过 TEST_DB_DSN 连接测试库执行 SQL
- _Requirements: 1.6_
- [x] 1.3 运行 `gen_consolidated_ddl.py` 从测试库导出 DDL 合并到主 DDL
- 执行 `python scripts/ops/gen_consolidated_ddl.py`,验证 `docs/database/ddl/etl_feiqiu__dws.sql` 已包含新表
- _Requirements: 1.7_
- [x] 2. 实现 SPI 核心算法(纯函数)
- [x] 2.1 创建 `SPIMemberFeatures` 数据类和 `SpendingPowerIndexTask` 骨架
- 新建 `apps/etl/connectors/feiqiu/tasks/dws/index/spending_power_index_task.py`
- 定义 `SPIMemberFeatures` dataclass
- 定义 `SpendingPowerIndexTask` 类继承 `BaseIndexTask`,包含 `INDEX_TYPE``DEFAULT_PARAMS`、抽象方法实现
- _Requirements: 7.2, 7.3, 9.1_
- [x] 2.2 实现 `compute_level` 静态方法
- Level 子分公式:`L = w_s30 × ln(1 + spend_30/M30) + w_s90 × ln(1 + spend_90/M90) + w_ticket × ln(1 + avg_ticket_90/T0) + w_r90 × ln(1 + recharge_90/R90)`
- 全零输入返回 0.0
- _Requirements: 3.1, 3.2, 3.3_
- [x] 2.3 实现 `compute_speed` 静态方法
- 绝对速度 V_abs、相对速度 V_rel仅加速加分、EWMA 速度 V_ewma
- Speed 子分公式:`S = w_abs × V_abs + w_rel × max(0, V_rel) + w_ewma × V_ewma`
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_
- [x] 2.4 实现 `compute_stability` 静态方法
- 周覆盖率:`P = active_weeks_90 / 13`
- 支持 `use_stability=0` 时返回 0.0
- _Requirements: 5.1, 5.2, 5.3, 5.4_
- [x] 2.5 实现 `compute_spi_raw` 静态方法
- 总分公式:`SPI_raw = w_L × L + w_S × S + w_P × P`
- _Requirements: 6.1_
- [x] 2.6 编写属性测试SPI 总分非负性
- **Property 1: SPI 总分非负性**
- **Validates: Requirements 6.1, 10.1**
- [x] 2.7 编写属性测试Level 子分单调性
- **Property 2: Level 子分关于消费金额单调非递减**
- **Validates: Requirements 3.1, 10.2**
- [x] 2.8 编写属性测试Speed 子分单调性
- **Property 3: Speed 子分关于 spend_30 单调非递减**
- **Validates: Requirements 4.1, 4.4, 10.3**
- [x] 2.9 编写属性测试Stability 子分范围
- **Property 4: Stability 子分取值范围 [0, 1]**
- **Validates: Requirements 5.2, 5.4, 10.4**
- [x] 2.10 编写属性测试Display Score 范围
- **Property 5: Display Score 取值范围 [0, 10]**
- **Validates: Requirements 6.6, 10.5**
- [x] 3. 检查点 - 确保核心算法测试通过
- 运行 `cd C:\NeoZQYY && pytest tests/test_spi_properties.py -v`
- 确保所有属性测试通过,如有问题请询问用户。
- [x] 4. 实现数据提取与执行流程
- [x] 4.1 实现 `_extract_spending_features` 方法
-`dwd.dwd_settlement_head` 提取近 90 天消费订单,聚合为会员级特征
- 计算 spend_30/90、orders_30/90、visit_days_30/90、avg_ticket_90、active_weeks_90
- _Requirements: 2.1, 2.3, 2.4, 2.5_
- [x] 4.2 实现 `_extract_recharge_features` 方法
-`dwd.dwd_recharge_order` 提取近 90 天充值订单
- _Requirements: 2.2_
- [x] 4.3 实现 `_compute_daily_spend_ewma` 方法
- 对近 90 天日消费序列计算 EWMA
- _Requirements: 2.6_
- [x] 4.4 实现 `_calibrate_amount_bases` 方法
- 从门店数据计算中位数作为金额压缩基数校准值
- 配置表优先级高于自动校准值
- 日志输出实际使用的基数值
- _Requirements: 8.1, 8.2, 8.3, 8.4_
- [x] 4.5 实现 `execute` 方法(完整执行流程)
- 获取 site_id → 加载参数 → 提取特征 → 校准基数 → 计算子分 → 合成总分 → 归一化展示分 → 持久化
- delete-before-insert 策略
- 无数据时返回 skipped
- 保存分位点历史
- _Requirements: 6.2, 6.3, 6.4, 6.5, 6.6, 9.3, 9.4, 9.5_
- [x] 4.6 实现 `_save_spi_data` 方法
- 批量 INSERT 到 dws_member_spending_power_index
- _Requirements: 1.1_
- [x] 5. 任务注册与模块导出
- [x] 5.1 在 `tasks/dws/index/__init__.py` 中导出 `SpendingPowerIndexTask`
- 添加 import 和 __all__ 条目
- _Requirements: 9.1_
- [x] 5.2 在 `tasks/dws/__init__.py` 中导出 `SpendingPowerIndexTask`
- 添加 import 和 __all__ 条目
- _Requirements: 9.1_
- [x] 5.3 在 `orchestration/task_registry.py` 中注册 SPI 任务
- `default_registry.register("DWS_SPENDING_POWER_INDEX", SpendingPowerIndexTask, requires_db_config=False, layer="INDEX", depends_on=["DWS_MEMBER_CONSUMPTION"])`
- _Requirements: 9.1, 9.2_
- [-] 6. 配置种子数据
- [x] 6.1 在 `db/etl_feiqiu/seeds/seed_index_parameters.sql` 中追加 SPI 参数
- 插入 `index_type='SPI'` 的全部参数行(窗口、基数、权重、映射、稳定性)
- 金额压缩基数使用合理初始默认值
- _Requirements: 7.1, 7.4, 8.5_
- [~] 6.2 在测试库执行种子数据脚本
- 通过 TEST_DB_DSN 连接测试库执行 INSERT
- _Requirements: 7.1_
- [~] 7. 检查点 - 确保单元测试和集成验证通过
- 运行 `cd apps/etl/connectors/feiqiu && pytest tests/unit/test_spi_task.py -v`
- 确保所有测试通过,如有问题请询问用户。
- [ ] 8. 文档更新
- [~] 8.1 编写数据库手册 `docs/database/BD_Manual_dws_member_spending_power_index.md`
- 包含表结构、字段说明、索引、验证 SQL至少 3 条)、兼容性说明、回滚策略
- _Requirements: 11.1_
- [~] 8.2 更新 ETL 任务文档 `apps/etl/connectors/feiqiu/docs/etl_tasks/index_tasks.md`
- 新增 DWS_SPENDING_POWER_INDEX 章节,包含算法公式、参数清单、数据来源、计算流程
- 更新概述表格和继承体系图
- _Requirements: 11.2, 11.3_
- [~] 9. 最终检查点 - 确保所有测试通过
- 运行属性测试:`cd C:\NeoZQYY && pytest tests/test_spi_properties.py -v`
- 运行单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit/test_spi_task.py -v`
- 确保所有测试通过,如有问题请询问用户。
## 备注
- 标记 `*` 的子任务为可选(属性测试),可跳过以加速 MVP
- 每个任务引用具体需求编号以确保可追溯
- 检查点确保增量验证
- 属性测试验证全称正确性属性,单元测试验证具体示例和边界情况