Files
junhong_cmp_fiber/openspec/specs/asset-resolve/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

6.0 KiB
Raw Blame History

asset-resolve Specification

Purpose

提供统一的资产解析入口,通过任意标识符(虚拟号/ICCID/IMEI/SN/MSISDN定位卡或设备并返回该资产的中等聚合信息包含套餐流量、保护期状态和绑定关系。

Requirements

Requirement: 统一资产解析入口

系统 SHALL 提供统一的资产查找接口,通过任意标识符定位卡或设备,并返回该资产的中等聚合信息。

API 端点: GET /api/admin/assets/resolve/:identifier

查找顺序:

  1. 先在 tb_device 表查找(匹配 virtual_no = ? OR imei = ? OR sn = ?
  2. 未命中则在 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
  • virtual_no: 虚拟号(设备/卡均使用此字段)
  • status: 资产状态(整型)
  • batch_no: 批次号
  • shop_id: 所属店铺 ID平台库存时为空
  • shop_name: 所属店铺名称
  • series_id: 套餐系列 ID未绑定时为空
  • series_name: 套餐系列名称
  • first_commission_paid: 一次性佣金是否已发放
  • accumulated_recharge: 累计充值金额(分)
  • activated_at: 激活时间(未激活时为空)
  • created_at: 创建时间
  • updated_at: 更新时间

状态与套餐字段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 同步时间

流量展示计算规则:

  • 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/89860123456789012345ICCID 匹配到一张独立卡
  • 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 = 0package_used_mb = 0package_remain_mb = 0