All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m5s
- 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>
9.5 KiB
9.5 KiB
asset-resolve Specification
Purpose
提供统一的资产解析入口,通过任意标识符(虚拟号/ICCID/IMEI/SN/MSISDN)定位卡或设备,并返回该资产的中等聚合信息,包含套餐流量、保护期状态和绑定关系。
Requirements
Requirement: 统一资产解析入口
系统 SHALL 提供统一的资产查找接口,通过任意标识符定位卡或设备,并返回该资产的中等聚合信息。
API 端点: GET /api/admin/assets/resolve/:identifier
查找顺序(更新后):
- 主路径:查
tb_asset_identifierWHERE identifier = ? → 命中则得到 asset_type + asset_id,直接查对应表取完整记录 - Fallback 路径(注册表未命中时):
- 先查
tb_device(匹配virtual_no = ? OR imei = ? OR sn = ?) - 未命中则查
tb_iot_card(匹配virtual_no = ? OR iccid = ? OR msisdn = ?)
- 先查
- 两条路径均未命中 → 返回 HTTP 404
- 找到后应用数据权限过滤,无权限 → 返回 HTTP 403
数据权限规则:
- 代理用户:只能查看
shop_id在自己及下级店铺范围内的资产 - 平台用户(SuperAdmin/Platform):可查看所有资产
- 企业账号:暂不支持此接口,调用时返回 HTTP 403
响应结构(AssetResolveResponse):
通用字段(device 和 card 均有):
asset_type: 资产类型("device"或"card")asset_id: 资产主键 IDidentifier: 本次查询所用的标识符(原样回传)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)
- card 类型:直接取
状态与套餐字段(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: IMEIsn: 序列号device_model: 设备型号device_type: 设备类型max_sim_slots: 最大插槽数manufacturer: 制造商
卡专属档案字段(asset_type=card 时有值,device 类型时为空/零值):
carrier_id: 运营商 IDcarrier_type: 运营商类型(CMCC/CUCC/CTCC/CBN)carrier_name: 运营商名称msisdn: 卡接入号imsi: IMSIcard_category: 卡业务类型(normal/industry)supplier: 供应商activation_status: 激活状态(0-未激活 1-已激活)enable_polling: 是否参与轮询
DeviceCardInfo 结构:
iot_card_id: 卡 IDiccid: ICCIDvirtual_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_ratiopackage_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不被修改