## 1. 新增 DataSource 接口与 ExportParams 类型,删除旧 SceneStrategy 实现 - [x] 1.1 在 `internal/exporter/datasource.go` 新建文件,定义 `DataSource` 接口(`Scene / Count / Headers / Fetch`)和 `ExportParams` 结构体(`Filters map[string]any / ScopeShopIDs []uint / UserType int`) - [x] 1.2 在 `internal/exporter/query_params.go` 新建文件,实现 `ParseExportParams(task *model.ExportTask) ExportParams`:从 `task.QueryJSON` 解析 `filters`,从 `task.ScopeShopIDs` 和 `task.CreatorUserType` 填充权限字段 - [x] 1.3 删除 `internal/exporter/device_scene.go` 和 `internal/exporter/iot_card_scene.go` 中的全部旧 `SceneStrategy` 实现代码(文件内容清空,后续第 5 组重写为 `DataSource` 实现) - [x] 1.4 重写 `internal/exporter/registry.go`:`Registry` 只存储 `DataSource`,删除 `SceneStrategy` 相关类型和方法;`Get` 直接返回 `DataSource`;`NewDefaultRegistry` 暂时为空(第 5 组补充) - [x] 1.5 删除 `internal/exporter/scope.go` 中 `applyShopScopeForTask` 函数(该逻辑后续由各 DataSource 实现内部通过 `ExportParams` 处理,不再作为公共函数) - [x] 1.6 用 `go build ./internal/exporter/...` 确认编译无错误 ## 2. 修改 dispatch:改为 offset/limit 切分分片 - [x] 2.1 修改 `internal/model/export_task.go` 中 `ExportShardTask`:将 `CursorStart / CursorEnd uint64` 字段语义改为 `Offset / Limit int`(字段名改为 `ShardOffset / ShardLimit`),同步更新 `gorm` 标签注释 - [x] 2.2 新增数据库迁移文件 `migrations/000XXX_alter_export_shard_task_offset.up.sql`:在 `tb_export_shard_task` 中添加 `shard_offset bigint not null default 0` 和 `shard_limit int not null default 0` 列(保留原 `cursor_start / cursor_end` 列兼容旧任务) - [x] 2.3 修改 `internal/task/export_dispatch.go`:`buildShards` 方法改为调用 `DataSource.Count` 获取总行数,按 `ExportDefaultShardSize` 计算 offset/limit 对,生成 `ExportShardTask` 列表(填 `ShardOffset / ShardLimit`);删除原 Keyset 游标逻辑 - [x] 2.4 修改 `internal/task/export_dispatch.go`:dispatch 阶段调用 `DataSource.Headers` 一次,将结果序列化后存入 `task.QueryJSON` 的 `resolved_headers` 子键(通过新增 `ExportTaskStore.UpdateResolvedHeaders` 方法写入) - [x] 2.5 修改 `internal/store/postgres/export_task_store.go`:新增 `UpdateResolvedHeaders(ctx, taskID, headers []string, updater uint) error` 方法 - [x] 2.6 用 `go build ./internal/task/...` 确认编译无错误 ## 3. 修改 shard:调用 DataSource.Fetch,分片文件不含表头 - [x] 3.1 修改 `internal/task/export_shard.go`:shard 阶段从 `task.QueryJSON` 的 `resolved_headers` 读取表头;调用 `DataSource.Fetch(ctx, params, shard.ShardOffset, shard.ShardLimit)` 获取数据行,删除原 `strategy.QueryRows` 调用 - [x] 3.2 修改 `internal/task/export_common.go` 的 `writeExportFile`:新增 `withoutHeader bool` 参数,当为 `true` 时跳过写表头行;shard 阶段调用时传 `true`,finalize 写入表头 - [x] 3.3 用 `go build ./internal/task/...` 确认编译无错误 ## 4. 修改 finalize:合并分片 OSS 文件,不重查数据库 - [x] 4.1 在 `pkg/storage/types.go` 确认(或新增)`Provider` 接口有 `Download(ctx, key string) (io.ReadCloser, error)` 方法;若无则添加并在对应实现中实现 - [x] 4.2 修改 `internal/task/export_finalize.go`:所有分片成功后,按 `shard_no` 升序循环从 OSS 下载各分片文件,流式追加写入本地临时 CSV 文件;写完后在文件头部插入 `resolved_headers` 行 - [x] 4.3 修改 `internal/task/export_finalize.go`:XLSX 格式走降级路径——合并 CSV 后用 `excelize` 流式写出 xlsx,而非直接合并 xlsx 分片 - [x] 4.4 用 `go build ./internal/task/...` 确认编译无错误 ## 5. 迁移 device 和 iot_card 场景到 DataSource 接口 - [x] 5.1 重写 `internal/exporter/device_scene.go`:实现 `DataSource` 接口,`Count` 按权限快照计总数,`Headers` 返回固定列头(非动态),`Fetch` 用 GORM 加 `ORDER BY id ASC LIMIT ? OFFSET ?` 查询并应用 `ExportParams.Filters`(支持 `status / shop_id / created_at_start / created_at_end` 过滤) - [x] 5.2 重写 `internal/exporter/iot_card_scene.go`:同上,支持 `status / shop_id / carrier_name / created_at_start / created_at_end` 过滤条件;`Fetch` 中用 `LEFT JOIN tb_package_usage` 补充套餐信息列(可选列,通过 `Filters["with_package"]` 开关控制) - [x] 5.3 修改 `internal/exporter/registry.go` 的 `NewDefaultRegistry`:改为注册新的 `DataSource` 实现,删除旧 `SceneStrategy` 注册 - [x] 5.4 用 MCP PostgreSQL 工具分别对 device 和 iot_card 场景各抽取一批数据,对比新 `Fetch` 输出与原 `QueryRows` 输出一致(重点验证权限过滤和筛选条件) - [x] 5.5 用 `go build ./...` 确认全项目编译无错误 ## 6. 执行数据库迁移并验证 - [x] 6.1 执行 `migrate -path migrations -database "..." up` 应用 shard 表字段迁移 - [x] 6.2 用 MCP PostgreSQL 工具确认 `tb_export_shard_task` 表已新增 `shard_offset / shard_limit` 列 - [x] 6.3 确认存量 shard 记录的 `cursor_start / cursor_end` 列数据未被破坏(迁移为纯新增列,无数据变更) ## 7. 端到端验证 - [x] 7.1 通过 API 创建一个 `scene=iot_card, format=csv` 的导出任务,传入筛选条件 `{"filters": {"shop_id": 5}}`,用 MCP 查询 `tb_export_task` 确认 `query_json` 已记录筛选参数 - [x] 7.2 等待任务完成,用 MCP 查询 `tb_export_shard_task` 确认各分片的 `shard_offset / shard_limit` 已按预期填写,`status = 3`(已成功) - [x] 7.3 调用任务详情接口获取 `download_url`,下载文件验证:列头与 `Headers` 一致、数据行仅含 `shop_id=5` 的记录、无重复行、无缺失行 - [x] 7.4 创建一个 `scene=device, format=xlsx` 的导出任务(不传筛选条件),验证全量导出正常,xlsx 文件可正常打开 - [x] 7.5 创建导出任务后立即取消(`status=处理中`),验证取消流程正常,任务最终状态为已取消