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

6.8 KiB
Raw Blame History

Context

当前项目已具备成熟的异步导入任务能力(设备导入、单卡导入)与对象存储能力,但导出能力尚未形成统一任务系统。面对未来超大数据量,直接同步导出会引发请求超时、内存峰值高、用户等待时间不可控等问题。

本次变更要先建立“通用导出任务骨架”,并以 deviceiot_card 作为首批场景验证架构可扩展性;导出字段与具体查询细节后续按场景迭代补充。

约束条件:

  • 必须遵循 Handler -> Service -> Store -> Model 分层。
  • 必须使用 Asynq 异步任务机制。
  • 禁止外键约束,任务主从关系通过 ID 字段维护。
  • 常量、状态枚举、Redis Key 必须定义在 pkg/constants/,禁止硬编码。

Goals / Non-Goals

Goals:

  • 提供全局导出任务中心,统一任务创建、列表、详情、取消。
  • 支持 xlsx/csv 两种格式选择。
  • 支持异步分片执行,满足大数据量导出性能需求。
  • 导出产物上传 OSS并在任务详情直接返回可下载 URL默认 24h
  • 设计可扩展场景机制,首批支持 deviceiot_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_nosceneformatstatusprogresscancel_requestedfile_keyerror_messagestarted_atcompleted_at
  • 分片任务:task_idshard_nostatuscursor_start/cursor_endrow_countfile_keyerror_message

事务设计

  • dispatch 中“创建分片记录 + 分片任务入队标记”在事务内完成,避免分片记录与队列状态不一致。
  • shardfinalize 使用状态条件更新保证幂等(如 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 注入 ExportTaskStoreExportShardTaskStorequeue.Clientstorage.ServiceSceneRegistry
  • 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. 接入首批场景策略:deviceiot_card(先完成骨架与最小可运行实现)。
  6. 更新接口文档生成器与业务文档,补充手工验证步骤。

回滚策略:

  • 若上线后发现异常,可先关闭导出任务入口(路由开关/权限收敛),保留已生成文件不影响现网核心交易链路。
  • 数据库迁移按标准 down 脚本回滚新增表与字段。

Open Questions

  • 是否需要在任务列表返回“最近一次下载 URL 过期时间”用于前端提示(当前方案不返回,仅详情返回)?
  • 分片默认大小与队列权重的初始值是否按环境差异化配置(开发/生产)?