# 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`(可选): 店铺 ID,NULL 表示平台库存 - `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` 的存在。 **数据库变更**: ```sql 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.go`:`DeviceNo` → `VirtualNo`,column tag 更新 - `internal/model/personal_customer_device.go`:`DeviceNo` → `VirtualNo`,column tag 更新 - `internal/model/dto/device_dto.go`:`DeviceResponse.DeviceNo` → `VirtualNo`,JSON tag 更新为 `"virtual_no"` - `internal/store/postgres/device_store.go`:`GetByIdentifier` 查询条件中 `device_no` → `virtual_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 使该设备进入新代际并恢复在库可售