diff --git a/server/initialize/gorm.go b/server/initialize/gorm.go index e5cdfa7c..618c5cf5 100644 --- a/server/initialize/gorm.go +++ b/server/initialize/gorm.go @@ -1,16 +1,21 @@ package initialize import ( + "fmt" "os" + "reflect" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/example" "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service" "go.uber.org/zap" "gorm.io/gorm" ) +var monitorService = service.ServiceGroupApp.SystemServiceGroup.MonitorService + func Gorm() *gorm.DB { switch global.GVA_CONFIG.System.DbType { case "mysql": @@ -56,6 +61,8 @@ func RegisterTables() { system.Condition{}, system.JoinTemplate{}, system.SysParams{}, + system.MonitorConfig{}, + system.ChangeLog{}, example.ExaFile{}, example.ExaCustomer{}, @@ -69,6 +76,8 @@ func RegisterTables() { } err = bizModel() + RegisterHooks(db) + err = monitorService.InitMonitor() if err != nil { global.GVA_LOG.Error("register biz_table failed", zap.Error(err)) @@ -76,3 +85,85 @@ func RegisterTables() { } global.GVA_LOG.Info("register table success") } + +func RegisterHooks(db *gorm.DB) { + err := db.Callback().Update().Before("gorm:update").Register("monitor:before_update", func(tx *gorm.DB) { + fmt.Println("==========钩子函数被调用") + if tx.Statement == nil || tx.Statement.Schema == nil { + + return + } + + // 获取表名(兼容自定义表名) + tableName := tx.Statement.Schema.Table + fmt.Printf("==========当前表名:%s", tableName) + + // 获取主键名 + pkField := tx.Statement.Schema.PrioritizedPrimaryField + //pkColumn := pkField.DBName + + // 获取主键值 + var pkValue interface{} + + // 方法 1:从 WHERE 条件中解析 + //if whereClause, ok := tx.Statement.Clauses["WHERE"]; ok { + // if whereExpr, ok := whereClause.Expression.(clause.Where); ok { + // for _, expr := range whereExpr.Exprs { + // if eq, ok := expr.(clause.Eq); ok && eq.Column.Name == pkColumn { + // pkValue = eq.Value + // break + // } + // } + // } + //} + + // 方法 2:从模型实例中获取 + if pkValue == nil && tx.Statement.Dest != nil { + dest := reflect.ValueOf(tx.Statement.Dest) + if dest.Kind() == reflect.Ptr { + dest = dest.Elem() + } + if dest.IsValid() { + pkValue = dest.FieldByName(pkField.Name).Interface() + } + } + + if pkValue == nil { + global.GVA_LOG.Warn("无法获取主键值,跳过记录变更", zap.String("table", tableName)) + return + } + // 查询旧记录 + oldModel := reflect.New(tx.Statement.Schema.ModelType).Interface() + if err := tx.Session(&gorm.Session{}).First(oldModel, pkValue).Error; err != nil { + return + } + + // 遍历所有字段,检查是否被监控 + for _, field := range tx.Statement.Schema.Fields { + fieldName := field.DBName + if !monitorService.IsFieldMonitored(tableName, fieldName) { + continue // 跳过未监控字段 + } + + // 对比新旧值 + oldVal := getFieldValue(oldModel, field.Name) + newVal := getFieldValue(tx.Statement.Dest, field.Name) + fmt.Printf("oldVal:%v;newVal:%v\n", oldVal, newVal) + if !reflect.DeepEqual(oldVal, newVal) { + // 记录日志 + logEntry := system.ChangeLog{ + Table: tableName, + Column: fieldName, + OldValue: toString(oldVal), + NewValue: toString(newVal), + } + fmt.Println("logEntry:", logEntry) + global.GVA_DB.Debug().Model(system.ChangeLog{}).Create(&logEntry) + } + } + }) + if err != nil { + fmt.Println("register hook failed%%%%%%%%%%%%%%", zap.Error(err)) + return + } +} diff --git a/server/initialize/gorm_biz.go b/server/initialize/gorm_biz.go index f4643a75..031bd489 100644 --- a/server/initialize/gorm_biz.go +++ b/server/initialize/gorm_biz.go @@ -13,5 +13,6 @@ func bizModel() error { } LocalDb := global.GetGlobalDBByDBName("Local") LocalDb.AutoMigrate(gvapp.Order{}) + RegisterHooks(LocalDb) return nil } diff --git a/server/initialize/monitor.go b/server/initialize/monitor.go new file mode 100644 index 00000000..8e6851d8 --- /dev/null +++ b/server/initialize/monitor.go @@ -0,0 +1,117 @@ +package initialize + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/monitor_service" + "gorm.io/gorm" + "reflect" + "time" +) + +// 注册全局 GORM 钩子 +func RegisterGlobalHooks(db *gorm.DB) { + // 注册 Update 前的全局钩子 + err := db.Callback().Update().Before("gorm:update").Register("monitor:before_update", func(tx *gorm.DB) { + if tx.Statement == nil || tx.Statement.Schema == nil { + return + } + + // 获取表名(兼容自定义表名) + tableName := tx.Statement.Schema.Table + + // 检查是否有监控配置(通过缓存) + monitor_service.CacheMutex.RLock() + monitoredFields, ok := monitor_service.MonitorCache[tableName] + monitor_service.CacheMutex.RUnlock() + if !ok || len(monitoredFields) == 0 { + return + } + + // 获取主键值 + pkField := tx.Statement.Schema.PrioritizedPrimaryField + if pkField == nil { + return + } + pkValue, ok := pkField.ValueOf(tx.Statement.Context, tx.Statement.ReflectValue) + if !ok { + return + } + + // 查询旧记录 + oldModel := reflect.New(tx.Statement.Schema.ModelType).Interface() + if err := tx.Session(&gorm.Session{}).First(oldModel, pkValue).Error; err != nil { + return + } + + // 遍历监控字段,对比新旧值 + for field := range monitoredFields { + oldVal := getFieldValue(oldModel, field) + newVal := getFieldValue(tx.Statement.Dest, field) + if oldVal != newVal { + // 记录变更日志 + logEntry := system.SysRecordLog{ + SysTableName: tableName, + FieldName: field, + OldValue: toString(oldVal), + NewValue: toString(newVal), + CreateTime: time.Now(), + } + tx.Create(&logEntry) + } + } + }) + if err != nil { + return + } +} + +// 辅助函数:获取字段值(处理指针) +func getFieldValue(model interface{}, field string) interface{} { + val := reflect.ValueOf(model) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + fieldVal := val.FieldByName(field) + if !fieldVal.IsValid() { + return nil + } + + if fieldVal.Kind() == reflect.Ptr || fieldVal.Kind() == reflect.Interface { + if fieldVal.IsNil() { + return nil + } + return fieldVal.Elem().Interface() + } + + return fieldVal.Interface() +} + +// 辅助函数:安全转换为字符串 +func toString(v interface{}) string { + if v == nil { + return "" + } + + val := reflect.ValueOf(v) + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return "" + } + return fmt.Sprintf("%v", val.Elem().Interface()) + } + + return fmt.Sprintf("%v", v) +} + +// 在初始化数据库时调用 +func MysqlTables(db *gorm.DB) { + db.AutoMigrate( + // ... 其他表 + system.SysMonitorConfig{}, + system.SysRecordLog{}, + ) + RegisterGlobalHooks(db) + monitor_service.InitMonitorCache() // 初始化监控配置缓存 +} diff --git a/server/middleware/operation.go b/server/middleware/operation.go index f34cf68e..ea5fee32 100644 --- a/server/middleware/operation.go +++ b/server/middleware/operation.go @@ -21,6 +21,7 @@ import ( ) var operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService +var monitorService = service.ServiceGroupApp.SystemServiceGroup.MonitorService var respPool sync.Pool var bufferSize = 1024 @@ -116,8 +117,17 @@ func OperationRecord() gin.HandlerFunc { } } + fieldMap := make(map[string]interface{}) + // 将body转换为map + err := json.Unmarshal(body, &fieldMap) + if err != nil { + global.GVA_LOG.Error("unmarshal body error:", zap.Error(err)) + } + + err = monitorService.InitMonitor() if err := operationRecordService.CreateSysOperationRecord(record); err != nil { global.GVA_LOG.Error("create operation record error:", zap.Error(err)) + } } } diff --git a/server/model/system/sys_monitor_config.go b/server/model/system/sys_monitor_config.go new file mode 100644 index 00000000..26b8f1cc --- /dev/null +++ b/server/model/system/sys_monitor_config.go @@ -0,0 +1,14 @@ +package system + +import "gorm.io/gorm" + +// 监控配置表(配置需要监听的表和字段) +type SysMonitorConfig struct { + gorm.Model + SysTableName string `gorm:colomn:sys_table_name;uniqueIndex:idx_table_field"` // 唯一索引防止重复配置 + FieldName string `gorm:fcolomn:field_name;uniqueIndex:idx_table_field"` +} + +func (s *SysMonitorConfig) TableName() string { + return "sys_monitor_configs" +} diff --git a/server/model/system/sys_operation_record.go b/server/model/system/sys_operation_record.go index 3d201d30..8584a801 100644 --- a/server/model/system/sys_operation_record.go +++ b/server/model/system/sys_operation_record.go @@ -21,4 +21,44 @@ type SysOperationRecord struct { Resp string `json:"resp" form:"resp" gorm:"type:text;column:resp;comment:响应Body"` // 响应Body UserID int `json:"user_id" form:"user_id" gorm:"column:user_id;comment:用户id"` // 用户id User SysUser `json:"user"` + //SysTableName string `json:"sys_table_name" form:"sys_table_name" gorm:"column:sys_table_name;comment:系统表名"` + //FieldRecords []SysFieldRecord `json:"field_records" gorm:"foreignKey:OperationRecordID;references:ID"` +} + +//type SysFieldRecord struct { +// global.GVA_MODEL +// OperationRecordID int `json:"operation_record_id"` // 操作记录id +// OperationType string `json:"operation_type"` // 操作类型 +// FieldName string `json:"field_name"` +// OldValue string `json:"old_value"` +// NewValue string `json:"new_value"` +//} + +// BeforeUpdate 钩子:记录旧值 +//func (s *SysOperationRecord) BeforeUpdate(tx *gorm.DB) (err error) { +// var oldModel SysOperationRecord +// // 查询数据库中的旧值 +// tx.Model(s).Last(&oldModel) +// return nil +//} + +type MonitorConfig struct { + global.GVA_MODEL + BusinessDB string `json:"business_db" gorm:"column:business_db;comment:业务数据库"` // 业务数据库 + Database string `json:"database" gorm:"column:database;comment:数据库名"` + Table string `json:"table" gorm:"column:table;comment:表名"` + Columns []string `json:"columns" gorm:"type:text;column:columns;comment:字段列表(json数组)"` + IsEnabled bool `json:"is_enabled" gorm:"column:is_enabled;comment:是否启用"` +} + +type ChangeLog struct { + global.GVA_MODEL + Database string `json:"database"` // 数据库名 + Table string `json:"table"` // 表名 + Column string `json:"column"` // 字段名 + OldValue string `json:"old_value"` // 旧值(json) + NewValue string `json:"new_value"` // 新值(json) + RecordID uint `json:"record_id"` // 记录ID + ChangedAt time.Time `json:"changed_at"` // 变更时间 + OperType string `json:"oper_type"` // 操作类型(U/P/D) } diff --git a/server/model/system/sys_record_log.go b/server/model/system/sys_record_log.go new file mode 100644 index 00000000..d8df2efb --- /dev/null +++ b/server/model/system/sys_record_log.go @@ -0,0 +1,20 @@ +package system + +import ( + "gorm.io/gorm" + "time" +) + +// 字段变更记录表 +type SysRecordLog struct { + gorm.Model + SysTableName string `` + FieldName string `gorm:"comment:字段名"` + OldValue string `gorm:"comment:旧值;type:text"` + NewValue string `gorm:"comment:新值;type:text"` + CreateTime time.Time `gorm:"comment:创建时间;autoCreateTime"` +} + +func (s *SysRecordLog) TableName() string { + return "sys_record_logs" +} diff --git a/server/service/monitor_service/monitor.go b/server/service/monitor_service/monitor.go new file mode 100644 index 00000000..a562b058 --- /dev/null +++ b/server/service/monitor_service/monitor.go @@ -0,0 +1,47 @@ +package monitor_service + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "sync" +) + +var ( + MonitorCache = make(map[string]map[string]bool) // 缓存结构: table -> field -> exists + CacheMutex sync.RWMutex +) + +// 初始化缓存(在应用启动时调用) +func InitMonitorCache() { + refreshCache() +} + +// 刷新缓存(定时任务或手动触发) +func refreshCache() { + var configs []system.SysMonitorConfig + global.GVA_DB.Find(&configs) + + newCache := make(map[string]map[string]bool) + for _, c := range configs { + if newCache[c.SysTableName] == nil { + newCache[c.SysTableName] = make(map[string]bool) + } + newCache[c.SysTableName][c.FieldName] = true + } + + CacheMutex.Lock() + MonitorCache = newCache + CacheMutex.Unlock() +} + +// 检查字段是否被监控 +func IsFieldMonitored(table, field string) bool { + CacheMutex.RLock() + defer CacheMutex.RUnlock() + + fields, ok := MonitorCache[table] + if !ok { + return false + } + return fields[field] +} diff --git a/server/service/system/enter.go b/server/service/system/enter.go index 634cd001..cecfb1c1 100644 --- a/server/service/system/enter.go +++ b/server/service/system/enter.go @@ -13,6 +13,7 @@ type ServiceGroup struct { DictionaryService SystemConfigService OperationRecordService + MonitorService DictionaryDetailService AuthorityBtnService SysExportTemplateService diff --git a/server/service/system/monitor.go b/server/service/system/monitor.go new file mode 100644 index 00000000..b290c7a9 --- /dev/null +++ b/server/service/system/monitor.go @@ -0,0 +1,78 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "sync" +) + +type MonitorService struct { +} + +var monitorService = new(MonitorService) + +var autoCodeService = new(AutoCodeService) +var ( + MonitorCache = make(map[string]map[string]bool) // 缓存结构: table -> field -> exists + CacheMutex sync.RWMutex +) + +// 初始化监控配置 +func (s *MonitorService) InitMonitor() (err error) { + // var configs []system.MonitorConfig + // err = global.GVA_DB.Find(&configs).Error + // if err != nil { + // return + // } + configs := []system.MonitorConfig{ + { + BusinessDB: "Local", + Database: "gvapp", + Table: "sample_order", + Columns: []string{"customer_code", "demand_quantity"}, + IsEnabled: true, + }, + { + BusinessDB: "Local", + Database: "gvapp", + Table: "customer", + Columns: []string{"code", "name"}, + IsEnabled: true, + }, + } + s.refreshCache(configs) + + return nil +} + +func (s *MonitorService) refreshCache(configs []system.MonitorConfig) { + newCache := make(map[string]map[string]bool) + for _, c := range configs { + if !c.IsEnabled { + continue + } + if newCache[c.Table] == nil { + newCache[c.Table] = make(map[string]bool) + } + for _, col := range c.Columns { + if col != "" { + newCache[c.Table][col] = true + } + } + } + + CacheMutex.Lock() + MonitorCache = newCache + CacheMutex.Unlock() +} + +// IsFieldMonitored 检查字段是否被监控 +func (s *MonitorService) IsFieldMonitored(table, field string) bool { + CacheMutex.RLock() + defer CacheMutex.RUnlock() + + fields, ok := MonitorCache[table] + if !ok { + return false + } + return fields[field] +}