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

7.8 KiB
Raw Blame History

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 = 0stopped_at = now()stop_reason = "manual"
  6. 在 Redis 中设置停机保护期:RedisDeviceProtectKey(deviceID, "stop")TTL = 1 小时
  7. 响应:返回成功,附带失败卡列表(如有)

保护期说明:

  • 保护期时长1 小时(常量 DeviceProtectPeriodDuration = 1 * time.Hour,定义在 pkg/constants/
  • 停机保护期 keyprotect:device:{device_id}:stop
  • 复机保护期 keyprotect: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 = 1resumed_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 = 0stopped_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 = 1resumed_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 自动触发套餐暂停联动逻辑