# PRD：UR#47 卡片手动限速

Status: ready-for-agent

---

## Problem Statement

当前后台只有设备维度的旧限速接口，且错误地把设备 IMEI 直接传给上游 `/device/speed-limit`；它既不能从设备定位当前卡，也不能支持独立卡资产。现有 Gateway 客户端还会对网络错误自动重试，无法区分请求未送达与上游已执行但响应丢失，可能产生不可控的重复副作用。

七月需求只要求运营人员对卡片执行一次手动限速，不涉及套餐级限速、流量阈值触发或自动恢复。系统需要用统一资产入口解析实际 ICCID，再按 Gateway 账号与运营商把业务语义档位映射为真实上游编码，并完整记录调用事实。

## Solution

新增统一 CMP 操作接口 `POST /api/admin/assets/{identifier}/speed-limit`。请求只提交稳定业务枚举 `speed_level`；卡资产直接取得 ICCID，设备资产必须先解析唯一的当前有效绑卡，再用该卡 ICCID 调用上游。

应用层在权限与资产校验后调用 Gateway Port；Gateway Adapter 根据非敏感账号标识、运营商和业务档位读取受控部署映射，仅中国电信与中国广电使用 `/flow-card/speedLimit` 直连。该操作关闭共享 Gateway 客户端的自动重试，每次尝试写 Integration Log，结果同时进入统一审计。

## User Stories

1. 作为有权管理资产的后台人员，我希望从卡详情或设备详情选择固定限速档位，而不用理解不同上游编码。
2. 作为设备管理人员，我希望设备限速实际作用于当前绑卡，而不是把 IMEI 错当成卡号。
3. 作为运营人员，我希望在运营商或映射不支持时得到明确拒绝，而不是发送猜测参数。
4. 作为审计人员，我希望查到操作者、资产、实际 ICCID、语义档位、上游编码及调用结果。
5. 作为排障人员，我希望网络结果不明时系统保留真实“未知”状态，不盲目自动重试或声称限速成功。

## Implementation Decisions

### API 契约

- 统一接口为 `POST /api/admin/assets/{identifier}/speed-limit`，同时接受当前统一资产解析器支持的卡与设备标识。
- 请求体只包含：

```json
{
  "speed_level": "limit_1_mbps"
}
```

- `speed_level` 为稳定枚举，业务语义固定如下：
  - `unlimited`：不限速，上游默认编码 `-1`。
  - `zero_kbps`：0 Kbps，上游默认编码 `0`。
  - `limit_128_kbps`：128 Kbps，上游默认编码 `1`。
  - `limit_512_kbps`：512 Kbps，上游默认编码 `2`。
  - `limit_1_mbps`：1 Mbps，上游默认编码 `3`。
  - `limit_2_mbps`：2 Mbps，上游默认编码 `4`。
  - `limit_10_mbps`：10 Mbps，上游默认编码 `5`。
  - `limit_20_mbps`：20 Mbps，上游默认编码 `6`。
  - `limit_50_mbps`：50 Mbps，上游默认编码 `7`。
  - `limit_100_mbps`：100 Mbps，上游默认编码 `8`。
- 上述编码只是当前对接资料中的默认描述，不能在 Application 或 Handler 中硬编码为跨账号通用事实。最终发送值必须由 Gateway Adapter 按账号与运营商映射取得。
- 成功响应返回资产类型、资产 ID、实际卡号、`speed_level`、实际发送编码、上游返回的 `applied_speed` 与 `channel_raw_value`。不得把目标档位描述成已经查询到的“当前实际限速”。
- 失败使用统一错误码与中文消息；参数枚举非法、无当前卡、运营商不支持、映射缺失、上游明确失败和结果未知需要可区分，但不得向客户端泄露底层错误或密钥。
- 旧 `PUT /api/admin/devices/by-identifier/{identifier}/speed-limit` 与错误的 `/device/speed-limit` 调用在同批发布时下线，不保留继续传 IMEI 的兼容路径。

### 资产与卡号解析

- 卡资产直接使用该卡的 ICCID；卡必须未删除且仍处于现有数据权限范围内。
- 设备资产只能使用唯一的当前有效绑定：`device_id` 匹配、`is_current=true`、绑定状态为有效且未软删除。上游 `cardNo` 必须是绑定卡的 ICCID，绝不使用设备 IMEI、序列号或虚拟号。
- 未找到当前有效绑卡时拒绝操作；同时找到多条当前有效绑卡属于数据不变量破坏，也必须拒绝并记录错误，不能任取第一条。
- 数据库应以不含外键的 PostgreSQL 部分唯一索引保证同一设备最多一条当前有效绑卡。迁移前先检测重复数据，发现异常则中止发布并人工修复，不能自动删除绑定事实。
- 运营商取卡片已有运营商字段及同步后的权威值。缺失、未知或与受支持能力不一致时拒绝，不根据 ICCID 前缀猜测。

### 运营商能力

- 本迭代的直连手动限速仅支持中国电信与中国广电，调用统一上游 `POST /flow-card/speedLimit`。
- 中国联通的资料要求通过通信计划调整，不等同于本接口的直接限速；在尚未形成独立通信计划适配契约前，本接口明确拒绝联通卡。
- 中国移动当前不支持本能力，明确拒绝。
- 能力校验必须发生在任何上游调用之前。前端是否隐藏入口不构成后端安全边界。
- 卡详情与设备详情可返回从后端能力判断得出的 `speed_limit_capability`，至少包含 `supported`、不支持原因和可选语义档位。设备详情同时显示实际当前 ICCID。该字段只是操作能力，不代表上游当前限速状态。

### Gateway 映射与配置

- Gateway 配置增加非敏感的逻辑账号标识，例如 `account_code`；不得用 App Secret 作为映射键，也不得把完整凭据写入日志或审计。
- 限速映射属于部署级渠道适配配置，按 `account_code + carrier + speed_level` 唯一定位上游编码，由 Viper/Gateway Adapter 管理；不放入运营人员可随意编辑的 `tb_system_config`。
- 缺少账号、运营商或档位映射时立即拒绝，禁止退回默认值、任选其他账号映射或把语义字符串原样发送给上游。
- 实现前必须用真实 Gateway 账号分别核实中国电信与中国广电各档位编码、请求签名和响应结构。未经核实的默认编码不能作为生产发布依据；映射核实是联调与发布门禁，不是让实现人员自行猜测的产品决策。
- Gateway Port 接收语义化命令并返回规范化结果；具体 `{params:{cardNo,code}}` 包装、签名、渠道响应解析和映射均封装在 Adapter 内，不泄漏进 Handler 或 Application。

### 调用可靠性与结果语义

- 限速是外部副作用操作，不沿用共享 Gateway 客户端当前的网络错误自动重试。一次 HTTP 请求只产生一次上游调用尝试。
- 每次尝试先写 Integration Log 的请求事实，再写终态。日志至少记录请求 ID、渠道、逻辑账号、运营商、脱敏卡号、语义档位、实际编码、耗时、响应摘要和结果。
- 上游返回明确业务成功时才向客户端返回成功；返回明确失败时记录失败并返回渠道操作失败。
- 超时、连接中断或无法判断上游是否执行时记录为 `unknown`，返回“渠道处理结果未知，请核实后重试”。不得自动补偿、自动重试或伪造成功。
- 用户可以在核实后再次提交同一目标档位。由于操作语义是设置目标值，重复人工操作应被上游安全接受；本迭代不新增本地操作表、Outbox 或补偿 Worker，也不承诺跨独立请求的业务幂等。
- 沿用请求中间件生成的 `request_id` 做链路关联；请求体不新增第二套业务请求号。

### 权限、架构与审计

- 接口只开放在后台管理端，不开放 C 端。沿用现有资产操作权限与数据范围，不扩大任何账号可见或可操作的卡、设备。
- Handler 只负责绑定参数、调用用例和统一响应。参数校验详情写日志，对外返回统一参数错误。
- 该用例采用 `Handler → Application UseCase → Gateway Port/Infrastructure Adapter`。资产解析可复用 Query/Repository 能力；外部副作用、能力校验、调用结果与审计由同一完整用例编排，不创建无业务价值的聚合根。
- 每次成功或失败尝试均写统一 Audit Event，至少包含操作者、请求 ID、资产类型与 ID、实际卡号、运营商、`speed_level`、实际渠道编码和结果；失败审计按全局审计方案使用独立短事务，不能被业务事务回滚掉。
- Integration Log 记录渠道交互，Audit Event 记录谁对什么业务资产做了什么，二者职责不同但以同一请求 ID 关联。
- 卡号、凭据、原始响应中的敏感字段遵循公共脱敏规则。访问日志也不得保存未脱敏凭据或无限制的渠道原文。

### 前端交互

- 卡详情和设备详情都提供“手动限速”入口；设备入口明确显示本次将作用的当前 ICCID。
- 档位使用后端允许的固定枚举与中文标签，前端不得自行提交任意 Kbps 数值或上游编码。
- 不支持、无当前卡或映射未配置时入口禁用并显示后端原因；即使前端状态过期，后端仍重新校验。
- 提交前二次确认资产、当前 ICCID和目标档位；提交期间禁止重复点击。
- 结果未知时不得显示成功，提示用户先通过渠道或后续卡信息核实，再决定是否人工重试。
- 页面不展示“当前实际限速”字段，除非未来存在独立、经验证的渠道查询能力。

### 发布与回滚

- 发布前完成当前绑定重复数据检查、部分唯一索引、真实账号映射核实、两家直连运营商联调和权限回归。
- 前后端同批切换统一接口；旧设备接口下线，避免新旧入口产生两套卡号语义。
- 通过配置开关可整体关闭手动限速入口和新接口，但不得用开关绕过权限或运营商能力校验。
- 回滚应用时保留 Audit Event 与 Integration Log；这些是已经发生的外部调用事实，不得删除或改写。

## Testing Decisions

- 枚举契约测试覆盖全部十个 `speed_level` 及未知值，验证前端永远不能直接提交上游编码或任意速率。
- 资产解析测试覆盖卡 ICCID、设备唯一当前绑卡、无当前绑卡、多条当前绑卡、软删除绑定和越权资产，确认从不把 IMEI 作为 `cardNo`。
- 运营商测试覆盖中国电信、中国广电成功进入 Adapter，以及中国联通、中国移动、未知运营商在调用前被拒绝。
- 配置测试覆盖不同逻辑账号和运营商映射、档位缺失、账号缺失、非法映射及凭据不进入日志；缺失时绝不使用猜测默认值。
- Gateway 契约测试使用假渠道验证请求包装为 `{params:{cardNo,code}}`、响应规范化、明确成功、明确失败、格式异常、超时和连接中断。
- 重试测试验证每个 HTTP 操作最多调用上游一次，尤其验证共享客户端原有 `maxRetries=2` 不作用于限速方法。
- 审计测试验证成功、明确失败和未知结果都有 Audit Event 与 Integration Log，并用同一请求 ID 关联；敏感字段均脱敏。
- 并发测试覆盖同一资产同时提交相同或不同档位，验证每个请求独立留下真实尝试记录且系统不谎报最终上游状态。
- PostgreSQL 迁移测试验证当前有效绑定部分唯一索引，并验证存量重复会使迁移门禁失败而不是被自动清理。
- HTTP 集成测试穿过 Fiber 认证、权限、Application、Gateway 假服务和统一错误处理，覆盖卡、设备、越权、结果未知及旧路由已下线。
- 联调验收必须用真实测试账号分别验证中国电信与中国广电的每个生产开放档位，并将核实后的映射作为受控部署配置。

## Out of Scope

- 不给套餐增加限速字段或套餐级限速策略。
- 不按流量、余额、到期时间或卡状态自动触发限速或恢复。
- 不改造现有轮询任务、队列频率或状态同步流程。
- 不实现中国联通通信计划调整，也不为中国移动伪造直连能力。
- 不新增限速操作业务表、Outbox、自动重试或补偿 Worker。
- 不查询或声称保存了上游当前实际限速状态。
- 不把上游编码暴露为公共 API 契约。

## Further Notes

- 当前代码的设备限速接口解析 IMEI，并通过 `/device/speed-limit` 发送整数 KB/s；该实现与本需求的 ICCID、语义档位和 `/flow-card/speedLimit` 契约冲突，不能作为兼容依据。
- 当前 Gateway 客户端默认对网络错误重试两次；本操作必须显式关闭，否则无法满足外部副作用“结果未知不盲重试”的要求。
- 真实 Gateway 映射和响应格式仍需外部联调确认，但产品范围、失败语义和安全边界已经确定，因此规格可进入实现代理。
