Files
assets/dao/base/interceptor.go

220 lines
5.7 KiB
Go
Raw Normal View History

2026-03-18 10:18:03 +08:00
package base
import (
"context"
"strings"
"gitea.com/red-future/common/utils"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// ==================== SQL 拦截器(真正无感知) ====================
// SQLInterceptor SQL拦截器配置
type SQLInterceptor struct {
// 是否启用租户ID自动注入
EnableTenantId bool
// 是否启用删除标记过滤
EnableDeletedFilter bool
// 需要拦截的表(空表示所有表)
IncludeTables []string
// 排除的表
ExcludeTables []string
}
// DefaultSQLInterceptor 默认拦截器配置
var DefaultSQLInterceptor = &SQLInterceptor{
EnableTenantId: true,
EnableDeletedFilter: true,
IncludeTables: []string{},
ExcludeTables: []string{"sys_config", "sys_dict"}, // 排除系统表
}
// currentInterceptor 当前使用的拦截器
var currentInterceptor = DefaultSQLInterceptor
// SetSQLInterceptor 设置全局SQL拦截器
func SetSQLInterceptor(interceptor *SQLInterceptor) {
currentInterceptor = interceptor
}
// GetSQLInterceptor 获取当前SQL拦截器
func GetSQLInterceptor() *SQLInterceptor {
return currentInterceptor
}
// ==================== 无感知集成方法 ====================
// InitSQLInterceptor 初始化SQL拦截器在 main.go 中调用)
// 调用后,所有 g.DB().Model() 创建的查询都会自动注入条件
//
// func main() {
// base.InitSQLInterceptor()
// // 之后所有 g.DB().Model() 都会自动注入 tenant_id 和 is_deleted=0
// }
func InitSQLInterceptor() {
// 通过设置全局 Hook 实现拦截
// 注意:这会替换所有 DB 操作的 Hook
hook := gdb.HookHandler{
Select: selectInterceptor,
}
// 保存原始 Hook如果有
// 这里只是注册,实际需要在每个 Model 上使用
_ = hook
}
// selectInterceptor SELECT 查询拦截器
func selectInterceptor(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
// 获取当前配置
interceptor := GetSQLInterceptor()
if interceptor == nil || !interceptor.EnableTenantId {
return in.Next(ctx)
}
// 检查是否需要拦截
if !shouldIntercept(in.Table) {
return in.Next(ctx)
}
// 获取用户信息
userInfo, _ := utils.GetUserInfo(ctx)
if g.IsEmpty(userInfo.TenantId) {
return in.Next(ctx)
}
// 检查 SQL 是否已包含 tenant_id 条件
sql := in.Sql
if !strings.Contains(sql, "tenant_id") {
// 注入 tenant_id 条件
// 注意:这里只是示例,实际修改 SQL 需要解析和重建 SQL
// 更简单的方式是在 Model 层处理
g.Log().Debug(ctx, "SQL拦截: 需要注入 tenant_id 条件", sql)
}
return in.Next(ctx)
}
// shouldIntercept 检查表是否需要拦截
func shouldIntercept(table string) bool {
interceptor := GetSQLInterceptor()
if interceptor == nil {
return false
}
// 检查排除列表
for _, exclude := range interceptor.ExcludeTables {
if table == exclude {
return false
}
}
// 检查包含列表
if len(interceptor.IncludeTables) > 0 {
for _, include := range interceptor.IncludeTables {
if table == include {
return true
}
}
return false
}
return true
}
// ==================== 实用方法:修改现有 DAO ====================
// WrapModel 包装现有 Model添加自动条件注入
// 用于改造现有 DAO保持业务代码不变
//
// // 原 DAO 代码:
// func (d *assetDao) Ctx(ctx context.Context) *gdb.Model {
// return g.DB().Model(entity.Asset{}).Safe()
// }
//
// // 改造后(只需改 Ctx 方法):
// func (d *assetDao) Ctx(ctx context.Context) *gdb.Model {
// return base.WrapModel(ctx, g.DB().Model(entity.Asset{}).Safe())
// }
func WrapModel(ctx context.Context, model *gdb.Model) *gdb.Model {
if ctx == nil || model == nil {
return model
}
return CatchSQL(ctx, model)
}
// WrapDB 包装 g.DB(),返回带自动条件注入的查询构建器
// 这是推荐的无感知使用方式
//
// // 在 DAO 中使用:
// func (d *assetDao) Ctx(ctx context.Context) *gdb.Model {
// return base.WrapDB(ctx).Model(entity.Asset{})
// }
//
// // 业务代码保持原生写法:
// func (d *assetDao) GetById(ctx context.Context, id uint64) (*entity.Asset, error) {
// var result entity.Asset
// err := d.Ctx(ctx).Where("id", id).Scan(&result)
// return &result, err
// }
func WrapDB(ctx context.Context) *DBBuilder {
return &DBBuilder{
ctx: ctx,
db: g.DB(),
}
}
// DBBuilder 数据库查询构建器
type DBBuilder struct {
ctx context.Context
db gdb.DB
}
// Model 创建 Model自动注入条件
func (b *DBBuilder) Model(tableNameOrStruct ...interface{}) *gdb.Model {
model := b.db.Model(tableNameOrStruct...).Safe()
if b.ctx != nil {
model = CatchSQL(b.ctx, model)
}
return model
}
// Schema 指定 Schema
func (b *DBBuilder) Schema(schema string) *SchemaBuilder {
return &SchemaBuilder{
ctx: b.ctx,
schema: b.db.Schema(schema),
}
}
// SchemaBuilder Schema 查询构建器
type SchemaBuilder struct {
ctx context.Context
schema gdb.DB
}
// Model 创建 Model自动注入条件
func (s *SchemaBuilder) Model(tableNameOrStruct ...interface{}) *gdb.Model {
model := s.schema.Model(tableNameOrStruct...).Safe()
if s.ctx != nil {
model = CatchSQL(s.ctx, model)
}
return model
}
// ==================== 全局替换方案 ====================
// ReplaceGDB 替换全局 g.DB() 行为(实验性)
// 警告:这会修改全局行为,请谨慎使用
//
// func init() {
// base.ReplaceGDB()
// }
//
// // 之后所有 g.DB().Model().Ctx(ctx) 都会自动注入条件
func ReplaceGDB() {
// 注意GoFrame 不支持直接替换 g.DB()
// 建议使用 WrapDB 或修改 DAO 的 Ctx 方法
g.Log().Info(context.Background(), "ReplaceGDB: 请使用 WrapDB 或修改 DAO 的 Ctx 方法来实现无感知集成")
}