增加任务管理模块

This commit is contained in:
2025-11-12 17:32:03 +08:00
parent 12a0ff8afc
commit db16ee70de
54 changed files with 3638 additions and 672 deletions
+87 -74
View File
@@ -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] + "..."
}