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),保留内部方法 - 完善单元测试和集成测试覆盖
344 lines
9.8 KiB
Go
344 lines
9.8 KiB
Go
package gorm
|
||
|
||
import (
|
||
"context"
|
||
"reflect"
|
||
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/schema"
|
||
)
|
||
|
||
// contextKey 用于 context value 的 key 类型
|
||
type contextKey string
|
||
|
||
// SkipDataPermissionKey 跳过数据权限过滤的 context key
|
||
const SkipDataPermissionKey contextKey = "skip_data_permission"
|
||
|
||
// SkipDataPermission 返回跳过数据权限过滤的 Context
|
||
// 用于需要查询所有数据的场景(如管理后台统计、系统任务等)
|
||
//
|
||
// 使用示例:
|
||
//
|
||
// ctx = gorm.SkipDataPermission(ctx)
|
||
// db.WithContext(ctx).Find(&accounts)
|
||
func SkipDataPermission(ctx context.Context) context.Context {
|
||
return context.WithValue(ctx, SkipDataPermissionKey, true)
|
||
}
|
||
|
||
// ShopStoreInterface 店铺 Store 接口
|
||
// 用于 Callback 获取下级店铺 ID,避免循环依赖
|
||
type ShopStoreInterface interface {
|
||
GetSubordinateShopIDs(ctx context.Context, shopID uint) ([]uint, error)
|
||
}
|
||
|
||
// RegisterDataPermissionCallback 注册 GORM 数据权限过滤 Callback
|
||
//
|
||
// 自动化数据权限过滤规则:
|
||
// 1. 超级管理员跳过过滤,可以查看所有数据
|
||
// 2. 平台用户跳过过滤,可以查看所有数据
|
||
// 3. 代理用户只能查看自己店铺及下级店铺的数据(基于 shop_id 字段)
|
||
// 4. 企业用户只能查看自己企业的数据(基于 enterprise_id 字段)
|
||
// 5. 个人客户只能查看自己的数据(基于 creator 字段或 customer_id 字段)
|
||
// 6. 通过 SkipDataPermission(ctx) 可以绕过权限过滤
|
||
//
|
||
// 软删除过滤规则:
|
||
// 1. 所有查询自动排除 deleted_at IS NOT NULL 的记录
|
||
// 2. 使用 db.Unscoped() 可以查询包含已删除的记录
|
||
//
|
||
// 注意:
|
||
// - Callback 根据表的字段自动选择过滤策略
|
||
// - 必须在初始化 Store 之前注册
|
||
//
|
||
// 参数:
|
||
// - db: GORM DB 实例
|
||
// - shopStore: 店铺 Store,用于查询下级店铺 ID
|
||
//
|
||
// 返回:
|
||
// - 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
|
||
if ctx == nil {
|
||
return
|
||
}
|
||
|
||
// 1. 检查是否跳过数据权限过滤
|
||
if skip, ok := ctx.Value(SkipDataPermissionKey).(bool); ok && skip {
|
||
return
|
||
}
|
||
|
||
// 2. 获取用户类型
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
|
||
// 3. 超级管理员和平台用户跳过过滤,可以查看所有数据
|
||
if userType == constants.UserTypeSuperAdmin || userType == constants.UserTypePlatform {
|
||
return
|
||
}
|
||
|
||
// 4. 获取当前用户信息
|
||
userID := middleware.GetUserIDFromContext(ctx)
|
||
if userID == 0 {
|
||
// 未登录用户返回空结果
|
||
logger.GetAppLogger().Warn("数据权限过滤:未获取到用户 ID")
|
||
tx.Where("1 = 0")
|
||
return
|
||
}
|
||
|
||
shopID := middleware.GetShopIDFromContext(ctx)
|
||
|
||
// 5. 根据用户类型和表结构应用不同的过滤规则
|
||
schema := tx.Statement.Schema
|
||
if schema == nil {
|
||
return
|
||
}
|
||
|
||
// 5.1 代理用户:基于店铺层级过滤
|
||
if userType == constants.UserTypeAgent {
|
||
tableName := schema.Table
|
||
|
||
// 特殊处理:授权记录表(通过企业归属过滤,不含下级店铺)
|
||
if tableName == "tb_enterprise_card_authorization" {
|
||
if shopID == 0 {
|
||
// 代理用户没有 shop_id,返回空结果
|
||
tx.Where("1 = 0")
|
||
return
|
||
}
|
||
// 只能看到自己店铺下企业的授权记录(不包含下级店铺)
|
||
tx.Where("enterprise_id IN (SELECT id FROM tb_enterprise WHERE owner_shop_id = ? AND deleted_at IS NULL)", shopID)
|
||
return
|
||
}
|
||
|
||
// 特殊处理:标签表和资源标签表(包含全局标签)
|
||
if tableName == "tb_tag" || tableName == "tb_resource_tag" {
|
||
if shopID == 0 {
|
||
// 没有 shop_id,只能看全局标签
|
||
tx.Where("enterprise_id IS NULL AND shop_id IS NULL")
|
||
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}
|
||
}
|
||
|
||
// 过滤:店铺标签(自己店铺及下级店铺)或全局标签
|
||
tx.Where("shop_id IN ? OR (enterprise_id IS NULL AND shop_id IS NULL)", subordinateShopIDs)
|
||
return
|
||
}
|
||
|
||
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)
|
||
tableName := schema.Table
|
||
|
||
// 特殊处理:标签表和资源标签表(包含全局标签)
|
||
if tableName == "tb_tag" || tableName == "tb_resource_tag" {
|
||
if enterpriseID != 0 {
|
||
// 过滤:企业标签或全局标签
|
||
tx.Where("enterprise_id = ? OR (enterprise_id IS NULL AND shop_id IS NULL)", enterpriseID)
|
||
} else {
|
||
// 没有 enterprise_id,只能看全局标签
|
||
tx.Where("enterprise_id IS NULL AND shop_id IS NULL")
|
||
}
|
||
return
|
||
}
|
||
|
||
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)
|
||
tableName := schema.Table
|
||
|
||
// 特殊处理:标签表和资源标签表(只能看全局标签)
|
||
if tableName == "tb_tag" || tableName == "tb_resource_tag" {
|
||
tx.Where("enterprise_id IS NULL AND shop_id IS NULL")
|
||
return
|
||
}
|
||
|
||
// 优先使用 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
|
||
}
|
||
|
||
// RegisterSetCreatorUpdaterCallback 注册 GORM 创建数据时创建人更新人 Callback
|
||
func RegisterSetCreatorUpdaterCallback(db *gorm.DB) error {
|
||
err := db.Callback().Create().Before("gorm:create").Register("set_creator_updater", func(tx *gorm.DB) {
|
||
ctx := tx.Statement.Context
|
||
userID, ok := tx.Statement.Context.Value(constants.ContextKeyUserID).(uint)
|
||
if !ok || tx.Statement.Schema == nil {
|
||
return
|
||
}
|
||
|
||
creatorField, hasCreator := tx.Statement.Schema.FieldsByName["Creator"]
|
||
updaterField, hasUpdater := tx.Statement.Schema.FieldsByName["Updater"]
|
||
if !hasCreator && !hasUpdater {
|
||
return
|
||
}
|
||
|
||
rv := tx.Statement.ReflectValue
|
||
switch rv.Kind() {
|
||
case reflect.Slice, reflect.Array:
|
||
for i := 0; i < rv.Len(); i++ {
|
||
elem := rv.Index(i)
|
||
if elem.Kind() == reflect.Ptr {
|
||
elem = elem.Elem()
|
||
}
|
||
if hasCreator {
|
||
_ = creatorField.Set(ctx, elem, userID)
|
||
}
|
||
if hasUpdater {
|
||
_ = updaterField.Set(ctx, elem, userID)
|
||
}
|
||
}
|
||
case reflect.Struct:
|
||
if hasCreator {
|
||
_ = creatorField.Set(ctx, rv, userID)
|
||
}
|
||
if hasUpdater {
|
||
_ = updaterField.Set(ctx, rv, userID)
|
||
}
|
||
case reflect.Ptr:
|
||
elem := rv.Elem()
|
||
if elem.Kind() == reflect.Struct {
|
||
if hasCreator {
|
||
_ = creatorField.Set(ctx, elem, userID)
|
||
}
|
||
if hasUpdater {
|
||
_ = updaterField.Set(ctx, elem, userID)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
return err
|
||
}
|
||
|
||
// hasCreatorField 检查 Schema 是否包含 creator 字段
|
||
func hasCreatorField(s *schema.Schema) bool {
|
||
if s == nil {
|
||
return false
|
||
}
|
||
_, ok := s.FieldsByDBName["creator"]
|
||
return ok
|
||
}
|
||
|
||
// hasShopIDField 检查 Schema 是否包含 shop_id 字段
|
||
func hasShopIDField(s *schema.Schema) bool {
|
||
if s == nil {
|
||
return false
|
||
}
|
||
_, 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
|
||
}
|
||
|
||
// hasDeletedAtField 检查 Schema 是否包含 deleted_at 字段
|
||
func hasDeletedAtField(s *schema.Schema) bool {
|
||
if s == nil {
|
||
return false
|
||
}
|
||
_, ok := s.FieldsByDBName["deleted_at"]
|
||
return ok
|
||
}
|