迭代计划准备

This commit is contained in:
2026-07-16 15:07:59 +08:00
parent 1a9db9328e
commit c4f430ccb3
22 changed files with 2969 additions and 1569 deletions

View File

@@ -1,194 +1,136 @@
# 需求10限速规则
# 需求10Gateway 手动卡限速
> 状态:待评审
> 已确认边界:本期只提供手动设置/取消限速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/s0=不限速
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/s0=不限速';
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/s0=不限速" 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/s0=取消限速"`
}
```
直接调 Gateway不更新套餐配置临时操作
---
## API 变更(套餐管理)
### 创建/更新套餐新增字段
```
POST /admin/packages
PUT /admin/packages/{id}
```
新增参数:
```go
SpeedLimitKbps int `json:"speed_limit_kbps" validate:"min=0" description:"限速值KB/s0=不限速"`
```
### 套餐列表/详情响应新增字段
```go
type PackageResponse struct {
// ...原有字段...
SpeedLimitKbps int `json:"speed_limit_kbps" description:"限速值KB/s0=不限速"`
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` 契约。