Files
junhong_cmp_fiber/openspec/specs/asset-queries/spec.md
huang b9c3875c08
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m3s
feat: 新增数据库迁移,重命名 device_no 为 virtual_no,新增 iot_card.virtual_no 和 package.virtual_ratio 字段
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-14 18:27:28 +08:00

173 lines
6.6 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-queries Specification
## Purpose
提供基于已知资产 ID 的轻量查询接口,包括实时状态查询、手动刷新、套餐历史列表和当前主套餐详情。供前端在 resolve 之后进行快速轮询和详情展示。
## Requirements
### Requirement: 轻量实时状态查询
系统 SHALL 提供基于持久化数据的轻量状态查询接口,供前端在已知资产 ID 后进行快速轮询。
**API 端点**: `GET /api/admin/assets/:asset_type/:id/realtime-status`
**约束**:
- `:asset_type` 取值为 `device``card`
- 此接口**不调用网关**,仅读取 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-已失效)
#### Scenario: 查询卡的套餐历史
- **WHEN** 管理员调用 `GET /api/admin/assets/card/123/packages`,该卡有 3 条套餐记录(含 1 条已失效)
- **THEN** 系统返回全部 3 条记录,按创建时间倒序排列
#### Scenario: 查询设备的套餐历史
- **WHEN** 管理员调用 `GET /api/admin/assets/device/456/packages`
- **THEN** 系统返回该设备 device_id 下的所有套餐记录
#### Scenario: 资产无套餐记录
- **WHEN** 管理员查询一张从未购买过套餐的卡
- **THEN** 系统返回空数组,不报错
---
### 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不返回加油包
**响应字段**:
- 完整套餐信息(同套餐历史列表中的单条记录字段)
- 当无生效主套餐时,返回 HTTP 404
#### Scenario: 返回当前主套餐
- **WHEN** 管理员调用 `GET /api/admin/assets/card/123/current-package`,该卡有 1 个生效主套餐和 1 个加油包
- **THEN** 系统只返回主套餐信息,不包含加油包
#### 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