160 lines
5.8 KiB
Go
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
|
|
}
|