Dockerfile
This commit is contained in:
325
dao/base/catch_sql.go
Normal file
325
dao/base/catch_sql.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// ==================== CatchSQL 全局SQL条件拼接控制 ====================
|
||||
|
||||
// SQLConditionBuilder SQL条件构建器
|
||||
type SQLConditionBuilder struct {
|
||||
// 是否自动添加租户ID条件
|
||||
EnableTenantId bool
|
||||
// 是否自动添加创建人条件
|
||||
EnableCreator bool
|
||||
// 是否自动添加修改人条件
|
||||
EnableUpdater bool
|
||||
// 是否自动添加删除标记条件(只查询未删除数据)
|
||||
EnableDeletedFilter bool
|
||||
// 自定义额外条件
|
||||
ExtraConditions map[string]interface{}
|
||||
}
|
||||
|
||||
// DefaultSQLConditionBuilder 默认SQL条件构建器配置
|
||||
var DefaultSQLConditionBuilder = SQLConditionBuilder{
|
||||
EnableTenantId: true,
|
||||
EnableCreator: false,
|
||||
EnableUpdater: false,
|
||||
EnableDeletedFilter: true,
|
||||
ExtraConditions: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
// ctxKeyCatchSQL 上下文键
|
||||
type ctxKeyCatchSQL string
|
||||
|
||||
const (
|
||||
// ctxKeySQLBuilder SQL构建器上下文键
|
||||
ctxKeySQLBuilder ctxKeyCatchSQL = "catch_sql_builder"
|
||||
// ctxKeySkipCatchSQL 跳过CatchSQL的上下文键
|
||||
ctxKeySkipCatchSQL ctxKeyCatchSQL = "catch_sql_skip"
|
||||
)
|
||||
|
||||
// WithSQLBuilder 设置自定义SQL条件构建器到上下文
|
||||
func WithSQLBuilder(ctx context.Context, builder *SQLConditionBuilder) context.Context {
|
||||
return context.WithValue(ctx, ctxKeySQLBuilder, builder)
|
||||
}
|
||||
|
||||
// GetSQLBuilder 从上下文获取SQL条件构建器
|
||||
func GetSQLBuilder(ctx context.Context) *SQLConditionBuilder {
|
||||
if ctx == nil {
|
||||
return &DefaultSQLConditionBuilder
|
||||
}
|
||||
if builder, ok := ctx.Value(ctxKeySQLBuilder).(*SQLConditionBuilder); ok && builder != nil {
|
||||
return builder
|
||||
}
|
||||
return &DefaultSQLConditionBuilder
|
||||
}
|
||||
|
||||
// SkipCatchSQL 跳过CatchSQL条件拼接
|
||||
func SkipCatchSQL(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, ctxKeySkipCatchSQL, true)
|
||||
}
|
||||
|
||||
// IsSkipCatchSQL 检查是否跳过CatchSQL
|
||||
func IsSkipCatchSQL(ctx context.Context) bool {
|
||||
if ctx == nil {
|
||||
return false
|
||||
}
|
||||
v, ok := ctx.Value(ctxKeySkipCatchSQL).(bool)
|
||||
return ok && v
|
||||
}
|
||||
|
||||
// ==================== CatchSQL 核心方法 ====================
|
||||
|
||||
// CatchSQL 全局统一控制SQL条件拼接
|
||||
// 根据上下文自动添加租户ID、创建人、修改人、删除标记等条件
|
||||
// 使用示例:
|
||||
//
|
||||
// // 基础使用(自动添加默认条件)
|
||||
// m := base.CatchSQL(ctx, g.DB().Model("asset"))
|
||||
// m.Where("status", 1).Scan(&result)
|
||||
//
|
||||
// // 自定义条件构建器
|
||||
// builder := &base.SQLConditionBuilder{
|
||||
// EnableTenantId: true,
|
||||
// EnableCreator: true,
|
||||
// }
|
||||
// ctx = base.WithSQLBuilder(ctx, builder)
|
||||
// m := base.CatchSQL(ctx, g.DB().Model("asset"))
|
||||
//
|
||||
// // 跳过CatchSQL
|
||||
// ctx = base.SkipCatchSQL(ctx)
|
||||
// m := base.CatchSQL(ctx, g.DB().Model("asset"))
|
||||
func CatchSQL(ctx context.Context, model *gdb.Model) *gdb.Model {
|
||||
if ctx == nil || model == nil {
|
||||
return model
|
||||
}
|
||||
|
||||
// 检查是否跳过
|
||||
if IsSkipCatchSQL(ctx) {
|
||||
return model
|
||||
}
|
||||
|
||||
builder := GetSQLBuilder(ctx)
|
||||
userInfo, _ := utils.GetUserInfo(ctx)
|
||||
|
||||
// 1. 自动添加租户ID条件
|
||||
if builder.EnableTenantId && !g.IsEmpty(userInfo.TenantId) {
|
||||
model = model.Where("tenant_id", userInfo.TenantId)
|
||||
}
|
||||
|
||||
// 2. 自动添加创建人条件
|
||||
if builder.EnableCreator && !g.IsEmpty(userInfo.UserName) {
|
||||
model = model.Where("creator", userInfo.UserName)
|
||||
}
|
||||
|
||||
// 3. 自动添加修改人条件
|
||||
if builder.EnableUpdater && !g.IsEmpty(userInfo.UserName) {
|
||||
model = model.Where("updater", userInfo.UserName)
|
||||
}
|
||||
|
||||
// 4. 自动添加删除标记条件(只查询未删除数据)
|
||||
if builder.EnableDeletedFilter {
|
||||
model = model.Where("is_deleted", 0)
|
||||
}
|
||||
|
||||
// 5. 添加自定义额外条件
|
||||
for field, value := range builder.ExtraConditions {
|
||||
if field != "" && value != nil {
|
||||
model = model.Where(field, value)
|
||||
}
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
// CatchSQLWithTable 指定表名创建带CatchSQL条件的Model
|
||||
// 使用示例:
|
||||
//
|
||||
// m := base.CatchSQLWithTable(ctx, "asset")
|
||||
// m.Where("status", 1).Scan(&result)
|
||||
func CatchSQLWithTable(ctx context.Context, table string) *gdb.Model {
|
||||
if ctx == nil {
|
||||
return g.DB().Model(table).Safe()
|
||||
}
|
||||
model := g.DB().Model(table).Safe().Ctx(ctx)
|
||||
return CatchSQL(ctx, model)
|
||||
}
|
||||
|
||||
// CatchSQLWithSchema 指定Schema和表名创建带CatchSQL条件的Model
|
||||
// 使用示例:
|
||||
//
|
||||
// m := base.CatchSQLWithSchema(ctx, "public", "asset")
|
||||
// m.Where("status", 1).Scan(&result)
|
||||
func CatchSQLWithSchema(ctx context.Context, schema, table string) *gdb.Model {
|
||||
if ctx == nil {
|
||||
return g.DB().Schema(schema).Model(table).Safe()
|
||||
}
|
||||
model := g.DB().Schema(schema).Model(table).Safe().Ctx(ctx)
|
||||
return CatchSQL(ctx, model)
|
||||
}
|
||||
|
||||
// ==================== 快捷条件构建器 ====================
|
||||
|
||||
// NewSQLBuilder 创建新的SQL条件构建器
|
||||
func NewSQLBuilder() *SQLConditionBuilder {
|
||||
return &SQLConditionBuilder{
|
||||
EnableTenantId: true,
|
||||
EnableCreator: false,
|
||||
EnableUpdater: false,
|
||||
EnableDeletedFilter: true,
|
||||
ExtraConditions: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// WithTenantId 启用/禁用租户ID条件
|
||||
func (b *SQLConditionBuilder) WithTenantId(enable bool) *SQLConditionBuilder {
|
||||
b.EnableTenantId = enable
|
||||
return b
|
||||
}
|
||||
|
||||
// WithCreator 启用/禁用创建人条件
|
||||
func (b *SQLConditionBuilder) WithCreator(enable bool) *SQLConditionBuilder {
|
||||
b.EnableCreator = enable
|
||||
return b
|
||||
}
|
||||
|
||||
// WithUpdater 启用/禁用修改人条件
|
||||
func (b *SQLConditionBuilder) WithUpdater(enable bool) *SQLConditionBuilder {
|
||||
b.EnableUpdater = enable
|
||||
return b
|
||||
}
|
||||
|
||||
// WithDeletedFilter 启用/禁用删除标记过滤
|
||||
func (b *SQLConditionBuilder) WithDeletedFilter(enable bool) *SQLConditionBuilder {
|
||||
b.EnableDeletedFilter = enable
|
||||
return b
|
||||
}
|
||||
|
||||
// WithExtraCondition 添加自定义条件
|
||||
func (b *SQLConditionBuilder) WithExtraCondition(field string, value interface{}) *SQLConditionBuilder {
|
||||
if b.ExtraConditions == nil {
|
||||
b.ExtraConditions = make(map[string]interface{})
|
||||
}
|
||||
b.ExtraConditions[field] = value
|
||||
return b
|
||||
}
|
||||
|
||||
// Build 构建并返回Model
|
||||
func (b *SQLConditionBuilder) Build(ctx context.Context, model *gdb.Model) *gdb.Model {
|
||||
ctx = WithSQLBuilder(ctx, b)
|
||||
return CatchSQL(ctx, model)
|
||||
}
|
||||
|
||||
// ==================== 常用场景快捷方法 ====================
|
||||
|
||||
// CatchSQLForTenant 只添加租户ID条件的快捷方法
|
||||
func CatchSQLForTenant(ctx context.Context, model *gdb.Model) *gdb.Model {
|
||||
builder := NewSQLBuilder().
|
||||
WithTenantId(true).
|
||||
WithDeletedFilter(false)
|
||||
ctx = WithSQLBuilder(ctx, builder)
|
||||
return CatchSQL(ctx, model)
|
||||
}
|
||||
|
||||
// CatchSQLForCreator 只添加创建人条件的快捷方法
|
||||
func CatchSQLForCreator(ctx context.Context, model *gdb.Model) *gdb.Model {
|
||||
builder := NewSQLBuilder().
|
||||
WithTenantId(false).
|
||||
WithCreator(true).
|
||||
WithDeletedFilter(false)
|
||||
ctx = WithSQLBuilder(ctx, builder)
|
||||
return CatchSQL(ctx, model)
|
||||
}
|
||||
|
||||
// CatchSQLForList 列表查询的快捷方法(租户ID + 删除标记)
|
||||
func CatchSQLForList(ctx context.Context, model *gdb.Model) *gdb.Model {
|
||||
builder := NewSQLBuilder().
|
||||
WithTenantId(true).
|
||||
WithDeletedFilter(true)
|
||||
ctx = WithSQLBuilder(ctx, builder)
|
||||
return CatchSQL(ctx, model)
|
||||
}
|
||||
|
||||
// CatchSQLForAdmin 管理员查询的快捷方法(只过滤删除标记,不过滤租户)
|
||||
func CatchSQLForAdmin(ctx context.Context, model *gdb.Model) *gdb.Model {
|
||||
builder := NewSQLBuilder().
|
||||
WithTenantId(false).
|
||||
WithDeletedFilter(true)
|
||||
ctx = WithSQLBuilder(ctx, builder)
|
||||
return CatchSQL(ctx, model)
|
||||
}
|
||||
|
||||
// ==================== DAO层无感知集成 ====================
|
||||
|
||||
// CtxModel 创建带 CatchSQL 条件的 Model(DAO层无感知使用)
|
||||
// 这是推荐的无感知使用方式,直接在 DAO 的 Ctx() 方法中调用
|
||||
//
|
||||
// 使用示例(DAO层):
|
||||
//
|
||||
// func (d *assetDao) Ctx(ctx context.Context) *gdb.Model {
|
||||
// return base.CtxModel(ctx, 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) // 自动带 tenant_id 和 is_deleted=0 条件
|
||||
// return &result, err
|
||||
// }
|
||||
func CtxModel(ctx context.Context, table interface{}) *gdb.Model {
|
||||
if ctx == nil {
|
||||
return g.DB().Model(table).Safe()
|
||||
}
|
||||
model := g.DB().Model(table).Safe().Ctx(ctx)
|
||||
return CatchSQL(ctx, model)
|
||||
}
|
||||
|
||||
// CtxModelWithHook 创建带 CatchSQL 条件和 Hook 的 Model(完整版)
|
||||
// 同时启用 CatchSQL 条件拼接和 CatchSQLHook 自动字段赋值
|
||||
//
|
||||
// 使用示例(DAO层):
|
||||
//
|
||||
// func (d *assetDao) Ctx(ctx context.Context) *gdb.Model {
|
||||
// return base.CtxModelWithHook(ctx, entity.Asset{})
|
||||
// }
|
||||
//
|
||||
// func (d *assetDao) Insert(ctx context.Context, data g.Map) (int64, error) {
|
||||
// return d.Ctx(ctx).Data(data).InsertAndGetId() // 自动赋值 tenant_id, creator, updater
|
||||
// }
|
||||
func CtxModelWithHook(ctx context.Context, table interface{}) *gdb.Model {
|
||||
if ctx == nil {
|
||||
return g.DB().Model(table).Safe()
|
||||
}
|
||||
model := g.DB().Model(table).Safe().Ctx(ctx)
|
||||
// 先应用 CatchSQL 条件
|
||||
model = CatchSQL(ctx, model)
|
||||
// 再应用 Hook(用于 Insert/Update 自动字段赋值)
|
||||
model = model.Hook(CatchSQLHook())
|
||||
return model
|
||||
}
|
||||
|
||||
// ==================== 调试工具 ====================
|
||||
|
||||
// GetCatchSQLInfo 获取当前CatchSQL的配置信息(用于调试)
|
||||
func GetCatchSQLInfo(ctx context.Context) string {
|
||||
if IsSkipCatchSQL(ctx) {
|
||||
return "CatchSQL: skipped"
|
||||
}
|
||||
|
||||
builder := GetSQLBuilder(ctx)
|
||||
userInfo, _ := utils.GetUserInfo(ctx)
|
||||
|
||||
return fmt.Sprintf(
|
||||
"CatchSQL{TenantId:%v(%v), Creator:%v(%v), Updater:%v(%v), DeletedFilter:%v, Extra:%v}",
|
||||
builder.EnableTenantId, userInfo.TenantId,
|
||||
builder.EnableCreator, userInfo.UserName,
|
||||
builder.EnableUpdater, userInfo.UserName,
|
||||
builder.EnableDeletedFilter,
|
||||
len(builder.ExtraConditions),
|
||||
)
|
||||
}
|
||||
75
dao/base/db.go
Normal file
75
dao/base/db.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"github.com/gogf/gf/contrib/drivers/pgsql/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
)
|
||||
|
||||
type GDB struct {
|
||||
*pgsql.Driver
|
||||
}
|
||||
|
||||
var (
|
||||
// customDriverName is my driver name, which is used for registering.
|
||||
customDriverName = "MyDriver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// It here registers my custom driver in package initialization function "init".
|
||||
// You can later use this type in the database configuration.
|
||||
if err := gdb.Register(customDriverName, &GDB{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates and returns a database object for mysql.
|
||||
// It implements the interface of gdb.Driver for extra database driver installation.
|
||||
func (d *GDB) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) {
|
||||
return &GDB{
|
||||
&pgsql.Driver{
|
||||
Core: core,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DoCommit commits current sql and arguments to underlying sql driver.
|
||||
func (d *GDB) DoCommit(ctx context.Context, in gdb.DoCommitInput) (out gdb.DoCommitOutput, err error) {
|
||||
out, err = d.Core.DoCommit(ctx, in)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 其他接口方法
|
||||
func (d *GDB) DoFilter(ctx context.Context, link gdb.Link, sql string, args []any) (string, []any, error) {
|
||||
return d.Core.DoFilter(ctx, link, sql, args)
|
||||
}
|
||||
|
||||
func (d *GDB) DoPrepare(ctx context.Context, link gdb.Link, sql string) (*gdb.Stmt, error) {
|
||||
return d.Core.DoPrepare(ctx, link, sql)
|
||||
}
|
||||
|
||||
func (d *GDB) ConvertValueForField(ctx context.Context, fieldType string, fieldValue any) (any, error) {
|
||||
return fieldValue, nil
|
||||
}
|
||||
func (d *GDB) ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue any) (any, error) {
|
||||
return d.Core.ConvertValueForLocal(ctx, fieldType, fieldValue)
|
||||
}
|
||||
|
||||
func (d *GDB) Exec(ctx context.Context, sql string, args ...any) (sql.Result, error) {
|
||||
in := gdb.DoCommitInput{Type: gdb.SqlTypeExecContext, Sql: sql, Args: args}
|
||||
out, err := d.DoCommit(ctx, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out.Result, nil
|
||||
}
|
||||
func (d *GDB) DoExec(ctx context.Context, link gdb.Link, sql string, args ...any) (sql.Result, error) {
|
||||
in := gdb.DoCommitInput{Type: gdb.SqlTypeExecContext, Sql: sql, Args: args}
|
||||
out, err := d.DoCommit(ctx, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out.Result, nil
|
||||
}
|
||||
456
dao/base/hook.go
Normal file
456
dao/base/hook.go
Normal file
@@ -0,0 +1,456 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"time"
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/database/gredis"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcache"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// ==================== 上下文键定义 ====================
|
||||
|
||||
type ctxKey string
|
||||
|
||||
const (
|
||||
// ctxKeySkipTenant 跳过租户ID自动赋值的上下文键
|
||||
ctxKeySkipTenant ctxKey = "hook_skip_tenant"
|
||||
// ctxKeyCacheEnabled 缓存启用标记的上下文键
|
||||
ctxKeyCacheEnabled ctxKey = "hook_cache_enabled"
|
||||
// ctxKeyCachePrefix 缓存key前缀的上下文键
|
||||
ctxKeyCachePrefix ctxKey = "hook_cache_prefix"
|
||||
)
|
||||
|
||||
// ==================== 租户相关 ====================
|
||||
|
||||
// SkipTenantId 在上下文中标记跳过租户ID自动赋值
|
||||
func SkipTenantId(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, ctxKeySkipTenant, true)
|
||||
}
|
||||
|
||||
// isSkipTenant 检查是否跳过租户ID
|
||||
func isSkipTenant(ctx context.Context) bool {
|
||||
if ctx == nil {
|
||||
return false
|
||||
}
|
||||
v, ok := ctx.Value(ctxKeySkipTenant).(bool)
|
||||
return ok && v
|
||||
}
|
||||
|
||||
// ==================== 缓存配置 ====================
|
||||
|
||||
// CacheConfig 缓存配置
|
||||
type CacheConfig struct {
|
||||
// 本地缓存过期时间(秒),默认60秒
|
||||
LocalTTL int
|
||||
// Redis缓存过期时间(秒),默认300秒
|
||||
RedisTTL int
|
||||
}
|
||||
|
||||
// DefaultCacheConfig 默认缓存配置
|
||||
var DefaultCacheConfig = CacheConfig{
|
||||
LocalTTL: 60,
|
||||
RedisTTL: 300,
|
||||
}
|
||||
|
||||
// isCacheEnabled 检查是否启用缓存
|
||||
func isCacheEnabled(ctx context.Context) bool {
|
||||
if ctx == nil {
|
||||
return false
|
||||
}
|
||||
v, ok := ctx.Value(ctxKeyCacheEnabled).(bool)
|
||||
return ok && v
|
||||
}
|
||||
|
||||
// getCachePrefix 获取缓存key前缀
|
||||
func getCachePrefix(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
v, ok := ctx.Value(ctxKeyCachePrefix).(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ==================== 缓存管理器(单例) ====================
|
||||
|
||||
var (
|
||||
localCache *gcache.Cache
|
||||
)
|
||||
|
||||
// getLocalCache 获取本地缓存实例
|
||||
func getLocalCache() *gcache.Cache {
|
||||
if localCache == nil {
|
||||
localCache = gcache.New()
|
||||
}
|
||||
return localCache
|
||||
}
|
||||
|
||||
// buildCacheKey 构建缓存key
|
||||
// 根据表名和查询条件自动生成key
|
||||
func buildCacheKey(prefix string, table string, where ...interface{}) string {
|
||||
// 基础key: prefix:table
|
||||
key := fmt.Sprintf("%s:%s", prefix, table)
|
||||
|
||||
// 如果有where条件,追加到key中
|
||||
if len(where) > 0 {
|
||||
for _, w := range where {
|
||||
key = fmt.Sprintf("%s:%v", key, w)
|
||||
}
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
// getFromCache 从缓存获取数据(本地缓存 -> Redis)
|
||||
func getFromCache(ctx context.Context, key string) ([]byte, bool) {
|
||||
config := DefaultCacheConfig
|
||||
|
||||
// 1. 先查本地缓存
|
||||
if val, err := getLocalCache().Get(ctx, key); err == nil && val != nil {
|
||||
if data := val.Bytes(); len(data) > 0 {
|
||||
glog.Debugf(ctx, "[Cache] Hit local cache: %s", key)
|
||||
return data, true
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 再查Redis缓存
|
||||
if g.Redis() != nil {
|
||||
result, err := g.Redis().Get(ctx, key)
|
||||
if err == nil && !result.IsEmpty() {
|
||||
data := result.Bytes()
|
||||
// 写入本地缓存
|
||||
getLocalCache().Set(ctx, key, data, time.Duration(config.LocalTTL)*time.Second)
|
||||
glog.Debugf(ctx, "[Cache] Hit redis cache: %s", key)
|
||||
return data, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// setToCache 写入缓存(本地缓存 + Redis)
|
||||
func setToCache(ctx context.Context, key string, data []byte) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
config := DefaultCacheConfig
|
||||
|
||||
// 1. 写入本地缓存
|
||||
getLocalCache().Set(ctx, key, data, time.Duration(config.LocalTTL)*time.Second)
|
||||
|
||||
// 2. 写入Redis缓存
|
||||
if g.Redis() != nil {
|
||||
expire := int64(config.RedisTTL)
|
||||
_, err := g.Redis().Set(ctx, key, data, gredis.SetOption{
|
||||
TTLOption: gredis.TTLOption{
|
||||
EX: &expire,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
glog.Warningf(ctx, "[Cache] Failed to set redis cache: %s, err: %v", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deleteCache 删除缓存
|
||||
func deleteCache(ctx context.Context, key string) {
|
||||
// 1. 删除本地缓存
|
||||
getLocalCache().Remove(ctx, key)
|
||||
|
||||
// 2. 删除Redis缓存
|
||||
if g.Redis() != nil {
|
||||
_, err := g.Redis().Del(ctx, key)
|
||||
if err != nil {
|
||||
glog.Warningf(ctx, "[Cache] Failed to delete redis cache: %s, err: %v", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deleteCacheByPattern 根据模式删除缓存
|
||||
func deleteCacheByPattern(ctx context.Context, pattern string) {
|
||||
// 1. 清空本地缓存(简单实现:清空所有)
|
||||
getLocalCache().Clear(ctx)
|
||||
|
||||
// 2. 删除Redis缓存(使用SCAN+DEL)
|
||||
if g.Redis() != nil {
|
||||
var cursor uint64 = 0
|
||||
for {
|
||||
result, err := g.Redis().Do(ctx, "SCAN", cursor, "MATCH", pattern, "COUNT", 100)
|
||||
if err != nil {
|
||||
glog.Warningf(ctx, "[Cache] Failed to scan redis keys: %s, err: %v", pattern, err)
|
||||
break
|
||||
}
|
||||
|
||||
resultMap := result.Map()
|
||||
cursor = gconv.Uint64(resultMap["cursor"])
|
||||
keys := gconv.Strings(resultMap["keys"])
|
||||
|
||||
if len(keys) > 0 {
|
||||
args := make([]interface{}, len(keys))
|
||||
for i, k := range keys {
|
||||
args[i] = k
|
||||
}
|
||||
_, err = g.Redis().Do(ctx, "DEL", args...)
|
||||
if err != nil {
|
||||
glog.Warningf(ctx, "[Cache] Failed to delete redis keys: %v, err: %v", keys, err)
|
||||
}
|
||||
}
|
||||
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 统一Hook入口 ====================
|
||||
|
||||
// CatchSQLHook 返回统一的 HookHandler(包含租户自动赋值和缓存)
|
||||
// 使用示例:
|
||||
//
|
||||
// // 基础使用(自动租户赋值,无缓存)
|
||||
// g.DB().Model("user").Hook(base.CatchSQLHook()).Ctx(ctx).Insert(data)
|
||||
//
|
||||
// // 启用缓存(用户无感知,自动处理缓存key)
|
||||
// ctx = base.WithCacheEnabled(ctx, "asset")
|
||||
// Asset.CtxWithCache(ctx).Where("id", 123).Scan(&result)
|
||||
func CatchSQLHook() gdb.HookHandler {
|
||||
return gdb.HookHandler{
|
||||
Insert: insertHook,
|
||||
Update: updateHook,
|
||||
Delete: deleteHook,
|
||||
Select: selectHook,
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Insert钩子 ====================
|
||||
|
||||
func insertHook(ctx context.Context, in *gdb.HookInsertInput) (result sql.Result, err error) {
|
||||
// 1. 自动赋值租户字段
|
||||
userInfo, _ := utils.GetUserInfo(ctx)
|
||||
|
||||
if !g.IsEmpty(userInfo.TenantId) {
|
||||
in.Model.Data("tenant_id", userInfo.TenantId)
|
||||
}
|
||||
if !g.IsEmpty(userInfo.UserName) {
|
||||
in.Model.Data("creator", userInfo.UserName)
|
||||
in.Model.Data("updater", userInfo.UserName)
|
||||
}
|
||||
//for i := range in.Data {
|
||||
// if !g.IsEmpty(userInfo.TenantId) {
|
||||
// if _, ok := in.Data[i]["tenant_id"]; !ok {
|
||||
// in.Data[i]["tenant_id"] = userInfo.TenantId
|
||||
// }
|
||||
// }
|
||||
// if !g.IsEmpty(userInfo.UserId) {
|
||||
// if _, ok := in.Data[i]["creator"]; !ok {
|
||||
// in.Data[i]["creator"] = userInfo.UserId
|
||||
// }
|
||||
// if _, ok := in.Data[i]["updater"]; !ok {
|
||||
// in.Data[i]["updater"] = userInfo.UserId
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// 2. 执行插入
|
||||
result, err = in.Next(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 清除相关缓存
|
||||
prefix := getCachePrefix(ctx)
|
||||
if prefix != "" {
|
||||
deleteCacheByPattern(ctx, prefix+":*")
|
||||
glog.Debugf(ctx, "[Hook] Cache cleared after insert, prefix: %s", prefix)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ==================== Update钩子 ====================
|
||||
|
||||
func updateHook(ctx context.Context, in *gdb.HookUpdateInput) (result sql.Result, err error) {
|
||||
// 1. 自动赋值修改人
|
||||
userInfo, _ := utils.GetUserInfo(ctx)
|
||||
|
||||
if !g.IsEmpty(userInfo.TenantId) {
|
||||
in.Model.Where("tenant_id", userInfo.TenantId)
|
||||
}
|
||||
if !g.IsEmpty(userInfo.UserName) {
|
||||
in.Model.Where("creator", userInfo.UserName)
|
||||
in.Model.Where("updater", userInfo.UserName)
|
||||
}
|
||||
|
||||
//switch data := in.Data.(type) {
|
||||
//case gdb.Map:
|
||||
// if !g.IsEmpty(userInfo.UserId) {
|
||||
// if _, ok := data["updater"]; !ok {
|
||||
// data["updater"] = userInfo.UserId
|
||||
// }
|
||||
// }
|
||||
//case gdb.List:
|
||||
// for i := range data {
|
||||
// if !g.IsEmpty(userInfo.UserId) {
|
||||
// if _, ok := data[i]["updater"]; !ok {
|
||||
// data[i]["updater"] = userInfo.UserId
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// 2. 执行更新
|
||||
result, err = in.Next(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 清除相关缓存
|
||||
prefix := getCachePrefix(ctx)
|
||||
if prefix != "" {
|
||||
deleteCacheByPattern(ctx, prefix+":*")
|
||||
glog.Debugf(ctx, "[Hook] Cache cleared after update, prefix: %s", prefix)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ==================== Delete钩子 ====================
|
||||
|
||||
func deleteHook(ctx context.Context, in *gdb.HookDeleteInput) (result sql.Result, err error) {
|
||||
// 1. 执行删除
|
||||
result, err = in.Next(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 清除相关缓存
|
||||
prefix := getCachePrefix(ctx)
|
||||
if prefix != "" {
|
||||
deleteCacheByPattern(ctx, prefix+":*")
|
||||
glog.Debugf(ctx, "[Hook] Cache cleared after delete, prefix: %s", prefix)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ==================== Select钩子(缓存读取) ====================
|
||||
|
||||
func selectHook(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
|
||||
|
||||
userInfo, _ := utils.GetUserInfo(ctx)
|
||||
|
||||
if !isSkipTenant(ctx) && !g.IsEmpty(userInfo.TenantId) {
|
||||
in.Model.Where("tenant_id", userInfo.TenantId)
|
||||
}
|
||||
|
||||
// 未启用缓存,直接执行查询
|
||||
if !isCacheEnabled(ctx) {
|
||||
return in.Next(ctx)
|
||||
}
|
||||
|
||||
prefix := getCachePrefix(ctx)
|
||||
if prefix == "" {
|
||||
return in.Next(ctx)
|
||||
}
|
||||
|
||||
// 从 SQL 字符串中提取 WHERE 条件部分
|
||||
whereCondition := extractWhereCondition(in.Sql)
|
||||
|
||||
// 构建缓存key:prefix:table:where条件:args
|
||||
cacheKey := buildCacheKey(prefix, in.Table, whereCondition, in.Args)
|
||||
|
||||
glog.Debugf(ctx, "[Hook] Cache key: %s", cacheKey)
|
||||
|
||||
// 1. 先查缓存
|
||||
if data, ok := getFromCache(ctx, cacheKey); ok {
|
||||
var records gdb.Result
|
||||
if err := json.Unmarshal(data, &records); err == nil && len(records) > 0 {
|
||||
glog.Debugf(ctx, "[Hook] Cache hit for key: %s", cacheKey)
|
||||
return records, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 执行数据库查询
|
||||
result, err = in.Next(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 写入缓存
|
||||
if len(result) > 0 {
|
||||
if data, err := json.Marshal(result); err == nil {
|
||||
setToCache(ctx, cacheKey, data)
|
||||
glog.Debugf(ctx, "[Hook] Cache set for key: %s", cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// extractWhereCondition 从 SQL 语句中提取 WHERE 条件部分
|
||||
func extractWhereCondition(sql string) string {
|
||||
// 查找 WHERE 关键字(不区分大小写)
|
||||
whereIndex := gstr.PosI(sql, " WHERE ")
|
||||
if whereIndex == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 提取 WHERE 之后的内容
|
||||
whereClause := sql[whereIndex+7:]
|
||||
|
||||
// 移除 ORDER BY, GROUP BY, HAVING, LIMIT 等后续子句
|
||||
for _, keyword := range []string{" ORDER BY ", " GROUP BY ", " HAVING ", " LIMIT ", " FOR UPDATE"} {
|
||||
if idx := gstr.PosI(whereClause, keyword); idx != -1 {
|
||||
whereClause = whereClause[:idx]
|
||||
}
|
||||
}
|
||||
|
||||
return whereClause
|
||||
}
|
||||
|
||||
// ==================== 快捷方法 ====================
|
||||
|
||||
type gfdb interface {
|
||||
Model(tableNameOrStruct ...any) *Model
|
||||
}
|
||||
type cache interface {
|
||||
Cache(ctx context.Context) *gdb.Model
|
||||
}
|
||||
type Model struct {
|
||||
*gdb.Model
|
||||
}
|
||||
|
||||
type DataBase struct {
|
||||
gdb.DB
|
||||
DbName string
|
||||
}
|
||||
|
||||
func DB(dbName string) gfdb {
|
||||
return &DataBase{
|
||||
DB: g.DB(dbName),
|
||||
DbName: dbName,
|
||||
}
|
||||
}
|
||||
func (d *DataBase) Model(tableNameOrStruct ...any) *Model {
|
||||
return &Model{
|
||||
Model: d.DB.Model(tableNameOrStruct...),
|
||||
}
|
||||
}
|
||||
func (d *Model) Cache(ctx context.Context) *gdb.Model {
|
||||
ctx = context.WithValue(ctx, ctxKeyCachePrefix, true)
|
||||
return d.Model
|
||||
}
|
||||
219
dao/base/interceptor.go
Normal file
219
dao/base/interceptor.go
Normal file
@@ -0,0 +1,219 @@
|
||||
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 方法来实现无感知集成")
|
||||
}
|
||||
Reference in New Issue
Block a user