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,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` 字段
|
||||
|
||||
**理由**:
|
||||
- 明确区分卡授权来源:单卡授权(NULL)vs 设备授权(有值)
|
||||
- 回收设备授权时可以精确定位需要回收的卡授权记录
|
||||
- 支持查询"某设备授权下的所有卡"
|
||||
|
||||
**表结构变更**:
|
||||
```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 个设备
|
||||
Reference in New Issue
Block a user