Files
junhong_cmp_fiber/openspec/changes/unified-export-task-system/design.md
huang fe4c545308
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
修正数据
2026-04-27 18:10:08 +08:00

156 lines
6.8 KiB
Markdown
Raw 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.
## Context
当前项目已具备成熟的异步导入任务能力(设备导入、单卡导入)与对象存储能力,但导出能力尚未形成统一任务系统。面对未来超大数据量,直接同步导出会引发请求超时、内存峰值高、用户等待时间不可控等问题。
本次变更要先建立“通用导出任务骨架”,并以 `device``iot_card` 作为首批场景验证架构可扩展性;导出字段与具体查询细节后续按场景迭代补充。
约束条件:
- 必须遵循 `Handler -> Service -> Store -> Model` 分层。
- 必须使用 Asynq 异步任务机制。
- 禁止外键约束,任务主从关系通过 ID 字段维护。
- 常量、状态枚举、Redis Key 必须定义在 `pkg/constants/`,禁止硬编码。
## Goals / Non-Goals
**Goals:**
- 提供全局导出任务中心,统一任务创建、列表、详情、取消。
- 支持 `xlsx/csv` 两种格式选择。
- 支持异步分片执行,满足大数据量导出性能需求。
- 导出产物上传 OSS并在任务详情直接返回可下载 URL默认 24h
- 设计可扩展场景机制,首批支持 `device``iot_card`
**Non-Goals:**
- 本阶段不定义最终导出字段清单与前端展示细节。
- 本阶段不实现跨场景聚合导出(仅单场景任务)。
- 本阶段不实现复杂运营能力(如任务重跑、任务克隆、批量取消)。
## Decisions
### 决策1统一导出引擎 + 场景策略注册
**选择**:采用“通用导出任务引擎 + 场景策略Strategy+ 执行模板Template Method”模式。
**理由**
- 避免设备导出、单卡导出重复实现任务编排、分片、上传、状态流转。
- 后续新增导出场景仅需注册策略(查询器、表头、行映射),无需改核心引擎。
**备选方案**:每个场景单独实现导出任务链路。
**放弃原因**:重复代码多,后续维护成本高,难以统一性能与取消语义。
---
### 决策2三段式异步任务流水线
**选择**Asynq 任务拆分为 `dispatch -> shard -> finalize` 三段。
**执行语义**
- `dispatch`:按查询规模创建分片子任务并入队。
- `shard`:每个分片独立查询与写文件,上传 OSS回写分片状态。
- `finalize`:聚合分片结果,生成主任务最终结果(`file_key/download_url`)。
**理由**
- 分片并行可线性提升吞吐。
- 分片失败可独立重试,不影响已完成分片。
- finalize 统一收口,便于生成一致的下载结果。
---
### 决策3任务数据模型拆分主任务与分片任务
**选择**:新增两张表(不建外键):
- `tb_export_task`(主任务)
- `tb_export_shard_task`(分片任务)
**关键字段**
- 主任务:`task_no``scene``format``status``progress``cancel_requested``file_key``error_message``started_at``completed_at`
- 分片任务:`task_id``shard_no``status``cursor_start/cursor_end``row_count``file_key``error_message`
**事务设计**
- `dispatch` 中“创建分片记录 + 分片任务入队标记”在事务内完成,避免分片记录与队列状态不一致。
- `shard``finalize` 使用状态条件更新保证幂等(如 `WHERE status = expected`)。
---
### 决策4下载信息仅在详情接口返回且直出可下载 URL
**选择**
- 任务列表仅返回任务元信息,不返回下载 URL。
- 任务详情在 `status=已完成` 时返回可直接下载的 `download_url`,有效期默认 24 小时。
**理由**
- 减少列表接口的预签名生成开销。
- 与用户已确认的交互模式一致。
---
### 决策5取消机制采用“控制面标记 + Worker 协作停止”
**选择**
- 取消接口将主任务标记为“取消请求”(`cancel_requested=true`)。
- `dispatch/shard/finalize` 在关键节点检查取消标记,命中即停止并回写取消状态。
**理由**
- 兼容已运行任务,避免强杀导致的中间态脏数据。
- 实现简单且可控,符合当前 Asynq 使用方式。
**权衡**
- 取消不是“瞬时终止”,而是“尽快停止”;单个分片运行中的小批次会执行到检查点再退出。
---
### 决策6性能基线采用 Keyset 分页 + 流式写出
**选择**
- 分片数据读取使用 Keyset 分页(`id > last_id`),避免深分页性能衰减。
- 文件写出采用流式写出策略,避免全量数据载入内存。
- 进度按批回写(例如每 N 行)降低数据库写放大。
**理由**
- 满足未来超大数据量导出场景。
- 控制数据库与 Worker 资源占用,减少峰值风险。
---
### 决策7依赖注入与模块边界
**选择**
- Handler 仅做参数解析、权限前置与统一响应。
- Service 注入 `ExportTaskStore``ExportShardTaskStore``queue.Client``storage.Service``SceneRegistry`
- Worker 通过 `pkg/queue/handler.go` 统一注册导出相关处理器。
**理由**
- 与现有导入任务模式保持一致,降低接入复杂度。
- 强化模块边界,便于后续场景扩展。
## Risks / Trade-offs
- **[风险] 分片大小配置不合理导致队列拥塞或分片过小**
**缓解**:分片大小与并发度配置化;增加任务监控指标与告警阈值。
- **[风险] xlsx 在极大数据量场景下生成耗时高**
**缓解**:允许用户选择 `csv`;必要时按分片生成多文件并由 finalize 打包归档。
- **[风险] 取消与 finalize 并发竞争造成状态抖动**
**缓解**统一使用状态条件更新finalize 前再次检查 `cancel_requested`
- **[权衡] 详情接口实时生成预签名 URL 会增加一次存储调用**
**接受**:仅详情触发,调用频率低,收益大于成本。
## Migration Plan
1. 新增导出任务常量、状态枚举、任务类型、Redis Key 生成函数。
2. 新增导出主任务与分片任务模型、Store、数据库迁移。
3. 新增全局导出任务 Handler/Service/Route创建、列表、详情、取消
4. 新增导出 Worker 处理器并注册 Asynq 任务类型dispatch/shard/finalize
5. 接入首批场景策略:`device``iot_card`(先完成骨架与最小可运行实现)。
6. 更新接口文档生成器与业务文档,补充手工验证步骤。
回滚策略:
- 若上线后发现异常,可先关闭导出任务入口(路由开关/权限收敛),保留已生成文件不影响现网核心交易链路。
- 数据库迁移按标准 down 脚本回滚新增表与字段。
## Open Questions
- 是否需要在任务列表返回“最近一次下载 URL 过期时间”用于前端提示(当前方案不返回,仅详情返回)?
- 分片默认大小与队列权重的初始值是否按环境差异化配置(开发/生产)?