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:
11
internal/cache/cache.go
vendored
11
internal/cache/cache.go
vendored
@@ -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) {
|
||||
|
||||
9
internal/cache/conversation_cache.go
vendored
9
internal/cache/conversation_cache.go
vendored
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
48
internal/cache/redis_cache.go
vendored
48
internal/cache/redis_cache.go
vendored
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user