# 资产套餐批量订购功能总结

## 交付范围

本功能复用现有对象存储、Asynq、统一异步任务五态和后台单笔订单 Service，为一批卡或设备统一购买一个套餐：

- `POST /api/admin/storage/upload-url` 使用 `purpose=batch_purchase` 获取只允许 `.csv` 的上传地址和稳定 `file_key`。
- `POST /api/admin/asset-package-batch-orders` 接收 CSV `file_key`、一个 `package_id`、整批统一的 `wallet|offline` 支付方式及可选线下凭证；请求不选择代理。
- `GET /api/admin/asset-package-batch-orders` 分页查询任务，默认 20、最大 100，可按统一五态筛选。
- `GET /api/admin/asset-package-batch-orders/:id` 返回任务汇总和逐行成功/失败明细。
- 平台可查看全部任务；代理只能查看自己及下级店铺创建的任务，权限上下文缺失时失败关闭。

## CSV 契约

- 文件必须为 UTF-8 编码的 `.csv`，允许 UTF-8 BOM，最大 10MB、最多 1000 个业务行。
- 文件只能有一列，每行填写一个资产标识。首行可使用 `资产标识`、`identifier`、`iccid` 或 `iccid/虚拟号` 表头，也可直接从第一行填写数据。
- 空资产标识作为行级失败；多列、非法 CSV、非 UTF-8、空文件和超限作为任务级失败，在任何订单写入前终止。
- 同一输入标识重复时仅第一行进入订单规则；不同标识解析为同一资产时复用订单幂等结果，后续行记录重复原因，不重复下单或扣款。

## 支付和订单规则

- 整批只选择一个套餐和一种支付方式，不选择代理。每行由现有订单 Service 根据资产当前归属解析结算关系、套餐授权、价格和钱包。
- `wallet` 不允许凭证，支持超级管理员、平台和代理账号；逐行复用现有代理主钱包扣款、信用边界、成功流水、套餐激活、佣金任务和订单幂等规则。
- `offline` 仅允许超级管理员或平台账号，必须提交 1～5 个凭证 Key；每个成功订单保存同一批凭证快照并按现有线下订单规则立即激活，不扣钱包。
- 每行独立处理，业务失败不回滚其他成功行。全部行业务失败仍表示任务完成，通过 `success_count=0` 和 `fail_count=total_count` 表达。

## 任务与幂等

- 任务状态固定为 `1:待处理, 2:处理中, 3:已完成, 4:已失败, 5:已取消`；当前版本没有取消入口，仅保留统一状态契约。
- Asynq 载荷只包含结构化 `task_id`，不传 CSV 字节、临时路径或预序列化 `[]byte`。
- Worker 通过任务状态和超时时间条件领取任务；重复消息发现任务正在有效执行或已终结时直接跳过。
- 超过任务最长执行时间仍停留在“处理中”的记录可被重新领取，用于 Worker 崩溃后恢复；未超时的处理中任务不会并发重入。
- Worker 使用任务快照恢复创建人权限；代理创建的任务仅能处理自己及下级店铺资产，权限范围查询失败时收紧为仅自己店铺，不会按平台无限制权限执行。
- 单行继续复用订单 Service 的资产级锁、Redis 业务键和钱包订单数据库幂等指纹，重复消费不得重复创建订单或扣款。

## 数据与发布

- 迁移 `000192_create_asset_package_batch_order_task` 新增 `tb_asset_package_batch_order_task`，保存套餐/操作者/支付/文件快照、五态、汇总及最多 1000 行 JSONB 结果；不建立外键。
- Worker 必须配置 Redis/Asynq 和对象存储，并监听 `asset:package:batch_order` 独立队列。
- 回滚时先隐藏创建入口并停止新任务，等待已领取任务完成。已经创建的订单、钱包流水、套餐使用和任务结果均为业务事实，不做反向删除。

## 当前验证边界

本次未运行测试、完整构建、LSP 或迁移；按用户后续明确要求已运行 OpenAPI 生成器，并执行 `gofmt`、只读一致性检查和 `git diff --check`。真实对象存储上传、Worker 消费、钱包扣款和线下订单需在后续联调流程验证。
