Files
assets/dao/base/interceptor.go
2026-03-18 10:18:03 +08:00

220 lines
5.7 KiB
Go
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.
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 方法来实现无感知集成")
}