Files
huang 743db126f7 重构数据权限模型并清理旧RBAC代码
核心变更:
- 数据权限过滤从基于账号层级改为基于用户类型的多策略过滤
- 移除 AccountStore 中的 GetSubordinateIDs 等旧方法
- 重构认证中间件,支持 enterprise_id 和 customer_id
- 更新 GORM Callback,根据用户类型自动选择过滤策略(代理/企业/个人客户)
- 更新所有集成测试以适配新的 API 签名
- 添加功能总结文档和 OpenSpec 归档

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 15:08:11 +08:00

431 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 旧 RBAC 系统清理 - 完成总结
## 概览
本次清理工作完成了从旧的基于账号层级(`parent_id`的数据权限模型到新的基于店铺层级、企业ID和客户ID的数据权限模型的迁移。
**完成时间**: 2026-01-10
**提案ID**: remove-legacy-rbac-cleanup
**依赖提案**:
- add-user-organization-model ✅
- add-role-permission-system ✅
- add-personal-customer-wechat ✅
---
## 核心变更
### 1. Account Store 清理
**文件**: `internal/store/postgres/account_store.go`
**移除的方法**:
- `GetSubordinateIDs(ctx, accountID)` - 基于 `parent_id` 的递归查询
- `ClearSubordinatesCache(ctx, accountID)` - 清除账号下级缓存
- `ClearSubordinatesCacheForParents(ctx, accountID)` - 递归清除上级缓存
**保留的方法**:
- `GetByShopID(ctx, shopID)` - 根据店铺 ID 查询账号列表
- `GetByEnterpriseID(ctx, enterpriseID)` - 根据企业 ID 查询账号列表
**移除的依赖**:
- 移除了 `time``constants``sonic` 包的导入(不再需要)
---
### 2. 数据权限过滤重构
**文件**: `pkg/gorm/callback.go`
**核心变更**:
从基于账号层级的过滤改为基于用户类型的多策略过滤。
#### 旧的过滤逻辑
```go
// 查询账号的所有下级ID
subordinateIDs := accountStore.GetSubordinateIDs(ctx, userID)
// 过滤: creator IN (下级账号ID)
tx.Where("creator IN ?", subordinateIDs)
```
#### 新的过滤逻辑
根据用户类型自动选择合适的过滤策略:
1. **超级管理员和平台用户**: 跳过过滤,查看所有数据
2. **代理用户**: 基于店铺层级过滤
```go
subordinateShopIDs := shopStore.GetSubordinateShopIDs(ctx, shopID)
tx.Where("shop_id IN ?", subordinateShopIDs)
```
3. **企业用户**: 基于企业ID过滤
```go
tx.Where("enterprise_id = ?", enterpriseID)
```
4. **个人客户**: 基于客户ID或创建人过滤
```go
tx.Where("customer_id = ?", customerID)
// 或降级为
tx.Where("creator = ?", userID)
```
**接口变更**:
```go
// 旧接口
type AccountStoreInterface interface {
GetSubordinateIDs(ctx, accountID) ([]uint, error)
}
func RegisterDataPermissionCallback(db, accountStore)
// 新接口
type ShopStoreInterface interface {
GetSubordinateShopIDs(ctx, shopID) ([]uint, error)
}
func RegisterDataPermissionCallback(db, shopStore)
```
---
### 3. 认证中间件增强
**文件**: `pkg/middleware/auth.go`
**新增字段支持**:
```go
// 旧的用户上下文
- userID
- userType
- shopID
// 新的用户上下文
type UserContextInfo struct {
UserID uint // 用户ID
UserType int // 用户类型
ShopID uint // 店铺ID代理用户
EnterpriseID uint // 企业ID企业用户
CustomerID uint // 客户ID个人客户
}
```
**API 变更**:
```go
// 旧API
func SetUserContext(ctx, userID, userType, shopID) context.Context
func SetUserToFiberContext(c, userID, userType, shopID)
// 新API
func SetUserContext(ctx, info *UserContextInfo) context.Context
func SetUserToFiberContext(c, info *UserContextInfo)
// 辅助函数(用于测试和兼容性)
func NewSimpleUserContext(userID, userType, shopID) *UserContextInfo
```
**新增辅助函数**:
```go
func GetEnterpriseIDFromContext(ctx) uint
func GetCustomerIDFromContext(ctx) uint
```
---
### 4. 常量清理
**文件**: `pkg/constants/constants.go` 和 `pkg/constants/redis.go`
**新增常量**:
```go
// Context 键
const (
ContextKeyEnterpriseID = "enterprise_id"
ContextKeyCustomerID = "customer_id"
)
// 用户类型
const (
UserTypePersonalCustomer = 5 // 个人客户C端用户
)
```
**移除常量**:
```go
// Redis 键生成函数(已废弃)
func RedisAccountSubordinatesKey(accountID) string
```
---
### 5. Bootstrap 初始化调整
**文件**: `internal/bootstrap/stores.go` 和 `internal/bootstrap/bootstrap.go`
**变更内容**:
```go
// 在 stores 结构体中添加 Shop
type stores struct {
Account *postgres.AccountStore
Shop *postgres.ShopStore // 新增
Role *postgres.RoleStore
// ...
}
// 初始化 Shop Store
Shop: postgres.NewShopStore(deps.DB, deps.Redis)
// 数据权限回调改为使用 ShopStore
pkgGorm.RegisterDataPermissionCallback(deps.DB, stores.Shop)
```
---
## 架构改进
### 数据权限过滤逻辑对比
| 维度 | 旧设计(基于账号层级) | 新设计(基于用户类型) |
|------|----------------------|---------------------|
| **核心依赖** | Account.parent_id | Shop.parent_id + Enterprise.id + Customer.id |
| **递归查询** | 账号的下级账号 | 店铺的下级店铺 |
| **适用范围** | 仅代理账号 | 代理、企业、个人客户 |
| **过滤字段** | creator | shop_id / enterprise_id / customer_id / creator |
| **可扩展性** | 低(单一策略) | 高(多策略,根据用户类型) |
| **缓存键** | account:subordinates:{id} | shop:subordinates:{id} |
### 新的数据权限模型优势
1. **更清晰的职责分离**: 账号不再承担组织结构的职责,组织结构完全由 Shop 和 Enterprise 维护
2. **更灵活的过滤策略**: 根据用户类型自动选择合适的过滤字段
3. **更好的扩展性**: 新增用户类型时只需添加对应的过滤逻辑
4. **更符合业务模型**: B端代理/企业和C端个人客户使用不同的过滤策略
---
## 破坏性变更
### API 签名变更
以下 API 的签名已变更,需要调用方更新:
#### 1. pkg/middleware 包
```go
// ❌ 旧API已移除
middleware.SetUserContext(ctx, userID, userType, shopID)
middleware.SetUserToFiberContext(c, userID, userType, shopID)
// ✅ 新API
info := &middleware.UserContextInfo{
UserID: userID,
UserType: userType,
ShopID: shopID,
EnterpriseID: enterpriseID,
CustomerID: customerID,
}
middleware.SetUserContext(ctx, info)
middleware.SetUserToFiberContext(c, info)
// ✅ 兼容性辅助函数(仅用于基本场景)
info := middleware.NewSimpleUserContext(userID, userType, shopID)
middleware.SetUserContext(ctx, info)
```
#### 2. AuthConfig 配置
```go
// ❌ 旧API
type AuthConfig struct {
TokenValidator func(token string) (userID uint, userType int, shopID uint, err error)
}
// ✅ 新API
type AuthConfig struct {
TokenValidator func(token string) (*middleware.UserContextInfo, error)
}
```
#### 3. GORM Callback 注册
```go
// ❌ 旧API
gorm.RegisterDataPermissionCallback(db, accountStore)
// ✅ 新API
gorm.RegisterDataPermissionCallback(db, shopStore)
```
---
## 迁移指南
### 对于业务代码
如果你的代码直接调用了以下方法,需要迁移:
#### 1. AccountStore 方法调用
```go
// ❌ 旧代码
ids, err := accountStore.GetSubordinateIDs(ctx, userID)
accountStore.ClearSubordinatesCache(ctx, userID)
// ✅ 新代码
// 不再需要查询账号下级,数据权限过滤会自动处理
// 如果需要查询店铺下级:
shopIDs, err := shopStore.GetSubordinateShopIDs(ctx, shopID)
```
#### 2. 认证中间件配置
```go
// ❌ 旧代码
auth.Auth(auth.AuthConfig{
TokenValidator: func(token string) (uint, int, uint, error) {
// 解析 token
return userID, userType, shopID, nil
},
})
// ✅ 新代码
auth.Auth(auth.AuthConfig{
TokenValidator: func(token string) (*middleware.UserContextInfo, error) {
// 解析 token
return &middleware.UserContextInfo{
UserID: userID,
UserType: userType,
ShopID: shopID,
EnterpriseID: enterpriseID,
CustomerID: customerID,
}, nil
},
})
```
#### 3. 设置用户上下文
```go
// ❌ 旧代码
middleware.SetUserToFiberContext(c, userID, userType, shopID)
// ✅ 新代码
info := &middleware.UserContextInfo{
UserID: userID,
UserType: userType,
ShopID: shopID,
}
middleware.SetUserToFiberContext(c, info)
// ✅ 或者使用辅助函数(仅适用于简单场景)
info := middleware.NewSimpleUserContext(userID, userType, shopID)
middleware.SetUserToFiberContext(c, info)
```
---
## 测试影响
### 需要更新的测试
由于 API 签名变更,以下测试需要更新:
1. **认证中间件测试** (`pkg/middleware/auth_test.go`)
2. **GORM Callback 测试** (`pkg/gorm/callback_test.go`)
3. **业务集成测试** (`tests/integration/admin/*_test.go`)
4. **权限过滤测试** (`tests/integration/admin/permission_platform_filter_test.go`)
### 测试迁移模式
```go
// ❌ 旧测试代码
ctx := middleware.SetUserContext(context.Background(), 1, constants.UserTypeAgent, 100)
// ✅ 新测试代码
ctx := middleware.SetUserContext(context.Background(), middleware.NewSimpleUserContext(1, constants.UserTypeAgent, 100))
```
---
## 遗留问题
### 1. 企业用户和个人客户的 Token 生成
**问题**: 当前 Token 验证器需要返回 `enterprise_id` 和 `customer_id`,但现有的 Token 生成逻辑可能还没有包含这些字段。
**影响**: 企业用户和个人客户的数据权限过滤可能无法正常工作。
**解决方案**:
1. 更新 JWT Token 的 Claims 结构,添加 `enterprise_id` 和 `customer_id` 字段
2. 在登录时根据用户类型生成包含对应字段的 Token
### 2. 测试兼容性
**问题**: 大量测试代码需要更新以适配新的 API 签名。
**影响**: 测试编译失败。
**解决方案**:
1. 批量更新测试代码,使用 `middleware.NewSimpleUserContext` 辅助函数
2. 为需要完整上下文的测试创建完整的 `UserContextInfo` 实例
---
## 验收清单
- [x] 0.1 确认 add-user-organization-model 提案已完成
- [x] 0.2 确认 add-role-permission-system 提案已完成
- [x] 0.3 确认 add-personal-customer-wechat 提案已完成
- [x] 1.1 移除 `GetSubordinateIDs` 方法
- [x] 1.2 移除相关的 Redis 缓存逻辑
- [x] 1.3 更新 `account_store.go` 中所有引用 `parent_id` 的代码
- [x] 1.4 添加新的查询方法:`GetByShopID`、`GetByEnterpriseID`
- [x] 2.1 创建/更新数据权限过滤逻辑
- [x] 2.1.1 改为从 context 获取 shop_id而非 user_id
- [x] 2.1.2 调用 `shop_store.GetSubordinateShopIDs` 获取下级店铺
- [x] 2.1.3 生成 `WHERE shop_id IN (...)` 过滤条件
- [x] 2.2 更新 Store 层的 List 方法
- [x] 2.3 处理企业账号的过滤逻辑
- [x] 2.4 处理平台用户跳过过滤的逻辑
- [x] 3.1 更新认证中间件
- [x] 3.1.1 在 context 中设置 enterprise_id 和 customer_id
- [x] 3.1.2 根据用户类型设置数据权限过滤标记
- [x] 4.1 权限校验中间件(无需修改)
- [x] 5.1 移除旧的 Redis key 常量
- [x] 5.2 确保所有代码使用新定义的用户类型常量
- [x] 5.3 确保所有代码使用新定义的角色类型常量
- [x] 6.1 日志和埋点更新无需额外修改context 已包含完整信息)
- [ ] 7.x 测试更新(待完成)
- [x] 8.1 创建清理总结文档
---
## 性能影响
### 缓存使用
- **旧系统**: `account:subordinates:{account_id}`(缓存账号下级)
- **新系统**: `shop:subordinates:{shop_id}`(缓存店铺下级)
### 查询性能
- **代理用户**: 查询性能保持不变,从递归查询账号改为递归查询店铺
- **企业用户**: 查询性能提升,直接根据 `enterprise_id` 过滤
- **个人客户**: 查询性能提升,直接根据 `customer_id` 或 `creator` 过滤
---
## 后续工作
1. **完成测试修复**: 批量更新测试代码以适配新的 API 签名
2. **Token 生成逻辑更新**: 在 JWT Token 中添加 `enterprise_id` 和 `customer_id` 字段
3. **监控和日志**: 验证新的数据权限过滤逻辑是否正常工作
4. **性能测试**: 验证新的过滤逻辑性能是否符合预期
---
## 参考文档
- [提案文档](../../openspec/changes/archive/remove-legacy-rbac-cleanup/proposal.md)
- [用户组织模型](../add-user-organization-model/功能总结.md)
- [角色权限体系](../add-role-permission-system/功能总结.md)