695 lines
30 KiB
Go
695 lines
30 KiB
Go
// 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_user_contribution_period_stats (
|
||
app_code VARCHAR(32) NOT NULL,
|
||
room_id VARCHAR(64) NOT NULL,
|
||
period VARCHAR(16) NOT NULL,
|
||
period_start_ms BIGINT NOT NULL,
|
||
period_end_ms BIGINT 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, period, period_start_ms, user_id),
|
||
KEY idx_room_contribution_period_rank (app_code, room_id, period, period_start_ms, 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_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
|
||
}
|
||
}
|
||
}
|
||
// 历史版本把 idx_room_outbox_retry 建成了和 idx_room_outbox_pending 完全相同的列组合,
|
||
// 每次写 outbox 都要多维护一份索引;没有任何查询引用它,存在即删除。
|
||
hasLegacyRetryIndex, err := r.indexExists(ctx, "room_outbox", "idx_room_outbox_retry")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if hasLegacyRetryIndex {
|
||
if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_outbox DROP INDEX idx_room_outbox_retry`); 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
|
||
}
|