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

9.5 KiB
Raw Blame History

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/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

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 数组中每个 DeviceCardInforeal_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 不被修改