Files
junhong_cmp_fiber/openspec/specs/device/spec.md
huang b9733c4913
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m12s
fix: 修正零售价架构错误 + 清理旧微信配置 + 归档提案 + 前端接口文档
1. 修正 retail_price 架构:
   - 删除 batch-pricing 接口的 pricing_target 字段和 retail_price 分支
     (上级只能改下级成本价,不能改零售价)
   - 新增 PATCH /api/admin/packages/:id/retail-price 接口
     (代理自己改自己的零售价,校验 retail_price >= cost_price)

2. 清理旧微信 YAML 配置(已全部迁移到数据库 tb_wechat_config):
   - 删除 config.yaml 中 wechat.official_account 配置节
   - 删除 NewOfficialAccountApp() 旧工厂函数
   - 清理 personal_customer service 中的死代码(旧登录/绑定微信方法)
   - 清理 docker-compose.prod.yml 中旧微信环境变量和证书挂载注释

3. 归档四个已完成提案到 openspec/changes/archive/

4. 新增前端接口变更说明文档(docs/前端接口变更说明.md)

5. 修正归档提案和 specs 中关于 pricing_target 的错误描述
2026-03-19 17:39:43 +08:00

13 KiB
Raw Blame History

device Specification

Purpose

TBD - created by archiving change add-device-management. Update Purpose after archive.

Requirements

Requirement: 设备列表查询

系统 SHALL 提供设备列表查询功能,支持多维度筛选和分页。

查询条件:

  • virtual_no(可选): 设备虚拟号,支持模糊匹配(原 device_no 字段,已全量改名)
  • device_name(可选): 设备名称,支持模糊匹配
  • status(可选): 设备状态,枚举值 1-在库 | 2-已分销 | 3-已激活 | 4-已停用
  • shop_id(可选): 店铺 IDNULL 表示平台库存
  • batch_no(可选): 批次号,精确匹配
  • device_type(可选): 设备类型
  • manufacturer(可选): 制造商,支持模糊匹配
  • created_at_start(可选): 创建时间起始
  • created_at_end(可选): 创建时间结束

分页:

  • 默认每页 20 条,最大每页 100 条
  • 返回总记录数和总页数

数据权限:

  • 平台用户可查看所有设备
  • 代理用户只能查看自己店铺及下级店铺的设备

API 端点: GET /api/admin/devices

响应字段:

  • id: 设备 ID
  • virtual_no: 设备虚拟号(原 device_no,已改名)
  • device_name: 设备名称
  • device_model: 设备型号
  • device_type: 设备类型
  • max_sim_slots: 最大插槽数
  • manufacturer: 制造商
  • batch_no: 批次号
  • shop_id: 店铺 ID
  • shop_name: 店铺名称
  • status: 状态
  • status_name: 状态名称
  • bound_card_count: 已绑定卡数量
  • activated_at: 激活时间
  • created_at: 创建时间
  • updated_at: 更新时间

Scenario: 平台查询所有设备

  • WHEN 平台管理员查询设备列表,不带任何筛选条件
  • THEN 系统返回所有设备,按创建时间倒序排列

Scenario: 按虚拟号模糊查询

  • WHEN 管理员输入 virtual_no = "GPS"
  • THEN 系统返回虚拟号包含 "GPS" 的所有设备

Scenario: 按状态筛选设备

  • WHEN 管理员查询状态为 1在库的设备
  • THEN 系统只返回在库状态的设备

Scenario: 代理查询自己店铺的设备

  • WHEN 代理用户(店铺 ID=10查询设备列表
  • THEN 系统只返回 shop_id 为 10 及其下级店铺的设备

Scenario: 查询平台库存设备

  • WHEN 平台管理员查询 shop_id 为空的设备
  • THEN 系统返回所有平台库存设备shop_id = NULL

Requirement: 设备详情查询

系统 SHALL 提供设备详情查询功能,返回设备的基本信息。

API 端点: GET /api/admin/devices/:id

响应字段:

  • 包含设备的所有基本字段(含 virtual_no,不再有 device_no
  • shop_name: 店铺名称(如果有)

数据权限:

  • 平台用户可查看所有设备
  • 代理用户只能查看自己店铺及下级店铺的设备

Scenario: 查询设备详情成功

  • WHEN 管理员查询设备详情ID=1
  • THEN 系统返回该设备的完整基本信息,响应中含 virtual_no 字段,不含 device_no

Scenario: 查询不存在的设备

  • WHEN 管理员查询不存在的设备ID=999
  • THEN 系统返回 404 错误,提示"设备不存在"

Scenario: 代理查询无权限的设备

  • WHEN 代理用户(店铺 ID=10查询其他店铺的设备shop_id=20非下级
  • THEN 系统返回 404 错误,提示"设备不存在"

Requirement: 删除设备

系统 SHALL 提供删除设备功能,仅平台用户可操作,执行软删除。

API 端点: DELETE /api/admin/devices/:id

业务规则:

  • 仅平台用户可删除设备
  • 删除设备时自动解绑该设备上的所有卡
  • 执行软删除(设置 deleted_at

权限: 仅平台用户

Scenario: 平台删除设备成功

  • WHEN 平台管理员删除设备ID=1
  • THEN 系统软删除该设备,并解绑设备上的所有卡

Scenario: 代理尝试删除设备

  • WHEN 代理用户尝试删除设备
  • THEN 系统返回 403 错误,提示"无权限执行此操作"

Scenario: 删除不存在的设备

  • WHEN 平台管理员删除不存在的设备ID=999
  • THEN 系统返回 404 错误,提示"设备不存在"

Requirement: 获取设备绑定的卡列表

系统 SHALL 提供查询设备绑定的 IoT 卡列表功能。

API 端点: GET /api/admin/devices/:id/cards

响应字段:

  • bindings: 绑定列表,每个元素包含:
    • id: 绑定记录 ID
    • slot_position: 插槽位置1-4
    • iot_card_id: IoT 卡 ID
    • iccid: ICCID
    • msisdn: 接入号
    • carrier_name: 运营商名称
    • status: 卡状态
    • bind_time: 绑定时间

Scenario: 查询设备绑定的卡

  • WHEN 管理员查询设备ID=1绑定的卡
  • THEN 系统返回该设备所有已绑定的卡信息,按插槽位置排序

Scenario: 查询无绑定卡的设备

  • WHEN 管理员查询没有绑定卡的设备
  • THEN 系统返回空的绑定列表

Requirement: 绑定卡到设备

系统 SHALL 提供将 IoT 卡绑定到设备指定插槽的功能,仅平台用户可操作。

API 端点: POST /api/admin/devices/:id/cards

请求参数:

  • iot_card_id: IoT 卡 ID必填
  • slot_position: 插槽位置 1-4必填

业务规则:

  • 仅平台用户可操作
  • 插槽位置不能超过设备的 max_sim_slots
  • 该插槽必须为空(无已绑定的卡)
  • 该卡不能已绑定到其他设备
  • 绑定操作不改变卡的 shop_id

权限: 仅平台用户

Scenario: 绑定卡到设备成功

  • WHEN 平台管理员将 IoT 卡ID=101绑定到设备ID=1的插槽 2
  • THEN 系统创建绑定记录,返回绑定成功信息

Scenario: 绑定到已占用的插槽

  • WHEN 平台管理员尝试绑定卡到已有卡的插槽
  • THEN 系统返回错误,提示"该插槽已有绑定的卡"

Scenario: 绑定已被绑定的卡

  • WHEN 平台管理员尝试绑定已绑定到其他设备的卡
  • THEN 系统返回错误,提示"该卡已绑定到其他设备"

Scenario: 插槽位置超出范围

  • WHEN 平台管理员尝试绑定卡到插槽 5设备 max_sim_slots=4
  • THEN 系统返回错误,提示"插槽位置超出设备最大插槽数"

Scenario: 代理尝试绑定卡

  • WHEN 代理用户尝试绑定卡到设备
  • THEN 系统返回 403 错误,提示"无权限执行此操作"

Requirement: 解绑设备上的卡

系统 SHALL 提供解绑设备上指定卡的功能,仅平台用户可操作。

API 端点: DELETE /api/admin/devices/:id/cards/:cardId

业务规则:

  • 仅平台用户可操作
  • 更新绑定记录的 bind_status 为 2已解绑记录 unbind_time
  • 解绑操作不改变卡的 shop_id

权限: 仅平台用户

Scenario: 解绑卡成功

  • WHEN 平台管理员解绑设备ID=1上的卡ID=101
  • THEN 系统更新绑定记录状态为已解绑,返回成功信息

Scenario: 解绑不存在的绑定关系

  • WHEN 平台管理员尝试解绑不存在的绑定关系
  • THEN 系统返回错误,提示"该卡未绑定到此设备"

Scenario: 代理尝试解绑卡

  • WHEN 代理用户尝试解绑设备上的卡
  • THEN 系统返回 403 错误,提示"无权限执行此操作"

Requirement: 批量分配设备

系统 SHALL 提供批量分配设备给下级店铺的功能,分配时自动同步绑定卡的归属。

API 端点: POST /api/admin/devices/allocate

请求参数:

  • target_shop_id: 目标店铺 ID必填
  • device_ids: 设备 ID 列表(必填,最多 100 个)
  • remark: 备注(可选)

业务规则:

  • 只能分配给直属下级店铺,不可跨级
  • 平台只能分配 shop_id=NULL 的设备
  • 代理只能分配自己店铺的设备
  • 分配后:
    • 设备的 shop_id 变更为目标店铺 ID
    • 设备绑定的所有卡的 shop_id 也变更为目标店铺 ID
    • 设备状态变为「已分销」(2)
  • 创建资产分配记录asset_type='device'

响应:

  • success_count: 成功数量
  • fail_count: 失败数量
  • failed_items: 失败详情列表

Scenario: 平台分配设备给一级代理

  • WHEN 平台管理员将 5 台设备分配给一级代理店铺ID=10
  • THEN 系统更新这 5 台设备及其绑定卡的 shop_id 为 10创建分配记录返回成功数量

Scenario: 代理分配设备给下级

  • WHEN 代理(店铺 ID=10将 3 台设备分配给直属下级店铺ID=101
  • THEN 系统更新这 3 台设备及其绑定卡的 shop_id 为 101创建分配记录

Scenario: 分配给非直属下级

  • WHEN 代理(店铺 ID=10尝试分配设备给非直属下级店铺ID=1011是 101 的下级)
  • THEN 系统返回错误,提示"只能分配给直属下级店铺"

Scenario: 分配不属于自己的设备

  • WHEN 代理(店铺 ID=10尝试分配其他店铺的设备
  • THEN 系统跳过这些设备,只分配属于自己的设备

Requirement: 批量回收设备

系统 SHALL 提供批量回收已分配设备的功能,回收时自动同步绑定卡的归属。

API 端点: POST /api/admin/devices/recall

请求参数:

  • device_ids: 设备 ID 列表(必填,最多 100 个)
  • remark: 备注(可选)

业务规则:

  • 只能回收直属下级店铺的设备,不可跨级
  • 平台回收后:设备和绑定卡的 shop_id 变为 NULL
  • 代理回收后:设备和绑定卡的 shop_id 变为执行回收的店铺 ID
  • 创建资产回收记录asset_type='device'

响应:

  • success_count: 成功数量
  • fail_count: 失败数量
  • failed_items: 失败详情列表

Scenario: 平台回收一级代理的设备

  • WHEN 平台管理员回收一级代理店铺ID=10的 3 台设备
  • THEN 系统更新这 3 台设备及其绑定卡的 shop_id 为 NULL创建回收记录

Scenario: 代理回收下级的设备

  • WHEN 代理(店铺 ID=10回收下级店铺ID=101的 2 台设备
  • THEN 系统更新这 2 台设备及其绑定卡的 shop_id 为 10创建回收记录

Scenario: 回收非直属下级的设备

  • WHEN 代理(店铺 ID=10尝试回收非直属下级的设备
  • THEN 系统返回错误,提示"只能回收直属下级店铺的设备"

Requirement: device_no 全量改名为 virtual_no

系统 SHALL 将 tb_device 表和 tb_personal_customer_device 表中的 device_no 字段全量改名为 virtual_no,确保系统中不再有 device_no 的存在。

数据库变更:

ALTER TABLE tb_device RENAME COLUMN device_no TO virtual_no;
ALTER TABLE tb_personal_customer_device RENAME COLUMN device_no TO virtual_no;

代码影响范围:

  • internal/model/device.goDeviceNoVirtualNocolumn tag 更新
  • internal/model/personal_customer_device.goDeviceNoVirtualNocolumn tag 更新
  • internal/model/dto/device_dto.goDeviceResponse.DeviceNoVirtualNoJSON tag 更新为 "virtual_no"
  • internal/store/postgres/device_store.goGetByIdentifier 查询条件中 device_novirtual_no
  • internal/store/postgres/personal_customer_device_store.go:所有 device_no 引用更新
  • 所有 Handler、Service 中引用 DeviceNo 字段的代码全量替换

设备导入模板:

  • 导入 Excel 模板中的列头从 device_no 更新为 virtual_no

Scenario: 改名后查询设备

  • WHEN 改名迁移完成后,调用 GetByIdentifier("GPS-001")
  • THEN 系统在 WHERE virtual_no = ? OR imei = ? OR sn = ? 中正确匹配,与改名前行为一致

Scenario: 响应中字段名已更新

  • WHEN 前端调用设备列表或详情接口
  • THEN 响应 JSON 中 key 为 virtual_no,不再有 device_no

Requirement: 设备实体定义

系统 SHALL 在 Device 模型新增以下字段:

  • asset_status int NOT NULL DEFAULT 1
  • generation int NOT NULL DEFAULT 1

Scenario: 新建设备默认资产状态

  • WHEN 创建新的设备记录
  • THEN asset_status MUST 默认为 1(在库)

Scenario: 新建设备默认代际

  • WHEN 创建新的设备记录
  • THEN generation MUST 默认为 1

Requirement: 设备换货状态语义扩展

系统 SHALL 将 asset_status=3 定义为"已换货",用于标记已被换出的旧设备资产。

Scenario: 换货完成后旧设备标记

  • WHEN H5 确认完成且旧资产为设备
  • THEN 系统 MUST 将旧设备 asset_status 更新为 3

Requirement: 设备转新重置规则

系统 SHALL 在 H7 转新时对设备执行以下重置:

  • generation = generation + 1
  • asset_status = 1(在库)
  • 清空累计充值与首充触发相关状态
  • 清除个人客户绑定关系
  • 创建新空钱包并与新代际设备关联

Scenario: 转新后设备可重新销售

  • WHEN 对已换货设备执行转新
  • THEN 系统 MUST 使该设备进入新代际并恢复在库可售