""" 数据库迁移脚本:创建形象相关表,并为 user_ui_states 添加 current_avatar_id 运行: python migrate_add_avatars.py """ import asyncio import sys from typing import Any import aiomysql from config import settings DEFAULT_AVATAR_ID = "avatar_default" REGISTERED_AVATAR_ID = "avatar_registered_cap" # 默认形象 GIF 资源 DEFAULT_AVATAR_ASSETS = [ (DEFAULT_AVATAR_ID, "default", "https://assets.lyrria.com/01-Default.gif"), (DEFAULT_AVATAR_ID, "positive", "https://assets.lyrria.com/04-Positive-emotion.gif"), (DEFAULT_AVATAR_ID, "negative", "https://assets.lyrria.com/05-Negative-emotion.gif"), (DEFAULT_AVATAR_ID, "talking", "https://assets.lyrria.com/06-talking.gif"), ] # 注册赠送形象 GIF 资源 REGISTERED_AVATAR_ASSETS = [ ( REGISTERED_AVATAR_ID, "default", "https://assets.lyrria.com/01-Default-Registered.gif", ), ( REGISTERED_AVATAR_ID, "positive", "https://assets.lyrria.com/04-Positive-emotion-Registered.gif", ), ( REGISTERED_AVATAR_ID, "negative", "https://assets.lyrria.com/05-Negative-emotion-Registered.gif", ), ( REGISTERED_AVATAR_ID, "talking", "https://assets.lyrria.com/06-talking-Registered.gif", ), ] # 统一的字符序(用于跨表 join 避免字符集/排序规则不一致报错) COLLATE_NAME = "utf8mb4_unicode_ci" async def migrate_create_avatar_tables() -> bool: """创建形象相关表,并初始化默认数据""" try: conn: aiomysql.Connection = await aiomysql.connect( host=settings.MYSQL_HOST, port=settings.MYSQL_PORT, user=settings.MYSQL_USER, password=settings.MYSQL_PASSWORD, db=settings.MYSQL_DATABASE, charset="utf8mb4", ) async with conn.cursor() as cursor: # 1. avatars 表 await cursor.execute( """ SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %s AND table_name = 'avatars' """, (settings.MYSQL_DATABASE,), ) exists_row: Any = await cursor.fetchone() avatars_exists = bool(exists_row and exists_row[0] > 0) if not avatars_exists: print("🔄 创建 avatars 表...") await cursor.execute( """ CREATE TABLE avatars ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键', avatar_id VARCHAR(64) NOT NULL UNIQUE COMMENT '形象唯一标识', avatar_name VARCHAR(100) NOT NULL COMMENT '形象名称', description VARCHAR(255) NULL COMMENT '形象描述', is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用', metadata JSON NULL COMMENT '扩展配置', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', INDEX idx_avatar_is_active (is_active), INDEX idx_avatar_id (avatar_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='形象配置表'; """ ) print("✅ avatars 表创建完成") else: print("✅ avatars 表已存在") # 2. avatar_assets 表 await cursor.execute( """ SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %s AND table_name = 'avatar_assets' """, (settings.MYSQL_DATABASE,), ) exists_row = await cursor.fetchone() avatar_assets_exists = bool(exists_row and exists_row[0] > 0) if not avatar_assets_exists: print("🔄 创建 avatar_assets 表...") await cursor.execute( """ CREATE TABLE avatar_assets ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键', avatar_id VARCHAR(64) NOT NULL COMMENT '形象ID', state ENUM('default', 'positive', 'negative', 'talking', 'idle', 'sleep') NOT NULL DEFAULT 'default' COMMENT '动作/情绪状态', image_url VARCHAR(500) NOT NULL COMMENT 'GIF 资源 URL', thumbnail_url VARCHAR(500) NULL COMMENT '缩略图URL', metadata JSON NULL COMMENT '扩展配置', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', UNIQUE KEY uk_avatar_asset_state (avatar_id, state), INDEX idx_avatar_asset_avatar_id (avatar_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='形象动作资源表'; """ ) print("✅ avatar_assets 表创建完成") else: print("✅ avatar_assets 表已存在") # 3. user_avatars 表 await cursor.execute( """ SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %s AND table_name = 'user_avatars' """, (settings.MYSQL_DATABASE,), ) exists_row = await cursor.fetchone() user_avatars_exists = bool(exists_row and exists_row[0] > 0) if not user_avatars_exists: print("🔄 创建 user_avatars 表...") await cursor.execute( """ CREATE TABLE user_avatars ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键', user_id VARCHAR(64) NOT NULL COMMENT '用户ID', avatar_id VARCHAR(64) NOT NULL COMMENT '形象ID', obtained_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '获得时间', obtained_type ENUM('default', 'register', 'level_up', 'mission', 'purchase', 'gift', 'event') NOT NULL DEFAULT 'default' COMMENT '获得方式', obtained_source VARCHAR(64) NULL COMMENT '获得来源(等级/任务/订单等)', metadata JSON NULL COMMENT '扩展信息', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', UNIQUE KEY uk_user_avatar (user_id, avatar_id), INDEX idx_user_avatar_user_obtained (user_id, obtained_at), INDEX idx_user_avatar_avatar_id (avatar_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户已拥有的形象'; """ ) print("✅ user_avatars 表创建完成") else: print("✅ user_avatars 表已存在") # 4. user_ui_states 表添加 current_avatar_id await cursor.execute( """ SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = %s AND table_name = 'user_ui_states' AND column_name = 'current_avatar_id' """, (settings.MYSQL_DATABASE,), ) exists_row = await cursor.fetchone() column_exists = bool(exists_row and exists_row[0] > 0) if not column_exists: print("🔄 为 user_ui_states 添加 current_avatar_id 字段...") await cursor.execute( """ ALTER TABLE user_ui_states ADD COLUMN current_avatar_id VARCHAR(64) NULL COMMENT '当前使用的形象ID(关联 avatars.avatar_id)' AFTER has_shared_avatar """ ) print("✅ current_avatar_id 字段添加完成") else: print("✅ current_avatar_id 字段已存在") await conn.commit() # 5. 初始化 avatars 数据 print("🔄 初始化 avatars 基础数据...") await cursor.execute( """ INSERT IGNORE INTO avatars (avatar_id, avatar_name, description, is_active) VALUES (%s, %s, %s, 1), (%s, %s, %s, 1) """, ( DEFAULT_AVATAR_ID, "默认形象", "基础形象(未注册/通用)", REGISTERED_AVATAR_ID, "注册赠送形象", "注册奖励形象(带帽子)", ), ) await conn.commit() print("✅ avatars 基础数据初始化完成") # 6. 初始化 avatar_assets 数据 print("🔄 初始化 avatar_assets 数据...") asset_rows = DEFAULT_AVATAR_ASSETS + REGISTERED_AVATAR_ASSETS for avatar_id, state, image_url in asset_rows: await cursor.execute( """ INSERT IGNORE INTO avatar_assets (avatar_id, state, image_url) VALUES (%s, %s, %s) """, (avatar_id, state, image_url), ) await conn.commit() print(f"✅ 已初始化 {len(asset_rows)} 条 avatar_assets 数据") # 7. 为所有用户分配默认形象 print("🔄 为用户添加默认形象拥有记录...") await cursor.execute( """ INSERT IGNORE INTO user_avatars (user_id, avatar_id, obtained_type, obtained_source) SELECT user_id, %s, 'default', 'system_init' FROM users """, (DEFAULT_AVATAR_ID,), ) await conn.commit() print(f"✅ 默认形象写入 user_avatars,受影响行数: {cursor.rowcount}") # 8. 为注册用户分配注册赠送形象 print("🔄 为注册用户添加注册赠送形象...") await cursor.execute( """ INSERT IGNORE INTO user_avatars (user_id, avatar_id, obtained_type, obtained_source) SELECT user_id, %s, 'register', 'register_bonus' FROM users WHERE user_type = 'registered' """, (REGISTERED_AVATAR_ID,), ) await conn.commit() print(f"✅ 注册赠送形象写入 user_avatars,受影响行数: {cursor.rowcount}") # 9. 设置当前形象(注册用户优先注册赠送,其余默认) print("🔄 设置 user_ui_states.current_avatar_id ...") # 注意:部分历史表的字符集/排序规则不一致,使用显式 COLLATE 避免 1267 错误 await cursor.execute( f""" UPDATE user_ui_states ui JOIN users u ON ui.user_id COLLATE {COLLATE_NAME} = u.user_id COLLATE {COLLATE_NAME} SET ui.current_avatar_id = %s WHERE ui.current_avatar_id IS NULL AND u.user_type = 'registered' """, (REGISTERED_AVATAR_ID,), ) await conn.commit() await cursor.execute( f""" UPDATE user_ui_states SET current_avatar_id = %s WHERE current_avatar_id IS NULL """, (DEFAULT_AVATAR_ID,), ) await conn.commit() print("✅ current_avatar_id 初始化完成") conn.close() return True except Exception: import traceback traceback.print_exc() return False if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] == "--force": confirm = "y" else: confirm = input("\n是否继续执行迁移? (y/n): ") if confirm.lower() != "y": sys.exit(0) ok = asyncio.run(migrate_create_avatar_tables()) if not ok: sys.exit(1)