添加操作记录支持
This commit is contained in:
parent
56d50536fc
commit
7098529421
|
@ -0,0 +1,189 @@
|
|||
package aeusadmin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.nobla.cn/golang/aeus-admin/models"
|
||||
"git.nobla.cn/golang/rest"
|
||||
"git.nobla.cn/golang/rest/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type (
|
||||
recorder struct {
|
||||
Domain string `json:"domain"`
|
||||
User string `json:"user"`
|
||||
Module string `json:"module"`
|
||||
Table string `json:"table"`
|
||||
Action string `json:"action"`
|
||||
PrimaryKey any `json:"primary_key"`
|
||||
Diffs []*types.DiffAttr `json:"diffs"`
|
||||
}
|
||||
)
|
||||
|
||||
type ActivityRecorder struct {
|
||||
db *gorm.DB
|
||||
ctx context.Context
|
||||
batchTimeout time.Duration
|
||||
BatchSize int
|
||||
ch chan *models.Activity
|
||||
}
|
||||
|
||||
func (s *ActivityRecorder) onAfterCreate(ctx context.Context, tx *gorm.DB, model any, diff []*types.DiffAttr) {
|
||||
v := ctx.Value(rest.RuntimeScopeKey)
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
runtimeScope, ok := v.(*types.RuntimeScope)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
data := &models.Activity{}
|
||||
data.Uid = runtimeScope.User
|
||||
data.Module = runtimeScope.ModuleName
|
||||
data.Table = runtimeScope.TableName
|
||||
data.Action = types.ScenarioCreate
|
||||
if buf, err := json.Marshal(diff); err == nil {
|
||||
data.Data = string(buf)
|
||||
}
|
||||
select {
|
||||
case s.ch <- data:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ActivityRecorder) onAfterUpdate(ctx context.Context, tx *gorm.DB, model any, diff []*types.DiffAttr) {
|
||||
v := ctx.Value(rest.RuntimeScopeKey)
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
runtimeScope, ok := v.(*types.RuntimeScope)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
data := &models.Activity{}
|
||||
data.Uid = runtimeScope.User
|
||||
data.Module = runtimeScope.ModuleName
|
||||
data.Table = runtimeScope.TableName
|
||||
data.Action = types.ScenarioUpdate
|
||||
if diff == nil {
|
||||
diff = make([]*types.DiffAttr, 0)
|
||||
}
|
||||
diff = append(diff, &types.DiffAttr{
|
||||
Column: "primary_key",
|
||||
NewValue: runtimeScope.PrimaryKeyValue,
|
||||
})
|
||||
if buf, err := json.Marshal(diff); err == nil {
|
||||
data.Data = string(buf)
|
||||
}
|
||||
select {
|
||||
case s.ch <- data:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ActivityRecorder) onAfterDelete(ctx context.Context, tx *gorm.DB, model any) {
|
||||
v := ctx.Value(rest.RuntimeScopeKey)
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
runtimeScope, ok := v.(*types.RuntimeScope)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
data := &models.Activity{}
|
||||
data.Uid = runtimeScope.User
|
||||
data.Module = runtimeScope.ModuleName
|
||||
data.Table = runtimeScope.TableName
|
||||
data.Action = types.ScenarioDelete
|
||||
diff := make([]*types.DiffAttr, 0, 1)
|
||||
diff = append(diff, &types.DiffAttr{
|
||||
Column: "primary_key",
|
||||
NewValue: runtimeScope.PrimaryKeyValue,
|
||||
})
|
||||
if buf, err := json.Marshal(diff); err == nil {
|
||||
data.Data = string(buf)
|
||||
}
|
||||
select {
|
||||
case s.ch <- data:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ActivityRecorder) batchWrite(values []*models.Activity) {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
if len(values) <= 0 {
|
||||
return
|
||||
}
|
||||
if err = s.db.Create(values).Error; err != nil {
|
||||
for _, row := range values {
|
||||
s.db.Create(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ActivityRecorder) writeLoop() {
|
||||
var (
|
||||
values []*models.Activity
|
||||
)
|
||||
timer := time.NewTimer(s.batchTimeout)
|
||||
defer func() {
|
||||
timer.Stop()
|
||||
if len(values) > 0 {
|
||||
s.batchWrite(values)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
if len(values) > 0 {
|
||||
s.batchWrite(values)
|
||||
values = nil
|
||||
}
|
||||
timer.Reset(s.batchTimeout)
|
||||
case msg, ok := <-s.ch:
|
||||
if ok {
|
||||
values = append(values, msg)
|
||||
if len(values) > s.BatchSize {
|
||||
s.batchWrite(values)
|
||||
values = nil
|
||||
timer.Reset(s.batchTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ActivityRecorder) Recoder(scenes ...string) {
|
||||
if len(scenes) == 0 {
|
||||
scenes = []string{types.ScenarioCreate, types.ScenarioUpdate, types.ScenarioDelete}
|
||||
}
|
||||
for _, str := range scenes {
|
||||
switch str {
|
||||
case types.ScenarioCreate:
|
||||
rest.OnAfterCreate(s.onAfterCreate)
|
||||
case types.ScenarioUpdate:
|
||||
rest.OnAfterUpdate(s.onAfterUpdate)
|
||||
case types.ScenarioDelete:
|
||||
rest.OnAfterDelete(s.onAfterDelete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewActivityRecorder(ctx context.Context, db *gorm.DB) *ActivityRecorder {
|
||||
s := &ActivityRecorder{
|
||||
db: db,
|
||||
ctx: ctx,
|
||||
batchTimeout: time.Second,
|
||||
BatchSize: 50,
|
||||
ch: make(chan *models.Activity, 100),
|
||||
}
|
||||
go s.writeLoop()
|
||||
return s
|
||||
}
|
|
@ -34,6 +34,9 @@ func (t *ZH) Menu(model *rest.Model, label string) string {
|
|||
if _, ok := model.Value().Interface().(models.Setting); ok {
|
||||
return "参数设置"
|
||||
}
|
||||
if _, ok := model.Value().Interface().(models.Activity); ok {
|
||||
return "操作记录"
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,10 @@ type (
|
|||
Setting struct {
|
||||
pb.SettingModel
|
||||
}
|
||||
|
||||
Activity struct {
|
||||
pb.ActivityModel
|
||||
}
|
||||
)
|
||||
|
||||
func (m *User) GetMenu() *types.Menu {
|
||||
|
@ -123,3 +127,22 @@ func (m *Setting) GetMenu() *types.Menu {
|
|||
func (m *Setting) ModuleName() string {
|
||||
return "system"
|
||||
}
|
||||
|
||||
func (m *Activity) GetMenu() *types.Menu {
|
||||
return &types.Menu{
|
||||
Name: "SystemActivity",
|
||||
Parent: "System",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Activity) Scenario() []string {
|
||||
return []string{restTypes.ScenarioList}
|
||||
}
|
||||
|
||||
func (m *Activity) ModuleName() string {
|
||||
return "system"
|
||||
}
|
||||
|
||||
func (m *Activity) GetPermission(s string) string {
|
||||
return "system:activity:list"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1188,6 +1188,128 @@ var _ interface {
|
|||
ErrorName() string
|
||||
} = SettingValidationError{}
|
||||
|
||||
// Validate checks the field values on Activity with the rules defined in the
|
||||
// proto definition for this message. If any rules are violated, the first
|
||||
// error encountered is returned, or nil if there are no violations.
|
||||
func (m *Activity) Validate() error {
|
||||
return m.validate(false)
|
||||
}
|
||||
|
||||
// ValidateAll checks the field values on Activity with the rules defined in
|
||||
// the proto definition for this message. If any rules are violated, the
|
||||
// result is a list of violation errors wrapped in ActivityMultiError, or nil
|
||||
// if none found.
|
||||
func (m *Activity) ValidateAll() error {
|
||||
return m.validate(true)
|
||||
}
|
||||
|
||||
func (m *Activity) validate(all bool) error {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errors []error
|
||||
|
||||
// no validation rules for Id
|
||||
|
||||
// no validation rules for CreatedAt
|
||||
|
||||
if l := utf8.RuneCountInString(m.GetUid()); l < 5 || l > 20 {
|
||||
err := ActivityValidationError{
|
||||
field: "Uid",
|
||||
reason: "value length must be between 5 and 20 runes, inclusive",
|
||||
}
|
||||
if !all {
|
||||
return err
|
||||
}
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
||||
// no validation rules for Action
|
||||
|
||||
// no validation rules for Module
|
||||
|
||||
// no validation rules for Table
|
||||
|
||||
// no validation rules for Data
|
||||
|
||||
if len(errors) > 0 {
|
||||
return ActivityMultiError(errors)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActivityMultiError is an error wrapping multiple validation errors returned
|
||||
// by Activity.ValidateAll() if the designated constraints aren't met.
|
||||
type ActivityMultiError []error
|
||||
|
||||
// Error returns a concatenation of all the error messages it wraps.
|
||||
func (m ActivityMultiError) Error() string {
|
||||
msgs := make([]string, 0, len(m))
|
||||
for _, err := range m {
|
||||
msgs = append(msgs, err.Error())
|
||||
}
|
||||
return strings.Join(msgs, "; ")
|
||||
}
|
||||
|
||||
// AllErrors returns a list of validation violation errors.
|
||||
func (m ActivityMultiError) AllErrors() []error { return m }
|
||||
|
||||
// ActivityValidationError is the validation error returned by
|
||||
// Activity.Validate if the designated constraints aren't met.
|
||||
type ActivityValidationError struct {
|
||||
field string
|
||||
reason string
|
||||
cause error
|
||||
key bool
|
||||
}
|
||||
|
||||
// Field function returns field value.
|
||||
func (e ActivityValidationError) Field() string { return e.field }
|
||||
|
||||
// Reason function returns reason value.
|
||||
func (e ActivityValidationError) Reason() string { return e.reason }
|
||||
|
||||
// Cause function returns cause value.
|
||||
func (e ActivityValidationError) Cause() error { return e.cause }
|
||||
|
||||
// Key function returns key value.
|
||||
func (e ActivityValidationError) Key() bool { return e.key }
|
||||
|
||||
// ErrorName returns error name.
|
||||
func (e ActivityValidationError) ErrorName() string { return "ActivityValidationError" }
|
||||
|
||||
// Error satisfies the builtin error interface
|
||||
func (e ActivityValidationError) Error() string {
|
||||
cause := ""
|
||||
if e.cause != nil {
|
||||
cause = fmt.Sprintf(" | caused by: %v", e.cause)
|
||||
}
|
||||
|
||||
key := ""
|
||||
if e.key {
|
||||
key = "key for "
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"invalid %sActivity.%s: %s%s",
|
||||
key,
|
||||
e.field,
|
||||
e.reason,
|
||||
cause)
|
||||
}
|
||||
|
||||
var _ error = ActivityValidationError{}
|
||||
|
||||
var _ interface {
|
||||
Field() string
|
||||
Reason() string
|
||||
Key() bool
|
||||
Cause() error
|
||||
ErrorName() string
|
||||
} = ActivityValidationError{}
|
||||
|
||||
// Validate checks the field values on LabelValue with the rules defined in the
|
||||
// proto definition for this message. If any rules are violated, the first
|
||||
// error encountered is returned, or nil if there are no violations.
|
||||
|
|
|
@ -103,7 +103,7 @@ message Login {
|
|||
};
|
||||
int64 id = 1 [(aeus.field) = {gorm:"primaryKey",comment:"ID"}];
|
||||
int64 created_at = 2 [(aeus.field)={scenarios:"list;search;view;export",comment:"登录时间"}];
|
||||
string uid = 4 [(aeus.field)={gorm:"index;size:20",rule:"required",props:"readonly:update",format:"user",comment: "用户工号"},(validate.rules).string = {min_len: 5, max_len: 20}];
|
||||
string uid = 4 [(aeus.field)={gorm:"index;size:20",rule:"required",props:"readonly:update",format:"user",comment: "用户"},(validate.rules).string = {min_len: 5, max_len: 20}];
|
||||
string ip = 5 [(aeus.field)={gorm:"size:128",scenarios:"list;search;view;export",comment: "登录地址"}];
|
||||
string browser = 6 [(aeus.field)={gorm:"size:128",scenarios:"list;view;export",comment: "浏览器"}];
|
||||
string os = 7 [(aeus.field)={gorm:"size:128",scenarios:"list;view;export",comment: "操作系统"}];
|
||||
|
@ -126,6 +126,20 @@ message Setting {
|
|||
}
|
||||
|
||||
|
||||
// Activity 活动记录
|
||||
message Activity {
|
||||
option (aeus.rest) = {
|
||||
table: "activities"
|
||||
};
|
||||
int64 id = 1 [(aeus.field) = {gorm:"primaryKey",comment:"ID"}];
|
||||
int64 created_at = 2 [(aeus.field)={scenarios:"search;search;view;export",comment:"创建时间"}];
|
||||
string uid = 3 [(aeus.field)={gorm:"index;size:20",rule:"required",props:"readonly:update",format:"user",comment: "用户"},(validate.rules).string = {min_len: 5, max_len: 20}];
|
||||
string action = 4 [(aeus.field)={props:"match:exactly",gorm:"index;size:20;not null;default:''",comment:"行为",enum:"create:新建#198754;update:更新#f09d00;delete:删除#e63757",scenarios:"search;list;create;update;view;export"}];
|
||||
string module = 5 [(aeus.field)={gorm:"size:60;not null;default:''",comment:"模块",scenarios:"search;list;create;update;view;export"}];
|
||||
string table = 6 [(aeus.field)={gorm:"size:60;not null;default:''",comment:"数据",scenarios:"list;create;update;view;export"}];
|
||||
string data = 7 [(aeus.field)={gorm:"size:10240;not null;default:''",comment:"内容",scenarios:"list;create;update;view;export"}];
|
||||
}
|
||||
|
||||
message LabelValue {
|
||||
string label = 1;
|
||||
string value = 2;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go-aeus. DO NOT EDIT.
|
||||
// source: organize.proto
|
||||
// date: 2025-06-17 15:01:15
|
||||
// date: 2025-06-17 18:12:28
|
||||
|
||||
package pb
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go-aeus. DO NOT EDIT.
|
||||
// source: organize.proto
|
||||
// date: 2025-06-17 15:01:15
|
||||
// date: 2025-06-17 18:12:28
|
||||
|
||||
package pb
|
||||
|
||||
|
@ -431,7 +431,7 @@ func NewDepartmentModel() *DepartmentModel {
|
|||
type LoginModel struct {
|
||||
Id int64 `json:"id" yaml:"id" xml:"id" gorm:"primaryKey" comment:"ID"`
|
||||
CreatedAt int64 `json:"created_at" yaml:"createdAt" xml:"createdAt" comment:"登录时间" scenarios:"list;search;view;export"`
|
||||
Uid string `json:"uid" yaml:"uid" xml:"uid" gorm:"index;size:20" comment:"用户工号" format:"user" props:"readonly:update" rule:"required"`
|
||||
Uid string `json:"uid" yaml:"uid" xml:"uid" gorm:"index;size:20" comment:"用户" format:"user" props:"readonly:update" rule:"required"`
|
||||
Ip string `json:"ip" yaml:"ip" xml:"ip" gorm:"size:128" comment:"登录地址" scenarios:"list;search;view;export"`
|
||||
Browser string `json:"browser" yaml:"browser" xml:"browser" gorm:"size:128" comment:"浏览器" scenarios:"list;view;export"`
|
||||
Os string `json:"os" yaml:"os" xml:"os" gorm:"size:128" comment:"操作系统" scenarios:"list;view;export"`
|
||||
|
@ -566,3 +566,71 @@ func (m *SettingModel) FindAll(db *gorm.DB, query any, args ...any) (err error)
|
|||
func NewSettingModel() *SettingModel {
|
||||
return &SettingModel{}
|
||||
}
|
||||
|
||||
type ActivityModel struct {
|
||||
Id int64 `json:"id" yaml:"id" xml:"id" gorm:"primaryKey" comment:"ID"`
|
||||
CreatedAt int64 `json:"created_at" yaml:"createdAt" xml:"createdAt" comment:"创建时间" scenarios:"search;search;view;export"`
|
||||
Uid string `json:"uid" yaml:"uid" xml:"uid" gorm:"index;size:20" comment:"用户" format:"user" props:"readonly:update" rule:"required"`
|
||||
Action string `json:"action" yaml:"action" xml:"action" gorm:"index;size:20;not null;default:''" comment:"行为" scenarios:"search;list;create;update;view;export" props:"match:exactly" enum:"create:新建#198754;update:更新#f09d00;delete:删除#e63757"`
|
||||
Module string `json:"module" yaml:"module" xml:"module" gorm:"size:60;not null;default:''" comment:"模块" scenarios:"search;list;create;update;view;export"`
|
||||
Table string `json:"table" yaml:"table" xml:"table" gorm:"size:60;not null;default:''" comment:"数据" scenarios:"list;create;update;view;export"`
|
||||
Data string `json:"data" yaml:"data" xml:"data" gorm:"size:10240;not null;default:''" comment:"内容" scenarios:"list;create;update;view;export"`
|
||||
}
|
||||
|
||||
func (m *ActivityModel) TableName() string {
|
||||
return "activities"
|
||||
}
|
||||
|
||||
func (m *ActivityModel) FromValue(x *Activity) {
|
||||
m.Id = x.Id
|
||||
m.CreatedAt = x.CreatedAt
|
||||
m.Uid = x.Uid
|
||||
m.Action = x.Action
|
||||
m.Module = x.Module
|
||||
m.Table = x.Table
|
||||
m.Data = x.Data
|
||||
}
|
||||
|
||||
func (m *ActivityModel) ToValue() (x *Activity) {
|
||||
x = &Activity{}
|
||||
x.Id = m.Id
|
||||
x.CreatedAt = m.CreatedAt
|
||||
x.Uid = m.Uid
|
||||
x.Action = m.Action
|
||||
x.Module = m.Module
|
||||
x.Table = m.Table
|
||||
x.Data = m.Data
|
||||
return x
|
||||
}
|
||||
|
||||
func (m *ActivityModel) Create(db *gorm.DB) (err error) {
|
||||
return db.Create(m).Error
|
||||
}
|
||||
|
||||
func (m *ActivityModel) UpdateColumn(db *gorm.DB, column string, value any) (err error) {
|
||||
return db.Model(m).UpdateColumn(column, value).Error
|
||||
}
|
||||
|
||||
func (m *ActivityModel) Save(db *gorm.DB) (err error) {
|
||||
return db.Save(m).Error
|
||||
}
|
||||
|
||||
func (m *ActivityModel) Delete(db *gorm.DB) (err error) {
|
||||
return db.Delete(m).Error
|
||||
}
|
||||
|
||||
func (m *ActivityModel) Find(db *gorm.DB, pk any) (err error) {
|
||||
return db.Where("data=?", pk).First(m).Error
|
||||
}
|
||||
|
||||
func (m *ActivityModel) FindOne(db *gorm.DB, query any, args ...any) (err error) {
|
||||
return db.Where(query, args...).First(m).Error
|
||||
}
|
||||
|
||||
func (m *ActivityModel) FindAll(db *gorm.DB, query any, args ...any) (err error) {
|
||||
return db.Where(query, args...).Find(m).Error
|
||||
}
|
||||
|
||||
func NewActivityModel() *ActivityModel {
|
||||
return &ActivityModel{}
|
||||
}
|
||||
|
|
17
server.go
17
server.go
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
httpkg "net/http"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
|
@ -14,6 +15,7 @@ import (
|
|||
"git.nobla.cn/golang/aeus-admin/models"
|
||||
"git.nobla.cn/golang/aeus-admin/pkg/dbcache"
|
||||
adminTypes "git.nobla.cn/golang/aeus-admin/types"
|
||||
"git.nobla.cn/golang/aeus/middleware/auth"
|
||||
"git.nobla.cn/golang/aeus/pkg/errors"
|
||||
"git.nobla.cn/golang/aeus/pkg/pool"
|
||||
"git.nobla.cn/golang/aeus/transport/http"
|
||||
|
@ -35,6 +37,7 @@ func getModels() []any {
|
|||
&models.Permission{},
|
||||
&models.RolePermission{},
|
||||
&models.Setting{},
|
||||
&models.Activity{},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,6 +210,19 @@ func generateVueFile(prefix string, apiPrefix string, mv *rest.Model) (err error
|
|||
return os.WriteFile(filename, writer.Bytes(), 0644)
|
||||
}
|
||||
|
||||
// restValueLookup 特殊字段获取方式
|
||||
func restValueLookup(column string, w httpkg.ResponseWriter, r *httpkg.Request) string {
|
||||
switch column {
|
||||
case "user":
|
||||
// 从授权信息里面获取用户的ID
|
||||
if t, ok := auth.FromContext(r.Context()); ok {
|
||||
uid, _ := t.GetSubject()
|
||||
return uid
|
||||
}
|
||||
}
|
||||
return r.Header.Get(column)
|
||||
}
|
||||
|
||||
// initREST 初始化REST模块
|
||||
func initREST(ctx context.Context, o *options) (err error) {
|
||||
tx := o.db
|
||||
|
@ -216,6 +232,7 @@ func initREST(ctx context.Context, o *options) (err error) {
|
|||
opts := make([]rest.Option, 0)
|
||||
opts = append(opts, o.restOpts...)
|
||||
opts = append(opts, rest.WithDB(tx))
|
||||
opts = append(opts, rest.WithValueLookup(restValueLookup))
|
||||
if o.apiPrefix != "" {
|
||||
opts = append(opts, rest.WithUriPrefix(o.apiPrefix))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue