重构数据权限模型并清理旧RBAC代码

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-10 15:08:11 +08:00
parent 9c6d4a3bd4
commit 743db126f7
26 changed files with 1292 additions and 322 deletions

View File

@@ -28,31 +28,33 @@ func SkipDataPermission(ctx context.Context) context.Context {
return context.WithValue(ctx, SkipDataPermissionKey, true)
}
// AccountStoreInterface 账号 Store 接口
// 用于 Callback 获取下级 ID,避免循环依赖
type AccountStoreInterface interface {
GetSubordinateIDs(ctx context.Context, accountID uint) ([]uint, error)
// ShopStoreInterface 店铺 Store 接口
// 用于 Callback 获取下级店铺 ID避免循环依赖
type ShopStoreInterface interface {
GetSubordinateShopIDs(ctx context.Context, shopID uint) ([]uint, error)
}
// RegisterDataPermissionCallback 注册 GORM 数据权限过滤 Callback
//
// 自动化数据权限过滤规则:
// 1. root 用户跳过过滤,可以查看所有数据
// 2. 普通用户只能查看自己和下级的数据(通过递归查询下级 ID)
// 3. 同时限制 shop_id 相同(如果配置了 shop_id)
// 4. 通过 SkipDataPermission(ctx) 可以绕过权限过滤
// 自动化数据权限过滤规则
// 1. 超级管理员跳过过滤可以查看所有数据
// 2. 平台用户跳过过滤,可以查看所有数据
// 3. 代理用户只能查看自己店铺及下级店铺的数据(基于 shop_id 字段)
// 4. 企业用户只能查看自己企业的数据(基于 enterprise_id 字段)
// 5. 个人客户只能查看自己的数据(基于 creator 字段或 customer_id 字段)
// 6. 通过 SkipDataPermission(ctx) 可以绕过权限过滤
//
// 注意:
// - Callback 只对包含 creator 字段的表生效
// 注意
// - Callback 根据表的字段自动选择过滤策略
// - 必须在初始化 Store 之前注册
//
// 参数:
// 参数
// - db: GORM DB 实例
// - accountStore: 账号 Store,用于查询下级 ID
// - shopStore: 店铺 Store用于查询下级店铺 ID
//
// 返回:
// 返回
// - error: 注册错误
func RegisterDataPermissionCallback(db *gorm.DB, accountStore AccountStoreInterface) error {
func RegisterDataPermissionCallback(db *gorm.DB, shopStore ShopStoreInterface) error {
// 注册查询前的 Callback
err := db.Callback().Query().Before("gorm:query").Register("data_permission:query", func(tx *gorm.DB) {
ctx := tx.Statement.Context
@@ -65,17 +67,15 @@ func RegisterDataPermissionCallback(db *gorm.DB, accountStore AccountStoreInterf
return
}
// 2. 检查是否为 root 用户,root 用户跳过过滤
if middleware.IsRootUser(ctx) {
// 2. 获取用户类型
userType := middleware.GetUserTypeFromContext(ctx)
// 3. 超级管理员和平台用户跳过过滤,可以查看所有数据
if userType == constants.UserTypeSuperAdmin || userType == constants.UserTypePlatform {
return
}
// 3. 检查表是否有 creator 字段(只对有 creator 字段的表生效)
if !hasCreatorField(tx.Statement.Schema) {
return
}
// 4. 获取当前用户 ID
// 4. 获取当前用户信息
userID := middleware.GetUserIDFromContext(ctx)
if userID == 0 {
// 未登录用户返回空结果
@@ -84,32 +84,102 @@ func RegisterDataPermissionCallback(db *gorm.DB, accountStore AccountStoreInterf
return
}
// 5. 获取当前用户及所有下级的 ID
subordinateIDs, err := accountStore.GetSubordinateIDs(ctx, userID)
if err != nil {
// 查询失败时,降级为只能看自己的数据
logger.GetAppLogger().Error("数据权限过滤:获取下级 ID 失败",
zap.Uint("user_id", userID),
zap.Error(err))
subordinateIDs = []uint{userID}
}
if len(subordinateIDs) == 0 {
subordinateIDs = []uint{userID}
}
// 6. 获取当前用户的 shop_id
shopID := middleware.GetShopIDFromContext(ctx)
// 7. 应用数据权限过滤条件
// creator IN (用户自己及所有下级) AND shop_id = 当前用户 shop_id
if shopID != 0 && hasShopIDField(tx.Statement.Schema) {
// 同时过滤 creator 和 shop_id
tx.Where("creator IN ? AND shop_id = ?", subordinateIDs, shopID)
} else {
// 只根据 creator 过滤
tx.Where("creator IN ?", subordinateIDs)
// 5. 根据用户类型和表结构应用不同的过滤规则
schema := tx.Statement.Schema
if schema == nil {
return
}
// 5.1 代理用户:基于店铺层级过滤
if userType == constants.UserTypeAgent {
if !hasShopIDField(schema) {
// 表没有 shop_id 字段,无法过滤
return
}
if shopID == 0 {
// 代理用户没有 shop_id只能看自己创建的数据
if hasCreatorField(schema) {
tx.Where("creator = ?", userID)
} else {
tx.Where("1 = 0")
}
return
}
// 查询该店铺及下级店铺的 ID
subordinateShopIDs, err := shopStore.GetSubordinateShopIDs(ctx, shopID)
if err != nil {
logger.GetAppLogger().Error("数据权限过滤:获取下级店铺 ID 失败",
zap.Uint("shop_id", shopID),
zap.Error(err))
// 降级为只能看自己店铺的数据
subordinateShopIDs = []uint{shopID}
}
// 过滤shop_id IN (自己店铺及下级店铺)
tx.Where("shop_id IN ?", subordinateShopIDs)
return
}
// 5.2 企业用户:基于 enterprise_id 过滤
if userType == constants.UserTypeEnterprise {
enterpriseID := middleware.GetEnterpriseIDFromContext(ctx)
if hasEnterpriseIDField(schema) {
if enterpriseID != 0 {
tx.Where("enterprise_id = ?", enterpriseID)
} else {
// 企业用户没有 enterprise_id返回空结果
tx.Where("1 = 0")
}
return
}
// 如果表没有 enterprise_id 字段,但有 creator 字段,则只能看自己创建的数据
if hasCreatorField(schema) {
tx.Where("creator = ?", userID)
return
}
// 无法过滤,返回空结果
tx.Where("1 = 0")
return
}
// 5.3 个人客户:只能看自己的数据
if userType == constants.UserTypePersonalCustomer {
customerID := middleware.GetCustomerIDFromContext(ctx)
// 优先使用 customer_id 字段
if hasCustomerIDField(schema) {
if customerID != 0 {
tx.Where("customer_id = ?", customerID)
} else {
// 个人客户没有 customer_id返回空结果
tx.Where("1 = 0")
}
return
}
// 降级为使用 creator 字段
if hasCreatorField(schema) {
tx.Where("creator = ?", userID)
return
}
// 无法过滤,返回空结果
tx.Where("1 = 0")
return
}
// 6. 默认:未知用户类型,返回空结果
logger.GetAppLogger().Warn("数据权限过滤:未知用户类型",
zap.Uint("user_id", userID),
zap.Int("user_type", userType))
tx.Where("1 = 0")
})
return err
}
@@ -149,3 +219,21 @@ func hasShopIDField(s *schema.Schema) bool {
_, ok := s.FieldsByDBName["shop_id"]
return ok
}
// hasEnterpriseIDField 检查 Schema 是否包含 enterprise_id 字段
func hasEnterpriseIDField(s *schema.Schema) bool {
if s == nil {
return false
}
_, ok := s.FieldsByDBName["enterprise_id"]
return ok
}
// hasCustomerIDField 检查 Schema 是否包含 customer_id 字段
func hasCustomerIDField(s *schema.Schema) bool {
if s == nil {
return false
}
_, ok := s.FieldsByDBName["customer_id"]
return ok
}