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

12 KiB
Raw Permalink Blame History

旧 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 查询账号列表

移除的依赖:

  • 移除了 timeconstantssonic 包的导入(不再需要)

2. 数据权限过滤重构

文件: pkg/gorm/callback.go

核心变更: 从基于账号层级的过滤改为基于用户类型的多策略过滤。

旧的过滤逻辑

// 查询账号的所有下级ID
subordinateIDs := accountStore.GetSubordinateIDs(ctx, userID)
// 过滤: creator IN (下级账号ID)
tx.Where("creator IN ?", subordinateIDs)

新的过滤逻辑

根据用户类型自动选择合适的过滤策略:

  1. 超级管理员和平台用户: 跳过过滤,查看所有数据
  2. 代理用户: 基于店铺层级过滤
    subordinateShopIDs := shopStore.GetSubordinateShopIDs(ctx, shopID)
    tx.Where("shop_id IN ?", subordinateShopIDs)
    
  3. 企业用户: 基于企业ID过滤
    tx.Where("enterprise_id = ?", enterpriseID)
    
  4. 个人客户: 基于客户ID或创建人过滤
    tx.Where("customer_id = ?", customerID)
    // 或降级为
    tx.Where("creator = ?", userID)
    

接口变更:

// 旧接口
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

新增字段支持:

// 旧的用户上下文
- userID
- userType
- shopID

// 新的用户上下文
type UserContextInfo struct {
    UserID       uint  // 用户ID
    UserType     int   // 用户类型
    ShopID       uint  // 店铺ID代理用户
    EnterpriseID uint  // 企业ID企业用户
    CustomerID   uint  // 客户ID个人客户
}

API 变更:

// 旧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

新增辅助函数:

func GetEnterpriseIDFromContext(ctx) uint
func GetCustomerIDFromContext(ctx) uint

4. 常量清理

文件: pkg/constants/constants.gopkg/constants/redis.go

新增常量:

// Context 键
const (
    ContextKeyEnterpriseID = "enterprise_id"
    ContextKeyCustomerID   = "customer_id"
)

// 用户类型
const (
    UserTypePersonalCustomer = 5  // 个人客户C端用户
)

移除常量:

// Redis 键生成函数(已废弃)
func RedisAccountSubordinatesKey(accountID) string

5. Bootstrap 初始化调整

文件: internal/bootstrap/stores.gointernal/bootstrap/bootstrap.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 包

// ❌ 旧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 配置

// ❌ 旧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 注册

// ❌ 旧API
gorm.RegisterDataPermissionCallback(db, accountStore)

// ✅ 新API
gorm.RegisterDataPermissionCallback(db, shopStore)

迁移指南

对于业务代码

如果你的代码直接调用了以下方法,需要迁移:

1. AccountStore 方法调用

// ❌ 旧代码
ids, err := accountStore.GetSubordinateIDs(ctx, userID)
accountStore.ClearSubordinatesCache(ctx, userID)

// ✅ 新代码
// 不再需要查询账号下级,数据权限过滤会自动处理
// 如果需要查询店铺下级:
shopIDs, err := shopStore.GetSubordinateShopIDs(ctx, shopID)

2. 认证中间件配置

// ❌ 旧代码
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. 设置用户上下文

// ❌ 旧代码
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)

测试迁移模式

// ❌ 旧测试代码
ctx := middleware.SetUserContext(context.Background(), 1, constants.UserTypeAgent, 100)

// ✅ 新测试代码
ctx := middleware.SetUserContext(context.Background(), middleware.NewSimpleUserContext(1, constants.UserTypeAgent, 100))

遗留问题

1. 企业用户和个人客户的 Token 生成

问题: 当前 Token 验证器需要返回 enterprise_idcustomer_id,但现有的 Token 生成逻辑可能还没有包含这些字段。

影响: 企业用户和个人客户的数据权限过滤可能无法正常工作。

解决方案:

  1. 更新 JWT Token 的 Claims 结构,添加 enterprise_idcustomer_id 字段
  2. 在登录时根据用户类型生成包含对应字段的 Token

2. 测试兼容性

问题: 大量测试代码需要更新以适配新的 API 签名。

影响: 测试编译失败。

解决方案:

  1. 批量更新测试代码,使用 middleware.NewSimpleUserContext 辅助函数
  2. 为需要完整上下文的测试创建完整的 UserContextInfo 实例

验收清单

  • 0.1 确认 add-user-organization-model 提案已完成
  • 0.2 确认 add-role-permission-system 提案已完成
  • 0.3 确认 add-personal-customer-wechat 提案已完成
  • 1.1 移除 GetSubordinateIDs 方法
  • 1.2 移除相关的 Redis 缓存逻辑
  • 1.3 更新 account_store.go 中所有引用 parent_id 的代码
  • 1.4 添加新的查询方法:GetByShopIDGetByEnterpriseID
  • 2.1 创建/更新数据权限过滤逻辑
    • 2.1.1 改为从 context 获取 shop_id而非 user_id
    • 2.1.2 调用 shop_store.GetSubordinateShopIDs 获取下级店铺
    • 2.1.3 生成 WHERE shop_id IN (...) 过滤条件
  • 2.2 更新 Store 层的 List 方法
  • 2.3 处理企业账号的过滤逻辑
  • 2.4 处理平台用户跳过过滤的逻辑
  • 3.1 更新认证中间件
    • 3.1.1 在 context 中设置 enterprise_id 和 customer_id
    • 3.1.2 根据用户类型设置数据权限过滤标记
  • 4.1 权限校验中间件(无需修改)
  • 5.1 移除旧的 Redis key 常量
  • 5.2 确保所有代码使用新定义的用户类型常量
  • 5.3 确保所有代码使用新定义的角色类型常量
  • 6.1 日志和埋点更新无需额外修改context 已包含完整信息)
  • 7.x 测试更新(待完成)
  • 8.1 创建清理总结文档

性能影响

缓存使用

  • 旧系统: account:subordinates:{account_id}(缓存账号下级)
  • 新系统: shop:subordinates:{shop_id}(缓存店铺下级)

查询性能

  • 代理用户: 查询性能保持不变,从递归查询账号改为递归查询店铺
  • 企业用户: 查询性能提升,直接根据 enterprise_id 过滤
  • 个人客户: 查询性能提升,直接根据 customer_idcreator 过滤

后续工作

  1. 完成测试修复: 批量更新测试代码以适配新的 API 签名
  2. Token 生成逻辑更新: 在 JWT Token 中添加 enterprise_idcustomer_id 字段
  3. 监控和日志: 验证新的数据权限过滤逻辑是否正常工作
  4. 性能测试: 验证新的过滤逻辑性能是否符合预期

参考文档