Files
yunzerwebsiteallinone/go/middleware/operationLog.go
T
2026-07-16 00:35:50 +08:00

368 lines
9.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package middleware
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
"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
}
fmt.Printf("[oplog-begin] method=%s url=%s\n", method, url)
// 解析用户信息(直接从 header 解析,不依赖 JWT 中间件设置的 context
var uid uint64
var tid *uint64
if auth := ctx.Request.Header.Get("Authorization"); auth != "" {
parts := strings.SplitN(auth, " ", 2)
if len(parts) == 2 && parts[0] == "Bearer" {
if claims, err := jwtutil.ParseToken(parts[1]); err == nil {
uid = uint64(claims.UserID)
if claims.TenantId > 0 {
tidVal := uint64(claims.TenantId)
tid = &tidVal
}
}
}
}
// 采集请求信息
reqBody := ""
if rb := ctx.Input.RequestBody; len(rb) > 0 {
reqBody = truncateString(maskSensitive(string(rb)), 5000)
} else if q := strings.TrimSpace(ctx.Request.URL.RawQuery); q != "" {
reqBody = truncateString(maskSensitive(q), 5000)
}
module := parseModule(url)
action := parseAction(method, url)
ip := truncateString(ctx.Input.IP(), 50)
userAgent := truncateString(ctx.Request.Header.Get("User-Agent"), 500)
// 异步写入数据库
go func() {
time.Sleep(100 * time.Millisecond) // 等待响应完成
logRow := &models.SystemOperationLog{
Tid: tid,
UserID: uid,
Module: module,
Action: action,
Method: method,
URL: truncateString(url, 255),
IP: ip,
UserAgent: userAgent,
RequestData: strPtr(reqBody),
Status: 1,
ExecutionTime: 0,
}
_, err := models.Orm.Insert(logRow)
if err != nil {
fmt.Printf("[oplog] INSERT ERROR: %v\n", err)
} else {
fmt.Printf("[oplog] INSERTED uid=%d module=%s action=%s\n", uid, module, action)
}
}()
}
func strPtr(s string) *string {
if s == "" {
return nil
}
return &s
}
// WriteDeleteLog 控制器删除后直接写操作日志(带标题)
func WriteDeleteLog(userID uint64, tid int, module, title string) {
moduleCN := oplogModuleLabel(module)
desc := fmt.Sprintf("您删除了%s:【%s】", moduleCN, truncateString(title, 20))
tidVal := uint64(tid)
logRow := &models.SystemOperationLog{
UserID: userID,
Tid: &tidVal,
Module: module,
Action: "删除",
Method: "DELETE",
URL: fmt.Sprintf("/app/%s", module),
Status: 1,
RequestData: &desc,
ExecutionTime: 0,
}
models.Orm.Insert(logRow)
}
func oplogModuleLabel(m string) string {
switch m {
case "notebook":
return "记事本"
case "schedule":
return "日程提醒"
default:
return m
}
}
// FinishOperationLog 在 FinishRouter 统一落库到 yz_system_operation_log
func FinishOperationLog(ctx *context.Context) {
url := ctx.Input.URL()
method := ctx.Input.Method()
fmt.Printf("[oplog-finish] method=%s url=%s\n", method, url)
if shouldSkipLogging(method, url) {
fmt.Printf("[oplog-finish] SKIPPED\n")
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"))
// 如果 context 中没有用户信息,尝试从 Authorization header 解析
if uid == 0 {
if auth := ctx.Request.Header.Get("Authorization"); auth != "" {
parts := strings.SplitN(auth, " ", 2)
if len(parts) == 2 && parts[0] == "Bearer" {
if claims, err := jwtutil.ParseToken(parts[1]); err == nil {
uid = uint64(claims.UserID)
if claims.TenantId > 0 {
tidVal = uint64(claims.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,
}
fmt.Printf("[oplog] method=%s url=%s uid=%d tid=%v module=%s action=%s\n", method, url, uid, tid, module, action)
_, err := models.Orm.Insert(logRow)
if err != nil {
fmt.Printf("[oplog] INSERT ERROR: %v\n", err)
}
}
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
}
// 客户端高频版本检查
if strings.HasPrefix(url, "/api/softwareupgrade/check") {
return true
}
// App 端读接口跳过(只记录写操作:POST/PUT/DELETE
if strings.HasPrefix(url, "/app/") && method == "GET" {
return true
}
// App 端 DELETE 由控制器自行记录(需要标题信息)
if strings.HasPrefix(url, "/app/") && method == "DELETE" {
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
}
}
}