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 cache
import (
"cmp"
"context"
"encoding/json"
"math/rand"
@@ -123,17 +124,11 @@ func GetSettings() Settings {
}
func normalizeKey(key string) string {
if settings.KeyPrefix == "" {
return key
}
return settings.KeyPrefix + ":" + key
return cmp.Or(settings.KeyPrefix, "") + ":" + key
}
func normalizePrefix(prefix string) string {
if settings.KeyPrefix == "" {
return prefix
}
return settings.KeyPrefix + ":" + prefix
return cmp.Or(settings.KeyPrefix, "") + ":" + prefix
}
func SetWithJitter(c Cache, key string, value interface{}, ttl time.Duration, jitterRatio float64) {

View File

@@ -4,10 +4,11 @@ import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"carrot_bbs/internal/model"
"go.uber.org/zap"
)
// CachedConversation 带缓存元数据的会话
@@ -713,6 +714,10 @@ func (c *ConversationCache) getMessagesBySeqs(ctx context.Context, convID string
// asyncCacheMessage 异步缓存单条消息
func (c *ConversationCache) asyncCacheMessage(ctx context.Context, convID string, msg *model.Message) {
if err := c.CacheMessage(ctx, convID, msg); err != nil {
log.Printf("[ConversationCache] async cache message failed, convID=%s, msgID=%s, err=%v", convID, msg.ID, err)
zap.L().Error("async cache message failed",
zap.String("convID", convID),
zap.String("msgID", msg.ID),
zap.Error(err),
)
}
}

View File

@@ -3,10 +3,10 @@ package cache
import (
"context"
"encoding/json"
"log"
"time"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
redisPkg "carrot_bbs/internal/pkg/redis"
)
@@ -32,13 +32,19 @@ func (c *RedisCache) Set(key string, value interface{}, ttl time.Duration) {
data, err := json.Marshal(value)
if err != nil {
recordSetError()
log.Printf("[RedisCache] Failed to marshal value for key %s: %v", key, err)
zap.L().Error("Failed to marshal value for key",
zap.String("key", key),
zap.Error(err),
)
return
}
if err := c.client.Set(c.ctx, key, data, ttl); err != nil {
recordSetError()
log.Printf("[RedisCache] Failed to set key %s: %v", key, err)
zap.L().Error("Failed to set key",
zap.String("key", key),
zap.Error(err),
)
}
}
@@ -50,7 +56,10 @@ func (c *RedisCache) Get(key string) (interface{}, bool) {
if err == redis.Nil {
return nil, false
}
log.Printf("[RedisCache] Failed to get key %s: %v", key, err)
zap.L().Error("Failed to get key",
zap.String("key", key),
zap.Error(err),
)
return nil, false
}
@@ -63,7 +72,10 @@ func (c *RedisCache) Delete(key string) {
key = normalizeKey(key)
recordInvalidate()
if err := c.client.Del(c.ctx, key); err != nil {
log.Printf("[RedisCache] Failed to delete key %s: %v", key, err)
zap.L().Error("Failed to delete key",
zap.String("key", key),
zap.Error(err),
)
}
}
@@ -76,14 +88,20 @@ func (c *RedisCache) DeleteByPrefix(prefix string) {
for {
keys, nextCursor, err := rdb.Scan(c.ctx, cursor, prefix+"*", 100).Result()
if err != nil {
log.Printf("[RedisCache] Failed to scan keys with prefix %s: %v", prefix, err)
zap.L().Error("Failed to scan keys with prefix",
zap.String("prefix", prefix),
zap.Error(err),
)
return
}
if len(keys) > 0 {
recordInvalidateMultiple(int64(len(keys)))
if err := c.client.Del(c.ctx, keys...); err != nil {
log.Printf("[RedisCache] Failed to delete keys with prefix %s: %v", prefix, err)
zap.L().Error("Failed to delete keys with prefix",
zap.String("prefix", prefix),
zap.Error(err),
)
}
}
@@ -97,13 +115,15 @@ func (c *RedisCache) DeleteByPrefix(prefix string) {
// Clear 清空所有缓存
func (c *RedisCache) Clear() {
if settings.DisableFlushDB {
log.Printf("[RedisCache] Skip FlushDB because cache.disable_flushdb=true")
zap.L().Debug("Skip FlushDB because cache.disable_flushdb=true")
return
}
recordInvalidate()
rdb := c.client.GetClient()
if err := rdb.FlushDB(c.ctx).Err(); err != nil {
log.Printf("[RedisCache] Failed to clear cache: %v", err)
zap.L().Error("Failed to clear cache",
zap.Error(err),
)
}
}
@@ -112,7 +132,10 @@ func (c *RedisCache) Exists(key string) bool {
key = normalizeKey(key)
n, err := c.client.Exists(c.ctx, key)
if err != nil {
log.Printf("[RedisCache] Failed to check existence of key %s: %v", key, err)
zap.L().Error("Failed to check existence of key",
zap.String("key", key),
zap.Error(err),
)
return false
}
return n > 0
@@ -129,7 +152,10 @@ func (c *RedisCache) IncrementBy(key string, value int64) int64 {
rdb := c.client.GetClient()
result, err := rdb.IncrBy(c.ctx, key, value).Result()
if err != nil {
log.Printf("[RedisCache] Failed to increment key %s: %v", key, err)
zap.L().Error("Failed to increment key",
zap.String("key", key),
zap.Error(err),
)
return 0
}
return result

View File

@@ -1,6 +1,7 @@
package config
import (
"cmp"
"fmt"
"os"
"strconv"
@@ -236,8 +237,5 @@ func Load(configPath string) (*Config, error) {
// getEnvOrDefault 获取环境变量值,如果未设置则返回默认值
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
return cmp.Or(os.Getenv(key), defaultValue)
}

View File

@@ -224,34 +224,34 @@ type TextSegmentData struct {
// ImageSegmentData 图片数据
type ImageSegmentData struct {
URL string `json:"url"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Width int `json:"width,omitzero"`
Height int `json:"height,omitzero"`
ThumbnailURL string `json:"thumbnail_url,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
FileSize int64 `json:"file_size,omitzero"`
}
// VoiceSegmentData 语音数据
type VoiceSegmentData struct {
URL string `json:"url"`
Duration int `json:"duration,omitempty"` // 秒
FileSize int64 `json:"file_size,omitempty"`
Duration int `json:"duration,omitzero"` // 秒
FileSize int64 `json:"file_size,omitzero"`
}
// VideoSegmentData 视频数据
type VideoSegmentData struct {
URL string `json:"url"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Duration int `json:"duration,omitempty"` // 秒
Width int `json:"width,omitzero"`
Height int `json:"height,omitzero"`
Duration int `json:"duration,omitzero"` // 秒
ThumbnailURL string `json:"thumbnail_url,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
FileSize int64 `json:"file_size,omitzero"`
}
// FileSegmentData 文件数据
type FileSegmentData struct {
URL string `json:"url"`
Name string `json:"name"`
Size int64 `json:"size,omitempty"`
Size int64 `json:"size,omitzero"`
MimeType string `json:"mime_type,omitempty"`
}
@@ -268,7 +268,7 @@ type ReplySegmentData struct {
// FaceSegmentData 表情数据
type FaceSegmentData struct {
ID int `json:"id"`
ID int `json:"id,omitzero"`
Name string `json:"name,omitempty"`
URL string `json:"url,omitempty"`
}
@@ -458,7 +458,7 @@ type DeviceTokenResponse struct {
DeviceType string `json:"device_type"`
IsActive bool `json:"is_active"`
DeviceName string `json:"device_name"`
LastUsedAt time.Time `json:"last_used_at,omitempty"`
LastUsedAt time.Time `json:"last_used_at,omitzero"`
CreatedAt time.Time `json:"created_at"`
}
@@ -470,9 +470,9 @@ type PushRecordResponse struct {
MessageID string `json:"message_id"`
PushChannel string `json:"push_channel"`
PushStatus string `json:"push_status"`
RetryCount int `json:"retry_count"`
PushedAt time.Time `json:"pushed_at,omitempty"`
DeliveredAt time.Time `json:"delivered_at,omitempty"`
RetryCount int `json:"retry_count,omitzero"`
PushedAt time.Time `json:"pushed_at,omitzero"`
DeliveredAt time.Time `json:"delivered_at,omitzero"`
CreatedAt time.Time `json:"created_at"`
}
@@ -859,7 +859,7 @@ type VoteResultDTO struct {
Options []VoteOptionDTO `json:"options"`
TotalVotes int `json:"total_votes"`
HasVoted bool `json:"has_voted"`
VotedOptionID string `json:"voted_option_id,omitempty"`
VotedOptionID string `json:"voted_option_id,omitzero"`
}
// ==================== WebSocket Response DTOs ====================
@@ -882,10 +882,10 @@ type AdminPostListResponse struct {
Content string `json:"content"`
Images []PostImageResponse `json:"images"`
Status string `json:"status"`
LikesCount int `json:"likes_count"`
CommentsCount int `json:"comments_count"`
FavoritesCount int `json:"favorites_count"`
ViewsCount int `json:"views_count"`
LikesCount int `json:"likes_count,omitzero"`
CommentsCount int `json:"comments_count,omitzero"`
FavoritesCount int `json:"favorites_count,omitzero"`
ViewsCount int `json:"views_count,omitzero"`
IsPinned bool `json:"is_pinned"`
IsFeatured bool `json:"is_featured"`
IsLocked bool `json:"is_locked"`
@@ -906,11 +906,11 @@ type AdminPostDetailResponse struct {
Content string `json:"content"`
Images []PostImageResponse `json:"images"`
Status string `json:"status"`
LikesCount int `json:"likes_count"`
CommentsCount int `json:"comments_count"`
FavoritesCount int `json:"favorites_count"`
SharesCount int `json:"shares_count"`
ViewsCount int `json:"views_count"`
LikesCount int `json:"likes_count,omitzero"`
CommentsCount int `json:"comments_count,omitzero"`
FavoritesCount int `json:"favorites_count,omitzero"`
SharesCount int `json:"shares_count,omitzero"`
ViewsCount int `json:"views_count,omitzero"`
HotScore float64 `json:"hot_score"`
IsPinned bool `json:"is_pinned"`
IsFeatured bool `json:"is_featured"`

View File

@@ -5,9 +5,9 @@ type ScheduleCourseResponse struct {
Name string `json:"name"`
Teacher string `json:"teacher,omitempty"`
Location string `json:"location,omitempty"`
DayOfWeek int `json:"day_of_week"`
StartSection int `json:"start_section"`
EndSection int `json:"end_section"`
DayOfWeek int `json:"day_of_week,omitzero"`
StartSection int `json:"start_section,omitzero"`
EndSection int `json:"end_section,omitzero"`
Weeks []int `json:"weeks"`
Color string `json:"color,omitempty"`
}

View File

@@ -3,6 +3,7 @@ package dto
import (
"encoding/json"
"fmt"
"slices"
"carrot_bbs/internal/model"
)
@@ -232,25 +233,27 @@ func ExtractMentionedUsers(segments model.MessageSegments) []string {
// IsAtAll 检查消息是否@了所有人
func IsAtAll(segments model.MessageSegments) bool {
for _, segment := range segments {
return slices.ContainsFunc(segments, func(segment model.MessageSegment) bool {
if segment.Type == string(SegmentTypeAt) {
if userID, ok := segment.Data["user_id"].(string); ok && userID == "all" {
return true
}
}
}
return false
return false
})
}
// GetReplyMessageID 从消息链中获取被回复的消息ID
// 如果没有回复segment返回空字符串
func GetReplyMessageID(segments model.MessageSegments) string {
for _, segment := range segments {
if segment.Type == string(SegmentTypeReply) {
if id, ok := segment.Data["id"].(string); ok {
return id
}
}
idx := slices.IndexFunc(segments, func(segment model.MessageSegment) bool {
return segment.Type == string(SegmentTypeReply)
})
if idx == -1 {
return ""
}
if id, ok := segments[idx].Data["id"].(string); ok {
return id
}
return ""
}
@@ -291,12 +294,9 @@ func BuildSegmentsFromContent(contentType, content string, mediaURL *string) mod
// HasSegmentType 检查消息链中是否包含指定类型的segment
func HasSegmentType(segments model.MessageSegments, segmentType SegmentType) bool {
for _, segment := range segments {
if segment.Type == string(segmentType) {
return true
}
}
return false
return slices.ContainsFunc(segments, func(segment model.MessageSegment) bool {
return segment.Type == string(segmentType)
})
}
// GetSegmentsByType 获取消息链中所有指定类型的segment
@@ -313,12 +313,14 @@ func GetSegmentsByType(segments model.MessageSegments, segmentType SegmentType)
// GetFirstImageURL 获取消息链中第一张图片的URL
// 如果没有图片,返回空字符串
func GetFirstImageURL(segments model.MessageSegments) string {
for _, segment := range segments {
if segment.Type == string(SegmentTypeImage) {
if url, ok := segment.Data["url"].(string); ok {
return url
}
}
idx := slices.IndexFunc(segments, func(segment model.MessageSegment) bool {
return segment.Type == string(SegmentTypeImage)
})
if idx == -1 {
return ""
}
if url, ok := segments[idx].Data["url"].(string); ok {
return url
}
return ""
}
@@ -326,13 +328,18 @@ func GetFirstImageURL(segments model.MessageSegments) string {
// GetFirstMediaURL 获取消息链中第一个媒体文件的URL图片/视频/语音/文件)
// 用于兼容旧版本API
func GetFirstMediaURL(segments model.MessageSegments) string {
for _, segment := range segments {
idx := slices.IndexFunc(segments, func(segment model.MessageSegment) bool {
switch segment.Type {
case string(SegmentTypeImage), string(SegmentTypeVideo), string(SegmentTypeVoice), string(SegmentTypeFile):
if url, ok := segment.Data["url"].(string); ok {
return url
}
return true
}
return false
})
if idx == -1 {
return ""
}
if url, ok := segments[idx].Data["url"].(string); ok {
return url
}
return ""
}

View File

@@ -2,7 +2,6 @@ package handler
import (
"context"
"log"
"strings"
"time"
@@ -13,6 +12,7 @@ import (
"github.com/gin-gonic/gin"
gorseio "github.com/gorse-io/gorse-go"
"go.uber.org/zap"
"gorm.io/gorm"
)
@@ -62,7 +62,7 @@ func (h *GorseHandler) ImportData(c *gin.Context) {
stats, err := h.importAllData(ctx)
if err != nil {
log.Printf("[ERROR] gorse import failed: %v", err)
zap.L().Error("gorse import failed", zap.Error(err))
response.InternalServerError(c, "gorse import failed: "+err.Error())
return
}
@@ -107,7 +107,10 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
for _, post := range posts {
embedding, err := gorse.GetEmbedding(strings.TrimSpace(post.Title + " " + post.Content))
if err != nil {
log.Printf("[WARN] get embedding failed for post %s: %v", post.ID, err)
zap.L().Warn("get embedding failed for post",
zap.String("postID", post.ID),
zap.Error(err),
)
embedding = make([]float64, 1024)
}
_, err = gorseClient.InsertItem(ctx, gorseio.Item{
@@ -121,7 +124,10 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
},
})
if err != nil {
log.Printf("[WARN] import item failed (%s): %v", post.ID, err)
zap.L().Warn("import item failed",
zap.String("postID", post.ID),
zap.Error(err),
)
continue
}
stats["items"] = stats["items"].(int) + 1
@@ -143,7 +149,10 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
Comment: user.Nickname,
})
if err != nil {
log.Printf("[WARN] import user failed (%s): %v", user.ID, err)
zap.L().Warn("import user failed",
zap.String("userID", user.ID),
zap.Error(err),
)
continue
}
stats["users"] = stats["users"].(int) + 1
@@ -162,7 +171,11 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
Timestamp: like.CreatedAt.UTC().Truncate(time.Second),
}})
if err != nil {
log.Printf("[WARN] import like failed (%s/%s): %v", like.UserID, like.PostID, err)
zap.L().Warn("import like failed",
zap.String("userID", like.UserID),
zap.String("postID", like.PostID),
zap.Error(err),
)
continue
}
stats["likes"] = stats["likes"].(int) + 1
@@ -181,7 +194,11 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
Timestamp: fav.CreatedAt.UTC().Truncate(time.Second),
}})
if err != nil {
log.Printf("[WARN] import favorite failed (%s/%s): %v", fav.UserID, fav.PostID, err)
zap.L().Warn("import favorite failed",
zap.String("userID", fav.UserID),
zap.String("postID", fav.PostID),
zap.Error(err),
)
continue
}
stats["favorites"] = stats["favorites"].(int) + 1
@@ -206,7 +223,11 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
Timestamp: cm.CreatedAt.UTC().Truncate(time.Second),
}})
if err != nil {
log.Printf("[WARN] import comment failed (%s/%s): %v", cm.UserID, cm.PostID, err)
zap.L().Warn("import comment failed",
zap.String("userID", cm.UserID),
zap.String("postID", cm.PostID),
zap.Error(err),
)
continue
}
stats["comments"] = stats["comments"].(int) + 1

View File

@@ -1,10 +1,10 @@
package handler
import (
"log"
"strconv"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
@@ -1361,12 +1361,24 @@ func (h *GroupHandler) HandleSetGroupBan(c *gin.Context) {
// duration > 0 或 duration = -1 表示禁言duration = 0 表示解除禁言
muted := params.Duration != 0
log.Printf("[HandleSetGroupBan] 开始禁言操作: userID=%s, groupID=%s, targetUserID=%s, duration=%d, muted=%v", userID, groupID, params.UserID, params.Duration, muted)
zap.L().Info("开始禁言操作",
zap.String("component", "HandleSetGroupBan"),
zap.String("userID", userID),
zap.String("groupID", groupID),
zap.String("targetUserID", params.UserID),
zap.Int64("duration", params.Duration),
zap.Bool("muted", muted),
)
err := h.groupService.MuteMember(userID, groupID, params.UserID, muted)
if err != nil {
log.Printf("[HandleSetGroupBan] 禁言操作失败: %v", err)
zap.L().Warn("禁言操作失败",
zap.String("component", "HandleSetGroupBan"),
zap.Error(err),
)
} else {
log.Printf("[HandleSetGroupBan] 禁言操作成功")
zap.L().Info("禁言操作成功",
zap.String("component", "HandleSetGroupBan"),
)
}
if err != nil {
if err == service.ErrNotGroupAdmin {

View File

@@ -1,6 +1,7 @@
package handler
import (
"cmp"
"context"
"crypto/sha256"
"fmt"
@@ -129,10 +130,7 @@ func (h *UserHandler) Login(c *gin.Context) {
return
}
account := req.Account
if account == "" {
account = req.Username
}
account := cmp.Or(req.Account, req.Username)
if account == "" {
response.BadRequest(c, "username or account is required")
return

View File

@@ -1,6 +1,7 @@
package middleware
import (
"slices"
"strings"
"carrot_bbs/internal/service"
@@ -104,18 +105,9 @@ func RequireRole(casbinService service.CasbinService, requiredRoles ...string) g
}
// 检查用户是否拥有任一所需角色
roleMap := make(map[string]bool)
for _, role := range userRoles {
roleMap[role] = true
}
hasRole := false
for _, required := range requiredRoles {
if roleMap[required] {
hasRole = true
break
}
}
hasRole := slices.ContainsFunc(requiredRoles, func(required string) bool {
return slices.Contains(userRoles, required)
})
if !hasRole {
c.AbortWithStatusJSON(403, gin.H{

View File

@@ -6,6 +6,7 @@ import (
"os"
"time"
"go.uber.org/zap"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
@@ -67,7 +68,9 @@ func NewDB(cfg *config.DatabaseConfig) (*gorm.DB, error) {
return nil, fmt.Errorf("failed to auto migrate: %w", err)
}
log.Printf("Database connected (%s) and migrated successfully", cfg.Type)
zap.L().Info("Database connected and migrated successfully",
zap.String("type", cfg.Type),
)
return db, nil
}
@@ -220,7 +223,9 @@ func seedRoles(db *gorm.DB) error {
return err
}
log.Printf("Seeded %d roles successfully", len(roles))
zap.L().Info("Seeded roles successfully",
zap.Int("count", len(roles)),
)
return nil
}
@@ -276,7 +281,9 @@ func seedPermissions(db *gorm.DB) error {
return err
}
log.Printf("Seeded %d permissions successfully", len(permissions))
zap.L().Info("Seeded permissions successfully",
zap.Int("count", len(permissions)),
)
return nil
}

View File

@@ -10,16 +10,17 @@ import (
"time"
gorseio "github.com/gorse-io/gorse-go"
"go.uber.org/zap"
)
// FeedbackType 反馈类型
type FeedbackType string
const (
FeedbackTypeLike FeedbackType = "like" // 点赞
FeedbackTypeStar FeedbackType = "star" // 收藏
FeedbackTypeComment FeedbackType = "comment" // 评论
FeedbackTypeRead FeedbackType = "read" // 浏览
FeedbackTypeLike FeedbackType = "like" // 点赞
FeedbackTypeStar FeedbackType = "star" // 收藏
FeedbackTypeComment FeedbackType = "comment" // 评论
FeedbackTypeRead FeedbackType = "read" // 浏览
)
// Score 非个性化推荐返回的评分项
@@ -206,7 +207,10 @@ func (c *client) UpsertItemWithEmbedding(ctx context.Context, itemID string, cat
var err error
embedding, err = GetEmbedding(textToEmbed)
if err != nil {
log.Printf("[WARN] Failed to get embedding for item %s: %v, using zero vector", itemID, err)
zap.L().Warn("Failed to get embedding for item, using zero vector",
zap.String("itemID", itemID),
zap.Error(err),
)
embedding = make([]float64, 1024)
}
} else {

View File

@@ -5,9 +5,10 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
"go.uber.org/zap"
)
// EmbeddingConfig embedding服务配置
@@ -94,7 +95,7 @@ func GetEmbedding(text string) ([]float64, error) {
// InitEmbeddingWithConfig 从应用配置初始化embedding
func InitEmbeddingWithConfig(apiKey, url, model string) {
if apiKey == "" {
log.Println("[WARN] Gorse embedding API key not set, using default")
zap.L().Warn("Gorse embedding API key not set, using default")
}
defaultEmbeddingConfig.APIKey = apiKey
if url != "" {
@@ -103,4 +104,4 @@ func InitEmbeddingWithConfig(apiKey, url, model string) {
if model != "" {
defaultEmbeddingConfig.Model = model
}
}
}

View File

@@ -285,6 +285,33 @@ func (r *PostRepository) IsLiked(postID, userID string) bool {
return count > 0
}
// IsLikedBatch 批量检查是否点赞(解决 N+1 问题)
// 返回 map[postID]bool
func (r *PostRepository) IsLikedBatch(postIDs []string, userID string) map[string]bool {
result := make(map[string]bool)
if len(postIDs) == 0 || userID == "" {
return result
}
// 初始化所有 postID 为 false
for _, postID := range postIDs {
result[postID] = false
}
// 一次性查询所有点赞记录
var likedPostIDs []string
r.db.Model(&model.PostLike{}).
Where("post_id IN ? AND user_id = ?", postIDs, userID).
Pluck("post_id", &likedPostIDs)
// 更新已点赞的帖子
for _, postID := range likedPostIDs {
result[postID] = true
}
return result
}
// Favorite 收藏帖子
func (r *PostRepository) Favorite(postID, userID string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
@@ -336,6 +363,33 @@ func (r *PostRepository) IsFavorited(postID, userID string) bool {
return count > 0
}
// IsFavoritedBatch 批量检查是否收藏(解决 N+1 问题)
// 返回 map[postID]bool
func (r *PostRepository) IsFavoritedBatch(postIDs []string, userID string) map[string]bool {
result := make(map[string]bool)
if len(postIDs) == 0 || userID == "" {
return result
}
// 初始化所有 postID 为 false
for _, postID := range postIDs {
result[postID] = false
}
// 一次性查询所有收藏记录
var favoritedPostIDs []string
r.db.Model(&model.Favorite{}).
Where("post_id IN ? AND user_id = ?", postIDs, userID).
Pluck("post_id", &favoritedPostIDs)
// 更新已收藏的帖子
for _, postID := range favoritedPostIDs {
result[postID] = true
}
return result
}
// IncrementViews 增加帖子观看量
func (r *PostRepository) IncrementViews(postID string) error {
return r.db.Model(&model.Post{}).Where("id = ?", postID).

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

View File

@@ -1,8 +1,6 @@
package wire
import (
"log"
"carrot_bbs/internal/cache"
"carrot_bbs/internal/config"
"carrot_bbs/internal/repository"
@@ -11,6 +9,7 @@ import (
"github.com/casbin/casbin/v3"
gormadapter "github.com/casbin/gorm-adapter/v3"
"github.com/google/wire"
"go.uber.org/zap"
"gorm.io/gorm"
)
@@ -26,21 +25,27 @@ func ProvideCasbinEnforcer(cfg *config.Config, db *gorm.DB) *casbin.Enforcer {
// 创建 GORM 适配器,连接数据库
adapter, err := gormadapter.NewAdapterByDB(db)
if err != nil {
log.Fatalf("Failed to create Casbin GORM adapter: %v", err)
zap.L().Fatal("Failed to create Casbin GORM adapter",
zap.Error(err),
)
}
// 创建 Casbin Enforcer使用数据库适配器
enforcer, err := casbin.NewEnforcer(cfg.Casbin.ModelPath, adapter)
if err != nil {
log.Fatalf("Failed to create Casbin enforcer: %v", err)
zap.L().Fatal("Failed to create Casbin enforcer",
zap.Error(err),
)
}
// 从数据库加载策略
if err := enforcer.LoadPolicy(); err != nil {
log.Printf("[WARNING] Failed to load Casbin policy: %v", err)
zap.L().Warn("Failed to load Casbin policy",
zap.Error(err),
)
}
log.Println("[Wire] Initialized Casbin enforcer with GORM adapter")
zap.L().Info("Initialized Casbin enforcer with GORM adapter")
return enforcer
}

View File

@@ -1,7 +1,6 @@
package wire
import (
"log"
"time"
"carrot_bbs/internal/cache"
@@ -62,7 +61,9 @@ func ProvideDB(cfg *config.Config) (*gorm.DB, error) {
func ProvideRedisClient(cfg *config.Config) *redis.Client {
client, err := redis.New(&cfg.Redis)
if err != nil {
log.Printf("[WARNING] Failed to connect to Redis: %v, falling back to memory cache", err)
zap.L().Warn("Failed to connect to Redis, falling back to memory cache",
zap.Error(err),
)
return nil
}
return client
@@ -85,12 +86,12 @@ func ProvideCache(cfg *config.Config, redisClient *redis.Client) cache.Cache {
// 直接创建 RedisCache 实例,不依赖全局变量
if redisClient == nil {
log.Println("[Wire] Warning: Redis client is nil, cache will not be available")
zap.L().Warn("Redis client is nil, cache will not be available")
return nil
}
cacheInstance := cache.NewRedisCache(redisClient)
log.Println("[Wire] Initialized Redis cache via dependency injection")
zap.L().Info("Initialized Redis cache via dependency injection")
return cacheInstance
}
@@ -128,7 +129,9 @@ func ProvideTransactionManager(db *gorm.DB) repository.TransactionManager {
func ProvideLogger() *zap.Logger {
logger, err := zap.NewProduction()
if err != nil {
log.Fatalf("Failed to create logger: %v", err)
panic("Failed to create logger: " + err.Error())
}
// 设置为全局 logger以便在其他地方使用 zap.L()
zap.ReplaceGlobals(logger)
return logger
}