操作日志和访问日志
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"server/models"
|
||||
"server/services"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -188,6 +189,24 @@ func (c *AuthController) Login() {
|
||||
_, _ = o.Raw("UPDATE yz_tenant_employees SET last_login_time = ?, last_login_ip = ? WHERE id = ?", loginTime, clientIP, employee.Id).Exec()
|
||||
}
|
||||
|
||||
// 记录登录操作
|
||||
loginLog := &models.OperationLog{
|
||||
TenantId: tenantId,
|
||||
UserId: userId,
|
||||
Username: usernameForToken,
|
||||
Module: "auth",
|
||||
ResourceType: "user",
|
||||
Operation: "LOGIN",
|
||||
IpAddress: clientIP,
|
||||
UserAgent: c.Ctx.Input.Header("User-Agent"),
|
||||
RequestMethod: "POST",
|
||||
RequestUrl: c.Ctx.Input.URL(),
|
||||
Status: 1,
|
||||
Duration: 0,
|
||||
CreateTime: loginTime,
|
||||
}
|
||||
_ = services.AddOperationLog(loginLog)
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "登录成功",
|
||||
@@ -205,6 +224,43 @@ func (c *AuthController) Login() {
|
||||
|
||||
// Logout 处理登出请求
|
||||
func (c *AuthController) Logout() {
|
||||
// 获取登出的用户信息
|
||||
userIdStr := c.GetString("user_id")
|
||||
username := c.GetString("username")
|
||||
tenantIdStr := c.GetString("tenant_id")
|
||||
|
||||
var userId, tenantId int
|
||||
if userIdStr != "" {
|
||||
id, err := strconv.Atoi(userIdStr)
|
||||
if err == nil {
|
||||
userId = id
|
||||
}
|
||||
}
|
||||
if tenantIdStr != "" {
|
||||
id, err := strconv.Atoi(tenantIdStr)
|
||||
if err == nil {
|
||||
tenantId = id
|
||||
}
|
||||
}
|
||||
|
||||
// 记录登出操作
|
||||
logoutLog := &models.OperationLog{
|
||||
TenantId: tenantId,
|
||||
UserId: userId,
|
||||
Username: username,
|
||||
Module: "auth",
|
||||
ResourceType: "user",
|
||||
Operation: "LOGOUT",
|
||||
IpAddress: c.Ctx.Input.IP(),
|
||||
UserAgent: c.Ctx.Input.Header("User-Agent"),
|
||||
RequestMethod: "POST",
|
||||
RequestUrl: c.Ctx.Input.URL(),
|
||||
Status: 1,
|
||||
Duration: 0,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_ = services.AddOperationLog(logoutLog)
|
||||
|
||||
// 在实际应用中,这里需要处理JWT或Session的清除
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
|
||||
+219
-1
@@ -5,6 +5,7 @@ import (
|
||||
"server/models"
|
||||
"server/services"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
@@ -160,6 +161,32 @@ func (c *DictController) AddDictType() {
|
||||
"data": map[string]interface{}{"id": id},
|
||||
}
|
||||
c.ServeJSON()
|
||||
|
||||
// 记录操作日志 - 创建字典类型
|
||||
go func() {
|
||||
// 异步写日志,避免阻塞请求响应
|
||||
rid := int(id)
|
||||
newVal, _ := json.Marshal(dictType)
|
||||
log := &models.OperationLog{
|
||||
TenantId: dictType.TenantId,
|
||||
UserId: userId,
|
||||
Username: username,
|
||||
Module: "dict",
|
||||
ResourceType: "dict_type",
|
||||
ResourceId: &rid,
|
||||
Operation: "CREATE",
|
||||
Description: "创建字典类型",
|
||||
NewValue: string(newVal),
|
||||
IpAddress: c.Ctx.Input.IP(),
|
||||
UserAgent: c.Ctx.Input.Header("User-Agent"),
|
||||
RequestMethod: c.Ctx.Input.Method(),
|
||||
RequestUrl: c.Ctx.Input.URL(),
|
||||
Status: 1,
|
||||
Duration: 0,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_ = services.AddOperationLog(log)
|
||||
}()
|
||||
}
|
||||
|
||||
// UpdateDictType 更新字典类型
|
||||
@@ -194,6 +221,15 @@ func (c *DictController) UpdateDictType() {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
userIdData := c.Ctx.Input.GetData("userId")
|
||||
userId := 0
|
||||
if userIdData != nil {
|
||||
if uid, ok := userIdData.(int); ok {
|
||||
userId = uid
|
||||
}
|
||||
}
|
||||
|
||||
dictType.Id = id
|
||||
dictType.UpdateBy = username
|
||||
|
||||
@@ -212,6 +248,31 @@ func (c *DictController) UpdateDictType() {
|
||||
"message": "更新成功",
|
||||
}
|
||||
c.ServeJSON()
|
||||
|
||||
// 记录操作日志 - 更新字典类型
|
||||
go func() {
|
||||
newVal, _ := json.Marshal(dictType)
|
||||
// 旧值可以从服务层获取,如果需要更详细的旧值,可以在 UpdateDictType 前查询并传入
|
||||
log := &models.OperationLog{
|
||||
TenantId: dictType.TenantId,
|
||||
UserId: userId,
|
||||
Username: username,
|
||||
Module: "dict",
|
||||
ResourceType: "dict_type",
|
||||
ResourceId: &dictType.Id,
|
||||
Operation: "UPDATE",
|
||||
Description: "更新字典类型",
|
||||
NewValue: string(newVal),
|
||||
IpAddress: c.Ctx.Input.IP(),
|
||||
UserAgent: c.Ctx.Input.Header("User-Agent"),
|
||||
RequestMethod: c.Ctx.Input.Method(),
|
||||
RequestUrl: c.Ctx.Input.URL(),
|
||||
Status: 1,
|
||||
Duration: 0,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_ = services.AddOperationLog(log)
|
||||
}()
|
||||
}
|
||||
|
||||
// DeleteDictType 删除字典类型
|
||||
@@ -227,7 +288,7 @@ func (c *DictController) DeleteDictType() {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取租户ID
|
||||
// 获取租户ID、用户名和用户ID 用于记录日志
|
||||
tenantIdData := c.Ctx.Input.GetData("tenantId")
|
||||
tenantId := 0
|
||||
if tenantIdData != nil {
|
||||
@@ -235,6 +296,20 @@ func (c *DictController) DeleteDictType() {
|
||||
tenantId = tid
|
||||
}
|
||||
}
|
||||
usernameData := c.Ctx.Input.GetData("username")
|
||||
username := ""
|
||||
if usernameData != nil {
|
||||
if u, ok := usernameData.(string); ok {
|
||||
username = u
|
||||
}
|
||||
}
|
||||
userIdData := c.Ctx.Input.GetData("userId")
|
||||
userId := 0
|
||||
if userIdData != nil {
|
||||
if uid, ok := userIdData.(int); ok {
|
||||
userId = uid
|
||||
}
|
||||
}
|
||||
|
||||
err = services.DeleteDictType(id, tenantId)
|
||||
if err != nil {
|
||||
@@ -251,6 +326,29 @@ func (c *DictController) DeleteDictType() {
|
||||
"message": "删除成功",
|
||||
}
|
||||
c.ServeJSON()
|
||||
|
||||
// 记录操作日志 - 删除字典类型
|
||||
go func() {
|
||||
rid := id
|
||||
log := &models.OperationLog{
|
||||
TenantId: tenantId,
|
||||
UserId: userId,
|
||||
Username: username,
|
||||
Module: "dict",
|
||||
ResourceType: "dict_type",
|
||||
ResourceId: &rid,
|
||||
Operation: "DELETE",
|
||||
Description: "删除字典类型",
|
||||
IpAddress: c.Ctx.Input.IP(),
|
||||
UserAgent: c.Ctx.Input.Header("User-Agent"),
|
||||
RequestMethod: c.Ctx.Input.Method(),
|
||||
RequestUrl: c.Ctx.Input.URL(),
|
||||
Status: 1,
|
||||
Duration: 0,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_ = services.AddOperationLog(log)
|
||||
}()
|
||||
}
|
||||
|
||||
// GetDictItems 获取字典项列表
|
||||
@@ -380,6 +478,46 @@ func (c *DictController) AddDictItem() {
|
||||
"data": map[string]interface{}{"id": id},
|
||||
}
|
||||
c.ServeJSON()
|
||||
|
||||
// 记录操作日志 - 创建字典项
|
||||
go func() {
|
||||
// 获取用户ID和租户ID
|
||||
userIdData := c.Ctx.Input.GetData("userId")
|
||||
userId := 0
|
||||
if userIdData != nil {
|
||||
if uid, ok := userIdData.(int); ok {
|
||||
userId = uid
|
||||
}
|
||||
}
|
||||
tenantIdData := c.Ctx.Input.GetData("tenantId")
|
||||
tenantId := 0
|
||||
if tenantIdData != nil {
|
||||
if tid, ok := tenantIdData.(int); ok {
|
||||
tenantId = tid
|
||||
}
|
||||
}
|
||||
rid := int(id)
|
||||
newVal, _ := json.Marshal(dictItem)
|
||||
log := &models.OperationLog{
|
||||
TenantId: tenantId,
|
||||
UserId: userId,
|
||||
Username: username,
|
||||
Module: "dict",
|
||||
ResourceType: "dict_item",
|
||||
ResourceId: &rid,
|
||||
Operation: "CREATE",
|
||||
Description: "创建字典项",
|
||||
NewValue: string(newVal),
|
||||
IpAddress: c.Ctx.Input.IP(),
|
||||
UserAgent: c.Ctx.Input.Header("User-Agent"),
|
||||
RequestMethod: c.Ctx.Input.Method(),
|
||||
RequestUrl: c.Ctx.Input.URL(),
|
||||
Status: 1,
|
||||
Duration: 0,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_ = services.AddOperationLog(log)
|
||||
}()
|
||||
}
|
||||
|
||||
// UpdateDictItem 更新字典项
|
||||
@@ -432,6 +570,41 @@ func (c *DictController) UpdateDictItem() {
|
||||
"message": "更新成功",
|
||||
}
|
||||
c.ServeJSON()
|
||||
|
||||
// 记录操作日志 - 更新字典项
|
||||
go func() {
|
||||
// 获取用户ID和租户ID
|
||||
userIdData := c.Ctx.Input.GetData("userId")
|
||||
userId := 0
|
||||
if userIdData != nil {
|
||||
if uid, ok := userIdData.(int); ok {
|
||||
userId = uid
|
||||
}
|
||||
}
|
||||
tenantId := 0
|
||||
// 通过字典项id尝试获取所属字典类型并推断租户(可选)
|
||||
// 这里先不查询,留 0 或者可从前端传入
|
||||
newVal, _ := json.Marshal(dictItem)
|
||||
log := &models.OperationLog{
|
||||
TenantId: tenantId,
|
||||
UserId: userId,
|
||||
Username: username,
|
||||
Module: "dict",
|
||||
ResourceType: "dict_item",
|
||||
ResourceId: &dictItem.Id,
|
||||
Operation: "UPDATE",
|
||||
Description: "更新字典项",
|
||||
NewValue: string(newVal),
|
||||
IpAddress: c.Ctx.Input.IP(),
|
||||
UserAgent: c.Ctx.Input.Header("User-Agent"),
|
||||
RequestMethod: c.Ctx.Input.Method(),
|
||||
RequestUrl: c.Ctx.Input.URL(),
|
||||
Status: 1,
|
||||
Duration: 0,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_ = services.AddOperationLog(log)
|
||||
}()
|
||||
}
|
||||
|
||||
// DeleteDictItem 删除字典项
|
||||
@@ -462,6 +635,51 @@ func (c *DictController) DeleteDictItem() {
|
||||
"message": "删除成功",
|
||||
}
|
||||
c.ServeJSON()
|
||||
|
||||
// 记录操作日志 - 删除字典项
|
||||
go func() {
|
||||
// 获取用户ID和租户ID
|
||||
userIdData := c.Ctx.Input.GetData("userId")
|
||||
userId := 0
|
||||
if userIdData != nil {
|
||||
if uid, ok := userIdData.(int); ok {
|
||||
userId = uid
|
||||
}
|
||||
}
|
||||
tenantIdData := c.Ctx.Input.GetData("tenantId")
|
||||
tenantId := 0
|
||||
if tenantIdData != nil {
|
||||
if tid, ok := tenantIdData.(int); ok {
|
||||
tenantId = tid
|
||||
}
|
||||
}
|
||||
usernameData := c.Ctx.Input.GetData("username")
|
||||
username := ""
|
||||
if usernameData != nil {
|
||||
if u, ok := usernameData.(string); ok {
|
||||
username = u
|
||||
}
|
||||
}
|
||||
rid := id
|
||||
log := &models.OperationLog{
|
||||
TenantId: tenantId,
|
||||
UserId: userId,
|
||||
Username: username,
|
||||
Module: "dict",
|
||||
ResourceType: "dict_item",
|
||||
ResourceId: &rid,
|
||||
Operation: "DELETE",
|
||||
Description: "删除字典项",
|
||||
IpAddress: c.Ctx.Input.IP(),
|
||||
UserAgent: c.Ctx.Input.Header("User-Agent"),
|
||||
RequestMethod: c.Ctx.Input.Method(),
|
||||
RequestUrl: c.Ctx.Input.URL(),
|
||||
Status: 1,
|
||||
Duration: 0,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_ = services.AddOperationLog(log)
|
||||
}()
|
||||
}
|
||||
|
||||
// GetDictItemsByCode 根据字典编码获取字典项(用于业务查询)
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"server/models"
|
||||
"server/services"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// OperationLogController 操作日志控制器
|
||||
type OperationLogController struct {
|
||||
web.Controller
|
||||
}
|
||||
|
||||
// GetOperationLogs 获取操作日志列表
|
||||
func (c *OperationLogController) GetOperationLogs() {
|
||||
// 获取租户ID和用户ID
|
||||
tenantIdData := c.Ctx.Input.GetData("tenantId")
|
||||
tenantId := 0
|
||||
if tenantIdData != nil {
|
||||
if tid, ok := tenantIdData.(int); ok {
|
||||
tenantId = tid
|
||||
}
|
||||
}
|
||||
|
||||
userIdData := c.Ctx.Input.GetData("userId")
|
||||
userId := 0
|
||||
if userIdData != nil {
|
||||
if uid, ok := userIdData.(int); ok {
|
||||
userId = uid
|
||||
}
|
||||
}
|
||||
|
||||
// 获取查询参数
|
||||
pageNum, _ := c.GetInt("page_num", 1)
|
||||
pageSize, _ := c.GetInt("page_size", 20)
|
||||
module := c.GetString("module")
|
||||
operation := c.GetString("operation")
|
||||
filterUserId, _ := c.GetInt("user_id", 0)
|
||||
startTimeStr := c.GetString("start_time")
|
||||
endTimeStr := c.GetString("end_time")
|
||||
|
||||
var startTime, endTime *time.Time
|
||||
|
||||
if startTimeStr != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", startTimeStr); err == nil {
|
||||
startTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
if endTimeStr != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", endTimeStr); err == nil {
|
||||
endTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
// 如果指定了用户ID筛选,使用该ID,否则使用当前用户ID
|
||||
queryUserId := filterUserId
|
||||
if queryUserId <= 0 {
|
||||
queryUserId = userId
|
||||
}
|
||||
|
||||
logs, total, err := services.GetOperationLogs(tenantId, queryUserId, module, operation, startTime, endTime, pageNum, pageSize)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "查询日志失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 为每条日志批量解析模块名称(减少对菜单表的重复查询)
|
||||
type LogWithModuleName struct {
|
||||
*models.OperationLog
|
||||
ModuleName string `json:"module_name"`
|
||||
}
|
||||
|
||||
// 收集唯一 module 列表
|
||||
moduleSet := make(map[string]struct{})
|
||||
modules := make([]string, 0)
|
||||
for _, l := range logs {
|
||||
m := l.Module
|
||||
if m == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := moduleSet[m]; !ok {
|
||||
moduleSet[m] = struct{}{}
|
||||
modules = append(modules, m)
|
||||
}
|
||||
}
|
||||
|
||||
moduleNamesMap := map[string]string{}
|
||||
if len(modules) > 0 {
|
||||
if mm, err := services.GetModuleNames(modules); err == nil {
|
||||
moduleNamesMap = mm
|
||||
}
|
||||
}
|
||||
|
||||
logsWithName := make([]*LogWithModuleName, len(logs))
|
||||
for i, log := range logs {
|
||||
name := ""
|
||||
if v, ok := moduleNamesMap[log.Module]; ok {
|
||||
name = v
|
||||
} else {
|
||||
// fallback 单个解析(保留老逻辑)
|
||||
name = services.GetModuleName(log.Module)
|
||||
}
|
||||
|
||||
logsWithName[i] = &LogWithModuleName{
|
||||
OperationLog: log,
|
||||
ModuleName: name,
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"data": logsWithName,
|
||||
"total": total,
|
||||
"page": pageNum,
|
||||
"page_size": pageSize,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetOperationLogById 根据ID获取操作日志
|
||||
func (c *OperationLogController) GetOperationLogById() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
log, err := services.GetOperationLogById(id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "日志不存在",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"data": log,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetUserStats 获取用户操作统计
|
||||
func (c *OperationLogController) GetUserStats() {
|
||||
userIdData := c.Ctx.Input.GetData("userId")
|
||||
userId := 0
|
||||
if userIdData != nil {
|
||||
if uid, ok := userIdData.(int); ok {
|
||||
userId = uid
|
||||
}
|
||||
}
|
||||
|
||||
tenantIdData := c.Ctx.Input.GetData("tenantId")
|
||||
tenantId := 0
|
||||
if tenantIdData != nil {
|
||||
if tid, ok := tenantIdData.(int); ok {
|
||||
tenantId = tid
|
||||
}
|
||||
}
|
||||
|
||||
days, _ := c.GetInt("days", 7)
|
||||
|
||||
stats, err := services.GetUserOperationStats(tenantId, userId, days)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "查询统计失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"data": stats,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetTenantStats 获取租户操作统计
|
||||
func (c *OperationLogController) GetTenantStats() {
|
||||
tenantIdData := c.Ctx.Input.GetData("tenantId")
|
||||
tenantId := 0
|
||||
if tenantIdData != nil {
|
||||
if tid, ok := tenantIdData.(int); ok {
|
||||
tenantId = tid
|
||||
}
|
||||
}
|
||||
|
||||
days, _ := c.GetInt("days", 7)
|
||||
|
||||
stats, err := services.GetTenantOperationStats(tenantId, days)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "查询统计失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"data": stats,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// ClearOldLogs 清空旧日志
|
||||
func (c *OperationLogController) ClearOldLogs() {
|
||||
keepDays, _ := c.GetInt("keep_days", 90)
|
||||
|
||||
rowsAffected, err := services.DeleteOldLogs(keepDays)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "删除日志失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "删除成功",
|
||||
"data": map[string]interface{}{
|
||||
"deleted_count": rowsAffected,
|
||||
"keep_days": keepDays,
|
||||
},
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"server/models"
|
||||
"server/services"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
// OperationLogMiddleware 操作日志中间件 - 记录所有的CREATE、UPDATE、DELETE操作
|
||||
func OperationLogMiddleware(ctx *context.Context) {
|
||||
// 记录所有重要操作,包括修改类(POST/PUT/PATCH/DELETE)和读取类(GET)用于统计账户访问功能
|
||||
method := ctx.Input.Method()
|
||||
|
||||
// 获取用户信息和租户信息(由 JWT 中间件设置在 Input.Data 中)
|
||||
userId := 0
|
||||
tenantId := 0
|
||||
username := ""
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 如果无法获取用户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"
|
||||
}
|
||||
}
|
||||
|
||||
// 读取请求体(对于有请求体的方法)
|
||||
var requestBody string
|
||||
if method == "POST" || method == "PUT" || method == "PATCH" {
|
||||
body, err := io.ReadAll(ctx.Request.Body)
|
||||
if err == nil {
|
||||
requestBody = string(body)
|
||||
// 重置请求体,使其可以被后续处理
|
||||
ctx.Request.Body = io.NopCloser(strings.NewReader(requestBody))
|
||||
}
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
// 使用延迟函数来记录操作
|
||||
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())
|
||||
|
||||
// 如果是读取/访问行为,写入访问日志(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"),
|
||||
RequestMethod: method,
|
||||
RequestUrl: ctx.Input.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
|
||||
}
|
||||
}
|
||||
|
||||
// 标记匿名访问信息(当 userId==0)
|
||||
if userId == 0 {
|
||||
if log.Description != "" {
|
||||
log.Description = "anonymous=true; " + log.Description
|
||||
} else {
|
||||
log.Description = "anonymous=true"
|
||||
}
|
||||
}
|
||||
|
||||
// 调用服务层保存日志
|
||||
if err := services.AddOperationLog(log); err != nil {
|
||||
fmt.Printf("Failed to save operation log: %v\n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// parseOperationType 根据HTTP方法解析操作类型
|
||||
func parseOperationType(method, url string) string {
|
||||
switch method {
|
||||
case "POST":
|
||||
// 检查URL是否包含特定的操作关键字
|
||||
if strings.Contains(url, "login") {
|
||||
return "LOGIN"
|
||||
}
|
||||
if strings.Contains(url, "logout") {
|
||||
return "LOGOUT"
|
||||
}
|
||||
if strings.Contains(url, "add") || strings.Contains(url, "create") {
|
||||
return "CREATE"
|
||||
}
|
||||
return "CREATE"
|
||||
case "PUT", "PATCH":
|
||||
return "UPDATE"
|
||||
case "DELETE":
|
||||
return "DELETE"
|
||||
default:
|
||||
return "READ"
|
||||
}
|
||||
}
|
||||
|
||||
// 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])
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// AccessLog 访问日志(针对读取/浏览/访问行为)
|
||||
type AccessLog struct {
|
||||
Id int64 `orm:"auto" json:"id"`
|
||||
TenantId int `orm:"column(tenant_id)" json:"tenant_id"`
|
||||
UserId int `orm:"column(user_id)" json:"user_id"`
|
||||
Username string `orm:"column(username)" json:"username"`
|
||||
Module string `orm:"column(module)" json:"module"`
|
||||
ResourceType string `orm:"column(resource_type)" json:"resource_type"`
|
||||
ResourceId *int `orm:"column(resource_id);null" json:"resource_id"`
|
||||
RequestUrl string `orm:"column(request_url);null" json:"request_url"`
|
||||
QueryString string `orm:"column(query_string);type(text);null" json:"query_string"`
|
||||
IpAddress string `orm:"column(ip_address);null" json:"ip_address"`
|
||||
UserAgent string `orm:"column(user_agent);null" json:"user_agent"`
|
||||
RequestMethod string `orm:"column(request_method);null" json:"request_method"`
|
||||
Duration int `orm:"column(duration);null" json:"duration"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add" json:"create_time"`
|
||||
}
|
||||
|
||||
func (a *AccessLog) TableName() string {
|
||||
return "sys_access_log"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// OperationLog 通用操作日志
|
||||
type OperationLog struct {
|
||||
Id int64 `orm:"auto" json:"id"`
|
||||
TenantId int `orm:"column(tenant_id)" json:"tenant_id"`
|
||||
UserId int `orm:"column(user_id)" json:"user_id"`
|
||||
Username string `orm:"column(username)" json:"username"`
|
||||
Module string `orm:"column(module)" json:"module"`
|
||||
ResourceType string `orm:"column(resource_type)" json:"resource_type"`
|
||||
ResourceId *int `orm:"column(resource_id);null" json:"resource_id"`
|
||||
Operation string `orm:"column(operation)" json:"operation"`
|
||||
Description string `orm:"column(description);null" json:"description"`
|
||||
OldValue string `orm:"column(old_value);type(longtext);null" json:"old_value"`
|
||||
NewValue string `orm:"column(new_value);type(longtext);null" json:"new_value"`
|
||||
IpAddress string `orm:"column(ip_address);null" json:"ip_address"`
|
||||
UserAgent string `orm:"column(user_agent);null" json:"user_agent"`
|
||||
RequestMethod string `orm:"column(request_method);null" json:"request_method"`
|
||||
RequestUrl string `orm:"column(request_url);null" json:"request_url"`
|
||||
Status int8 `orm:"column(status)" json:"status"`
|
||||
ErrorMessage string `orm:"column(error_message);type(text);null" json:"error_message"`
|
||||
Duration int `orm:"column(duration);null" json:"duration"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add" json:"create_time"`
|
||||
}
|
||||
|
||||
func (o *OperationLog) TableName() string {
|
||||
return "sys_operation_log"
|
||||
}
|
||||
@@ -46,6 +46,7 @@ func Init(version string) {
|
||||
orm.RegisterModel(new(KnowledgeTag))
|
||||
orm.RegisterModel(new(DictType))
|
||||
orm.RegisterModel(new(DictItem))
|
||||
orm.RegisterModel(new(OperationLog))
|
||||
|
||||
ormConfig, err := beego.AppConfig.String("orm")
|
||||
if err != nil {
|
||||
@@ -79,9 +80,9 @@ func Init(version string) {
|
||||
}
|
||||
|
||||
// 设置连接池参数
|
||||
dbConn.SetMaxIdleConns(10) // 设置空闲连接池中连接的最大数量
|
||||
dbConn.SetMaxOpenConns(100) // 设置打开数据库连接的最大数量
|
||||
dbConn.SetConnMaxLifetime(30 * time.Minute) // 设置连接可复用的最大时间(30分钟,避免连接过期)
|
||||
dbConn.SetMaxIdleConns(10) // 设置空闲连接池中连接的最大数量
|
||||
dbConn.SetMaxOpenConns(100) // 设置打开数据库连接的最大数量
|
||||
dbConn.SetConnMaxLifetime(30 * time.Minute) // 设置连接可复用的最大时间(30分钟,避免连接过期)
|
||||
// 注意:SetConnMaxIdleTime 在 Go 1.15+ 和 database/sql 1.4+ 中可用
|
||||
// 如果版本不支持,可以移除这一行
|
||||
// dbConn.SetConnMaxIdleTime(10 * time.Minute) // 设置空闲连接的最大空闲时间
|
||||
|
||||
@@ -207,6 +207,15 @@ func init() {
|
||||
}
|
||||
})
|
||||
|
||||
// 在请求完成后记录操作日志(包括访问/读取行为)
|
||||
beego.InsertFilter("/api/*", beego.FinishRouter, func(ctx *context.Context) {
|
||||
// 避免记录操作日志接口自身以免循环
|
||||
if strings.HasPrefix(ctx.Input.URL(), "/api/operation-logs") {
|
||||
return
|
||||
}
|
||||
middleware.OperationLogMiddleware(ctx)
|
||||
})
|
||||
|
||||
// 添加主页路由
|
||||
beego.Router("/", &controllers.MainController{})
|
||||
|
||||
@@ -329,4 +338,11 @@ func init() {
|
||||
beego.Router("/api/program-infos/public", &controllers.ProgramInfoController{}, "get:GetProgramInfosPublic")
|
||||
beego.Router("/api/files/public", &controllers.FileController{}, "get:GetFilesPublic")
|
||||
|
||||
// 操作日志路由
|
||||
beego.Router("/api/operation-logs", &controllers.OperationLogController{}, "get:GetOperationLogs")
|
||||
beego.Router("/api/operation-logs/:id", &controllers.OperationLogController{}, "get:GetOperationLogById")
|
||||
beego.Router("/api/operation-logs/user/stats", &controllers.OperationLogController{}, "get:GetUserStats")
|
||||
beego.Router("/api/operation-logs/tenant/stats", &controllers.OperationLogController{}, "get:GetTenantStats")
|
||||
beego.Router("/api/operation-logs/clear", &controllers.OperationLogController{}, "post:ClearOldLogs")
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"server/models"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// GetModuleName 根据module代码获取模块中文名称
|
||||
// 首先从菜单表查询,如果找不到则使用默认映射
|
||||
func GetModuleName(module string) string {
|
||||
if module == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 从菜单表中查询:优先匹配 permission,其次尝试匹配 path 的最后一段(如 /system/dict -> dict)
|
||||
o := orm.NewOrm()
|
||||
var menuName string
|
||||
// 先尝试通过 permission 精准匹配
|
||||
err := o.Raw(
|
||||
"SELECT name FROM yz_menus WHERE permission = ? AND delete_time IS NULL LIMIT 1",
|
||||
module,
|
||||
).QueryRow(&menuName)
|
||||
|
||||
if err == nil && menuName != "" {
|
||||
return menuName
|
||||
}
|
||||
|
||||
// 再尝试通过 path 末段匹配(例如 /system/dict -> dict)或 path 包含该段
|
||||
err = o.Raw(
|
||||
"SELECT name FROM yz_menus WHERE (path LIKE CONCAT('%/', ?, '/%') OR path LIKE CONCAT('%/', ?)) AND delete_time IS NULL LIMIT 1",
|
||||
module, module,
|
||||
).QueryRow(&menuName)
|
||||
|
||||
if err == nil && menuName != "" {
|
||||
return menuName
|
||||
}
|
||||
|
||||
// 如果菜单表中找不到,使用默认映射
|
||||
defaultMap := map[string]string{
|
||||
"auth": "认证",
|
||||
"dict": "字典管理",
|
||||
"user": "用户管理",
|
||||
"role": "角色管理",
|
||||
"permission": "权限管理",
|
||||
"department": "部门管理",
|
||||
"employee": "员工管理",
|
||||
"position": "职位管理",
|
||||
"tenant": "租户管理",
|
||||
"system": "系统设置",
|
||||
"oa": "OA",
|
||||
"menu": "菜单管理",
|
||||
"knowledge": "知识库",
|
||||
}
|
||||
|
||||
if name, exists := defaultMap[module]; exists {
|
||||
return name
|
||||
}
|
||||
|
||||
return module
|
||||
}
|
||||
|
||||
// GetModuleNames 批量获取模块名称,返回 module->name 映射
|
||||
func GetModuleNames(modules []string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
if len(modules) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
o := orm.NewOrm()
|
||||
|
||||
// 1. 尝试通过 permission 精准匹配
|
||||
placeholders := make([]string, 0)
|
||||
args := make([]interface{}, 0)
|
||||
for _, m := range modules {
|
||||
if m == "" {
|
||||
continue
|
||||
}
|
||||
placeholders = append(placeholders, "?")
|
||||
args = append(args, m)
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
query := "SELECT permission, name FROM yz_menus WHERE permission IN (" + strings.Join(placeholders, ",") + ") AND delete_time IS NULL"
|
||||
rows := make([]struct {
|
||||
Permission string
|
||||
Name string
|
||||
}, 0)
|
||||
_, err := o.Raw(query, args...).QueryRows(&rows)
|
||||
if err == nil {
|
||||
for _, r := range rows {
|
||||
if r.Permission != "" {
|
||||
result[r.Permission] = r.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 对于仍未匹配的模块,尝试通过 path 的末段进行模糊匹配
|
||||
missing := make([]string, 0)
|
||||
for _, m := range modules {
|
||||
if m == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := result[m]; !ok {
|
||||
missing = append(missing, m)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
// 使用 OR 组合 path LIKE '%/m' OR path LIKE '%/m/%'
|
||||
conds := make([]string, 0)
|
||||
args2 := make([]interface{}, 0)
|
||||
for _, m := range missing {
|
||||
conds = append(conds, "path LIKE ? OR path LIKE ?")
|
||||
args2 = append(args2, "%/"+m, "%/"+m+"/%")
|
||||
}
|
||||
query2 := "SELECT path, name FROM yz_menus WHERE (" + strings.Join(conds, " OR ") + ") AND delete_time IS NULL"
|
||||
rows2 := make([]struct {
|
||||
Path string
|
||||
Name string
|
||||
}, 0)
|
||||
_, err := o.Raw(query2, args2...).QueryRows(&rows2)
|
||||
if err == nil {
|
||||
for _, r := range rows2 {
|
||||
// extract last segment from path
|
||||
parts := strings.Split(strings.Trim(r.Path, "/"), "/")
|
||||
if len(parts) > 0 {
|
||||
key := parts[len(parts)-1]
|
||||
if key != "" {
|
||||
if _, exists := result[key]; !exists {
|
||||
result[key] = r.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 对于仍然没有匹配的,使用默认映射
|
||||
defaultMap := map[string]string{
|
||||
"auth": "认证",
|
||||
"dict": "字典管理",
|
||||
"user": "用户管理",
|
||||
"role": "角色管理",
|
||||
"permission": "权限管理",
|
||||
"department": "部门管理",
|
||||
"employee": "员工管理",
|
||||
"position": "职位管理",
|
||||
"tenant": "租户管理",
|
||||
"system": "系统设置",
|
||||
"oa": "OA",
|
||||
"menu": "菜单管理",
|
||||
"knowledge": "知识库",
|
||||
}
|
||||
for _, m := range modules {
|
||||
if m == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := result[m]; !exists {
|
||||
if v, ok := defaultMap[m]; ok {
|
||||
result[m] = v
|
||||
} else {
|
||||
result[m] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AddOperationLog 添加操作日志
|
||||
func AddOperationLog(log *models.OperationLog) error {
|
||||
o := orm.NewOrm()
|
||||
log.CreateTime = time.Now()
|
||||
_, err := o.Insert(log)
|
||||
if err != nil {
|
||||
return fmt.Errorf("添加操作日志失败: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddAccessLog 添加访问日志(用于记录 GET / READ 行为)
|
||||
func AddAccessLog(log *models.AccessLog) error {
|
||||
o := orm.NewOrm()
|
||||
log.CreateTime = time.Now()
|
||||
_, err := o.Insert(log)
|
||||
if err != nil {
|
||||
return fmt.Errorf("添加访问日志失败: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOperationLogs 获取操作日志列表
|
||||
func GetOperationLogs(tenantId int, userId int, module string, operation string, startTime *time.Time, endTime *time.Time, pageNum int, pageSize int) ([]*models.OperationLog, int64, error) {
|
||||
o := orm.NewOrm()
|
||||
qs := o.QueryTable("sys_operation_log").Filter("tenant_id", tenantId)
|
||||
|
||||
// 用户过滤
|
||||
if userId > 0 {
|
||||
qs = qs.Filter("user_id", userId)
|
||||
}
|
||||
|
||||
// 模块过滤
|
||||
if module != "" {
|
||||
qs = qs.Filter("module", module)
|
||||
}
|
||||
|
||||
// 操作类型过滤
|
||||
if operation != "" {
|
||||
qs = qs.Filter("operation", operation)
|
||||
}
|
||||
|
||||
// 时间范围过滤
|
||||
if startTime != nil {
|
||||
qs = qs.Filter("create_time__gte", startTime)
|
||||
}
|
||||
if endTime != nil {
|
||||
qs = qs.Filter("create_time__lte", endTime)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("查询日志总数失败: %v", err)
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
var logs []*models.OperationLog
|
||||
offset := (pageNum - 1) * pageSize
|
||||
_, err = qs.OrderBy("-create_time").Offset(offset).Limit(pageSize).All(&logs)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("查询操作日志失败: %v", err)
|
||||
}
|
||||
|
||||
return logs, total, nil
|
||||
}
|
||||
|
||||
// GetOperationLogById 根据ID获取操作日志
|
||||
func GetOperationLogById(id int64) (*models.OperationLog, error) {
|
||||
o := orm.NewOrm()
|
||||
log := &models.OperationLog{Id: id}
|
||||
err := o.Read(log)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("日志不存在: %v", err)
|
||||
}
|
||||
return log, nil
|
||||
}
|
||||
|
||||
// GetUserOperationStats 获取用户操作统计
|
||||
func GetUserOperationStats(tenantId int, userId int, days int) (map[string]interface{}, error) {
|
||||
o := orm.NewOrm()
|
||||
|
||||
startTime := time.Now().AddDate(0, 0, -days)
|
||||
|
||||
// 获取总操作数
|
||||
var totalCount int64
|
||||
err := o.Raw(
|
||||
"SELECT COUNT(*) FROM sys_operation_log WHERE tenant_id = ? AND user_id = ? AND create_time >= ?",
|
||||
tenantId, userId, startTime,
|
||||
).QueryRow(&totalCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取按模块分组的操作数
|
||||
type ModuleCount struct {
|
||||
Module string
|
||||
Count int
|
||||
}
|
||||
var moduleCounts []ModuleCount
|
||||
_, err = o.Raw(
|
||||
"SELECT module, COUNT(*) as count FROM sys_operation_log WHERE tenant_id = ? AND user_id = ? AND create_time >= ? GROUP BY module",
|
||||
tenantId, userId, startTime,
|
||||
).QueryRows(&moduleCounts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建结果
|
||||
stats := map[string]interface{}{
|
||||
"total_operations": totalCount,
|
||||
"module_stats": moduleCounts,
|
||||
"period_days": days,
|
||||
"from_time": startTime,
|
||||
"to_time": time.Now(),
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetTenantOperationStats 获取租户操作统计
|
||||
func GetTenantOperationStats(tenantId int, days int) (map[string]interface{}, error) {
|
||||
o := orm.NewOrm()
|
||||
|
||||
startTime := time.Now().AddDate(0, 0, -days)
|
||||
|
||||
// 获取总操作数
|
||||
var totalCount int64
|
||||
err := o.Raw(
|
||||
"SELECT COUNT(*) FROM sys_operation_log WHERE tenant_id = ? AND create_time >= ?",
|
||||
tenantId, startTime,
|
||||
).QueryRow(&totalCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取按用户分组的操作数(Top 10)
|
||||
type UserCount struct {
|
||||
UserId int
|
||||
Username string
|
||||
Count int
|
||||
}
|
||||
var userCounts []UserCount
|
||||
_, err = o.Raw(
|
||||
"SELECT user_id, username, COUNT(*) as count FROM sys_operation_log WHERE tenant_id = ? AND create_time >= ? GROUP BY user_id, username ORDER BY count DESC LIMIT 10",
|
||||
tenantId, startTime,
|
||||
).QueryRows(&userCounts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取按操作类型分组的操作数
|
||||
type OperationCount struct {
|
||||
Operation string
|
||||
Count int
|
||||
}
|
||||
var operationCounts []OperationCount
|
||||
_, err = o.Raw(
|
||||
"SELECT operation, COUNT(*) as count FROM sys_operation_log WHERE tenant_id = ? AND create_time >= ? GROUP BY operation",
|
||||
tenantId, startTime,
|
||||
).QueryRows(&operationCounts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建结果
|
||||
stats := map[string]interface{}{
|
||||
"total_operations": totalCount,
|
||||
"top_users": userCounts,
|
||||
"operation_breakdown": operationCounts,
|
||||
"period_days": days,
|
||||
"from_time": startTime,
|
||||
"to_time": time.Now(),
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// DeleteOldLogs 删除旧日志(保留指定天数)
|
||||
func DeleteOldLogs(keepDays int) (int64, error) {
|
||||
o := orm.NewOrm()
|
||||
cutoffTime := time.Now().AddDate(0, 0, -keepDays)
|
||||
|
||||
result, err := o.Raw("DELETE FROM sys_operation_log WHERE create_time < ?", cutoffTime).Exec()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("删除旧日志失败: %v", err)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("获取受影响行数失败: %v", err)
|
||||
}
|
||||
|
||||
return rowsAffected, nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
-- 通用操作日志表(记录所有用户在所有模块的操作)
|
||||
CREATE TABLE `sys_operation_log` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||||
`tenant_id` int(11) NOT NULL DEFAULT 0 COMMENT '租户ID(0表示平台操作,>0表示租户操作)',
|
||||
`user_id` int(11) NOT NULL COMMENT '操作用户ID',
|
||||
`username` varchar(50) NOT NULL COMMENT '操作用户名',
|
||||
`module` varchar(100) NOT NULL COMMENT '操作模块(user/tenant/dict/role等)',
|
||||
`resource_type` varchar(50) NOT NULL COMMENT '资源类型(如User/Tenant/Dict等)',
|
||||
`resource_id` int(11) NULL COMMENT '资源ID(如被操作的用户ID、租户ID等)',
|
||||
`operation` varchar(20) NOT NULL COMMENT '操作类型(CREATE/UPDATE/DELETE/LOGIN/LOGOUT/VIEW等)',
|
||||
`description` varchar(500) NULL COMMENT '操作描述',
|
||||
`old_value` longtext NULL COMMENT '修改前的值(JSON格式,用于UPDATE操作)',
|
||||
`new_value` longtext NULL COMMENT '修改后的值(JSON格式,用于UPDATE操作)',
|
||||
`ip_address` varchar(50) NULL COMMENT 'IP地址',
|
||||
`user_agent` varchar(500) NULL COMMENT '用户代理信息',
|
||||
`request_method` varchar(10) NULL COMMENT '请求方法(GET/POST/PUT/DELETE等)',
|
||||
`request_url` varchar(500) NULL COMMENT '请求URL',
|
||||
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1-成功,0-失败',
|
||||
`error_message` text NULL COMMENT '错误信息',
|
||||
`duration` int(11) NULL COMMENT '执行时长(毫秒)',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_id` (`tenant_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_module` (`module`),
|
||||
KEY `idx_resource_type` (`resource_type`),
|
||||
KEY `idx_resource_id` (`resource_id`),
|
||||
KEY `idx_operation` (`operation`),
|
||||
KEY `idx_create_time` (`create_time`),
|
||||
KEY `idx_tenant_user_time` (`tenant_id`, `user_id`, `create_time`),
|
||||
KEY `idx_tenant_module_time` (`tenant_id`, `module`, `create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表';
|
||||
Binary file not shown.
Reference in New Issue
Block a user