feat: 添加日志管理和敏感词过滤功能

- 新增日志管理模块:操作日志、登录日志、数据变更日志
- 新增敏感词过滤器 (sanitizer)
- 新增日志异步写入管理器
- 新增日志定时清理服务
- 优化帖子相关服务和上传服务
This commit is contained in:
2026-03-15 02:25:10 +08:00
parent d3065b30cc
commit ebd9cb8b04
31 changed files with 2753 additions and 60 deletions

View File

@@ -0,0 +1,215 @@
package handler
import (
"strconv"
"time"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/repository"
"carrot_bbs/internal/service"
"github.com/gin-gonic/gin"
)
// AdminLogHandler 管理后台日志处理器
type AdminLogHandler struct {
operationLogService service.OperationLogService
loginLogService service.LoginLogService
dataChangeLogService service.DataChangeLogService
}
// NewAdminLogHandler 创建管理后台日志处理器
func NewAdminLogHandler(
operationLogService service.OperationLogService,
loginLogService service.LoginLogService,
dataChangeLogService service.DataChangeLogService,
) *AdminLogHandler {
return &AdminLogHandler{
operationLogService: operationLogService,
loginLogService: loginLogService,
dataChangeLogService: dataChangeLogService,
}
}
// GetOperationLogs 获取操作日志列表
// GET /api/v1/admin/logs/operations
func (h *AdminLogHandler) GetOperationLogs(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
filters := repository.LogFilter{
UserID: c.Query("user_id"),
Operation: c.Query("operation"),
TargetType: c.Query("target_type"),
TargetID: c.Query("target_id"),
Status: c.Query("status"),
IP: c.Query("ip"),
StartTime: c.Query("start_time"),
EndTime: c.Query("end_time"),
}
logs, total, err := h.operationLogService.GetOperationLogs(c.Request.Context(), filters, page, pageSize)
if err != nil {
response.HandleError(c, err, "failed to get operation logs")
return
}
response.Success(c, gin.H{
"logs": logs,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
},
})
}
// GetLoginLogs 获取登录日志列表
// GET /api/v1/admin/logs/logins
func (h *AdminLogHandler) GetLoginLogs(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
filters := repository.LoginFilter{
UserID: c.Query("user_id"),
Event: c.Query("event"),
Result: c.Query("result"),
FailReason: c.Query("fail_reason"),
LoginType: c.Query("login_type"),
IP: c.Query("ip"),
}
if startTime := c.Query("start_time"); startTime != "" {
if t, err := time.Parse(time.RFC3339, startTime); err == nil {
filters.StartTime = t
}
}
if endTime := c.Query("end_time"); endTime != "" {
if t, err := time.Parse(time.RFC3339, endTime); err == nil {
filters.EndTime = t
}
}
logs, total, err := h.loginLogService.GetLoginLogs(c.Request.Context(), filters, page, pageSize)
if err != nil {
response.HandleError(c, err, "failed to get login logs")
return
}
response.Success(c, gin.H{
"logs": logs,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
},
})
}
// GetDataChangeLogs 获取数据变更日志列表
// GET /api/v1/admin/logs/data-changes
func (h *AdminLogHandler) GetDataChangeLogs(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
filters := repository.DataChangeFilter{
UserID: c.Query("user_id"),
OperatorID: c.Query("operator_id"),
ChangeType: c.Query("change_type"),
TargetType: c.Query("target_type"),
OperatorType: c.Query("operator_type"),
IP: c.Query("ip"),
StartTime: c.Query("start_time"),
EndTime: c.Query("end_time"),
}
logs, total, err := h.dataChangeLogService.GetDataChangeLogs(c.Request.Context(), filters, page, pageSize)
if err != nil {
response.HandleError(c, err, "failed to get data change logs")
return
}
response.Success(c, gin.H{
"logs": logs,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
},
})
}
// GetLogStatistics 获取日志统计
// GET /api/v1/admin/logs/statistics
func (h *AdminLogHandler) GetLogStatistics(c *gin.Context) {
startTime := c.DefaultQuery("start_time", "")
endTime := c.DefaultQuery("end_time", "")
operationStats, err := h.operationLogService.GetStatistics(c.Request.Context(), startTime, endTime)
if err != nil {
response.HandleError(c, err, "failed to get operation log statistics")
return
}
loginStats, err := h.loginLogService.GetStatistics(c.Request.Context(), startTime, endTime)
if err != nil {
response.HandleError(c, err, "failed to get login log statistics")
return
}
dataChangeStats, err := h.dataChangeLogService.GetStatistics(c.Request.Context(), startTime, endTime)
if err != nil {
response.HandleError(c, err, "failed to get data change log statistics")
return
}
response.Success(c, gin.H{
"operation_logs": operationStats,
"login_logs": loginStats,
"data_change_logs": dataChangeStats,
})
}
// ExportLogs 导出日志
// GET /api/v1/admin/logs/export
func (h *AdminLogHandler) ExportLogs(c *gin.Context) {
logType := c.Query("type")
startTime := c.Query("start_time")
endTime := c.Query("end_time")
switch logType {
case "operation":
logs, _, err := h.operationLogService.GetOperationLogsByTimeRange(c.Request.Context(), startTime, endTime, 1, 10000)
if err != nil {
response.HandleError(c, err, "failed to export operation logs")
return
}
response.Success(c, gin.H{
"type": "operation",
"logs": logs,
})
case "login":
logs, _, err := h.loginLogService.GetLoginLogs(c.Request.Context(), repository.LoginFilter{}, 1, 10000)
if err != nil {
response.HandleError(c, err, "failed to export login logs")
return
}
response.Success(c, gin.H{
"type": "login",
"log": logs,
})
case "data_change":
logs, _, err := h.dataChangeLogService.GetDataChangeLogs(c.Request.Context(), repository.DataChangeFilter{}, 1, 10000)
if err != nil {
response.HandleError(c, err, "failed to export data change logs")
return
}
response.Success(c, gin.H{
"type": "data_change",
"log": logs,
})
default:
response.Error(c, 400, "invalid log type")
}
}

View File

@@ -33,13 +33,17 @@ func (h *UploadHandler) UploadImage(c *gin.Context) {
return
}
url, err := h.uploadService.UploadImage(c.Request.Context(), file)
url, previewURL, previewURLLarge, err := h.uploadService.UploadImage(c.Request.Context(), file)
if err != nil {
response.InternalServerError(c, "failed to upload image")
return
}
response.Success(c, gin.H{"url": url})
response.Success(c, gin.H{
"url": url,
"preview_url": previewURL,
"preview_url_large": previewURLLarge,
})
}
// UploadAvatar 上传头像

View File

@@ -2,6 +2,8 @@ package handler
import (
"context"
"crypto/sha256"
"fmt"
"strconv"
"github.com/gin-gonic/gin"
@@ -17,6 +19,7 @@ type UserHandler struct {
userService service.UserService
activityService service.UserActivityService // 用户活跃统计服务
jwtService *service.JWTService
logService *service.LogService
}
// NewUserHandler 创建用户处理器
@@ -27,6 +30,28 @@ func NewUserHandler(userService service.UserService, activityService service.Use
}
}
// SetJWTService 设置JWT服务
func (h *UserHandler) SetJWTService(jwtService *service.JWTService) {
h.jwtService = jwtService
}
// SetActivityService 设置活跃统计服务
func (h *UserHandler) SetActivityService(activityService service.UserActivityService) {
h.activityService = activityService
}
// SetLogService 设置日志服务
func (h *UserHandler) SetLogService(logService *service.LogService) {
h.logService = logService
}
// generateTokenID 生成Token ID
func generateTokenID(token string) string {
h := sha256.New()
h.Write([]byte(token))
return fmt.Sprintf("%x", h.Sum(nil))[:16]
}
// Register 用户注册
func (h *UserHandler) Register(c *gin.Context) {
type RegisterRequest struct {
@@ -54,6 +79,35 @@ func (h *UserHandler) Register(c *gin.Context) {
accessToken, _ := h.jwtService.GenerateAccessToken(user.ID, user.Username)
refreshToken, _ := h.jwtService.GenerateRefreshToken(user.ID, user.Username)
ip := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
// 记录用户活跃
if h.activityService != nil {
go func() {
ctx := context.Background()
loginType := model.LoginTypeLogin
h.activityService.RecordUserActive(ctx, user.ID, loginType, ip, userAgent)
}()
}
// 记录注册后首次登录
if h.logService != nil {
go func() {
h.logService.LoginLog.RecordLogin(&model.LoginLog{
UserID: user.ID,
UserName: user.Username,
NickName: user.Nickname,
LoginType: string(model.LoginTypePassword),
Event: string(model.LoginEventLogin),
IP: ip,
UserAgent: userAgent,
Result: string(model.LoginResultSuccess),
TokenID: generateTokenID(refreshToken),
})
}()
}
response.Success(c, gin.H{
"user": dto.ConvertUserToResponse(user),
"token": accessToken,
@@ -94,17 +148,36 @@ func (h *UserHandler) Login(c *gin.Context) {
accessToken, _ := h.jwtService.GenerateAccessToken(user.ID, user.Username)
refreshToken, _ := h.jwtService.GenerateRefreshToken(user.ID, user.Username)
ip := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
// 记录用户活跃
if h.activityService != nil {
go func() {
ctx := context.Background()
loginType := model.LoginTypeLogin
ip := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
h.activityService.RecordUserActive(ctx, user.ID, loginType, ip, userAgent)
}()
}
// 更新登录日志补充IP和UserAgent
if h.logService != nil {
ip := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
go func() {
h.logService.LoginLog.RecordLogin(&model.LoginLog{
UserID: user.ID,
UserName: user.Username,
NickName: user.Nickname,
LoginType: string(model.LoginTypePassword),
Event: string(model.LoginEventLogin),
IP: ip,
UserAgent: userAgent,
Result: string(model.LoginResultSuccess),
})
}()
}
response.Success(c, gin.H{
"user": dto.ConvertUserToResponse(user),
"token": accessToken,
@@ -364,18 +437,43 @@ func (h *UserHandler) RefreshToken(c *gin.Context) {
return
}
// 获取用户信息
user, err := h.userService.GetUserByID(c.Request.Context(), claims.UserID)
if err != nil {
response.InternalServerError(c, "user not found")
return
}
// 生成新 token
accessToken, _ := h.jwtService.GenerateAccessToken(claims.UserID, claims.Username)
refreshToken, _ := h.jwtService.GenerateRefreshToken(claims.UserID, claims.Username)
accessToken, _ := h.jwtService.GenerateAccessToken(user.ID, user.Username)
refreshToken, _ := h.jwtService.GenerateRefreshToken(user.ID, user.Username)
ip := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
// 记录用户活跃
if h.activityService != nil {
go func() {
ctx := context.Background()
loginType := model.LoginTypeRefresh
ip := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
h.activityService.RecordUserActive(ctx, claims.UserID, loginType, ip, userAgent)
h.activityService.RecordUserActive(ctx, user.ID, loginType, ip, userAgent)
}()
}
// 记录Token刷新日志
if h.logService != nil {
go func() {
h.logService.LoginLog.RecordLogin(&model.LoginLog{
UserID: user.ID,
UserName: user.Username,
NickName: user.Nickname,
LoginType: string(model.LoginTypePassword),
Event: string(model.LoginEventRefresh),
IP: ip,
UserAgent: userAgent,
Result: string(model.LoginResultSuccess),
TokenID: generateTokenID(req.RefreshToken),
})
}()
}
@@ -385,16 +483,6 @@ func (h *UserHandler) RefreshToken(c *gin.Context) {
})
}
// SetJWTService 设置JWT服务
func (h *UserHandler) SetJWTService(jwtSvc *service.JWTService) {
h.jwtService = jwtSvc
}
// SetActivityService 设置活跃统计服务
func (h *UserHandler) SetActivityService(activityService service.UserActivityService) {
h.activityService = activityService
}
// FollowUser 关注用户
func (h *UserHandler) FollowUser(c *gin.Context) {
userID := c.Param("id")
@@ -657,6 +745,40 @@ func (h *UserHandler) SendChangePasswordCode(c *gin.Context) {
response.Success(c, gin.H{"success": true})
}
// Logout 用户登出
func (h *UserHandler) Logout(c *gin.Context) {
currentUserID := c.GetString("user_id")
if currentUserID == "" {
response.Unauthorized(c, "not logged in")
return
}
ip := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
// 获取用户信息
user, err := h.userService.GetUserByID(c.Request.Context(), currentUserID)
if err != nil {
response.InternalServerError(c, "user not found")
return
}
// 记录登出日志
if h.logService != nil {
h.logService.LoginLog.RecordLogin(&model.LoginLog{
UserID: user.ID,
UserName: user.Username,
NickName: user.Nickname,
Event: string(model.LoginEventLogout),
IP: ip,
UserAgent: userAgent,
Result: string(model.LoginResultSuccess),
})
}
response.Success(c, gin.H{"success": true})
}
// Search 搜索用户
func (h *UserHandler) Search(c *gin.Context) {
keyword := c.Query("keyword")