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:
lafay
2026-03-17 00:47:17 +08:00
parent b028f7e1d3
commit 5d6c982c9c
38 changed files with 2256 additions and 290 deletions

View File

@@ -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) {

View File

@@ -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 清理旧的审核日志

View File

@@ -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),
)
}
}()

View File

@@ -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),
)
}
}()
}

View File

@@ -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),
)
}
}

View File

@@ -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),
)
}
}()

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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

View File

@@ -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