增加任务管理模块
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"server/models"
|
||||
@@ -12,15 +13,22 @@ import (
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
// OperationLogMiddleware 操作日志中间件 - 记录所有的CREATE、UPDATE、DELETE操作
|
||||
// OperationLogMiddleware 操作日志中间件 - 记录所有接口的调用记录
|
||||
func OperationLogMiddleware(ctx *context.Context) {
|
||||
// 记录所有重要操作,包括修改类(POST/PUT/PATCH/DELETE)和读取类(GET)用于统计账户访问功能
|
||||
// 跳过静态资源和内部路由
|
||||
url := ctx.Input.URL()
|
||||
if shouldSkipLogging(url) {
|
||||
return
|
||||
}
|
||||
|
||||
method := ctx.Input.Method()
|
||||
|
||||
// 获取用户信息和租户信息(由 JWT 中间件设置在 Input.Data 中)
|
||||
userId := 0
|
||||
tenantId := 0
|
||||
username := ""
|
||||
userType := "" // 用户类型:user(平台用户) 或 employee(租户员工)
|
||||
|
||||
if v := ctx.Input.GetData("userId"); v != nil {
|
||||
if id, ok := v.(int); ok {
|
||||
userId = id
|
||||
@@ -36,112 +44,90 @@ func OperationLogMiddleware(ctx *context.Context) {
|
||||
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"
|
||||
if v := ctx.Input.GetData("userType"); v != nil {
|
||||
if s, ok := v.(string); ok {
|
||||
userType = s
|
||||
}
|
||||
}
|
||||
|
||||
// 用户信息补全
|
||||
if username == "" {
|
||||
username = "anonymous"
|
||||
}
|
||||
|
||||
// 读取请求体(对于有请求体的方法)
|
||||
var requestBody string
|
||||
if method == "POST" || method == "PUT" || method == "PATCH" {
|
||||
body, err := io.ReadAll(ctx.Request.Body)
|
||||
if err == nil {
|
||||
if err == nil && len(body) > 0 {
|
||||
requestBody = string(body)
|
||||
// 重置请求体,使其可以被后续处理
|
||||
ctx.Request.Body = io.NopCloser(strings.NewReader(requestBody))
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
ipAddress := ctx.Input.IP()
|
||||
userAgent := ctx.Input.Header("User-Agent")
|
||||
queryString := ctx.Request.URL.RawQuery
|
||||
|
||||
// 使用延迟函数来记录操作
|
||||
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())
|
||||
// 解析操作相关信息
|
||||
operation := parseOperationType(method, url)
|
||||
module := parseModule(url)
|
||||
resourceType := parseResourceType(url)
|
||||
resourceId := parseResourceId(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"),
|
||||
IpAddress: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
RequestMethod: method,
|
||||
RequestUrl: ctx.Input.URL(),
|
||||
Status: 1, // 默认成功,实际应该根据响应状态码更新
|
||||
RequestUrl: 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
|
||||
}
|
||||
// 设置资源ID
|
||||
if resourceId > 0 {
|
||||
log.ResourceId = &resourceId
|
||||
}
|
||||
|
||||
// 标记匿名访问信息(当 userId==0)
|
||||
if userId == 0 {
|
||||
if log.Description != "" {
|
||||
log.Description = "anonymous=true; " + log.Description
|
||||
} else {
|
||||
log.Description = "anonymous=true"
|
||||
// 记录请求信息到Description
|
||||
var description strings.Builder
|
||||
if requestBody != "" {
|
||||
description.WriteString("Request: " + truncateString(requestBody, 500))
|
||||
}
|
||||
if queryString != "" {
|
||||
if description.Len() > 0 {
|
||||
description.WriteString(" | ")
|
||||
}
|
||||
description.WriteString("Query: " + queryString)
|
||||
}
|
||||
|
||||
log.Description = description.String()
|
||||
|
||||
// 如果有请求体,作为NewValue保存
|
||||
if requestBody != "" {
|
||||
log.NewValue = requestBody
|
||||
}
|
||||
|
||||
// 添加用户类型信息到Description
|
||||
if userType != "" {
|
||||
if log.Description != "" {
|
||||
log.Description += " | "
|
||||
}
|
||||
log.Description += "UserType: " + userType
|
||||
}
|
||||
|
||||
// 调用服务层保存日志
|
||||
@@ -207,3 +193,30 @@ func parseModule(url string) string {
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// shouldSkipLogging 判断是否需要跳过日志记录
|
||||
func shouldSkipLogging(url string) bool {
|
||||
// 跳过静态资源、健康检查等
|
||||
skipPatterns := []string{
|
||||
"/static/",
|
||||
"/uploads/",
|
||||
"/favicon.ico",
|
||||
"/health",
|
||||
"/ping",
|
||||
}
|
||||
|
||||
for _, pattern := range skipPatterns {
|
||||
if strings.HasPrefix(url, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// truncateString 截断字符串到指定长度
|
||||
func truncateString(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user