feat: 实现企业设备授权功能并归档 OpenSpec 变更
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m39s
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:
@@ -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`: 被授权企业ID(BIGINT,关联 enterprises 表)
|
||||
- `card_id`: IoT卡ID(BIGINT,关联 iot_cards 表)
|
||||
- `authorizer_id`: 授权人账号ID(BIGINT,关联 accounts 表)
|
||||
- `authorizer_type`: 授权人类型(SMALLINT,2=平台用户 3=代理账号)
|
||||
- `authorized_at`: 授权时间(TIMESTAMP)
|
||||
- `revoked_at`: 回收时间(TIMESTAMP,可空)
|
||||
- `revoked_by`: 回收人账号ID(BIGINT,可空)
|
||||
- `remark`: 备注(VARCHAR(500))
|
||||
- **`device_auth_id`: 关联的设备授权ID(BIGINT,可空)**
|
||||
- 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 张失败及各自失败原因
|
||||
@@ -0,0 +1,319 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 设备授权企业数据模型
|
||||
|
||||
系统 SHALL 定义 EnterpriseDeviceAuthorization 实体,记录设备与企业的授权关系。
|
||||
|
||||
**实体字段**:
|
||||
- `id`: 主键(BIGSERIAL)
|
||||
- `enterprise_id`: 被授权企业ID(BIGINT,NOT NULL)
|
||||
- `device_id`: 被授权设备ID(BIGINT,NOT NULL)
|
||||
- `authorized_by`: 授权人账号ID(BIGINT,NOT NULL)
|
||||
- `authorized_at`: 授权时间(TIMESTAMP,NOT NULL)
|
||||
- `authorizer_type`: 授权人类型(SMALLINT,2=平台用户 3=代理账号)
|
||||
- `revoked_by`: 回收人账号ID(BIGINT,可空)
|
||||
- `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`: 关联的设备授权ID(BIGINT,可空)
|
||||
- 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 错误
|
||||
Reference in New Issue
Block a user