From e780e16e52485b2f9100621e0d38698e03d39cef Mon Sep 17 00:00:00 2001 From: SysProChan Date: Mon, 28 Apr 2025 23:19:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=8F=98=E6=9B=B4?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=9B=91=E6=8E=A7=E4=B8=8EAPI=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/plugin/monitor/api/change_log.go | 206 +++++++ server/plugin/monitor/api/enter.go | 6 +- server/plugin/monitor/gen/gen.go | 13 +- server/plugin/monitor/initialize/api.go | 18 +- server/plugin/monitor/initialize/gorm.go | 156 ++++- server/plugin/monitor/initialize/menu.go | 37 +- server/plugin/monitor/initialize/router.go | 1 + server/plugin/monitor/model/change_log.go | 25 + server/plugin/monitor/model/monitor_config.go | 22 + .../monitor/model/request/change_log.go | 20 + server/plugin/monitor/plugin.go | 5 +- server/plugin/monitor/router/change_log.go | 31 + server/plugin/monitor/router/enter.go | 6 +- server/plugin/monitor/service/change_log.go | 148 +++++ server/plugin/monitor/service/enter.go | 5 +- .../plugin/monitor/service/monitor_config.go | 74 ++- web/src/plugin/monitor/api/changeLog.js | 122 ++++ web/src/plugin/monitor/form/changeLog.vue | 143 +++++ web/src/plugin/monitor/view/changeLog.vue | 574 ++++++++++++++++++ web/src/plugin/monitor/view/monitorConfig.vue | 29 +- 20 files changed, 1620 insertions(+), 21 deletions(-) create mode 100644 server/plugin/monitor/api/change_log.go create mode 100644 server/plugin/monitor/model/change_log.go create mode 100644 server/plugin/monitor/model/request/change_log.go create mode 100644 server/plugin/monitor/router/change_log.go create mode 100644 server/plugin/monitor/service/change_log.go create mode 100644 web/src/plugin/monitor/api/changeLog.js create mode 100644 web/src/plugin/monitor/form/changeLog.vue create mode 100644 web/src/plugin/monitor/view/changeLog.vue diff --git a/server/plugin/monitor/api/change_log.go b/server/plugin/monitor/api/change_log.go new file mode 100644 index 00000000..3eb81520 --- /dev/null +++ b/server/plugin/monitor/api/change_log.go @@ -0,0 +1,206 @@ +package api + +import ( + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/monitor/model" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/monitor/model/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +var ChangeLog = new(CL) + +type CL struct {} + +// CreateChangeLog 创建变更日志 +// @Tags ChangeLog +// @Summary 创建变更日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.ChangeLog true "创建变更日志" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /CL/createChangeLog [post] +func (a *CL) CreateChangeLog(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var info model.ChangeLog + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = serviceChangeLog.CreateChangeLog(ctx,&info) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败:" + err.Error(), c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteChangeLog 删除变更日志 +// @Tags ChangeLog +// @Summary 删除变更日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.ChangeLog true "删除变更日志" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /CL/deleteChangeLog [delete] +func (a *CL) DeleteChangeLog(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + ID := c.Query("ID") + err := serviceChangeLog.DeleteChangeLog(ctx,ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteChangeLogByIds 批量删除变更日志 +// @Tags ChangeLog +// @Summary 批量删除变更日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /CL/deleteChangeLogByIds [delete] +func (a *CL) DeleteChangeLogByIds(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + IDs := c.QueryArray("IDs[]") + err := serviceChangeLog.DeleteChangeLogByIds(ctx,IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateChangeLog 更新变更日志 +// @Tags ChangeLog +// @Summary 更新变更日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.ChangeLog true "更新变更日志" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /CL/updateChangeLog [put] +func (a *CL) UpdateChangeLog(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var info model.ChangeLog + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = serviceChangeLog.UpdateChangeLog(ctx,info) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败:" + err.Error(), c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindChangeLog 用id查询变更日志 +// @Tags ChangeLog +// @Summary 用id查询变更日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param ID query uint true "用id查询变更日志" +// @Success 200 {object} response.Response{data=model.ChangeLog,msg=string} "查询成功" +// @Router /CL/findChangeLog [get] +func (a *CL) FindChangeLog(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + ID := c.Query("ID") + reCL, err := serviceChangeLog.GetChangeLog(ctx,ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(reCL, c) +} +// GetChangeLogList 分页获取变更日志列表 +// @Tags ChangeLog +// @Summary 分页获取变更日志列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query request.ChangeLogSearch true "分页获取变更日志列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /CL/getChangeLogList [get] +func (a *CL) GetChangeLogList(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var pageInfo request.ChangeLogSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := serviceChangeLog.GetChangeLogInfoList(ctx,pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:" + err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} +// GetChangeLogDataSource 获取ChangeLog的数据源 +// @Tags ChangeLog +// @Summary 获取ChangeLog的数据源 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "查询成功" +// @Router /CL/getChangeLogDataSource [get] +func (a *CL) GetChangeLogDataSource(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口为获取数据源定义的数据 + dataSource, err := serviceChangeLog.GetChangeLogDataSource(ctx) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(dataSource, c) +} +// GetChangeLogPublic 不需要鉴权的变更日志接口 +// @Tags ChangeLog +// @Summary 不需要鉴权的变更日志接口 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /CL/getChangeLogPublic [get] +func (a *CL) GetChangeLogPublic(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口不需要鉴权 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + serviceChangeLog.GetChangeLogPublic(ctx) + response.OkWithDetailed(gin.H{"info": "不需要鉴权的变更日志接口信息"}, "获取成功", c) +} diff --git a/server/plugin/monitor/api/enter.go b/server/plugin/monitor/api/enter.go index aaa55e7b..fa238248 100644 --- a/server/plugin/monitor/api/enter.go +++ b/server/plugin/monitor/api/enter.go @@ -5,6 +5,10 @@ import "github.com/flipped-aurora/gin-vue-admin/server/plugin/monitor/service" var ( Api = new(api) serviceMonitorConfig = service.Service.MonitorConfig + serviceChangeLog = service.Service.ChangeLog ) -type api struct{ MonitorConfig MC } +type api struct { + MonitorConfig MC + ChangeLog CL +} diff --git a/server/plugin/monitor/gen/gen.go b/server/plugin/monitor/gen/gen.go index d5aa1dd0..6509fd32 100644 --- a/server/plugin/monitor/gen/gen.go +++ b/server/plugin/monitor/gen/gen.go @@ -1,17 +1,18 @@ package main import ( - "gorm.io/gen" - "path/filepath" //go:generate go mod tidy - //go:generate go mod download - //go:generate go run gen.go "github.com/flipped-aurora/gin-vue-admin/server/plugin/monitor/model" + "gorm.io/gen" + "path/filepath" ) func main() { g := gen.NewGenerator(gen.Config{OutPath: filepath.Join("..", "..", "..", "monitor", "blender", "model", "dao"), Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface}) - g.ApplyBasic( - new(model.MonitorConfig), + g.ApplyBasic(new(model.MonitorConfig), //go:generate go mod tidy + //go:generate go mod download + //go:generate go run gen.go + + new(model.ChangeLog), ) g.Execute() } diff --git a/server/plugin/monitor/initialize/api.go b/server/plugin/monitor/initialize/api.go index adf5d64c..6304ad1c 100644 --- a/server/plugin/monitor/initialize/api.go +++ b/server/plugin/monitor/initialize/api.go @@ -2,11 +2,25 @@ package initialize import ( "context" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils" ) func Api(ctx context.Context) { - entities := []model.SysApi{} + entities := []model.SysApi{ + {ApiGroup: "MC", Path: "/MC/getMonitorConfigList", Description: "获取配置列表", Method: "GET"}, + {ApiGroup: "MC", Path: "/MC/findMonitorConfig", Description: "查找配置", Method: "GET"}, + {ApiGroup: "MC", Path: "/MC/createMonitorConfig", Description: "创建配置", Method: "POST"}, + {ApiGroup: "MC", Path: "/MC/deleteMonitorConfig", Description: "删除配置", Method: "DELETE"}, + {ApiGroup: "MC", Path: "/MC/deleteMonitorConfigByIds", Description: "批量删除配置", Method: "DELETE"}, + {ApiGroup: "MC", Path: "/MC/updateMonitorConfig", Description: "更新配置", Method: "PUT"}, + {ApiGroup: "CL", Path: "/CL/getChangeLogList", Description: "获取变更日志列表", Method: "GET"}, + {ApiGroup: "CL", Path: "/CL/findChangeLog", Description: "查找变更日志", Method: "GET"}, + {ApiGroup: "CL", Path: "/CL/createChangeLog", Description: "创建变更日志", Method: "POST"}, + {ApiGroup: "CL", Path: "/CL/deleteChangeLog", Description: "删除变更日志", Method: "DELETE"}, + {ApiGroup: "CL", Path: "/CL/deleteChangeLogByIds", Description: "批量删除变更日志", Method: "DELETE"}, + {ApiGroup: "CL", Path: "/CL/updateChangeLog", Description: "更新变更日志", Method: "PUT"}, + } utils.RegisterApis(entities...) -} +} \ No newline at end of file diff --git a/server/plugin/monitor/initialize/gorm.go b/server/plugin/monitor/initialize/gorm.go index 95116095..65b1c3e2 100644 --- a/server/plugin/monitor/initialize/gorm.go +++ b/server/plugin/monitor/initialize/gorm.go @@ -3,16 +3,170 @@ package initialize import ( "context" "fmt" + "reflect" + "strings" + "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/plugin/monitor/model" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/monitor/service" "github.com/pkg/errors" "go.uber.org/zap" + "gorm.io/gorm" ) +var monitorService service.MC + func Gorm(ctx context.Context) { - err := global.GVA_DB.WithContext(ctx).AutoMigrate(model.MonitorConfig{}) + err := global.GVA_DB.WithContext(ctx).AutoMigrate(model.MonitorConfig{}, model.ChangeLog{}) if err != nil { err = errors.Wrap(err, "注册表失败!") zap.L().Error(fmt.Sprintf("%+v", err)) } + RegisterHooks(ctx, global.GVA_DB) + for _, db := range global.GVA_DBList { + RegisterHooks(ctx, db) + } + monitorService.InitMonitor() +} + +func RegisterHooks(ctx context.Context, 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("tableName:%s \n", tableName) + + // 获取时间过期缓存 + ExpireCaChe := monitorService.RefreshExipre() + + // 判断是否过期 + if !ExpireCaChe[tableName] { + fmt.Printf("tableName:%s 不在有效期 \n", tableName) + return + } + // 跳过系统表监控 + if strings.HasPrefix(tableName, "sys_") || strings.HasPrefix(tableName, "exa_") { + return + } + // 获取主键名 + pkField := tx.Statement.Schema.PrioritizedPrimaryField + + // 获取主键值 + var pkValue any + + // 从模型实例中获取 + if tx.Statement.Dest != nil { + dest := reflect.ValueOf(tx.Statement.Dest) + if dest.Kind() == reflect.Ptr { + dest = dest.Elem() + } + if dest.Kind() == reflect.Map { + pkValue = dest.MapIndex(reflect.ValueOf(pkField.Name)) + } + if dest.Kind() == reflect.Struct { + 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 := model.ChangeLog{ + Table: &tableName, + Column: &fieldName, + OldValue: toStringPtr(oldVal), + NewValue: toStringPtr(newVal), + } + fmt.Println("logEntry:", logEntry) + global.GVA_DB.Debug().Model(model.ChangeLog{}).Create(&logEntry) + } + } + }) + if err != nil { + fmt.Println("register hook failed%%%%%%%%%%%%%%", zap.Error(err)) + return + } +} + +// 辅助函数:获取字段值(处理指针) +func getFieldValue(model interface{}, field string) interface{} { + val := reflect.ValueOf(model) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + if val.Kind() == reflect.Map { + mapVal := val.Interface().(map[string]interface{}) + if v, ok := mapVal[field]; ok { + if v != nil { + return val + } + return nil + } + // return val.MapIndex(reflect.ValueOf(field)) + } + + 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 toStringPtr(v interface{}) (strPrt *string) { + if v == nil { + return nil + } + + val := reflect.ValueOf(v) + + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return nil + } + return val.Elem().Interface().(*string) + } + if val.Kind() == reflect.String { + str := val.String() + return &str + } + if val.Kind() == reflect.Uint { + str := fmt.Sprintf("%d", val.Uint()) + return &str + } + str := fmt.Sprintf("%v", v) + return &str } diff --git a/server/plugin/monitor/initialize/menu.go b/server/plugin/monitor/initialize/menu.go index 06993db3..fac1bc48 100644 --- a/server/plugin/monitor/initialize/menu.go +++ b/server/plugin/monitor/initialize/menu.go @@ -2,11 +2,46 @@ package initialize import ( "context" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils" ) func Menu(ctx context.Context) { - entities := []model.SysBaseMenu{} + entities := []model.SysBaseMenu{ + { + Hidden: false, + Path: "changeMonitor", + Name: "changeMonitor", + Component: "view/routerHolder.vue", + Sort: 99, + Meta: model.Meta{ + Title: "变更监控", + Icon: "camera", + }, + }, + { + Hidden: false, + Path: "MC", + Name: "MC", + Component: "plugin/monitor/view/monitorConfig.vue", + Sort: 1, + Meta: model.Meta{ + Title: "监控配置", + Icon: "monitor", + }, + }, + { + Hidden: false, + Path: "CL", + Name: "CL", + Component: "plugin/monitor/view/changeLog.vue", + Sort: 2, + Meta: model.Meta{ + Title: "变更日志", + Icon: "list", + }, + }, + } utils.RegisterMenus(entities...) } diff --git a/server/plugin/monitor/initialize/router.go b/server/plugin/monitor/initialize/router.go index f89b742a..85e94813 100644 --- a/server/plugin/monitor/initialize/router.go +++ b/server/plugin/monitor/initialize/router.go @@ -13,4 +13,5 @@ func Router(engine *gin.Engine) { private := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") private.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) router.Router.MonitorConfig.Init(public, private) + router.Router.ChangeLog.Init(public, private) } diff --git a/server/plugin/monitor/model/change_log.go b/server/plugin/monitor/model/change_log.go new file mode 100644 index 00000000..da4a509d --- /dev/null +++ b/server/plugin/monitor/model/change_log.go @@ -0,0 +1,25 @@ +package model + +import ( + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// ChangeLog 变更日志 结构体 +type ChangeLog struct { + global.GVA_MODEL + Database *string `json:"database" form:"database" gorm:"column:database;"` //数据库名 + Table *string `json:"table" form:"table" gorm:"column:db_table;"` //表名 + Column *string `json:"column" form:"column" gorm:"column:db_column;"` //字段名 + OldValue *string `json:"oldValue" form:"oldValue" gorm:"column:old_value;"` //旧值 + NewValue *string `json:"newValue" form:"newValue" gorm:"column:new_value;"` //新值 + RecordID *int `json:"recordId" form:"recordId" gorm:"column:record_id;"` //记录ID + ChangedAt *time.Time `json:"changedAt" form:"changedAt" gorm:"column:changed_at;"` //变更时间 + OperationType *string `json:"operationType" form:"operationType" gorm:"column:operation_type;"` //操作类型 +} + +// TableName 变更日志 ChangeLog自定义表名 change_log +func (ChangeLog) TableName() string { + return "change_log" +} diff --git a/server/plugin/monitor/model/monitor_config.go b/server/plugin/monitor/model/monitor_config.go index 0aa8ebe5..ebd9c764 100644 --- a/server/plugin/monitor/model/monitor_config.go +++ b/server/plugin/monitor/model/monitor_config.go @@ -22,3 +22,25 @@ type MonitorConfig struct { func (MonitorConfig) TableName() string { return "monitor_config" } + +func (m *MonitorConfig) IsExpire() bool { + T := time.Now() + // 如果如果只设置了开始时间,则判断开始时间是否在当前时间之前 + if m.StartTime != nil && m.EndTime == nil { + // fmt.Println(*m.Table, "是否生效1:", m.StartTime.Before(T)) + return m.StartTime.Before(T) + } + // 如果只设置了结束时间,则判断结束时间是否在当前时间之后 + if m.StartTime == nil && m.EndTime != nil { + // fmt.Println(*m.Table, "是否生效2:", m.EndTime.After(T)) + return m.EndTime.After(T) + } + // 如果同时设置了开始时间和结束时间,则判断开始时间是否在当前时间之前,结束时间是否在当前时间之后 + if m.StartTime != nil && m.EndTime != nil { + // fmt.Println(*m.Table, "是否生效3:", m.StartTime.Before(T) && m.EndTime.After(T)) + return m.StartTime.Before(T) && m.EndTime.After(T) + } + // fmt.Println(*m.Table, "是否生效4:", true) + return true + +} diff --git a/server/plugin/monitor/model/request/change_log.go b/server/plugin/monitor/model/request/change_log.go new file mode 100644 index 00000000..76258d2e --- /dev/null +++ b/server/plugin/monitor/model/request/change_log.go @@ -0,0 +1,20 @@ + +package request +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "time" +) +type ChangeLogSearch struct{ + StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` + Database *string `json:"database" form:"database" ` + Table *string `json:"table" form:"table" ` + Column *string `json:"column" form:"column" ` + OldValue *string `json:"oldValue" form:"oldValue" ` + StartChangedAt *time.Time `json:"startChangedAt" form:"startChangedAt"` + EndChangedAt *time.Time `json:"endChangedAt" form:"endChangedAt"` + OperationType *string `json:"operationType" form:"operationType" ` + request.PageInfo + Sort string `json:"sort" form:"sort"` + Order string `json:"order" form:"order"` +} \ No newline at end of file diff --git a/server/plugin/monitor/plugin.go b/server/plugin/monitor/plugin.go index 007a820f..d529aaba 100644 --- a/server/plugin/monitor/plugin.go +++ b/server/plugin/monitor/plugin.go @@ -2,6 +2,7 @@ package monitor import ( "context" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/monitor/initialize" interfaces "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin/v2" "github.com/gin-gonic/gin" @@ -20,7 +21,9 @@ type plugin struct{} // 安装插件时候自动注册的api数据请到下方法.Menu方法中实现并添加如下方法 // initialize.Menu(ctx) func (p *plugin) Register(group *gin.Engine) { - ctx := context.Background() + ctx := context.Background() initialize.Gorm(ctx) initialize.Router(group) + initialize.Menu(ctx) + initialize.Api(ctx) } diff --git a/server/plugin/monitor/router/change_log.go b/server/plugin/monitor/router/change_log.go new file mode 100644 index 00000000..d15ac2b1 --- /dev/null +++ b/server/plugin/monitor/router/change_log.go @@ -0,0 +1,31 @@ +package router + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +var ChangeLog = new(CL) + +type CL struct {} + +// Init 初始化 变更日志 路由信息 +func (r *CL) Init(public *gin.RouterGroup, private *gin.RouterGroup) { + { + group := private.Group("CL").Use(middleware.OperationRecord()) + group.POST("createChangeLog", apiChangeLog.CreateChangeLog) // 新建变更日志 + group.DELETE("deleteChangeLog", apiChangeLog.DeleteChangeLog) // 删除变更日志 + group.DELETE("deleteChangeLogByIds", apiChangeLog.DeleteChangeLogByIds) // 批量删除变更日志 + group.PUT("updateChangeLog", apiChangeLog.UpdateChangeLog) // 更新变更日志 + } + { + group := private.Group("CL") + group.GET("findChangeLog", apiChangeLog.FindChangeLog) // 根据ID获取变更日志 + group.GET("getChangeLogList", apiChangeLog.GetChangeLogList) // 获取变更日志列表 + } + { + group := public.Group("CL") + group.GET("getChangeLogDataSource", apiChangeLog.GetChangeLogDataSource) // 获取变更日志数据源 + group.GET("getChangeLogPublic", apiChangeLog.GetChangeLogPublic) // 变更日志开放接口 + } +} diff --git a/server/plugin/monitor/router/enter.go b/server/plugin/monitor/router/enter.go index a12293f9..6de29e8c 100644 --- a/server/plugin/monitor/router/enter.go +++ b/server/plugin/monitor/router/enter.go @@ -5,6 +5,10 @@ import "github.com/flipped-aurora/gin-vue-admin/server/plugin/monitor/api" var ( Router = new(router) apiMonitorConfig = api.Api.MonitorConfig + apiChangeLog = api.Api.ChangeLog ) -type router struct{ MonitorConfig MC } +type router struct { + MonitorConfig MC + ChangeLog CL +} diff --git a/server/plugin/monitor/service/change_log.go b/server/plugin/monitor/service/change_log.go new file mode 100644 index 00000000..538230dc --- /dev/null +++ b/server/plugin/monitor/service/change_log.go @@ -0,0 +1,148 @@ +package service + +import ( + "context" + "encoding/json" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/monitor/model" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/monitor/model/request" +) + +var ChangeLog = new(CL) + +type CL struct{} + +// CreateChangeLog 创建变更日志记录 +// Author [yourname](https://github.com/yourname) +func (s *CL) CreateChangeLog(ctx context.Context, CL *model.ChangeLog) (err error) { + err = global.GVA_DB.Create(CL).Error + return err +} + +// DeleteChangeLog 删除变更日志记录 +// Author [yourname](https://github.com/yourname) +func (s *CL) DeleteChangeLog(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.Delete(&model.ChangeLog{}, "id = ?", ID).Error + return err +} + +// DeleteChangeLogByIds 批量删除变更日志记录 +// Author [yourname](https://github.com/yourname) +func (s *CL) DeleteChangeLogByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.Delete(&[]model.ChangeLog{}, "id in ?", IDs).Error + return err +} + +// UpdateChangeLog 更新变更日志记录 +// Author [yourname](https://github.com/yourname) +func (s *CL) UpdateChangeLog(ctx context.Context, CL model.ChangeLog) (err error) { + err = global.GVA_DB.Model(&model.ChangeLog{}).Where("id = ?", CL.ID).Updates(&CL).Error + return err +} + +// GetChangeLog 根据ID获取变更日志记录 +// Author [yourname](https://github.com/yourname) +func (s *CL) GetChangeLog(ctx context.Context, ID string) (CL model.ChangeLog, err error) { + err = global.GVA_DB.Where("id = ?", ID).First(&CL).Error + return +} + +// GetChangeLogInfoList 分页获取变更日志记录 +// Author [yourname](https://github.com/yourname) +func (s *CL) GetChangeLogInfoList(ctx context.Context, info request.ChangeLogSearch) (list []model.ChangeLog, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&model.ChangeLog{}) + var CLs []model.ChangeLog + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + + if info.Database != nil && *info.Database != "" { + db = db.Where("database = ?", *info.Database) + } + if info.Table != nil && *info.Table != "" { + db = db.Where("db_table = ?", *info.Table) + } + if info.Column != nil && *info.Column != "" { + db = db.Where("db_column = ?", *info.Column) + } + if info.OldValue != nil && *info.OldValue != "" { + db = db.Where("old_value LIKE ?", "%"+*info.OldValue+"%") + } + if info.StartChangedAt != nil && info.EndChangedAt != nil { + db = db.Where("changed_at BETWEEN ? AND ? ", info.StartChangedAt, info.EndChangedAt) + } + if info.OperationType != nil && *info.OperationType != "" { + db = db.Where("operation_type = ?", *info.OperationType) + } + err = db.Count(&total).Error + if err != nil { + return + } + var OrderStr string + orderMap := make(map[string]bool) + orderMap["ID"] = true + orderMap["CreatedAt"] = true + orderMap["changed_at"] = true + if orderMap[info.Sort] { + OrderStr = info.Sort + if info.Order == "descending" { + OrderStr = OrderStr + " desc" + } + db = db.Order(OrderStr) + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + err = db.Find(&CLs).Error + return CLs, total, err +} +func (s *CL) GetChangeLogDataSource(ctx context.Context) (res map[string][]map[string]any, err error) { + res = make(map[string][]map[string]any) + + column := make([]map[string]any, 0) + columnOptions := make([]map[string]any, 0) + global.GVA_DB.Table("monitor_config").Where("deleted_at IS NULL").Select("columns").Scan(&column) + for i, v := range column { + var m []string + err := json.Unmarshal([]byte(v["columns"].(string)), &m) + if err != nil { + continue + } + column[i]["columns"] = m + for _, v1 := range m { + columnOptions = append(columnOptions, map[string]any{"label": v1, "value": v1}) + } + } + // 对columnOptions进行去重 + columnOptions = removeDuplicate(columnOptions) + res["column"] = columnOptions + database := make([]map[string]any, 0) + global.GVA_DB.Table("monitor_config").Where("deleted_at IS NULL").Select("DISTINCT database as label, database as value").Scan(&database) + res["database"] = database + table := make([]map[string]any, 0) + global.GVA_DB.Table("monitor_config").Where("deleted_at IS NULL").Select("DISTINCT db_table as label,db_table as value").Scan(&table) + res["table"] = table + return +} + +func (s *CL) GetChangeLogPublic(ctx context.Context) { + +} +func removeDuplicate(slice []map[string]any) []map[string]any { + seen := make(map[string]bool) + result := []map[string]any{} + for _, v := range slice { + label, ok := v["label"].(string) + if ok && !seen[label] { + seen[label] = true + result = append(result, v) + } + } + return result +} diff --git a/server/plugin/monitor/service/enter.go b/server/plugin/monitor/service/enter.go index d761d1c3..c4755e69 100644 --- a/server/plugin/monitor/service/enter.go +++ b/server/plugin/monitor/service/enter.go @@ -2,4 +2,7 @@ package service var Service = new(service) -type service struct{ MonitorConfig MC } +type service struct { + MonitorConfig MC + ChangeLog CL +} diff --git a/server/plugin/monitor/service/monitor_config.go b/server/plugin/monitor/service/monitor_config.go index e92687b4..4260c971 100644 --- a/server/plugin/monitor/service/monitor_config.go +++ b/server/plugin/monitor/service/monitor_config.go @@ -3,12 +3,18 @@ package service import ( "context" "fmt" + "sync" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/plugin/monitor/model" "github.com/flipped-aurora/gin-vue-admin/server/plugin/monitor/model/request" ) +var ( + MonitorCache = make(map[string]map[string]bool) // 缓存结构: table -> field -> exists + CacheMutex sync.RWMutex + ConfigList = []model.MonitorConfig{} +) var MonitorConfig = new(MC) type MC struct{} @@ -29,6 +35,7 @@ func (s *MC) CreateMonitorConfig(ctx context.Context, MC *model.MonitorConfig) ( } err = global.GVA_DB.Create(MC).Error + go s.InitMonitor() return err } @@ -60,7 +67,17 @@ func (s *MC) UpdateMonitorConfig(ctx context.Context, MC model.MonitorConfig) (e if num > 0 { return fmt.Errorf("当前【业务库%s-数据库%s-表名%s】已存在", *MC.BusinessDB, *MC.Database, *MC.Table) } - err = global.GVA_DB.Model(&model.MonitorConfig{}).Where("id = ?", MC.ID).Updates(&MC).Error + err = global.GVA_DB.Model(&model.MonitorConfig{}).Debug().Where("id = ?", MC.ID).Updates(&MC).Error + + db := global.GVA_DB.Model(&model.MonitorConfig{}).Where("id = ?", MC.ID) + // 如果开始时间/结束时间的字段为空,则强制更新为nil + if MC.StartTime == nil { + db.Updates(map[string]interface{}{"start_time": nil}) + } + if MC.EndTime == nil { + db.Updates(map[string]interface{}{"end_time": nil}) + } + go s.InitMonitor() return err } @@ -98,3 +115,58 @@ func (s *MC) GetMonitorConfigInfoList(ctx context.Context, info request.MonitorC func (s *MC) GetMonitorConfigPublic(ctx context.Context) { } + +// 初始化监控配置 +func (s *MC) InitMonitor() (err error) { + var configs []model.MonitorConfig + + if err = global.GVA_DB.Find(&configs).Error; err != nil { + return + } + s.refreshCache(configs) + fmt.Println("configs refreshed success") + ConfigList = configs + return nil +} + +func (s *MC) refreshCache(configs []model.MonitorConfig) { + newCache := make(map[string]map[string]bool) + for _, c := range configs { + if c.IsEnable == nil || !*c.IsEnable { + 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 *MC) IsFieldMonitored(table, field string) bool { + CacheMutex.RLock() + defer CacheMutex.RUnlock() + + fields, ok := MonitorCache[table] + if !ok { + return false + } + return fields[field] +} + +func (s *MC) RefreshExipre() (newExpire map[string]bool) { + newExpire = make(map[string]bool) + for _, c := range ConfigList { + newExpire[*c.Table] = c.IsExpire() + } + fmt.Println("Expire refreshed success") + return +} diff --git a/web/src/plugin/monitor/api/changeLog.js b/web/src/plugin/monitor/api/changeLog.js new file mode 100644 index 00000000..f354dbf8 --- /dev/null +++ b/web/src/plugin/monitor/api/changeLog.js @@ -0,0 +1,122 @@ +import service from '@/utils/request' +// @Tags ChangeLog +// @Summary 创建变更日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.ChangeLog true "创建变更日志" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /CL/createChangeLog [post] +export const createChangeLog = (data) => { + return service({ + url: '/CL/createChangeLog', + method: 'post', + data + }) +} + +// @Tags ChangeLog +// @Summary 删除变更日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.ChangeLog true "删除变更日志" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /CL/deleteChangeLog [delete] +export const deleteChangeLog = (params) => { + return service({ + url: '/CL/deleteChangeLog', + method: 'delete', + params + }) +} + +// @Tags ChangeLog +// @Summary 批量删除变更日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除变更日志" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /CL/deleteChangeLog [delete] +export const deleteChangeLogByIds = (params) => { + return service({ + url: '/CL/deleteChangeLogByIds', + method: 'delete', + params + }) +} + +// @Tags ChangeLog +// @Summary 更新变更日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.ChangeLog true "更新变更日志" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /CL/updateChangeLog [put] +export const updateChangeLog = (data) => { + return service({ + url: '/CL/updateChangeLog', + method: 'put', + data + }) +} + +// @Tags ChangeLog +// @Summary 用id查询变更日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query model.ChangeLog true "用id查询变更日志" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /CL/findChangeLog [get] +export const findChangeLog = (params) => { + return service({ + url: '/CL/findChangeLog', + method: 'get', + params + }) +} + +// @Tags ChangeLog +// @Summary 分页获取变更日志列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取变更日志列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /CL/getChangeLogList [get] +export const getChangeLogList = (params) => { + return service({ + url: '/CL/getChangeLogList', + method: 'get', + params + }) +} +// @Tags ChangeLog +// @Summary 获取数据源 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /CL/findChangeLogDataSource [get] +export const getChangeLogDataSource = () => { + return service({ + url: '/CL/getChangeLogDataSource', + method: 'get', + }) +} +// @Tags ChangeLog +// @Summary 不需要鉴权的变更日志接口 +// @Accept application/json +// @Produce application/json +// @Param data query request.ChangeLogSearch true "分页获取变更日志列表" +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /CL/getChangeLogPublic [get] +export const getChangeLogPublic = () => { + return service({ + url: '/CL/getChangeLogPublic', + method: 'get', + }) +} diff --git a/web/src/plugin/monitor/form/changeLog.vue b/web/src/plugin/monitor/form/changeLog.vue new file mode 100644 index 00000000..c7e76a5e --- /dev/null +++ b/web/src/plugin/monitor/form/changeLog.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/web/src/plugin/monitor/view/changeLog.vue b/web/src/plugin/monitor/view/changeLog.vue new file mode 100644 index 00000000..f85e9133 --- /dev/null +++ b/web/src/plugin/monitor/view/changeLog.vue @@ -0,0 +1,574 @@ + + + + + diff --git a/web/src/plugin/monitor/view/monitorConfig.vue b/web/src/plugin/monitor/view/monitorConfig.vue index c7a9146f..84181581 100644 --- a/web/src/plugin/monitor/view/monitorConfig.vue +++ b/web/src/plugin/monitor/view/monitorConfig.vue @@ -129,12 +129,12 @@ inactive-text="否" clearable> - + - + @@ -255,8 +255,13 @@ const formData = ref({ startTime: null, endTime: null, }) - - +const setNullValue = (val) => { + if (val === 'startTime') { + formData.value.startTime = null + } else if (val === 'endTime') { + formData.value.endTime = null + } +} const init = () => { getDbFunc() getTableFunc() @@ -310,6 +315,18 @@ const rule = reactive({ trigger: ['input', 'blur'], }, ], + endTime: [ + { + validator: (rule, value, callback) => { + if (formData.value.startTime && value && new Date(value) <= new Date(formData.value.startTime)) { + callback(new Error('结束时间需晚于开始时间')) + } else { + callback() + } + }, + trigger: ['change', 'blur'] + }, + ], }) const searchRule = reactive({