// Package audit records administrative write actions to the admin_action_log // table so security/compliance can trace who did what across lottery admin // endpoints. Every admin CRUD in PR C calls WriteAdminAction inside its own // transaction; the caller is expected to have already validated permissions. package audit import ( "context" "crypto/sha1" "encoding/hex" "fmt" "strings" "time" "gorm.io/gorm" ) // Action code convention: dot-separated, prefix by domain (e.g. // "lottery.activity.create", "lottery.prize.delete"). Keep them short and // stable so downstream analytics can pivot without maintaining a translation // table. const ( ActionLotteryActivityCreate = "lottery.activity.create" ActionLotteryActivityUpdate = "lottery.activity.update" ActionLotteryActivityDelete = "lottery.activity.delete" ActionLotteryActivityPublish = "lottery.activity.publish" ActionLotteryActivityPause = "lottery.activity.pause" ActionLotteryPrizeCreate = "lottery.prize.create" ActionLotteryPrizeUpdate = "lottery.prize.update" ActionLotteryPrizeDelete = "lottery.prize.delete" ActionLotteryRulesPut = "lottery.activity.rules.put" ActionLotteryChancesGrant = "lottery.chances.grant" ) // AdminActionLog is the GORM entity for admin_action_log. type AdminActionLog struct { Id int64 `gorm:"primaryKey"` ActorUserId int64 `gorm:"type:bigint unsigned;not null;comment:操作者 user.id"` Action string `gorm:"type:varchar(64);not null;comment:动作 code"` TargetIds string `gorm:"type:varchar(255);not null;default:'';comment:被操作对象 ID"` RequestHash string `gorm:"type:varchar(64);not null;default:'';comment:请求摘要"` IP string `gorm:"type:varchar(45);not null;default:'';comment:操作者 IP"` UserAgent string `gorm:"type:varchar(255);not null;default:'';comment:操作者 UA"` CreatedAt time.Time `gorm:"<-:create;default:CURRENT_TIMESTAMP;comment:操作时间"` } // TableName pins the entity to the migration table name. func (AdminActionLog) TableName() string { return "admin_action_log" } // Entry is the pre-hashed convenience input to WriteAdminAction. Callers // build one with actor + action + payload fields; the writer computes the // request hash and inserts inside tx. type Entry struct { ActorUserId int64 Action string // TargetIds is stringified list of primary keys touched by this action. // Free-form: comma-separated ints, JSON array, etc. TargetIds string // RequestBody is hashed to produce request_hash. Pass nil if not applicable. RequestBody []byte IP string UserAgent string } // WriteAdminAction inserts an admin_action_log row inside the caller's tx. // The row lives-or-dies with the caller's transaction: a rollback drops the // audit trail, which is the intended coupling — we don't want to record // actions that never happened. func WriteAdminAction(ctx context.Context, tx *gorm.DB, e Entry) error { if tx == nil { return fmt.Errorf("audit: WriteAdminAction requires a transaction handle") } if e.ActorUserId == 0 || e.Action == "" { return fmt.Errorf("audit: WriteAdminAction requires ActorUserId and Action") } row := AdminActionLog{ ActorUserId: e.ActorUserId, Action: strings.TrimSpace(e.Action), TargetIds: e.TargetIds, RequestHash: hashBody(e.RequestBody), IP: e.IP, UserAgent: truncate(e.UserAgent, 255), } return tx.WithContext(ctx).Create(&row).Error } func hashBody(body []byte) string { if len(body) == 0 { return "" } sum := sha1.Sum(body) return hex.EncodeToString(sum[:]) } func truncate(s string, max int) string { if len(s) <= max { return s } return s[:max] }