系统管理基本搞定
This commit is contained in:
+184
-159
@@ -1,202 +1,153 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"server/models"
|
||||
"server/services"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
// OperationLogMiddleware 操作日志中间件 - 记录所有接口的调用记录
|
||||
func OperationLogMiddleware(ctx *context.Context) {
|
||||
// 跳过静态资源和内部路由
|
||||
const (
|
||||
oplogStartKey = "__oplog_start"
|
||||
oplogReqBodyKey = "__oplog_req_body"
|
||||
)
|
||||
|
||||
// BeginOperationLog 在 BeforeRouter 采集请求信息
|
||||
func BeginOperationLog(ctx *context.Context) {
|
||||
url := ctx.Input.URL()
|
||||
if shouldSkipLogging(url) {
|
||||
method := ctx.Input.Method()
|
||||
if shouldSkipLogging(method, url) {
|
||||
return
|
||||
}
|
||||
ctx.Input.SetData(oplogStartKey, time.Now())
|
||||
|
||||
// 请求体由 main.go 的 CopyBody 保留在 Input.RequestBody
|
||||
if rb := ctx.Input.RequestBody; len(rb) > 0 {
|
||||
s := string(rb)
|
||||
ctx.Input.SetData(oplogReqBodyKey, truncateString(maskSensitive(s), 5000))
|
||||
}
|
||||
}
|
||||
|
||||
// FinishOperationLog 在 FinishRouter 统一落库到 yz_system_operation_log
|
||||
func FinishOperationLog(ctx *context.Context) {
|
||||
url := ctx.Input.URL()
|
||||
method := ctx.Input.Method()
|
||||
if shouldSkipLogging(method, url) {
|
||||
return
|
||||
}
|
||||
|
||||
method := ctx.Input.Method()
|
||||
start, _ := ctx.Input.GetData(oplogStartKey).(time.Time)
|
||||
if start.IsZero() {
|
||||
start = time.Now()
|
||||
}
|
||||
execSec := float64(time.Since(start).Milliseconds()) / 1000.0
|
||||
|
||||
// 获取用户信息和租户信息(由 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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
if v := ctx.Input.GetData("userType"); v != nil {
|
||||
if s, ok := v.(string); ok {
|
||||
userType = s
|
||||
}
|
||||
uid := parseUint64FromCtx(ctx.Input.GetData("userId"))
|
||||
tidVal := parseUint64FromCtx(ctx.Input.GetData("tenantId"))
|
||||
var tid *uint64
|
||||
if tidVal > 0 {
|
||||
tid = &tidVal
|
||||
}
|
||||
|
||||
// 用户信息补全
|
||||
if username == "" {
|
||||
username = "anonymous"
|
||||
module := parseModule(url)
|
||||
action := parseAction(method, url)
|
||||
ip := ctx.Input.IP()
|
||||
userAgent := truncateString(ctx.Input.Header("User-Agent"), 500)
|
||||
status := int8(1)
|
||||
if code := ctx.ResponseWriter.Status; code >= 400 {
|
||||
status = 0
|
||||
}
|
||||
|
||||
// 读取请求体(对于有请求体的方法)
|
||||
var requestBody string
|
||||
if method == "POST" || method == "PUT" || method == "PATCH" {
|
||||
body, err := io.ReadAll(ctx.Request.Body)
|
||||
if err == nil && len(body) > 0 {
|
||||
requestBody = string(body)
|
||||
// 重置请求体,使其可以被后续处理
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
var reqData *string
|
||||
if v, ok := ctx.Input.GetData(oplogReqBodyKey).(string); ok && strings.TrimSpace(v) != "" {
|
||||
reqData = &v
|
||||
} else if q := strings.TrimSpace(ctx.Request.URL.RawQuery); q != "" {
|
||||
q = truncateString(maskSensitive(q), 5000)
|
||||
reqData = &q
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
ipAddress := ctx.Input.IP()
|
||||
userAgent := ctx.Input.Header("User-Agent")
|
||||
queryString := ctx.Request.URL.RawQuery
|
||||
var respData *string
|
||||
if code := ctx.ResponseWriter.Status; code >= 400 {
|
||||
msg := "HTTP " + strconv.Itoa(code)
|
||||
respData = &msg
|
||||
}
|
||||
|
||||
// 使用延迟函数来记录操作
|
||||
defer func() {
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// 解析操作相关信息
|
||||
operation := parseOperationType(method, url)
|
||||
module := parseModule(url)
|
||||
resourceType := parseResourceType(url)
|
||||
resourceId := parseResourceId(url)
|
||||
|
||||
// 为所有接口都记录日志
|
||||
log := &models.OperationLog{
|
||||
TenantId: tenantId,
|
||||
UserId: userId,
|
||||
Username: username,
|
||||
Module: module,
|
||||
ResourceType: resourceType,
|
||||
Operation: operation,
|
||||
IpAddress: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
RequestMethod: method,
|
||||
RequestUrl: url,
|
||||
Status: 1, // 默认成功
|
||||
Duration: int(duration.Milliseconds()),
|
||||
CreateTime: time.Now(),
|
||||
var errMsg *string
|
||||
if status == 0 {
|
||||
msg := "请求失败"
|
||||
if respData != nil {
|
||||
msg = *respData
|
||||
}
|
||||
errMsg = &msg
|
||||
}
|
||||
|
||||
// 设置资源ID
|
||||
if resourceId > 0 {
|
||||
log.ResourceId = &resourceId
|
||||
}
|
||||
|
||||
// 记录请求信息到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
|
||||
}
|
||||
|
||||
// 调用服务层保存日志
|
||||
if err := services.AddOperationLog(log); err != nil {
|
||||
fmt.Printf("Failed to save operation log: %v\n", err)
|
||||
}
|
||||
}()
|
||||
logRow := &models.SystemOperationLog{
|
||||
Tid: tid,
|
||||
UserID: uid,
|
||||
Module: module,
|
||||
Action: action,
|
||||
Method: method,
|
||||
URL: truncateString(url, 255),
|
||||
IP: truncateString(ip, 50),
|
||||
UserAgent: userAgent,
|
||||
RequestData: reqData,
|
||||
ResponseData: respData,
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
ExecutionTime: execSec,
|
||||
}
|
||||
_, _ = models.Orm.Insert(logRow)
|
||||
}
|
||||
|
||||
// parseOperationType 根据HTTP方法解析操作类型
|
||||
func parseOperationType(method, url string) string {
|
||||
func parseAction(method, url string) string {
|
||||
u := strings.ToLower(url)
|
||||
if strings.Contains(u, "login") {
|
||||
return "登录"
|
||||
}
|
||||
if strings.Contains(u, "logout") {
|
||||
return "退出"
|
||||
}
|
||||
if strings.Contains(u, "upload") {
|
||||
return "上传"
|
||||
}
|
||||
switch method {
|
||||
case "POST":
|
||||
// 检查URL是否包含特定的操作关键字
|
||||
if strings.Contains(url, "login") {
|
||||
return "LOGIN"
|
||||
if strings.Contains(u, "delete") {
|
||||
return "删除"
|
||||
}
|
||||
if strings.Contains(url, "logout") {
|
||||
return "LOGOUT"
|
||||
if strings.Contains(u, "update") || strings.Contains(u, "edit") || strings.Contains(u, "rename") {
|
||||
return "编辑"
|
||||
}
|
||||
if strings.Contains(url, "add") || strings.Contains(url, "create") {
|
||||
return "CREATE"
|
||||
if strings.Contains(u, "create") || strings.Contains(u, "add") {
|
||||
return "新增"
|
||||
}
|
||||
return "CREATE"
|
||||
return "提交"
|
||||
case "PUT", "PATCH":
|
||||
return "UPDATE"
|
||||
return "编辑"
|
||||
case "DELETE":
|
||||
return "DELETE"
|
||||
return "删除"
|
||||
default:
|
||||
return "READ"
|
||||
return "查询"
|
||||
}
|
||||
}
|
||||
|
||||
// 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])
|
||||
path := strings.Trim(strings.ToLower(url), "/")
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) >= 2 {
|
||||
return truncateString(parts[1], 50)
|
||||
}
|
||||
if len(parts) == 1 && parts[0] != "" {
|
||||
return truncateString(parts[0], 50)
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// shouldSkipLogging 判断是否需要跳过日志记录
|
||||
func shouldSkipLogging(url string) bool {
|
||||
// 跳过静态资源、健康检查等
|
||||
func shouldSkipLogging(method, url string) bool {
|
||||
skipPatterns := []string{
|
||||
"/static/",
|
||||
"/uploads/",
|
||||
@@ -204,19 +155,93 @@ func shouldSkipLogging(url string) bool {
|
||||
"/health",
|
||||
"/ping",
|
||||
}
|
||||
|
||||
for _, pattern := range skipPatterns {
|
||||
if strings.HasPrefix(url, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 高频噪声接口:默认跳过(可按需再扩充)
|
||||
if method == "GET" {
|
||||
noisyExact := map[string]bool{
|
||||
"/platform/currentUser": true,
|
||||
"/platform/allmenu": true,
|
||||
"/platform/getOpenVerify": true, // 若未来改名/迁移可再调整
|
||||
}
|
||||
if noisyExact[url] {
|
||||
return true
|
||||
}
|
||||
// 菜单详情/列表类:频率高且多为前端路由加载
|
||||
if strings.HasPrefix(url, "/platform/menu/") {
|
||||
return true
|
||||
}
|
||||
// 登录页极验配置轮询/获取(不影响关键业务)
|
||||
if strings.HasPrefix(url, "/platform/login/getGeetest") || strings.HasPrefix(url, "/platform/login/getOpenVerify") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// truncateString 截断字符串到指定长度
|
||||
func parseUint64FromCtx(v interface{}) uint64 {
|
||||
switch x := v.(type) {
|
||||
case int:
|
||||
if x > 0 {
|
||||
return uint64(x)
|
||||
}
|
||||
case int64:
|
||||
if x > 0 {
|
||||
return uint64(x)
|
||||
}
|
||||
case uint64:
|
||||
return x
|
||||
case float64:
|
||||
if x > 0 {
|
||||
return uint64(x)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func truncateString(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
|
||||
func maskSensitive(s string) string {
|
||||
// 尝试 JSON 脱敏(失败则返回原文)
|
||||
var obj interface{}
|
||||
if err := json.Unmarshal([]byte(s), &obj); err != nil {
|
||||
return s
|
||||
}
|
||||
maskInObj(&obj)
|
||||
bs, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
func maskInObj(v *interface{}) {
|
||||
switch t := (*v).(type) {
|
||||
case map[string]interface{}:
|
||||
for k, val := range t {
|
||||
lk := strings.ToLower(k)
|
||||
if lk == "password" || lk == "pwd" || lk == "token" || lk == "api_key" || lk == "api_secret" || lk == "authorization" {
|
||||
t[k] = "***"
|
||||
continue
|
||||
}
|
||||
tmp := val
|
||||
maskInObj(&tmp)
|
||||
t[k] = tmp
|
||||
}
|
||||
case []interface{}:
|
||||
for i := range t {
|
||||
tmp := t[i]
|
||||
maskInObj(&tmp)
|
||||
t[i] = tmp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user