refactor: improve system stability, performance, and code structure
This commit introduces several architectural improvements and optimizations across the codebase: - **Performance & Reliability**: - Implemented Redis pipelining in `ConversationCache.CacheMessage` to reduce network round-trips. - Added a circuit breaker to the JPush client to prevent cascading failures. - Introduced batch deletion and batch member addition capabilities in repositories. - Added message idempotency support using `client_msg_id` and a Redis-based cache. - Optimized WebSocket handling with connection limits (total and per-user) and improved error logging. - **Code Refactoring**: - Refactored `Router` to use a `RouterDeps` struct, simplifying the constructor and improving maintainability. - Unified model ID generation logic using new `id_helper.go` (supporting UUID and Snowflake). - Standardized JSON serialization/deserialization in models using `json_helper.go`. - Refactored DTO conversion logic, specifically for `UserResponse` (using functional options) and `Report` responses. - Removed redundant/deprecated DTOs like `PostDetailResponse` and `TradeItemDetailResponse`. - **Cache Improvements**: - Enhanced `LayeredCache` with `SetRaw` to avoid double-encoding when promoting values from Redis to local cache. - Added `DeleteBatch` support to the cache interface. - **Other Changes**: - Cleaned up `config.go` by removing redundant default values and explicit environment variable overrides. - Improved WebSocket registration flow to handle connection limits gracefully.
This commit is contained in:
@@ -20,6 +20,9 @@ import (
|
||||
// 撤回消息的时间限制(2分钟)
|
||||
const RecallMessageTimeout = 2 * time.Minute
|
||||
|
||||
// 消息幂等性 TTL:24 小时内同一 client_msg_id 不会重复发送
|
||||
const messageIdempotentTTL = 24 * time.Hour
|
||||
|
||||
// ChatService 聊天服务接口
|
||||
type ChatService interface {
|
||||
// 会话管理
|
||||
@@ -31,7 +34,7 @@ type ChatService interface {
|
||||
SetConversationNotificationMuted(ctx context.Context, conversationID string, userID string, notificationMuted bool) error
|
||||
|
||||
// 消息操作
|
||||
SendMessage(ctx context.Context, senderID string, conversationID string, segments model.MessageSegments, replyToID *string) (*model.Message, error)
|
||||
SendMessage(ctx context.Context, senderID string, conversationID string, segments model.MessageSegments, replyToID *string, clientMsgID string) (*model.Message, error)
|
||||
GetMessages(ctx context.Context, conversationID string, userID string, page, pageSize int) ([]*model.Message, int64, error)
|
||||
GetMessagesAfterSeq(ctx context.Context, conversationID string, userID string, afterSeq int64, limit int) ([]*model.Message, error)
|
||||
GetMessagesBeforeSeq(ctx context.Context, conversationID string, userID string, beforeSeq int64, limit int) ([]*model.Message, error)
|
||||
@@ -57,15 +60,15 @@ type ChatService interface {
|
||||
|
||||
// chatServiceImpl 聊天服务实现
|
||||
type chatServiceImpl struct {
|
||||
repo repository.MessageRepository
|
||||
userRepo repository.UserRepository
|
||||
sensitive SensitiveService
|
||||
wsHub *ws.Hub
|
||||
pushSvc PushService
|
||||
repo repository.MessageRepository
|
||||
userRepo repository.UserRepository
|
||||
sensitive SensitiveService
|
||||
wsHub *ws.Hub
|
||||
pushSvc PushService
|
||||
cache cache.Cache
|
||||
|
||||
// 缓存相关字段
|
||||
conversationCache *cache.ConversationCache
|
||||
uploadService *UploadService // 校验聊天图片 URL 来源
|
||||
uploadService *UploadService
|
||||
}
|
||||
|
||||
// NewChatService 创建聊天服务
|
||||
@@ -96,6 +99,7 @@ func NewChatService(
|
||||
sensitive: sensitive,
|
||||
wsHub: wsHub,
|
||||
pushSvc: pushSvc,
|
||||
cache: cacheBackend,
|
||||
conversationCache: conversationCache,
|
||||
uploadService: uploadService,
|
||||
}
|
||||
@@ -245,9 +249,16 @@ func (s *chatServiceImpl) SetConversationNotificationMuted(ctx context.Context,
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendMessage 发送消息(使用 segments)
|
||||
func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conversationID string, segments model.MessageSegments, replyToID *string) (*model.Message, error) {
|
||||
// 首先验证会话是否存在
|
||||
func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conversationID string, segments model.MessageSegments, replyToID *string, clientMsgID string) (*model.Message, error) {
|
||||
if clientMsgID != "" && s.cache != nil {
|
||||
idemKey := cache.MessageIdempotentKey(senderID, clientMsgID)
|
||||
if cachedMsgID, ok := cache.GetTyped[string](s.cache, idemKey); ok && cachedMsgID != "" {
|
||||
if existing, err := s.repo.GetMessageByID(cachedMsgID); err == nil && existing != nil {
|
||||
return existing, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conv, err := s.getConversation(ctx, conversationID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -332,7 +343,6 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
|
||||
s.conversationCache.InvalidateMessagePages(conversationID)
|
||||
}
|
||||
|
||||
// 异步写入缓存
|
||||
go func() {
|
||||
if err := s.cacheMessage(context.Background(), conversationID, message); err != nil {
|
||||
zap.L().Warn("async cache message failed",
|
||||
@@ -344,12 +354,15 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
|
||||
}
|
||||
}()
|
||||
|
||||
if clientMsgID != "" && s.cache != nil {
|
||||
s.cache.Set(cache.MessageIdempotentKey(senderID, clientMsgID), message.ID, messageIdempotentTTL)
|
||||
}
|
||||
|
||||
// 获取会话中的参与者并发送消息
|
||||
participants, err := s.getParticipants(ctx, conversationID)
|
||||
if err == nil {
|
||||
targetIDs := make([]string, 0, len(participants))
|
||||
for _, p := range participants {
|
||||
// 私聊场景下,发送者已经从 HTTP 响应拿到消息,避免再通过 WebSocket 回推导致本端重复展示。
|
||||
if conv.Type == model.ConversationTypePrivate && p.UserID == senderID {
|
||||
continue
|
||||
}
|
||||
@@ -359,18 +372,21 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
|
||||
if conv.Type == model.ConversationTypeGroup {
|
||||
detailType = "group"
|
||||
}
|
||||
s.publishToUsers(targetIDs, "chat_message", map[string]any{
|
||||
"detail_type": detailType,
|
||||
"message": dto.ConvertMessageToResponse(message),
|
||||
})
|
||||
if len(targetIDs) > 20 && conv.Type == model.ConversationTypeGroup {
|
||||
s.wsHub.PublishToUsersConcurrent(targetIDs, "chat_message", map[string]any{
|
||||
"detail_type": detailType,
|
||||
"message": dto.ConvertMessageToResponse(message),
|
||||
})
|
||||
} else {
|
||||
s.publishToUsers(targetIDs, "chat_message", map[string]any{
|
||||
"detail_type": detailType,
|
||||
"message": dto.ConvertMessageToResponse(message),
|
||||
})
|
||||
}
|
||||
for _, p := range participants {
|
||||
if p.UserID == senderID {
|
||||
continue
|
||||
}
|
||||
// 失效未读数缓存
|
||||
if s.conversationCache != nil {
|
||||
s.conversationCache.InvalidateUnreadCount(p.UserID, conversationID)
|
||||
}
|
||||
if totalUnread, uErr := s.repo.GetAllUnreadCount(p.UserID); uErr == nil {
|
||||
s.publishToUsers([]string{p.UserID}, "conversation_unread", map[string]any{
|
||||
"conversation_id": conversationID,
|
||||
@@ -380,14 +396,22 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
|
||||
}
|
||||
}
|
||||
|
||||
// 失效会话列表缓存
|
||||
if s.conversationCache != nil {
|
||||
keys := make([]string, 0, len(participants)*2)
|
||||
for _, p := range participants {
|
||||
keys = append(keys,
|
||||
cache.UnreadDetailKey(p.UserID, conversationID),
|
||||
cache.UnreadConversationKey(p.UserID),
|
||||
)
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
s.cache.DeleteBatch(keys)
|
||||
}
|
||||
for _, p := range participants {
|
||||
s.conversationCache.InvalidateConversationList(p.UserID)
|
||||
}
|
||||
}
|
||||
|
||||
// 对离线用户通过 JPush 推送聊天消息通知
|
||||
if s.pushSvc != nil && len(participants) > 0 {
|
||||
sender := ChatMessageSender{ID: senderID}
|
||||
if senderUser, sErr := s.userRepo.GetByID(senderID); sErr == nil {
|
||||
@@ -399,26 +423,37 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
|
||||
if conv.Type == model.ConversationTypeGroup && conv.Group != nil {
|
||||
convName = conv.Group.Name
|
||||
}
|
||||
allTargetIDs := make([]string, 0, len(participants))
|
||||
for _, p := range participants {
|
||||
if p.UserID == senderID {
|
||||
continue
|
||||
}
|
||||
go func(userID string, sender ChatMessageSender) {
|
||||
if pushErr := s.pushSvc.PushChatMessage(context.Background(), userID, conversationID, &sender, convType, convName, message); pushErr != nil {
|
||||
zap.L().Debug("push chat message skipped or failed",
|
||||
zap.String("userID", userID),
|
||||
zap.String("conversationID", conversationID),
|
||||
zap.Error(pushErr),
|
||||
)
|
||||
allTargetIDs = append(allTargetIDs, p.UserID)
|
||||
}
|
||||
var offlineTargetIDs []string
|
||||
if s.wsHub != nil && len(allTargetIDs) > 0 {
|
||||
_, offlineTargetIDs = s.wsHub.FilterOnline(allTargetIDs)
|
||||
} else {
|
||||
offlineTargetIDs = allTargetIDs
|
||||
}
|
||||
if len(offlineTargetIDs) > 0 {
|
||||
go func(ids []string, sender ChatMessageSender) {
|
||||
for _, uid := range ids {
|
||||
if pushErr := s.pushSvc.PushChatMessage(context.Background(), uid, conversationID, &sender, convType, convName, message); pushErr != nil {
|
||||
zap.L().Debug("push chat message failed",
|
||||
zap.String("userID", uid),
|
||||
zap.String("conversationID", conversationID),
|
||||
zap.Error(pushErr),
|
||||
)
|
||||
}
|
||||
}
|
||||
}(p.UserID, sender)
|
||||
}(offlineTargetIDs, sender)
|
||||
}
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
// getConversation 获取会话(优先使用缓存)
|
||||
func (s *chatServiceImpl) getConversation(ctx context.Context, conversationID string) (*model.Conversation, error) {
|
||||
if s.conversationCache != nil {
|
||||
return s.conversationCache.GetConversation(ctx, conversationID)
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
type TradeService interface {
|
||||
Create(ctx context.Context, userID string, req dto.CreateTradeItemRequest) (*model.TradeItem, error)
|
||||
GetByID(ctx context.Context, id string, currentUserID *string) (*dto.TradeItemDetailResponse, error)
|
||||
GetByID(ctx context.Context, id string, currentUserID *string) (*dto.TradeItemResponse, error)
|
||||
Update(ctx context.Context, userID string, id string, req dto.UpdateTradeItemRequest) error
|
||||
Delete(ctx context.Context, userID string, id string) error
|
||||
UpdateStatus(ctx context.Context, userID string, id string, status model.TradeItemStatus) error
|
||||
@@ -62,7 +62,7 @@ func (s *tradeService) Create(ctx context.Context, userID string, req dto.Create
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *tradeService) GetByID(ctx context.Context, id string, currentUserID *string) (*dto.TradeItemDetailResponse, error) {
|
||||
func (s *tradeService) GetByID(ctx context.Context, id string, currentUserID *string) (*dto.TradeItemResponse, error) {
|
||||
item, err := s.tradeRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -81,7 +81,7 @@ func (s *tradeService) GetByID(ctx context.Context, id string, currentUserID *st
|
||||
|
||||
_ = s.tradeRepo.IncrementViews(ctx, id)
|
||||
|
||||
return dto.ConvertTradeItemToDetailResponse(item, isFavorited), nil
|
||||
return dto.ConvertTradeItemToResponse(item, isFavorited), nil
|
||||
}
|
||||
|
||||
func (s *tradeService) Update(ctx context.Context, userID string, id string, req dto.UpdateTradeItemRequest) error {
|
||||
|
||||
Reference in New Issue
Block a user