zlwu 11 hónapja
szülő
commit
3efad955cd

+ 226 - 0
backend/migrate_add_background_images.py

@@ -0,0 +1,226 @@
+"""
+数据库迁移脚本:创建背景图片相关表并初始化数据
+运行: python migrate_add_background_images.py
+"""
+import asyncio
+import sys
+from typing import Any
+
+import aiomysql
+
+from config import settings
+
+
+async def migrate_create_background_images_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. 检查并创建 background_images 表
+            await cursor.execute(
+                """
+                SELECT COUNT(*) FROM information_schema.tables
+                WHERE table_schema = %s AND table_name = 'background_images'
+                """,
+                (settings.MYSQL_DATABASE,),
+            )
+            exists_row: Any = await cursor.fetchone()
+            bg_images_exists = bool(exists_row and exists_row[0] > 0)
+
+            if not bg_images_exists:
+                print("🔄 创建 background_images 表...")
+                await cursor.execute(
+                    """
+                    CREATE TABLE background_images (
+                        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
+                        image_id VARCHAR(64) NOT NULL UNIQUE COMMENT '背景图片唯一标识',
+                        image_name VARCHAR(100) NOT NULL COMMENT '背景图片名称',
+                        image_url VARCHAR(500) NOT NULL COMMENT '背景图片URL',
+                        thumbnail_url VARCHAR(500) NULL COMMENT '缩略图URL',
+                        unlock_type ENUM('default', 'level', 'purchase', 'mission') NOT NULL DEFAULT 'default' COMMENT '解锁类型',
+                        unlock_level INT NULL COMMENT '解锁所需等级(unlock_type=level时)',
+                        unlock_mission_type VARCHAR(64) NULL COMMENT '解锁所需任务类型(unlock_type=mission时)',
+                        display_order INT NOT NULL DEFAULT 0 COMMENT '显示顺序',
+                        is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
+                        description VARCHAR(255) NULL 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_image_id (image_id),
+                        INDEX idx_unlock_type (unlock_type),
+                        INDEX idx_display_order (display_order)
+                    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='背景图片配置表';
+                    """
+                )
+                print("✅ background_images 表创建完成")
+            else:
+                print("✅ background_images 表已存在")
+
+            # 2. 检查并创建 user_background_images 表
+            await cursor.execute(
+                """
+                SELECT COUNT(*) FROM information_schema.tables
+                WHERE table_schema = %s AND table_name = 'user_background_images'
+                """,
+                (settings.MYSQL_DATABASE,),
+            )
+            exists_row = await cursor.fetchone()
+            user_bg_images_exists = bool(exists_row and exists_row[0] > 0)
+
+            if not user_bg_images_exists:
+                print("🔄 创建 user_background_images 表...")
+                await cursor.execute(
+                    """
+                    CREATE TABLE user_background_images (
+                        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
+                        user_id VARCHAR(64) NOT NULL COMMENT '用户ID',
+                        image_id VARCHAR(64) NOT NULL COMMENT '背景图片ID',
+                        obtained_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '获得时间',
+                        obtained_type ENUM('default', 'level_up', 'purchase', 'mission', 'gift') 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_background_image (user_id, image_id),
+                        INDEX idx_user_id (user_id),
+                        INDEX idx_image_id (image_id),
+                        INDEX idx_user_obtained (user_id, obtained_at)
+                    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户背景图片关联表';
+                    """
+                )
+                print("✅ user_background_images 表创建完成")
+            else:
+                print("✅ user_background_images 表已存在")
+
+            # 3. 检查并添加 users 表的 current_background_image_id 字段
+            await cursor.execute(
+                """
+                SELECT COUNT(*) FROM information_schema.columns
+                WHERE table_schema = %s 
+                  AND table_name = 'users'
+                  AND column_name = 'current_background_image_id'
+                """,
+                (settings.MYSQL_DATABASE,),
+            )
+            exists_row = await cursor.fetchone()
+            column_exists = bool(exists_row and exists_row[0] > 0)
+
+            if not column_exists:
+                print("🔄 为 users 表添加 current_background_image_id 字段...")
+                await cursor.execute(
+                    """
+                    ALTER TABLE users
+                    ADD COLUMN current_background_image_id VARCHAR(64) NULL COMMENT '当前使用的背景图片ID'
+                    AFTER last_active_at
+                    """
+                )
+                print("✅ current_background_image_id 字段添加完成")
+            else:
+                print("✅ current_background_image_id 字段已存在")
+
+            await conn.commit()
+
+            # 4. 初始化背景图片数据
+            print("🔄 初始化背景图片数据...")
+            
+            # 检查默认背景图片是否存在
+            await cursor.execute(
+                "SELECT COUNT(*) FROM background_images WHERE image_id = 'default'"
+            )
+            default_exists = (await cursor.fetchone())[0] > 0
+            
+            if not default_exists:
+                await cursor.execute(
+                    """
+                    INSERT INTO background_images 
+                    (image_id, image_name, image_url, unlock_type, display_order, description)
+                    VALUES 
+                    ('default', '默认背景', 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/init.jpg', 'default', 1, '默认背景图片')
+                    """
+                )
+                print("✅ 默认背景图片已添加")
+            else:
+                print("✅ 默认背景图片已存在")
+
+            # 检查新年背景图片是否存在
+            await cursor.execute(
+                "SELECT COUNT(*) FROM background_images WHERE image_id = 'happynewyear'"
+            )
+            newyear_exists = (await cursor.fetchone())[0] > 0
+            
+            if not newyear_exists:
+                await cursor.execute(
+                    """
+                    INSERT INTO background_images 
+                    (image_id, image_name, image_url, unlock_type, display_order, description)
+                    VALUES 
+                    ('happynewyear', '新年背景', 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/happynewyear.jpg', 'default', 2, '新年背景图片')
+                    """
+                )
+                print("✅ 新年背景图片已添加")
+            else:
+                print("✅ 新年背景图片已存在")
+
+            await conn.commit()
+
+            # 5. 为所有现有用户设置默认背景图片
+            print("🔄 为现有用户设置默认背景图片...")
+            
+            # 更新所有用户的 current_background_image_id 为 'default'(如果为 NULL)
+            await cursor.execute(
+                """
+                UPDATE users 
+                SET current_background_image_id = 'default'
+                WHERE current_background_image_id IS NULL
+                """
+            )
+            updated_users = cursor.rowcount
+            print(f"✅ 已为 {updated_users} 个用户设置默认背景图片")
+
+            # 6. 为所有现有用户添加默认背景图片到已获得列表
+            print("🔄 为现有用户添加默认背景图片到已获得列表...")
+            
+            await cursor.execute(
+                """
+                INSERT IGNORE INTO user_background_images 
+                (user_id, image_id, obtained_type, obtained_source)
+                SELECT user_id, 'default', 'default', 'system_init'
+                FROM users
+                """
+            )
+            added_records = cursor.rowcount
+            print(f"✅ 已为 {added_records} 个用户添加默认背景图片到已获得列表")
+
+            await conn.commit()
+            print("✅ 迁移完成")
+
+        await conn.ensure_closed()
+        return True
+    except Exception as exc:  # noqa: BLE001
+        import traceback
+
+        print(f"❌ 迁移失败: {exc}")
+        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_background_images_tables())
+    if not ok:
+        sys.exit(1)
+

+ 97 - 1
backend/models_db.py

@@ -55,6 +55,9 @@ class User(Base):
     user_level = Column(Integer, default=0, nullable=False, comment="用户等级(0-5)")
     last_active_at = Column(TIMESTAMP, nullable=True, comment="最后活跃时间")
     
+    # 背景图片
+    current_background_image_id = Column(String(64), nullable=True, comment="当前使用的背景图片ID")
+    
     created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
     updated_at = Column(TIMESTAMP, server_default=func.current_timestamp(), onupdate=func.current_timestamp(), comment="更新时间")
     
@@ -78,6 +81,7 @@ class User(Base):
             "total_speech_duration_ms": self.total_speech_duration_ms,
             "user_level": self.user_level,
             "last_active_at": self.last_active_at.isoformat() if self.last_active_at else None,
+            "current_background_image_id": self.current_background_image_id,
             "created_at": self.created_at.isoformat() if self.created_at else None,
         }
 
@@ -319,6 +323,98 @@ def _attach_metadata_alias(model_cls):
     model_cls.metadata = property(getter, setter)
 
 
-for _model in (MissionType, MissionEnergyStone, ConversationEnergyStone):
+class BackgroundImage(Base):
+    """背景图片配置表"""
+    __tablename__ = "background_images"
+    
+    id = Column(PKBigInt, primary_key=True, autoincrement=True)
+    image_id = Column(String(64), unique=True, nullable=False, index=True, comment="背景图片唯一标识")
+    
+    # 基本信息
+    image_name = Column(String(100), nullable=False, comment="背景图片名称")
+    image_url = Column(String(500), nullable=False, comment="背景图片URL")
+    thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL")
+    
+    # 解锁条件
+    unlock_type = Column(Enum("default", "level", "purchase", "mission", name="unlock_type"), 
+                        default="default", comment="解锁类型")
+    unlock_level = Column(Integer, nullable=True, comment="解锁所需等级(unlock_type=level时)")
+    unlock_mission_type = Column(String(64), nullable=True, comment="解锁所需任务类型(unlock_type=mission时)")
+    
+    # 显示信息
+    display_order = Column(Integer, default=0, comment="显示顺序")
+    is_active = Column(Boolean, default=True, comment="是否启用")
+    description = Column(String(255), nullable=True, comment="背景图片描述")
+    
+    # 扩展信息
+    metadata_json = Column("metadata", JSON, nullable=True, comment="扩展配置")
+    
+    created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
+    updated_at = Column(TIMESTAMP, server_default=func.current_timestamp(), 
+                       onupdate=func.current_timestamp(), comment="更新时间")
+    
+    __table_args__ = (
+        Index("idx_unlock_type", "unlock_type"),
+        Index("idx_display_order", "display_order"),
+    )
+    
+    def to_dict(self) -> dict:
+        """转换为字典"""
+        return {
+            "image_id": self.image_id,
+            "image_name": self.image_name,
+            "image_url": self.image_url,
+            "thumbnail_url": self.thumbnail_url,
+            "unlock_type": self.unlock_type,
+            "unlock_level": self.unlock_level,
+            "unlock_mission_type": self.unlock_mission_type,
+            "display_order": self.display_order,
+            "is_active": self.is_active,
+            "description": self.description,
+            "metadata": self.metadata_json,
+            "created_at": self.created_at.isoformat() if self.created_at else None,
+            "updated_at": self.updated_at.isoformat() if self.updated_at else None,
+        }
+
+
+class UserBackgroundImage(Base):
+    """用户背景图片关联表(记录用户已获得的背景图片)"""
+    __tablename__ = "user_background_images"
+    
+    id = Column(PKBigInt, primary_key=True, autoincrement=True)
+    user_id = Column(String(64), nullable=False, index=True, comment="用户ID")
+    image_id = Column(String(64), nullable=False, index=True, comment="背景图片ID")
+    
+    # 获得信息
+    obtained_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="获得时间")
+    obtained_type = Column(Enum("default", "level_up", "purchase", "mission", "gift", name="obtained_type"), 
+                          default="default", comment="获得方式")
+    obtained_source = Column(String(64), nullable=True, comment="获得来源(任务类型、等级等)")
+    
+    # 扩展信息
+    metadata_json = Column("metadata", JSON, nullable=True, comment="扩展信息")
+    
+    created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
+    
+    __table_args__ = (
+        UniqueConstraint("user_id", "image_id", name="uk_user_background_image"),
+        Index("idx_user_obtained", "user_id", "obtained_at"),
+    )
+    
+    def to_dict(self) -> dict:
+        """转换为字典"""
+        return {
+            "id": self.id,
+            "user_id": self.user_id,
+            "image_id": self.image_id,
+            "obtained_at": self.obtained_at.isoformat() if self.obtained_at else None,
+            "obtained_type": self.obtained_type,
+            "obtained_source": self.obtained_source,
+            "metadata": self.metadata_json,
+            "created_at": self.created_at.isoformat() if self.created_at else None,
+        }
+
+
+for _model in (MissionType, MissionEnergyStone, ConversationEnergyStone, BackgroundImage, UserBackgroundImage):
     _attach_metadata_alias(_model)
 

+ 68 - 1
backend/services/user_service.py

@@ -10,7 +10,7 @@ from datetime import datetime
 from sqlalchemy import select, update, func, insert
 from sqlalchemy.ext.asyncio import AsyncSession
 from database import db_manager
-from models_db import User, UserUIState
+from models_db import User, UserUIState, UserBackgroundImage
 
 logger = logging.getLogger(__name__)
 
@@ -36,6 +36,42 @@ class UserService:
                 return level
         return 0
     
+    async def _init_user_background_image(self, session: AsyncSession, user_id: str) -> None:
+        """
+        初始化用户背景图片(添加到已获得列表)
+        
+        Args:
+            session: 数据库会话
+            user_id: 用户ID
+        """
+        try:
+            default_image_id = "default"
+            
+            # 检查是否已存在,避免重复添加
+            result = await session.execute(
+                select(UserBackgroundImage).where(
+                    UserBackgroundImage.user_id == user_id,
+                    UserBackgroundImage.image_id == default_image_id
+                )
+            )
+            existing = result.scalar_one_or_none()
+            
+            if not existing:
+                user_bg_image = UserBackgroundImage(
+                    user_id=user_id,
+                    image_id=default_image_id,
+                    obtained_type="default",
+                    obtained_source="system_init"
+                )
+                session.add(user_bg_image)
+                logger.debug(f"✅ 为用户初始化背景图片: user_id={user_id}, image_id={default_image_id}")
+            else:
+                logger.debug(f"⏭️ 用户已拥有默认背景图片: user_id={user_id}")
+                
+        except Exception as e:
+            # 背景图片初始化失败不应该影响用户创建,只记录日志
+            logger.warning(f"⚠️ 初始化用户背景图片失败: user_id={user_id}, error={e}")
+    
     async def _generate_temp_username(self, session: AsyncSession) -> str:
         """
         生成临时用户名(格式:User + 7位随机数字)
@@ -119,8 +155,14 @@ class UserService:
                     username=username,
                     user_type="guest",
                     bot_name=bot_name,
+                    current_background_image_id="default",  # 设置默认背景图片
                 )
                 session.add(user)
+                await session.flush()  # 先 flush 确保 user 有 ID,但不提交事务
+                
+                # 初始化背景图片
+                await self._init_user_background_image(session, user_id)
+                
                 await session.commit()
                 await session.refresh(user)
             
@@ -132,6 +174,7 @@ class UserService:
                 "user_type": "guest",
                 "bot_name": bot_name,
                 "user_level": 0,
+                "current_background_image_id": "default",
                 "created_at": datetime.now().isoformat()
             }
             
@@ -483,8 +526,14 @@ class UserService:
                     user_type="registered",
                     is_verified=True,
                     registered_at=datetime.now(),
+                    current_background_image_id="default",  # 设置默认背景图片
                 )
                 session.add(user)
+                await session.flush()  # 先 flush 确保 user 有 ID,但不提交事务
+                
+                # 初始化背景图片
+                await self._init_user_background_image(session, user_id)
+                
                 await session.commit()
                 await session.refresh(user)
             
@@ -528,8 +577,14 @@ class UserService:
                     user_type="registered",
                     is_verified=True,
                     registered_at=datetime.now(),
+                    current_background_image_id="default",  # 设置默认背景图片
                 )
                 session.add(user)
+                await session.flush()  # 先 flush 确保 user 有 ID,但不提交事务
+                
+                # 初始化背景图片
+                await self._init_user_background_image(session, user_id)
+                
                 await session.commit()
                 await session.refresh(user)
             
@@ -575,8 +630,14 @@ class UserService:
                     user_type="registered",
                     is_verified=True,
                     registered_at=datetime.now(),
+                    current_background_image_id="default",  # 设置默认背景图片
                 )
                 session.add(user)
+                await session.flush()  # 先 flush 确保 user 有 ID,但不提交事务
+                
+                # 初始化背景图片
+                await self._init_user_background_image(session, user_id)
+                
                 await session.commit()
                 await session.refresh(user)
             
@@ -654,8 +715,14 @@ class UserService:
                     user_type="registered",
                     is_verified=True,
                     registered_at=datetime.now(),
+                    current_background_image_id="default",  # 设置默认背景图片
                 )
                 session.add(user)
+                await session.flush()  # 先 flush 确保 user 有 ID,但不提交事务
+                
+                # 初始化背景图片
+                await self._init_user_background_image(session, user_id)
+                
                 await session.commit()
                 await session.refresh(user)
             

+ 1 - 1
frontend/ENV_CONFIG.md

@@ -23,7 +23,7 @@ cp .env.example .env
 | `VITE_WS_URL` | WebSocket URL | `wss://localhost:8000/ws` | 否 |
 | `VITE_API_BASE_URL` | API 基础 URL | - | 否 |
 | `VITE_APP_BASE_PATH` | 前端部署子路径(以 `/` 开头和结尾) | `/` | 否 |
-| `VITE_WELCOME_VIDEO_URL` | Welcome 页面视频 URL | `https://tolan.oss-cn-hangzhou.aliyuncs.com/ABC.mp4` | 否 |
+| `VITE_WELCOME_VIDEO_URL` | Welcome 页面视频 URL | `https://lyrria-pro.oss-cn-beijing.aliyuncs.com/Lyrria-video.mp4` | 否 |
 
 ## 使用场景
 

BIN
frontend/src/assets/close-btn.png


BIN
frontend/src/assets/continue-chat.png


BIN
frontend/src/assets/full-progress-bar.png


+ 70 - 5
frontend/src/pages/ChatBotPage.tsx

@@ -17,6 +17,8 @@ import {
   getUserId,
   getVisitedRoutes,
   setUserLevel,
+  getCurrentBackgroundImageUrl,
+  syncBackgroundImageToStorage,
 } from '@/utils/storage';
 import { onScreenResize, calculateLayout, type CalculatedLayout, updateTooSmallFlag } from '@/utils/screenSize';
 import { performanceLogger } from '@/utils/logger';
@@ -51,10 +53,10 @@ const COUNTDOWN_THRESHOLD_MS = 10_000;
 const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost:8000/ws';
 
 // GIF URL 常量
-const DEFAULT_AVATAR_GIF = 'https://tolan.oss-cn-hangzhou.aliyuncs.com/01-Default.gif';
-const POSITIVE_EMOTION_GIF = 'https://tolan.oss-cn-hangzhou.aliyuncs.com/04-Positive%20emotion.gif';
-const NEGATIVE_EMOTION_GIF = 'https://tolan.oss-cn-hangzhou.aliyuncs.com/05-Negative%20emotion.gif';
-const TALKING_GIF = 'https://tolan.oss-cn-hangzhou.aliyuncs.com/06-talking.gif';
+const DEFAULT_AVATAR_GIF = 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/01-Default.gif';
+const POSITIVE_EMOTION_GIF = 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/04-Positive-emotion.gif';
+const NEGATIVE_EMOTION_GIF = 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/05-Negative-emotion.gif';
+const TALKING_GIF = 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/06-talking.gif';
 
 // 情绪分类函数
 const getEmotionCategory = (emotion: string): 'positive' | 'negative' | 'neutral' => {
@@ -107,6 +109,12 @@ export const ChatBotPage = () => {
   const [userLevelValue, setUserLevelValue] = useState(() => getUserLevel());
   const levelRedirectRef = useRef(false);
   const [hasSharedAvatar, setHasSharedAvatarState] = useState(() => getHasSharedAvatar());
+  // 背景图片 URL
+  const [backgroundImageUrl, setBackgroundImageUrl] = useState<string | null>(() => {
+    // 初始化时从 localStorage 获取背景图片 URL
+    const url = getCurrentBackgroundImageUrl();
+    return url || null;
+  });
   // 初始化时检查 localStorage,确保第一次打开时立即显示提示
   const [showTapToChat, setShowTapToChat] = useState(() => {
     const hasShown = getStorage(STORAGE_KEYS.HAS_SHOWN_TAP_TO_CHAT);
@@ -592,6 +600,30 @@ export const ChatBotPage = () => {
     return cleanup;
   }, []);
 
+  // 初始化背景图片(从 localStorage 读取)
+  useEffect(() => {
+    const bgUrl = getCurrentBackgroundImageUrl();
+    if (bgUrl) {
+      setBackgroundImageUrl(bgUrl);
+    } else {
+      // 如果没有背景图片,尝试从用户信息同步
+      const user_id = getUserId();
+      if (user_id) {
+        getUserInfo(user_id).then((userInfo) => {
+          if (userInfo.success && userInfo.data?.current_background_image_id) {
+            syncBackgroundImageToStorage(userInfo.data.current_background_image_id);
+            const newBgUrl = getCurrentBackgroundImageUrl();
+            if (newBgUrl) {
+              setBackgroundImageUrl(newBgUrl);
+            }
+          }
+        }).catch(() => {
+          // 静默处理错误
+        });
+      }
+    }
+  }, []);
+
   // 初始化
   useEffect(() => {
     // 记录用户当前所在页面
@@ -694,6 +726,14 @@ export const ChatBotPage = () => {
               if (typeof userInfo.data.user_level === 'number') {
                 syncUserLevel(userInfo.data.user_level);
               }
+              // 同步背景图片
+              if (userInfo.data.current_background_image_id) {
+                syncBackgroundImageToStorage(userInfo.data.current_background_image_id);
+                const bgUrl = getCurrentBackgroundImageUrl();
+                if (bgUrl) {
+                  setBackgroundImageUrl(bgUrl);
+                }
+              }
               if (totalDurationMs > 0) {
                 // 根据总说话时长计算初始进度(30秒=100%)
                 const PROGRESS_FULL_DURATION_MS = 30 * 1000; // 30000ms
@@ -1290,7 +1330,32 @@ export const ChatBotPage = () => {
   const userInitial = getUserInitial();
   
   return (
-    <div>
+    <div
+      style={{
+        position: 'relative',
+        width: '100%',
+        minHeight: '100vh',
+        minHeight: '100dvh',
+      }}
+    >
+      {/* 背景图片层 - 铺满整个界面 */}
+      {backgroundImageUrl && (
+        <div
+          style={{
+            position: 'fixed',
+            top: 0,
+            left: 0,
+            width: '100%',
+            height: '100%',
+            backgroundImage: `url(${backgroundImageUrl})`,
+            backgroundSize: 'cover',
+            backgroundPosition: 'center',
+            backgroundRepeat: 'no-repeat',
+            zIndex: 0,
+            pointerEvents: 'none',
+          }}
+        />
+      )}
       {/* 树木装饰 - 设计稿:height 220px,top 175px */}
       <img
         src={treesImage}

+ 1 - 1
frontend/src/pages/FirstLoginPage.tsx

@@ -44,7 +44,7 @@ export const FirstLoginPage = () => {
       />
       {/* 中部 GIF 动画图片 */}
       <img
-        src="https://tolan.oss-cn-hangzhou.aliyuncs.com/03-Cheer.gif"
+        src="https://lyrria-pro.oss-cn-beijing.aliyuncs.com/03-Cheer.gif"
         alt="cheer animation"
         style={{
           position: 'absolute',

+ 1 - 1
frontend/src/pages/GreetingPage.tsx

@@ -370,7 +370,7 @@ export const GreetingPage = () => {
           }}
         >
           <img
-            src="https://tolan.oss-cn-hangzhou.aliyuncs.com/01-Default.gif"
+            src="https://lyrria-pro.oss-cn-beijing.aliyuncs.com/01-Default.gif"
             alt="Character Avatar"
             className="w-full h-full object-contain object-bottom"
             style={{ width: '100%', height: '100%' }}

+ 1 - 1
frontend/src/pages/Level34WelcomePage.tsx

@@ -54,7 +54,7 @@ export const Level34WelcomePage = () => {
       />
       {/* GIF动画图片 - Y: 192, 左右居中, width: 343, height: 280,动画结束后显示 */}
       <img
-        src="https://tolan.oss-cn-hangzhou.aliyuncs.com/03-Cheer.gif"
+        src="https://lyrria-pro.oss-cn-beijing.aliyuncs.com/03-Cheer.gif"
         alt="cheer animation"
         style={{
           position: 'absolute',

+ 1 - 1
frontend/src/pages/Level5WelcomePage.tsx

@@ -54,7 +54,7 @@ export const Level5WelcomePage = () => {
       />
       {/* GIF动画图片 - Y: 192, 左右居中, width: 343, height: 280,动画结束后显示 */}
       <img
-        src="https://tolan.oss-cn-hangzhou.aliyuncs.com/03-Cheer.gif"
+        src="https://lyrria-pro.oss-cn-beijing.aliyuncs.com/03-Cheer.gif"
         alt="cheer animation"
         style={{
           position: 'absolute',

+ 1 - 1
frontend/src/pages/LoginPage.tsx

@@ -59,7 +59,7 @@ export const LoginPage = () => {
       </div>
       {/* 中部 GIF 动画图片 */}
       <img
-        src="https://tolan.oss-cn-hangzhou.aliyuncs.com/01-Default.gif"
+        src="https://lyrria-pro.oss-cn-beijing.aliyuncs.com/01-Default.gif"
         alt="cheer animation"
         style={{
           position: 'absolute',

+ 1 - 1
frontend/src/pages/LumiNamingPage.tsx

@@ -248,7 +248,7 @@ export const LumiNamingPage = () => {
           }}
         >
           <img
-            src="https://tolan.oss-cn-hangzhou.aliyuncs.com/01-Default.gif"
+            src="https://lyrria-pro.oss-cn-beijing.aliyuncs.com/01-Default.gif"
             alt="Character Avatar"
             className="w-full h-full object-contain object-bottom"
             style={{ width: '100%', height: '100%' }}

+ 1 - 1
frontend/src/pages/VideoIntroPage.tsx

@@ -555,7 +555,7 @@ export const VideoIntroPage = () => {
           controlsList="nodownload nofullscreen noremoteplayback"
           disablePictureInPicture
         >
-          <source src="https://tolan.oss-cn-hangzhou.aliyuncs.com/ABC.mp4" type="video/mp4" />
+          <source src="https://lyrria-pro.oss-cn-beijing.aliyuncs.com/Lyrria-video.mp4" type="video/mp4" />
           <p className="vjs-no-js">
             您的浏览器不支持HTML5视频,请升级浏览器。
           </p>

+ 2 - 2
frontend/src/pages/WelcomePage.tsx

@@ -63,7 +63,7 @@ export const WelcomePage = () => {
     }
 
     // 🚀 提前预加载视频资源(使用 link preload)
-    const videoUrl = import.meta.env.VITE_WELCOME_VIDEO_URL || 'https://tolan.oss-cn-hangzhou.aliyuncs.com/ABC.mp4';
+    const videoUrl = import.meta.env.VITE_WELCOME_VIDEO_URL || 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/Lyrria-video.mp4';
     const preloadLink = document.createElement('link');
     preloadLink.rel = 'preload';
     preloadLink.as = 'video';
@@ -673,7 +673,7 @@ export const WelcomePage = () => {
           preload="auto"
         >
           <source 
-            src={import.meta.env.VITE_WELCOME_VIDEO_URL || 'https://tolan.oss-cn-hangzhou.aliyuncs.com/ABC.mp4'} 
+            src={import.meta.env.VITE_WELCOME_VIDEO_URL || 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/Lyrria-video.mp4'} 
             type="video/mp4" 
           />
           <p className="vjs-no-js">

+ 18 - 1
frontend/src/utils/api.ts

@@ -41,6 +41,7 @@ export interface CreateUserResponse {
     username: string;
     user_type: string;
     user_level: number;
+    current_background_image_id?: string;
     created_at: string;
   };
 }
@@ -82,6 +83,13 @@ export const createUser = async (username: string = ''): Promise<CreateUserRespo
 
     const data = await response.json();
     console.log('[API] 创建用户成功:', data);
+    
+    // 同步背景图片到 localStorage
+    if (data.success && data.data?.current_background_image_id) {
+      const { syncBackgroundImageToStorage } = await import('./storage');
+      syncBackgroundImageToStorage(data.data.current_background_image_id);
+    }
+    
     return data;
   } catch (error) {
     console.error('[API] 创建用户异常:', error);
@@ -107,6 +115,7 @@ export interface GetUserResponse {
     bot_name?: string;
     total_speech_duration_ms?: number;
     user_level?: number;
+    current_background_image_id?: string;
     created_at: string;
   };
 }
@@ -124,7 +133,15 @@ export const getUserInfo = async (user_id: string): Promise<GetUserResponse> =>
     throw new Error(`获取用户信息失败: ${response.statusText}`);
   }
 
-  return response.json();
+  const data = await response.json();
+  
+  // 同步背景图片到 localStorage
+  if (data.success && data.data?.current_background_image_id) {
+    const { syncBackgroundImageToStorage } = await import('./storage');
+    syncBackgroundImageToStorage(data.data.current_background_image_id);
+  }
+  
+  return data;
 };
 
 /**

+ 104 - 0
frontend/src/utils/storage.ts

@@ -20,6 +20,8 @@ export const STORAGE_KEYS = {
   IS_LOGGED_IN: 'Lyrria_is_logged_in', // 是否已登录
   USER_LEVEL: 'Lyrria_user_level', // 用户当前等级
   HAS_SHARED_AVATAR: 'Lyrria_has_shared_avatar', // 是否已分享头像奖励
+  CURRENT_BACKGROUND_IMAGE_ID: 'Lyrria_current_background_image_id', // 当前背景图片ID
+  CURRENT_BACKGROUND_IMAGE_URL: 'Lyrria_current_background_image_url', // 当前背景图片URL
 } as const;
 
 // 路由顺序表
@@ -249,6 +251,108 @@ export const setHasSharedAvatar = (value: boolean): boolean => {
   return setStorage(STORAGE_KEYS.HAS_SHARED_AVATAR, value ? 'true' : 'false');
 };
 
+/**
+ * 获取当前背景图片ID
+ */
+export const getCurrentBackgroundImageId = (): string | null => {
+  return getStorage(STORAGE_KEYS.CURRENT_BACKGROUND_IMAGE_ID);
+};
+
+/**
+ * 设置当前背景图片ID
+ */
+export const setCurrentBackgroundImageId = (image_id: string): boolean => {
+  return setStorage(STORAGE_KEYS.CURRENT_BACKGROUND_IMAGE_ID, image_id);
+};
+
+/**
+ * 清除当前背景图片ID
+ */
+export const clearCurrentBackgroundImageId = (): boolean => {
+  return removeStorage(STORAGE_KEYS.CURRENT_BACKGROUND_IMAGE_ID);
+};
+
+/**
+ * 获取当前背景图片URL
+ */
+export const getCurrentBackgroundImageUrl = (): string | null => {
+  return getStorage(STORAGE_KEYS.CURRENT_BACKGROUND_IMAGE_URL);
+};
+
+/**
+ * 设置当前背景图片URL
+ */
+export const setCurrentBackgroundImageUrl = (image_url: string): boolean => {
+  return setStorage(STORAGE_KEYS.CURRENT_BACKGROUND_IMAGE_URL, image_url);
+};
+
+/**
+ * 清除当前背景图片URL
+ */
+export const clearCurrentBackgroundImageUrl = (): boolean => {
+  return removeStorage(STORAGE_KEYS.CURRENT_BACKGROUND_IMAGE_URL);
+};
+
+/**
+ * 设置背景图片信息(同时设置ID和URL)
+ */
+export const setBackgroundImage = (image_id: string, image_url: string): boolean => {
+  const idOk = setCurrentBackgroundImageId(image_id);
+  const urlOk = setCurrentBackgroundImageUrl(image_url);
+  return idOk && urlOk;
+};
+
+/**
+ * 清除背景图片信息
+ */
+export const clearBackgroundImage = (): void => {
+  clearCurrentBackgroundImageId();
+  clearCurrentBackgroundImageUrl();
+};
+
+/**
+ * 背景图片 URL 映射
+ */
+export const BACKGROUND_IMAGE_URL_MAP: Record<string, string> = {
+  default: 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/init.jpg',
+  happynewyear: 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/happynewyear.jpg',
+};
+
+/**
+ * 根据背景图片ID获取URL
+ */
+export const getBackgroundImageUrlById = (image_id: string | null | undefined): string | null => {
+  if (!image_id) {
+    return null;
+  }
+  return BACKGROUND_IMAGE_URL_MAP[image_id] || null;
+};
+
+/**
+ * 同步背景图片信息到 localStorage
+ * 根据 image_id 自动获取对应的 URL
+ */
+export const syncBackgroundImageToStorage = (image_id: string | null | undefined): void => {
+  if (!image_id) {
+    // 如果没有 image_id,使用默认值
+    const defaultId = 'default';
+    const defaultUrl = getBackgroundImageUrlById(defaultId);
+    if (defaultUrl) {
+      setBackgroundImage(defaultId, defaultUrl);
+    }
+    return;
+  }
+  
+  const image_url = getBackgroundImageUrlById(image_id);
+  if (image_url) {
+    setBackgroundImage(image_id, image_url);
+  } else {
+    // 如果找不到对应的 URL,只存储 ID
+    setCurrentBackgroundImageId(image_id);
+    console.warn(`[storage] 未找到背景图片URL映射: image_id=${image_id}`);
+  }
+};
+
 /**
  * 清除用户相关的路由和页面状态
  * 当用户ID不存在时调用,清空 Lyrria_visited_routes 和 Lyrria_current_page