# 需求10：限速规则

---

## 背景

Gateway 已有 `SetSpeedLimit` 接口，支持卡和设备：

```go
// internal/gateway/device.go
func (c *Client) SetSpeedLimit(ctx context.Context, req *SpeedLimitReq) error

// internal/gateway/models.go
type SpeedLimitReq struct {
    CardNo     string `json:"cardNo,omitempty"`   // 卡（与 DeviceID 二选一）
    DeviceID   string `json:"deviceId,omitempty"` // 设备（与 CardNo 二选一）
    SpeedLimit int    `json:"speedLimit"`          // 限速值（KB/s），0=不限速
    Extend     string `json:"extend,omitempty"`
}
```

**"根据不同运营商限速规则，基于套餐流量设置不同的卡/设备的限速规则"**

设计思路：限速规则配置在**套餐**上（不同运营商、不同流量档位对应不同套餐，套餐本身就是差异载体）。套餐激活时自动调用 Gateway 设置限速。

---

## 数据库变更

```sql
-- 套餐表增加限速字段
ALTER TABLE tb_package
    ADD COLUMN speed_limit_kbps INT NOT NULL DEFAULT 0
        COMMENT '限速值（KB/s），0=不限速';
```

---

## Model 变更

```go
// internal/model/package.go
type Package struct {
    // ...原有字段...
    SpeedLimitKbps int `gorm:"column:speed_limit_kbps;type:int;not null;default:0;comment:限速值（KB/s），0=不限速" json:"speed_limit_kbps"`
}
```

---

## 后端实现

### 1. 套餐激活时设置限速

在 `internal/service/package/activation_service.go` 中，套餐激活（`ActivatedAt` 赋值）时调用限速：

```go
// activatePackageUsage 套餐激活核心逻辑（已有函数，追加限速调用）
func (s *ActivationService) activatePackageUsage(ctx context.Context, usage *model.PackageUsage, pkg *model.Package) error {
    // ...现有激活逻辑...

    // 套餐有限速配置时，调用 Gateway 设置
    if pkg.SpeedLimitKbps > 0 {
        if err := s.applySpeedLimit(ctx, usage, pkg.SpeedLimitKbps); err != nil {
            // 限速失败不阻断套餐激活，记录错误日志
            s.logger.Error("设置限速失败", zap.Error(err),
                zap.Uint("package_usage_id", usage.ID),
                zap.Int("speed_limit", pkg.SpeedLimitKbps))
        }
    }
    return nil
}

// applySpeedLimit 调用 Gateway 设置限速
func (s *ActivationService) applySpeedLimit(ctx context.Context, usage *model.PackageUsage, speedKbps int) error {
    req := &gateway.SpeedLimitReq{SpeedLimit: speedKbps}
    
    switch usage.UsageType {
    case constants.PackageUsageTypeSingleCard: // "single_card"，定义在 pkg/constants/iot.go
        // 查卡的 ICCID
        card, err := s.iotCardStore.GetByID(ctx, usage.IotCardID)
        if err != nil { return err }
        req.CardNo = card.ICCID

    case constants.PackageUsageTypeDevice: // "device"，定义在 pkg/constants/iot.go
        // 查设备的 IMEI
        device, err := s.deviceStore.GetByID(ctx, usage.DeviceID)
        if err != nil { return err }
        req.DeviceID = device.IMEI
    }

    return s.gatewayClient.SetSpeedLimit(ctx, req)
}
```

### 2. 套餐到期/失效时取消限速

当套餐状态变为 `已过期(3)` 或 `已失效(4)` 时，重置限速为 0（不限速）。

在套餐过期处理任务中追加：

```go
// 套餐过期时取消限速（SpeedLimit=0 表示不限速）
if expiredPkg.SpeedLimitKbps > 0 {
    s.applySpeedLimit(ctx, usage, 0)  // 0 = 不限速
}
```

> **注意**：如果有续费的排队套餐立即生效，应以新套餐的限速为准，而不是先取消再设置。

### 3. 手动操作限速（后台管理）

```
POST /admin/iot-cards/{id}/speed-limit
POST /admin/devices/{id}/speed-limit
```

请求体：
```go
type SetSpeedLimitRequest struct {
    SpeedLimitKbps int `json:"speed_limit_kbps" validate:"min=0" description:"限速值（KB/s），0=取消限速"`
}
```

直接调 Gateway，不更新套餐配置（临时操作）。

---

## API 变更（套餐管理）

### 创建/更新套餐新增字段

```
POST /admin/packages
PUT  /admin/packages/{id}
```

新增参数：
```go
SpeedLimitKbps int `json:"speed_limit_kbps" validate:"min=0" description:"限速值（KB/s），0=不限速"`
```

### 套餐列表/详情响应新增字段

```go
type PackageResponse struct {
    // ...原有字段...
    SpeedLimitKbps int    `json:"speed_limit_kbps" description:"限速值（KB/s），0=不限速"`
    SpeedLimitDesc string `json:"speed_limit_desc" description:"限速描述（如 '512 KB/s'，0时为'不限速'）"`
}
```

---

## 前端对接

### 页面：套餐创建/编辑

在"套餐配置"区域新增限速项：

```
限速配置：
  限速值：[____] KB/s  （0 或留空 = 不限速）
  提示文案：留空或填 0 表示不限速；建议根据运营商规则填写
```

前端换算提示（可选）：512 KB/s ≈ 4 Mbps

### 页面：套餐列表

新增"限速"列：
- `0` → 显示"不限速"
- `>0` → 显示"XXX KB/s"

### 页面：资产详情 > 操作区

新增"手动设置限速"按钮（仅当有生效套餐时显示）：

```
弹框：
  当前限速：xxx KB/s（来自套餐配置）
  临时设置：[____] KB/s  [0=取消限速]
  
  [确认设置] → POST /admin/iot-cards/{id}/speed-limit
```

---

## 注意事项

1. **Gateway 调用失败不阻断业务**：限速设置失败只记录日志，套餐仍然激活。
2. **限速精度**：Gateway `SpeedLimit` 单位是 KB/s，最小值为 1；填 0 表示不限速。
3. **运营商差异**：不同运营商通过创建不同套餐来体现限速差异，不需要在套餐外额外维护运营商-限速映射表。
4. **续费场景**：新套餐生效时重新设置限速，以新套餐的限速值为准。
