Files
junhong_cmp_fiber/openspec/specs/asset-resolve/spec.md
huang 6dfc5c0301
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m5s
feat: 资产解析接口新增实名时间字段
- AssetResolveResponse 新增 real_name_at 字段:card 类型取 first_realname_at,device 类型取绑定卡中最小的实名时间
- BoundCardInfo 新增 real_name_at 字段:各卡自己的 first_realname_at
- RefreshCardDataFromGateway 手动刷新路径补写实名时间:检测到 0→1 变化时同步写入 first_realname_at
- 同步更新 asset-resolve 规格文档

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 12:12:28 +08:00

206 lines
9.5 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-resolve Specification
## Purpose
提供统一的资产解析入口,通过任意标识符(虚拟号/ICCID/IMEI/SN/MSISDN定位卡或设备并返回该资产的中等聚合信息包含套餐流量、保护期状态和绑定关系。
## Requirements
### Requirement: 统一资产解析入口
系统 SHALL 提供统一的资产查找接口,通过任意标识符定位卡或设备,并返回该资产的中等聚合信息。
**API 端点**: `GET /api/admin/assets/resolve/:identifier`
**查找顺序(更新后)**:
1. **主路径**:查 `tb_asset_identifier` WHERE identifier = ? → 命中则得到 asset_type + asset_id直接查对应表取完整记录
2. **Fallback 路径**(注册表未命中时):
- 先查 `tb_device`(匹配 `virtual_no = ? OR imei = ? OR sn = ?`
- 未命中则查 `tb_iot_card`(匹配 `virtual_no = ? OR iccid = ? OR msisdn = ?`
3. 两条路径均未命中 → 返回 HTTP 404
4. 找到后应用数据权限过滤,无权限 → 返回 HTTP 403
**数据权限规则**:
- 代理用户:只能查看 `shop_id` 在自己及下级店铺范围内的资产
- 平台用户SuperAdmin/Platform可查看所有资产
- 企业账号:暂不支持此接口,调用时返回 HTTP 403
**响应结构AssetResolveResponse**:
*通用字段device 和 card 均有)*:
- `asset_type`: 资产类型(`"device"``"card"`
- `asset_id`: 资产主键 ID
- `identifier`: 本次查询所用的标识符(原样回传)
- `virtual_no`: 虚拟号(设备/卡均使用此字段)
- `status`: 资产状态(整型)
- `asset_status`: 业务状态1-在库 2-已销售 3-已换货 4-已停用)
- `generation`: 资产世代编号
- `batch_no`: 批次号
- `shop_id`: 所属店铺 ID平台库存时为空
- `shop_name`: 所属店铺名称
- `series_id`: 套餐系列 ID未绑定时为空
- `series_name`: 套餐系列名称
- `first_commission_paid`: 一次性佣金是否已发放
- `accumulated_recharge`: 累计充值金额(分)
- `activated_at`: 激活时间(未激活时为空)
- `created_at`: 创建时间
- `updated_at`: 更新时间
- `real_name_at``*time.Time`,可为 null最近一次完成实名的时间
- **card 类型**:直接取 `IotCard.first_realname_at`(未实名过则为 null
- **device 类型**:当前所有绑定卡 `first_realname_at` 中的最小值(无已实名卡则为 null
*状态与套餐字段device 和 card 均有)*:
- `real_name_status`: 实名状态(整型)
- `current_package`: 当前套餐名称(无套餐时返回空字符串)
- `package_total_mb`: 真总流量,即 RealDataMB无套餐时返回 0
- `package_virtual_mb`: 虚总流量/停机阈值,即 VirtualDataMB无套餐时返回 0
- `package_used_mb`: 客户端展示已使用流量(经虚流量换算,见流量计算规则)
- `package_remain_mb`: 客户端展示剩余流量
- `device_protect_status`: 保护期状态(`"none"` / `"stop"` / `"start"`card 类型时若绑定的设备有保护期也返回该设备的保护期状态
*绑定关系字段*:
- `iccid`: 仅 card 类型时有值,供前端调用停复机接口使用
- `bound_device_id`: 仅 card 类型且卡绑定了设备时有值
- `bound_device_no`: 绑定设备的虚拟号
- `bound_device_name`: 绑定设备的名称
- `bound_card_count`: 仅 device 类型时有值,绑定卡的总数量
- `cards`: 仅 device 类型时有值,所有绑定卡列表(含未实名、已停用)
*设备专属档案字段asset_type=device 时有值card 类型时为空/零值)*:
- `device_name`: 设备名称
- `imei`: IMEI
- `sn`: 序列号
- `device_model`: 设备型号
- `device_type`: 设备类型
- `max_sim_slots`: 最大插槽数
- `manufacturer`: 制造商
*卡专属档案字段asset_type=card 时有值device 类型时为空/零值)*:
- `carrier_id`: 运营商 ID
- `carrier_type`: 运营商类型CMCC/CUCC/CTCC/CBN
- `carrier_name`: 运营商名称
- `msisdn`: 卡接入号
- `imsi`: IMSI
- `card_category`: 卡业务类型normal/industry
- `supplier`: 供应商
- `activation_status`: 激活状态0-未激活 1-已激活)
- `enable_polling`: 是否参与轮询
**DeviceCardInfo 结构**:
- `iot_card_id`: 卡 ID
- `iccid`: ICCID
- `virtual_no`: 卡的虚拟号
- `real_name_status`: 实名状态
- `network_status`: 网络状态
- `current_month_usage_mb`: 本月已用流量(来自持久化缓存字段)
- `last_sync_at`: 最后与 Gateway 同步时间
- `real_name_at``*time.Time`,可为 null该卡最近一次完成实名的时间`IotCard.first_realname_at`
**流量展示计算规则**:
- `package_used_mb = current_month_usage_mb × virtual_ratio`
- `package_remain_mb = package_total_mb - package_used_mb`
-`enable_virtual_data = false` 时,`virtual_ratio = 1.0`(无换算)
- 设备级套餐:`current_month_usage_mb` 为所有绑定卡本月用量之和
**特殊情况处理**:
- 卡绑定的设备已被软删除:视为独立卡,不填充绑定信息
- `cards` 列表包含所有状态的绑定卡,不过滤未实名或已停用的卡
#### Scenario: 通过 ICCID 找到卡
- **WHEN** 管理员调用 `GET /api/admin/assets/resolve/89860123456789012345`ICCID 匹配到一张独立卡
- **THEN** 系统返回 `asset_type="card"`,包含该卡的虚拟号、状态、套餐流量信息,`bound_device_id` 为空
#### Scenario: 通过虚拟号找到设备
- **WHEN** 管理员调用 `GET /api/admin/assets/resolve/GPS-001`,设备表中 `virtual_no = "GPS-001"` 存在
- **THEN** 系统返回 `asset_type="device"`包含该设备的绑定卡列表DeviceCardInfo 数组),`bound_card_count` 为绑定卡总数
#### Scenario: 标识符同时命中设备和卡(设备优先)
- **WHEN** `GPS-001` 在 device 表和 iot_card 表均有匹配virtual_no 相同)
- **THEN** 系统返回设备信息device 优先),不返回卡信息
#### Scenario: 标识符未命中任何资产
- **WHEN** 管理员查询不存在的标识符 `UNKNOWN-999`
- **THEN** 系统返回 HTTP 404
#### Scenario: 代理用户查询无权限的资产
- **WHEN** 代理用户shop_id=10查询属于 shop_id=99非下级的设备
- **THEN** 系统返回 HTTP 403明确提示无权限
#### Scenario: 企业账号调用 resolve
- **WHEN** 企业账号调用 `GET /api/admin/assets/resolve/:identifier`
- **THEN** 系统返回 HTTP 403提示企业账号暂不支持此接口
#### Scenario: 卡绑定了有停机保护期的设备
- **WHEN** 管理员通过 ICCID 查询某张卡,该卡绑定的设备当前有 stop 保护期
- **THEN** 响应中 `device_protect_status = "stop"`,反映所属设备的保护期状态
#### Scenario: 设备无当前生效套餐
- **WHEN** 管理员查询一台没有购买任何套餐的设备
- **THEN** `current_package = ""``package_total_mb = 0``package_used_mb = 0``package_remain_mb = 0`
#### Scenario: 通过注册表主路径精确解析
- **WHEN** 管理员输入 identifier 为已存在于 `tb_asset_identifier` 的 VirtualNo 或 ICCID
- **THEN** 系统单次查询注册表命中,直接查对应表返回完整资产信息,响应时间 < 50ms
#### Scenario: Fallback 路径解析 IMEI
- **WHEN** 管理员输入 identifier 为设备 IMEI不在注册表中
- **THEN** 注册表未命中,系统 fallback 查 tb_device 的 imei 字段,找到后返回资产信息
- **THEN** 响应中 `identifier` 字段原样回传该 IMEI 值
#### Scenario: Fallback 路径解析 MSISDN
- **WHEN** 管理员输入 identifier 为 IoT 卡的手机号MSISDN
- **THEN** 注册表未命中fallback 查 tb_iot_card 的 msisdn 字段
- **THEN** 若存在多张卡的 MSISDN 相同返回第一条匹配记录MSISDN 非唯一,存在歧义,记录 warn 日志)
#### Scenario: 已实名的卡查询实名时间
- **WHEN** 通过标识符解析一张 `real_name_status = 1` 的卡
- **THEN** 响应中 `real_name_at` 返回该卡最近一次 `0→1` 变化时的时间戳
#### Scenario: 未实名的卡查询实名时间
- **WHEN** 通过标识符解析一张 `real_name_status = 0` 的卡
- **THEN** 响应中 `real_name_at` 返回 null
#### Scenario: 设备视角查询实名时间(部分卡已实名)
- **WHEN** 通过标识符解析一台设备,其中绑定了多张卡,部分卡已实名
- **THEN** 响应中 `real_name_at` 返回所有已实名绑定卡中 `first_realname_at` 最小的时间戳
#### Scenario: 设备视角查询实名时间(无已实名卡)
- **WHEN** 通过标识符解析一台设备,其所有绑定卡均未实名
- **THEN** 响应中 `real_name_at` 返回 null
#### Scenario: 设备绑定卡列表实名时间
- **WHEN** 通过标识符解析一台设备,绑定卡列表非空
- **THEN** `cards` 数组中每个 `DeviceCardInfo``real_name_at` 返回各卡自己的 `first_realname_at`(未实名则为 null
---
### Requirement: 手动刷新路径同步写入实名时间
系统 SHALL 在手动刷新路径(`RefreshCardDataFromGateway`)检测到实名状态由非已实名变为已实名(`0→1`)时,同步写入 `first_realname_at`,与轮询路径行为一致。
#### Scenario: 手动刷新触发实名状态变更
- **WHEN** 调用手动刷新接口,网关返回实名状态为已实名,且卡当前状态为未实名
- **THEN** `tb_iot_card.first_realname_at` 被更新为当前时间戳
#### Scenario: 手动刷新时实名状态无变化
- **WHEN** 调用手动刷新接口,网关返回实名状态与卡当前状态相同
- **THEN** `tb_iot_card.first_realname_at` 不被修改