## 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 过期时间”用于前端提示（当前方案不返回，仅详情返回）？
- 分片默认大小与队列权重的初始值是否按环境差异化配置（开发/生产）？
