迭代计划准备
This commit is contained in:
@@ -1,194 +1,136 @@
|
||||
# 需求10:限速规则
|
||||
# 需求10:Gateway 手动卡限速
|
||||
|
||||
> 状态:待评审
|
||||
> 已确认边界:本期只提供手动设置/取消限速;Gateway 最终对象始终是卡,统一使用 `cardNo`。
|
||||
|
||||
---
|
||||
|
||||
## 背景
|
||||
## 一、范围
|
||||
|
||||
Gateway 已有 `SetSpeedLimit` 接口,支持卡和设备:
|
||||
本期提供一个统一的后台能力:对一张实际联网卡设置或取消限速。
|
||||
|
||||
- 单卡资产:使用 `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
|
||||
// internal/gateway/device.go
|
||||
func (c *Client) SetSpeedLimit(ctx context.Context, req *SpeedLimitReq) error
|
||||
// GatewaySpeedLimitPort 设置或取消卡限速。
|
||||
// speedKbps=0 表示取消,具体上游参数由适配器转换。
|
||||
type GatewaySpeedLimitPort interface {
|
||||
SetSpeedLimit(ctx context.Context, cardNo string, speedKbps int) 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"`
|
||||
// SpeedLimitCardResolver 将卡或设备资产解析为实际联网卡 ICCID。
|
||||
type SpeedLimitCardResolver interface {
|
||||
ResolveCardNo(ctx context.Context, assetType string, assetID uint) (string, error)
|
||||
}
|
||||
```
|
||||
|
||||
**"根据不同运营商限速规则,基于套餐流量设置不同的卡/设备的限速规则"**
|
||||
|
||||
设计思路:限速规则配置在**套餐**上(不同运营商、不同流量档位对应不同套餐,套餐本身就是差异载体)。套餐激活时自动调用 Gateway 设置限速。
|
||||
|
||||
---
|
||||
|
||||
## 数据库变更
|
||||
新建操作记录表,既用于审计,也用于同一 `request_id` 的幂等重试:
|
||||
|
||||
```sql
|
||||
-- 套餐表增加限速字段
|
||||
ALTER TABLE tb_package
|
||||
ADD COLUMN speed_limit_kbps INT NOT NULL DEFAULT 0
|
||||
COMMENT '限速值(KB/s),0=不限速';
|
||||
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` 是“设为目标状态”的幂等操作。
|
||||
|
||||
---
|
||||
|
||||
## 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"`
|
||||
```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 的实时状态。
|
||||
|
||||
---
|
||||
|
||||
## 后端实现
|
||||
## 五、审计与人工验收
|
||||
|
||||
### 1. 套餐激活时设置限速
|
||||
每次请求记录:资产类型/ID、最终 `cardNo`、目标 `speed_kbps`、设置或取消、操作人、`request_id`、Gateway 结果和脱敏错误摘要。
|
||||
|
||||
在 `internal/service/package/activation_service.go` 中,套餐激活(`ActivatedAt` 赋值)时调用限速:
|
||||
人工验收覆盖:
|
||||
|
||||
```go
|
||||
// activatePackageUsage 套餐激活核心逻辑(已有函数,追加限速调用)
|
||||
func (s *ActivationService) activatePackageUsage(ctx context.Context, usage *model.PackageUsage, pkg *model.Package) error {
|
||||
// ...现有激活逻辑...
|
||||
1. 单卡按 ICCID 设置限速。
|
||||
2. 设备按 `is_current=true` 的绑定卡设置限速,请求中不出现设备 IMEI。
|
||||
3. 设备无当前卡时拒绝调用 Gateway。
|
||||
4. `speed_kbps=0` 经同一 Gateway 方法取消限速。
|
||||
5. 同一 `request_id` 重试不重复生成操作记录;不同请求复用 ID 返回冲突。
|
||||
6. Gateway 失败记录错误并允许相同目标值重试。
|
||||
|
||||
// 套餐有限速配置时,调用 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. **续费场景**:新套餐生效时重新设置限速,以新套餐的限速值为准。
|
||||
上线前置条件:Gateway 提供设置和取消所需的最终报文字段约定。业务层只保持 `cardNo + speedKbps` 契约。
|
||||
|
||||
Reference in New Issue
Block a user