190 lines
6.0 KiB
Go
190 lines
6.0 KiB
Go
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
|
||
}
|