Files
Neo-ZQYY/.kiro/specs/etl-coupon-detail/requirements.md

257 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 需求文档:团购详情接口整合 ETL 数据流
## 简介
将飞球 SaaS 的团购详情接口(`QueryPackageCouponInfo`)整合到现有 ETL 的 API → ODS → DWD 三层数据流中。当前 ETL 仅拉取团购列表(`QueryPackageCouponList`),缺少每个团购套餐的详情数据(助教服务关联、可用台区列表、关联门店等)。本次改造在现有团购列表拉取任务完成后,串行遍历所有 `couponId` 调用详情接口,将详情数据落入 ODS 层并向下传导至 DWD 层,丰富团购维度表的分析能力。采用"串行请求 + 异步处理 + 单线程写库"架构在控制请求频率5-20 秒随机间隔)的同时最大化数据处理吞吐。
## 术语表
- **ETL_System**:飞球 Connector ETL 系统(`apps/etl/connectors/feiqiu/`),负责从飞球 SaaS API 拉取数据并加载到 PostgreSQL 的 ODS → DWD → DWS 各层
- **API_Client**ETL 系统中的 HTTP 客户端模块(`api/client.py`),封装 POST 请求、重试、分页逻辑
- **Rate_Limiter**:请求间隔控制器,在串行请求模式下控制相邻两次 API 请求之间的随机等待时间5-20 秒),防止触发上游风控
- **ODS_Group_Package_Task**:现有 ODS 层团购套餐拉取任务(任务代码 `ODS_GROUP_PACKAGE`),调用 `QueryPackageCouponList` 获取团购列表并写入 `ods.group_buy_packages`
- **Detail_Fetcher**:团购详情拉取子流程,在列表拉取完成后遍历所有 `couponId` 调用 `QueryPackageCouponInfo` 获取详情
- **ODS_Detail_Table**ODS 层团购详情表(`ods.group_buy_package_details`),存储详情接口返回的原始数据
- **DWD_Groupbuy_Package**DWD 层团购套餐维度主表(`dwd.dim_groupbuy_package`
- **DWD_Groupbuy_Package_Ex**DWD 层团购套餐维度扩展表(`dwd.dim_groupbuy_package_ex`
- **SCD2**:缓慢变化维度 Type 2通过版本号和生效/失效时间追踪维度历史变化
- **couponId**:团购套餐的唯一标识 ID对应 `ods.group_buy_packages.id` 字段
## 需求
### 需求 1DetailFetcher 串行请求与异步处理
> RateLimiter 和 CancellationToken 已在 etl-unified-pipeline 中实现并上线,本 spec 直接复用。
**用户故事:** 作为 ETL 运维人员,我希望团购详情拉取采用"串行请求 + 异步处理 + 单线程写库"的架构,复用现有限流和取消机制,在控制请求频率的同时最大化数据处理吞吐。
#### 验收标准
1. THE Detail_Fetcher SHALL 采用严格串行请求模式:等待上一个 API 请求的响应完整返回后,再等待 5 至 20 秒之间的随机间隔(均匀分布),然后发送下一个请求
2. THE Detail_Fetcher SHALL 在每个请求的响应返回后,立即将响应数据提交到异步处理队列,不阻塞下一次请求的等待计时
3. THE Detail_Fetcher SHALL 支持多个工作线程并行消费处理队列中的响应数据字段提取、数据清洗、content_hash 计算等)
4. THE Detail_Fetcher SHALL 使用单独的写入线程汇总所有处理完成的结果,统一执行数据库写入操作,避免并发写入冲突
5. THE Detail_Fetcher SHALL 复用现有 RateLimiter`api/rate_limiter.py`)控制请求间隔,通过构造参数配置最小/最大间隔
6. WHEN 所有请求发送完毕后THE Detail_Fetcher SHALL 等待处理队列和写入线程全部完成后再返回最终结果
7. THE Detail_Fetcher SHALL 通过 CancellationToken`utils/cancellation.py`)支持外部取消信号,收到信号后立即停止发送新请求、等待当前已提交到处理队列的数据处理完成并写入数据库、然后返回部分完成的统计结果
8. WHEN 取消信号到达时THE Detail_Fetcher SHALL 在当前请求的等待间隔期或响应等待期中断,不再发起后续请求,已进入处理队列的数据不丢弃、正常完成处理和写入
### 需求 2团购详情 API 拉取
**用户故事:** 作为数据分析师,我希望 ETL 系统能拉取每个团购套餐的详情数据(助教服务关联、可用台区、关联门店等),以便进行团购套餐的深度分析。
#### 验收标准
1. WHEN ODS_Group_Package_Task 完成团购列表拉取后THE Detail_Fetcher SHALL 遍历所有已拉取的 `couponId`(包含 `is_enabled=0``is_delete=1` 的记录)调用 `QueryPackageCouponInfo` 接口
2. THE Detail_Fetcher SHALL 向 `https://pc.ficoo.vip/apiprod/admin/v1/PackageCoupon/QueryPackageCouponInfo` 发送 POST 请求,请求体为 `{"couponId": <id>}`
3. THE Detail_Fetcher SHALL 严格串行发送请求,等待上一个请求响应返回后,经过 5-20 秒随机间隔再发送下一个请求
4. WHEN 详情接口返回成功响应时THE Detail_Fetcher SHALL 将响应数据提交到异步处理队列,由工作线程提取 `data` 层级下的 `groupPurchasePackage``tableAreaNameList``packageCouponAssistants``grouponSiteInfos``packagePackageService``packageCouponDetailsList` 等字段
5. IF 详情接口对某个 `couponId` 返回错误或超时THEN THE Detail_Fetcher SHALL 记录错误日志(含 `couponId` 和错误信息)并继续处理下一个 `couponId`
6. WHEN 所有 `couponId` 遍历完成后THE Detail_Fetcher SHALL 等待异步处理和写入线程全部完成,然后返回拉取统计信息(成功数、失败数、跳过数)
### 需求 3ODS 层团购详情数据存储
**用户故事:** 作为数据工程师,我希望团购详情数据以结构化方式存储在 ODS 层,以便下游 DWD 层消费。
#### 验收标准
1. THE ETL_System SHALL 将团购详情数据存储到 ODS 层,具体方案(独立新表 `ods.group_buy_package_details` 还是在现有 `ods.group_buy_packages` 中扩展字段)在技术设计阶段通过实际数据调研后确定(见附录 B待调研项
2. THE ODS_Detail_Table SHALL 包含以下结构化字段:`coupon_id`BIGINT, PK`package_name`TEXT`duration`INT`start_time`TIMESTAMPTZ`end_time`TIMESTAMPTZ`add_start_clock`TEXT`add_end_clock`TEXT`is_enabled`INT`is_delete`INT`site_id`BIGINT`tenant_id`BIGINT`create_time`TIMESTAMPTZ`creator_name`TEXT
3. THE ODS_Detail_Table SHALL 将数组类型字段以 JSONB 格式存储:`table_area_ids`(台区 ID 列表)、`table_area_names`(台区名称列表)、`assistant_services`(助教服务关联数组,含 `skillId``assistantLevel``assistantDuration`)、`groupon_site_infos`(关联门店数组)、`package_services`(套餐服务数组)、`coupon_details_list`(券明细数组)
4. THE ODS_Detail_Table SHALL 包含 ETL 元数据字段:`content_hash`TEXT`payload`JSONB完整原始响应`fetched_at`TIMESTAMPTZ
5. THE ETL_System SHALL 采用全量快照模式(`SnapshotMode.FULL_TABLE`)写入 ODS_Detail_Table每次运行覆盖全部记录
6. WHEN 详情接口返回的字段中存在未标注字段且所有记录的值均相同时THE ETL_System SHALL 忽略该字段不写入 ODS 表
### 需求 4DWD 层团购维度表扩展
**用户故事:** 作为数据分析师,我希望 DWD 层的团购维度表能包含详情数据中的关键字段,以便在结算分析和财务审计中使用团购套餐的完整信息。
#### 验收标准
1. THE ETL_System SHALL 在 DWD 层扩展团购维度数据,具体方案(在现有 `dwd.dim_groupbuy_package_ex` 扩展表中新增字段,还是新建独立详情维度表)在技术设计阶段通过实际数据调研后确定(见附录 B待调研项
2. THE ETL_System SHALL 在 DWD 加载任务中建立 ODS 详情数据到 DWD 团购维度表的字段映射,通过 `coupon_id` = `groupbuy_package_id` 关联
3. WHEN ODS 详情表中存在 `assistant_services` 数据时THE DWD_Groupbuy_Package_Ex SHALL 保留完整的助教服务关联信息(`skillId``assistantLevel``assistantDuration``assistantDuration` 单位为秒
4. THE ETL_System SHALL 通过 SCD2 机制追踪团购详情字段的历史变化,与现有 DWD_Groupbuy_Package_Ex 的 SCD2 版本保持同步
5. WHEN `ods.group_buy_package_details` 中某个 `coupon_id``ods.group_buy_packages` 中不存在时THE ETL_System SHALL 记录警告日志并跳过该记录的 DWD 加载
### 需求 5任务编排与依赖管理
**用户故事:** 作为 ETL 运维人员,我希望团购详情拉取任务能正确嵌入现有调度流程,不影响其他任务的执行顺序。
#### 验收标准
1. THE Detail_Fetcher SHALL 作为 ODS_Group_Package_Task 的内部子流程执行,在列表数据写入 ODS 完成后串行启动,不注册为独立的调度任务
2. WHEN ODS_Group_Package_Task 执行时THE ETL_System SHALL 先完成 `QueryPackageCouponList` 的全量拉取和 ODS 写入,再启动 Detail_Fetcher 遍历详情
3. THE ODS_Group_Package_Task SHALL 在执行结果中包含详情拉取的统计信息(详情成功数、详情失败数),与列表拉取统计合并返回
4. IF Detail_Fetcher 执行过程中发生不可恢复的错误如网络完全不可达THEN THE ODS_Group_Package_Task SHALL 将任务状态标记为部分成功,并在结果中记录已完成的列表拉取数据和详情拉取的中断点
### 需求 6文档同步更新
**用户故事:** 作为开发者,我希望所有相关文档在功能交付时同步更新,以保持文档与代码的一致性。
#### 验收标准
1. WHEN 团购详情 ETL 功能开发完成后THE ETL_System 的文档 SHALL 更新以下内容ODS 层 DDL 文档(新增 `ods.group_buy_package_details` 表定义、ODS 字段映射文档(新增 `QueryPackageCouponInfo` 映射、数据库手册BD Manual 中新增详情表说明)
2. WHEN DWD 层扩展字段添加完成后THE ETL_System 的文档 SHALL 更新DWD 全景文档(`docs/reports/dwd-panorama/dwd-dimension-panorama.md` 中 dim_groupbuy_package_ex 章节、DWD 表结构概览文档
3. THE ETL_System 的 README 文档 SHALL 更新任务清单,反映 ODS_GROUP_PACKAGE 任务新增的详情拉取子流程
4. WHEN 全局限流机制实现后THE ETL_System 的架构文档 SHALL 新增限流机制的说明,包含配置参数和使用方式
---
## 附录 AAPI 请求与响应示例
### 请求
```
POST https://pc.ficoo.vip/apiprod/admin/v1/PackageCoupon/QueryPackageCouponInfo
Content-Type: application/json
Authorization: Bearer <token>
{"couponId": 3030873437310021}
```
### 响应
```json
{
"data": {
"groupPurchasePackage": {
"count": 1,
"tableAreaId": [2791960001957765],
"tableAreaNameList": ["A区"],
"id": 3030873437310021,
"add_end_clock": "1.00:00:00",
"add_start_clock": "00:00:00",
"area_tag_type": 1,
"card_type_ids": "0",
"coupon_money": 0.00,
"create_time": "2025-12-31 12:23:56",
"creator_name": "店长:郑丽珊",
"date_info": "",
"date_type": 1,
"duration": 7200,
"end_clock": "1.00:00:00",
"end_time": "2027-01-01 00:00:00",
"group_type": 1,
"is_delete": 0,
"is_enabled": 1,
"is_first_limit": 1,
"max_selectable_categories": 0,
"package_id": 1173128804,
"package_name": "助理教练竞技教学两小时",
"selling_price": 0.00,
"site_id": 2790685415443269,
"sort": 100,
"start_clock": "00:00:00",
"start_time": "2025-07-21 00:00:00",
"system_group_type": 1,
"table_area_id": "0",
"table_area_id_list": "",
"table_area_name": "",
"tenant_id": 2790683160709957,
"tenant_table_area_id": "0",
"tenant_table_area_id_list": "",
"type": 1,
"usable_count": 0,
"usable_range": ""
},
"couponCode": "",
"grouponSiteInfos": [
{
"siteId": 2790685415443269,
"siteName": "朗朗桌球"
}
],
"packageCouponAssistants": [
{
"couponAssistantId": 3030873437310023,
"skillId": 0,
"assistantLevel": "",
"assistantDuration": 7200
}
],
"packagePackageService": [],
"packageCouponDetailsList": []
},
"code": 0
}
```
### 字段标注
| 字段路径 | 含义 | 已标注 |
|----------|------|--------|
| `groupPurchasePackage.id` | 团购 ID= couponId | ✅ |
| `groupPurchasePackage.package_name` | 团购名称 | ✅ |
| `groupPurchasePackage.duration` | 台费计时时长(秒) | ✅ |
| `groupPurchasePackage.start_time` / `end_time` | 可用日期范围 | ✅ |
| `groupPurchasePackage.add_start_clock` / `add_end_clock` | 可用时段范围 | ✅ |
| `groupPurchasePackage.is_enabled` | 是否启用 | ✅ |
| `groupPurchasePackage.is_delete` | 是否已删除 | ✅ |
| `groupPurchasePackage.site_id` | 店铺 ID | ✅ |
| `groupPurchasePackage.tenant_id` | 租户 ID | ✅ |
| `groupPurchasePackage.create_time` | 创建时间 | ✅ |
| `groupPurchasePackage.creator_name` | 创建人 | ✅ |
| `groupPurchasePackage.tableAreaId` / `tableAreaNameList` | 可用台桌区域 | ✅ |
| `packageCouponAssistants[].skillId` | 可用课程 ID | ✅ |
| `packageCouponAssistants[].assistantLevel` | 可用助教等级 | ✅ |
| `packageCouponAssistants[].assistantDuration` | 助教时长(秒) | ✅ |
| `grouponSiteInfos[].siteId` / `siteName` | 关联门店 | ✅ |
| `groupPurchasePackage.count` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.area_tag_type` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.card_type_ids` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.coupon_money` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.date_info` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.date_type` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.end_clock` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.group_type` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.is_first_limit` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.max_selectable_categories` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.package_id` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.selling_price` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.sort` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.start_clock` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.system_group_type` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.table_area_id` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.table_area_id_list` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.table_area_name` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.tenant_table_area_id` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.tenant_table_area_id_list` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.type` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.usable_count` | 未标注 — 待调研 | ❌ |
| `groupPurchasePackage.usable_range` | 未标注 — 待调研 | ❌ |
| `couponCode` | 未标注 — 待调研 | ❌ |
| `packageCouponAssistants[].couponAssistantId` | 未标注 — 待调研 | ❌ |
| `packagePackageService` | 示例为空 — 待调研实际数据 | ❌ |
| `packageCouponDetailsList` | 示例为空 — 待调研实际数据 | ❌ |
---
## 附录 B待调研项技术设计阶段完成
### 调研 1ODS 层表方案
- 选项 A新建独立表 `ods.group_buy_package_details`
- 选项 B在现有 `ods.group_buy_packages` 中扩展字段
- 决策依据:详情数据与列表数据的字段重叠度、数据量差异、写入模式兼容性
- 需要调研现有 `ods.group_buy_packages` 的表结构和写入逻辑
### 调研 2DWD 层表方案
- 选项 A在现有 `dwd.dim_groupbuy_package_ex` 扩展表中新增 JSONB 字段
- 选项 B新建独立的团购详情维度表
- 决策依据现有扩展表的字段密度、SCD2 版本同步复杂度、下游查询模式
- 需要调研现有 DWD 团购相关表结构(参考 `docs/reports/dwd-panorama/`
### 调研 3未标注字段用途
- 获取全部团购的详情数据后,对附录 A 中标记为 ❌ 的字段进行值分布分析
- 所有记录值完全相同的字段 → 忽略,不写入 ODS
- 值有变化的字段 → 推测用途,决定是否纳入 ODS/DWD 字段映射
### 调研 4空数组字段实际数据
- `packagePackageService`:获取全部团购后确认是否有非空数据
- `packageCouponDetailsList`:同上
- 如有数据,需补充 ODS 字段定义和 DWD 映射