refactor: replace standard log with zap logger and optimize code patterns
- Migrate all log.Printf/log.Println calls to structured zap logging - Use cmp.Or for cleaner default value handling - Replace manual loops with slices.ContainsFunc/IndexFunc - Add batch query methods (IsLikedBatch, IsFavoritedBatch) to solve N+1 problem - Update JSON tags from omitempty to omitzero for numeric fields - Use errgroup-style wg.Go for worker goroutines - Remove duplicate casbin/v2 dependency (keeping v3) - Add plans/ to gitignore
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
@@ -67,9 +68,11 @@ func NewAsyncLogManager(
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < cfg.WorkerCount; i++ {
|
||||
m.wg.Add(1)
|
||||
go m.worker(i, operationRepo, loginRepo, dataChangeRepo)
|
||||
for i := range cfg.WorkerCount {
|
||||
workerID := i
|
||||
m.wg.Go(func() {
|
||||
m.worker(workerID, operationRepo, loginRepo, dataChangeRepo)
|
||||
})
|
||||
}
|
||||
|
||||
go m.flushScheduler()
|
||||
@@ -91,8 +94,6 @@ func (m *AsyncLogManager) worker(
|
||||
loginRepo *repository.LoginLogRepository,
|
||||
dataChangeRepo *repository.DataChangeLogRepository,
|
||||
) {
|
||||
defer m.wg.Done()
|
||||
|
||||
opBatch := make([]*model.OperationLog, 0, m.cfg.BatchSize)
|
||||
loginBatch := make([]*model.LoginLog, 0, m.cfg.BatchSize)
|
||||
changeBatch := make([]*model.DataChangeLog, 0, m.cfg.BatchSize)
|
||||
@@ -228,9 +229,7 @@ func (m *AsyncLogManager) writeToFallback(logType string, logs interface{}) erro
|
||||
|
||||
// initFallbackWriter 初始化降级写入器
|
||||
func (m *AsyncLogManager) initFallbackWriter() error {
|
||||
if m.cfg.FallbackPath == "" {
|
||||
m.cfg.FallbackPath = "./logs/fallback.log"
|
||||
}
|
||||
m.cfg.FallbackPath = cmp.Or(m.cfg.FallbackPath, "./logs/fallback.log")
|
||||
|
||||
dir := "./logs"
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/openai"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -97,7 +97,9 @@ func (s *auditServiceImpl) AuditText(ctx context.Context, text string, auditType
|
||||
// 使用 AI 审核文本
|
||||
approved, reason, err := s.openaiClient.ModerateComment(ctx, text, nil)
|
||||
if err != nil {
|
||||
log.Printf("AI audit text error: %v", err)
|
||||
zap.L().Error("AI audit text error",
|
||||
zap.Error(err),
|
||||
)
|
||||
if s.config.StrictModeration {
|
||||
return &AuditResult{
|
||||
Pass: false,
|
||||
@@ -163,7 +165,9 @@ func (s *auditServiceImpl) AuditImage(ctx context.Context, imageURL string) (*Au
|
||||
// 使用 AI 审核图片
|
||||
approved, reason, err := s.openaiClient.ModerateComment(ctx, "", []string{imageURL})
|
||||
if err != nil {
|
||||
log.Printf("AI audit image error: %v", err)
|
||||
zap.L().Error("AI audit image error",
|
||||
zap.Error(err),
|
||||
)
|
||||
if s.config.StrictModeration {
|
||||
return &AuditResult{
|
||||
Pass: false,
|
||||
@@ -219,7 +223,9 @@ func (s *auditServiceImpl) AuditPost(ctx context.Context, title, content string,
|
||||
// 使用 AI 审核帖子
|
||||
approved, reason, err := s.openaiClient.ModeratePost(ctx, title, content, images)
|
||||
if err != nil {
|
||||
log.Printf("AI audit post error: %v", err)
|
||||
zap.L().Error("AI audit post error",
|
||||
zap.Error(err),
|
||||
)
|
||||
if s.config.StrictModeration {
|
||||
return &AuditResult{
|
||||
Pass: false,
|
||||
@@ -279,7 +285,9 @@ func (s *auditServiceImpl) AuditComment(ctx context.Context, content string, ima
|
||||
// 使用 AI 审核评论
|
||||
approved, reason, err := s.openaiClient.ModerateComment(ctx, content, images)
|
||||
if err != nil {
|
||||
log.Printf("AI audit comment error: %v", err)
|
||||
zap.L().Error("AI audit comment error",
|
||||
zap.Error(err),
|
||||
)
|
||||
if s.config.StrictModeration {
|
||||
return &AuditResult{
|
||||
Pass: false,
|
||||
@@ -395,7 +403,9 @@ func (s *auditServiceImpl) saveAuditLog(ctx context.Context, contentType, conten
|
||||
}
|
||||
|
||||
if err := s.db.Create(&auditLog).Error; err != nil {
|
||||
log.Printf("Failed to save audit log: %v", err)
|
||||
zap.L().Error("Failed to save audit log",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,7 +429,11 @@ func (c *AuditCallback) HandleTextCallback(ctx context.Context, auditID string,
|
||||
return fmt.Errorf("invalid parameters")
|
||||
}
|
||||
|
||||
log.Printf("Processing text audit callback: auditID=%s, result=%+v", auditID, result)
|
||||
zap.L().Debug("Processing text audit callback",
|
||||
zap.String("auditID", auditID),
|
||||
zap.Bool("pass", result.Pass),
|
||||
zap.String("risk", result.Risk),
|
||||
)
|
||||
|
||||
// 根据审核结果执行相应操作
|
||||
// 例如: 更新帖子状态、发送通知等
|
||||
@@ -433,7 +447,11 @@ func (c *AuditCallback) HandleImageCallback(ctx context.Context, auditID string,
|
||||
return fmt.Errorf("invalid parameters")
|
||||
}
|
||||
|
||||
log.Printf("Processing image audit callback: auditID=%s, result=%+v", auditID, result)
|
||||
zap.L().Debug("Processing image audit callback",
|
||||
zap.String("auditID", auditID),
|
||||
zap.Bool("pass", result.Pass),
|
||||
zap.String("risk", result.Risk),
|
||||
)
|
||||
|
||||
// 根据审核结果执行相应操作
|
||||
// 例如: 更新图片状态、删除违规图片等
|
||||
@@ -568,7 +586,7 @@ func (s *AuditScheduler) processPendingAudits() {
|
||||
// 3. 更新审核状态
|
||||
|
||||
// 示例逻辑,实际需要根据业务需求实现
|
||||
log.Println("Processing pending audits...")
|
||||
zap.L().Debug("Processing pending audits")
|
||||
}
|
||||
|
||||
// CleanupOldLogs 清理旧的审核日志
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/cache"
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"carrot_bbs/internal/pkg/sse"
|
||||
"carrot_bbs/internal/repository"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -298,7 +299,12 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
|
||||
// 异步写入缓存
|
||||
go func() {
|
||||
if err := s.cacheMessage(context.Background(), conversationID, message); err != nil {
|
||||
log.Printf("[ChatService] async cache message failed, convID=%s, msgID=%s, err=%v", conversationID, message.ID, err)
|
||||
zap.L().Warn("async cache message failed",
|
||||
zap.String("component", "ChatService"),
|
||||
zap.String("convID", conversationID),
|
||||
zap.String("msgID", message.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -379,12 +385,9 @@ func (s *chatServiceImpl) cacheMessage(ctx context.Context, convID string, msg *
|
||||
}
|
||||
|
||||
func containsImageSegment(segments model.MessageSegments) bool {
|
||||
for _, seg := range segments {
|
||||
if seg.Type == string(model.ContentTypeImage) || seg.Type == "image" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.ContainsFunc(segments, func(seg model.MessageSegment) bool {
|
||||
return seg.Type == string(model.ContentTypeImage) || seg.Type == "image"
|
||||
})
|
||||
}
|
||||
|
||||
// GetMessages 获取消息历史(分页,带缓存)
|
||||
@@ -718,7 +721,12 @@ func (s *chatServiceImpl) SaveMessage(ctx context.Context, senderID string, conv
|
||||
// 异步写入缓存
|
||||
go func() {
|
||||
if err := s.cacheMessage(context.Background(), conversationID, message); err != nil {
|
||||
log.Printf("[ChatService] async cache message failed, convID=%s, msgID=%s, err=%v", conversationID, message.ID, err)
|
||||
zap.L().Warn("async cache message failed",
|
||||
zap.String("component", "ChatService"),
|
||||
zap.String("convID", conversationID),
|
||||
zap.String("msgID", message.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"carrot_bbs/internal/cache"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/gorse"
|
||||
"carrot_bbs/internal/repository"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// CommentService 评论服务
|
||||
@@ -115,11 +116,16 @@ func (s *CommentService) reviewCommentAsync(
|
||||
// 未启用AI时,直接通过审核并发送后续通知
|
||||
if s.postAIService == nil || !s.postAIService.IsEnabled() {
|
||||
if err := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished); err != nil {
|
||||
log.Printf("[WARN] Failed to publish comment without AI moderation: %v", err)
|
||||
zap.L().Warn("Failed to publish comment without AI moderation",
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
if err := s.applyCommentPublishedStats(commentID); err != nil {
|
||||
log.Printf("[WARN] Failed to apply published stats for comment %s: %v", commentID, err)
|
||||
zap.L().Warn("Failed to apply published stats for comment",
|
||||
zap.String("commentID", commentID),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
s.invalidatePostCaches(postID)
|
||||
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
|
||||
@@ -131,7 +137,10 @@ func (s *CommentService) reviewCommentAsync(
|
||||
var rejectedErr *CommentModerationRejectedError
|
||||
if errors.As(err, &rejectedErr) {
|
||||
if delErr := s.commentRepo.Delete(commentID); delErr != nil {
|
||||
log.Printf("[WARN] Failed to delete rejected comment %s: %v", commentID, delErr)
|
||||
zap.L().Warn("Failed to delete rejected comment",
|
||||
zap.String("commentID", commentID),
|
||||
zap.Error(delErr),
|
||||
)
|
||||
}
|
||||
s.notifyCommentModerationRejected(userID, rejectedErr.Reason)
|
||||
return
|
||||
@@ -139,24 +148,39 @@ func (s *CommentService) reviewCommentAsync(
|
||||
|
||||
// 审核服务异常时降级放行,避免评论长期pending
|
||||
if updateErr := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished); updateErr != nil {
|
||||
log.Printf("[WARN] Failed to publish comment %s after moderation error: %v", commentID, updateErr)
|
||||
zap.L().Warn("Failed to publish comment after moderation error",
|
||||
zap.String("commentID", commentID),
|
||||
zap.Error(updateErr),
|
||||
)
|
||||
return
|
||||
}
|
||||
if statsErr := s.applyCommentPublishedStats(commentID); statsErr != nil {
|
||||
log.Printf("[WARN] Failed to apply published stats for comment %s: %v", commentID, statsErr)
|
||||
zap.L().Warn("Failed to apply published stats for comment",
|
||||
zap.String("commentID", commentID),
|
||||
zap.Error(statsErr),
|
||||
)
|
||||
}
|
||||
s.invalidatePostCaches(postID)
|
||||
log.Printf("[WARN] Comment moderation failed, fallback publish comment=%s err=%v", commentID, err)
|
||||
zap.L().Warn("Comment moderation failed, fallback publish comment",
|
||||
zap.String("commentID", commentID),
|
||||
zap.Error(err),
|
||||
)
|
||||
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
|
||||
return
|
||||
}
|
||||
|
||||
if updateErr := s.commentRepo.UpdateModerationStatus(commentID, model.CommentStatusPublished); updateErr != nil {
|
||||
log.Printf("[WARN] Failed to publish comment %s: %v", commentID, updateErr)
|
||||
zap.L().Warn("Failed to publish comment",
|
||||
zap.String("commentID", commentID),
|
||||
zap.Error(updateErr),
|
||||
)
|
||||
return
|
||||
}
|
||||
if statsErr := s.applyCommentPublishedStats(commentID); statsErr != nil {
|
||||
log.Printf("[WARN] Failed to apply published stats for comment %s: %v", commentID, statsErr)
|
||||
zap.L().Warn("Failed to apply published stats for comment",
|
||||
zap.String("commentID", commentID),
|
||||
zap.Error(statsErr),
|
||||
)
|
||||
}
|
||||
s.invalidatePostCaches(postID)
|
||||
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
|
||||
@@ -184,7 +208,9 @@ func (s *CommentService) afterCommentPublished(userID, postID, commentID string,
|
||||
if parentUserID != userID {
|
||||
notifyErr := s.systemMessageService.SendReplyNotification(context.Background(), parentUserID, userID, postID, *parentID, commentID)
|
||||
if notifyErr != nil {
|
||||
log.Printf("[ERROR] Error sending reply notification: %v", notifyErr)
|
||||
zap.L().Error("Error sending reply notification",
|
||||
zap.Error(notifyErr),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -192,7 +218,9 @@ func (s *CommentService) afterCommentPublished(userID, postID, commentID string,
|
||||
if postOwnerID != userID {
|
||||
notifyErr := s.systemMessageService.SendCommentNotification(context.Background(), postOwnerID, userID, postID, commentID)
|
||||
if notifyErr != nil {
|
||||
log.Printf("[ERROR] Error sending comment notification: %v", notifyErr)
|
||||
zap.L().Error("Error sending comment notification",
|
||||
zap.Error(notifyErr),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,7 +231,9 @@ func (s *CommentService) afterCommentPublished(userID, postID, commentID string,
|
||||
go func() {
|
||||
if s.gorseClient.IsEnabled() {
|
||||
if err := s.gorseClient.InsertFeedback(context.Background(), gorse.FeedbackTypeComment, userID, postID); err != nil {
|
||||
log.Printf("[WARN] Failed to insert comment feedback to Gorse: %v", err)
|
||||
zap.L().Warn("Failed to insert comment feedback to Gorse",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -226,7 +256,9 @@ func (s *CommentService) notifyCommentModerationRejected(userID, reason string)
|
||||
"评论审核未通过",
|
||||
content,
|
||||
); err != nil {
|
||||
log.Printf("[WARN] Failed to send comment moderation reject notification: %v", err)
|
||||
zap.L().Warn("Failed to send comment moderation reject notification",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -307,7 +339,9 @@ func (s *CommentService) Like(ctx context.Context, commentID, userID string) err
|
||||
)
|
||||
}
|
||||
if notifyErr != nil {
|
||||
log.Printf("[ERROR] Error sending like notification: %v", notifyErr)
|
||||
zap.L().Error("Error sending like notification",
|
||||
zap.Error(notifyErr),
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"maps"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
"carrot_bbs/internal/pkg/utils"
|
||||
"carrot_bbs/internal/repository"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -137,7 +140,10 @@ type groupNoticeMessage struct {
|
||||
func (s *groupService) publishGroupNotice(groupID string, notice groupNoticeMessage) {
|
||||
members, _, err := s.groupRepo.GetMembers(groupID, 1, 1000)
|
||||
if err != nil {
|
||||
log.Printf("[groupService] 获取群成员失败: groupID=%s, err=%v", groupID, err)
|
||||
zap.L().Error("获取群成员失败",
|
||||
zap.String("groupID", groupID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
if s.sseHub != nil {
|
||||
@@ -368,7 +374,10 @@ func (s *groupService) DissolveGroup(userID string, groupID string) error {
|
||||
// 先删除群组对应的会话(包括参与者、消息)
|
||||
if s.messageRepo != nil {
|
||||
if err := s.messageRepo.DeleteConversationByGroupID(groupID); err != nil {
|
||||
log.Printf("[DissolveGroup] 删除会话失败: groupID=%s, err=%v", groupID, err)
|
||||
zap.L().Warn("删除会话失败",
|
||||
zap.String("groupID", groupID),
|
||||
zap.Error(err),
|
||||
)
|
||||
// 继续删除群组,不因为会话删除失败而中断
|
||||
}
|
||||
}
|
||||
@@ -453,7 +462,12 @@ func (s *groupService) createSystemNotification(receiverID string, notifyType mo
|
||||
ExtraData: extra,
|
||||
}
|
||||
if err := s.notifyRepo.Create(notification); err != nil {
|
||||
log.Printf("[groupService] create system notification failed: receiverID=%s type=%s err=%v", receiverID, notifyType, err)
|
||||
zap.L().Warn("create system notification failed",
|
||||
zap.String("component", "groupService"),
|
||||
zap.String("receiverID", receiverID),
|
||||
zap.String("type", string(notifyType)),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,13 +496,22 @@ func (s *groupService) broadcastMemberJoinNotice(groupID string, targetUserID st
|
||||
Category: model.CategoryNotification,
|
||||
}
|
||||
if err := s.messageRepo.CreateMessageWithSeq(msg); err != nil {
|
||||
log.Printf("[broadcastMemberJoinNotice] 保存入群提示消息失败: groupID=%s, userID=%s, err=%v", groupID, targetUserID, err)
|
||||
zap.L().Warn("保存入群提示消息失败",
|
||||
zap.String("component", "broadcastMemberJoinNotice"),
|
||||
zap.String("groupID", groupID),
|
||||
zap.String("userID", targetUserID),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
savedMessage = msg
|
||||
s.invalidateConversationCachesAfterSystemMessage(conv.ID)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[broadcastMemberJoinNotice] 获取群组会话失败: groupID=%s, err=%v", groupID, err)
|
||||
zap.L().Warn("获取群组会话失败",
|
||||
zap.String("component", "broadcastMemberJoinNotice"),
|
||||
zap.String("groupID", groupID),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,7 +565,12 @@ func (s *groupService) addMemberToGroupAndConversation(group *model.Group, userI
|
||||
conv, err := s.messageRepo.GetConversationByGroupID(group.ID)
|
||||
if err == nil && conv != nil {
|
||||
if err := s.messageRepo.AddParticipant(conv.ID, userID); err != nil {
|
||||
log.Printf("[addMemberToGroupAndConversation] 添加会话参与者失败: groupID=%s, userID=%s, err=%v", group.ID, userID, err)
|
||||
zap.L().Warn("添加会话参与者失败",
|
||||
zap.String("component", "addMemberToGroupAndConversation"),
|
||||
zap.String("groupID", group.ID),
|
||||
zap.String("userID", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
s.invalidateConversationCachesAfterMembershipChange(conv.ID, userID)
|
||||
}
|
||||
@@ -568,11 +596,7 @@ func (s *groupService) collectGroupReviewerIDs(groupID string, ownerID string, s
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(reviewerSet))
|
||||
for id := range reviewerSet {
|
||||
result = append(result, id)
|
||||
}
|
||||
return result
|
||||
return slices.Collect(maps.Keys(reviewerSet))
|
||||
}
|
||||
|
||||
func (s *groupService) notifyJoinApplyReviewers(
|
||||
@@ -655,10 +679,8 @@ func (s *groupService) InviteMembers(userID string, groupID string, memberIDs []
|
||||
inviter, _ := s.userRepo.GetByID(userID)
|
||||
inviterName := "群成员"
|
||||
inviterAvatar := ""
|
||||
if inviter != nil && inviter.Nickname != "" {
|
||||
inviterName = inviter.Nickname
|
||||
}
|
||||
if inviter != nil {
|
||||
inviterName = cmp.Or(inviter.Nickname, "群成员")
|
||||
inviterAvatar = inviter.Avatar
|
||||
}
|
||||
|
||||
@@ -743,10 +765,8 @@ func (s *groupService) JoinGroup(userID string, groupID string) error {
|
||||
applicant, _ := s.userRepo.GetByID(userID)
|
||||
applicantName := "用户"
|
||||
applicantAvatar := ""
|
||||
if applicant != nil && applicant.Nickname != "" {
|
||||
applicantName = applicant.Nickname
|
||||
}
|
||||
if applicant != nil {
|
||||
applicantName = cmp.Or(applicant.Nickname, "用户")
|
||||
applicantAvatar = applicant.Avatar
|
||||
}
|
||||
// 已有待审批单时补发一次提醒,避免管理端漏看
|
||||
@@ -767,10 +787,8 @@ func (s *groupService) JoinGroup(userID string, groupID string) error {
|
||||
applicant, _ := s.userRepo.GetByID(userID)
|
||||
applicantName := "用户"
|
||||
applicantAvatar := ""
|
||||
if applicant != nil && applicant.Nickname != "" {
|
||||
applicantName = applicant.Nickname
|
||||
}
|
||||
if applicant != nil {
|
||||
applicantName = cmp.Or(applicant.Nickname, "用户")
|
||||
applicantAvatar = applicant.Avatar
|
||||
}
|
||||
expireAt := time.Now().Add(72 * time.Hour)
|
||||
@@ -856,9 +874,7 @@ func (s *groupService) RespondInvite(userID string, flag string, approve bool, r
|
||||
targetUserName := "用户"
|
||||
targetUserAvatar := ""
|
||||
if u, e := s.userRepo.GetByID(userID); e == nil && u != nil {
|
||||
if u.Nickname != "" {
|
||||
targetUserName = u.Nickname
|
||||
}
|
||||
targetUserName = cmp.Or(u.Nickname, "用户")
|
||||
targetUserAvatar = u.Avatar
|
||||
}
|
||||
|
||||
@@ -866,9 +882,7 @@ func (s *groupService) RespondInvite(userID string, flag string, approve bool, r
|
||||
inviterName := "群成员"
|
||||
inviterAvatar := ""
|
||||
if u, e := s.userRepo.GetByID(req.InitiatorID); e == nil && u != nil {
|
||||
if u.Nickname != "" {
|
||||
inviterName = u.Nickname
|
||||
}
|
||||
inviterName = cmp.Or(u.Nickname, "群成员")
|
||||
inviterAvatar = u.Avatar
|
||||
}
|
||||
|
||||
@@ -962,15 +976,13 @@ func (s *groupService) SetGroupAddRequest(userID string, flag string, approve bo
|
||||
// 保存管理员ID到ReviewerID(覆盖被邀请人的ID)
|
||||
req.Reason = reason
|
||||
targetUserName := "用户"
|
||||
if u, e := s.userRepo.GetByID(req.TargetUserID); e == nil && u != nil && u.Nickname != "" {
|
||||
targetUserName = u.Nickname
|
||||
if u, e := s.userRepo.GetByID(req.TargetUserID); e == nil && u != nil {
|
||||
targetUserName = cmp.Or(u.Nickname, "用户")
|
||||
}
|
||||
reviewerName := "管理员"
|
||||
reviewerAvatar := ""
|
||||
if u, e := s.userRepo.GetByID(userID); e == nil && u != nil {
|
||||
if u.Nickname != "" {
|
||||
reviewerName = u.Nickname
|
||||
}
|
||||
reviewerName = cmp.Or(u.Nickname, "管理员")
|
||||
reviewerAvatar = u.Avatar
|
||||
}
|
||||
|
||||
@@ -1185,7 +1197,12 @@ func (s *groupService) RemoveMember(userID string, groupID string, targetUserID
|
||||
conv, err := s.messageRepo.GetConversationByGroupID(groupID)
|
||||
if err == nil && conv != nil {
|
||||
if err := s.messageRepo.RemoveParticipant(conv.ID, targetUserID); err != nil {
|
||||
log.Printf("[RemoveMember] 移除会话参与者失败: groupID=%s, userID=%s, err=%v", groupID, targetUserID, err)
|
||||
zap.L().Warn("移除会话参与者失败",
|
||||
zap.String("component", "RemoveMember"),
|
||||
zap.String("groupID", groupID),
|
||||
zap.String("userID", targetUserID),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
s.invalidateConversationCachesAfterMembershipChange(conv.ID, targetUserID)
|
||||
}
|
||||
@@ -1330,22 +1347,35 @@ func (s *groupService) MuteMember(userID string, groupID string, targetUserID st
|
||||
|
||||
member, err := s.groupRepo.GetMember(groupID, targetUserID)
|
||||
if err != nil {
|
||||
log.Printf("[MuteMember] 获取成员失败: %v", err)
|
||||
zap.L().Warn("获取成员失败",
|
||||
zap.String("component", "MuteMember"),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[MuteMember] 禁言前状态: member.Muted=%v, 即将设置 muted=%v", member.Muted, muted)
|
||||
zap.L().Debug("禁言前状态",
|
||||
zap.String("component", "MuteMember"),
|
||||
zap.Bool("member.Muted", member.Muted),
|
||||
zap.Bool("muted", muted),
|
||||
)
|
||||
member.Muted = muted
|
||||
if err := s.groupRepo.UpdateMember(member); err != nil {
|
||||
log.Printf("[MuteMember] 更新成员失败: %v", err)
|
||||
zap.L().Warn("更新成员失败",
|
||||
zap.String("component", "MuteMember"),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
log.Printf("[MuteMember] 禁言状态已更新到数据库")
|
||||
zap.L().Debug("禁言状态已更新到数据库", zap.String("component", "MuteMember"))
|
||||
|
||||
// 验证更新结果
|
||||
updatedMember, _ := s.groupRepo.GetMember(groupID, targetUserID)
|
||||
if updatedMember != nil {
|
||||
log.Printf("[MuteMember] 验证: member.Muted=%v", updatedMember.Muted)
|
||||
zap.L().Debug("验证成员状态",
|
||||
zap.String("component", "MuteMember"),
|
||||
zap.Bool("member.Muted", updatedMember.Muted),
|
||||
)
|
||||
}
|
||||
|
||||
// 获取被禁言用户的显示名称
|
||||
@@ -1382,14 +1412,24 @@ func (s *groupService) MuteMember(userID string, groupID string, targetUserID st
|
||||
|
||||
// 保存消息并获取 seq
|
||||
if err := s.messageRepo.CreateMessageWithSeq(msg); err != nil {
|
||||
log.Printf("[MuteMember] 保存禁言消息失败: %v", err)
|
||||
zap.L().Warn("保存禁言消息失败",
|
||||
zap.String("component", "MuteMember"),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
savedMessage = msg
|
||||
log.Printf("[MuteMember] 禁言消息已保存, ID=%s, Seq=%d", msg.ID, msg.Seq)
|
||||
zap.L().Debug("禁言消息已保存",
|
||||
zap.String("component", "MuteMember"),
|
||||
zap.String("msgID", msg.ID),
|
||||
zap.Int64("seq", msg.Seq),
|
||||
)
|
||||
s.invalidateConversationCachesAfterSystemMessage(conv.ID)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[MuteMember] 获取群组会话失败: %v", err)
|
||||
zap.L().Warn("获取群组会话失败",
|
||||
zap.String("component", "MuteMember"),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/cache"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/repository"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -94,7 +94,12 @@ func (s *MessageService) SendMessage(ctx context.Context, senderID, receiverID s
|
||||
// 异步写入缓存
|
||||
go func() {
|
||||
if err := s.cacheMessage(context.Background(), conv.ID, msg); err != nil {
|
||||
log.Printf("[MessageService] async cache message failed, convID=%s, msgID=%s, err=%v", conv.ID, msg.ID, err)
|
||||
zap.L().Warn("async cache message failed",
|
||||
zap.String("component", "MessageService"),
|
||||
zap.String("convID", conv.ID),
|
||||
zap.String("msgID", msg.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"carrot_bbs/internal/pkg/openai"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// PostModerationRejectedError 帖子审核拒绝错误
|
||||
@@ -73,7 +74,9 @@ func (s *PostAIService) ModeratePost(ctx context.Context, title, content string,
|
||||
if s.openAIClient.Config().StrictModeration {
|
||||
return err
|
||||
}
|
||||
log.Printf("[WARN] AI moderation failed, fallback allow: %v", err)
|
||||
zap.L().Warn("AI moderation failed, fallback allow",
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
if !approved {
|
||||
@@ -93,7 +96,9 @@ func (s *PostAIService) ModerateComment(ctx context.Context, content string, ima
|
||||
if s.openAIClient.Config().StrictModeration {
|
||||
return err
|
||||
}
|
||||
log.Printf("[WARN] AI comment moderation failed, fallback allow: %v", err)
|
||||
zap.L().Warn("AI comment moderation failed, fallback allow",
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
if !approved {
|
||||
|
||||
@@ -514,10 +514,9 @@ func (s *postServiceImpl) GetPostInteractionStatus(ctx context.Context, postIDs
|
||||
return isLikedMap, isFavoritedMap, nil
|
||||
}
|
||||
|
||||
for _, postID := range postIDs {
|
||||
isLikedMap[postID] = s.postRepo.IsLiked(postID, userID)
|
||||
isFavoritedMap[postID] = s.postRepo.IsFavorited(postID, userID)
|
||||
}
|
||||
// 使用批量查询替代循环中的单个查询,解决 N+1 问题
|
||||
isLikedMap = s.postRepo.IsLikedBatch(postIDs, userID)
|
||||
isFavoritedMap = s.postRepo.IsFavoritedBatch(postIDs, userID)
|
||||
|
||||
return isLikedMap, isFavoritedMap, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -188,12 +189,7 @@ func normalizeWeeks(source []int) []int {
|
||||
}
|
||||
|
||||
func containsWeek(weeks []int, target int) bool {
|
||||
for _, week := range weeks {
|
||||
if week == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.Contains(weeks, target)
|
||||
}
|
||||
|
||||
func normalizeHexColor(color string) string {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -14,6 +13,7 @@ import (
|
||||
"carrot_bbs/internal/model"
|
||||
redisclient "carrot_bbs/internal/pkg/redis"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -289,13 +289,17 @@ func NewSensitiveService(db *gorm.DB, redisClient *redisclient.Client, config *S
|
||||
// 初始化加载敏感词
|
||||
if config.LoadFromDB {
|
||||
if err := s.loadFromDB(context.Background()); err != nil {
|
||||
log.Printf("Failed to load sensitive words from database: %v", err)
|
||||
zap.L().Warn("Failed to load sensitive words from database",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if config.LoadFromRedis && redisClient != nil {
|
||||
if err := s.loadFromRedis(context.Background()); err != nil {
|
||||
log.Printf("Failed to load sensitive words from redis: %v", err)
|
||||
zap.L().Warn("Failed to load sensitive words from redis",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,7 +369,9 @@ func (s *sensitiveServiceImpl) AddWord(ctx context.Context, word string, categor
|
||||
result := s.db.Where("word = ?", word).First(&existing)
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
if err := s.db.Create(&sensitiveWord).Error; err != nil {
|
||||
log.Printf("Failed to save sensitive word to database: %v", err)
|
||||
zap.L().Warn("Failed to save sensitive word to database",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
} else if result.Error == nil {
|
||||
// 更新已存在的记录
|
||||
@@ -373,7 +379,9 @@ func (s *sensitiveServiceImpl) AddWord(ctx context.Context, word string, categor
|
||||
existing.Level = wordLevel
|
||||
existing.IsActive = true
|
||||
if err := s.db.Save(&existing).Error; err != nil {
|
||||
log.Printf("Failed to update sensitive word in database: %v", err)
|
||||
zap.L().Warn("Failed to update sensitive word in database",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -406,7 +414,9 @@ func (s *sensitiveServiceImpl) RemoveWord(ctx context.Context, word string) erro
|
||||
if s.db != nil {
|
||||
result := s.db.Model(&model.SensitiveWord{}).Where("word = ?", word).Update("is_active", false)
|
||||
if result.Error != nil {
|
||||
log.Printf("Failed to deactivate sensitive word in database: %v", result.Error)
|
||||
zap.L().Warn("Failed to deactivate sensitive word in database",
|
||||
zap.Error(result.Error),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,7 +471,9 @@ func (s *sensitiveServiceImpl) loadFromDB(ctx context.Context) error {
|
||||
s.tree.AddWord(word.Word, word.Level, word.Category)
|
||||
}
|
||||
|
||||
log.Printf("Loaded %d sensitive words from database", len(words))
|
||||
zap.L().Info("Loaded sensitive words from database",
|
||||
zap.Int("count", len(words)),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
@@ -9,7 +10,6 @@ import (
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
@@ -17,7 +17,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"carrot_bbs/internal/pkg/s3"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"go.uber.org/zap"
|
||||
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
@@ -65,7 +67,9 @@ func (s *UploadService) UploadImage(ctx context.Context, file *multipart.FileHea
|
||||
// 生成预览图
|
||||
previewURL, previewURLLarge, err := s.GeneratePreviewImages(ctx, processedData, hashStr)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to generate preview images: %v", err)
|
||||
zap.L().Warn("Failed to generate preview images",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
|
||||
return url, previewURL, previewURLLarge, nil
|
||||
@@ -189,10 +193,7 @@ func prepareImageForUpload(file *multipart.FileHeader) ([]byte, string, string,
|
||||
// 优先从文件字节探测真实类型,避免前端压缩/转码后 header 与实际格式不一致
|
||||
detectedType := normalizeImageContentType(http.DetectContentType(originalData))
|
||||
headerType := normalizeImageContentType(file.Header.Get("Content-Type"))
|
||||
contentType := detectedType
|
||||
if contentType == "" || contentType == "application/octet-stream" {
|
||||
contentType = headerType
|
||||
}
|
||||
contentType := cmp.Or(detectedType, headerType, "application/octet-stream")
|
||||
|
||||
compressedData, compressedType, err := compressImageData(originalData, contentType)
|
||||
if err != nil {
|
||||
@@ -201,21 +202,9 @@ func prepareImageForUpload(file *multipart.FileHeader) ([]byte, string, string,
|
||||
compressedType = contentType
|
||||
}
|
||||
|
||||
if compressedType == "" {
|
||||
compressedType = contentType
|
||||
}
|
||||
if compressedType == "" {
|
||||
compressedType = http.DetectContentType(compressedData)
|
||||
}
|
||||
compressedType = cmp.Or(compressedType, http.DetectContentType(compressedData))
|
||||
|
||||
ext := getExtFromContentType(compressedType)
|
||||
if ext == "" {
|
||||
ext = strings.ToLower(filepath.Ext(file.Filename))
|
||||
}
|
||||
if ext == "" {
|
||||
// 最终兜底,避免对象名无扩展名导致 URL 语义不明确
|
||||
ext = ".jpg"
|
||||
}
|
||||
ext := cmp.Or(getExtFromContentType(compressedType), strings.ToLower(filepath.Ext(file.Filename)), ".jpg")
|
||||
|
||||
return compressedData, compressedType, ext, nil
|
||||
}
|
||||
@@ -302,13 +291,17 @@ func (s *UploadService) GeneratePreviewImages(ctx context.Context, originalData
|
||||
// 生成普通预览图(列表/网格模式)
|
||||
previewURL, err = s.generatePreview(ctx, img, originalWidth, originalHeight, "preview", PreviewMaxWidth, hash)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to generate preview: %v", err)
|
||||
zap.L().Warn("Failed to generate preview",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
|
||||
// 生成大预览图(详情页)
|
||||
previewURLLarge, err = s.generatePreview(ctx, img, originalWidth, originalHeight, "large", PreviewMaxHeight, hash)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to generate large preview: %v", err)
|
||||
zap.L().Warn("Failed to generate large preview",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
|
||||
return previewURL, previewURLLarge, nil
|
||||
|
||||
@@ -3,10 +3,11 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/repository"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// UserActivityService 用户活跃统计服务接口
|
||||
@@ -76,14 +77,18 @@ func (s *userActivityService) RecordUserActive(ctx context.Context, userID, logi
|
||||
// 1. 记录到 Redis(同步,快速)
|
||||
if err := s.repo.RecordActivityToRedis(ctx, userID); err != nil {
|
||||
// 记录日志但不阻断流程
|
||||
log.Printf("failed to record activity to redis: %v", err)
|
||||
zap.L().Warn("failed to record activity to redis",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
|
||||
// 2. 异步写入数据库(可以使用 goroutine 或消息队列)
|
||||
go func() {
|
||||
bgCtx := context.Background()
|
||||
if err := s.repo.RecordActivity(bgCtx, userID, loginType, ip, userAgent); err != nil {
|
||||
log.Printf("failed to record activity to db: %v", err)
|
||||
zap.L().Warn("failed to record activity to db",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -158,7 +163,9 @@ func (s *userActivityService) GetActivityStats(ctx context.Context) (*ActivitySt
|
||||
// 获取今日 DAU
|
||||
todayDAU, err := s.GetDAU(ctx, today)
|
||||
if err != nil {
|
||||
log.Printf("failed to get today DAU: %v", err)
|
||||
zap.L().Warn("failed to get today DAU",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
resp.Today = DailyStats{
|
||||
Date: today.Format("2006-01-02"),
|
||||
@@ -168,7 +175,9 @@ func (s *userActivityService) GetActivityStats(ctx context.Context) (*ActivitySt
|
||||
// 获取昨日 DAU
|
||||
yesterdayDAU, err := s.GetDAU(ctx, yesterday)
|
||||
if err != nil {
|
||||
log.Printf("failed to get yesterday DAU: %v", err)
|
||||
zap.L().Warn("failed to get yesterday DAU",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
resp.Yesterday = DailyStats{
|
||||
Date: yesterday.Format("2006-01-02"),
|
||||
@@ -178,7 +187,9 @@ func (s *userActivityService) GetActivityStats(ctx context.Context) (*ActivitySt
|
||||
// 获取本周 WAU
|
||||
wau, err := s.GetWAU(ctx, year, week)
|
||||
if err != nil {
|
||||
log.Printf("failed to get WAU: %v", err)
|
||||
zap.L().Warn("failed to get WAU",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
resp.ThisWeek = PeriodStats{
|
||||
Period: fmt.Sprintf("%d-W%02d", year, week),
|
||||
@@ -188,7 +199,9 @@ func (s *userActivityService) GetActivityStats(ctx context.Context) (*ActivitySt
|
||||
// 获取本月 MAU
|
||||
mau, err := s.GetMAU(ctx, now.Year(), int(now.Month()))
|
||||
if err != nil {
|
||||
log.Printf("failed to get MAU: %v", err)
|
||||
zap.L().Warn("failed to get MAU",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
resp.ThisMonth = PeriodStats{
|
||||
Period: now.Format("2006-01"),
|
||||
@@ -199,7 +212,9 @@ func (s *userActivityService) GetActivityStats(ctx context.Context) (*ActivitySt
|
||||
// 次日留存
|
||||
day1Retention, err := s.GetRetention(ctx, yesterday, today)
|
||||
if err != nil {
|
||||
log.Printf("failed to get day 1 retention: %v", err)
|
||||
zap.L().Warn("failed to get day 1 retention",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
resp.Retention.Day1 = day1Retention
|
||||
|
||||
@@ -207,7 +222,9 @@ func (s *userActivityService) GetActivityStats(ctx context.Context) (*ActivitySt
|
||||
day7Date := today.AddDate(0, 0, -7)
|
||||
day7Retention, err := s.GetRetention(ctx, day7Date, today)
|
||||
if err != nil {
|
||||
log.Printf("failed to get day 7 retention: %v", err)
|
||||
zap.L().Warn("failed to get day 7 retention",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
resp.Retention.Day7 = day7Retention
|
||||
|
||||
@@ -215,7 +232,9 @@ func (s *userActivityService) GetActivityStats(ctx context.Context) (*ActivitySt
|
||||
day30Date := today.AddDate(0, 0, -30)
|
||||
day30Retention, err := s.GetRetention(ctx, day30Date, today)
|
||||
if err != nil {
|
||||
log.Printf("failed to get day 30 retention: %v", err)
|
||||
zap.L().Warn("failed to get day 30 retention",
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
resp.Retention.Day30 = day30Retention
|
||||
|
||||
|
||||
Reference in New Issue
Block a user