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 验证数据权限:代理用户只能看到自己店铺的数据(集成测试验证通过)