## Context

当前系统已经具备代理套餐列表、后台钱包支付订单、主钱包流水、卡实名/状态和套餐使用记录等能力，但这些能力主要挂在 `/api/admin` 或 C 端接口下，认证方式依赖后台 JWT 或个人客户 JWT。代理商第三方系统对接需要长期稳定的开放接口，并且调用方需要通过请求签名保证参数未被篡改。

本次设计新增 `/api/open/v1` 代理开放接口域。开放接口不直接暴露后台 JWT，也不改变现有后台接口契约，而是通过独立中间件把校验通过的后台代理账号转换为内部代理上下文，再复用已有 Service 能力。所有业务实现仍遵循 Handler → Service → Store → Model 分层。

约束：
- 复用后台代理账号密码作为当前阶段的对接凭据。
- 后台账号密码以 bcrypt 存储，服务端不能反推出明文密码。
- 流量展示不能暴露虚流量、倍率、停机阈值等内部语义。
- 开放接口暂只考虑单卡查询；套餐购买接口支持多卡输入，但按单卡分别下单。
- 不引入新依赖，签名使用 Go 标准库 HMAC-SHA256。

## Goals / Non-Goals

**Goals:**

- 提供代理开放接口认证中间件，校验账号、密码、时间戳、随机串和签名。
- 提供卡流量查询、卡状态查询、卡实名状态查询、代理套餐列表、钱包套餐购买、钱包余额、钱包流水接口。
- 将开放接口请求映射为代理账号上下文，使现有 `middleware.CanManageShop`、套餐分配过滤和钱包扣款逻辑继续生效。
- 流量字段只对外展示代理可理解的真实业务口径：总流量、已用流量、剩余流量。
- 套餐购买按卡独立处理，允许部分成功并返回失败卡列表。
- 保持分页、错误码、响应格式、日志和文档生成的一致性。

**Non-Goals:**

- 不提供设备开放接口。
- 不支持单次购买多个套餐编码。
- 不引入独立 API key / API secret；当前阶段只复用后台代理账号密码。
- 不暴露虚流量字段、展示倍率、停机阈值或内部扣减阈值。
- 不改造现有后台 `/api/admin` 接口。
- 不讨论或新增自动化测试文件。

## Decisions

### 决策 1：新增独立开放接口域 `/api/open/v1`

选择：新增 `internal/routes/open.go` 注册 `/api/open/v1` 路由组，并新增 `internal/handler/openapi` 包承载 Handler。

理由：
- 与后台管理端和 C 端接口隔离，避免第三方系统依赖后台 JWT。
- 便于统一挂载开放接口签名中间件、访问日志和限流策略。
- 不改变现有后台接口路径和权限语义。

替代方案：
- 直接开放 `/api/admin`：会暴露后台管理语义，且要求三方维护后台 JWT，不适合长期对接。

### 决策 2：复用后台代理账号密码，不新增开放接口授权表

选择：开放接口认证直接复用后台账号表和店铺数据权限。只要后台账号存在、账号类型为代理账号、账号状态启用，并且账号已绑定有效代理店铺，即默认允许调用 `/api/open/v1`。不新增 `tb_agent_open_api_account` 或其他开放接口账号授权表。

理由：
- 满足当前“复用后台代理账号密码”的低接入成本。
- 业务口径明确为所有代理店铺都能使用开放接口，不需要平台额外维护启用记录。
- 继续使用现有账号禁用和店铺状态作为统一入口控制，避免同一代理账号出现后台可用但开放接口不可用的二义性。
- 不需要保存明文密码或第二套密钥。

替代方案：
- 新增开放接口授权表：增加配置和运维成本，且与“所有代理店铺都能使用开放接口”的要求冲突。
- 新增 API secret：更安全，但当前用户已确认先复用后台账号密码。

### 决策 3：签名中间件先校验密码，再用本次请求密码重算 HMAC

选择：客户端在 Header 传入：

```text
X-Agent-Account
X-Agent-Password
X-Agent-Timestamp
X-Agent-Nonce
X-Agent-Sign
```

签名原文：

```text
METHOD + "\n" +
PATH + "\n" +
canonical_query_without_sign + "\n" +
SHA256(raw_body) + "\n" +
timestamp + "\n" +
nonce + "\n" +
account
```

签名值：

```text
hex(HMAC-SHA256(password, sign_payload))
```

服务端流程：
1. 按账号查询后台账号。
2. 校验账号类型必须为代理账号，账号状态必须启用，并且已绑定启用状态的代理店铺。
3. 使用 bcrypt 校验 `X-Agent-Password`。
4. 校验时间戳在允许时间窗内。
5. 使用 Redis `SET NX` 记录 nonce，防止重放。
6. 使用本次请求密码重算 HMAC 并进行常量时间比较。
7. 注入 `ContextKeyUserID`、`ContextKeyUserType`、`ContextKeyShopID`、`ContextKeySubordinateShopIDs` 等代理上下文。

理由：
- 由于数据库中没有明文密码，服务端只能用请求中的明文密码重算签名。
- HMAC 覆盖 method、path、query、body、timestamp、nonce、account，能发现请求参数或 body 被篡改。
- Redis nonce 防止有效时间窗内重放。

替代方案：
- `MD5(account + params + password)`：实现简单但抗碰撞和密钥使用方式不如 HMAC。
- 服务端保存可逆密码：不符合安全要求。

安全约束：
- 开放接口必须部署在 HTTPS 下；否则后台密码会被链路暴露。
- 日志、访问日志和错误日志必须脱敏 `X-Agent-Password`、`X-Agent-Sign`。

### 决策 4：开放接口业务服务做编排，核心能力复用现有 Service

选择：新增 `internal/service/agent_open_api.Service` 作为编排层，依赖注入现有服务和 Store：
- 资产/卡解析：复用资产解析或 IoT 卡 Store 的标识符查询能力。
- 套餐列表：复用 `package.Service.List` 的代理上下文过滤。
- 钱包购买：复用 `order.Service.CreateAdminOrder`，为每张卡构造 `CreateAdminOrderRequest`。
- 主钱包流水：复用 `ShopCommissionService.ListMainWalletTransactions` 或抽取其查询逻辑。
- 主钱包余额：复用 `AgentWalletStore.GetMainWallet`。

理由：
- 保持 Handler 只负责参数解析和响应。
- 避免复制后台复杂的代理套餐权限、钱包扣款、套餐激活和分佣规则。
- 让开放接口与后台业务规则保持一致。

替代方案：
- 为开放接口重新实现购买逻辑：容易绕过幂等、钱包乐观锁、套餐激活和佣金规则。

### 决策 5：流量展示只输出真实业务口径，不输出内部虚流量口径

选择：卡流量查询按 `PackageUsage` 快照计算：
- `total_flow_mb = data_limit_mb`
- `used_flow_mb = min(data_usage_mb * display_gain_ratio_snapshot, data_limit_mb)`
- `remaining_flow_mb = max(total_flow_mb - used_flow_mb, 0)`

对外字段使用 `total_flow_mb`、`used_flow_mb`、`remaining_flow_mb`，不出现 `virtual_*`、`ratio`、`threshold`、`reduction`。

理由：
- 符合“让代理觉得这就是真正的流量”的业务要求。
- 复用现有快照字段，避免套餐配置变更影响历史使用记录。
- 不泄露系统内部停机保护阈值。

替代方案：
- 直接返回 `BuildTrafficMetrics` 的所有字段：会暴露虚流量字段，不符合要求。

### 决策 6：多卡购买按单卡独立下单，允许部分成功

选择：`POST /api/open/v1/wallet/package-orders` 接收 `card_nos[]` 和单个 `package_code`。Service 按卡逐个解析套餐和创建订单，每张卡独立返回成功订单号或失败原因。

理由：
- 用户已确认允许部分成功。
- 现有订单模型以单卡/设备为订单载体，逐卡复用最稳。
- 单卡失败不影响其他卡购买，便于代理侧重试失败卡。

替代方案：
- 一个批次事务全部成功或全部失败：一张坏卡会拖住整批，不符合当前要求。

## Risks / Trade-offs

- [Risk] 复用后台密码要求客户端传明文密码 → Mitigation：强制 HTTPS 部署，日志脱敏，后续可演进为独立 API secret。
- [Risk] bcrypt 校验每次请求都有成本 → Mitigation：开放接口调用量初期可接受；后续可用短 TTL Redis 认证缓存，但必须谨慎避免密码变更后缓存过久。
- [Risk] 多卡购买部分成功导致代理侧需要处理批次差异 → Mitigation：响应固定返回 `success_count`、`failed_count`、`orders`、`failed_cards`。
- [Risk] 流量换算字段若重复手写可能与现有逻辑不一致 → Mitigation：在模型或服务层抽取开放接口专用展示方法，统一复用快照计算。
- [Risk] 签名 canonical 规则双方理解不一致 → Mitigation：在接口文档中提供明确排序、空 body、数组参数和 JSON body 签名示例。

## Migration Plan

1. 不新增数据库迁移，直接复用现有 `tb_account`、`tb_shop`、卡、套餐和钱包数据。
2. 发布代码后，所有启用状态的代理账号可按签名规则调用开放接口。
3. 若需要回滚，禁用 `/api/open/v1` 路由或回滚本次代码；已有后台功能不受影响。

## Open Questions

无。当前已确认：所有启用代理店铺均可使用开放接口、不新增开放接口账号授权表、流量不暴露虚流量、多卡部分成功、复用后台代理账号密码、一次只买一个套餐编码、套餐列表返回生效零售价。
