audit-demo

This commit is contained in:
SysProChan 2025-04-14 20:25:19 +08:00
parent 5152756185
commit 68331edd3e
10 changed files with 419 additions and 0 deletions

View File

@ -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
}
}

View File

@ -13,5 +13,6 @@ func bizModel() error {
}
LocalDb := global.GetGlobalDBByDBName("Local")
LocalDb.AutoMigrate(gvapp.Order{})
RegisterHooks(LocalDb)
return nil
}

View File

@ -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() // 初始化监控配置缓存
}

View File

@ -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))
}
}
}

View File

@ -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"
}

View File

@ -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)
}

View File

@ -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"
}

View File

@ -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]
}

View File

@ -13,6 +13,7 @@ type ServiceGroup struct {
DictionaryService
SystemConfigService
OperationRecordService
MonitorService
DictionaryDetailService
AuthorityBtnService
SysExportTemplateService

View File

@ -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]
}