tokenFactory/model/operation_log.go

190 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package model
import (
"context"
"fmt"
"github.com/QuantumNous/new-api/common"
"github.com/gin-gonic/gin"
)
// OperationLog 记录用户对数据的增删改操作,用于审计。
type OperationLog struct {
Id int `json:"id" gorm:"index:idx_oplog_created_at_id,priority:1"`
UserId int `json:"user_id" gorm:"index;index:idx_oplog_user_id_id,priority:1"`
Username string `json:"username" gorm:"index;default:''"`
CreatedAt int64 `json:"created_at" gorm:"bigint;index:idx_oplog_created_at_id,priority:2;index:idx_oplog_created_at_action"`
Action string `json:"action" gorm:"type:varchar(32);index;index:idx_oplog_created_at_action"` // create / update / delete
TargetType string `json:"target_type" gorm:"type:varchar(64);index;default:''"` // 资源类型: channel, token, user, redemption, model, setting …
TargetId int `json:"target_id" gorm:"index;default:0"` // 资源 ID
TargetName string `json:"target_name" gorm:"type:varchar(255);default:''"` // 资源名称(便于检索)
Content string `json:"content"` // 操作描述
Ip string `json:"ip" gorm:"index;default:''"` // 操作者 IP
RequestBody string `json:"request_body" gorm:"type:text"` // 请求体快照(脱敏后)
UserRole int `json:"user_role" gorm:"default:0"` // 操作者角色
}
// 操作类型常量
const (
OperationActionCreate = "create"
OperationActionUpdate = "update"
OperationActionDelete = "delete"
)
// RecordOperationLog 记录一条操作日志。
func RecordOperationLog(c *gin.Context, userId int, action string, targetType string, targetId int, targetName string, content string, requestBody string) {
username := c.GetString("username")
userRole := c.GetInt("role")
ip := c.ClientIP()
log := &OperationLog{
UserId: userId,
Username: username,
CreatedAt: common.GetTimestamp(),
Action: action,
TargetType: targetType,
TargetId: targetId,
TargetName: targetName,
Content: content,
Ip: ip,
RequestBody: requestBody,
UserRole: userRole,
}
err := DB.Create(log).Error
if err != nil {
common.SysError("failed to record operation log: " + err.Error())
}
}
// GetOperationLogs 管理员分页查询操作日志。
func GetOperationLogs(action string, targetType string, targetId int, username string, startTimestamp int64, endTimestamp int64, startIdx int, num int) (logs []*OperationLog, total int64, err error) {
tx := DB.Model(&OperationLog{})
if action != "" {
tx = tx.Where("action = ?", action)
}
if targetType != "" {
tx = tx.Where("target_type = ?", targetType)
}
if targetId != 0 {
tx = tx.Where("target_id = ?", targetId)
}
if username != "" {
tx = tx.Where("username = ?", username)
}
if startTimestamp != 0 {
tx = tx.Where("created_at >= ?", startTimestamp)
}
if endTimestamp != 0 {
tx = tx.Where("created_at <= ?", endTimestamp)
}
err = tx.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = tx.Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error
return logs, total, err
}
// GetUserOperationLogs 普通用户查询自己的操作日志。
func GetUserOperationLogs(userId int, action string, targetType string, startTimestamp int64, endTimestamp int64, startIdx int, num int) (logs []*OperationLog, total int64, err error) {
tx := DB.Model(&OperationLog{}).Where("user_id = ?", userId)
if action != "" {
tx = tx.Where("action = ?", action)
}
if targetType != "" {
tx = tx.Where("target_type = ?", targetType)
}
if startTimestamp != 0 {
tx = tx.Where("created_at >= ?", startTimestamp)
}
if endTimestamp != 0 {
tx = tx.Where("created_at <= ?", endTimestamp)
}
err = tx.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = tx.Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error
return logs, total, err
}
// DeleteOldOperationLogs 清理指定时间之前的操作日志。
func DeleteOldOperationLogs(ctx context.Context, targetTimestamp int64, limit int) (int64, error) {
var total int64 = 0
for {
if ctx.Err() != nil {
return total, ctx.Err()
}
result := DB.Where("created_at < ?", targetTimestamp).Limit(limit).Delete(&OperationLog{})
if result.Error != nil {
return total, result.Error
}
total += result.RowsAffected
if result.RowsAffected < int64(limit) {
break
}
}
return total, nil
}
// GetOperationLogStats 按操作类型统计数量。
func GetOperationLogStats(startTimestamp int64, endTimestamp int64) (map[string]int64, error) {
type result struct {
Action string
Count int64
}
var results []result
tx := DB.Model(&OperationLog{}).Select("action, count(*) as count")
if startTimestamp != 0 {
tx = tx.Where("created_at >= ?", startTimestamp)
}
if endTimestamp != 0 {
tx = tx.Where("created_at <= ?", endTimestamp)
}
tx = tx.Group("action")
if err := tx.Scan(&results).Error; err != nil {
return nil, fmt.Errorf("查询操作日志统计失败: %w", err)
}
stats := make(map[string]int64, len(results))
for _, r := range results {
stats[r.Action] = r.Count
}
return stats, nil
}
// SanitizeRequestBody 脱敏请求体中的敏感字段。
func SanitizeRequestBody(body string) string {
if body == "" {
return ""
}
var m map[string]interface{}
if err := common.Unmarshal([]byte(body), &m); err != nil {
// 无法解析为 JSON直接返回截断后的原始内容
if len(body) > 2000 {
return body[:2000] + "...(truncated)"
}
return body
}
// 脱敏敏感字段
sensitiveKeys := []string{"password", "key", "secret", "token", "access_token", "refresh_token", "api_key", "credential"}
for _, k := range sensitiveKeys {
if _, ok := m[k]; ok {
m[k] = "******"
}
}
sanitized, err := common.Marshal(m)
if err != nil {
return body
}
result := string(sanitized)
if len(result) > 4000 {
result = result[:4000] + "...(truncated)"
}
return result
}