feat: 新增数据库迁移,重命名 device_no 为 virtual_no,新增 iot_card.virtual_no 和 package.virtual_ratio 字段
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>
This commit is contained in:
2026-03-14 18:27:28 +08:00
parent b5147d1acb
commit b9c3875c08
77 changed files with 5832 additions and 2393 deletions

View File

@@ -0,0 +1,172 @@
# 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