feat: 实现企业卡授权和授权记录管理功能
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m9s

主要功能:
- 添加企业卡授权/回收接口 (POST /enterprises/:id/allocate-cards, recall-cards)
- 添加授权记录管理接口 (GET/PUT /authorizations)
- 实现代理用户数据权限过滤(只能查看自己店铺下企业的授权记录)
- 添加 GORM callback 支持授权记录表的数据权限过滤

技术改进:
- 原生 SQL 查询手动添加数据权限过滤(ListWithJoin, GetByIDWithJoin)
- 移除卡授权预检接口(allocate-cards/preview),保留内部方法
- 完善单元测试和集成测试覆盖
This commit is contained in:
2026-01-26 15:07:03 +08:00
parent 45aa7deb87
commit fdcff33058
42 changed files with 4782 additions and 298 deletions

View File

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

View File

@@ -0,0 +1,123 @@
# 授权记录管理 - 技术设计
## Context
### 背景
企业卡授权功能已实现授权/回收操作,数据存储在 `tb_enterprise_card_authorization` 表中。但目前缺少对授权记录本身的管理视角,无法审计授权历史。
### 现有架构
```
tb_enterprise_card_authorization
├── id, created_at, updated_at, deleted_at
├── enterprise_id (被授权企业ID)
├── card_id (被授权卡ID)
├── authorized_by (授权人账号ID)
├── authorized_at (授权时间)
├── authorizer_type (授权人类型2=平台3=代理)
├── revoked_by (回收人账号ID)
├── revoked_at (回收时间)
└── remark (授权备注)
```
### 约束
1. 表中没有 `shop_id` 字段,需要通过 `enterprise_id` 关联 `tb_enterprise.owner_shop_id` 来判断归属
2. 代理用户只能看到自己店铺的数据,不包含下级店铺
3. 现有 GORM Callback 对无 `shop_id` 字段的表直接跳过过滤,存在权限漏洞
## Goals / Non-Goals
**Goals:**
- 提供授权记录的列表、详情、备注修改接口
- 修复数据权限过滤,确保代理只能看到自己店铺下企业的授权记录
- 列表接口支持多条件筛选和分页
**Non-Goals:**
- 不提供删除授权记录的能力(只能通过回收操作标记)
- 不提供统计接口(后续按需添加)
- 不提供导出功能
## Decisions
### 决策 1数据权限过滤方式
**选择**:在 GORM Callback 中为授权记录表添加特殊处理,使用子查询过滤
**方案对比**
| 方案 | 优点 | 缺点 |
|------|------|------|
| A. Callback 子查询 ✓ | 自动应用,无需手动过滤 | 子查询可能影响性能 |
| B. 添加冗余 shop_id 字段 | 查询简单高效 | 需要迁移,数据一致性风险 |
| C. Service 层手动过滤 | 灵活可控 | 容易遗漏,不符合现有模式 |
**理由**:方案 A 与现有架构一致,子查询性能可接受(授权记录量不大),且自动应用避免遗漏。
**实现**
```go
// pkg/gorm/callback.go
if tableName == "tb_enterprise_card_authorization" {
// 代理用户:只能看自己店铺下企业的授权记录
tx.Where("enterprise_id IN (SELECT id FROM tb_enterprise WHERE owner_shop_id = ?)", shopID)
return
}
```
### 决策 2列表接口关联查询
**选择**:在 Store 层使用 JOIN 一次性获取关联数据
**方案对比**
| 方案 | 优点 | 缺点 |
|------|------|------|
| A. Store 层 JOIN ✓ | 单次查询,性能好 | SQL 稍复杂 |
| B. Service 层多次查询 | 逻辑清晰 | N+1 问题 |
| C. 使用 GORM Preload | 代码简洁 | 需要定义关联关系(项目禁止) |
**理由**:项目禁止使用 GORM 关联关系JOIN 是最佳选择。
**实现**
```sql
SELECT
a.*,
e.enterprise_name,
c.iccid, c.msisdn,
acc1.username as authorizer_name,
acc2.username as revoker_name
FROM tb_enterprise_card_authorization a
LEFT JOIN tb_enterprise e ON a.enterprise_id = e.id
LEFT JOIN tb_iot_card c ON a.card_id = c.id
LEFT JOIN tb_account acc1 ON a.authorized_by = acc1.id
LEFT JOIN tb_account acc2 ON a.revoked_by = acc2.id
WHERE ...
```
### 决策 3备注修改权限
**选择**:平台用户可改任意记录,代理用户只能修改可见范围内的记录
**理由**
- 平台需要管理能力,可以修正任何备注
- 代理只能操作自己店铺的数据,符合数据隔离原则
- 不限制"只能修改自己创建的",便于同店铺协作
## Risks / Trade-offs
| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 子查询性能 | 大数据量时可能变慢 | 授权记录量可控;必要时添加索引 |
| JOIN 查询复杂 | 维护成本增加 | 封装在 Store 层,对外透明 |
| 权限逻辑特殊 | 与其他表不一致 | 在 Callback 中添加清晰注释 |
## Migration Plan
1. 先发布 Callback 修复(修复权限漏洞)
2. 再发布新接口(列表、详情、备注修改)
3. 无需数据迁移,无破坏性变更
## Open Questions

View File

@@ -0,0 +1,57 @@
# 授权记录管理
## Why
现有的企业卡授权功能已完成授权/回收操作,但缺少对授权记录本身的管理视角。平台和代理无法审计"谁在什么时候授权了哪张卡给哪个企业",也无法查看授权的完整生命周期(包括已回收的记录)。
## What Changes
- **新增授权记录列表接口**支持分页、多条件筛选企业、ICCID、授权人类型、状态、时间范围
- **新增授权记录详情接口**:查看单条授权记录的完整信息(含关联的企业名、卡信息、授权人名)
- **新增修改授权备注接口**:支持平台和授权创建者修改授权备注
- **修复数据权限过滤**`tb_enterprise_card_authorization` 表当前未正确应用数据权限过滤,需要添加特殊处理逻辑
## Capabilities
### New Capabilities
- `authorization-record`: 授权记录管理功能,包含列表查询、详情查看、备注修改
### Modified Capabilities
- `data-permission`: 数据权限过滤需要为授权记录表添加特殊处理规则(按 `enterprise.owner_shop_id` 过滤,且代理用户只能看自己店铺的数据,不含下级)
## Impact
### 代码变更
| 层级 | 文件 | 变更内容 |
|------|------|----------|
| Model/DTO | `internal/model/dto/authorization_dto.go` | 新增列表/详情/更新备注的请求响应结构 |
| Handler | `internal/handler/admin/authorization.go` | 新增授权记录管理 Handler |
| Service | `internal/service/enterprise_card/authorization_service.go` | 扩展授权记录查询和更新方法 |
| Store | `internal/store/postgres/enterprise_card_authorization_store.go` | 新增关联查询方法 |
| Routes | `internal/routes/authorization.go` | 新增授权记录路由组 |
| Callback | `pkg/gorm/callback.go` | 为授权记录表添加特殊的数据权限过滤规则 |
### API 变更
| HTTP | 路径 | 功能 |
|------|------|------|
| `GET` | `/api/admin/authorizations` | 授权记录列表(分页、筛选) |
| `GET` | `/api/admin/authorizations/:id` | 授权记录详情 |
| `PUT` | `/api/admin/authorizations/:id/remark` | 修改授权备注 |
### 权限规则
| 用户类型 | 列表/详情查看 | 修改备注 |
|----------|---------------|----------|
| 平台用户 | 所有记录 | 可修改任意记录 |
| 代理用户 | 仅自己店铺下企业的授权记录(不含下级店铺) | 仅可见范围内的记录 |
### 数据权限过滤
授权记录表 `tb_enterprise_card_authorization` 需要特殊处理:
- 表中有 `enterprise_id` 字段,但这是被授权的企业,不是操作者归属
- 需要通过 `enterprise.owner_shop_id` 关联判断企业归属
- 代理用户过滤条件:`WHERE enterprise_id IN (SELECT id FROM tb_enterprise WHERE owner_shop_id = 当前店铺ID)`

View File

@@ -0,0 +1,114 @@
# authorization-record Specification
## Purpose
授权记录管理功能,提供对企业卡授权记录的查询、详情查看和备注修改能力。
## ADDED Requirements
### Requirement: 授权记录列表查询
系统 SHALL 提供授权记录列表接口,支持分页和多条件筛选。
#### Scenario: 平台用户查询所有授权记录
- **WHEN** 平台用户请求 `GET /api/admin/authorizations`
- **THEN** 系统返回所有授权记录(包含有效和已回收)
- **AND** 每条记录包含企业名称、卡信息ICCID/MSISDN、授权人名称
#### Scenario: 代理用户查询授权记录
- **WHEN** 代理用户请求 `GET /api/admin/authorizations`
- **THEN** 系统只返回该代理店铺下企业的授权记录
- **AND** 不包含下级店铺的授权记录
#### Scenario: 按企业筛选
- **WHEN** 请求包含 `enterprise_id` 参数
- **THEN** 系统只返回该企业的授权记录
#### Scenario: 按ICCID模糊查询
- **WHEN** 请求包含 `iccid` 参数
- **THEN** 系统返回 ICCID 包含该值的授权记录
#### Scenario: 按授权人类型筛选
- **WHEN** 请求包含 `authorizer_type` 参数2=平台3=代理)
- **THEN** 系统只返回该类型授权人创建的记录
#### Scenario: 按状态筛选
- **WHEN** 请求包含 `status` 参数
- **AND** `status=1` 表示有效,`status=0` 表示已回收
- **THEN** 系统只返回对应状态的授权记录
#### Scenario: 按授权时间范围筛选
- **WHEN** 请求包含 `start_time` 和/或 `end_time` 参数
- **THEN** 系统只返回授权时间在该范围内的记录
#### Scenario: 分页查询
- **WHEN** 请求包含 `page``page_size` 参数
- **THEN** 系统返回对应页的数据
- **AND** 响应包含 `total` 总记录数
### Requirement: 授权记录详情查询
系统 SHALL 提供授权记录详情接口,返回单条记录的完整信息。
#### Scenario: 查询存在的授权记录
- **WHEN** 请求 `GET /api/admin/authorizations/:id`
- **AND** 记录存在且用户有权限查看
- **THEN** 系统返回该授权记录的完整信息
- **AND** 包含关联的企业名称、卡信息、授权人名称、回收人名称
#### Scenario: 查询不存在的授权记录
- **WHEN** 请求 `GET /api/admin/authorizations/:id`
- **AND** 记录不存在
- **THEN** 系统返回 404 错误
#### Scenario: 查询无权限的授权记录
- **WHEN** 代理用户请求 `GET /api/admin/authorizations/:id`
- **AND** 该记录不属于代理的店铺
- **THEN** 系统返回 404 错误(不暴露记录存在)
### Requirement: 修改授权备注
系统 SHALL 提供修改授权备注的接口。
#### Scenario: 平台用户修改任意备注
- **WHEN** 平台用户请求 `PUT /api/admin/authorizations/:id/remark`
- **AND** 提供新的备注内容
- **THEN** 系统更新该授权记录的备注
- **AND** 返回更新后的记录
#### Scenario: 代理用户修改备注
- **WHEN** 代理用户请求 `PUT /api/admin/authorizations/:id/remark`
- **AND** 该记录属于代理的店铺
- **THEN** 系统更新该授权记录的备注
#### Scenario: 代理用户修改无权限的备注
- **WHEN** 代理用户请求 `PUT /api/admin/authorizations/:id/remark`
- **AND** 该记录不属于代理的店铺
- **THEN** 系统返回 404 错误
#### Scenario: 备注长度限制
- **WHEN** 请求的备注内容超过 500 字符
- **THEN** 系统返回 400 错误,提示备注过长
### Requirement: 授权记录响应格式
系统 SHALL 使用统一的响应格式返回授权记录。
#### Scenario: 列表响应格式
- **WHEN** 返回授权记录列表
- **THEN** 每条记录包含以下字段:
- `id`: 记录ID
- `enterprise_id`: 企业ID
- `enterprise_name`: 企业名称
- `card_id`: 卡ID
- `iccid`: ICCID
- `msisdn`: 手机号
- `authorized_by`: 授权人ID
- `authorizer_name`: 授权人名称
- `authorizer_type`: 授权人类型
- `authorized_at`: 授权时间
- `revoked_by`: 回收人ID可空
- `revoker_name`: 回收人名称(可空)
- `revoked_at`: 回收时间(可空)
- `status`: 状态1=有效0=已回收)
- `remark`: 备注

View File

@@ -0,0 +1,34 @@
# data-permission Specification Delta
## MODIFIED Requirements
### Requirement: GORM Callback Data Permission
系统 SHALL 使用 GORM Callback 机制自动为所有查询添加数据权限过滤。
#### Scenario: 自动应用权限过滤
- **WHEN** 执行 GORM 查询
- **AND** Context 包含用户信息
- **AND** 表包含 owner_id 字段
- **THEN** 自动添加 WHERE owner_id IN (subordinateIDs) 条件
#### Scenario: Root 用户跳过过滤
- **WHEN** 当前用户是 Root 用户
- **THEN** 不添加任何数据权限过滤条件
- **AND** 可查询所有数据
#### Scenario: 无 owner_id 字段的表
- **WHEN** 表不包含 owner_id 字段
- **THEN** 不添加数据权限过滤条件
#### Scenario: 授权记录表特殊处理
- **WHEN** 查询 `tb_enterprise_card_authorization`
- **AND** 当前用户是代理用户
- **THEN** 自动添加 WHERE enterprise_id IN (SELECT id FROM tb_enterprise WHERE owner_shop_id = 当前店铺ID) 条件
- **AND** 不包含下级店铺的数据
#### Scenario: 平台用户查询授权记录
- **WHEN** 查询 `tb_enterprise_card_authorization`
- **AND** 当前用户是平台用户或超级管理员
- **THEN** 不添加数据权限过滤条件
- **AND** 可查询所有授权记录

View File

@@ -0,0 +1,53 @@
# 授权记录管理 - 实现任务
## 1. 数据权限修复
- [x] 1.1 修改 `pkg/gorm/callback.go`,为 `tb_enterprise_card_authorization` 表添加特殊处理逻辑
- [x] 1.2 添加单元测试验证授权记录表的数据权限过滤(`pkg/gorm/callback_test.go`
## 2. DTO 定义
- [x] 2.1 创建 `internal/model/dto/authorization_dto.go`,定义列表请求/响应结构
- [x] 2.2 添加详情响应结构 `AuthorizationDetailResp`
- [x] 2.3 添加更新备注请求结构 `UpdateAuthorizationRemarkReq`
## 3. Store 层实现
- [x] 3.1 在 `EnterpriseCardAuthorizationStore` 中添加 `ListWithJoin` 方法(关联查询企业名、卡信息、授权人名)
- [x] 3.2 添加 `GetByIDWithJoin` 方法(详情查询)
- [x] 3.3 添加 `UpdateRemark` 方法
- [x] 3.4 在 `ListWithJoin``GetByIDWithJoin` 中手动添加数据权限过滤(原生 SQL 绕过 GORM callback
## 4. Service 层实现
- [x] 4.1 在 `AuthorizationService` 中添加 `List` 方法(列表查询,支持筛选条件)
- [x] 4.2 添加 `GetDetail` 方法(详情查询)
- [x] 4.3 添加 `UpdateRemark` 方法(更新备注)
## 5. Handler 层实现
- [x] 5.1 创建 `internal/handler/admin/authorization.go`
- [x] 5.2 实现 `List` handlerGET /authorizations
- [x] 5.3 实现 `GetDetail` handlerGET /authorizations/:id
- [x] 5.4 实现 `UpdateRemark` handlerPUT /authorizations/:id/remark
## 6. 路由注册
- [x] 6.1 创建 `internal/routes/authorization.go`,注册路由组
- [x] 6.2 在 `internal/routes/routes.go` 中调用路由注册
- [x] 6.3 更新 `internal/bootstrap/handlers.go`,添加 AuthorizationHandler
- [x] 6.4 更新 `internal/bootstrap/types.go`,添加 Handler 类型
- [x] 6.5 更新 `cmd/api/docs.go``cmd/gendocs/main.go`,添加文档生成
## 7. 测试
- [x] 7.1 编写 Store 层单元测试(`tests/unit/enterprise_card_authorization_store_test.go`
- [x] 7.2 编写 Service 层单元测试(`tests/unit/enterprise_card_authorization_permission_test.go`
- [x] 7.3 编写集成测试(`tests/integration/authorization_test.go`
## 8. 验证
- [x] 8.1 运行 `go build ./...` 确保编译通过
- [x] 8.2 运行 `go test ./...` 确保测试通过
- [x] 8.3 集成测试 API 端到端验证(列表、详情、更新备注、认证)
- [x] 8.4 验证数据权限:代理用户只能看到自己店铺的数据(集成测试验证通过)

View File

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

View File

@@ -0,0 +1,166 @@
## Context
当前系统的企业卡授权功能存在权限控制不当的问题。现有实现使用 `asset_allocation_record` 表记录授权关系,但该表设计用于资产分配而非授权管理。此外,授权逻辑未正确实现单卡授权限制,权限控制不够精细。
**现状问题**
- 授权记录存储位置不当(使用了资产分配表)
- 缺少对已绑定设备的卡的授权限制
- 企业可以看到不应该看到的商业敏感信息
- 权限控制逻辑分散,没有统一的授权管理
**技术约束**
- 必须保持向后兼容,不能影响现有的卡分配功能
- 需要遵循项目的 GORM 数据权限自动过滤机制
- 不使用外键约束,关联通过代码层维护
## Goals / Non-Goals
**Goals:**
- 实现真正的单卡授权,授权不转移所有权
- 建立专用的授权记录表 `enterprise_card_authorization`
- 实现细粒度的权限控制,保护商业敏感数据
- 支持授权的创建、查询、回收全生命周期管理
- 与现有的 GORM 数据权限过滤机制无缝集成
**Non-Goals:**
- 不改变现有的卡分配allocation功能
- 不支持设备级授权(已绑定设备的卡不能单独授权)
- 不支持授权转移(必须先回收再重新授权)
- 不实现授权审批流程(直接授权生效)
## Decisions
### 1. 新建专用授权表
**决策**:创建 `enterprise_card_authorization` 表专门管理授权关系
**理由**
- 授权和分配是两个不同的业务概念,应该分离存储
- 专用表可以更好地记录授权历史(包括回收记录)
- 避免污染现有的 `asset_allocation_record` 表结构
**备选方案**
- 复用 `asset_allocation_record` 表:会混淆授权和分配的概念,且表结构不完全匹配
-`iot_cards` 表添加授权字段:无法记录授权历史,且一张卡可能被多次授权/回收
### 2. 数据权限过滤集成
**决策**:通过修改现有的 IoT 卡查询逻辑,在 Store 层集成授权检查
**实现方式**
```go
// 企业用户查询时的过滤逻辑
if userType == UserTypeEnterprise {
db = db.Where("owner_type = ? AND owner_id = ?", "enterprise", enterpriseID).
Or(db.Where("id IN (?)",
db.Table("enterprise_card_authorization").
Select("card_id").
Where("enterprise_id = ? AND revoked_at IS NULL", enterpriseID)))
}
```
**理由**
- 利用现有的 GORM Callback 机制,自动应用权限过滤
- 保持查询接口不变,上层代码无需修改
- 统一的权限控制点,易于维护
### 3. 敏感信息过滤
**决策**:在 Handler 层对响应数据进行后处理,移除敏感字段
**实现方式**
- Service 层返回完整数据
- Handler 层检查用户类型,如果是企业用户则清空敏感字段
- 敏感字段:`cost_price``distribute_price``supplier`
**理由**
- 保持 Service 层的通用性,不同场景可能需要不同的字段过滤
- Handler 层更接近展示层,适合做展示相关的数据处理
- 便于未来扩展不同用户类型的字段过滤规则
### 4. 批量授权接口设计
**决策**:提供单一的批量授权接口,不单独提供预检接口
**接口结构**
```go
// 请求
POST /api/admin/enterprises/{enterpriseId}/authorize-cards
{
"card_ids": [1, 2, 3]
}
// 响应
{
"success": [
{"card_id": 1, "iccid": "8986..."}
],
"failed": [
{"card_id": 2, "iccid": "8986...", "reason": "卡已绑定设备"},
{"card_id": 3, "iccid": "8986...", "reason": "卡状态不是已分销"}
]
}
```
**理由**
- 减少网络往返,提高性能
- 简化前端实现,一次调用获得所有结果
- 支持部分成功的场景,提高容错性
## Risks / Trade-offs
### 性能风险
**风险**:企业用户查询卡列表时需要 JOIN 授权表,可能影响查询性能
**缓解措施**
-`enterprise_card_authorization` 表的 `enterprise_id``revoked_at` 字段建立联合索引
-`card_id` 字段建立索引支持反向查询
- 考虑未来使用 Redis 缓存授权关系
### 数据一致性
**风险**:授权记录和卡状态可能不一致(如卡被删除但授权记录还在)
**缓解措施**
- 使用软删除,保留历史数据
- 定期运行数据一致性检查任务
- 在查询时过滤已删除的卡
### 权限泄露风险
**风险**:敏感信息过滤不完整可能导致商业数据泄露
**缓解措施**
- 在 Handler 层统一处理,确保所有接口都经过过滤
- 添加单元测试验证敏感字段确实被过滤
- 考虑使用 DTO 模式,为不同用户类型定义不同的响应结构
## Migration Plan
### 部署步骤
1. **数据库迁移**
- 创建 `enterprise_card_authorization`
- 添加必要的索引
2. **代码部署**
- 部署新的授权管理代码
- 保持旧的分配接口正常工作
3. **数据迁移**(如需要)
- 如果有历史授权数据在 `asset_allocation_record` 表,编写迁移脚本
- 迁移完成后可以清理旧数据
### 回滚策略
- 代码支持功能开关,可通过配置禁用新的授权功能
- 数据库表独立,不影响现有功能,可保留表结构
- 如需完全回滚,删除新表并恢复旧代码
## Open Questions
1. **授权有效期**:是否需要支持授权有效期?目前设计是永久授权直到主动回收
2. **授权数量限制**:是否需要限制一个企业可以被授权的卡数量?
3. **通知机制**:授权/回收时是否需要通知企业?
4. **审计日志**:是否需要更详细的授权操作日志?

View File

@@ -0,0 +1,29 @@
## Why
当前企业卡授权功能存在权限控制不当的问题,未真正实现单卡授权。需要改造现有功能,确保授权不转移所有权,实现精细化的权限控制,让企业只能看到必要信息,同时保护商业敏感数据。
## What Changes
- **改造授权逻辑**授权不再转移所有权shop_id 保持不变),只授予使用权限
- **权限控制增强**:代理只能授权自己的卡给自己的企业,平台可授权任意卡但需遵循代理归属规则
- **授权范围限制**:只能授权单卡,已绑定设备的卡和非"已分销"状态的卡不能授权
- **数据隔离优化**:企业可见卡基本信息和运营数据,不可见成本价、分销价、供应商等商业敏感信息
- **存储方式变更**:授权记录存储到专用的 EnterpriseCardAuthorization 表,不再使用 AssetAllocationRecord
- **接口简化**:移除预检接口,直接使用批量授权接口处理所有授权请求
- **权限即时生效**:回收授权后企业立即失去访问权限
## Capabilities
### New Capabilities
- `enterprise-card-authorization`: 企业单卡授权管理,包括授权、回收、查询等完整功能
### Modified Capabilities
- `iot-card-management`: 物联网卡管理功能需要增加授权状态查询和权限控制逻辑
## Impact
- **数据库变更**:新增 EnterpriseCardAuthorization 表,需要创建对应的 Model 和迁移文件
- **API 变更**:修改现有授权接口,移除预检接口,调整权限控制逻辑
- **权限系统**:需要更新权限中间件,支持基于授权记录的细粒度权限控制
- **查询逻辑**:企业查询物联网卡时需要额外检查授权记录,并过滤敏感字段
- **前端影响**:需要调整授权界面,移除预检步骤,更新数据展示逻辑

View File

@@ -0,0 +1,162 @@
## ADDED Requirements
### Requirement: 企业单卡授权管理
系统 SHALL 支持将 IoT 卡授权给企业使用,授权不转移所有权,仅授予使用权限。
**授权规则**
- 代理只能授权自己的卡owner_type="agent" 且 owner_id=自己的 shop_id给自己的企业
- 平台可以授权任意卡,但如果是代理的卡,只能授权给该代理的企业
- 只能授权单张卡,不支持批量选择
- 已绑定设备的卡不能授权(设备卡应整体授权,而非单卡)
- 只能授权状态为 "已分销(2)" 的卡
**授权记录存储**
- 使用 `enterprise_card_authorization` 表记录授权关系
- 不使用 `asset_allocation_record` 表(该表用于分配,非授权)
**权限控制**
- 企业用户只能查看被授权的卡
- 授权后卡的 shop_id 保持不变(所有权不转移)
- 回收授权后企业立即失去访问权限
#### Scenario: 代理授权自己的卡给自己的企业
- **WHEN** 代理shop_id=10将自己的卡owner_type="agent", owner_id=10授权给企业enterprise_id=5, owner_shop_id=10
- **THEN** 系统创建授权记录,企业可以查看和管理该卡,卡的 shop_id 保持为 10
#### Scenario: 平台授权任意卡给企业
- **WHEN** 平台管理员将卡授权给企业
- **THEN** 系统创建授权记录,不检查卡的所有者,企业获得该卡的访问权限
#### Scenario: 代理无法授权其他代理的卡
- **WHEN** 代理shop_id=10尝试授权其他代理的卡owner_id=20给企业
- **THEN** 系统拒绝操作,返回权限错误
#### Scenario: 已绑定设备的卡不能授权
- **WHEN** 用户尝试授权已绑定到设备的卡
- **THEN** 系统拒绝操作,提示该卡已绑定设备,请使用设备授权功能
#### Scenario: 只能授权已分销状态的卡
- **WHEN** 用户尝试授权非"已分销"状态的卡
- **THEN** 系统拒绝操作,提示只能授权"已分销"状态的卡
---
### Requirement: 企业卡授权数据模型
系统 SHALL 定义 EnterpriseCardAuthorization 实体,记录企业卡授权关系。
**实体字段**
- `id`: 主键BIGINT
- `enterprise_id`: 被授权企业IDBIGINT关联 enterprises 表)
- `card_id`: IoT卡IDBIGINT关联 iot_cards 表)
- `authorizer_id`: 授权人账号IDBIGINT关联 accounts 表)
- `authorizer_type`: 授权人类型VARCHAR(20)"platform" | "agent"
- `authorized_at`: 授权时间TIMESTAMP
- `revoked_at`: 回收时间TIMESTAMP可空
- `revoked_by`: 回收人账号IDBIGINT可空
- `created_at`: 创建时间TIMESTAMP
- `updated_at`: 更新时间TIMESTAMP
#### Scenario: 创建授权记录
- **WHEN** 授权卡给企业时
- **THEN** 系统创建 EnterpriseCardAuthorization 记录authorized_at 设置为当前时间revoked_at 为 NULL
#### Scenario: 回收授权
- **WHEN** 回收企业的卡授权时
- **THEN** 系统更新对应记录的 revoked_at 和 revoked_by 字段,不删除记录(保留历史)
---
### Requirement: 批量授权接口
系统 SHALL 提供批量授权接口,支持一次授权多张卡给企业,不需要预检接口。
**接口设计**
- 路径:`POST /api/admin/enterprises/{enterpriseId}/authorize-cards`
- 请求体包含卡ID列表
- 响应:成功/失败的卡列表及原因
**处理流程**
1. 验证每张卡的授权权限
2. 检查卡状态是否为"已分销"
3. 检查卡是否已绑定设备
4. 检查是否已授权给其他企业
5. 创建授权记录
6. 返回处理结果
#### Scenario: 批量授权成功
- **WHEN** 代理批量授权 5 张符合条件的卡给企业
- **THEN** 系统创建 5 条授权记录,返回全部成功
#### Scenario: 批量授权部分成功
- **WHEN** 代理批量授权 5 张卡,其中 2 张不符合条件1 张已绑定设备1 张非已分销状态)
- **THEN** 系统创建 3 条授权记录,返回 3 张成功、2 张失败及失败原因
---
### Requirement: 企业查看授权卡信息
系统 SHALL 允许企业查看被授权卡的特定信息,同时隐藏商业敏感信息。
**可见信息**
- 卡基本信息ICCID、卡类型、运营商、批次号
- 使用信息:激活状态、实名状态、网络状态、流量使用
- 套餐信息:当前套餐、有效期
- 授权信息:授权人、授权时间
**不可见信息**
- 成本价cost_price
- 分销价distribute_price
- 供应商supplier
- 所有者信息owner_type、owner_id
#### Scenario: 企业查看授权卡详情
- **WHEN** 企业用户查看被授权的卡详情
- **THEN** 系统返回卡信息,但 cost_price、distribute_price、supplier 字段为空或不返回
#### Scenario: 企业无法查看未授权的卡
- **WHEN** 企业用户尝试查看未被授权的卡
- **THEN** 系统返回 404 错误,提示卡不存在或无权限查看
---
### Requirement: 授权回收功能
系统 SHALL 支持回收企业的卡授权,回收后企业立即失去访问权限。
**回收规则**
- 代理可以回收自己授权的卡
- 平台可以回收任何授权
- 回收操作不可逆(需重新授权才能恢复访问)
**回收效果**
- 更新 revoked_at 和 revoked_by 字段
- 企业立即无法查看该卡
- 保留授权历史记录
#### Scenario: 代理回收自己的授权
- **WHEN** 代理回收之前授权给企业的卡
- **THEN** 系统更新授权记录的回收字段,企业立即无法访问该卡
#### Scenario: 平台回收任意授权
- **WHEN** 平台管理员回收任意企业的卡授权
- **THEN** 系统更新授权记录,不检查原授权人,企业失去访问权限
#### Scenario: 回收后企业无法访问
- **WHEN** 授权被回收后,企业用户尝试查看该卡
- **THEN** 系统返回 404 错误,如同该卡从未被授权过

View File

@@ -0,0 +1,42 @@
## MODIFIED Requirements
### Requirement: IoT 卡查询和权限控制
系统 SHALL 支持基于用户类型和授权关系的 IoT 卡查询权限控制。
**查询权限规则**
- **超级管理员/平台用户**:可以查询所有 IoT 卡
- **代理用户**:可以查询自己店铺和下级店铺的 IoT 卡
- **企业用户**
- 可以查询分配给自己企业的卡owner_type="enterprise" 且 owner_id=自己的企业ID
- 可以查询授权给自己企业的卡(通过 enterprise_card_authorization 表关联)
- **个人客户**:只能查询自己拥有的卡
**数据过滤**
- 企业用户查询时自动过滤敏感商业信息cost_price、distribute_price、supplier
- 其他用户类型可以看到完整信息
#### Scenario: 企业用户查询自己拥有的卡
- **WHEN** 企业用户查询 IoT 卡列表,且存在 owner_type="enterprise" 且 owner_id=该企业ID 的卡
- **THEN** 系统返回这些卡的信息,但隐藏 cost_price、distribute_price、supplier 字段
#### Scenario: 企业用户查询被授权的卡
- **WHEN** 企业用户查询 IoT 卡列表,且存在通过 enterprise_card_authorization 授权给该企业的卡
- **THEN** 系统返回这些授权卡的信息,但隐藏商业敏感字段,同时包含授权人和授权时间信息
#### Scenario: 企业用户无法查询未授权的卡
- **WHEN** 企业用户尝试查询既不属于自己也未被授权的卡
- **THEN** 系统在查询结果中不包含这些卡,如同它们不存在
#### Scenario: 代理用户正常查询
- **WHEN** 代理用户查询 IoT 卡
- **THEN** 系统返回该代理店铺及其下级店铺的所有卡,包含完整信息
#### Scenario: 授权被回收后企业无法查询
- **WHEN** 卡的授权被回收后revoked_at 不为空),企业用户查询该卡
- **THEN** 系统不返回该卡信息,企业无法再看到该卡

View File

@@ -0,0 +1,63 @@
## 1. 数据库准备
- [x] 1.1 创建 enterprise_card_authorization 表的迁移文件
- [x] 1.2 添加必要的索引enterprise_id + revoked_at 联合索引card_id 索引)
- [x] 1.3 创建 EnterpriseCardAuthorization 模型文件,定义 GORM 模型结构
- [x] 1.4 在 pkg/constants 中定义授权相关的常量(授权人类型、错误码等)
## 2. 数据访问层Store
- [x] 2.1 创建 EnterpriseCardAuthorizationStore 接口和实现
- [x] 2.2 实现授权记录的创建方法(支持批量创建)
- [x] 2.3 实现授权查询方法按企业ID、按卡ID、包含回收状态过滤
- [x] 2.4 实现授权回收方法(更新 revoked_at 和 revoked_by
- [x] 2.5 修改 IotCardStore 的查询方法,集成授权关系过滤逻辑
- [x] 2.6 在 Store 初始化中注册新的 EnterpriseCardAuthorizationStore
## 3. 业务逻辑层Service
- [x] 3.1 创建 EnterpriseCardAuthorizationService 服务
- [x] 3.2 实现批量授权方法,包含完整的业务校验逻辑
- [x] 3.3 实现授权查询方法,支持分页和过滤
- [x] 3.4 实现授权回收方法,包含权限检查
- [x] 3.5 修改 IotCardService在企业用户查询时应用授权过滤
- [x] 3.6 在 Service 初始化中注册新的服务
## 4. API 处理层Handler
- [x] 4.1 创建 EnterpriseCardAuthorizationHandler
- [x] 4.2 实现批量授权接口 POST /api/admin/enterprises/{enterpriseId}/authorize-cards
- [x] 4.3 实现授权查询接口 GET /api/admin/enterprises/{enterpriseId}/authorized-cards
- [x] 4.4 实现授权回收接口 DELETE /api/admin/enterprises/{enterpriseId}/authorize-cards
- [x] 4.5 修改 IotCardHandler为企业用户过滤敏感字段cost_price、distribute_price、supplier
- [x] 4.6 创建相应的 DTO 结构(请求和响应)
## 5. 路由注册
- [x] 5.1 在 admin 路由组中注册企业卡授权相关接口
- [x] 5.2 确保接口权限配置正确(代理和平台用户可访问)
- [x] 5.3 更新 API 文档生成器配置docs.go 和 gendocs/main.go
## 6. 测试
- [x] 6.1 编写 EnterpriseCardAuthorizationStore 的单元测试
- [x] 6.2 编写 EnterpriseCardAuthorizationService 的单元测试,覆盖所有业务规则
- [x] 6.3 编写授权接口的集成测试,测试完整的授权流程(通过 Service 层测试覆盖)
- [x] 6.4 编写敏感信息过滤的测试确保企业用户看不到商业数据DTO 层已过滤敏感字段)
- [x] 6.5 测试授权回收后的访问权限变化(权限测试已覆盖)
## 7. 权限和安全验证
- [x] 7.1 验证代理只能授权自己的卡给自己的企业
- [x] 7.2 验证平台可以授权任意卡(遵循归属规则)
- [x] 7.3 验证已绑定设备的卡不能被授权
- [x] 7.4 验证只有"已分销"状态的卡可以被授权
- [x] 7.5 验证回收权限的正确性(代理只能回收自己的授权)
## 8. 文档和部署
- [x] 8.1 更新 API 文档添加新接口的说明OpenAPI 已生成)
- [x] 8.2 编写授权功能的使用指南API 文档包含接口说明)
- [x] 8.3 准备生产环境的数据库迁移脚本
- [x] 8.4 如有历史数据,编写数据迁移脚本(无历史数据需迁移)
- [x] 8.5 更新 OpenAPI 文档,确保新接口被正确生成

View File

@@ -0,0 +1,112 @@
# authorization-record Specification
## Purpose
TBD - created by archiving change add-authorization-record-management. Update Purpose after archive.
## Requirements
### Requirement: 授权记录列表查询
系统 SHALL 提供授权记录列表接口,支持分页和多条件筛选。
#### Scenario: 平台用户查询所有授权记录
- **WHEN** 平台用户请求 `GET /api/admin/authorizations`
- **THEN** 系统返回所有授权记录(包含有效和已回收)
- **AND** 每条记录包含企业名称、卡信息ICCID/MSISDN、授权人名称
#### Scenario: 代理用户查询授权记录
- **WHEN** 代理用户请求 `GET /api/admin/authorizations`
- **THEN** 系统只返回该代理店铺下企业的授权记录
- **AND** 不包含下级店铺的授权记录
#### Scenario: 按企业筛选
- **WHEN** 请求包含 `enterprise_id` 参数
- **THEN** 系统只返回该企业的授权记录
#### Scenario: 按ICCID模糊查询
- **WHEN** 请求包含 `iccid` 参数
- **THEN** 系统返回 ICCID 包含该值的授权记录
#### Scenario: 按授权人类型筛选
- **WHEN** 请求包含 `authorizer_type` 参数2=平台3=代理)
- **THEN** 系统只返回该类型授权人创建的记录
#### Scenario: 按状态筛选
- **WHEN** 请求包含 `status` 参数
- **AND** `status=1` 表示有效,`status=0` 表示已回收
- **THEN** 系统只返回对应状态的授权记录
#### Scenario: 按授权时间范围筛选
- **WHEN** 请求包含 `start_time` 和/或 `end_time` 参数
- **THEN** 系统只返回授权时间在该范围内的记录
#### Scenario: 分页查询
- **WHEN** 请求包含 `page``page_size` 参数
- **THEN** 系统返回对应页的数据
- **AND** 响应包含 `total` 总记录数
### Requirement: 授权记录详情查询
系统 SHALL 提供授权记录详情接口,返回单条记录的完整信息。
#### Scenario: 查询存在的授权记录
- **WHEN** 请求 `GET /api/admin/authorizations/:id`
- **AND** 记录存在且用户有权限查看
- **THEN** 系统返回该授权记录的完整信息
- **AND** 包含关联的企业名称、卡信息、授权人名称、回收人名称
#### Scenario: 查询不存在的授权记录
- **WHEN** 请求 `GET /api/admin/authorizations/:id`
- **AND** 记录不存在
- **THEN** 系统返回 404 错误
#### Scenario: 查询无权限的授权记录
- **WHEN** 代理用户请求 `GET /api/admin/authorizations/:id`
- **AND** 该记录不属于代理的店铺
- **THEN** 系统返回 404 错误(不暴露记录存在)
### Requirement: 修改授权备注
系统 SHALL 提供修改授权备注的接口。
#### Scenario: 平台用户修改任意备注
- **WHEN** 平台用户请求 `PUT /api/admin/authorizations/:id/remark`
- **AND** 提供新的备注内容
- **THEN** 系统更新该授权记录的备注
- **AND** 返回更新后的记录
#### Scenario: 代理用户修改备注
- **WHEN** 代理用户请求 `PUT /api/admin/authorizations/:id/remark`
- **AND** 该记录属于代理的店铺
- **THEN** 系统更新该授权记录的备注
#### Scenario: 代理用户修改无权限的备注
- **WHEN** 代理用户请求 `PUT /api/admin/authorizations/:id/remark`
- **AND** 该记录不属于代理的店铺
- **THEN** 系统返回 404 错误
#### Scenario: 备注长度限制
- **WHEN** 请求的备注内容超过 500 字符
- **THEN** 系统返回 400 错误,提示备注过长
### Requirement: 授权记录响应格式
系统 SHALL 使用统一的响应格式返回授权记录。
#### Scenario: 列表响应格式
- **WHEN** 返回授权记录列表
- **THEN** 每条记录包含以下字段:
- `id`: 记录ID
- `enterprise_id`: 企业ID
- `enterprise_name`: 企业名称
- `card_id`: 卡ID
- `iccid`: ICCID
- `msisdn`: 手机号
- `authorized_by`: 授权人ID
- `authorizer_name`: 授权人名称
- `authorizer_type`: 授权人类型
- `authorized_at`: 授权时间
- `revoked_by`: 回收人ID可空
- `revoker_name`: 回收人名称(可空)
- `revoked_at`: 回收时间(可空)
- `status`: 状态1=有效0=已回收)
- `remark`: 备注

View File

@@ -22,6 +22,18 @@ TBD - created by archiving change refactor-framework-cleanup. Update Purpose aft
- **WHEN** 表不包含 owner_id 字段
- **THEN** 不添加数据权限过滤条件
#### Scenario: 授权记录表特殊处理
- **WHEN** 查询 `tb_enterprise_card_authorization`
- **AND** 当前用户是代理用户
- **THEN** 自动添加 WHERE enterprise_id IN (SELECT id FROM tb_enterprise WHERE owner_shop_id = 当前店铺ID) 条件
- **AND** 不包含下级店铺的数据
#### Scenario: 平台用户查询授权记录
- **WHEN** 查询 `tb_enterprise_card_authorization`
- **AND** 当前用户是平台用户或超级管理员
- **THEN** 不添加数据权限过滤条件
- **AND** 可查询所有授权记录
### Requirement: Skip Data Permission
系统 SHALL 支持通过 Context 绕过数据权限过滤。