audit-demo
This commit is contained in:
parent
5152756185
commit
68331edd3e
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,5 +13,6 @@ func bizModel() error {
|
|||
}
|
||||
LocalDb := global.GetGlobalDBByDBName("Local")
|
||||
LocalDb.AutoMigrate(gvapp.Order{})
|
||||
RegisterHooks(LocalDb)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -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 "<nil>"
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(v)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
if val.IsNil() {
|
||||
return "<nil>"
|
||||
}
|
||||
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() // 初始化监控配置缓存
|
||||
}
|
|
@ -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))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -13,6 +13,7 @@ type ServiceGroup struct {
|
|||
DictionaryService
|
||||
SystemConfigService
|
||||
OperationRecordService
|
||||
MonitorService
|
||||
DictionaryDetailService
|
||||
AuthorityBtnService
|
||||
SysExportTemplateService
|
||||
|
|
|
@ -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]
|
||||
}
|
Loading…
Reference in New Issue