feat: 实现企业设备授权功能并归档 OpenSpec 变更
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m39s

- 新增企业设备授权模块(Model、DTO、Service、Handler、Store)
- 实现设备授权的创建、查询、更新、删除等完整业务逻辑
- 添加企业卡授权与设备授权的关联关系
- 新增 2 个数据库迁移脚本
- 同步 OpenSpec delta specs 到 main specs
- 归档 add-enterprise-device-authorization 变更
- 更新 API 文档和路由配置
- 新增完整的集成测试和单元测试覆盖
This commit is contained in:
2026-01-29 13:18:49 +08:00
parent e87513541b
commit b02175271a
118 changed files with 14306 additions and 472 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-01-29

View File

@@ -0,0 +1,181 @@
## Context
当前系统支持"单卡授权企业用户"功能,通过 `tb_enterprise_card_authorization` 表记录卡与企业的授权关系。但现有实现存在以下问题:
1. **设备维度缺失**:企业用户只能看到卡列表,无法以设备为单位管理资产
2. **逻辑不一致**:单卡授权入口支持 DeviceBundle确认后授权设备下所有卡但没有独立的设备授权概念
3. **记录关联缺失**:无法追踪卡授权是通过单独授权还是设备授权创建的
现有相关模块:
- `Device` 模型:设备表 `tb_device`,通过 `shop_id` 标识所有权
- `DeviceSimBinding` 模型:设备-卡绑定关系表,一设备最多绑定 4 张卡
- `EnterpriseCardAuthorization` 模型:卡授权表
- 设备分销功能:`AllocateDevices` 将设备分销给代理店铺(修改 `shop_id`
## Goals / Non-Goals
**Goals:**
- 支持以设备为单位授权给企业,自动授权设备下所有已绑定的卡
- 一个设备同一时间只能授权给一个企业(唯一性约束)
- 授权设备时自动创建卡授权记录,回收时同步回收
- 卡授权记录关联设备授权,支持追溯授权来源
- 企业端可以查看设备列表、设备详情及其绑定的卡
- 企业端可以对设备下的卡进行停机/复机操作
- 单卡授权入口禁止授权已绑定设备的卡
**Non-Goals:**
- 不涉及设备分销逻辑(设备 → 店铺,已有功能)
- 不涉及设备级别的停机/复机(仍然是卡级别操作)
- 不涉及设备解绑卡的功能(企业只能查看,不能解绑)
- 不涉及设备钱包或套餐购买功能
## Decisions
### 1. 新增独立的设备授权表
**决策**:创建 `tb_enterprise_device_authorization` 表,与卡授权表结构类似
**理由**
- 设备授权是独立的业务概念,需要独立的授权记录
- 支持设备级别的授权/回收操作和记录查询
- 与卡授权表解耦,职责清晰
**替代方案**
- ❌ 在卡授权表中添加 device_id 字段:无法表达"设备授权"这个独立概念,回收设备时逻辑复杂
- ❌ 只用卡授权表+标记字段:无法追踪设备授权的元信息(授权人、时间等)
### 2. 卡授权表添加 device_auth_id 关联字段
**决策**:在 `tb_enterprise_card_authorization` 表添加 `device_auth_id` 字段
**理由**
- 明确区分卡授权来源单卡授权NULLvs 设备授权(有值)
- 回收设备授权时可以精确定位需要回收的卡授权记录
- 支持查询"某设备授权下的所有卡"
**表结构变更**
```sql
ALTER TABLE tb_enterprise_card_authorization
ADD COLUMN device_auth_id BIGINT DEFAULT NULL;
CREATE INDEX idx_eca_device_auth ON tb_enterprise_card_authorization(device_auth_id);
```
### 3. 设备授权唯一性约束
**决策**:使用部分唯一索引保证一个设备同时只能授权给一个企业
**实现**
```sql
CREATE UNIQUE INDEX uq_active_device_auth
ON tb_enterprise_device_authorization(device_id)
WHERE revoked_at IS NULL AND deleted_at IS NULL;
```
**理由**
- 允许历史授权记录(已回收的)存在多条
- 只限制"当前有效"的授权唯一性
- 与卡授权的设计模式一致
### 4. 授权联动机制
**决策**:授权设备时在同一事务内创建设备授权和卡授权记录
**流程**
```
授权设备 → 事务开始
→ 创建 EnterpriseDeviceAuthorization
→ 获取 device_auth_id
→ 查询设备下所有已绑定的卡
→ 批量创建 EnterpriseCardAuthorization (device_auth_id = 上一步的ID)
→ 事务提交
```
**回收流程**
```
回收设备 → 事务开始
→ 更新 EnterpriseDeviceAuthorization.revoked_at
→ 批量更新关联的 EnterpriseCardAuthorization.revoked_at
→ 事务提交
```
### 5. 单卡授权入口修改
**决策**:移除 DeviceBundle 支持,禁止授权已绑定设备的卡
**修改点**
- `Service.AllocateCards`:移除 DeviceBundle 预检和处理逻辑
- `Service.AllocateCardsPreview`:直接返回错误而非 DeviceBundle
- DTO移除 `DeviceBundle``ConfirmDeviceBundles` 等相关结构
**理由**
- 职责分离:单卡授权只处理独立卡,设备授权处理设备
- 避免逻辑混淆:用户不会再在单卡入口看到设备相关提示
- 简化代码:移除复杂的 DeviceBundle 处理逻辑
**BREAKING CHANGE**:前端单卡授权页面需要适配,不再支持确认设备包
### 6. API 路径设计
**后台管理Admin**
```
POST /api/admin/enterprises/:id/allocate-devices # 授权设备
POST /api/admin/enterprises/:id/recall-devices # 回收设备
GET /api/admin/enterprises/:id/devices # 设备列表
```
**企业端H5**
```
GET /api/h5/enterprise/devices # 设备列表
GET /api/h5/enterprise/devices/:device_id # 设备详情
POST /api/h5/enterprise/devices/:device_id/cards/:card_id/suspend # 停机
POST /api/h5/enterprise/devices/:device_id/cards/:card_id/resume # 复机
```
**理由**
- 与现有单卡授权 API 风格一致(`/enterprises/:id/allocate-cards`
- H5 端使用 `/enterprise/devices` 而非 `/enterprises/:id/devices`,因为企业用户只能访问自己的资源
### 7. 权限控制
**后台管理**
- 平台用户:可以授权任意设备给任意企业
- 代理用户:只能授权自己店铺的设备给自己店铺下的企业
**企业端**
- 只能访问授权给自己企业的设备
- 通过 GORM Callback 自动过滤
## Risks / Trade-offs
### [风险] 单卡授权入口 Breaking Change
**影响**:前端单卡授权页面行为变更,不再支持授权设备卡
**缓解**
- 提前通知前端团队
- 返回明确的错误信息引导用户使用设备授权入口
- 可选:在错误响应中返回涉及的设备信息,方便前端跳转
### [风险] 数据迁移
**影响**:如果现有数据中有通过单卡授权入口授权的设备卡,无法追溯来源
**缓解**
- 新字段 `device_auth_id` 默认 NULL兼容历史数据
- 历史数据视为"单卡授权",行为不变
- 无需数据迁移脚本
### [权衡] 回收粒度
**选择**:回收设备授权时同步回收所有关联的卡授权
**权衡**:不支持只回收设备授权但保留卡授权
**理由**:简化业务逻辑,保持授权关系一致性
### [权衡] 设备新增卡后的处理
**场景**:设备已授权给企业后,又绑定了新的卡
**选择**:新卡不自动授权,需要重新授权设备或单独处理
**理由**:避免隐式授权带来的安全风险,保持授权行为显式可控
## Open Questions
1. **设备授权记录管理页面**:是否需要独立的设备授权记录列表页面(类似现有的卡授权记录页面)?
- 建议:本期先不做,通过企业设备列表满足基本需求
2. **批量操作限制**:单次授权/回收设备的数量上限?
- 建议:与单卡授权一致,最多 100 个设备

View File

@@ -0,0 +1,53 @@
## Why
企业用户目前只能管理被授权的单卡,但实际业务中设备(绑定 1-4 张卡)是更常见的授权单位。企业需要以设备为维度查看和管理被授权的资产,包括查看设备列表、设备详情及其绑定的卡,以及对卡进行停机/复机操作。这与"分销设备给代理"的模式类似,但目标是企业而非店铺。
## What Changes
- **新增设备授权表**`tb_enterprise_device_authorization`,记录设备与企业的授权关系
- **修改卡授权表**`tb_enterprise_card_authorization` 新增 `device_auth_id` 字段,关联设备授权记录
- **新增设备授权 API**(后台):授权设备给企业、回收设备授权、企业设备列表
- **新增企业端设备管理 API**H5设备列表、设备详情含卡、停机/复机
- **修改单卡授权逻辑****BREAKING** 禁止通过单卡授权入口授权已绑定设备的卡,移除 DeviceBundle 支持
- **授权联动**:授权设备时自动授权设备下所有已绑定的卡,回收时同步回收
## Capabilities
### New Capabilities
- `enterprise-device-authorization`: 设备授权企业用户功能,包含设备授权/回收、设备授权记录管理、企业端设备列表和管理
### Modified Capabilities
- `enterprise-card-authorization`: 禁止授权已绑定设备的卡,移除 DeviceBundle 确认流程,强制使用设备授权入口
## Impact
**数据库**:
- 新增表 `tb_enterprise_device_authorization`
- 修改表 `tb_enterprise_card_authorization`(新增字段 + 索引)
**后台 API**:
- 新增 `POST /api/admin/enterprises/:id/allocate-devices`
- 新增 `POST /api/admin/enterprises/:id/recall-devices`
- 新增 `GET /api/admin/enterprises/:id/devices`
- 修改 `POST /api/admin/enterprises/:id/allocate-cards`(禁止设备卡)
**H5 API**:
- 新增 `GET /api/h5/enterprise/devices`
- 新增 `GET /api/h5/enterprise/devices/:device_id`
- 新增 `POST /api/h5/enterprise/devices/:device_id/cards/:card_id/suspend`
- 新增 `POST /api/h5/enterprise/devices/:device_id/cards/:card_id/resume`
**代码模块**:
- Model: 新增 `EnterpriseDeviceAuthorization`,修改 `EnterpriseCardAuthorization`
- Store: 新增 `EnterpriseDeviceAuthorizationStore`
- Service: 新增 `enterprise_device` 服务,修改 `enterprise_card` 服务
- Handler: 新增 `admin/enterprise_device.go`,新增 `h5/enterprise_device.go`
- Routes: 新增设备授权路由注册
- DTO: 新增设备授权相关 DTO
**前端影响**:
- 后台管理系统需要新增设备授权功能页面
- 企业端 H5 需要新增设备列表和管理页面
- 单卡授权页面行为变更(不再支持授权设备卡)

View File

@@ -0,0 +1,132 @@
## MODIFIED Requirements
### Requirement: 企业单卡授权管理
系统 SHALL 支持将 IoT 卡授权给企业使用,授权不转移所有权,仅授予使用权限。
**授权规则**
- 代理只能授权自己的卡owner_type="agent" 且 owner_id=自己的 shop_id给自己的企业
- 平台可以授权任意卡,但如果是代理的卡,只能授权给该代理的企业
- 支持批量授权最多1000张卡
- **已绑定设备的卡不能通过单卡授权接口授权MUST 使用设备授权接口**
- 只能授权状态为 "已分销(2)" 的卡
**授权记录存储**
- 使用 `enterprise_card_authorization` 表记录授权关系
- 通过单卡授权创建的记录 device_auth_id 为 NULL
- 不使用 `asset_allocation_record` 表(该表用于分配,非授权)
**权限控制**
- 企业用户只能查看被授权的卡
- 授权后卡的 shop_id 保持不变(所有权不转移)
- 回收授权后企业立即失去访问权限
#### Scenario: 代理授权自己的卡给自己的企业
- **WHEN** 代理shop_id=10将自己的未绑定设备的卡授权给企业enterprise_id=5, owner_shop_id=10
- **THEN** 系统创建授权记录device_auth_id=NULL企业可以查看和管理该卡
#### Scenario: 平台授权任意卡给企业
- **WHEN** 平台管理员将未绑定设备的卡授权给企业
- **THEN** 系统创建授权记录device_auth_id=NULL企业获得该卡的访问权限
#### Scenario: 代理无法授权其他代理的卡
- **WHEN** 代理shop_id=10尝试授权其他代理的卡owner_id=20给企业
- **THEN** 系统拒绝操作,返回权限错误
#### Scenario: 已绑定设备的卡不能通过单卡授权
- **WHEN** 用户尝试通过单卡授权接口授权已绑定到设备的卡
- **THEN** 系统拒绝操作,返回错误码 CodeCannotAuthorizeBoundCard提示"该卡已绑定设备,请使用设备授权功能"
#### Scenario: 只能授权已分销状态的卡
- **WHEN** 用户尝试授权非"已分销"状态的卡
- **THEN** 系统拒绝操作,提示只能授权"已分销"状态的卡
---
### Requirement: 企业卡授权数据模型
系统 SHALL 定义 EnterpriseCardAuthorization 实体,记录企业卡授权关系。
**实体字段**
- `id`: 主键BIGINT
- `enterprise_id`: 被授权企业IDBIGINT关联 enterprises 表)
- `card_id`: IoT卡IDBIGINT关联 iot_cards 表)
- `authorizer_id`: 授权人账号IDBIGINT关联 accounts 表)
- `authorizer_type`: 授权人类型SMALLINT2=平台用户 3=代理账号)
- `authorized_at`: 授权时间TIMESTAMP
- `revoked_at`: 回收时间TIMESTAMP可空
- `revoked_by`: 回收人账号IDBIGINT可空
- `remark`: 备注VARCHAR(500)
- **`device_auth_id`: 关联的设备授权IDBIGINT可空**
- NULL = 通过单卡授权创建
- 有值 = 通过设备授权创建
- `created_at`: 创建时间TIMESTAMP
- `updated_at`: 更新时间TIMESTAMP
**新增索引**
- `idx_eca_device_auth ON tb_enterprise_card_authorization(device_auth_id)`
#### Scenario: 创建单卡授权记录
- **WHEN** 通过单卡授权接口授权卡给企业时
- **THEN** 系统创建 EnterpriseCardAuthorization 记录device_auth_id 为 NULL
#### Scenario: 创建设备关联卡授权记录
- **WHEN** 通过设备授权创建卡授权记录时
- **THEN** 系统创建 EnterpriseCardAuthorization 记录device_auth_id 指向对应的设备授权ID
#### Scenario: 回收授权
- **WHEN** 回收企业的卡授权时
- **THEN** 系统更新对应记录的 revoked_at 和 revoked_by 字段,不删除记录(保留历史)
---
### Requirement: 批量授权接口
系统 SHALL 提供批量授权接口,支持一次授权多张卡给企业。
**接口设计**
- 路径:`POST /api/admin/enterprises/:id/allocate-cards`
- 请求体:
```json
{
"iccids": ["8986001234567890", "8986001234567891"],
"remark": "批量授权"
}
```
- 响应:成功/失败的卡列表及原因
**处理流程**
1. 验证每张卡的授权权限
2. 检查卡状态是否为"已分销"
3. **检查卡是否已绑定设备,绑定设备的卡直接拒绝并返回错误**
4. 检查是否已授权给该企业
5. 创建授权记录device_auth_id = NULL
6. 返回处理结果
**移除功能**
- ~~DeviceBundle 预检和确认流程~~(已移除)
- ~~confirm_device_bundles 参数~~(已移除)
- ~~AllocatedDevices 响应字段~~(已移除)
#### Scenario: 批量授权成功
- **WHEN** 代理批量授权 5 张未绑定设备的卡给企业
- **THEN** 系统创建 5 条授权记录device_auth_id 均为 NULL返回全部成功
#### Scenario: 批量授权遇到设备卡
- **WHEN** 代理批量授权 5 张卡,其中 2 张已绑定设备
- **THEN** 系统创建 3 条授权记录,返回 3 张成功、2 张失败,失败原因为"该卡已绑定设备,请使用设备授权功能"
#### Scenario: 批量授权部分成功
- **WHEN** 代理批量授权 5 张卡,其中 1 张已绑定设备、1 张非已分销状态
- **THEN** 系统创建 3 条授权记录,返回 3 张成功、2 张失败及各自失败原因

View File

@@ -0,0 +1,319 @@
## ADDED Requirements
### Requirement: 设备授权企业数据模型
系统 SHALL 定义 EnterpriseDeviceAuthorization 实体,记录设备与企业的授权关系。
**实体字段**
- `id`: 主键BIGSERIAL
- `enterprise_id`: 被授权企业IDBIGINTNOT NULL
- `device_id`: 被授权设备IDBIGINTNOT NULL
- `authorized_by`: 授权人账号IDBIGINTNOT NULL
- `authorized_at`: 授权时间TIMESTAMPNOT NULL
- `authorizer_type`: 授权人类型SMALLINT2=平台用户 3=代理账号)
- `revoked_by`: 回收人账号IDBIGINT可空
- `revoked_at`: 回收时间TIMESTAMP可空
- `remark`: 备注VARCHAR(500)
- `created_at`, `updated_at`, `deleted_at`: 标准时间字段
**唯一性约束**
- 一个设备同时只能授权给一个企业:`UNIQUE (device_id) WHERE revoked_at IS NULL AND deleted_at IS NULL`
**表名**`tb_enterprise_device_authorization`
#### Scenario: 创建设备授权记录
- **WHEN** 授权设备给企业时
- **THEN** 系统创建 EnterpriseDeviceAuthorization 记录authorized_at 设置为当前时间revoked_at 为 NULL
#### Scenario: 设备重复授权被拒绝
- **WHEN** 尝试将已授权给企业A的设备未回收再授权给企业B
- **THEN** 系统拒绝操作,返回错误"设备已授权给其他企业"
#### Scenario: 回收后可重新授权
- **WHEN** 设备授权已被回收后,重新授权给同一企业或其他企业
- **THEN** 系统允许创建新的授权记录
---
### Requirement: 卡授权记录关联设备授权
系统 SHALL 在 EnterpriseCardAuthorization 表中添加 device_auth_id 字段,关联设备授权记录。
**新增字段**
- `device_auth_id`: 关联的设备授权IDBIGINT可空
- NULL = 通过单卡授权创建
- 有值 = 通过设备授权创建
**索引**
- `idx_eca_device_auth ON tb_enterprise_card_authorization(device_auth_id)`
#### Scenario: 设备授权创建关联卡授权
- **WHEN** 通过设备授权创建卡授权记录时
- **THEN** 卡授权记录的 device_auth_id 字段设置为对应的设备授权ID
#### Scenario: 单卡授权不关联设备
- **WHEN** 通过单卡授权创建卡授权记录时
- **THEN** 卡授权记录的 device_auth_id 字段为 NULL
---
### Requirement: 设备授权管理功能
系统 SHALL 提供设备授权给企业的功能,支持批量授权和回收。
**授权规则**
- 代理只能授权自己店铺的设备给自己店铺下的企业
- 平台可以授权任意设备给任意企业
- 设备 MUST 属于操作者(平台或代理店铺)
- 设备 MUST 处于"已分销"状态status=2
- 设备 MUST 未授权给其他企业(唯一性约束)
**授权联动**
- 授权设备时,系统 SHALL 自动授权设备下所有已绑定的卡
- 卡授权记录的 device_auth_id 指向设备授权记录
- 如果设备没有绑定卡,仍然创建设备授权记录(无卡授权)
#### Scenario: 代理授权设备给自己的企业
- **WHEN** 代理shop_id=10将自己店铺的设备授权给企业owner_shop_id=10
- **THEN** 系统创建设备授权记录,并为设备下所有已绑定的卡创建卡授权记录
#### Scenario: 平台授权任意设备
- **WHEN** 平台管理员授权设备给任意企业
- **THEN** 系统创建授权记录,不检查设备和企业的归属关系
#### Scenario: 代理无法授权其他店铺的设备
- **WHEN** 代理shop_id=10尝试授权其他店铺的设备shop_id=20
- **THEN** 系统拒绝操作,返回权限错误
#### Scenario: 设备授权联动卡授权
- **WHEN** 授权一个绑定了3张卡的设备给企业
- **THEN** 系统创建1条设备授权记录和3条卡授权记录所有卡授权的 device_auth_id 指向该设备授权
---
### Requirement: 批量授权设备接口
系统 SHALL 提供批量授权设备给企业的后台接口。
**接口设计**
- 路径:`POST /api/admin/enterprises/:id/allocate-devices`
- 请求体:
```json
{
"device_nos": ["D001", "D002", "D003"],
"remark": "批量授权备注"
}
```
- 响应体:
```json
{
"success_count": 2,
"fail_count": 1,
"failed_items": [
{ "device_no": "D003", "reason": "设备不存在" }
],
"authorized_devices": [
{ "device_id": 1, "device_no": "D001", "card_count": 3 },
{ "device_id": 2, "device_no": "D002", "card_count": 2 }
]
}
```
**处理流程**
1. 验证企业存在且有权限
2. 验证每个设备的授权权限
3. 检查设备状态和唯一性约束
4. 在事务内创建设备授权和卡授权记录
5. 返回处理结果
#### Scenario: 批量授权成功
- **WHEN** 平台批量授权3个符合条件的设备给企业
- **THEN** 系统创建3条设备授权记录和对应的卡授权记录返回全部成功
#### Scenario: 批量授权部分成功
- **WHEN** 代理批量授权3个设备其中1个已授权给其他企业
- **THEN** 系统创建2条设备授权记录返回2个成功、1个失败及失败原因
---
### Requirement: 设备授权回收功能
系统 SHALL 提供回收设备授权的功能,回收时同步回收关联的卡授权。
**回收规则**
- 代理可以回收自己授权的设备
- 平台可以回收任何设备授权
- 回收操作在事务内完成
**回收联动**
- 回收设备授权时,系统 SHALL 同步回收所有 device_auth_id 指向该设备授权的卡授权记录
- 更新 revoked_at 和 revoked_by 字段
**接口设计**
- 路径:`POST /api/admin/enterprises/:id/recall-devices`
- 请求体:
```json
{
"device_nos": ["D001", "D002"]
}
```
#### Scenario: 回收设备授权联动回收卡授权
- **WHEN** 回收一个绑定了3张卡的设备的授权
- **THEN** 系统更新设备授权的 revoked_at同时更新3条关联卡授权的 revoked_at
#### Scenario: 回收后企业无法访问设备和卡
- **WHEN** 设备授权被回收后,企业用户查询设备或卡
- **THEN** 系统不返回该设备和其下的卡
---
### Requirement: 后台企业设备列表
系统 SHALL 提供后台管理查询企业授权设备列表的接口。
**接口设计**
- 路径:`GET /api/admin/enterprises/:id/devices`
- 查询参数:`page`, `page_size`, `device_no`, `status`
- 响应:设备列表,包含设备信息和绑定卡数量
**数据权限**
- 平台用户可查看所有企业的授权设备
- 代理用户只能查看自己店铺下企业的授权设备
#### Scenario: 查询企业授权设备列表
- **WHEN** 管理员查询企业ID=5的授权设备
- **THEN** 系统返回该企业所有授权设备列表,每个设备包含绑定卡数量
---
### Requirement: 企业端设备列表
系统 SHALL 提供企业用户查询自己授权设备列表的 H5 接口。
**接口设计**
- 路径:`GET /api/h5/enterprise/devices`
- 查询参数:`page`, `page_size`, `device_no`
- 响应:
```json
{
"list": [
{
"device_id": 1,
"device_no": "D001",
"device_name": "GPS追踪器-001",
"device_model": "GT-100",
"card_count": 3,
"authorized_at": "2025-01-29T10:00:00Z"
}
],
"total": 10
}
```
**数据权限**
- 企业用户只能看到授权给自己企业的设备
- 通过 GORM Callback 自动过滤
#### Scenario: 企业用户查看设备列表
- **WHEN** 企业用户查询设备列表
- **THEN** 系统返回授权给该企业的所有设备,包含设备信息和卡数量
#### Scenario: 企业用户无法看到未授权设备
- **WHEN** 企业用户查询设备列表
- **THEN** 系统不返回未授权给该企业的设备
---
### Requirement: 企业端设备详情
系统 SHALL 提供企业用户查询设备详情的 H5 接口,包含设备绑定的卡列表。
**接口设计**
- 路径:`GET /api/h5/enterprise/devices/:device_id`
- 响应:
```json
{
"device": {
"device_id": 1,
"device_no": "D001",
"device_name": "GPS追踪器-001",
"device_model": "GT-100",
"device_type": "GPS",
"authorized_at": "2025-01-29T10:00:00Z"
},
"cards": [
{
"card_id": 101,
"iccid": "8986001234567890",
"msisdn": "1380000001",
"carrier_name": "中国联通",
"network_status": 1,
"network_status_name": "开机"
}
]
}
```
**可见信息**
- 设备基本信息:设备号、名称、型号、类型
- 卡信息ICCID、MSISDN、运营商、网络状态
**不可见信息**
- 成本价、分销价、供应商等商业敏感信息
#### Scenario: 企业用户查看设备详情
- **WHEN** 企业用户查看授权设备ID=1的详情
- **THEN** 系统返回设备信息和该设备绑定的所有卡信息
#### Scenario: 企业用户无法查看未授权设备
- **WHEN** 企业用户尝试查看未授权的设备详情
- **THEN** 系统返回 404 错误
---
### Requirement: 企业端设备卡停机复机
系统 SHALL 提供企业用户对设备下的卡进行停机/复机操作的 H5 接口。
**接口设计**
- 停机:`POST /api/h5/enterprise/devices/:device_id/cards/:card_id/suspend`
- 复机:`POST /api/h5/enterprise/devices/:device_id/cards/:card_id/resume`
**权限校验**
- 设备 MUST 授权给当前企业
- 卡 MUST 属于该设备(通过 device_sim_binding 验证)
- 卡 MUST 通过设备授权device_auth_id 不为空且有效)
#### Scenario: 企业用户停机设备下的卡
- **WHEN** 企业用户对授权设备下的卡执行停机操作
- **THEN** 系统更新卡的 network_status 为 0停机
#### Scenario: 企业用户复机设备下的卡
- **WHEN** 企业用户对授权设备下的卡执行复机操作
- **THEN** 系统更新卡的 network_status 为 1开机
#### Scenario: 无法操作未授权设备的卡
- **WHEN** 企业用户尝试操作未授权设备下的卡
- **THEN** 系统返回 403 错误

View File

@@ -0,0 +1,163 @@
## 1. 数据库迁移
- [x] 1.1 创建 `tb_enterprise_device_authorization` 表迁移文件
- 包含所有字段enterprise_id, device_id, authorized_by, authorized_at, authorizer_type, revoked_by, revoked_at, remark
- 添加部分唯一索引:`UNIQUE (device_id) WHERE revoked_at IS NULL AND deleted_at IS NULL`
- 添加常规索引idx_eda_enterprise, idx_eda_device, idx_eda_authorized_by
- [x] 1.2 创建 `tb_enterprise_card_authorization` 表修改迁移文件
- 新增字段device_auth_idBIGINT可空
- 添加索引idx_eca_device_auth
- [x] 1.3 执行迁移并验证表结构正确
## 2. Model 层
- [x] 2.1 创建 `EnterpriseDeviceAuthorization` 模型
- 文件路径:`internal/model/enterprise_device_authorization.go`
- 包含所有字段和 TableName 方法
- 遵循项目 GORM 模型规范
- [x] 2.2 修改 `EnterpriseCardAuthorization` 模型
- 新增 `DeviceAuthID` 字段(`*uint`
- 添加 GORM 标签:`gorm:"column:device_auth_id;comment:关联的设备授权ID"`
## 3. Store 层
- [x] 3.1 创建 `EnterpriseDeviceAuthorizationStore`
- 文件路径:`internal/store/postgres/enterprise_device_authorization_store.go`
- 实现方法Create, BatchCreate, GetByID, GetByDeviceID, GetByEnterpriseID
- 实现方法ListByEnterprise分页、筛选, RevokeByIDs, GetActiveAuthsByDeviceIDs
- 实现方法ListDeviceIDsByEnterprise获取企业授权的设备ID列表
- [x] 3.2 修改 `EnterpriseCardAuthorizationStore`
- 新增方法RevokeByDeviceAuthID根据设备授权ID批量回收卡授权
## 4. Service 层 - 设备授权服务
- [x] 4.1 创建 `enterprise_device` 服务
- 文件路径:`internal/service/enterprise_device/service.go`
- 依赖注入db, enterpriseStore, deviceStore, deviceSimBindingStore, enterpriseDeviceAuthStore, enterpriseCardAuthStore, logger
- [x] 4.2 实现 `AllocateDevices` 方法(授权设备给企业)
- 验证企业存在和权限
- 验证每个设备的权限和状态
- 检查唯一性约束(设备未授权给其他企业)
- 在事务内创建设备授权和卡授权记录
- 返回授权结果
- [x] 4.3 实现 `RecallDevices` 方法(回收设备授权)
- 验证授权记录存在
- 在事务内回收设备授权和关联的卡授权
- 返回回收结果
- [x] 4.4 实现 `ListDevices` 方法(后台管理:企业设备列表)
- 分页查询授权给企业的设备
- 包含设备信息和绑定卡数量
- [x] 4.5 实现 `ListDevicesForEnterprise` 方法H5企业设备列表
- 企业用户查询自己的授权设备
- 数据权限自动过滤
- [x] 4.6 实现 `GetDeviceDetail` 方法H5设备详情
- 查询设备信息和绑定的卡列表
- 验证企业权限
- [x] 4.7 实现 `SuspendCard``ResumeCard` 方法H5停机/复机)
- 验证设备和卡的授权关系
- 更新卡的网络状态
## 5. Service 层 - 修改单卡授权服务
- [x] 5.1 修改 `enterprise_card/service.go``AllocateCardsPreview` 方法
- 移除 DeviceBundle 处理逻辑
- 绑定设备的卡直接加入 FailedItems原因为"该卡已绑定设备,请使用设备授权功能"
- 移除 DeviceBundles 响应字段
- [x] 5.2 修改 `enterprise_card/service.go``AllocateCards` 方法
- 移除 DeviceBundle 确认流程confirm_device_bundles 参数)
- 移除 AllocatedDevices 响应字段
- 绑定设备的卡直接拒绝
- [x] 5.3 清理相关 DTO
- 移除或标记废弃DeviceBundle, DeviceBundleCard, ConfirmDeviceBundles, AllocatedDevice 相关字段
- 更新 AllocateCardsReq 和 AllocateCardsResp
## 6. Handler 层 - 后台管理
- [x] 6.1 创建 `admin/enterprise_device.go` Handler
- AllocateDevices授权设备给企业
- RecallDevices回收设备授权
- ListDevices企业设备列表
- [x] 6.2 注册后台路由
- 文件路径:`internal/routes/enterprise_device.go`
- POST /api/admin/enterprises/:id/allocate-devices
- POST /api/admin/enterprises/:id/recall-devices
- GET /api/admin/enterprises/:id/devices
- [x] 6.3 更新 Bootstrap 注册
-`internal/bootstrap/` 中注册新的 Store、Service、Handler
## 7. Handler 层 - 企业端 H5
- [x] 7.1 创建 `h5/enterprise_device.go` Handler
- ListDevices设备列表
- GetDeviceDetail设备详情
- SuspendCard停机卡
- ResumeCard复机卡
- [x] 7.2 注册 H5 路由
- 文件路径:`internal/routes/h5/enterprise_device.go`
- GET /api/h5/enterprise/devices
- GET /api/h5/enterprise/devices/:device_id
- POST /api/h5/enterprise/devices/:device_id/cards/:card_id/suspend
- POST /api/h5/enterprise/devices/:device_id/cards/:card_id/resume
## 8. DTO 层
- [x] 8.1 创建设备授权相关 DTO
- 文件路径:`internal/model/dto/enterprise_device_authorization_dto.go`
- AllocateDevicesReq / AllocateDevicesResp
- RecallDevicesReq / RecallDevicesResp
- EnterpriseDeviceListReq / EnterpriseDeviceListResp
- EnterpriseDeviceDetailResp
- DeviceCardSuspendReq / DeviceCardResumeReq
## 9. 错误码
- [x] 9.1 新增设备授权相关错误码
- CodeDeviceAlreadyAuthorized设备已授权给该企业
- CodeDeviceNotAuthorized设备未授权给该企业
- CodeDeviceAuthorizedToOther设备已授权给其他企业
- CodeCannotAuthorizeOthersDevice不能授权非自己的设备
## 10. 测试
- [x] 10.1 Store 层单元测试
- EnterpriseDeviceAuthorizationStore 各方法测试
- EnterpriseCardAuthorizationStore 新方法测试
- [x] 10.2 Service 层单元测试
- enterprise_device 服务各方法测试
- 权限验证测试
- 授权联动测试
- 测试覆盖率88.9%
- [x] 10.3 修改 enterprise_card 服务测试
- 验证绑定设备的卡被正确拒绝
- 移除 DeviceBundle 相关测试
- [x] 10.4 集成测试
- 完整授权/回收流程测试
- 企业端 API 测试
- 权限隔离测试
## 11. 文档更新
- [x] 11.1 更新 OpenAPI 文档生成器
-`cmd/api/docs.go``cmd/gendocs/main.go` 中注册新 Handler
- 重新生成 OpenAPI 文档
- [x] 11.2 创建功能文档
-`docs/enterprise-device-authorization/` 目录下创建设备授权功能说明文档