操作日志和访问日志

This commit is contained in:
2025-11-11 22:32:46 +08:00
parent efc079c0e5
commit 12a0ff8afc
17 changed files with 2300 additions and 4 deletions
+209
View File
@@ -0,0 +1,209 @@
package middleware
import (
"fmt"
"io"
"server/models"
"server/services"
"strconv"
"strings"
"time"
"github.com/beego/beego/v2/server/web/context"
)
// OperationLogMiddleware 操作日志中间件 - 记录所有的CREATE、UPDATE、DELETE操作
func OperationLogMiddleware(ctx *context.Context) {
// 记录所有重要操作,包括修改类(POST/PUT/PATCH/DELETE)和读取类(GET)用于统计账户访问功能
method := ctx.Input.Method()
// 获取用户信息和租户信息(由 JWT 中间件设置在 Input.Data 中)
userId := 0
tenantId := 0
username := ""
if v := ctx.Input.GetData("userId"); v != nil {
if id, ok := v.(int); ok {
userId = id
}
}
if v := ctx.Input.GetData("tenantId"); v != nil {
if id, ok := v.(int); ok {
tenantId = id
}
}
if v := ctx.Input.GetData("username"); v != nil {
if s, ok := v.(string); ok {
username = s
}
}
// 如果无法获取用户ID,继续记录为匿名访问(userId=0),以便统计未登录或授权失败的访问
if userId == 0 {
// debug: 输出一些上下文信息,帮助定位为何未能获取 userId
fmt.Printf("OperationLogMiddleware: anonymous request %s %s, Authorization header length=%d\n", method, ctx.Input.URL(), len(ctx.Input.Header("Authorization")))
// 确保 username 有值,便于区分
if username == "" {
username = "anonymous"
}
}
// 读取请求体(对于有请求体的方法)
var requestBody string
if method == "POST" || method == "PUT" || method == "PATCH" {
body, err := io.ReadAll(ctx.Request.Body)
if err == nil {
requestBody = string(body)
// 重置请求体,使其可以被后续处理
ctx.Request.Body = io.NopCloser(strings.NewReader(requestBody))
}
}
startTime := time.Now()
// 使用延迟函数来记录操作
defer func() {
duration := time.Since(startTime)
// 解析操作类型
operation := parseOperationType(method, ctx.Input.URL())
resourceType := parseResourceType(ctx.Input.URL())
resourceId := parseResourceId(ctx.Input.URL())
module := parseModule(ctx.Input.URL())
// 如果是读取/访问行为,写入访问日志(sys_access_log),否则写入操作日志(sys_operation_log
if operation == "READ" {
access := &models.AccessLog{
TenantId: tenantId,
UserId: userId,
Username: username,
Module: module,
ResourceType: resourceType,
ResourceId: &resourceId,
RequestUrl: ctx.Input.URL(),
IpAddress: ctx.Input.IP(),
UserAgent: ctx.Input.Header("User-Agent"),
RequestMethod: method,
Duration: int(duration.Milliseconds()),
}
// 在 QueryString 中记录查询参数和匿名标识
qs := ctx.Request.URL.RawQuery
if qs != "" {
access.QueryString = qs
}
if userId == 0 {
// 将匿名标识拼入 QueryString 以便查询(也可改为独立字段)
if access.QueryString != "" {
access.QueryString = "anonymous=true; " + access.QueryString
} else {
access.QueryString = "anonymous=true"
}
}
if err := services.AddAccessLog(access); err != nil {
fmt.Printf("Failed to save access log: %v\n", err)
}
return
}
// 创建操作日志
log := &models.OperationLog{
TenantId: tenantId,
UserId: userId,
Username: username,
Module: module,
ResourceType: resourceType,
ResourceId: &resourceId,
Operation: operation,
IpAddress: ctx.Input.IP(),
UserAgent: ctx.Input.Header("User-Agent"),
RequestMethod: method,
RequestUrl: ctx.Input.URL(),
Status: 1, // 默认成功,实际应该根据响应状态码更新
Duration: int(duration.Milliseconds()),
CreateTime: time.Now(),
}
// 如果是写操作,保存请求体作为新值;对于读取操作可以在Description里记录query
if requestBody != "" {
log.NewValue = requestBody
} else if method == "GET" {
// 把查询字符串放到描述里,便于分析访问参数
qs := ctx.Request.URL.RawQuery
if qs != "" {
log.Description = "query=" + qs
}
}
// 标记匿名访问信息(当 userId==0)
if userId == 0 {
if log.Description != "" {
log.Description = "anonymous=true; " + log.Description
} else {
log.Description = "anonymous=true"
}
}
// 调用服务层保存日志
if err := services.AddOperationLog(log); err != nil {
fmt.Printf("Failed to save operation log: %v\n", err)
}
}()
}
// parseOperationType 根据HTTP方法解析操作类型
func parseOperationType(method, url string) string {
switch method {
case "POST":
// 检查URL是否包含特定的操作关键字
if strings.Contains(url, "login") {
return "LOGIN"
}
if strings.Contains(url, "logout") {
return "LOGOUT"
}
if strings.Contains(url, "add") || strings.Contains(url, "create") {
return "CREATE"
}
return "CREATE"
case "PUT", "PATCH":
return "UPDATE"
case "DELETE":
return "DELETE"
default:
return "READ"
}
}
// parseResourceType 根据URL解析资源类型
func parseResourceType(url string) string {
parts := strings.Split(strings.TrimPrefix(url, "/api/"), "/")
if len(parts) > 0 {
// 移除复数形式的s
resourceType := strings.TrimSuffix(parts[0], "s")
return resourceType
}
return "unknown"
}
// parseResourceId 从URL中提取资源ID
func parseResourceId(url string) int {
parts := strings.Split(strings.TrimPrefix(url, "/api/"), "/")
if len(parts) >= 2 {
// 尝试解析第二个部分为ID
if id, err := strconv.Atoi(parts[1]); err == nil {
return id
}
}
return 0
}
// parseModule 根据URL解析模块名称
func parseModule(url string) string {
// 返回与 sys_operation_log.module 字段匹配的短code(例如 dict、user 等)
parts := strings.Split(strings.TrimPrefix(url, "/api/"), "/")
if len(parts) > 0 {
return strings.ToLower(parts[0])
}
return "unknown"
}