16 KiB
16 KiB
需求文档:团购详情接口整合 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字段
需求
需求 1:DetailFetcher 串行请求与异步处理
RateLimiter 和 CancellationToken 已在 etl-unified-pipeline 中实现并上线,本 spec 直接复用。
用户故事: 作为 ETL 运维人员,我希望团购详情拉取采用"串行请求 + 异步处理 + 单线程写库"的架构,复用现有限流和取消机制,在控制请求频率的同时最大化数据处理吞吐。
验收标准
- THE Detail_Fetcher SHALL 采用严格串行请求模式:等待上一个 API 请求的响应完整返回后,再等待 5 至 20 秒之间的随机间隔(均匀分布),然后发送下一个请求
- THE Detail_Fetcher SHALL 在每个请求的响应返回后,立即将响应数据提交到异步处理队列,不阻塞下一次请求的等待计时
- THE Detail_Fetcher SHALL 支持多个工作线程并行消费处理队列中的响应数据(字段提取、数据清洗、content_hash 计算等)
- THE Detail_Fetcher SHALL 使用单独的写入线程汇总所有处理完成的结果,统一执行数据库写入操作,避免并发写入冲突
- THE Detail_Fetcher SHALL 复用现有 RateLimiter(
api/rate_limiter.py)控制请求间隔,通过构造参数配置最小/最大间隔 - WHEN 所有请求发送完毕后,THE Detail_Fetcher SHALL 等待处理队列和写入线程全部完成后再返回最终结果
- THE Detail_Fetcher SHALL 通过 CancellationToken(
utils/cancellation.py)支持外部取消信号,收到信号后立即停止发送新请求、等待当前已提交到处理队列的数据处理完成并写入数据库、然后返回部分完成的统计结果 - WHEN 取消信号到达时,THE Detail_Fetcher SHALL 在当前请求的等待间隔期或响应等待期中断,不再发起后续请求,已进入处理队列的数据不丢弃、正常完成处理和写入
需求 2:团购详情 API 拉取
用户故事: 作为数据分析师,我希望 ETL 系统能拉取每个团购套餐的详情数据(助教服务关联、可用台区、关联门店等),以便进行团购套餐的深度分析。
验收标准
- WHEN ODS_Group_Package_Task 完成团购列表拉取后,THE Detail_Fetcher SHALL 遍历所有已拉取的
couponId(包含is_enabled=0和is_delete=1的记录)调用QueryPackageCouponInfo接口 - THE Detail_Fetcher SHALL 向
https://pc.ficoo.vip/apiprod/admin/v1/PackageCoupon/QueryPackageCouponInfo发送 POST 请求,请求体为{"couponId": <id>} - THE Detail_Fetcher SHALL 严格串行发送请求,等待上一个请求响应返回后,经过 5-20 秒随机间隔再发送下一个请求
- WHEN 详情接口返回成功响应时,THE Detail_Fetcher SHALL 将响应数据提交到异步处理队列,由工作线程提取
data层级下的groupPurchasePackage、tableAreaNameList、packageCouponAssistants、grouponSiteInfos、packagePackageService、packageCouponDetailsList等字段 - IF 详情接口对某个
couponId返回错误或超时,THEN THE Detail_Fetcher SHALL 记录错误日志(含couponId和错误信息)并继续处理下一个couponId - WHEN 所有
couponId遍历完成后,THE Detail_Fetcher SHALL 等待异步处理和写入线程全部完成,然后返回拉取统计信息(成功数、失败数、跳过数)
需求 3:ODS 层团购详情数据存储
用户故事: 作为数据工程师,我希望团购详情数据以结构化方式存储在 ODS 层,以便下游 DWD 层消费。
验收标准
- THE ETL_System SHALL 将团购详情数据存储到 ODS 层,具体方案(独立新表
ods.group_buy_package_details还是在现有ods.group_buy_packages中扩展字段)在技术设计阶段通过实际数据调研后确定(见附录 B:待调研项) - 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) - 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(券明细数组) - THE ODS_Detail_Table SHALL 包含 ETL 元数据字段:
content_hash(TEXT)、payload(JSONB,完整原始响应)、fetched_at(TIMESTAMPTZ) - THE ETL_System SHALL 采用全量快照模式(
SnapshotMode.FULL_TABLE)写入 ODS_Detail_Table,每次运行覆盖全部记录 - WHEN 详情接口返回的字段中存在未标注字段且所有记录的值均相同时,THE ETL_System SHALL 忽略该字段不写入 ODS 表
需求 4:DWD 层团购维度表扩展
用户故事: 作为数据分析师,我希望 DWD 层的团购维度表能包含详情数据中的关键字段,以便在结算分析和财务审计中使用团购套餐的完整信息。
验收标准
- THE ETL_System SHALL 在 DWD 层扩展团购维度数据,具体方案(在现有
dwd.dim_groupbuy_package_ex扩展表中新增字段,还是新建独立详情维度表)在技术设计阶段通过实际数据调研后确定(见附录 B:待调研项) - THE ETL_System SHALL 在 DWD 加载任务中建立 ODS 详情数据到 DWD 团购维度表的字段映射,通过
coupon_id=groupbuy_package_id关联 - WHEN ODS 详情表中存在
assistant_services数据时,THE DWD_Groupbuy_Package_Ex SHALL 保留完整的助教服务关联信息(skillId、assistantLevel、assistantDuration),assistantDuration单位为秒 - THE ETL_System SHALL 通过 SCD2 机制追踪团购详情字段的历史变化,与现有 DWD_Groupbuy_Package_Ex 的 SCD2 版本保持同步
- WHEN
ods.group_buy_package_details中某个coupon_id在ods.group_buy_packages中不存在时,THE ETL_System SHALL 记录警告日志并跳过该记录的 DWD 加载
需求 5:任务编排与依赖管理
用户故事: 作为 ETL 运维人员,我希望团购详情拉取任务能正确嵌入现有调度流程,不影响其他任务的执行顺序。
验收标准
- THE Detail_Fetcher SHALL 作为 ODS_Group_Package_Task 的内部子流程执行,在列表数据写入 ODS 完成后串行启动,不注册为独立的调度任务
- WHEN ODS_Group_Package_Task 执行时,THE ETL_System SHALL 先完成
QueryPackageCouponList的全量拉取和 ODS 写入,再启动 Detail_Fetcher 遍历详情 - THE ODS_Group_Package_Task SHALL 在执行结果中包含详情拉取的统计信息(详情成功数、详情失败数),与列表拉取统计合并返回
- IF Detail_Fetcher 执行过程中发生不可恢复的错误(如网络完全不可达),THEN THE ODS_Group_Package_Task SHALL 将任务状态标记为部分成功,并在结果中记录已完成的列表拉取数据和详情拉取的中断点
需求 6:文档同步更新
用户故事: 作为开发者,我希望所有相关文档在功能交付时同步更新,以保持文档与代码的一致性。
验收标准
- WHEN 团购详情 ETL 功能开发完成后,THE ETL_System 的文档 SHALL 更新以下内容:ODS 层 DDL 文档(新增
ods.group_buy_package_details表定义)、ODS 字段映射文档(新增QueryPackageCouponInfo映射)、数据库手册(BD Manual 中新增详情表说明) - WHEN DWD 层扩展字段添加完成后,THE ETL_System 的文档 SHALL 更新:DWD 全景文档(
docs/reports/dwd-panorama/dwd-dimension-panorama.md中 dim_groupbuy_package_ex 章节)、DWD 表结构概览文档 - THE ETL_System 的 README 文档 SHALL 更新任务清单,反映 ODS_GROUP_PACKAGE 任务新增的详情拉取子流程
- WHEN 全局限流机制实现后,THE ETL_System 的架构文档 SHALL 新增限流机制的说明,包含配置参数和使用方式
附录 A:API 请求与响应示例
请求
POST https://pc.ficoo.vip/apiprod/admin/v1/PackageCoupon/QueryPackageCouponInfo
Content-Type: application/json
Authorization: Bearer <token>
{"couponId": 3030873437310021}
响应
{
"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:待调研项(技术设计阶段完成)
调研 1:ODS 层表方案
- 选项 A:新建独立表
ods.group_buy_package_details - 选项 B:在现有
ods.group_buy_packages中扩展字段 - 决策依据:详情数据与列表数据的字段重叠度、数据量差异、写入模式兼容性
- 需要调研现有
ods.group_buy_packages的表结构和写入逻辑
调研 2:DWD 层表方案
- 选项 A:在现有
dwd.dim_groupbuy_package_ex扩展表中新增 JSONB 字段 - 选项 B:新建独立的团购详情维度表
- 决策依据:现有扩展表的字段密度、SCD2 版本同步复杂度、下游查询模式
- 需要调研现有 DWD 团购相关表结构(参考
docs/reports/dwd-panorama/)
调研 3:未标注字段用途
- 获取全部团购的详情数据后,对附录 A 中标记为 ❌ 的字段进行值分布分析
- 所有记录值完全相同的字段 → 忽略,不写入 ODS
- 值有变化的字段 → 推测用途,决定是否纳入 ODS/DWD 字段映射
调研 4:空数组字段实际数据
packagePackageService:获取全部团购后确认是否有非空数据packageCouponDetailsList:同上- 如有数据,需补充 ODS 字段定义和 DWD 映射