修正数据
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s

This commit is contained in:
2026-04-27 18:10:08 +08:00
parent bb33232b1b
commit fe4c545308
57 changed files with 4477 additions and 226 deletions

View File

@@ -0,0 +1,103 @@
# 统一导出任务功能总结
## 1. 功能概览
本次交付新增统一导出任务系统,提供全局任务入口,支持:
- 创建导出任务(`scene=device/iot_card``format=xlsx/csv`
- 导出任务列表与详情
- 导出任务取消(待处理/处理中)
- 异步三段式执行:`dispatch -> shard -> finalize`
- 分片产物与最终产物上传 OSS
- 详情接口直出下载链接(固定 24 小时)
## 2. 新增接口
- `POST /api/admin/export-tasks`:创建导出任务
- `GET /api/admin/export-tasks`:导出任务列表(支持 `scene/status/time` 过滤)
- `GET /api/admin/export-tasks/:id`:导出任务详情(已完成时返回 `download_url`
- `POST /api/admin/export-tasks/:id/cancel`:取消导出任务
## 3. 状态机
### 3.1 主任务状态
- `1` 待处理
- `2` 处理中
- `3` 已完成
- `4` 已失败
- `5` 已取消
### 3.2 分片状态
- `1` 待处理
- `2` 处理中
- `3` 已成功
- `4` 已失败
- `5` 已取消
## 4. 核心设计
### 4.1 模型与迁移
新增两张表:
- `tb_export_task`:主任务
- `tb_export_shard_task`:分片任务
关键点:
- 无外键,仅使用 `task_id` 做逻辑关联
- 主任务保留进度、分片统计、权限快照、产物信息
- 分片任务保留游标范围、分片产物、执行状态
### 4.2 场景策略注册中心
新增 `internal/exporter` 场景注册中心与策略接口,首批接入:
- `device` 场景策略
- `iot_card` 场景策略
统一定义:
- `Headers()`:导出表头
- `NextShardBoundary()`:按 keyset 方式计算分片边界
- `QueryRows()`:按分片边界查询行数据
### 4.3 执行链路
- `export:dispatch`:构建分片并入队 `export:shard`
- `export:shard`:按分片查询数据、写文件、上传 OSS、回写分片结果
- `export:finalize`:汇总分片结果并生成最终文件,更新主任务状态
### 4.4 幂等与重试安全
- dispatch/shard/finalize 增加 Redis 锁,避免并发重复执行
- 分片状态条件更新,重复消费不会重复计数
- shard/finalize 利用 Asynq 重试上下文,在最终重试失败时才落库失败状态
### 4.5 取消语义
- 待处理:直接更新为已取消
- 处理中:仅设置 `cancel_requested=true`,由 Worker 检查点协作停止
- dispatch/shard/finalize 均增加取消检查点
### 4.6 下载链接
-`status=已完成` 返回 `download_url`
- 链接有效期固定 `24h`(常量 `ExportDownloadURLExpire`
- URL 由详情接口实时生成,过期后再次访问可刷新
## 5. 权限与可见性
- 平台/超管:可见全部导出任务
- 代理:仅可见其店铺范围内创建的导出任务
- 详情越权统一返回:`无权限操作该资源或资源不存在`
## 6. 文档与生成器更新
已同步更新:
- 路由文档(新增 `导出任务` Tag 的 4 个接口)
- 文档构建 Handler 集合(`pkg/openapi/handlers.go` 新增 `ExportTask`
- OpenAPI 文件重新生成:`docs/admin-openapi.yaml`

View File

@@ -0,0 +1,56 @@
# 统一导出任务手工验证清单
## 验证工具与原则
- 数据库结构与数据状态:使用 PostgreSQL MCP只读
- 接口契约验证:使用 Postman 或 curl
- 明确约束:不新增自动化测试文件(`*_test.go`
## A. 创建导出任务
- [ ] `POST /api/admin/export-tasks``scene=device``format=xlsx`
- [ ] `POST /api/admin/export-tasks``scene=iot_card``format=csv`
- [ ] 非法 `scene`(如 `order`)返回参数错误
- [ ] 非法 `format`(如 `pdf`)返回参数错误
## B. 列表与过滤
- [ ] `GET /api/admin/export-tasks?page=1&page_size=20` 默认分页生效
- [ ] `scene` 过滤生效(`device` / `iot_card`
- [ ] `status` 过滤生效1~5
- [ ] `start_time/end_time` 时间过滤生效
- [ ] `page_size > 100` 时被限制到 100
## C. 详情与下载
- [ ] 未完成任务详情不返回 `download_url`
- [ ] 已完成任务详情返回 `download_url`
- [ ] `download_url` 有效期固定 24 小时
- [ ] 过期后再次访问详情可刷新新链接
## D. 取消语义
- [ ] 待处理任务可取消,状态立即变为已取消
- [ ] 处理中任务可提交取消请求(`cancel_requested=true`
- [ ] 已完成/已失败/已取消任务再次取消被拒绝
- [ ] 重复取消处理中任务幂等返回
## E. 异步链路dispatch -> shard -> finalize
- [ ] 创建任务后 `export:dispatch` 被消费
- [ ] 分片任务写入 `tb_export_shard_task`
- [ ] `export:shard` 执行后分片状态更新,产物写入 OSS
- [ ] `export:finalize` 汇总后主任务状态收敛
- [ ] 分片失败时主任务最终失败,错误信息可追踪
## F. 数据权限与可见性
- [ ] 平台用户可见全量任务
- [ ] 代理仅可见其店铺范围任务
- [ ] 越权访问详情统一返回:`无权限操作该资源或资源不存在`
## G. 数据库核查项PostgreSQL MCP
- [ ] `tb_export_task` 表存在,字段与注释完整
- [ ] `tb_export_shard_task` 表存在,字段与注释完整
- [ ] 关键索引存在并生效(任务号唯一索引、`task_id+status``scene+status+created_at`

View File

@@ -0,0 +1,59 @@
# 统一导出任务验收记录
## 1. 验收范围
- 代码编译验证
- 数据库迁移验证PostgreSQL MCP
- OpenAPI 生成验证
- 手工接口链路验证准备(受环境变量约束)
## 2. 已完成结果
### 2.1 编译结果
- 命令:`go build ./...`
- 结果:通过
### 2.2 OpenAPI 结果
- 命令:`go run cmd/gendocs/main.go`
- 结果:通过
- 产物:`docs/admin-openapi.yaml`
### 2.3 数据库迁移结果
- 命令:`migrate -path migrations -database <...> up 1`
- 结果:`135/u create_export_task_tables`
### 2.4 PostgreSQL MCP 结构校验
已确认:
- 存在 `tb_export_task`
- 存在 `tb_export_shard_task`
- 表注释和字段注释已生效
已确认索引:
- `idx_export_task_no`(唯一,软删条件)
- `idx_export_task_scene_status_created_at`
- `idx_export_task_creator_user_id`
- `idx_export_task_creator_shop_id`
- `idx_export_task_creator_enterprise_id`
- `idx_export_shard_task_task_shard_no`(唯一,软删条件)
- `idx_export_shard_task_task_status`
## 3. 待手工执行项(环境阻塞)
以下项已准备 curl/Postman 清单与模板,但当前环境缺少 API 启动所需配置(`JUNHONG_REDIS_ADDRESS``JUNHONG_JWT_SECRET_KEY` 等),暂未执行:
- 接口契约验证(创建/列表/详情/取消)
- 异步状态流转观察dispatch/shard/finalize
- 两个场景(`device` / `iot_card`)各 `xlsx``csv` 完整导出
- 取消场景回归(待处理取消、处理中取消、重复取消)
可复现命令与模板见:
- `docs/unified-export-task-system/手工验证清单.md`
- `docs/unified-export-task-system/验收记录模板.md`
- `docs/unified-export-task-system/验证数据样本.md`

View File

@@ -0,0 +1,64 @@
# 统一导出任务验收记录模板
## 1. 基本信息
- 验收日期:
- 验收环境:
- 验收人:
- 代码版本commit
## 2. 请求与响应记录
### 2.1 创建任务
- 请求:
- 响应:
- 期望:
- 结果:通过 / 不通过
### 2.2 列表过滤
- 请求:
- 响应:
- 期望:
- 结果:通过 / 不通过
### 2.3 详情下载
- 请求:
- 响应:
- 期望:
- 结果:通过 / 不通过
### 2.4 取消任务
- 请求:
- 响应:
- 期望:
- 结果:通过 / 不通过
## 3. 数据库快照PostgreSQL MCP
- `tb_export_task` 关键字段快照:
- `tb_export_shard_task` 关键字段快照:
- 索引核查结果:
## 4. OSS 产物记录
- 分片文件 Key 列表:
- 最终文件 Key
- 文件大小:
## 5. 日志关键字段
- `task_id`
- `task_no`
- `scene`
- `status`
- `error`(如有):
## 6. 下载 URL 有效期验证
- 首次获取时间:
- 返回 `download_expires_at`
- 过期后刷新结果:

View File

@@ -0,0 +1,84 @@
# 统一导出任务验证数据样本
## 目标
`device``iot_card` 场景准备两类样本:
- 小数据集便于快速验证状态流转10~30 条)
- 大数据集:便于验证分片执行(>= 5000 条)
## 1. device 场景
### 1.1 小数据集(示例)
```sql
-- 建议在测试库执行,按实际字段补齐 creator/updater
INSERT INTO tb_device (creator, updater, virtual_no, device_name, device_model, device_type, max_sim_slots, manufacturer, status)
VALUES
(1, 1, 'EXP-DEV-S-0001', '导出设备样本1', 'M1', 'tracker', 4, 'JH', 1),
(1, 1, 'EXP-DEV-S-0002', '导出设备样本2', 'M1', 'tracker', 4, 'JH', 1),
(1, 1, 'EXP-DEV-S-0003', '导出设备样本3', 'M2', 'router', 4, 'JH', 1);
```
### 1.2 大数据集(示例)
```sql
-- 生成 5000 条设备样本
INSERT INTO tb_device (creator, updater, virtual_no, device_name, device_model, device_type, max_sim_slots, manufacturer, status)
SELECT
1,
1,
'EXP-DEV-L-' || LPAD(gs::text, 6, '0'),
'导出设备大样本' || gs,
'M-L',
'tracker',
4,
'JH',
1
FROM generate_series(1, 5000) gs;
```
## 2. iot_card 场景
### 2.1 小数据集(示例)
```sql
INSERT INTO tb_iot_card (
creator, updater, iccid, iccid_19, carrier_id, carrier_type, carrier_name,
msisdn, status, activation_status, real_name_status, network_status
)
VALUES
(1, 1, '89860000000000000001', '8986000000000000000', 1, 'CMCC', '中国移动', '13900000001', 1, 0, 0, 1),
(1, 1, '89860000000000000002', '8986000000000000000', 1, 'CMCC', '中国移动', '13900000002', 1, 0, 0, 1),
(1, 1, '89860000000000000003', '8986000000000000000', 1, 'CMCC', '中国移动', '13900000003', 1, 0, 0, 1);
```
### 2.2 大数据集(示例)
```sql
INSERT INTO tb_iot_card (
creator, updater, iccid, iccid_19, carrier_id, carrier_type, carrier_name,
msisdn, status, activation_status, real_name_status, network_status
)
SELECT
1,
1,
'8986001' || LPAD(gs::text, 13, '0'),
LEFT('8986001' || LPAD(gs::text, 13, '0'), 19),
1,
'CMCC',
'中国移动',
'138' || LPAD(gs::text, 8, '0'),
1,
0,
0,
1
FROM generate_series(1, 5000) gs;
```
## 3. 清理建议
```sql
DELETE FROM tb_device WHERE virtual_no LIKE 'EXP-DEV-%';
DELETE FROM tb_iot_card WHERE iccid LIKE '8986001%';
```