package voiceroomredpacket import ( "context" "encoding/json" "errors" "fmt" "log" "strconv" "strings" "chatapp3-golang/internal/config" "chatapp3-golang/internal/model" "chatapp3-golang/internal/service/tencentim" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) type redisNotifier struct { redis redis.Cmdable streamKey string } func newRedisNotifier(cache redis.Cmdable, streamKey string) *redisNotifier { return &redisNotifier{redis: cache, streamKey: streamKey} } func (n *redisNotifier) publish(ctx context.Context, scope string, regionCode string, excludeRoomID int64, message notificationMessage) error { if n == nil || n.redis == nil { return nil } body, err := json.Marshal(message) if err != nil { return err } return n.redis.XAdd(ctx, &redis.XAddArgs{ Stream: n.streamKey, Values: map[string]any{ "scope": scope, "regionCode": regionCode, "excludeRoomId": strconv.FormatInt(excludeRoomID, 10), "roomId": strconv.FormatInt(message.RoomID, 10), "packetNo": message.PacketNo, "body": string(body), }, }).Err() } func (s *Service) notifyCreated(ctx context.Context, packet model.VoiceRoomRedPacket) error { message := notificationMessage{ Type: "VOICE_ROOM_RED_PACKET", Event: "CREATED", PacketNo: packet.PacketNo, RoomID: packet.RoomID, PacketMode: packet.PacketMode, SenderUserID: packet.SenderUserID, TotalAmount: packet.TotalAmount, TotalCount: packet.TotalCount, ClaimStartTime: packet.ClaimStartTime.UnixMilli(), ExpireTime: packet.ExpireTime.UnixMilli(), } if packet.PacketMode == packetModeDelayed { var notifyErr error if err := s.notifier.publish(ctx, "REGION", packet.SenderRegionCode, packet.RoomID, message); err != nil { notifyErr = err } if err := s.sendRegionIMRedPacketNotification(ctx, packet); err != nil { return mergeNotifyErrors(notifyErr, err) } return notifyErr } var notifyErr error if err := s.notifier.publish(ctx, "ROOM", packet.SenderRegionCode, 0, message); err != nil { notifyErr = err } if err := s.sendRoomIMRedPacketNotification(ctx, packet); err != nil { return mergeNotifyErrors(notifyErr, err) } return notifyErr } func (s *Service) sendRegionIMRedPacketNotification(ctx context.Context, packet model.VoiceRoomRedPacket) error { if s == nil || s.regionIM == nil { return tencentim.ErrNotConfigured } body, err := s.buildRoomRedPacketIMBody(ctx, packet) if err != nil { return err } _, err = s.regionIM.SendRegionCustomMessage(ctx, packet.SysOrigin, packet.SenderRegionCode, body) return err } func (s *Service) sendRoomIMRedPacketNotification(ctx context.Context, packet model.VoiceRoomRedPacket) error { if s == nil || s.im == nil { return tencentim.ErrNotConfigured } groupID, err := s.resolveRoomIMGroupID(ctx, packet.RoomID) if err != nil { return err } body, err := s.buildRoomRedPacketIMBodyWithRoomAccount(ctx, packet, groupID) if err != nil { return err } return s.im.SendCustomGroupMessage(ctx, groupID, body) } func (s *Service) buildRoomRedPacketIMBody(ctx context.Context, packet model.VoiceRoomRedPacket) (map[string]any, error) { roomAccount, _ := s.resolveRoomIMGroupID(ctx, packet.RoomID) return s.buildRoomRedPacketIMBodyWithRoomAccount(ctx, packet, roomAccount) } func (s *Service) buildRoomRedPacketIMBodyWithRoomAccount(ctx context.Context, packet model.VoiceRoomRedPacket, roomAccount string) (map[string]any, error) { profile, err := s.loadBroadcastUserProfile(ctx, packet.SenderUserID) if err != nil { return nil, err } return map[string]any{ "type": "ROOM_RED_PACKET", "data": map[string]any{ "userNickname": profile.UserNickname, "account": profile.Account, "actualAccount": profile.Account, "userAvatar": profile.UserAvatar, "countryCode": profile.CountryCode, "countryName": profile.CountryName, "roomAccount": roomAccount, "roomId": strconv.FormatInt(packet.RoomID, 10), "packetId": packet.PacketNo, "packetMode": packet.PacketMode, "totalAmount": packet.TotalAmount, "totalCount": packet.TotalCount, "regionCode": packet.SenderRegionCode, "claimStartTime": packet.ClaimStartTime.UnixMilli(), "expireTime": packet.ExpireTime.UnixMilli(), }, }, nil } func (s *Service) resolveRoomIMGroupID(ctx context.Context, roomID int64) (string, error) { if roomID <= 0 { return "", fmt.Errorf("room id is required") } if s != nil && s.roomProfiles != nil { profiles, err := s.roomProfiles.MapRoomProfiles(ctx, []int64{roomID}) if err == nil { if profile, ok := profiles[roomID]; ok { if roomAccount := strings.TrimSpace(profile.RoomAccount); roomAccount != "" { return roomAccount, nil } } for _, profile := range profiles { if int64(profile.ID) == roomID { if roomAccount := strings.TrimSpace(profile.RoomAccount); roomAccount != "" { return roomAccount, nil } } } } } var room model.RoomLiveBroadcastVIP err := s.db.WithContext(ctx). Where("room_id = ? OR id = ?", roomID, roomID). Order("update_time DESC"). First(&room).Error if err == nil && room.RoomAccount > 0 { return strconv.FormatInt(room.RoomAccount, 10), nil } if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return "", err } return strconv.FormatInt(roomID, 10), nil } func mergeNotifyErrors(first error, second error) error { if first == nil { return second } if second == nil { return first } return fmt.Errorf("%v; %w", first, second) } func (s *Service) loadBroadcastUserProfile(ctx context.Context, userID int64) (model.UserBaseInfo, error) { var user model.UserBaseInfo err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error if errors.Is(err, gorm.ErrRecordNotFound) { account := strconv.FormatInt(userID, 10) return model.UserBaseInfo{ ID: userID, Account: account, UserNickname: account, }, nil } if err != nil { return model.UserBaseInfo{}, err } if strings.TrimSpace(user.Account) == "" { user.Account = strconv.FormatInt(userID, 10) } if strings.TrimSpace(user.UserNickname) == "" { user.UserNickname = user.Account } return user, nil } func notifyStreamKey(cfg config.Config) string { if value := cfg.VoiceRoomRedPacket.NotifyStreamKey; value != "" { return value } return defaultNotifyStreamKey } func logNotifyFailure(packetNo string, err error) { if err != nil { log.Printf("voice room red packet notify failed packetNo=%s err=%v", packetNo, err) } }