840 lines
32 KiB
Go
840 lines
32 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"math/rand"
|
|
"strings"
|
|
"time"
|
|
|
|
roomv1 "hyapp.local/api/proto/room/v1"
|
|
"hyapp/pkg/appcode"
|
|
"hyapp/pkg/logx"
|
|
"hyapp/pkg/xerr"
|
|
)
|
|
|
|
const (
|
|
humanRobotLuckyPoolID = "human_robot_lucky_display"
|
|
humanRobotSuperLuckyPoolID = "human_robot_super_lucky_display"
|
|
|
|
defaultHumanRobotCandidateRoomMaxOnline = int32(5)
|
|
defaultHumanRobotRoomFullStopOnline = int32(10)
|
|
defaultHumanRobotRoomTargetMinOnline = int32(8)
|
|
defaultHumanRobotRoomTargetMaxOnline = int32(10)
|
|
defaultHumanRobotStayMinMS = int64(60 * 1000)
|
|
defaultHumanRobotStayMaxMS = int64(10 * 60 * 1000)
|
|
defaultHumanRobotReplaceMinMS = int64(60 * 1000)
|
|
defaultHumanRobotReplaceMaxMS = int64(3 * 60 * 1000)
|
|
defaultHumanRobotNormalGiftIntervalMS = int64(10 * 1000)
|
|
defaultHumanRobotNormalGiftIntervalMinMS = int64(1 * 1000)
|
|
defaultHumanRobotNormalGiftIntervalMaxMS = int64(20 * 1000)
|
|
defaultHumanRobotLuckyPauseMinMS = int64(5 * 1000)
|
|
defaultHumanRobotLuckyPauseMaxMS = int64(20 * 1000)
|
|
defaultHumanRobotLuckyComboMin = int64(1)
|
|
defaultHumanRobotLuckyComboMax = int64(3)
|
|
defaultHumanRobotMaxGiftSenders = int64(1)
|
|
)
|
|
|
|
func (s *Service) AdminGetHumanRoomRobotConfig(ctx context.Context, req *roomv1.AdminGetHumanRoomRobotConfigRequest) (*roomv1.AdminGetHumanRoomRobotConfigResponse, error) {
|
|
ctx = contextFromMeta(ctx, req.GetMeta())
|
|
config, exists, err := s.repository.GetHumanRoomRobotConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exists {
|
|
config = defaultHumanRoomRobotConfig(appcode.FromContext(ctx), s.clock.Now().UnixMilli())
|
|
}
|
|
return &roomv1.AdminGetHumanRoomRobotConfigResponse{Config: humanRoomRobotConfigToProto(config), ServerTimeMs: s.clock.Now().UnixMilli()}, nil
|
|
}
|
|
|
|
func (s *Service) AdminUpdateHumanRoomRobotConfig(ctx context.Context, req *roomv1.AdminUpdateHumanRoomRobotConfigRequest) (*roomv1.AdminUpdateHumanRoomRobotConfigResponse, error) {
|
|
ctx = contextFromMeta(ctx, req.GetMeta())
|
|
config, err := normalizeHumanRoomRobotConfig(appcode.FromContext(ctx), req.GetConfig(), req.GetAdminId(), s.clock.Now().UnixMilli())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
saved, err := s.repository.UpsertHumanRoomRobotConfig(ctx, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !saved.Enabled {
|
|
s.stopAllHumanRoomRobotRuntimes()
|
|
}
|
|
return &roomv1.AdminUpdateHumanRoomRobotConfigResponse{Config: humanRoomRobotConfigToProto(saved), ServerTimeMs: s.clock.Now().UnixMilli()}, nil
|
|
}
|
|
|
|
// RunHumanRoomRobotRuntimeManager 定期把各国家空闲机器人补进低人数真人房间。
|
|
func (s *Service) RunHumanRoomRobotRuntimeManager(ctx context.Context, interval time.Duration) {
|
|
if interval <= 0 {
|
|
interval = 15 * time.Second
|
|
}
|
|
logx.Info(ctx, "human_room_robot_runtime_manager_started", slog.Int64("interval_ms", interval.Milliseconds()))
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
s.scanHumanRoomRobotRuntimes(ctx)
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
s.stopAllHumanRoomRobotRuntimes()
|
|
return
|
|
case <-ticker.C:
|
|
s.scanHumanRoomRobotRuntimes(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) scanHumanRoomRobotRuntimes(ctx context.Context) {
|
|
config, exists, err := s.repository.GetHumanRoomRobotConfig(ctx)
|
|
if err != nil {
|
|
logx.Warn(ctx, "human_room_robot_config_load_failed", slog.String("error", err.Error()))
|
|
return
|
|
}
|
|
if !exists {
|
|
return
|
|
}
|
|
config = withHumanRoomRobotDefaults(config)
|
|
if !config.Enabled {
|
|
s.stopAllHumanRoomRobotRuntimes()
|
|
return
|
|
}
|
|
pools, err := s.loadHumanRoomRobotPools(ctx, config.AppCode)
|
|
if err != nil {
|
|
logx.Warn(ctx, "human_room_robot_pool_load_failed", slog.String("error", err.Error()))
|
|
return
|
|
}
|
|
config.CountryPools = pools
|
|
for _, pool := range config.CountryPools {
|
|
countryCode := normalizeRoomCountryCode(pool.CountryCode)
|
|
if countryCode == "" || len(pool.RobotUserIDs) == 0 {
|
|
continue
|
|
}
|
|
rooms, err := s.repository.ListHumanRobotCandidateRooms(ctx, config.AppCode, countryCode, config.CandidateRoomMaxOnline, config.RoomTargetMaxOnline, 50)
|
|
if err != nil {
|
|
logx.Warn(ctx, "human_room_robot_candidate_rooms_failed", slog.String("country_code", countryCode), slog.String("error", err.Error()))
|
|
continue
|
|
}
|
|
for _, room := range rooms {
|
|
if s.humanRoomRobotRuntimeConfigChanged(room.RoomID, config.UpdatedAtMS) {
|
|
s.stopHumanRoomRobotRuntime(room.RoomID)
|
|
}
|
|
if s.humanRoomRobotRuntimeExists(room.RoomID) {
|
|
continue
|
|
}
|
|
s.startHumanRoomRobotRuntime(ctx, config, countryCode, room.RoomID, pool.RobotUserIDs)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) loadHumanRoomRobotPools(ctx context.Context, appCode string) ([]HumanRoomRobotCountryPool, error) {
|
|
if s.humanRobotPoolProvider == nil {
|
|
return nil, xerr.New(xerr.Unavailable, "human room robot pool provider is not configured")
|
|
}
|
|
items, err := s.humanRobotPoolProvider.ListHumanRoomRobotPools(ctx, appCode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pools := make([]HumanRoomRobotCountryPool, 0, len(items))
|
|
for _, item := range items {
|
|
countryCode := normalizeRoomCountryCode(item.CountryCode)
|
|
ids := normalizeRobotUserIDs(item.RobotUserIDs)
|
|
if countryCode == "" || len(ids) == 0 {
|
|
continue
|
|
}
|
|
pools = append(pools, HumanRoomRobotCountryPool{CountryCode: countryCode, RobotUserIDs: ids})
|
|
}
|
|
return pools, nil
|
|
}
|
|
|
|
func (s *Service) startHumanRoomRobotRuntime(parent context.Context, config HumanRoomRobotConfig, countryCode string, roomID string, pool []int64) {
|
|
if parent.Err() != nil || roomID == "" || len(pool) == 0 {
|
|
return
|
|
}
|
|
s.humanRobotRuntimeMu.Lock()
|
|
if s.humanRobotRuntimes == nil {
|
|
s.humanRobotRuntimes = make(map[string]robotRoomRuntime)
|
|
}
|
|
if _, exists := s.humanRobotRuntimes[roomID]; exists {
|
|
s.humanRobotRuntimeMu.Unlock()
|
|
return
|
|
}
|
|
s.humanRobotRuntimeMu.Unlock()
|
|
|
|
roomCtx := appcode.WithContext(parent, config.AppCode)
|
|
targetOnline := randomHumanRoomTargetOnline(config)
|
|
selected, err := s.prepareHumanRoomRobots(roomCtx, config, roomID, pool, targetOnline)
|
|
if err != nil {
|
|
logx.Warn(roomCtx, "human_room_robot_prepare_failed", slog.String("room_id", roomID), slog.String("country_code", countryCode), slog.String("error", err.Error()))
|
|
return
|
|
}
|
|
if len(selected) == 0 {
|
|
return
|
|
}
|
|
|
|
s.humanRobotRuntimeMu.Lock()
|
|
if _, exists := s.humanRobotRuntimes[roomID]; exists {
|
|
s.humanRobotRuntimeMu.Unlock()
|
|
return
|
|
}
|
|
ctx, cancel := context.WithCancel(roomCtx)
|
|
token := fmt.Sprintf("%s:%d", roomID, time.Now().UTC().UnixNano())
|
|
s.humanRobotRuntimes[roomID] = robotRoomRuntime{cancel: cancel, token: token, configUpdatedAtMS: config.UpdatedAtMS}
|
|
s.humanRobotRuntimeMu.Unlock()
|
|
|
|
logx.Info(ctx, "human_room_robot_runtime_started",
|
|
slog.String("room_id", roomID),
|
|
slog.String("country_code", countryCode),
|
|
slog.Int("target_online", targetOnline),
|
|
slog.Int("active_robot_count", len(selected)),
|
|
)
|
|
go func() {
|
|
defer s.clearHumanRoomRobotRuntime(roomID, token)
|
|
s.runHumanRoomRobotRuntime(ctx, config, roomID, pool, selected, targetOnline)
|
|
}()
|
|
}
|
|
|
|
func (s *Service) prepareHumanRoomRobots(ctx context.Context, config HumanRoomRobotConfig, roomID string, pool []int64, targetOnline int) ([]int64, error) {
|
|
snapshot, err := s.currentSnapshot(ctx, roomID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if snapshot.GetStatus() != "active" {
|
|
return nil, nil
|
|
}
|
|
online := make(map[int64]bool, len(snapshot.GetOnlineUsers()))
|
|
for _, user := range snapshot.GetOnlineUsers() {
|
|
online[user.GetUserId()] = true
|
|
}
|
|
humanOnline := humanRoomRealOnlineCount(snapshot.GetOnlineUsers(), allHumanRoomRobotIDs(config))
|
|
if humanOnline >= int(config.CandidateRoomMaxOnline) || len(online) >= targetOnline {
|
|
return nil, nil
|
|
}
|
|
limit := targetOnline - len(online)
|
|
if limit <= 0 {
|
|
return nil, nil
|
|
}
|
|
freeSeats := freeSeatNumbers(snapshot.GetMicSeats())
|
|
if len(freeSeats) < limit {
|
|
limit = len(freeSeats)
|
|
}
|
|
if limit <= 0 {
|
|
return nil, nil
|
|
}
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
candidates := append([]int64(nil), pool...)
|
|
rng.Shuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] })
|
|
selected := make([]int64, 0, limit)
|
|
for _, userID := range candidates {
|
|
if len(selected) >= limit {
|
|
break
|
|
}
|
|
if userID <= 0 || online[userID] || s.robotHasActiveRoomPresence(ctx, userID, roomID) {
|
|
continue
|
|
}
|
|
seatNo := freeSeats[len(selected)]
|
|
if err := s.joinHumanRoomRobot(ctx, roomID, userID, seatNo, fmt.Sprintf("initial:%d:%d", userID, time.Now().UTC().UnixNano())); err != nil {
|
|
logx.Warn(ctx, "human_room_robot_initial_join_failed", slog.String("room_id", roomID), slog.Int64("robot_user_id", userID), slog.String("error", err.Error()))
|
|
continue
|
|
}
|
|
selected = append(selected, userID)
|
|
}
|
|
return selected, nil
|
|
}
|
|
|
|
func (s *Service) runHumanRoomRobotRuntime(ctx context.Context, config HumanRoomRobotConfig, roomID string, pool []int64, initialActive []int64, targetOnline int) {
|
|
activity := newRobotRoomActivity(pool)
|
|
for _, userID := range initialActive {
|
|
activity.markActive(userID)
|
|
}
|
|
markInitialHumanRoomGiftSenders(activity, initialActive, int(config.MaxGiftSenders))
|
|
giftSlots := make(chan struct{}, int(max(1, int(config.MaxGiftSenders))))
|
|
for _, userID := range pool {
|
|
userID := userID
|
|
go s.runHumanRobotPresenceLoop(ctx, config, roomID, pool, activity, userID, targetOnline)
|
|
go s.runHumanRobotNormalGiftLoop(ctx, config, roomID, activity, giftSlots, userID)
|
|
go s.runHumanRobotLuckyGiftLoop(ctx, config, roomID, activity, giftSlots, userID, config.LuckyGiftIDs, humanRobotLuckyPoolID, "lucky", 5_000_000)
|
|
go s.runHumanRobotLuckyGiftLoop(ctx, config, roomID, activity, giftSlots, userID, config.SuperLuckyGiftIDs, humanRobotSuperLuckyPoolID, "super_lucky", 20_000_000)
|
|
}
|
|
go s.runHumanRobotFillLoop(ctx, config, roomID, pool, activity, targetOnline)
|
|
<-ctx.Done()
|
|
}
|
|
|
|
func (s *Service) runHumanRobotFillLoop(ctx context.Context, config HumanRoomRobotConfig, roomID string, pool []int64, activity *robotRoomActivity, targetOnline int) {
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
timer := time.NewTimer(time.Duration(1+rng.Intn(3)) * time.Second)
|
|
defer timer.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-timer.C:
|
|
s.fillHumanRoomRobotsOnce(ctx, config, roomID, pool, activity, rng, targetOnline)
|
|
timer.Reset(time.Duration(5+rng.Intn(5)) * time.Second)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) fillHumanRoomRobotsOnce(ctx context.Context, config HumanRoomRobotConfig, roomID string, pool []int64, activity *robotRoomActivity, rng *rand.Rand, targetOnline int) {
|
|
snapshot, err := s.currentSnapshot(ctx, roomID)
|
|
if err != nil || snapshot.GetStatus() != "active" {
|
|
return
|
|
}
|
|
humanOnline := humanRoomRealOnlineCount(snapshot.GetOnlineUsers(), allHumanRoomRobotIDs(config))
|
|
if humanOnline >= int(config.CandidateRoomMaxOnline) || len(snapshot.GetOnlineUsers()) >= targetOnline {
|
|
return
|
|
}
|
|
limit := targetOnline - len(snapshot.GetOnlineUsers())
|
|
freeSeats := freeSeatNumbers(snapshot.GetMicSeats())
|
|
if len(freeSeats) < limit {
|
|
limit = len(freeSeats)
|
|
}
|
|
if limit <= 0 {
|
|
return
|
|
}
|
|
online := make(map[int64]bool, len(snapshot.GetOnlineUsers()))
|
|
for _, user := range snapshot.GetOnlineUsers() {
|
|
online[user.GetUserId()] = true
|
|
}
|
|
candidates := append([]int64(nil), pool...)
|
|
rng.Shuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] })
|
|
joined := 0
|
|
for _, userID := range candidates {
|
|
if joined >= limit {
|
|
return
|
|
}
|
|
if userID <= 0 || online[userID] || activity.isActive(userID) || s.robotHasActiveRoomPresence(ctx, userID, roomID) {
|
|
continue
|
|
}
|
|
seatNo := freeSeats[joined]
|
|
if err := s.joinHumanRoomRobot(ctx, roomID, userID, seatNo, fmt.Sprintf("fill:%d:%d", userID, time.Now().UTC().UnixNano())); err != nil {
|
|
logx.Warn(ctx, "human_room_robot_fill_join_failed", slog.String("room_id", roomID), slog.Int64("robot_user_id", userID), slog.String("error", err.Error()))
|
|
continue
|
|
}
|
|
activity.markActive(userID)
|
|
activity.markGiftSenderWithinLimit(userID, int(config.MaxGiftSenders))
|
|
online[userID] = true
|
|
joined++
|
|
}
|
|
}
|
|
|
|
func (s *Service) runHumanRobotPresenceLoop(ctx context.Context, config HumanRoomRobotConfig, roomID string, pool []int64, activity *robotRoomActivity, userID int64, targetOnline int) {
|
|
if userID <= 0 {
|
|
return
|
|
}
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano() + userID*43))
|
|
for {
|
|
if !activity.isActive(userID) {
|
|
if !waitRobotRuntimeDelay(ctx, time.Duration(300+rng.Intn(700))*time.Millisecond) {
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
stay := time.Duration(randomInt64Range(rng, config.RobotStayMinMS, config.RobotStayMaxMS)) * time.Millisecond
|
|
if !waitRobotRuntimeDelay(ctx, stay) {
|
|
return
|
|
}
|
|
// 送礼名额绑定在房间内的少数机器人身上;送礼机器人离开时,补位机器人接替这个名额,保证房间内送礼者数量稳定且不会扩散到所有机器人。
|
|
wasGiftSender := activity.isGiftSender(userID)
|
|
if !activity.markInactive(userID) {
|
|
continue
|
|
}
|
|
seatNo := s.humanRoomRobotSeatNo(ctx, roomID, userID)
|
|
if err := s.leaveHumanRoomRobot(ctx, roomID, userID); err != nil {
|
|
logx.Warn(ctx, "human_room_robot_leave_failed", slog.String("room_id", roomID), slog.Int64("robot_user_id", userID), slog.String("error", err.Error()))
|
|
activity.markActive(userID)
|
|
if wasGiftSender {
|
|
activity.markGiftSender(userID)
|
|
}
|
|
continue
|
|
}
|
|
delay := time.Duration(randomInt64Range(rng, config.RobotReplaceMinMS, config.RobotReplaceMaxMS)) * time.Millisecond
|
|
if !waitRobotRuntimeDelay(ctx, delay) {
|
|
return
|
|
}
|
|
replacementID := s.pickHumanRoomReplacement(ctx, config, roomID, pool, activity, userID)
|
|
if replacementID <= 0 {
|
|
continue
|
|
}
|
|
if seatNo <= 0 {
|
|
seatNo = s.firstHumanRoomFreeSeatNo(ctx, roomID)
|
|
}
|
|
if seatNo <= 0 || !s.humanRoomNeedsRobot(ctx, config, roomID, targetOnline) {
|
|
continue
|
|
}
|
|
if err := s.joinHumanRoomRobot(ctx, roomID, replacementID, seatNo, fmt.Sprintf("replace:%d:%d", replacementID, time.Now().UTC().UnixNano())); err != nil {
|
|
logx.Warn(ctx, "human_room_robot_replace_join_failed", slog.String("room_id", roomID), slog.Int64("replacement_user_id", replacementID), slog.String("error", err.Error()))
|
|
continue
|
|
}
|
|
activity.markActive(replacementID)
|
|
if wasGiftSender || activity.giftSenderCount() < int(config.MaxGiftSenders) {
|
|
activity.markGiftSenderWithinLimit(replacementID, int(config.MaxGiftSenders))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) runHumanRobotNormalGiftLoop(ctx context.Context, config HumanRoomRobotConfig, roomID string, activity *robotRoomActivity, giftSlots chan struct{}, senderID int64) {
|
|
if config.NormalGiftIntervalMinMS <= 0 || config.NormalGiftIntervalMaxMS <= 0 || len(config.GiftIDs) == 0 {
|
|
return
|
|
}
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano() + senderID*59))
|
|
timer := time.NewTimer(humanRoomRobotNormalGiftDelay(config, rng))
|
|
defer timer.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-timer.C:
|
|
if activity.isActive(senderID) && activity.isGiftSender(senderID) {
|
|
targetID := randomRobotTarget(rng, activity.activeIDs(), senderID)
|
|
giftID := randomString(rng, config.GiftIDs)
|
|
s.sendHumanRoomRobotGiftBestEffort(ctx, config, roomID, activity, giftSlots, senderID, targetID, giftID, "", "normal", 0)
|
|
}
|
|
timer.Reset(humanRoomRobotNormalGiftDelay(config, rng))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) runHumanRobotLuckyGiftLoop(ctx context.Context, config HumanRoomRobotConfig, roomID string, activity *robotRoomActivity, giftSlots chan struct{}, senderID int64, giftIDs []string, poolID string, kind string, multiplierPPM int64) {
|
|
if len(giftIDs) == 0 || config.LuckyComboMax <= 0 {
|
|
return
|
|
}
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano() + senderID*71))
|
|
for {
|
|
if !activity.isActive(senderID) || !activity.isGiftSender(senderID) {
|
|
if !waitRobotRuntimeDelay(ctx, time.Duration(300+rng.Intn(700))*time.Millisecond) {
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
combo := randomInt64Range(rng, config.LuckyComboMin, config.LuckyComboMax)
|
|
for i := int64(0); i < combo; i++ {
|
|
if !activity.isActive(senderID) || !activity.isGiftSender(senderID) {
|
|
break
|
|
}
|
|
targetID := randomRobotTarget(rng, activity.activeIDs(), senderID)
|
|
giftID := randomString(rng, giftIDs)
|
|
s.sendHumanRoomRobotGiftBestEffort(ctx, config, roomID, activity, giftSlots, senderID, targetID, giftID, poolID, kind, multiplierPPM)
|
|
if !waitRobotGiftPace(ctx, rng) {
|
|
return
|
|
}
|
|
}
|
|
pause := time.Duration(randomInt64Range(rng, config.LuckyPauseMinMS, config.LuckyPauseMaxMS)) * time.Millisecond
|
|
if !waitRobotRuntimeDelay(ctx, pause) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) sendHumanRoomRobotGiftBestEffort(ctx context.Context, config HumanRoomRobotConfig, roomID string, activity *robotRoomActivity, giftSlots chan struct{}, senderID int64, targetID int64, giftID string, poolID string, kind string, multiplierPPM int64) {
|
|
if !activity.isActive(senderID) || !activity.isGiftSender(senderID) || !activity.isActive(targetID) || giftID == "" {
|
|
return
|
|
}
|
|
select {
|
|
case giftSlots <- struct{}{}:
|
|
defer func() { <-giftSlots }()
|
|
default:
|
|
return
|
|
}
|
|
_, err := s.RobotSendGift(ctx, RobotSendGiftInput{
|
|
Meta: humanRoomRobotCommandMeta(config, roomID, senderID, fmt.Sprintf("gift:%s:%d:%d", kind, targetID, time.Now().UTC().UnixNano())),
|
|
TargetUserID: targetID,
|
|
GiftID: giftID,
|
|
GiftCount: 1,
|
|
PoolID: poolID,
|
|
RobotUserIDs: activity.pool,
|
|
SyntheticMultiplierPPM: multiplierPPM,
|
|
RealRoomHeat: true,
|
|
})
|
|
if err != nil {
|
|
logx.Warn(ctx, "human_room_robot_send_gift_failed", slog.String("room_id", roomID), slog.String("kind", kind), slog.Int64("sender_user_id", senderID), slog.Int64("target_user_id", targetID), slog.String("gift_id", giftID), slog.String("error", err.Error()))
|
|
}
|
|
}
|
|
|
|
func markInitialHumanRoomGiftSenders(activity *robotRoomActivity, initialActive []int64, maxSenders int) {
|
|
if activity == nil || maxSenders <= 0 || len(initialActive) == 0 {
|
|
return
|
|
}
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
candidates := append([]int64(nil), initialActive...)
|
|
rng.Shuffle(len(candidates), func(i int, j int) {
|
|
candidates[i], candidates[j] = candidates[j], candidates[i]
|
|
})
|
|
for _, userID := range candidates {
|
|
if activity.giftSenderCount() >= maxSenders {
|
|
return
|
|
}
|
|
activity.markGiftSenderWithinLimit(userID, maxSenders)
|
|
}
|
|
}
|
|
|
|
func humanRoomRobotNormalGiftDelay(config HumanRoomRobotConfig, rng *rand.Rand) time.Duration {
|
|
minMS := config.NormalGiftIntervalMinMS
|
|
maxMS := config.NormalGiftIntervalMaxMS
|
|
if minMS <= 0 && maxMS <= 0 && config.NormalGiftIntervalMS > 0 {
|
|
minMS = config.NormalGiftIntervalMS
|
|
maxMS = config.NormalGiftIntervalMS
|
|
}
|
|
if minMS <= 0 {
|
|
minMS = defaultHumanRobotNormalGiftIntervalMinMS
|
|
}
|
|
if maxMS < minMS {
|
|
maxMS = minMS
|
|
}
|
|
return time.Duration(randomInt64Range(rng, minMS, maxMS)) * time.Millisecond
|
|
}
|
|
|
|
func randomHumanRoomTargetOnline(config HumanRoomRobotConfig) int {
|
|
minOnline := config.RoomTargetMinOnline
|
|
maxOnline := config.RoomTargetMaxOnline
|
|
if minOnline <= 0 && maxOnline <= 0 && config.RoomFullStopOnline > 0 {
|
|
minOnline = config.RoomFullStopOnline
|
|
maxOnline = config.RoomFullStopOnline
|
|
}
|
|
if minOnline <= 0 {
|
|
minOnline = defaultHumanRobotRoomTargetMinOnline
|
|
}
|
|
if maxOnline < minOnline {
|
|
maxOnline = minOnline
|
|
}
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
return int(randomInt64Range(rng, int64(minOnline), int64(maxOnline)))
|
|
}
|
|
|
|
func (s *Service) joinHumanRoomRobot(ctx context.Context, roomID string, userID int64, seatNo int32, suffix string) error {
|
|
if _, err := s.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
|
Meta: humanRoomRobotCommandMeta(HumanRoomRobotConfig{AppCode: appcode.FromContext(ctx)}, roomID, userID, "join:"+suffix),
|
|
Role: "audience",
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if _, err := s.RobotVirtualMicUp(ctx, RobotMicUpInput{
|
|
Meta: humanRoomRobotCommandMeta(HumanRoomRobotConfig{AppCode: appcode.FromContext(ctx)}, roomID, userID, "mic:"+suffix),
|
|
TargetUserID: userID,
|
|
SeatNo: seatNo,
|
|
}); err != nil {
|
|
if xerr.CodeOf(err) == xerr.Conflict && strings.Contains(xerr.MessageOf(err), "already on seat") {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) leaveHumanRoomRobot(ctx context.Context, roomID string, userID int64) error {
|
|
_, err := s.LeaveRoom(ctx, &roomv1.LeaveRoomRequest{
|
|
Meta: humanRoomRobotCommandMeta(HumanRoomRobotConfig{AppCode: appcode.FromContext(ctx)}, roomID, userID, fmt.Sprintf("leave:%d:%d", userID, time.Now().UTC().UnixNano())),
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (s *Service) pickHumanRoomReplacement(ctx context.Context, config HumanRoomRobotConfig, roomID string, pool []int64, activity *robotRoomActivity, departingUserID int64) int64 {
|
|
active := make(map[int64]bool, len(activity.activeIDs()))
|
|
for _, id := range activity.activeIDs() {
|
|
active[id] = true
|
|
}
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
candidates := append([]int64(nil), pool...)
|
|
rng.Shuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] })
|
|
for _, userID := range candidates {
|
|
if userID <= 0 || active[userID] || userID == departingUserID || s.robotHasActiveRoomPresence(ctx, userID, roomID) {
|
|
continue
|
|
}
|
|
return userID
|
|
}
|
|
if !s.robotHasActiveRoomPresence(ctx, departingUserID, roomID) {
|
|
return departingUserID
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (s *Service) humanRoomNeedsRobot(ctx context.Context, config HumanRoomRobotConfig, roomID string, targetOnline int) bool {
|
|
snapshot, err := s.currentSnapshot(ctx, roomID)
|
|
if err != nil || snapshot.GetStatus() != "active" {
|
|
return false
|
|
}
|
|
humanOnline := humanRoomRealOnlineCount(snapshot.GetOnlineUsers(), allHumanRoomRobotIDs(config))
|
|
return humanOnline < int(config.CandidateRoomMaxOnline) && len(snapshot.GetOnlineUsers()) < targetOnline
|
|
}
|
|
|
|
func (s *Service) robotHasActiveRoomPresence(ctx context.Context, userID int64, allowedRoomID string) bool {
|
|
presence, exists, err := s.repository.GetCurrentRoomPresence(ctx, userID)
|
|
if err != nil || !exists {
|
|
return false
|
|
}
|
|
return presence.RoomID != "" && presence.RoomID != allowedRoomID
|
|
}
|
|
|
|
func (s *Service) humanRoomRobotSeatNo(ctx context.Context, roomID string, userID int64) int32 {
|
|
snapshot, err := s.currentSnapshot(ctx, roomID)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
for _, seat := range snapshot.GetMicSeats() {
|
|
if seat.GetUserId() == userID {
|
|
return seat.GetSeatNo()
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (s *Service) firstHumanRoomFreeSeatNo(ctx context.Context, roomID string) int32 {
|
|
snapshot, err := s.currentSnapshot(ctx, roomID)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
seats := freeSeatNumbers(snapshot.GetMicSeats())
|
|
if len(seats) == 0 {
|
|
return 0
|
|
}
|
|
return seats[0]
|
|
}
|
|
|
|
func (s *Service) humanRoomRobotRuntimeExists(roomID string) bool {
|
|
s.humanRobotRuntimeMu.Lock()
|
|
defer s.humanRobotRuntimeMu.Unlock()
|
|
_, exists := s.humanRobotRuntimes[roomID]
|
|
return exists
|
|
}
|
|
|
|
func (s *Service) humanRoomRobotRuntimeConfigChanged(roomID string, updatedAtMS int64) bool {
|
|
if updatedAtMS <= 0 {
|
|
return false
|
|
}
|
|
s.humanRobotRuntimeMu.Lock()
|
|
defer s.humanRobotRuntimeMu.Unlock()
|
|
runtime, exists := s.humanRobotRuntimes[roomID]
|
|
return exists && runtime.configUpdatedAtMS > 0 && runtime.configUpdatedAtMS != updatedAtMS
|
|
}
|
|
|
|
func (s *Service) stopHumanRoomRobotRuntime(roomID string) {
|
|
s.humanRobotRuntimeMu.Lock()
|
|
runtime := s.humanRobotRuntimes[roomID]
|
|
delete(s.humanRobotRuntimes, roomID)
|
|
s.humanRobotRuntimeMu.Unlock()
|
|
if runtime.cancel != nil {
|
|
runtime.cancel()
|
|
}
|
|
}
|
|
|
|
func (s *Service) clearHumanRoomRobotRuntime(roomID string, token string) {
|
|
s.humanRobotRuntimeMu.Lock()
|
|
defer s.humanRobotRuntimeMu.Unlock()
|
|
runtime, exists := s.humanRobotRuntimes[roomID]
|
|
if !exists || runtime.token != token {
|
|
return
|
|
}
|
|
delete(s.humanRobotRuntimes, roomID)
|
|
}
|
|
|
|
func (s *Service) stopAllHumanRoomRobotRuntimes() {
|
|
s.humanRobotRuntimeMu.Lock()
|
|
cancels := make([]context.CancelFunc, 0, len(s.humanRobotRuntimes))
|
|
for roomID, runtime := range s.humanRobotRuntimes {
|
|
if runtime.cancel != nil {
|
|
cancels = append(cancels, runtime.cancel)
|
|
}
|
|
delete(s.humanRobotRuntimes, roomID)
|
|
}
|
|
s.humanRobotRuntimeMu.Unlock()
|
|
for _, cancel := range cancels {
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
func normalizeHumanRoomRobotConfig(appCode string, input *roomv1.AdminHumanRoomRobotConfig, adminID uint64, nowMS int64) (HumanRoomRobotConfig, error) {
|
|
if input == nil {
|
|
return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "human room robot config is required")
|
|
}
|
|
config := HumanRoomRobotConfig{
|
|
AppCode: appcode.Normalize(appCode),
|
|
Enabled: input.GetEnabled(),
|
|
CandidateRoomMaxOnline: input.GetCandidateRoomMaxOnline(),
|
|
RoomFullStopOnline: input.GetRoomFullStopOnline(),
|
|
RoomTargetMinOnline: input.GetRoomTargetMinOnline(),
|
|
RoomTargetMaxOnline: input.GetRoomTargetMaxOnline(),
|
|
RobotStayMinMS: input.GetRobotStayMinMs(),
|
|
RobotStayMaxMS: input.GetRobotStayMaxMs(),
|
|
RobotReplaceMinMS: input.GetRobotReplaceMinMs(),
|
|
RobotReplaceMaxMS: input.GetRobotReplaceMaxMs(),
|
|
NormalGiftIntervalMS: input.GetNormalGiftIntervalMs(),
|
|
NormalGiftIntervalMinMS: input.GetNormalGiftIntervalMinMs(),
|
|
NormalGiftIntervalMaxMS: input.GetNormalGiftIntervalMaxMs(),
|
|
GiftIDs: normalizeStringSet(input.GetGiftIds()),
|
|
LuckyGiftIDs: normalizeStringSet(input.GetLuckyGiftIds()),
|
|
SuperLuckyGiftIDs: normalizeStringSet(input.GetSuperLuckyGiftIds()),
|
|
LuckyComboMin: input.GetLuckyComboMin(),
|
|
LuckyComboMax: input.GetLuckyComboMax(),
|
|
LuckyPauseMinMS: input.GetLuckyPauseMinMs(),
|
|
LuckyPauseMaxMS: input.GetLuckyPauseMaxMs(),
|
|
MaxGiftSenders: input.GetMaxGiftSenders(),
|
|
UpdatedByAdminID: adminID,
|
|
CreatedAtMS: input.GetCreatedAtMs(),
|
|
UpdatedAtMS: nowMS,
|
|
}
|
|
config = withHumanRoomRobotDefaults(config)
|
|
if config.CandidateRoomMaxOnline <= 0 || config.RoomTargetMinOnline <= 0 || config.RoomTargetMaxOnline < config.RoomTargetMinOnline {
|
|
return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "room online thresholds are invalid")
|
|
}
|
|
if config.RobotStayMinMS <= 0 || config.RobotStayMaxMS < config.RobotStayMinMS {
|
|
return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "robot stay range is invalid")
|
|
}
|
|
if config.RobotReplaceMinMS < 0 || config.RobotReplaceMaxMS < config.RobotReplaceMinMS {
|
|
return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "robot replace range is invalid")
|
|
}
|
|
if config.NormalGiftIntervalMinMS <= 0 || config.NormalGiftIntervalMaxMS < config.NormalGiftIntervalMinMS {
|
|
return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "normal gift interval range is invalid")
|
|
}
|
|
if config.Enabled && len(config.GiftIDs) == 0 {
|
|
return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "gift_ids is required")
|
|
}
|
|
if config.Enabled && len(config.LuckyGiftIDs) == 0 && len(config.SuperLuckyGiftIDs) == 0 {
|
|
return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "lucky gift ids are required")
|
|
}
|
|
if config.LuckyComboMin <= 0 || config.LuckyComboMax < config.LuckyComboMin {
|
|
return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "lucky combo range is invalid")
|
|
}
|
|
if config.LuckyPauseMinMS <= 0 || config.LuckyPauseMaxMS < config.LuckyPauseMinMS {
|
|
return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "lucky pause range is invalid")
|
|
}
|
|
if config.MaxGiftSenders <= 0 {
|
|
return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "max_gift_senders is required")
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func withHumanRoomRobotDefaults(config HumanRoomRobotConfig) HumanRoomRobotConfig {
|
|
if config.AppCode == "" {
|
|
config.AppCode = appcode.Default
|
|
}
|
|
if config.CandidateRoomMaxOnline <= 0 {
|
|
config.CandidateRoomMaxOnline = defaultHumanRobotCandidateRoomMaxOnline
|
|
}
|
|
if config.RoomTargetMinOnline <= 0 && config.RoomTargetMaxOnline <= 0 {
|
|
if config.RoomFullStopOnline > 0 {
|
|
config.RoomTargetMinOnline = config.RoomFullStopOnline
|
|
config.RoomTargetMaxOnline = config.RoomFullStopOnline
|
|
} else {
|
|
config.RoomTargetMinOnline = defaultHumanRobotRoomTargetMinOnline
|
|
config.RoomTargetMaxOnline = defaultHumanRobotRoomTargetMaxOnline
|
|
}
|
|
}
|
|
if config.RoomFullStopOnline <= 0 {
|
|
config.RoomFullStopOnline = config.RoomTargetMaxOnline
|
|
}
|
|
if config.RobotStayMinMS <= 0 && config.RobotStayMaxMS <= 0 {
|
|
config.RobotStayMinMS = defaultHumanRobotStayMinMS
|
|
config.RobotStayMaxMS = defaultHumanRobotStayMaxMS
|
|
}
|
|
if config.RobotReplaceMinMS == 0 && config.RobotReplaceMaxMS == 0 {
|
|
config.RobotReplaceMinMS = defaultHumanRobotReplaceMinMS
|
|
config.RobotReplaceMaxMS = defaultHumanRobotReplaceMaxMS
|
|
}
|
|
if config.NormalGiftIntervalMinMS <= 0 && config.NormalGiftIntervalMaxMS <= 0 {
|
|
if config.NormalGiftIntervalMS > 0 {
|
|
config.NormalGiftIntervalMinMS = config.NormalGiftIntervalMS
|
|
config.NormalGiftIntervalMaxMS = config.NormalGiftIntervalMS
|
|
} else {
|
|
config.NormalGiftIntervalMinMS = defaultHumanRobotNormalGiftIntervalMinMS
|
|
config.NormalGiftIntervalMaxMS = defaultHumanRobotNormalGiftIntervalMaxMS
|
|
}
|
|
}
|
|
if config.NormalGiftIntervalMS <= 0 {
|
|
config.NormalGiftIntervalMS = config.NormalGiftIntervalMaxMS
|
|
}
|
|
if config.LuckyComboMin <= 0 && config.LuckyComboMax <= 0 {
|
|
config.LuckyComboMin = defaultHumanRobotLuckyComboMin
|
|
config.LuckyComboMax = defaultHumanRobotLuckyComboMax
|
|
}
|
|
if config.LuckyPauseMinMS <= 0 && config.LuckyPauseMaxMS <= 0 {
|
|
config.LuckyPauseMinMS = defaultHumanRobotLuckyPauseMinMS
|
|
config.LuckyPauseMaxMS = defaultHumanRobotLuckyPauseMaxMS
|
|
}
|
|
if config.MaxGiftSenders <= 0 {
|
|
config.MaxGiftSenders = defaultHumanRobotMaxGiftSenders
|
|
}
|
|
return config
|
|
}
|
|
|
|
func defaultHumanRoomRobotConfig(appCode string, nowMS int64) HumanRoomRobotConfig {
|
|
return withHumanRoomRobotDefaults(HumanRoomRobotConfig{AppCode: appcode.Normalize(appCode), CreatedAtMS: nowMS, UpdatedAtMS: nowMS})
|
|
}
|
|
|
|
func humanRoomRobotConfigToProto(config HumanRoomRobotConfig) *roomv1.AdminHumanRoomRobotConfig {
|
|
config = withHumanRoomRobotDefaults(config)
|
|
out := &roomv1.AdminHumanRoomRobotConfig{
|
|
AppCode: config.AppCode,
|
|
Enabled: config.Enabled,
|
|
CandidateRoomMaxOnline: config.CandidateRoomMaxOnline,
|
|
RoomFullStopOnline: config.RoomFullStopOnline,
|
|
RoomTargetMinOnline: config.RoomTargetMinOnline,
|
|
RoomTargetMaxOnline: config.RoomTargetMaxOnline,
|
|
RobotStayMinMs: config.RobotStayMinMS,
|
|
RobotStayMaxMs: config.RobotStayMaxMS,
|
|
RobotReplaceMinMs: config.RobotReplaceMinMS,
|
|
RobotReplaceMaxMs: config.RobotReplaceMaxMS,
|
|
NormalGiftIntervalMs: config.NormalGiftIntervalMS,
|
|
NormalGiftIntervalMinMs: config.NormalGiftIntervalMinMS,
|
|
NormalGiftIntervalMaxMs: config.NormalGiftIntervalMaxMS,
|
|
GiftIds: append([]string(nil), config.GiftIDs...),
|
|
LuckyGiftIds: append([]string(nil), config.LuckyGiftIDs...),
|
|
SuperLuckyGiftIds: append([]string(nil), config.SuperLuckyGiftIDs...),
|
|
LuckyComboMin: config.LuckyComboMin,
|
|
LuckyComboMax: config.LuckyComboMax,
|
|
LuckyPauseMinMs: config.LuckyPauseMinMS,
|
|
LuckyPauseMaxMs: config.LuckyPauseMaxMS,
|
|
MaxGiftSenders: config.MaxGiftSenders,
|
|
UpdatedByAdminId: config.UpdatedByAdminID,
|
|
CreatedAtMs: config.CreatedAtMS,
|
|
UpdatedAtMs: config.UpdatedAtMS,
|
|
}
|
|
return out
|
|
}
|
|
|
|
func humanRoomRobotCommandMeta(config HumanRoomRobotConfig, roomID string, actorUserID int64, suffix string) *roomv1.RequestMeta {
|
|
commandID := fmt.Sprintf("human-room-robot:%s:%s", roomID, strings.TrimSpace(suffix))
|
|
if len(commandID) > 128 {
|
|
commandID = commandID[:128]
|
|
}
|
|
return &roomv1.RequestMeta{
|
|
RequestId: commandID,
|
|
CommandId: commandID,
|
|
ActorUserId: actorUserID,
|
|
RoomId: roomID,
|
|
AppCode: config.AppCode,
|
|
SentAtMs: time.Now().UTC().UnixMilli(),
|
|
}
|
|
}
|
|
|
|
func humanRoomRealOnlineCount(users []*roomv1.RoomUser, robotIDs map[int64]bool) int {
|
|
count := 0
|
|
for _, user := range users {
|
|
if user.GetUserId() <= 0 || robotIDs[user.GetUserId()] {
|
|
continue
|
|
}
|
|
count++
|
|
}
|
|
return count
|
|
}
|
|
|
|
func allHumanRoomRobotIDs(config HumanRoomRobotConfig) map[int64]bool {
|
|
out := make(map[int64]bool)
|
|
for _, pool := range config.CountryPools {
|
|
for _, id := range pool.RobotUserIDs {
|
|
if id > 0 {
|
|
out[id] = true
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func freeSeatNumbers(seats []*roomv1.SeatState) []int32 {
|
|
out := make([]int32, 0, len(seats))
|
|
for _, seat := range seats {
|
|
if seat.GetSeatNo() > 0 && seat.GetUserId() == 0 && !seat.GetLocked() {
|
|
out = append(out, seat.GetSeatNo())
|
|
}
|
|
}
|
|
return out
|
|
}
|