Files
junhong_cmp_fiber/openspec/specs/asset-queries/spec.md
huang 2b3a9cb33f
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m19s
feat: 新增套餐使用记录实付金额快照字段 paid_amount
- 新增迁移 000129:tb_package_usage 添加 paid_amount BIGINT 字段,存量数据通过 JOIN tb_order 回填
- PackageUsage Model 新增 PaidAmount *int64 字段
- 4 个写入点(order service 主套餐/加油包、auto_purchase 主套餐/加油包)赋值 order.ActualPaidAmount
- AssetPackageResponse DTO 新增 paid_amount 字段
- GetCurrentPackage / GetPackages 填充 paid_amount,接口直接返回购买价格无需 JOIN 订单表
2026-04-18 10:19:21 +08:00

7.7 KiB
Raw Blame History

asset-queries Specification

Purpose

提供基于已知资产 ID 的轻量查询接口,包括实时状态查询、手动刷新、套餐历史列表和当前主套餐详情。供前端在 resolve 之后进行快速轮询和详情展示。

Requirements

Requirement: 轻量实时状态查询

系统 SHALL 提供基于持久化数据的轻量状态查询接口,供前端在已知资产 ID 后进行快速轮询。

API 端点: GET /api/admin/assets/:asset_type/:id/realtime-status

约束:

  • :asset_type 取值为 devicecard
  • 此接口不调用网关,仅读取 DB/Redis 中持久化的最新数据
  • 不包含套餐流量计算(与 resolve 的区别)
  • "实时性"依赖轮询系统定期刷新(实名状态约 5 分钟,流量约 10 分钟)

card 类型响应字段:

  • network_status: 网络状态0-停机 1-开机)
  • real_name_status: 实名状态0-未实名 1-已实名)
  • current_month_usage_mb: 本月已用流量(持久化缓存值)
  • last_sync_at: 最后与 Gateway 同步时间

device 类型响应字段:

  • device_protect_status: 保护期状态("none" / "stop" / "start"
  • cards: 所有绑定卡的状态列表(同 DeviceCardInfo 结构)

Scenario: 查询单卡实时状态

  • WHEN 管理员调用 GET /api/admin/assets/card/123/realtime-status
  • THEN 系统返回该卡的 network_status、real_name_status、current_month_usage_mb、last_sync_at

Scenario: 查询设备实时状态

  • WHEN 管理员调用 GET /api/admin/assets/device/456/realtime-status
  • THEN 系统返回设备的保护期状态及所有绑定卡的当前状态列表

Scenario: asset_type 参数非法

  • WHEN 管理员调用 GET /api/admin/assets/unknown-type/123/realtime-status
  • THEN 系统返回 HTTP 400 参数错误

Requirement: 手动刷新接口

系统 SHALL 提供手动触发网关同步的接口,用于客服主动刷新资产最新状态。

API 端点: POST /api/admin/assets/:asset_type/:id/refresh

行为规则:

  • card 类型:直接调用 RefreshCardDataFromGateway(iccid) 同步网络状态、实名状态、本月流量、最后同步时间
  • device 类型:对该设备所有绑定卡遍历调用 RefreshCardDataFromGateway

设备类型频率限制:

  • 使用 Redis Key RedisDeviceRefreshCooldownKey(deviceID) 限频
  • 同一设备 30 秒冷却期内不允许重复触发
  • 冷却期内调用返回 HTTP 429

响应:

  • 刷新完成后返回刷新后的最新状态(与 realtime-status 响应结构相同)

Scenario: 刷新单卡状态

  • WHEN 客服调用 POST /api/admin/assets/card/123/refresh
  • THEN 系统调用 RefreshCardDataFromGateway更新 DB 中的卡状态字段,返回刷新后的最新状态

Scenario: 刷新设备状态(首次)

  • WHEN 管理员调用 POST /api/admin/assets/device/456/refresh,该设备有 3 张绑定卡
  • THEN 系统依次刷新 3 张卡,设置 30 秒冷却期,返回最新状态

Scenario: 设备刷新冷却期内重复触发

  • WHEN 管理员在 30 秒冷却期内第二次调用 POST /api/admin/assets/device/456/refresh
  • THEN 系统返回 HTTP 429提示"刷新过于频繁,请稍后再试"

Requirement: 套餐历史列表查询

系统 SHALL 提供资产的全量套餐记录查询接口,包含历史和当前生效套餐。

API 端点: GET /api/admin/assets/:asset_type/:id/packages

排序: 按 created_at 倒序(最新套餐在前)

分页: 不分页,全量返回

范围: 包含所有状态(含 status=4 已失效的历史套餐)

按 asset_type 区分查询:

  • card查询 PackageUsage.iot_card_id = :id
  • device查询 PackageUsage.device_id = :id

每条记录响应字段:

  • package_usage_id: 套餐使用记录 ID
  • package_name: 套餐名称
  • package_type: 套餐类型formal/addon
  • master_usage_id: 主套餐 ID加油包时有值主套餐时为 null
  • real_data_mb: 真总流量MB
  • virtual_data_mb: 虚总流量/停机阈值MB
  • package_used_mb: 展示已使用流量(经虚流量换算)
  • package_remain_mb: 展示剩余流量
  • activated_at: 生效时间
  • expires_at: 过期时间
  • status: 套餐状态0-待生效 1-生效中 2-已用完 3-已过期 4-已失效)
  • paid_amount: 购买时实付金额(分),线下支付或无订单分配时为 null

Scenario: 查询卡的套餐历史

  • WHEN 管理员调用 GET /api/admin/assets/card/123/packages,该卡有 3 条套餐记录(含 1 条已失效)
  • THEN 系统返回全部 3 条记录,按创建时间倒序排列,每条记录含 paid_amount 字段

Scenario: 查询设备的套餐历史

  • WHEN 管理员调用 GET /api/admin/assets/device/456/packages
  • THEN 系统返回该设备 device_id 下的所有套餐记录,含 paid_amount 字段

Scenario: 资产无套餐记录

  • WHEN 管理员查询一张从未购买过套餐的卡
  • THEN 系统返回空数组,不报错

Scenario: 线下支付套餐的 paid_amount 为 null

  • WHEN 管理员查询一张通过线下支付购买套餐的卡
  • THEN 对应套餐记录的 paid_amount 字段缺省omitempty不展示

Requirement: 当前主套餐详情查询

系统 SHALL 提供查询资产当前生效主套餐的接口,用于展示套餐详细信息。

API 端点: GET /api/admin/assets/:asset_type/:id/current-package

查询条件: status = 1生效中AND master_usage_id IS NULL

多套餐同时生效时只返回主套餐master_usage_id IS NULL不返回加油包

响应字段:

  • 完整套餐信息(同套餐历史列表中的单条记录字段)
  • paid_amount: 购买时实付金额(分),线下支付或无订单分配时为 null
  • 当无生效主套餐时,返回 HTTP 404

Scenario: 返回当前主套餐(含实付金额)

  • WHEN 管理员调用 GET /api/admin/assets/card/123/current-package,该卡有 1 个生效主套餐和 1 个加油包,主套餐 paid_amount = 9900
  • THEN 系统只返回主套餐信息,响应中包含 paid_amount: 9900,不包含加油包

Scenario: 无当前生效主套餐

  • WHEN 管理员查询没有生效中主套餐的资产
  • THEN 系统返回 HTTP 404

Requirement: RefreshCardDataFromGateway 完整同步

系统 SHALL 提供从 Gateway 完整同步卡数据的方法,替代原 SyncCardStatusFromGateway(仅为示例实现)。

方法签名: RefreshCardDataFromGateway(ctx context.Context, iccid string) error

同步字段:

  • network_status: 网络状态(从网关卡状态映射)
  • real_name_status: 实名状态(从网关实名接口获取)
  • current_month_usage_mb: 本月已用流量(从网关流量接口获取)
  • last_sync_time: 更新为当前时间

错误处理: 网关调用失败时记录 Error 日志并返回错误,不更新 DB

Scenario: 完整同步卡数据

  • WHEN 调用 RefreshCardDataFromGateway(ctx, "89860123456789012345")
  • THEN 系统调用网关接口,将 network_status、real_name_status、current_month_usage_mb、last_sync_time 写回 DB

MODIFIED Requirements

Requirement: 资产查询错误处理

资产查询 Service 在获取绑定卡信息时,数据库查询错误 SHALL 被记录到日志而非被静默忽略。查询失败 SHALL NOT 中断主查询流程,但 SHALL 记录 Warn 级别日志,包含失败的卡 ID 列表和错误详情。

Scenario: 绑定卡查询失败时记录日志

  • WHEN iotCardStore.GetByIDs() 返回错误
  • THEN 系统 SHALL 记录 Warn 日志(含 card_ids 和 error继续返回已有数据不返回错误给调用方