Files
junhong_cmp_fiber/openspec/specs/asset-suspend-resume/spec.md
huang cebcada950
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m16s
refactor: 流量系统重构 — 增量累加算法 + 日粒度缓冲 + 旧详单清理
核心改造:
- 增量算法:流量计算从覆盖式改为增量累加(gateway - lastReading),支持上游运营商重置检测
- 日流量缓冲:insertDataUsageRecord 改为 Redis INCRBYFLOAT,每日凌晨落盘到 tb_card_daily_usage
- 运营商:新增 data_reset_day 字段(联通=27,其余=1)
- IoT卡:新增 last_gateway_reading_mb 字段存储上次网关读数
- 查询层:新建 TrafficQueryService 合并 Redis(今日)+ DB(历史)数据源
- 清理:删除 DataUsageRecord model/store,移除 polling_handler 旧引用

迁移:000094-000097(carrier字段、iot_card字段、数据初始化、日流量表)
2026-03-30 09:59:30 +08:00

190 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# asset-suspend-resume Specification
## Purpose
提供统一的资产停复机接口,包括设备级批量停复机和单卡停复机,含保护期感知逻辑。废弃原分散在各模块的旧停复机接口,统一使用 `/api/admin/assets/` 路径。
## Requirements
### Requirement: 设备停机接口
系统 SHALL 提供设备停机接口,批量停用设备下所有已实名卡,并建立停机保护期。
**API 端点**: `POST /api/admin/assets/device/:device_id/stop`
**执行流程**:
1. 验证设备存在(不存在返回 HTTP 404
2. 检查设备是否在保护期(`RedisDeviceProtectKey(deviceID, "stop")``"start"` 存在则返回 HTTP 403
3. 获取该设备所有已实名(`real_name_status = 1`)的绑定卡
4. 遍历调用网关停机接口(未实名卡跳过,永远是停机状态)
5. 更新成功停机的卡的 `network_status = 0``stopped_at = now()``stop_reason = "manual"`
6. 在 Redis 中设置停机保护期:`RedisDeviceProtectKey(deviceID, "stop")`TTL = 1 小时
7. 响应:返回成功,附带失败卡列表(如有)
**保护期说明**:
- 保护期时长1 小时(常量 `DeviceProtectPeriodDuration = 1 * time.Hour`,定义在 `pkg/constants/`
- 停机保护期 key`protect:device:{device_id}:stop`
- 复机保护期 key`protect:device:{device_id}:start`
- 两个 key 互斥:设置 stop 保护期时删除 start 保护期,反之亦然
**批量部分失败策略**:
- 部分卡调网关失败:**仍设置** Redis 保护期(保护期从发起操作时算起)
- 已成功停机的卡**不回滚**
- 失败的卡记录 Error 日志,响应体中携带失败列表
#### Scenario: 成功执行设备停机
- **WHEN** 管理员调用 `POST /api/admin/assets/device/456/stop`,该设备有 3 张已实名卡
- **THEN** 系统批量调网关停机,更新 3 张卡 network_status=0设置 1 小时 stop 保护期,返回成功
#### Scenario: 设备存在保护期
- **WHEN** 管理员在设备已有 stop 保护期时再次调用停机接口
- **THEN** 系统返回 HTTP 403提示"设备处于保护期,不允许操作"
#### Scenario: 设备下无已实名卡
- **WHEN** 管理员对只有未实名卡的设备执行停机
- **THEN** 系统返回成功0 张卡操作),设置 stop 保护期
#### Scenario: 设备不存在
- **WHEN** 管理员调用不存在的设备 ID
- **THEN** 系统返回 HTTP 404
#### Scenario: 部分卡停机失败
- **WHEN** 设备有 3 张卡1 张网关调用失败
- **THEN** 2 张成功停机1 张失败记录日志,**仍设置** stop 保护期,响应中包含失败卡信息
---
### Requirement: 设备复机接口
系统 SHALL 提供设备复机接口,批量恢复设备下所有已实名卡,并建立复机保护期。
**API 端点**: `POST /api/admin/assets/device/:device_id/start`
**执行流程**:
1. 验证设备存在(不存在返回 HTTP 404
2. 检查设备是否在保护期stop 或 start 保护期均存在时返回 HTTP 403
3. 获取该设备所有已实名(`real_name_status = 1`)的绑定卡
4. 遍历调用网关复机接口
5. 更新成功复机的卡的 `network_status = 1``resumed_at = now()`
6. 设置复机保护期:`RedisDeviceProtectKey(deviceID, "start")`TTL = 1 小时
7. 响应:返回成功
#### Scenario: 成功执行设备复机
- **WHEN** 管理员调用 `POST /api/admin/assets/device/456/start`,该设备有 2 张已实名卡
- **THEN** 系统批量复机,更新卡状态,设置 1 小时 start 保护期,返回成功
#### Scenario: 设备在 start 保护期内再次复机
- **WHEN** 设备已有 start 保护期时再次调用复机接口
- **THEN** 系统返回 HTTP 403提示"设备处于保护期,不允许操作"
---
### Requirement: 卡停机接口
系统 SHALL 提供单卡停机接口,含保护期感知逻辑。
**API 端点**: `POST /api/admin/assets/card/:iccid/stop`
**执行流程**:
1. 通过 ICCID 查找卡(不存在返回 HTTP 404
2. 检查卡是否已实名(`real_name_status = 0` 时返回 HTTP 403未实名卡不允许停复机
3. 若卡绑定了设备,检查该设备的保护期:
- 设备有 **stop 保护期**:允许停机(本已是停机方向,无冲突)
- 设备有 **start 保护期**:允许停机(用户可主动停单张卡)
- 设备无保护期:正常执行
4. 调用网关停机接口
5. 更新卡 `network_status = 0``stopped_at = now()``stop_reason = "manual"`
#### Scenario: 独立卡(未绑定设备)停机
- **WHEN** 管理员对一张未绑定设备的已实名卡执行停机
- **THEN** 系统正常调网关停机,更新卡状态
#### Scenario: 绑定设备且设备在 start 保护期内停机
- **WHEN** 管理员对绑定了设备且设备有 start 保护期的卡执行停机
- **THEN** 系统允许执行(用户主动停单张卡不违反 start 保护期),正常停机
#### Scenario: 对未实名卡执行停机
- **WHEN** 管理员对 real_name_status=0未实名的卡执行停机
- **THEN** 系统返回 HTTP 403提示"未实名卡不允许停复机操作"
---
### Requirement: 卡复机接口
系统 SHALL 提供单卡复机接口,含保护期感知逻辑。
**API 端点**: `POST /api/admin/assets/card/:iccid/start`
**执行流程**:
1. 通过 ICCID 查找卡(不存在返回 HTTP 404
2. 检查卡是否已实名(`real_name_status = 0` 时返回 HTTP 403
3. 若卡绑定了设备,检查该设备的保护期:
- 设备有 **stop 保护期****不允许**手动复机,返回 HTTP 403设备处于停机保护期
- 设备有 **start 保护期**:允许复机(本已是复机方向,无冲突)
- 设备无保护期:正常执行
4. 调用网关复机接口
5. 更新卡 `network_status = 1``resumed_at = now()`,清空 `stop_reason`
#### Scenario: 独立卡(未绑定设备)复机
- **WHEN** 管理员对一张未绑定设备的已实名停机卡执行复机
- **THEN** 系统正常调网关复机,更新卡状态
#### Scenario: 设备处于 stop 保护期时尝试复机
- **WHEN** 管理员对绑定了设备且设备有 stop 保护期的卡执行复机
- **THEN** 系统返回 HTTP 403提示"设备处于停机保护期,不允许手动复机"
#### Scenario: 设备在 start 保护期内复机
- **WHEN** 管理员对绑定了设备且设备有 start 保护期的卡执行复机
- **THEN** 系统允许执行(本已是复机方向),正常复机
#### Scenario: 对未实名卡执行复机
- **WHEN** 管理员对 real_name_status=0 的卡执行复机
- **THEN** 系统返回 HTTP 403提示"未实名卡不允许停复机操作"
---
### Requirement: 废弃旧停复机接口
系统 SHALL 删除以下重复的停复机接口,统一使用新的 `/api/admin/assets/` 路径。
**待删除接口**:
- `POST /api/admin/enterprises/:id/cards/:card_id/suspend`
- `POST /api/admin/enterprises/:id/cards/:card_id/resume`
- `POST /h5/devices/:device_id/cards/:card_id/suspend`
- `POST /h5/devices/:device_id/cards/:card_id/resume`
- 旧 Admin 卡停复机接口(`POST /iot-cards/:iccid/suspend|resume`
#### Scenario: 调用已删除的旧接口
- **WHEN** 前端调用 `POST /api/admin/enterprises/:id/cards/:card_id/suspend`
- **THEN** 系统返回 HTTP 404路由已不存在
---
## MODIFIED Requirements
### Requirement: 停复机回调注入
系统启动时 SHALL 在 bootstrap 阶段注入停复机回调,确保停机/复机操作完成后自动触发套餐联动逻辑(暂停/恢复套餐计时)。
#### Scenario: 系统启动后回调已注入
- **WHEN** API 或 Worker 服务完成 bootstrap 初始化
- **THEN** `usageService.SetStopResumeCallback(stopResumeService)``activationService.SetResumeCallback(stopResumeService)` SHALL 已被调用
#### Scenario: 停机触发套餐暂停
- **WHEN** 管理员对卡执行停机操作且回调已注入
- **THEN** 停机成功后 SHALL 自动触发套餐暂停联动逻辑