2026-05-09 18:01:08 +08:00

160 lines
5.8 KiB
Go

package voiceroomredpacket
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"chatapp3-golang/internal/config"
"chatapp3-golang/internal/model"
"gorm.io/gorm"
)
// Heartbeat 记录用户当前语音房在线态,供 Go 侧红包广播和房间校验使用。
func (s *Service) Heartbeat(ctx context.Context, user AuthUser, req PresenceRequest) (*PresenceResponse, error) {
roomID := req.RoomID.Int64()
if roomID <= 0 {
return nil, NewAppError(http.StatusBadRequest, "missing_room_id", "roomId is required")
}
sysOrigin := normalizeSysOrigin(user.SysOrigin)
regionCode, err := s.resolveUserRegion(ctx, sysOrigin, user.UserID)
if err != nil {
return nil, err
}
ttl := presenceTTL(s.cfg)
roomKey := onlineRoomKey(sysOrigin, roomID)
if err := s.redis.SAdd(ctx, roomKey, user.UserID).Err(); err != nil {
return nil, NewAppError(http.StatusServiceUnavailable, "voice_room_presence_failed", err.Error())
}
if err := s.redis.Expire(ctx, roomKey, ttl).Err(); err != nil {
return nil, NewAppError(http.StatusServiceUnavailable, "voice_room_presence_failed", err.Error())
}
if err := s.redis.Set(ctx, userRoomKey(sysOrigin, user.UserID), strconv.FormatInt(roomID, 10), ttl).Err(); err != nil {
return nil, NewAppError(http.StatusServiceUnavailable, "voice_room_presence_failed", err.Error())
}
if err := s.redis.Set(ctx, userRegionKey(sysOrigin, user.UserID), regionCode, ttl).Err(); err != nil {
return nil, NewAppError(http.StatusServiceUnavailable, "voice_room_presence_failed", err.Error())
}
if err := s.redis.Set(ctx, userSeenKey(sysOrigin, user.UserID), strconv.FormatInt(time.Now().UnixMilli(), 10), ttl).Err(); err != nil {
return nil, NewAppError(http.StatusServiceUnavailable, "voice_room_presence_failed", err.Error())
}
return &PresenceResponse{
UserID: user.UserID,
SysOrigin: sysOrigin,
RoomID: roomID,
RegionCode: regionCode,
TTLSeconds: int(ttl / time.Second),
}, nil
}
// Leave 清理用户当前房间在线态。
func (s *Service) Leave(ctx context.Context, user AuthUser, req PresenceRequest) (*PresenceResponse, error) {
roomID := req.RoomID.Int64()
if roomID <= 0 {
return nil, NewAppError(http.StatusBadRequest, "missing_room_id", "roomId is required")
}
sysOrigin := normalizeSysOrigin(user.SysOrigin)
regionCode, _ := s.resolveCachedUserRegion(ctx, sysOrigin, user.UserID)
if err := s.redis.SRem(ctx, onlineRoomKey(sysOrigin, roomID), user.UserID).Err(); err != nil {
return nil, NewAppError(http.StatusServiceUnavailable, "voice_room_presence_failed", err.Error())
}
if err := s.redis.Del(ctx, userRoomKey(sysOrigin, user.UserID), userSeenKey(sysOrigin, user.UserID)).Err(); err != nil {
return nil, NewAppError(http.StatusServiceUnavailable, "voice_room_presence_failed", err.Error())
}
return &PresenceResponse{
UserID: user.UserID,
SysOrigin: sysOrigin,
RoomID: roomID,
RegionCode: regionCode,
TTLSeconds: int(presenceTTL(s.cfg) / time.Second),
}, nil
}
func (s *Service) ensureUserInRoom(ctx context.Context, sysOrigin string, userID, roomID int64) error {
online, err := s.redis.SIsMember(ctx, onlineRoomKey(sysOrigin, roomID), userID).Result()
if err != nil {
return NewAppError(http.StatusServiceUnavailable, "voice_room_presence_failed", err.Error())
}
if !online {
return NewAppError(http.StatusBadRequest, "voice_room_red_packet_room_presence_required", "user is not online in this room")
}
return nil
}
func (s *Service) resolveCachedUserRegion(ctx context.Context, sysOrigin string, userID int64) (string, error) {
region, err := s.redis.Get(ctx, userRegionKey(sysOrigin, userID)).Result()
if err == nil && strings.TrimSpace(region) != "" {
return strings.ToUpper(strings.TrimSpace(region)), nil
}
return s.resolveUserRegion(ctx, sysOrigin, userID)
}
func (s *Service) resolveUserRegion(ctx context.Context, sysOrigin string, userID int64) (string, error) {
var user model.UserBaseInfo
err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", NewAppError(http.StatusBadRequest, "voice_room_region_missing", "user region is missing")
}
if err != nil {
return "", err
}
countryCode := strings.ToUpper(strings.TrimSpace(user.CountryCode))
if countryCode == "" {
return "", NewAppError(http.StatusBadRequest, "voice_room_region_missing", "user country code is missing")
}
if s != nil && s.regionResolver != nil {
regionCode, err := s.regionResolver.ResolveRegionCodeByCountryCode(ctx, sysOrigin, countryCode)
if err == nil {
if normalized := strings.ToUpper(strings.TrimSpace(regionCode)); normalized != "" {
return normalized, nil
}
}
}
var mapping model.VoiceRoomRegionCountry
err = s.db.WithContext(ctx).
Where("sys_origin = ? AND country_code = ? AND enabled = ?", sysOrigin, countryCode, true).
First(&mapping).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return countryCode, nil
}
if err != nil {
return "", err
}
regionCode := strings.ToUpper(strings.TrimSpace(mapping.RegionCode))
if regionCode == "" {
return countryCode, nil
}
return regionCode, nil
}
func onlineRoomKey(sysOrigin string, roomID int64) string {
return fmt.Sprintf("voice_room:online:%s:%d", normalizeSysOrigin(sysOrigin), roomID)
}
func userRoomKey(sysOrigin string, userID int64) string {
return fmt.Sprintf("voice_room:user_room:%s:%d", normalizeSysOrigin(sysOrigin), userID)
}
func userRegionKey(sysOrigin string, userID int64) string {
return fmt.Sprintf("voice_room:user_region:%s:%d", normalizeSysOrigin(sysOrigin), userID)
}
func userSeenKey(sysOrigin string, userID int64) string {
return fmt.Sprintf("voice_room:user_seen:%s:%d", normalizeSysOrigin(sysOrigin), userID)
}
func presenceTTL(cfg config.Config) time.Duration {
seconds := cfg.VoiceRoomRedPacket.PresenceTTLSeconds
if seconds <= 0 {
seconds = defaultPresenceTTLSeconds
}
return time.Duration(seconds) * time.Second
}