All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m3s
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
173 lines
6.6 KiB
Markdown
173 lines
6.6 KiB
Markdown
# 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
|