// Package mysql 实现 room-service 的 MySQL 持久化边界。 package mysql import ( "context" "database/sql" "encoding/json" "errors" "strconv" "strings" "time" "hyapp/pkg/appcode" "hyapp/pkg/xerr" roomservice "hyapp/services/room-service/internal/room/service" ) // ListRobotRooms 读取后台机器人房间列表;机器人房间配置归 room-service 所有,后台不直连房间库。 func (r *Repository) ListRobotRooms(ctx context.Context, query roomservice.RobotRoomListQuery) ([]roomservice.RobotRoomConfig, int64, error) { appCode := normalizedRecordAppCode(ctx, query.AppCode) page := query.Page if page <= 0 { page = 1 } pageSize := query.PageSize if pageSize <= 0 || pageSize > 100 { pageSize = 50 } status := strings.TrimSpace(query.Status) args := []any{appCode} where := `WHERE app_code = ?` if status != "" { where += ` AND status = ?` args = append(args, status) } var total int64 if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM room_robot_rooms `+where, args...).Scan(&total); err != nil { return nil, 0, err } args = append(args, pageSize, (page-1)*pageSize) rows, err := r.db.QueryContext(ctx, `SELECT app_code, room_id, room_short_id, title, cover_url, visible_region_id, status, owner_robot_user_id, robot_user_ids_json, active_robot_count, seat_count, gift_ids_json, lucky_gift_ids_json, normal_gift_interval_ms, lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms, robot_stay_min_ms, robot_stay_max_ms, robot_replace_min_ms, robot_replace_max_ms, max_gift_senders, created_by_admin_id, created_at_ms, updated_at_ms FROM room_robot_rooms `+where+` ORDER BY updated_at_ms DESC, room_id DESC LIMIT ? OFFSET ?`, args..., ) if err != nil { return nil, 0, err } defer rows.Close() items, err := scanRobotRoomConfigs(rows) return items, total, err } // GetRobotRoom 精确读取一个机器人房间配置。 func (r *Repository) GetRobotRoom(ctx context.Context, roomID string) (roomservice.RobotRoomConfig, bool, error) { row := r.db.QueryRowContext(ctx, `SELECT app_code, room_id, room_short_id, title, cover_url, visible_region_id, status, owner_robot_user_id, robot_user_ids_json, active_robot_count, seat_count, gift_ids_json, lucky_gift_ids_json, normal_gift_interval_ms, lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms, robot_stay_min_ms, robot_stay_max_ms, robot_replace_min_ms, robot_replace_max_ms, max_gift_senders, created_by_admin_id, created_at_ms, updated_at_ms FROM room_robot_rooms WHERE app_code = ? AND room_id = ?`, appcode.FromContext(ctx), strings.TrimSpace(roomID), ) item, err := scanRobotRoomConfig(row) if errors.Is(err, sql.ErrNoRows) { return roomservice.RobotRoomConfig{}, false, nil } return item, err == nil, err } // CreateRobotRoomConfig 写入机器人房间配置,并用事务锁定候选账号避免两个 active 机器人房复用同一批账号。 func (r *Repository) CreateRobotRoomConfig(ctx context.Context, input roomservice.CreateRobotRoomConfigInput) (roomservice.RobotRoomConfig, error) { config := input.Config appCode := normalizedRecordAppCode(ctx, config.AppCode) nowMS := input.NowMS if nowMS <= 0 { nowMS = time.Now().UTC().UnixMilli() } config.AppCode = appCode config.CreatedAtMS = firstPositiveInt64(config.CreatedAtMS, nowMS) config.UpdatedAtMS = firstPositiveInt64(config.UpdatedAtMS, nowMS) if config.Status == "" { config.Status = "active" } rawRobots, rawGifts, rawLuckyGifts, err := robotRoomJSON(config) if err != nil { return roomservice.RobotRoomConfig{}, err } tx, err := r.db.BeginTx(ctx, nil) if err != nil { return roomservice.RobotRoomConfig{}, err } defer func() { _ = tx.Rollback() }() occupied, err := occupiedRobotUserIDsTx(ctx, tx, appCode, config.RobotUserIDs) if err != nil { return roomservice.RobotRoomConfig{}, err } if len(occupied) > 0 { return roomservice.RobotRoomConfig{}, xerr.New(xerr.Conflict, "robot user already assigned to active robot room") } _, err = tx.ExecContext(ctx, `INSERT INTO room_robot_rooms ( app_code, room_id, room_short_id, title, cover_url, visible_region_id, status, owner_robot_user_id, robot_user_ids_json, active_robot_count, seat_count, gift_ids_json, lucky_gift_ids_json, normal_gift_interval_ms, lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms, robot_stay_min_ms, robot_stay_max_ms, robot_replace_min_ms, robot_replace_max_ms, max_gift_senders, created_by_admin_id, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, config.AppCode, config.RoomID, config.RoomShortID, config.Title, config.CoverURL, config.VisibleRegionID, config.Status, config.OwnerRobotUserID, string(rawRobots), config.ActiveRobotCount, config.SeatCount, string(rawGifts), string(rawLuckyGifts), config.GiftRule.NormalGiftIntervalMS, config.GiftRule.LuckyComboMin, config.GiftRule.LuckyComboMax, config.GiftRule.LuckyPauseMinMS, config.GiftRule.LuckyPauseMaxMS, config.GiftRule.RobotStayMinMS, config.GiftRule.RobotStayMaxMS, config.GiftRule.RobotReplaceMinMS, config.GiftRule.RobotReplaceMaxMS, config.GiftRule.MaxGiftSenders, config.CreatedByAdminID, config.CreatedAtMS, config.UpdatedAtMS, ) if err != nil { return roomservice.RobotRoomConfig{}, err } if err := tx.Commit(); err != nil { return roomservice.RobotRoomConfig{}, err } return config, nil } // UpdateRobotRoomStatus 只切换机器人房间运行态;停止后账号会从“已占用”集合释放。 func (r *Repository) UpdateRobotRoomStatus(ctx context.Context, roomID string, status string, nowMS int64) (roomservice.RobotRoomConfig, bool, error) { status = strings.TrimSpace(status) if nowMS <= 0 { nowMS = time.Now().UTC().UnixMilli() } res, err := r.db.ExecContext(ctx, `UPDATE room_robot_rooms SET status = ?, updated_at_ms = ? WHERE app_code = ? AND room_id = ?`, status, nowMS, appcode.FromContext(ctx), strings.TrimSpace(roomID), ) if err != nil { return roomservice.RobotRoomConfig{}, false, err } affected, _ := res.RowsAffected() if affected == 0 { return roomservice.RobotRoomConfig{}, false, nil } item, exists, err := r.GetRobotRoom(ctx, roomID) return item, exists, err } // OccupiedRobotUserIDs 返回仍被 active 机器人房占用的机器人账号集合。 func (r *Repository) OccupiedRobotUserIDs(ctx context.Context, userIDs []int64) (map[int64]bool, error) { return occupiedRobotUserIDsQuery(ctx, r.db, appcode.FromContext(ctx), userIDs, false) } // ListActiveRobotRooms 为 room-service 重启恢复运行时循环提供当前 active 配置。 func (r *Repository) ListActiveRobotRooms(ctx context.Context) ([]roomservice.RobotRoomConfig, error) { rows, err := r.db.QueryContext(ctx, `SELECT app_code, room_id, room_short_id, title, cover_url, visible_region_id, status, owner_robot_user_id, robot_user_ids_json, active_robot_count, seat_count, gift_ids_json, lucky_gift_ids_json, normal_gift_interval_ms, lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms, robot_stay_min_ms, robot_stay_max_ms, robot_replace_min_ms, robot_replace_max_ms, max_gift_senders, created_by_admin_id, created_at_ms, updated_at_ms FROM room_robot_rooms WHERE status = 'active' ORDER BY app_code ASC, updated_at_ms DESC, room_id DESC`, ) if err != nil { return nil, err } defer rows.Close() return scanRobotRoomConfigs(rows) } // GetHumanRoomRobotConfig 读取真人房间机器人行为配置;没有配置时由 service 层补默认值。 func (r *Repository) GetHumanRoomRobotConfig(ctx context.Context) (roomservice.HumanRoomRobotConfig, bool, error) { row := r.db.QueryRowContext(ctx, `SELECT app_code, enabled, candidate_room_max_online, room_full_stop_online, room_target_min_online, room_target_max_online, robot_stay_min_ms, robot_stay_max_ms, robot_replace_min_ms, robot_replace_max_ms, normal_gift_interval_ms, normal_gift_interval_min_ms, normal_gift_interval_max_ms, gift_ids_json, lucky_gift_ids_json, super_lucky_gift_ids_json, lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms, max_gift_senders, country_pools_json, COALESCE(allowed_owner_ids_json, JSON_ARRAY()), country_limit_enabled, COALESCE(country_rules_json, JSON_ARRAY()), updated_by_admin_id, created_at_ms, updated_at_ms FROM room_human_robot_configs WHERE app_code = ?`, appcode.FromContext(ctx), ) config, err := scanHumanRoomRobotConfig(row) if errors.Is(err, sql.ErrNoRows) { return roomservice.HumanRoomRobotConfig{}, false, nil } return config, err == nil, err } // UpsertHumanRoomRobotConfig 保存真人房间机器人行为配置;运行时扫描每轮读取最新配置。 func (r *Repository) UpsertHumanRoomRobotConfig(ctx context.Context, config roomservice.HumanRoomRobotConfig) (roomservice.HumanRoomRobotConfig, error) { appCode := normalizedRecordAppCode(ctx, config.AppCode) nowMS := config.UpdatedAtMS if nowMS <= 0 { nowMS = time.Now().UTC().UnixMilli() } config.AppCode = appCode config.CreatedAtMS = firstPositiveInt64(config.CreatedAtMS, nowMS) config.UpdatedAtMS = nowMS rawGifts, rawLucky, rawSuperLucky, rawPools, rawAllowedOwners, rawCountryRules, err := humanRoomRobotConfigJSON(config) if err != nil { return roomservice.HumanRoomRobotConfig{}, err } _, err = r.db.ExecContext(ctx, `INSERT INTO room_human_robot_configs ( app_code, enabled, candidate_room_max_online, room_full_stop_online, room_target_min_online, room_target_max_online, robot_stay_min_ms, robot_stay_max_ms, robot_replace_min_ms, robot_replace_max_ms, normal_gift_interval_ms, normal_gift_interval_min_ms, normal_gift_interval_max_ms, gift_ids_json, lucky_gift_ids_json, super_lucky_gift_ids_json, lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms, max_gift_senders, country_pools_json, allowed_owner_ids_json, country_limit_enabled, country_rules_json, updated_by_admin_id, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE enabled = VALUES(enabled), candidate_room_max_online = VALUES(candidate_room_max_online), room_full_stop_online = VALUES(room_full_stop_online), room_target_min_online = VALUES(room_target_min_online), room_target_max_online = VALUES(room_target_max_online), robot_stay_min_ms = VALUES(robot_stay_min_ms), robot_stay_max_ms = VALUES(robot_stay_max_ms), robot_replace_min_ms = VALUES(robot_replace_min_ms), robot_replace_max_ms = VALUES(robot_replace_max_ms), normal_gift_interval_ms = VALUES(normal_gift_interval_ms), normal_gift_interval_min_ms = VALUES(normal_gift_interval_min_ms), normal_gift_interval_max_ms = VALUES(normal_gift_interval_max_ms), gift_ids_json = VALUES(gift_ids_json), lucky_gift_ids_json = VALUES(lucky_gift_ids_json), super_lucky_gift_ids_json = VALUES(super_lucky_gift_ids_json), lucky_combo_min = VALUES(lucky_combo_min), lucky_combo_max = VALUES(lucky_combo_max), lucky_pause_min_ms = VALUES(lucky_pause_min_ms), lucky_pause_max_ms = VALUES(lucky_pause_max_ms), max_gift_senders = VALUES(max_gift_senders), country_pools_json = VALUES(country_pools_json), allowed_owner_ids_json = VALUES(allowed_owner_ids_json), country_limit_enabled = VALUES(country_limit_enabled), country_rules_json = VALUES(country_rules_json), updated_by_admin_id = VALUES(updated_by_admin_id), updated_at_ms = VALUES(updated_at_ms)`, config.AppCode, boolToInt(config.Enabled), config.CandidateRoomMaxOnline, config.RoomFullStopOnline, config.RoomTargetMinOnline, config.RoomTargetMaxOnline, config.RobotStayMinMS, config.RobotStayMaxMS, config.RobotReplaceMinMS, config.RobotReplaceMaxMS, config.NormalGiftIntervalMS, config.NormalGiftIntervalMinMS, config.NormalGiftIntervalMaxMS, string(rawGifts), string(rawLucky), string(rawSuperLucky), config.LuckyComboMin, config.LuckyComboMax, config.LuckyPauseMinMS, config.LuckyPauseMaxMS, config.MaxGiftSenders, string(rawPools), string(rawAllowedOwners), boolToInt(config.CountryLimitEnabled), string(rawCountryRules), config.UpdatedByAdminID, config.CreatedAtMS, config.UpdatedAtMS, ) if err != nil { return roomservice.HumanRoomRobotConfig{}, err } saved, _, err := r.GetHumanRoomRobotConfig(ctx) return saved, err } // ListHumanRobotCandidateRooms 找出同国家低人数真人房;allowedOwnerIDs 为空时保持全量扫描,非空时只命中房主短号或内部 owner_user_id。 func (r *Repository) ListHumanRobotCandidateRooms(ctx context.Context, appCode string, countryCode string, maxOnline int32, fullStopOnline int32, limit int, allowedOwnerIDs []string) ([]roomservice.RoomListEntry, error) { appCode = normalizedRecordAppCode(ctx, appCode) countryCode = strings.ToUpper(strings.TrimSpace(countryCode)) if maxOnline <= 0 { maxOnline = 5 } if fullStopOnline <= 0 { fullStopOnline = 10 } if maxOnline > fullStopOnline { maxOnline = fullStopOnline } if limit <= 0 || limit > 100 { limit = 50 } shortIDs, ownerUserIDs := splitHumanRobotAllowedOwnerIDs(allowedOwnerIDs) where := []string{ "app_code = ?", "status = 'active'", "owner_country_code = ?", "online_count < ?", "online_count < ?", "room_id NOT LIKE 'robot\\_%'", } args := []any{appCode, countryCode, maxOnline, fullStopOnline} if len(shortIDs) > 0 { ownerWhere := make([]string, 0, 2) shortPlaceholders := strings.TrimRight(strings.Repeat("?,", len(shortIDs)), ",") ownerWhere = append(ownerWhere, "room_short_id IN ("+shortPlaceholders+")") for _, id := range shortIDs { args = append(args, id) } if len(ownerUserIDs) > 0 { userPlaceholders := strings.TrimRight(strings.Repeat("?,", len(ownerUserIDs)), ",") ownerWhere = append(ownerWhere, "owner_user_id IN ("+userPlaceholders+")") for _, id := range ownerUserIDs { args = append(args, id) } } where = append(where, "("+strings.Join(ownerWhere, " OR ")+")") } args = append(args, limit) query := `SELECT app_code, room_id, room_short_id, visible_region_id, owner_country_code, owner_user_id, title, cover_url, mode, status, locked, heat, online_count, seat_count, occupied_seat_count, sort_score, created_at_ms, updated_at_ms FROM room_list_entries WHERE ` + strings.Join(where, "\n AND ") + ` ORDER BY online_count ASC, updated_at_ms DESC, room_id DESC LIMIT ?` rows, err := r.db.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() items := make([]roomservice.RoomListEntry, 0, limit) for rows.Next() { var entry roomservice.RoomListEntry if err := rows.Scan( &entry.AppCode, &entry.RoomID, &entry.RoomShortID, &entry.VisibleRegionID, &entry.OwnerCountryCode, &entry.OwnerUserID, &entry.Title, &entry.CoverURL, &entry.Mode, &entry.Status, &entry.Locked, &entry.Heat, &entry.OnlineCount, &entry.SeatCount, &entry.OccupiedSeatCount, &entry.SortScore, &entry.CreatedAtMS, &entry.UpdatedAtMS, ); err != nil { return nil, err } items = append(items, entry) } return items, rows.Err() } type humanRoomRobotConfigScanner interface { Scan(dest ...any) error } func scanHumanRoomRobotConfig(row humanRoomRobotConfigScanner) (roomservice.HumanRoomRobotConfig, error) { var config roomservice.HumanRoomRobotConfig var enabled int var rawGifts string var rawLucky string var rawSuperLucky string var rawPools string var rawAllowedOwners string var countryLimitEnabled int var rawCountryRules string if err := row.Scan( &config.AppCode, &enabled, &config.CandidateRoomMaxOnline, &config.RoomFullStopOnline, &config.RoomTargetMinOnline, &config.RoomTargetMaxOnline, &config.RobotStayMinMS, &config.RobotStayMaxMS, &config.RobotReplaceMinMS, &config.RobotReplaceMaxMS, &config.NormalGiftIntervalMS, &config.NormalGiftIntervalMinMS, &config.NormalGiftIntervalMaxMS, &rawGifts, &rawLucky, &rawSuperLucky, &config.LuckyComboMin, &config.LuckyComboMax, &config.LuckyPauseMinMS, &config.LuckyPauseMaxMS, &config.MaxGiftSenders, &rawPools, &rawAllowedOwners, &countryLimitEnabled, &rawCountryRules, &config.UpdatedByAdminID, &config.CreatedAtMS, &config.UpdatedAtMS, ); err != nil { return roomservice.HumanRoomRobotConfig{}, err } config.Enabled = enabled == 1 if err := json.Unmarshal([]byte(rawGifts), &config.GiftIDs); err != nil { return roomservice.HumanRoomRobotConfig{}, err } if err := json.Unmarshal([]byte(rawLucky), &config.LuckyGiftIDs); err != nil { return roomservice.HumanRoomRobotConfig{}, err } if err := json.Unmarshal([]byte(rawSuperLucky), &config.SuperLuckyGiftIDs); err != nil { return roomservice.HumanRoomRobotConfig{}, err } if err := json.Unmarshal([]byte(rawPools), &config.CountryPools); err != nil { return roomservice.HumanRoomRobotConfig{}, err } if err := json.Unmarshal([]byte(rawAllowedOwners), &config.AllowedOwnerIDs); err != nil { return roomservice.HumanRoomRobotConfig{}, err } config.CountryLimitEnabled = countryLimitEnabled == 1 if err := json.Unmarshal([]byte(rawCountryRules), &config.CountryRules); err != nil { return roomservice.HumanRoomRobotConfig{}, err } return config, nil } func humanRoomRobotConfigJSON(config roomservice.HumanRoomRobotConfig) ([]byte, []byte, []byte, []byte, []byte, []byte, error) { rawGifts, err := json.Marshal(config.GiftIDs) if err != nil { return nil, nil, nil, nil, nil, nil, err } rawLucky, err := json.Marshal(config.LuckyGiftIDs) if err != nil { return nil, nil, nil, nil, nil, nil, err } rawSuperLucky, err := json.Marshal(config.SuperLuckyGiftIDs) if err != nil { return nil, nil, nil, nil, nil, nil, err } rawPools, err := json.Marshal(config.CountryPools) if err != nil { return nil, nil, nil, nil, nil, nil, err } rawAllowedOwners, err := json.Marshal(config.AllowedOwnerIDs) if err != nil { return nil, nil, nil, nil, nil, nil, err } rawCountryRules, err := json.Marshal(config.CountryRules) if err != nil { return nil, nil, nil, nil, nil, nil, err } return rawGifts, rawLucky, rawSuperLucky, rawPools, rawAllowedOwners, rawCountryRules, nil } func splitHumanRobotAllowedOwnerIDs(values []string) ([]string, []int64) { seenShortIDs := make(map[string]bool, len(values)) seenUserIDs := make(map[int64]bool, len(values)) shortIDs := make([]string, 0, len(values)) userIDs := make([]int64, 0, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value == "" || seenShortIDs[value] { continue } seenShortIDs[value] = true shortIDs = append(shortIDs, value) if strings.ContainsAny(value, ".eE") { continue } userID, err := strconv.ParseInt(value, 10, 64) if err != nil || userID <= 0 || seenUserIDs[userID] { continue } seenUserIDs[userID] = true userIDs = append(userIDs, userID) } return shortIDs, userIDs } type robotRoomScanner interface { Scan(dest ...any) error } func scanRobotRoomConfigs(rows *sql.Rows) ([]roomservice.RobotRoomConfig, error) { items := make([]roomservice.RobotRoomConfig, 0) for rows.Next() { item, err := scanRobotRoomConfig(rows) if err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } func scanRobotRoomConfig(row robotRoomScanner) (roomservice.RobotRoomConfig, error) { var item roomservice.RobotRoomConfig var rawRobots string var rawGifts string var rawLuckyGifts string if err := row.Scan( &item.AppCode, &item.RoomID, &item.RoomShortID, &item.Title, &item.CoverURL, &item.VisibleRegionID, &item.Status, &item.OwnerRobotUserID, &rawRobots, &item.ActiveRobotCount, &item.SeatCount, &rawGifts, &rawLuckyGifts, &item.GiftRule.NormalGiftIntervalMS, &item.GiftRule.LuckyComboMin, &item.GiftRule.LuckyComboMax, &item.GiftRule.LuckyPauseMinMS, &item.GiftRule.LuckyPauseMaxMS, &item.GiftRule.RobotStayMinMS, &item.GiftRule.RobotStayMaxMS, &item.GiftRule.RobotReplaceMinMS, &item.GiftRule.RobotReplaceMaxMS, &item.GiftRule.MaxGiftSenders, &item.CreatedByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS, ); err != nil { return roomservice.RobotRoomConfig{}, err } if err := json.Unmarshal([]byte(rawRobots), &item.RobotUserIDs); err != nil { return roomservice.RobotRoomConfig{}, err } if err := json.Unmarshal([]byte(rawGifts), &item.GiftRule.GiftIDs); err != nil { return roomservice.RobotRoomConfig{}, err } if err := json.Unmarshal([]byte(rawLuckyGifts), &item.GiftRule.LuckyGiftIDs); err != nil { return roomservice.RobotRoomConfig{}, err } if item.ActiveRobotCount <= 0 { item.ActiveRobotCount = int32(len(item.RobotUserIDs)) } if item.SeatCount <= 0 { item.SeatCount = 10 } item.GiftRule = roomservice.WithRobotRoomGiftRuleStorageDefaults(item.GiftRule) return item, nil } func robotRoomJSON(config roomservice.RobotRoomConfig) ([]byte, []byte, []byte, error) { rawRobots, err := json.Marshal(config.RobotUserIDs) if err != nil { return nil, nil, nil, err } rawGifts, err := json.Marshal(config.GiftRule.GiftIDs) if err != nil { return nil, nil, nil, err } rawLuckyGifts, err := json.Marshal(config.GiftRule.LuckyGiftIDs) if err != nil { return nil, nil, nil, err } return rawRobots, rawGifts, rawLuckyGifts, nil } func occupiedRobotUserIDsTx(ctx context.Context, tx *sql.Tx, appCode string, userIDs []int64) (map[int64]bool, error) { return occupiedRobotUserIDsQuery(ctx, tx, appCode, userIDs, true) } type dbQuerier interface { QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) } func occupiedRobotUserIDsQuery(ctx context.Context, db dbQuerier, appCode string, userIDs []int64, lock bool) (map[int64]bool, error) { ids := uniquePositiveInt64(userIDs) occupied := make(map[int64]bool, len(ids)) if len(ids) == 0 { return occupied, nil } query := `SELECT robot_user_ids_json FROM room_robot_rooms WHERE app_code = ? AND status = 'active'` if lock { query += ` FOR UPDATE` } rows, err := db.QueryContext(ctx, query, appcode.Normalize(appCode)) if err != nil { return nil, err } defer rows.Close() wanted := make(map[int64]bool, len(ids)) for _, id := range ids { wanted[id] = true } for rows.Next() { var raw string if err := rows.Scan(&raw); err != nil { return nil, err } var roomRobotIDs []int64 if err := json.Unmarshal([]byte(raw), &roomRobotIDs); err != nil { return nil, err } for _, id := range roomRobotIDs { if wanted[id] { occupied[id] = true } } } return occupied, rows.Err() }