# PRD：UR#36 批量订购套餐

Status: ready-for-agent

---

## Problem Statement

运营人员需要根据一份离散资产清单批量为卡或设备订购套餐。现有后台单笔下单只能一次处理一个资产，无法提供批次进度、逐行结果、部分成功恢复和稳定幂等；原始需求中把整批理解为单一代理，也无法覆盖同一文件包含多个代理名下资产的真实场景。

本需求需要在不弱化现有单笔订单规则的前提下，增加 CSV 直传、异步解析和逐行下单能力。批次只统一支付方式，不统一代理：系统在处理每一行时根据资产当前归属确定结算代理，再校验该代理的套餐授权、成本价和主钱包。文件结构错误必须整批失败且不产生订单；资产、套餐、归属、重复行和余额等业务错误允许逐行失败。

## Solution

新增“批量订购套餐”入口。操作员在创建批次时选择一个套餐和一种支付方式，前端通过现有对象存储预签名接口把只含资产标识的 CSV 和线下凭证直传真实私有 S3，再提交唯一 `request_id`、`package_id`、整批支付方式和稳定对象 Key 创建任务；请求不包含 `shop_id`，也不上传文件字节。

创建接口完成对象归属、类型和 10MB 大小校验后立即返回任务。Worker 下载完整 CSV，先完成文件级校验并持久化逐行明细，再严格按 CSV 行号执行。每个有效业务行在独立事务中解析当前结算代理、复用统一套餐可售策略和订单领域，以 `wallet` 扣结算代理主钱包，或以 `offline` 创建已支付订单。任务和明细使用状态条件、处理租约及稳定幂等键保证重复消费不会重复下单或扣款。

## User Stories

1. 作为平台运营人员，我希望上传一份 CSV 为多张卡或设备订购套餐，而不是逐笔创建订单。
2. 作为平台运营人员，我希望同一 CSV 可以包含不同代理名下的资产，不需要预先按代理拆文件或在页面选择代理。
3. 作为平台运营人员，我希望整批明确选择钱包或线下支付，避免一份文件混合不同支付语义。
4. 作为运营人员，我希望 CSV 只填写资产标识，由系统统一识别卡或设备及其支持的标识形式。
5. 作为平台运营人员，我希望文件结构错误整批失败且不产生订单，而单行业务错误不影响其他合规行。
6. 作为平台运营人员，我希望任务刷新后仍能恢复进度，并按行查看结算代理、金额、订单和中文失败原因。
7. 作为财务人员，我希望线下凭证作为批次业务资料永久保留，但本期不把它扩展为财务核销系统。
8. 作为代理，我希望批量钱包订购只扣我的主钱包，并继续遵守我的套餐授权、成本价和信用额度规则。
9. 作为审计人员，我希望每一行都能追溯原始标识、规范资产、结算代理、套餐、金额和最终订单。
10. 作为系统维护人员，我希望重复 HTTP 请求、重复 Worker 消费和进程中断恢复都不会产生重复订单或重复扣款。

## Implementation Decisions

### 范围与领域口径

- “批量订购批次”是一份选择单个套餐并使用统一支付方式的 CSV 订购任务，不绑定单一代理。
- “结算代理”是每一行处理时根据资产当前归属解析出的代理；套餐授权、成本价、钱包、订单买卖方快照和失败原因均以该代理为准。
- `shop_id` 不是创建参数、CSV 字段或前端隐含参数。后端即使收到未知字段也不能据此改变结算代理。
- 批量用例复用 Order 领域和统一 Wallet Domain，只迁移完成本用例所需的最小完整复杂写边界；列表、任务详情和明细使用 Query 通道。
- 不复制现有巨大订单 Service 的宽松分支。需要把资产、套餐、可售策略、定价、订单、套餐生效和钱包扣款收口为可由单笔后台下单与批量单行命令共同调用的领域/Application 能力。
- 本需求不改变现有单笔后台下单 HTTP 接口，也不把未触碰的订单模块一次性整体迁移。

### 入口与现有认证边界

- 不新增 `order:bulk_purchase` 或其他批量订购权限码，不在后端增加超级管理员、平台、代理或企业类型的显式拦截。
- 页面是否展示入口由前端现有菜单与可见性规则决定；能够通过现有后台认证调用接口的主体，后端视为可以使用该能力。
- 创建、任务详情和明细接口只复用现有后台路由认证及公共数据范围机制，不额外建立任务创建人隔离或新的 RBAC 判断。
- “无新增后端权限拦截”不等于跳过业务规则。每一行仍必须校验资产当前归属、结算代理、所选套餐授权、可售状态、定价和钱包。
- 创建任务成功、任务终态及每个成功订单均写公共审计。审计包含任务号、支付方式、文件安全摘要、凭证数量、汇总、操作者及逐行关联，不记录预签名 URL、鉴权令牌或环境密钥。

### CSV 契约

- 只接受 `.csv`，不接受 `.xlsx`、`.xls` 或把 Excel 文件改扩展名后的内容。
- 编码固定为 UTF-8，可带 UTF-8 BOM；换行允许 LF 或 CRLF。
- 前端随版本发布静态模板，后端不增加模板下载接口。建议模板文件名为 `批量订购套餐模板-v1.csv`。
- 固定且唯一的表头为单列 `资产标识`。缺列、多列、重复列或未知列均为文件级错误；CSV 不包含资产类型、套餐编码或套餐名称。
- 套餐由创建任务请求中的单个 `package_id` 决定，整个批次的每一行都订购该套餐。
- 资产标识去除首尾空白后交给系统统一资产解析能力，解析结果包含资产类型、资产 ID 和规范标识。批量订购不得另写一套卡/设备识别分支或维护自己的标识白名单。
- 当前统一解析能力支持 ICCID、卡 `virtual_no`、MSISDN、设备 `virtual_no`、IMEI 和 SN；未来统一解析能力新增或修正标识规则时，批量订购自动复用。
- 空资产标识、未命中或无法唯一解析属于行级失败；禁止猜测或自动修复科学计数法、控制字符等被破坏的数据。
- 单个文件最大 10MB，数据行最多 1000 行。空文件、只有表头、非法 UTF-8、CSV 引号语法错误、表头错误或第 1001 行出现，均使任务整体失败且不创建任何订单。
- Worker 必须先完成整个文件的结构和行数校验，再开始任何订单写入；不能边解析边下单后才发现文件级错误。
- 文件有效后，每一个数据行都持久化为明细。业务无效行、空字段行和重复行仍占用原始行号并计入失败，确保 `total_count = success_count + fail_count`。

### 资产解析与文件内判重

- Worker 通过统一资产解析能力得到唯一资产类型、资产 ID 和规范标识后再判定文件内重复，不能直接按用户填写的字符串判重。
- 明细同时保留用户原始资产标识和统一解析结果；规范标识的选取规则属于统一资产能力，批量订购不自行决定卡或设备的标识优先级。
- 未命中或命中多个资产时该行失败；统一资产解析能力必须保证唯一结果，不能使用 `First` 任取一条。
- 文件内重复键为统一解析得到的 `资产类型 + 资产ID`。整个批次只有一个套餐，因此重复键不再包含套餐。
- 重复键首次出现的行正常进入业务处理，后续行失败并在 `failure_reason` 中指出首次出现的 CSV 行号。
- 本期不把重复行解释为数量；未来需要多份订购时再增加明确数量字段。
- 统一资产解析能力必须提供适用于批量调用的接口，避免逐行跨表查询形成 N+1；历史脏数据多命中必须明确失败，不能由批量用例任取一条。

### 对象存储与上传归属

- 复用 `POST /api/admin/storage/upload-url`，新增用途 `bulk_purchase`。该用途只生成批量订购目录下的 `.csv` Key，Content-Type 固定为允许的 CSV 类型。
- 线下凭证继续使用 `attachment` 用途；允许项目现有支持的图片或文件类型，数量沿用后台订单凭证上限，当前为 1 至 5 个。
- 前端使用预签名 URL 直接 `PUT` 到当前真实私有 S3，业务接口只接收稳定 `file_key` 和 `voucher_keys`，不接收 multipart 或字节流。
- 现有存储 Provider 缺少对象元数据和上传主体证明。实现必须补充对象元数据查询能力，至少返回对象是否存在、实际大小和 Content-Type；同时保存预签名上传授权记录，包含 Key、purpose、申请人账号、声明文件名/类型和签发时间。
- 创建任务时校验 CSV Key 来自 `bulk_purchase` 用途、上传授权属于当前账号、对象真实存在、实际大小不超过 10MB、扩展名和 Content-Type 合法。凭证 Key 也必须属于当前账号的附件上传授权并真实存在。
- 上传授权在任务创建事务中绑定到该任务。同一 Key 可以随相同 `request_id` 的幂等重试返回原任务，但不能被另一个批次或另一个账号再次绑定。
- 预签名 URL 和永久对象 Key 是不同概念。任务只保存稳定私有 Key，不保存会过期的上传 URL；查询时按现有受权下载机制展示凭证。
- 源 CSV 和凭证按对象存储统一生命周期保留。Worker 下载失败属于任务级基础设施失败；已经确定的订单结果不因后续对象清理失败而回滚，但必须记录告警。
- 自动化测试直接调用真实 S3，不实现或保留内存 Provider 替身。测试使用本次运行的唯一对象 Key，并在结束时只删除自己创建的对象；真实上传、Head、下载或删除失败都必须使相应测试失败。

### 创建任务 API

- `POST /api/admin/bulk-purchases` 使用 JSON 请求：

```json
{
  "request_id": "01J...",
  "package_id": 1001,
  "payment_method": "wallet",
  "file_key": "bulk-purchase/2026/07/unique.csv",
  "voucher_keys": []
}
```

- `request_id` 必填，最大 64 字符，由前端为一次用户提交生成全局唯一值。`package_id` 必填且大于 0，一个任务只接受一个套餐。`payment_method` 只允许复用现有后台订单枚举 `wallet|offline`，不新增 `agent_wallet`。
- 创建任务时加载并快照所选套餐的 ID、编码和名称，确认套餐存在；各结算代理是否拥有授权、价格是否有效及当前是否可售仍在逐行处理时按最新事实校验。
- `wallet` 时 `voucher_keys` 必须为空；`offline` 时必须提供 1 至 5 个凭证 Key。一个批次不能混合支付方式。
- 请求 DTO 不声明 `shop_id`，创建接口若检测到显式提交 `shop_id` 必须返回参数错误，不能忽略后让调用方误以为它参与了结算；其他未知字段延续项目统一 JSON 兼容策略，但不参与业务决策。
- `request_id` 建立数据库唯一约束。相同 `request_id`、相同操作者和相同请求指纹重复提交时返回原任务，不重复绑定文件、创建任务或投递消息。
- 相同 `request_id` 但 `package_id`、支付方式、文件 Key、凭证 Key 或操作者不同，返回冲突错误，不能静默返回语义不同的旧任务。
- 创建事务保存任务、上传授权绑定和可靠任务事件。Asynq 不可用时不能丢失已提交任务；数据库中的待处理任务/事件是事实源，由投递器重试发送。
- 成功响应至少返回 `task_id`、`task_no`、`request_id`、所选套餐快照、`payment_method`、`status`、`status_name` 和 `created_at`。成功只表示任务已接收，不表示 CSV 已通过或订单已创建。

### 任务与明细查询 API

- `GET /api/admin/bulk-purchases/{task_id}` 返回任务汇总，至少包括：
  - 任务 ID、任务号、请求 ID、所选套餐 ID/编码/名称快照、支付方式。
  - 状态与中文状态名、源文件名、凭证数量和有权预览所需的附件引用。
  - 总行数、已处理数、成功数、失败数、涉及代理数。
  - 全部有效成功行金额合计；金额单位固定为分，类型为 `int64`。
  - 任务级错误码与中文原因、操作者快照、创建/开始/完成时间。
- `GET /api/admin/bulk-purchases/{task_id}/items` 支持 `page`、`page_size`、`status` 和 `asset_identifier`：
  - 默认第 1 页、每页 20 条，`page_size` 最大 100。
  - 默认按 `row_no ASC`，不允许前端改变业务处理顺序。
  - `status` 只允许明细状态枚举；多个筛选参数使用 AND 组合。
  - `asset_identifier` 去除首尾空白后，对该任务内的用户原始标识或规范标识做精确匹配，不做跨任务搜索。
- 明细响应至少包括：行号、解析后的资产类型、原始资产标识、解析资产 ID、规范标识、结算代理 ID/名称快照、金额、状态与中文名、订单 ID、失败码、中文失败原因和处理时间；套餐信息统一来自任务快照，不保存不存在的逐行套餐输入。
- 查询仅返回业务安全信息，不返回 SQL、底层错误、对象存储永久凭证、Redis Key 或其他代理的钱包余额。
- 所有接口使用统一 `{code,msg,data,timestamp}` 响应。参数校验统一返回参数错误；任务不存在返回统一不存在错误；现有后台认证失败沿用公共认证错误；同一请求 ID 的不同载荷、文件已绑定等返回冲突；对象不存在、类型错误、文件过大和存储失败复用或补齐统一存储错误码。

### 数据模型与索引

- 新建独立任务表和逐行明细表，不复用语义不同的导入、导出或设备批量分配表；不建立数据库外键或 GORM 关联标签。
- 任务至少保存：任务号、`request_id`、请求指纹、源文件 Key/名称/类型/大小、所选套餐 ID/编码/名称快照、支付方式、凭证 Key 快照、操作者账号/类型/名称快照、涉及代理数、总数/已处理数/成功数/失败数、成功金额、状态、任务级错误码/原因、任务租约持有者/到期时间、开始/完成时间和公共审计字段。
- 明细至少保存：任务 ID、CSV 行号、统一解析得到的资产类型、原始资产标识、解析资产 ID、规范标识快照、结算代理 ID/名称快照、金额、状态、订单 ID、失败码/原因、幂等键、处理租约和处理时间。
- 上传授权记录需要稳定保存 Key、purpose、申请账号、声明元数据、绑定业务类型/ID和绑定时间，以便创建接口证明“属于当前上传主体”；不通过可猜测目录前缀代替归属校验。
- 任务号、`request_id` 和上传授权 Key 使用有效记录唯一索引；明细使用 `(task_id,row_no)` 和 `idempotency_key` 唯一索引，并为 `(task_id,status,row_no)` 建查询索引。
- `request_id` 是全局唯一；行幂等键固定为 `bulk_purchase:{task_id}:{row_no}`。
- 状态类字段使用 `int`，类型/方式类使用 `string`。任务复用全局异步任务状态常量，不定义 Bulk 私有任务状态；逐行明细使用公共行处理状态。支付方式、资产类型、失败码和 Redis Key 生成函数定义到公共常量包，并添加中文注释。
- 不需要迁移或回填历史订单。新表上线前为空；已成功生成的标准订单继续按现有订单事实保留。

### 状态机与任务恢复

- 全局异步任务状态统一为：`1:待处理, 2:处理中, 3:已完成, 4:已失败, 5:已取消`；任务响应必须包含对应 `status_name`。
- 文件有效并完成全部行业务处理后，任务固定为 `3:已完成`。全部成功、部分成功或全部业务行失败由 `success_count` 与 `fail_count` 表达，不创建“部分成功”状态。
- 文件级校验失败、源对象下载失败或无法恢复的任务级基础设施错误进入 `4:已失败`。文件级失败不得创建订单；是否保留零条明细由错误发生阶段决定，并通过任务错误说明。
- 逐行明细不是独立异步任务，使用公共行处理状态：`1:待处理, 2:处理中, 3:成功, 4:失败`，并返回对应 `status_name`；不为明细增加没有业务语义的取消状态。
- 本期任务没有取消入口，但保留全局任务状态码 `5:已取消`，不把它改作其他含义。
- Worker 使用状态条件和带过期时间的租约领取任务。只有待处理或租约已过期的处理中任务可被领取；终态不可被重新执行。
- 文件校验通过后，在开始下单前一次性持久化全部明细。Worker 重启时读取现有明细继续，而不是重新生成不同的行号或幂等键。
- 每个明细也通过条件更新或行锁领取。成功/失败终态不可被另一消费者覆盖；租约过期的处理中明细根据事务事实安全恢复。
- 任务汇总始终从明细表重新聚合，不信任进程内累加值。`processed_count = success_count + fail_count`，终态时等于 `total_count`。
- Asynq 载荷只传结构化 `task_id`，不得传预序列化 `[]byte`、CSV 字节、临时路径、凭证内容或认证上下文。

### 严格行序与逐行业务流程

- 文件通过后严格按 `row_no ASC` 串行推进业务结算。可以批量预加载只读数据，但不得并发执行钱包扣款或改变行序结果。
- 每行处理时重新读取资产当前归属，不使用创建任务时的归属快照；结算代理不存在、归属异常或没有有效主钱包时只失败当前行。
- 解析出的结算代理必须拥有对应套餐当前有效授权；金额使用该代理当前授权成本价和现有订单定价规则，不使用 CSV 套餐名称或前端金额。
- 每行复用统一套餐可售策略。批量订购不是个人本人续费入口，不能利用后台或批量身份绕过下架限制；渠道下架套餐固定拒绝。
- 继续执行现有后台订单中适用于该支付方式的资产状态、套餐组合、互斥、使用期、生效、赠送、强充等不变量。批量入口不得复制一套较宽规则。
- `offline` 把整批凭证快照关联到每个成功订单，直接创建符合现有后台线下语义的已支付订单并激活套餐；不扣代理钱包。
- `wallet` 逐行锁定该资产结算代理的有效主钱包，使用统一公式计算可用金额：只有启用信用时才计入有效信用额度。
- `wallet` 不预占整批或某一代理的全部金额。当前行余额不足只失败当前行并继续；后续金额更小的行在当时可用金额足够时仍可成功。因此，同一代理资金不足时 CSV 行序就是订购优先级。
- 单个成功行的事务必须原子提交：订单、订单明细、套餐使用/激活、结算代理与价格快照、钱包余额与版本、钱包真实金额流水、明细成功状态，以及本用例要求的可靠事件/审计。
- 单行事务失败不得留下已扣钱包但无订单、已有订单但明细仍可重复执行，或套餐已生效但订单回滚的中间事实。
- 行级业务失败记录稳定 `failure_code` 和中文原因后继续下一行；底层数据库、S3 或 Redis 错误不能原样写给用户。

### 幂等、并发与失败码

- HTTP 幂等由 `request_id` 唯一约束和请求指纹保护；任务投递幂等由任务 ID 和任务状态/租约保护；单行幂等由 `(task_id,row_no)`、稳定幂等键、明细行锁/条件状态和单行事务共同保护。
- 同一任务的两个 Worker、Worker 崩溃后重试以及 Asynq 至少一次投递都不能重复创建订单、扣款、写钱包流水、激活套餐或写成功审计。
- 钱包扣款使用统一 Wallet Domain 的版本/条件更新或等价并发保护，不能只在内存判断余额；并发扣款后总可用金额不得小于 0。
- 与普通订单并发购买同一资产时仍要由 Order Domain 保护套餐和资产不变量，不能只依赖批量任务自身租约。
- 推荐稳定任务失败码包括：`storage_download_failed`、`invalid_encoding`、`invalid_header`、`unknown_column`、`invalid_csv`、`empty_file`、`row_limit_exceeded`。
- 推荐稳定行失败码包括：`invalid_asset_identifier`、`asset_not_found`、`asset_identifier_ambiguous`、`duplicate_row`、`package_not_authorized`、`package_not_purchasable`、`settlement_agent_invalid`、`main_wallet_not_found`、`insufficient_balance`、`order_create_failed`。
- 失败码供前端稳定展示和筛选，`failure_reason` 使用用户可理解中文并可补充首次重复行号等上下文；不把整个中文文案当作程序判断条件。

### 线下凭证语义

- 线下凭证是整个批次的业务资料快照，所有成功的线下订单都能追溯到该批次及凭证。
- 本期不校验同一凭证是否在其他批次使用，不建立凭证金额与订单金额的自动核销，也不因重复文件内容拒绝批次。
- 凭证可以是图片或文件，保存私有对象 Key；页面通过受权下载地址预览或下载，不把附件字节写入 CSV、数据库大字段或任务载荷。
- 对象存储中的凭证按现有附件保留策略长期可访问；不能把创建时的短期预签名 URL 当作永久业务地址。

### 前端交互

- 页面采用“参数确认 → 上传 → 处理中 → 结果”四个稳定阶段，可放在批量订购独立页或现有订单页入口。
- 参数阶段选择单个套餐和整批支付方式，不展示代理选择器。`offline` 显示 1 至 5 个凭证上传；`wallet` 不显示或清空凭证。
- CSV 模板是前端静态资源，只有 `资产标识` 一列，说明标识复用系统统一识别能力，当前支持 ICCID、卡虚拟号、MSISDN、设备虚拟号、IMEI 和 SN，并明确不接受 Excel。
- 整个 CSV 使用参数阶段选择的套餐，前端不让用户在行内填写或为不同资产选择不同套餐。
- 前端先申请上传 URL并直传真实 S3，再以同一个用户动作生成的 `request_id` 创建任务。网络超时重试必须复用原 `request_id`，用户主动新建批次才生成新值。
- 提交前展示所选套餐、支付方式、CSV 文件名和凭证数量，不展示虚构的单一代理或整批钱包余额；提交期间禁止重复点击。
- 创建成功后保存任务 ID并刷新详情。页面刷新、关闭后重开或网络恢复时，可以根据任务 ID恢复，不依赖持续驻留的轮询内存状态。
- 处理中显示任务号、操作员、总数、已处理数、成功数、失败数、成功金额和涉及代理数。解析尚未完成时允许总数为 0，并显示“正在校验文件”。
- 结果页默认筛选失败明细，可切换全部/成功/失败，并按资产标识精确搜索；展示行号、原始资产、规范资产、批次套餐、结算代理、金额、订单和中文原因。
- 任务完成后根据成功数和失败数展示“全部成功/部分成功/全部业务行失败”的结果摘要，不把“部分成功”当成状态码。本期没有单行重试接口；用户复制失败行、修正后重新上传会创建全新任务，旧任务历史不改变。
- 文件级失败展示任务错误和模板修正建议；行级失败展示逐行原因。前端不得自行推断或改写后端失败码。

### 发布、回滚与依赖

- 钱包路径依赖统一 Wallet Domain 已具备信用启用开关、有效信用额度公式、版本并发保护和真实金额流水。若 UR#38 尚未落地，必须先完成这段共享能力，禁止在批量代码中复制旧余额判断。
- 套餐校验依赖统一可售策略能够区分 C 端本人续费与后台/批量入口。若 UR#40 尚未落地，必须先具备该策略，批量不能暂时放宽下架限制。
- 发布包含新表、索引、权限、上传用途、存储元数据能力、API、任务投递器和 Worker。Worker 尚未部署或数据库迁移未完成时不得开放前端入口。
- 上线前验证 PostgreSQL、Redis、Asynq 和真实 S3 配置，生成接口文档，并完成上传、任务、钱包、线下、部分成功和现有后台认证联调。
- 发布窗口内短暂停止新批量任务，先部署兼容迁移和 Worker，再部署 API/前端。旧版本不会读取新表，不需要历史回填。
- 回滚时先关闭前端入口和任务创建，等待或人工处置已领取任务，再回滚应用。已经创建的标准订单、钱包流水、套餐使用、任务和审计均作为业务事实保留，不做反向删除。
- 不在仍有待处理或处理中任务时删除新表或上传用途；数据库降级迁移不是常规应用回滚步骤。

## 公共能力发布依赖

- 最终 11 号发布票阻塞于公共基础 12 号票、全局审计 19 号票和 UR#38 钱包切换 10 号票；公共能力未完成不阻止独立切片开发，但阻止生产放量。
- 批量任务必须登记事件类型、载荷版本、Worker 幂等键和逐行失败明细；订单与钱包仍以各自 Domain Ledger 为权威，任务创建、终态及人工操作进入 Audit Event。

## Testing Decisions

### 可复用 Agent 集成测试规范与工具

- 本需求实现时同时沉淀一份“Agent 集成测试规范”，覆盖环境防误连、夹具命名、唯一运行标识、精确清理、失败后清理、敏感信息保护和禁止事项，供后续批量任务复用。
- 提供可复用 Go 测试 Harness，至少封装：配置守卫、真实 PostgreSQL/Redis/S3 连接、Fiber 请求、测试账号和真实认证令牌、受控任务投递捕获、公开 Worker Handler 驱动、统一响应解码、资源清理台账。
- 为本需求提供 UTF-8、BOM、非法表头、重复资产、多代理、钱包不足和线下凭证等固定 CSV `testdata`，并提供穿过公开边界的完整示例测试。
- Go 集成测试是主要自动化入口；`curl` 只作为已部署环境的真实网络冒烟模板，验证登录、预签名上传、PUT、创建、查询和终态，不承担并发、幂等和数据库断言。
- 测试公共行为，不直接测试私有函数。创建接口使用 Fiber `app.Test` 穿过真实路由、认证、Handler、Application/Domain、GORM 和统一响应；Worker 通过公开任务 Handler 驱动，不依赖私有解析方法。
- 为避免后台 Worker 抢任务，集成测试在应用注入点捕获待投递 `task_id`，然后直接调用公开 Worker Handler；不通过 `sleep` 等待异步碰运气。生产仍使用真实 Asynq 投递器。

### Redis 与 Asynq 隔离

- 已部署测试环境固定使用 Redis DB 6；本地开发和 Agent 自动化测试固定使用 Redis DB 7。
- 每次自动化测试启动前必须读取实际生效配置，并确认普通 Redis Client、Asynq Client 和 Worker Server 的 DB 都等于 7。任一不是 7 时立即失败，绝不向 DB 6 写普通 Key或投递任务。
- 测试不得执行 `FLUSHDB`、全前缀扫描删除或清空队列。每次运行生成唯一 `run_id`，Redis 业务键、任务标识和清理名单只指向本次实际创建的精确 Key。
- 测试结束和失败清理都按资源台账删除精确 Key；其他本地进程在 DB 7 的数据不受影响。

### PostgreSQL 共享测试库

- 自动化测试继续使用当前现有测试数据库，不创建新的数据库或专用数据库。
- 每次运行创建带唯一 `run_id` 的平台账号、权限、代理、钱包、资产、套餐、授权和其他夹具，只使用本次创建记录的实际主键执行业务。
- 清理按依赖顺序和实际主键精确删除本次创建的记录；禁止 `TRUNCATE`、整表删除、模糊条件删除、复用后篡改现有业务数据或假设测试库为空。
- 测试开始前记录资源清理台账；任何中途失败仍执行清理，并将未清理的精确 ID 输出为诊断信息，但不得输出密码、Token、S3 密钥或完整敏感业务数据。
- 钱包并发测试也在该共享测试库内使用完全独立的本次夹具，不能锁定或扣减已有代理钱包。

### 真实 S3

- 自动化测试直接使用当前可调用的真实 S3 Provider，不使用内存替身、临时本地对象模拟或绕过预签名协议。
- 每个测试对象 Key 都包含唯一 `run_id`，CSV 和凭证走真实上传；测试覆盖对象 Head/元数据、下载和精确删除。
- 清理只删除本次资源台账中记录的对象 Key，禁止删除目录前缀或执行 Bucket 级清理。
- 真实 S3 网络、鉴权、上传、下载、Head 或删除失败均使集成测试失败，便于及早发现环境和 Provider 契约问题。

### 后端场景

- 上传契约覆盖 `bulk_purchase` purpose、`.csv`、实际 Content-Type、10MB 边界、对象不存在、其他账号 Key、其他用途 Key、Key 已绑定、凭证类型和 1/5/6 个凭证。
- CSV 解析覆盖 UTF-8、UTF-8 BOM、LF、CRLF、单列中文固定表头、缺失/额外/重复列、非法引号、空文件、只有表头、1000/1001 行、空字段、控制字符和伪装 Excel。
- 资产解析通过统一公共接缝覆盖卡 ICCID、卡虚拟号、唯一 MSISDN、MSISDN 未命中/多命中、设备虚拟号、IMEI、SN、未命中和历史多命中；断言批量用例没有另一套识别规则。
- 判重覆盖同一资产使用不同受支持标识只首行处理、后续指出首行号，以及无法解析资产的行不会误判成同一个空资产。
- 认证测试覆盖现有后台认证有效和失效；断言没有新增 `order:bulk_purchase`、账号类型拦截或任务创建人隔离。
- 请求幂等覆盖相同 `request_id` 同载荷返回原任务、改变 `package_id` 或其他载荷/操作者时冲突、并发创建只有一个任务和一个可靠事件。
- 文件级错误验证任务失败且订单、钱包流水、套餐使用均为零；行业务错误验证部分成功且已成功行不被回滚；全部行业务失败仍是任务已完成且 `success_count=0`、`fail_count=total_count`。
- `wallet` 覆盖多代理各扣自己的主钱包、信用启用/禁用、无主钱包、余额不足、行序优先、前一行不足但后一便宜行成功，以及不产生整批预占。
- `offline` 覆盖凭证必填、成功订单立即支付和激活、不扣钱包、凭证可跨批复用且保留批次关联。
- 套餐规则覆盖创建时套餐不存在、任务套餐快照、各结算代理的授权成本、未授权、禁用、下架不能被批量绕过，以及现有组合/资产状态/强充规则；任何价格都不能由前端或 CSV 提交。
- 重复消费覆盖两个 Worker 同时领取、任务租约过期、单行处理中崩溃、事务提交前/后故障、重复 Asynq 消息，断言订单、扣款、流水和激活各只有一次。
- 查询覆盖默认分页、最大页大小、行号排序、状态和资产标识筛选使用 AND、任务汇总、全局状态名称和失败码。
- 审计覆盖任务创建/终态、逐行订单和钱包事实，并验证日志和响应不包含环境密钥、认证令牌、预签名 URL 或底层错误。

### 前端、联调和完成命令

- 前端验收覆盖入口可见性、单套餐选择、四阶段页面、单列静态 CSV 模板、真实直传、支付方式切换、线下凭证、重复点击、刷新恢复、解析中空进度、计数表达部分成功、失败默认筛选、精确搜索和新任务重试。
- 部署联调使用 `curl` 模板完成真实 HTTP 和真实 S3 PUT，但凭据只从环境读取，不写入脚本、文档或终端回显；验证部署环境明确连接 Redis DB 6。
- 自动化完成门禁至少包括目标单元/集成测试、相关包测试、全量 Go 测试、竞态或并发专项测试、静态检查、OpenAPI 生成校验和数据库迁移验证。
- 新 Handler 必须同步两个接口文档生成入口；DTO 枚举描述从公共常量原文复制，所有状态响应包含中文 `status_name`。

## Out of Scope

- 不支持 Excel，不维护 CSV/Excel 双解析器。
- 不在创建请求或 CSV 中接受 `shop_id`，不让前端选择或推断结算代理。
- 不允许一批混合 `wallet` 与 `offline`，也不新增 `agent_wallet` 支付枚举。
- 不在批量订购内维护资产标识白名单或另写卡/设备识别逻辑；不支持模糊搜索或资产号段匹配。
- 不用重复行表达套餐数量，不提供单行重试或修改旧任务接口。
- 不为钱包支付预占整批或某代理全部金额，不因一行余额不足回滚其他成功行。
- 不允许批量订购绕过下架、授权、资产状态、定价、强充或订单领域规则。
- 不对线下凭证做跨批唯一、金额核销或自动财务对账。
- 不新增后端 CSV 模板下载或失败结果文件下载接口。
- 不创建新的 PostgreSQL 测试数据库，不对共享测试库或 Redis DB 7 执行破坏性清理。
- 不为对象存储实现内存替身；自动化和联调均验证当前真实 S3。
- 不新增批量订购后端权限码、账号类型拦截或任务创建人隔离。
- 不重构当前需求未触碰的整个订单、钱包或存储模块。

## Further Notes

- 当前仓库没有批量订购 API、任务模型或 Worker，需要作为新用例实现。
- 当前后台订单已使用 `wallet|offline`，批量必须复用这两个常量；部分现有 DTO 对线下凭证和支付枚举的描述不完全一致，实现时以公共常量和本规格为准并同步修正触碰处。
- 当前已有统一 `Asset.Resolve` 和全局资产标识注册表，但 fallback 对历史多命中仍可能任取第一条。本需求必须复用并完善这一个统一解析边界，使单个和批量解析都能返回唯一资产类型与 ID；不能在批量模块内再实现一套。
- 当前存储接口只有存在性、上传/下载和预签名能力，没有对象元数据与上传主体归属事实；这两项是安全接收 `file_key` 的必要实现，不得只检查字符串前缀。
- 当前 Redis 普通客户端、Asynq Client 和 Worker Server使用同一个 Redis DB 配置来源，测试守卫必须验证最终生效值为 7，而不是只检查某个环境变量字符串。
- 用户已最终确认：CSV 只有 `资产标识`，资产类型由统一解析能力识别；创建任务选择单个 `package_id`；后端不新增显性权限拦截；任务状态复用全局五态且部分成功只通过计数表达；自动化测试直接使用真实 S3，PostgreSQL 沿用现有测试库。
