go-platform/middleware/operationLog.go

248 lines
5.3 KiB
Go

package middleware
import (
"encoding/json"
"strconv"
"strings"
"time"
"server/models"
"github.com/beego/beego/v2/server/web/context"
)
const (
oplogStartKey = "__oplog_start"
oplogReqBodyKey = "__oplog_req_body"
)
// BeginOperationLog 在 BeforeRouter 采集请求信息
func BeginOperationLog(ctx *context.Context) {
url := ctx.Input.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
}
start, _ := ctx.Input.GetData(oplogStartKey).(time.Time)
if start.IsZero() {
start = time.Now()
}
execSec := float64(time.Since(start).Milliseconds()) / 1000.0
uid := parseUint64FromCtx(ctx.Input.GetData("userId"))
tidVal := parseUint64FromCtx(ctx.Input.GetData("tenantId"))
var tid *uint64
if tidVal > 0 {
tid = &tidVal
}
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 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
}
var respData *string
if code := ctx.ResponseWriter.Status; code >= 400 {
msg := "HTTP " + strconv.Itoa(code)
respData = &msg
}
var errMsg *string
if status == 0 {
msg := "请求失败"
if respData != nil {
msg = *respData
}
errMsg = &msg
}
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)
}
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":
if strings.Contains(u, "delete") {
return "删除"
}
if strings.Contains(u, "update") || strings.Contains(u, "edit") || strings.Contains(u, "rename") {
return "编辑"
}
if strings.Contains(u, "create") || strings.Contains(u, "add") {
return "新增"
}
return "提交"
case "PUT", "PATCH":
return "编辑"
case "DELETE":
return "删除"
default:
return "查询"
}
}
func parseModule(url string) string {
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"
}
func shouldSkipLogging(method, url string) bool {
skipPatterns := []string{
"/static/",
"/uploads/",
"/favicon.ico",
"/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
}
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
}
}
}