2026-07-04 00:09:06 +08:00

671 lines
29 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package mysql 实现 room-service 的 MySQL 持久化边界。
package mysql
import "context"
// Migrate 创建 room-service 首版需要的 MySQL 表。
func (r *Repository) Migrate(ctx context.Context) error {
// 这里保持与 deploy/mysql/initdb 的表结构一致,服务本地启动可自动建表。
statements := []string{
`CREATE TABLE IF NOT EXISTS rooms (
app_code VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL,
room_short_id VARCHAR(32) NOT NULL DEFAULT '',
owner_user_id BIGINT NOT NULL,
seat_count INT NOT NULL,
mode VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
room_password_hash VARCHAR(128) NOT NULL DEFAULT '',
close_reason VARCHAR(512) NOT NULL DEFAULT '',
closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '',
closed_at_ms BIGINT NOT NULL DEFAULT 0,
visible_region_id BIGINT NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, room_id),
UNIQUE KEY uk_rooms_short_id (app_code, room_short_id),
UNIQUE KEY uk_rooms_owner_user (app_code, owner_user_id),
KEY idx_rooms_region_status (app_code, visible_region_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_list_entries (
app_code VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL,
room_short_id VARCHAR(32) NOT NULL DEFAULT '',
visible_region_id BIGINT NOT NULL DEFAULT 0,
owner_country_code VARCHAR(3) NOT NULL DEFAULT '',
owner_user_id BIGINT NOT NULL,
title VARCHAR(128) NOT NULL DEFAULT '',
cover_url VARCHAR(512) NOT NULL DEFAULT '',
mode VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
locked BOOLEAN NOT NULL DEFAULT FALSE,
close_reason VARCHAR(512) NOT NULL DEFAULT '',
closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '',
closed_at_ms BIGINT NOT NULL DEFAULT 0,
heat BIGINT NOT NULL DEFAULT 0,
weekly_contribution BIGINT NOT NULL DEFAULT 0,
contribution_week_start_ms BIGINT NOT NULL DEFAULT 0,
online_count INT NOT NULL DEFAULT 0,
seat_count INT NOT NULL DEFAULT 0,
occupied_seat_count INT NOT NULL DEFAULT 0,
sort_score BIGINT NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, room_id),
KEY idx_room_list_short_id (app_code, room_short_id),
KEY idx_room_list_region_hot (app_code, visible_region_id, status, sort_score DESC, room_id),
KEY idx_room_list_region_new (app_code, visible_region_id, status, created_at_ms DESC, room_id),
KEY idx_room_list_region_country_hot (app_code, visible_region_id, owner_country_code, status, sort_score DESC, room_id),
KEY idx_room_list_region_country_new (app_code, visible_region_id, owner_country_code, status, created_at_ms DESC, room_id),
KEY idx_room_list_weekly_contribution (app_code, status, visible_region_id, weekly_contribution DESC, created_at_ms DESC, room_id),
KEY idx_room_list_owner (app_code, owner_user_id, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_user_presence (
app_code VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
room_id VARCHAR(64) NOT NULL,
role VARCHAR(32) NOT NULL,
publish_state VARCHAR(32) NOT NULL DEFAULT '',
mic_session_id VARCHAR(128) NOT NULL DEFAULT '',
room_version BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
joined_at_ms BIGINT NOT NULL DEFAULT 0,
last_seen_at_ms BIGINT NOT NULL DEFAULT 0,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, user_id),
KEY idx_room_user_presence_room (app_code, room_id, status),
KEY idx_room_user_presence_updated (app_code, status, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_user_gift_stats (
app_code VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
gift_value BIGINT NOT NULL DEFAULT 0,
last_gift_at_ms BIGINT NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, room_id, user_id),
KEY idx_room_user_gift_stats_room_value (app_code, room_id, gift_value DESC, last_gift_at_ms DESC, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_background_images (
app_code VARCHAR(32) NOT NULL,
background_id BIGINT NOT NULL AUTO_INCREMENT,
room_id VARCHAR(64) NOT NULL,
image_url VARCHAR(256) NOT NULL,
created_by_user_id BIGINT NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (background_id),
UNIQUE KEY uk_room_background_url (app_code, room_id, image_url),
KEY idx_room_background_list (app_code, room_id, created_at_ms DESC, background_id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_snapshots (
app_code VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL,
room_version BIGINT NOT NULL,
payload LONGBLOB NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, room_id),
INDEX idx_room_snapshots_version (app_code, room_id, room_version)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_command_log (
app_code VARCHAR(32) NOT NULL,
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
room_id VARCHAR(64) NOT NULL,
room_version BIGINT NOT NULL,
command_id VARCHAR(128) NOT NULL,
command_type VARCHAR(64) NOT NULL,
owner_node_id VARCHAR(128) NOT NULL DEFAULT '',
lease_token VARCHAR(128) NOT NULL DEFAULT '',
payload LONGBLOB NOT NULL,
replayable BOOLEAN NOT NULL,
created_at_ms BIGINT NOT NULL,
UNIQUE KEY uk_room_command (app_code, room_id, command_id),
KEY idx_room_command_replay (app_code, room_id, room_version)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_outbox (
app_code VARCHAR(32) NOT NULL,
event_id VARCHAR(128) NOT NULL,
event_type VARCHAR(64) NOT NULL,
room_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
worker_id VARCHAR(128) NOT NULL DEFAULT '',
lock_until_ms BIGINT NULL,
envelope LONGBLOB NOT NULL,
created_at_ms BIGINT NOT NULL,
retry_count INT NOT NULL DEFAULT 0,
next_retry_at_ms BIGINT NOT NULL DEFAULT 0,
last_error TEXT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, event_id),
KEY idx_room_outbox_pending (app_code, status, next_retry_at_ms, created_at_ms, event_id),
KEY idx_room_outbox_claim (app_code, status, lock_until_ms, created_at_ms, event_id),
KEY idx_room_outbox_event_created (app_code, event_type, created_at_ms, event_id),
KEY idx_room_outbox_retention (app_code, status, updated_at_ms, event_id),
KEY idx_room_outbox_room_event (app_code, room_id, event_type, created_at_ms, event_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_user_feed_entries (
app_code VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
feed_type VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, user_id, feed_type, room_id),
KEY idx_room_user_feed_list (app_code, user_id, feed_type, updated_at_ms DESC, room_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_follows (
app_code VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
room_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
followed_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, user_id, room_id),
KEY idx_room_follows_user_list (app_code, user_id, status, followed_at_ms DESC, room_id),
KEY idx_room_follows_room (app_code, room_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_region_pins (
id BIGINT NOT NULL AUTO_INCREMENT,
app_code VARCHAR(32) NOT NULL,
visible_region_id BIGINT NOT NULL,
pin_type VARCHAR(32) NOT NULL DEFAULT 'region',
room_id VARCHAR(64) NOT NULL,
weight BIGINT NOT NULL DEFAULT 0,
status VARCHAR(32) NOT NULL,
pinned_at_ms BIGINT NOT NULL,
expires_at_ms BIGINT NOT NULL,
cancelled_at_ms BIGINT NOT NULL DEFAULT 0,
created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
cancelled_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_room_region_pins_scope (app_code, pin_type, visible_region_id, room_id),
KEY idx_room_region_pins_active_scope (app_code, pin_type, visible_region_id, status, pinned_at_ms, expires_at_ms, weight DESC, room_id),
KEY idx_room_region_pins_room (app_code, room_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间置顶表'`,
`CREATE TABLE IF NOT EXISTS room_seat_configs (
app_code VARCHAR(32) NOT NULL,
allowed_seat_counts VARCHAR(128) NOT NULL,
default_seat_count INT NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_rocket_configs (
app_code VARCHAR(32) NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 0,
config_version BIGINT NOT NULL DEFAULT 1,
fuel_source VARCHAR(32) NOT NULL,
launch_delay_ms BIGINT NOT NULL,
broadcast_enabled TINYINT(1) NOT NULL DEFAULT 1,
broadcast_scope VARCHAR(32) NOT NULL,
broadcast_delay_ms BIGINT NOT NULL DEFAULT 0,
reward_stack_policy VARCHAR(32) NOT NULL,
levels_json JSON NOT NULL,
gift_fuel_rules_json JSON NOT NULL,
updated_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code),
KEY idx_room_rocket_enabled (app_code, enabled)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_robot_rooms (
app_code VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL,
room_short_id VARCHAR(32) NOT NULL DEFAULT '',
title VARCHAR(128) NOT NULL DEFAULT '',
cover_url VARCHAR(512) NOT NULL DEFAULT '',
visible_region_id BIGINT NOT NULL DEFAULT 0,
status VARCHAR(32) NOT NULL,
owner_robot_user_id BIGINT NOT NULL,
robot_user_ids_json JSON NOT NULL,
active_robot_count INT NOT NULL DEFAULT 0,
seat_count INT NOT NULL DEFAULT 10,
gift_ids_json JSON NOT NULL,
lucky_gift_ids_json JSON NOT NULL,
normal_gift_interval_ms BIGINT NOT NULL,
lucky_combo_min BIGINT NOT NULL,
lucky_combo_max BIGINT NOT NULL,
lucky_pause_min_ms BIGINT NOT NULL,
lucky_pause_max_ms BIGINT NOT NULL,
robot_stay_min_ms BIGINT NOT NULL DEFAULT 180000,
robot_stay_max_ms BIGINT NOT NULL DEFAULT 600000,
robot_replace_min_ms BIGINT NOT NULL DEFAULT 0,
robot_replace_max_ms BIGINT NOT NULL DEFAULT 60000,
max_gift_senders BIGINT NOT NULL DEFAULT 1,
created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, room_id),
KEY idx_room_robot_rooms_status (app_code, status, updated_at_ms, room_id),
KEY idx_room_robot_rooms_owner (app_code, owner_robot_user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_human_robot_configs (
app_code VARCHAR(32) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT FALSE,
candidate_room_max_online INT NOT NULL DEFAULT 5,
room_full_stop_online INT NOT NULL DEFAULT 10,
room_target_min_online INT NOT NULL DEFAULT 8,
room_target_max_online INT NOT NULL DEFAULT 10,
robot_stay_min_ms BIGINT NOT NULL DEFAULT 60000,
robot_stay_max_ms BIGINT NOT NULL DEFAULT 600000,
robot_replace_min_ms BIGINT NOT NULL DEFAULT 60000,
robot_replace_max_ms BIGINT NOT NULL DEFAULT 180000,
normal_gift_interval_ms BIGINT NOT NULL DEFAULT 10000,
normal_gift_interval_min_ms BIGINT NOT NULL DEFAULT 1000,
normal_gift_interval_max_ms BIGINT NOT NULL DEFAULT 20000,
gift_ids_json JSON NOT NULL,
lucky_gift_ids_json JSON NOT NULL,
super_lucky_gift_ids_json JSON NOT NULL,
lucky_combo_min BIGINT NOT NULL DEFAULT 1,
lucky_combo_max BIGINT NOT NULL DEFAULT 3,
lucky_pause_min_ms BIGINT NOT NULL DEFAULT 5000,
lucky_pause_max_ms BIGINT NOT NULL DEFAULT 20000,
max_gift_senders BIGINT NOT NULL DEFAULT 1,
country_pools_json JSON NOT NULL,
allowed_owner_ids_json JSON NOT NULL,
country_limit_enabled BOOLEAN NOT NULL DEFAULT FALSE,
country_rules_json JSON NOT NULL,
updated_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code),
KEY idx_room_human_robot_enabled (app_code, enabled)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`INSERT IGNORE INTO room_seat_configs (app_code, allowed_seat_counts, default_seat_count, created_at_ms, updated_at_ms)
VALUES ('lalu', '[10,15,20,25,30]', 15, 0, 0)`,
`INSERT IGNORE INTO room_rocket_configs (
app_code,
enabled,
config_version,
fuel_source,
launch_delay_ms,
broadcast_enabled,
broadcast_scope,
broadcast_delay_ms,
reward_stack_policy,
levels_json,
gift_fuel_rules_json,
updated_by_admin_id,
created_at_ms,
updated_at_ms
) VALUES (
'lalu',
0,
1,
'heat_value',
30000,
1,
'region',
0,
'allow_stack',
JSON_ARRAY(
JSON_OBJECT('level', 1, 'fuelThreshold', 1000, 'coverUrl', '', 'animationUrl', '', 'launchAnimationUrl', '', 'launchedImageUrl', '', 'inRoomRewards', JSON_ARRAY(), 'top1Rewards', JSON_ARRAY(), 'igniterRewards', JSON_ARRAY()),
JSON_OBJECT('level', 2, 'fuelThreshold', 3000, 'coverUrl', '', 'animationUrl', '', 'launchAnimationUrl', '', 'launchedImageUrl', '', 'inRoomRewards', JSON_ARRAY(), 'top1Rewards', JSON_ARRAY(), 'igniterRewards', JSON_ARRAY()),
JSON_OBJECT('level', 3, 'fuelThreshold', 6000, 'coverUrl', '', 'animationUrl', '', 'launchAnimationUrl', '', 'launchedImageUrl', '', 'inRoomRewards', JSON_ARRAY(), 'top1Rewards', JSON_ARRAY(), 'igniterRewards', JSON_ARRAY()),
JSON_OBJECT('level', 4, 'fuelThreshold', 10000, 'coverUrl', '', 'animationUrl', '', 'launchAnimationUrl', '', 'launchedImageUrl', '', 'inRoomRewards', JSON_ARRAY(), 'top1Rewards', JSON_ARRAY(), 'igniterRewards', JSON_ARRAY()),
JSON_OBJECT('level', 5, 'fuelThreshold', 15000, 'coverUrl', '', 'animationUrl', '', 'launchAnimationUrl', '', 'launchedImageUrl', '', 'inRoomRewards', JSON_ARRAY(), 'top1Rewards', JSON_ARRAY(), 'igniterRewards', JSON_ARRAY())
),
JSON_ARRAY(),
0,
0,
0
)`,
`ALTER TABLE rooms MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_list_entries MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_user_presence MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_snapshots MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_command_log MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_outbox MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_user_feed_entries MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_follows MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_region_pins MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_seat_configs MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_rocket_configs MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_robot_rooms MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
}
for _, statement := range statements {
// 每条 DDL 独立执行,失败时返回具体 SQL 错误给启动流程。
if _, err := r.db.ExecContext(ctx, statement); err != nil {
return err
}
}
if err := r.ensureRoomListLockSchema(ctx); err != nil {
return err
}
if err := r.ensureRoomListCountrySchema(ctx); err != nil {
return err
}
if err := r.ensureRoomPasswordSchema(ctx); err != nil {
return err
}
if err := r.ensureRoomAdminCloseSchema(ctx); err != nil {
return err
}
if err := r.ensureRoomWeeklyContributionSchema(ctx); err != nil {
return err
}
if err := r.ensureRoomUserPresenceSchema(ctx); err != nil {
return err
}
if err := r.ensureRoomPinSchema(ctx); err != nil {
return err
}
if err := r.ensureRobotRoomBehaviorSchema(ctx); err != nil {
return err
}
if err := r.ensureHumanRobotConfigBehaviorSchema(ctx); err != nil {
return err
}
return r.ensureOutboxRetrySchema(ctx)
}
func (r *Repository) ensureHumanRobotConfigBehaviorSchema(ctx context.Context) error {
columns := []struct {
name string
statement string
}{
{name: "normal_gift_interval_min_ms", statement: `ALTER TABLE room_human_robot_configs ADD COLUMN normal_gift_interval_min_ms BIGINT NOT NULL DEFAULT 1000 AFTER normal_gift_interval_ms`},
{name: "normal_gift_interval_max_ms", statement: `ALTER TABLE room_human_robot_configs ADD COLUMN normal_gift_interval_max_ms BIGINT NOT NULL DEFAULT 20000 AFTER normal_gift_interval_min_ms`},
{name: "room_target_min_online", statement: `ALTER TABLE room_human_robot_configs ADD COLUMN room_target_min_online INT NOT NULL DEFAULT 8 AFTER room_full_stop_online`},
{name: "room_target_max_online", statement: `ALTER TABLE room_human_robot_configs ADD COLUMN room_target_max_online INT NOT NULL DEFAULT 10 AFTER room_target_min_online`},
{name: "allowed_owner_ids_json", statement: `ALTER TABLE room_human_robot_configs ADD COLUMN allowed_owner_ids_json JSON NULL AFTER country_pools_json`},
{name: "country_limit_enabled", statement: `ALTER TABLE room_human_robot_configs ADD COLUMN country_limit_enabled BOOLEAN NOT NULL DEFAULT FALSE AFTER allowed_owner_ids_json`},
{name: "country_rules_json", statement: `ALTER TABLE room_human_robot_configs ADD COLUMN country_rules_json JSON NULL AFTER country_limit_enabled`},
}
for _, column := range columns {
exists, err := r.columnExists(ctx, "room_human_robot_configs", column.name)
if err != nil {
return err
}
if exists {
continue
}
if _, err := r.db.ExecContext(ctx, column.statement); err != nil {
return err
}
}
return nil
}
func (r *Repository) ensureRobotRoomBehaviorSchema(ctx context.Context) error {
columns := []struct {
name string
statement string
}{
{name: "active_robot_count", statement: `ALTER TABLE room_robot_rooms ADD COLUMN active_robot_count INT NOT NULL DEFAULT 0 AFTER robot_user_ids_json`},
{name: "seat_count", statement: `ALTER TABLE room_robot_rooms ADD COLUMN seat_count INT NOT NULL DEFAULT 10 AFTER active_robot_count`},
{name: "robot_stay_min_ms", statement: `ALTER TABLE room_robot_rooms ADD COLUMN robot_stay_min_ms BIGINT NOT NULL DEFAULT 180000 AFTER lucky_pause_max_ms`},
{name: "robot_stay_max_ms", statement: `ALTER TABLE room_robot_rooms ADD COLUMN robot_stay_max_ms BIGINT NOT NULL DEFAULT 600000 AFTER robot_stay_min_ms`},
{name: "robot_replace_min_ms", statement: `ALTER TABLE room_robot_rooms ADD COLUMN robot_replace_min_ms BIGINT NOT NULL DEFAULT 0 AFTER robot_stay_max_ms`},
{name: "robot_replace_max_ms", statement: `ALTER TABLE room_robot_rooms ADD COLUMN robot_replace_max_ms BIGINT NOT NULL DEFAULT 60000 AFTER robot_replace_min_ms`},
{name: "max_gift_senders", statement: `ALTER TABLE room_robot_rooms ADD COLUMN max_gift_senders BIGINT NOT NULL DEFAULT 1 AFTER robot_replace_max_ms`},
}
for _, column := range columns {
exists, err := r.columnExists(ctx, "room_robot_rooms", column.name)
if err != nil {
return err
}
if exists {
continue
}
if _, err := r.db.ExecContext(ctx, column.statement); err != nil {
return err
}
}
return nil
}
func (r *Repository) ensureRoomWeeklyContributionSchema(ctx context.Context) error {
columns := []struct {
column string
statement string
}{
{
column: "weekly_contribution",
statement: `ALTER TABLE room_list_entries ADD COLUMN weekly_contribution BIGINT NOT NULL DEFAULT 0 AFTER heat`,
},
{
column: "contribution_week_start_ms",
statement: `ALTER TABLE room_list_entries ADD COLUMN contribution_week_start_ms BIGINT NOT NULL DEFAULT 0 AFTER weekly_contribution`,
},
}
for _, item := range columns {
hasColumn, err := r.columnExists(ctx, "room_list_entries", item.column)
if err != nil {
return err
}
if !hasColumn {
if _, err := r.db.ExecContext(ctx, item.statement); err != nil {
return err
}
}
}
hasIndex, err := r.indexExists(ctx, "room_list_entries", "idx_room_list_weekly_contribution")
if err != nil {
return err
}
if !hasIndex {
// 后台按房间贡献排序时只读当前 UTC 周贡献;索引用于收敛 active/region 过滤后的排序成本。
if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_list_entries ADD INDEX idx_room_list_weekly_contribution (app_code, status, visible_region_id, weekly_contribution DESC, created_at_ms DESC, room_id)`); err != nil {
return err
}
}
return nil
}
func (r *Repository) ensureRoomPinSchema(ctx context.Context) error {
hasColumn, err := r.columnExists(ctx, "room_region_pins", "pin_type")
if err != nil {
return err
}
if !hasColumn {
// pin_type 把“全区置顶”和“区域置顶”的作用域写进同一 owner 表;历史数据默认都是区域置顶。
if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_region_pins ADD COLUMN pin_type VARCHAR(32) NOT NULL DEFAULT 'region' AFTER visible_region_id`); err != nil {
return err
}
}
hasScopeUnique, err := r.indexExists(ctx, "room_region_pins", "uk_room_region_pins_scope")
if err != nil {
return err
}
if !hasScopeUnique {
if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_region_pins ADD UNIQUE KEY uk_room_region_pins_scope (app_code, pin_type, visible_region_id, room_id)`); err != nil {
return err
}
}
hasLegacyUnique, err := r.indexExists(ctx, "room_region_pins", "uk_room_region_pins_room")
if err != nil {
return err
}
if hasLegacyUnique {
// 新唯一键允许 region_id=0 的区域置顶和全区置顶并存;旧唯一键会错误地把两者互斥。
if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_region_pins DROP INDEX uk_room_region_pins_room`); err != nil {
return err
}
}
hasActiveScope, err := r.indexExists(ctx, "room_region_pins", "idx_room_region_pins_active_scope")
if err != nil {
return err
}
if !hasActiveScope {
if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_region_pins ADD INDEX idx_room_region_pins_active_scope (app_code, pin_type, visible_region_id, status, pinned_at_ms, expires_at_ms, weight DESC, room_id)`); err != nil {
return err
}
}
return nil
}
func (r *Repository) ensureRoomUserPresenceSchema(ctx context.Context) error {
hasColumn, err := r.columnExists(ctx, "room_user_presence", "joined_at_ms")
if err != nil {
return err
}
if !hasColumn {
if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_user_presence ADD COLUMN joined_at_ms BIGINT NOT NULL DEFAULT 0 AFTER status`); err != nil {
return err
}
}
return nil
}
func (r *Repository) ensureRoomPasswordSchema(ctx context.Context) error {
hasColumn, err := r.columnExists(ctx, "rooms", "room_password_hash")
if err != nil {
return err
}
if !hasColumn {
// rooms 是最新快照恢复密码哈希的持久来源;列表只使用 locked 布尔投影。
if _, err := r.db.ExecContext(ctx, `ALTER TABLE rooms ADD COLUMN room_password_hash VARCHAR(128) NOT NULL DEFAULT '' AFTER status`); err != nil {
return err
}
}
return nil
}
func (r *Repository) ensureRoomListLockSchema(ctx context.Context) error {
hasColumn, err := r.columnExists(ctx, "room_list_entries", "locked")
if err != nil {
return err
}
if !hasColumn {
// locked 是列表/搜索需要的纯投影字段,历史行默认 false后续房间状态变更会刷新为真实值。
if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_list_entries ADD COLUMN locked BOOLEAN NOT NULL DEFAULT FALSE AFTER status`); err != nil {
return err
}
}
return nil
}
func (r *Repository) ensureRoomListCountrySchema(ctx context.Context) error {
hasColumn, err := r.columnExists(ctx, "room_list_entries", "owner_country_code")
if err != nil {
return err
}
if !hasColumn {
// owner_country_code 是房主国家快照,只服务发现页国家筛选;历史行先为空,后续房间投影刷新会补真实值。
if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_list_entries ADD COLUMN owner_country_code VARCHAR(3) NOT NULL DEFAULT '' AFTER visible_region_id`); err != nil {
return err
}
}
indexes := []struct {
name string
statement string
}{
{
name: "idx_room_list_region_country_hot",
statement: `ALTER TABLE room_list_entries ADD INDEX idx_room_list_region_country_hot (app_code, visible_region_id, owner_country_code, status, sort_score DESC, room_id)`,
},
{
name: "idx_room_list_region_country_new",
statement: `ALTER TABLE room_list_entries ADD INDEX idx_room_list_region_country_new (app_code, visible_region_id, owner_country_code, status, created_at_ms DESC, room_id)`,
},
}
for _, item := range indexes {
hasIndex, err := r.indexExists(ctx, "room_list_entries", item.name)
if err != nil {
return err
}
if !hasIndex {
if _, err := r.db.ExecContext(ctx, item.statement); err != nil {
return err
}
}
}
return nil
}
func (r *Repository) ensureRoomAdminCloseSchema(ctx context.Context) error {
columns := []struct {
table string
column string
statement string
}{
{"rooms", "close_reason", `ALTER TABLE rooms ADD COLUMN close_reason VARCHAR(512) NOT NULL DEFAULT '' AFTER room_password_hash`},
{"rooms", "closed_by_admin_id", `ALTER TABLE rooms ADD COLUMN closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER close_reason`},
{"rooms", "closed_by_admin_name", `ALTER TABLE rooms ADD COLUMN closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '' AFTER closed_by_admin_id`},
{"rooms", "closed_at_ms", `ALTER TABLE rooms ADD COLUMN closed_at_ms BIGINT NOT NULL DEFAULT 0 AFTER closed_by_admin_name`},
{"room_list_entries", "close_reason", `ALTER TABLE room_list_entries ADD COLUMN close_reason VARCHAR(512) NOT NULL DEFAULT '' AFTER locked`},
{"room_list_entries", "closed_by_admin_id", `ALTER TABLE room_list_entries ADD COLUMN closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER close_reason`},
{"room_list_entries", "closed_by_admin_name", `ALTER TABLE room_list_entries ADD COLUMN closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '' AFTER closed_by_admin_id`},
{"room_list_entries", "closed_at_ms", `ALTER TABLE room_list_entries ADD COLUMN closed_at_ms BIGINT NOT NULL DEFAULT 0 AFTER closed_by_admin_name`},
}
for _, item := range columns {
hasColumn, err := r.columnExists(ctx, item.table, item.column)
if err != nil {
return err
}
if !hasColumn {
if _, err := r.db.ExecContext(ctx, item.statement); err != nil {
return err
}
}
}
return nil
}
func (r *Repository) ensureOutboxRetrySchema(ctx context.Context) error {
hasColumn, err := r.columnExists(ctx, "room_outbox", "next_retry_at_ms")
if err != nil {
return err
}
if !hasColumn {
if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_outbox ADD COLUMN next_retry_at_ms BIGINT NOT NULL DEFAULT 0 AFTER retry_count`); err != nil {
return err
}
}
indexes := []struct {
name string
statement string
}{
{"idx_room_outbox_pending", `ALTER TABLE room_outbox ADD INDEX idx_room_outbox_pending (app_code, status, next_retry_at_ms, created_at_ms, event_id)`},
{"idx_room_outbox_retry", `ALTER TABLE room_outbox ADD INDEX idx_room_outbox_retry (app_code, status, next_retry_at_ms, created_at_ms, event_id)`},
{"idx_room_outbox_claim", `ALTER TABLE room_outbox ADD INDEX idx_room_outbox_claim (app_code, status, lock_until_ms, created_at_ms, event_id)`},
{"idx_room_outbox_event_created", `ALTER TABLE room_outbox ADD INDEX idx_room_outbox_event_created (app_code, event_type, created_at_ms, event_id)`},
{"idx_room_outbox_retention", `ALTER TABLE room_outbox ADD INDEX idx_room_outbox_retention (app_code, status, updated_at_ms, event_id)`},
// 房间维度访问路径deleteRoomRows 的按房清理 DELETE 和按房排查事件的诊断查询都依赖它;
// 没有这个索引时两者都要扫整个 app 分片(线上 百万级 行)。
{"idx_room_outbox_room_event", `ALTER TABLE room_outbox ADD INDEX idx_room_outbox_room_event (app_code, room_id, event_type, created_at_ms, event_id)`},
}
for _, index := range indexes {
hasIndex, err := r.indexExists(ctx, "room_outbox", index.name)
if err != nil {
return err
}
if !hasIndex {
if _, err := r.db.ExecContext(ctx, index.statement); err != nil {
return err
}
}
}
return nil
}
func (r *Repository) columnExists(ctx context.Context, tableName string, columnName string) (bool, error) {
var exists int
err := r.db.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?
`, tableName, columnName).Scan(&exists)
return exists > 0, err
}
func (r *Repository) indexExists(ctx context.Context, tableName string, indexName string) (bool, error) {
var exists int
err := r.db.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?
`, tableName, indexName).Scan(&exists)
return exists > 0, err
}