# 需求10：Gateway 手动卡限速

> 状态：原需求独立稿；最终口径以标准评审稿为准。
> 已确认边界：本期只提供手动设置/取消限速；Gateway 最终对象始终是卡，统一使用 `cardNo`。

---

## 一、范围

本期提供一个统一的后台能力：对一张实际联网卡设置或取消限速。

- 单卡资产：使用 `IotCard.ICCID` 作为 `cardNo`。
- 设备资产：查询 `tb_device_sim_binding.is_current=true` 的当前绑定卡，再使用该卡 ICCID。
- `speed_kbps > 0` 表示设置限速；`speed_kbps = 0` 表示取消限速。
- 内部业务单位固定为 `kbps`，前端也按 `kbps` 输入和展示。

本期不做：

- 套餐限速字段或套餐编辑页限速配置。
- 套餐激活、到期、续费、停机、切卡后的自动限速或自动取消。
- 设备 IMEI 限速。
- “Gateway 当前实际限速”查询展示。上游未提供查询接口时，页面只能展示最近一次本系统操作记录。

取消限速仍调用同一个 Gateway 方法。`speed_kbps=0` 是本系统语义，Gateway 所需的取消报文仅在 Infrastructure 适配器中转换，不散落在 Handler、Service 或前端。

---

## 二、流程

```mermaid
sequenceDiagram
    actor User as 平台员工
    participant Web as 资产详情页
    participant API as SpeedLimit Application
    participant DB as PostgreSQL
    participant Resolver as 当前卡解析器
    participant Gateway as Gateway Adapter

    User->>Web: 输入限速值或点击取消
    Web->>API: asset identifier + speed_kbps + request_id
    API->>Resolver: 解析实际 cardNo
    Resolver-->>API: ICCID 或无当前卡错误
    API->>DB: 创建或复用限速操作记录
    API->>Gateway: SetSpeedLimit(cardNo, speedKbps)
    Gateway-->>API: 成功或失败
    API->>DB: 写结果、错误摘要和操作审计
    API-->>Web: 返回本次操作结果
```

设备不存在当前绑定卡时，接口返回业务错误并记录失败原因；绝不把设备 IMEI、设备 ID 或空字符串发送给 Gateway。

---

## 三、应用端口与数据

```go
// GatewaySpeedLimitPort 设置或取消卡限速。
// speedKbps=0 表示取消，具体上游参数由适配器转换。
type GatewaySpeedLimitPort interface {
    SetSpeedLimit(ctx context.Context, cardNo string, speedKbps int) error
}

// SpeedLimitCardResolver 将卡或设备资产解析为实际联网卡 ICCID。
type SpeedLimitCardResolver interface {
    ResolveCardNo(ctx context.Context, assetType string, assetID uint) (string, error)
}
```

新建操作记录表，既用于审计，也用于同一 `request_id` 的幂等重试：

```sql
CREATE TABLE tb_gateway_speed_limit_operation (
    id                 BIGSERIAL PRIMARY KEY,
    request_id         VARCHAR(64) NOT NULL,
    asset_type         VARCHAR(20) NOT NULL,
    asset_id           BIGINT NOT NULL,
    card_no            VARCHAR(100) NOT NULL,
    desired_speed_kbps INT NOT NULL,
    status             INT NOT NULL DEFAULT 1,
    error_summary      TEXT NOT NULL DEFAULT '',
    operator_id        BIGINT NOT NULL,
    completed_at       TIMESTAMPTZ,
    created_at         TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at         TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE UNIQUE INDEX uq_gateway_speed_limit_operation_request
    ON tb_gateway_speed_limit_operation(request_id);
CREATE INDEX idx_gateway_speed_limit_operation_asset
    ON tb_gateway_speed_limit_operation(asset_type, asset_id, created_at DESC);
```

状态：`1=处理中, 2=成功, 3=失败`。同一 `request_id` 重试时，任务、操作人、资产和目标限速一致才返回或继续原记录；不一致返回冲突。Gateway 网络超时后允许使用相同目标值重试，因为 `SetSpeedLimit` 是“设为目标状态”的幂等操作。

---

## 四、接口与前端

```text
POST /api/admin/assets/{identifier}/speed-limit
```

```json
{
  "request_id": "01JZ...",
  "speed_kbps": 1024
}
```

- `speed_kbps` 必须为整数且大于等于 0。
- 取消操作提交 `speed_kbps=0`，不新增第二个取消接口。
- 后端根据 `identifier` 解析资产类型和数据范围，前端不得传 `cardNo`、设备 ID 或资产类型作为权威依据。

资产详情页提供两个独立命令：

- 数字输入框加“保存”用于设置限速，单位固定显示 `kbps`。
- 取消按钮用于提交 `speed_kbps=0`，并使用确认弹窗避免误操作。

设备详情页显示“当前使用卡 ICCID”。没有当前卡时禁用两个命令并展示后端返回的原因。页面可展示最近一次操作的目标限速、结果、操作人和时间，但不能把该记录描述为 Gateway 的实时状态。

---

## 五、审计与人工验收

每次请求记录：资产类型/ID、最终 `cardNo`、目标 `speed_kbps`、设置或取消、操作人、`request_id`、Gateway 结果和脱敏错误摘要。

人工验收覆盖：

1. 单卡按 ICCID 设置限速。
2. 设备按 `is_current=true` 的绑定卡设置限速，请求中不出现设备 IMEI。
3. 设备无当前卡时拒绝调用 Gateway。
4. `speed_kbps=0` 经同一 Gateway 方法取消限速。
5. 同一 `request_id` 重试不重复生成操作记录；不同请求复用 ID 返回冲突。
6. Gateway 失败记录错误并允许相同目标值重试。

上线前置条件：Gateway 提供设置和取消所需的最终报文字段约定。业务层只保持 `cardNo + speedKbps` 契约。
