zlwu пре 10 месеци
родитељ
комит
a7e404450a

+ 183 - 0
backend/.env.cy

@@ -0,0 +1,183 @@
+# 应用基础配置
+# 应用名称
+APP_NAME=Lyrria Voice Assistant
+# 应用版本号
+APP_VERSION=1.0.0
+# 服务器监听地址
+HOST=0.0.0.0
+# FastAPI 服务端口
+PORT=8766
+# 是否启用调试模式
+DEBUG=false
+
+# WebSocket 会话配置
+# 客户端心跳发送间隔(秒)
+WS_HEARTBEAT_INTERVAL=30
+# WebSocket 连接超时时间(秒)
+WS_TIMEOUT=60
+
+# 能量石 / 缓存配置
+# 能量石有效期(秒)
+ENERGY_STONE_TTL_SECONDS=20
+# 对话石刷新间隔(秒)
+CONVERSATION_STONE_INTERVAL_SECONDS=30
+# 任务类型缓存时长(秒)
+MISSION_TYPE_CACHE_SECONDS=60
+
+# DashScope API 配置
+# 国内版 DashScope API Key #sk-92f2fec23dc94de49a31c9945d04e5f7
+DASHSCOPE_API_KEY=sk-59d99526b02f405db8f3cd2c81a5009c
+# 国际版 DashScope API Key
+DASHSCOPE_API_KEY_INTL=
+# 是否优先使用国际版 DashScope
+USE_DASHSCOPE_INTL=false
+
+# LLM 模型配置
+# 默认使用的对话模型
+LLM_MODEL=qwen-flash
+# 自定义 LLM 兼容模式接口地址
+LLM_BASE_URL=
+# LLM 连接健康检查间隔(秒)
+LLM_HEALTHCHECK_INTERVAL=300
+
+# STT 模型配置
+# 语音识别模型名称
+STT_MODEL=qwen3-asr-flash-realtime
+# 自定义 STT WebSocket 地址
+STT_WS_URL=
+# 语音识别语言(zh/en/ja/ko/auto)
+STT_LANGUAGE=zh
+
+# TTS 模型配置
+# 语音合成模型名称
+TTS_MODEL=qwen3-tts-flash
+# 自定义 TTS HTTP API 地址
+TTS_API_URL=
+# 语音风格(如 Cherry/Jada)
+TTS_VOICE=Jada
+# 语速倍率(1.0 为正常)
+TTS_SPEECH_RATE=0.75
+# 连续 TTS 调用的最小间隔(秒)
+TTS_MIN_INTERVAL=0.8
+# TTS 请求最大重试次数
+TTS_MAX_RETRIES=3
+# TTS 初始重试等待时间(秒)
+TTS_RETRY_DELAY=5.0
+# TTS 指数退避倍数
+TTS_RETRY_BACKOFF=2.0
+# TTS 连接健康检查间隔(秒)
+TTS_HEALTHCHECK_INTERVAL=300
+
+# AI 助手提示词配置
+# 学习者语言水平标签
+LEARNER_PROFICIENCY=A1-A2
+# 学习者年龄段标签
+AGE_TAG=Adult
+# 当前对话主题标签
+TOPIC_TAG=Daily Conversation
+# 实验分组标签(control/experiment)
+EXPERIMENT_TAG=control
+# 是否使用简化提示词
+USE_SIMPLIFIED_PROMPT=false
+
+# SSL 证书配置(本地开发留空自动查找 ../ssl)
+# 自定义 SSL 私钥路径
+SSL_KEYFILE=
+# 自定义 SSL 证书路径
+SSL_CERTFILE=
+
+# CORS 配置
+# 是否允许任意来源访问
+CORS_ALLOW_ALL=true
+
+# MySQL 配置
+# 数据库主机地址
+MYSQL_HOST=101.201.60.170
+# 数据库端口
+MYSQL_PORT=13306
+# 数据库用户名
+MYSQL_USER=root
+# 数据库密码
+MYSQL_PASSWORD=Lyrria@Root@123456
+# 默认数据库名
+MYSQL_DATABASE=Lyrria
+# 连接池基础连接数
+MYSQL_POOL_SIZE=10
+# 连接池最大溢出连接数
+MYSQL_MAX_OVERFLOW=20
+# 连接超时时间(秒)
+MYSQL_CONNECT_TIMEOUT=10
+# 读操作超时时间(秒)
+MYSQL_READ_TIMEOUT=10
+# 连接池回收时间(秒)
+MYSQL_POOL_RECYCLE=7200
+# 数据库健康检查间隔(秒)
+MYSQL_HEALTHCHECK_INTERVAL=600
+
+# Redis 配置
+# Redis 主机地址
+REDIS_HOST=101.201.60.170
+# Redis 端口
+REDIS_PORT=16379
+# Redis 库序号
+REDIS_DB=1
+# Redis 认证密码
+REDIS_PASSWORD=Lyrria@redis@123456
+# 是否使用 SSL 连接 Redis
+REDIS_SSL=false
+# 会话数据过期时间(秒)
+REDIS_SESSION_TTL=86400
+# 连接池最大连接数
+REDIS_MAX_CONNECTIONS=50
+# Redis 连接健康检查间隔(秒)
+REDIS_HEALTHCHECK_INTERVAL=300
+
+# HTTP 客户端配置(DashScope API)
+# keep-alive 连接池大小
+HTTPX_MAX_KEEPALIVE_CONNECTIONS=20
+# HTTP 客户端最大连接数
+HTTPX_MAX_CONNECTIONS=50
+# keep-alive 连接超时时长(秒)
+HTTPX_KEEPALIVE_EXPIRY=600.0
+
+# 日志配置
+# 日志等级
+LOG_LEVEL=DEBUG
+# 日志输出目录
+LOG_DIR=logs
+# 单个日志文件大小上限
+LOG_ROTATION="500 MB"
+# 日志保留时长
+LOG_RETENTION="10 days"
+
+# 微信开放平台配置
+WECHAT_MP_TOKEN=lyrria_mp_token_2024
+WECHAT_APP_ID=wx9f1c0f005a84435d
+WECHAT_APP_SECRET=b719c27b07b5db0abd2b83cc8dc22c7b
+WECHAT_REDIRECT_URI=http://www.chengyantek.cn/api/auth/wechat/callback
+
+# QQ开放平台配置
+QQ_APP_ID=102820972
+QQ_APP_KEY=gCpENJ1WlkU8cyEG
+QQ_REDIRECT_URI=https://www.chengyantek.cn/api/qq/mp/verify
+QQ_APP_NAME=Tolan英语教学助手
+
+# 前端URL配置(用于OAuth回调后的前端跳转)
+FRONTEND_URL=https://www.chengyantek.cn
+
+# ==================== 微信开放平台(用于登录)====================
+# 开放平台的AppID
+WECHAT_OPEN_APP_ID=wx9f1c0f005a84435d
+# 开放平台的AppSecret
+WECHAT_OPEN_APP_SECRET=b719c27b07b5db0abd2b83cc8dc22c7b
+WECHAT_REDIRECT_URI=https://www.chengyantek.cn/api/auth/wechat/callback
+
+# ==================== 微信公众平台(用于分享)====================
+WECHAT_MP_APP_ID=wxa20cdbd5ad8b28f2
+WECHAT_MP_APP_SECRET=fb6e6420171fd3cb6fefd35ea2c5bd18
+
+# ==================== 微信公众平台(用于公众号消息推送)====================
+WECHAT_MP_TOKEN=lyrria_mp_token_2024
+
+API_CLIENT_KEY=jdafdsfasfaakkkzxmfdxdd
+API_KEY_ENABLED=false

+ 183 - 0
backend/.env.lyrria

@@ -0,0 +1,183 @@
+# 应用基础配置
+# 应用名称
+APP_NAME=Lyrria Voice Assistant
+# 应用版本号
+APP_VERSION=1.0.0
+# 服务器监听地址
+HOST=0.0.0.0
+# FastAPI 服务端口
+PORT=8766
+# 是否启用调试模式
+DEBUG=false
+
+# WebSocket 会话配置
+# 客户端心跳发送间隔(秒)
+WS_HEARTBEAT_INTERVAL=30
+# WebSocket 连接超时时间(秒)
+WS_TIMEOUT=60
+
+# 能量石 / 缓存配置
+# 能量石有效期(秒)
+ENERGY_STONE_TTL_SECONDS=20
+# 对话石刷新间隔(秒)
+CONVERSATION_STONE_INTERVAL_SECONDS=30
+# 任务类型缓存时长(秒)
+MISSION_TYPE_CACHE_SECONDS=60
+
+# DashScope API 配置
+# 国内版 DashScope API Key #sk-92f2fec23dc94de49a31c9945d04e5f7
+DASHSCOPE_API_KEY=sk-59d99526b02f405db8f3cd2c81a5009c
+# 国际版 DashScope API Key
+DASHSCOPE_API_KEY_INTL=
+# 是否优先使用国际版 DashScope
+USE_DASHSCOPE_INTL=false
+
+# LLM 模型配置
+# 默认使用的对话模型
+LLM_MODEL=qwen-flash
+# 自定义 LLM 兼容模式接口地址
+LLM_BASE_URL=
+# LLM 连接健康检查间隔(秒)
+LLM_HEALTHCHECK_INTERVAL=300
+
+# STT 模型配置
+# 语音识别模型名称
+STT_MODEL=qwen3-asr-flash-realtime
+# 自定义 STT WebSocket 地址
+STT_WS_URL=
+# 语音识别语言(zh/en/ja/ko/auto)
+STT_LANGUAGE=zh
+
+# TTS 模型配置
+# 语音合成模型名称
+TTS_MODEL=qwen3-tts-flash
+# 自定义 TTS HTTP API 地址
+TTS_API_URL=
+# 语音风格(如 Cherry/Jada)
+TTS_VOICE=Jada
+# 语速倍率(1.0 为正常)
+TTS_SPEECH_RATE=0.75
+# 连续 TTS 调用的最小间隔(秒)
+TTS_MIN_INTERVAL=0.8
+# TTS 请求最大重试次数
+TTS_MAX_RETRIES=3
+# TTS 初始重试等待时间(秒)
+TTS_RETRY_DELAY=5.0
+# TTS 指数退避倍数
+TTS_RETRY_BACKOFF=2.0
+# TTS 连接健康检查间隔(秒)
+TTS_HEALTHCHECK_INTERVAL=300
+
+# AI 助手提示词配置
+# 学习者语言水平标签
+LEARNER_PROFICIENCY=A1-A2
+# 学习者年龄段标签
+AGE_TAG=Adult
+# 当前对话主题标签
+TOPIC_TAG=Daily Conversation
+# 实验分组标签(control/experiment)
+EXPERIMENT_TAG=control
+# 是否使用简化提示词
+USE_SIMPLIFIED_PROMPT=false
+
+# SSL 证书配置(本地开发留空自动查找 ../ssl)
+# 自定义 SSL 私钥路径
+SSL_KEYFILE=
+# 自定义 SSL 证书路径
+SSL_CERTFILE=
+
+# CORS 配置
+# 是否允许任意来源访问
+CORS_ALLOW_ALL=true
+
+# MySQL 配置
+# 数据库主机地址
+MYSQL_HOST=101.201.60.170
+# 数据库端口
+MYSQL_PORT=13306
+# 数据库用户名
+MYSQL_USER=root
+# 数据库密码
+MYSQL_PASSWORD=Lyrria@Root@123456
+# 默认数据库名
+MYSQL_DATABASE=Lyrria
+# 连接池基础连接数
+MYSQL_POOL_SIZE=10
+# 连接池最大溢出连接数
+MYSQL_MAX_OVERFLOW=20
+# 连接超时时间(秒)
+MYSQL_CONNECT_TIMEOUT=10
+# 读操作超时时间(秒)
+MYSQL_READ_TIMEOUT=10
+# 连接池回收时间(秒)
+MYSQL_POOL_RECYCLE=7200
+# 数据库健康检查间隔(秒)
+MYSQL_HEALTHCHECK_INTERVAL=600
+
+# Redis 配置
+# Redis 主机地址
+REDIS_HOST=101.201.60.170
+# Redis 端口
+REDIS_PORT=16379
+# Redis 库序号
+REDIS_DB=1
+# Redis 认证密码
+REDIS_PASSWORD=Lyrria@redis@123456
+# 是否使用 SSL 连接 Redis
+REDIS_SSL=false
+# 会话数据过期时间(秒)
+REDIS_SESSION_TTL=86400
+# 连接池最大连接数
+REDIS_MAX_CONNECTIONS=50
+# Redis 连接健康检查间隔(秒)
+REDIS_HEALTHCHECK_INTERVAL=300
+
+# HTTP 客户端配置(DashScope API)
+# keep-alive 连接池大小
+HTTPX_MAX_KEEPALIVE_CONNECTIONS=20
+# HTTP 客户端最大连接数
+HTTPX_MAX_CONNECTIONS=50
+# keep-alive 连接超时时长(秒)
+HTTPX_KEEPALIVE_EXPIRY=600.0
+
+# 日志配置
+# 日志等级
+LOG_LEVEL=DEBUG
+# 日志输出目录
+LOG_DIR=logs
+# 单个日志文件大小上限
+LOG_ROTATION="500 MB"
+# 日志保留时长
+LOG_RETENTION="10 days"
+
+# 微信开放平台配置
+WECHAT_MP_TOKEN=lyrria_mp_token_2024
+WECHAT_APP_ID=wx9f1c0f005a84435d
+WECHAT_APP_SECRET=b719c27b07b5db0abd2b83cc8dc22c7b
+WECHAT_REDIRECT_URI=http://www.lyrria.com/api/auth/wechat/callback
+
+# QQ开放平台配置
+QQ_APP_ID=102820972
+QQ_APP_KEY=gCpENJ1WlkU8cyEG
+QQ_REDIRECT_URI=https://www.lyrria.com/api/qq/mp/verify
+QQ_APP_NAME=Tolan英语教学助手
+
+# 前端URL配置(用于OAuth回调后的前端跳转)
+FRONTEND_URL=https://www.lyrria.com
+
+# ==================== 微信开放平台(用于登录)====================
+# 开放平台的AppID
+WECHAT_OPEN_APP_ID=wx9f1c0f005a84435d
+# 开放平台的AppSecret
+WECHAT_OPEN_APP_SECRET=b719c27b07b5db0abd2b83cc8dc22c7b
+WECHAT_REDIRECT_URI=https://www.lyrria.com/api/auth/wechat/callback
+
+# ==================== 微信公众平台(用于分享)====================
+WECHAT_MP_APP_ID=wxa20cdbd5ad8b28f2
+WECHAT_MP_APP_SECRET=fb6e6420171fd3cb6fefd35ea2c5bd18
+
+# ==================== 微信公众平台(用于公众号消息推送)====================
+WECHAT_MP_TOKEN=lyrria_mp_token_2024
+
+API_CLIENT_KEY=jdafdsfasfaakkkzxmfdxdd
+API_KEY_ENABLED=false

+ 2 - 0
backend/ENV_CONFIG.md

@@ -44,4 +44,6 @@
 | `QQ_REDIRECT_URI` | *(空)* | QQ OAuth回调地址,格式:`http://www.chengyantek.cn/api/qq/mp/verify`。 |
 | `QQ_APP_NAME` | `Lyrria` | QQ应用名称,用于分享功能(显示在QQ分享卡片中)。 |
 | `FRONTEND_URL` | `http://localhost:5173` | 前端URL,用于OAuth回调后的前端跳转。 |
+| `API_CLIENT_KEY` | *(空)* | 前端API Key,用于验证请求来源。前后端必须配置相同的值,前端通过 `X-Client-Key` header 发送。 |
+| `API_KEY_ENABLED` | `true` | 是否启用API Key验证。设为 `false` 可临时禁用验证(仅用于开发调试)。 |
 

+ 4 - 0
backend/config.py

@@ -194,6 +194,10 @@ class Settings(BaseSettings):
     # 前端URL(用于OAuth回调后的前端跳转)
     FRONTEND_URL: str = os.getenv("FRONTEND_URL", "http://localhost:5173")
     
+    # API安全配置
+    API_CLIENT_KEY: str = os.getenv("API_CLIENT_KEY", "")  # 前端API Key,用于验证请求来源
+    API_KEY_ENABLED: bool = os.getenv("API_KEY_ENABLED", "true").lower() == "true"  # 是否启用API Key验证
+    
     class Config:
         env_file = ".env"
         case_sensitive = True

+ 62 - 3
backend/main.py

@@ -8,9 +8,12 @@ import hashlib
 import uuid
 import random
 from typing import Optional, List, Dict, Any
-from fastapi import FastAPI, WebSocket, HTTPException, Request, Query
+from fastapi import FastAPI, WebSocket, HTTPException, Request, Query, Header
 from fastapi.responses import RedirectResponse, PlainTextResponse
 from fastapi.middleware.cors import CORSMiddleware
+from starlette.middleware.base import BaseHTTPMiddleware
+from starlette.requests import Request as StarletteRequest
+from starlette.responses import Response
 from pydantic import BaseModel
 from contextlib import asynccontextmanager
 import httpx
@@ -80,15 +83,71 @@ app = FastAPI(
     lifespan=lifespan
 )
 
-# 配置CORS
+
+class APIKeyMiddleware(BaseHTTPMiddleware):
+    """API Key验证中间件"""
+    
+    # 不需要验证的路径(健康检查、WebSocket、微信/QQ回调等)
+    EXEMPT_PATHS = [
+        "/",
+        "/docs",
+        "/openapi.json",
+        "/redoc",
+        "/ws",
+        # 微信 OAuth & 公众号回调
+        "/api/auth/wechat/authorize",
+        "/api/auth/wechat/callback",
+        "/api/wechat/mp/verify",
+        "/api/wechat/mp/message",
+        # QQ OAuth & 回调
+        "/api/auth/qq/authorize",
+        "/api/qq/mp/verify",
+        # QQ 分享配置(SDK 拉取)
+        "/api/qq/share/config",
+    ]
+    
+    async def dispatch(self, request: StarletteRequest, call_next):
+        # 检查是否为豁免路径
+        path = request.url.path
+        if any(path.startswith(exempt) for exempt in self.EXEMPT_PATHS):
+            return await call_next(request)
+        
+        # 如果未启用API Key验证,直接通过
+        if not settings.API_KEY_ENABLED:
+            return await call_next(request)
+        
+        # 检查API Key
+        api_key = request.headers.get("X-Client-Key", "")
+        expected_key = settings.API_CLIENT_KEY
+        
+        if not expected_key:
+            logger.warning("⚠️ API_CLIENT_KEY未配置,跳过验证")
+            return await call_next(request)
+        
+        if api_key != expected_key:
+            logger.warning(f"❌ API Key验证失败: path={path}, provided_key={'***' if api_key else '(empty)'}")
+            return Response(
+                content='{"detail":"Invalid API Key"}',
+                status_code=403,
+                media_type="application/json"
+            )
+        
+        return await call_next(request)
+
+
+# 配置CORS - 使用配置中的允许来源
+cors_origins = settings.CORS_ORIGINS if not settings.CORS_ALLOW_ALL else ["*"]
 app.add_middleware(
     CORSMiddleware,
-    allow_origins=["*"],  # 生产环境应该限制具体域名
+    allow_origins=cors_origins,
     allow_credentials=True,
     allow_methods=["*"],
     allow_headers=["*"],
 )
 
+# 添加API Key验证中间件(在CORS之后)
+app.add_middleware(APIKeyMiddleware)
+
 
 @app.get("/")
 async def root():

+ 517 - 0
backend/文档/高并发优化方案.md

@@ -0,0 +1,517 @@
+# 后端高并发优化方案
+
+## 当前架构分析
+
+### ✅ 已实现的优化
+- **异步编程**:FastAPI + async/await
+- **连接池**:MySQL (SQLAlchemy)、Redis、HTTP (httpx)
+- **连接保活**:HTTP连接池心跳机制
+- **超时控制**:数据库、Redis、HTTP请求超时
+- **健康检查**:自动检测并重建失效连接
+
+### ⚠️ 潜在瓶颈
+- WebSocket 单进程处理
+- 数据库查询未优化(索引、批量操作)
+- 缓存策略可进一步优化
+- 缺少限流和熔断机制
+- 缺少异步任务队列
+
+---
+
+## 优化方案
+
+### 1. 部署架构优化
+
+#### 1.1 多进程部署(Gunicorn + Uvicorn Workers)
+
+```bash
+# 安装 Gunicorn
+pip install gunicorn
+
+# 启动命令(生产环境)
+gunicorn main:app \
+  --workers 4 \
+  --worker-class uvicorn.workers.UvicornWorker \
+  --bind 0.0.0.0:8000 \
+  --timeout 120 \
+  --keep-alive 5 \
+  --max-requests 1000 \
+  --max-requests-jitter 50 \
+  --preload
+```
+
+**配置说明**:
+- `--workers 4`:根据 CPU 核心数设置(建议:CPU核心数 × 2)
+- `--worker-class uvicorn.workers.UvicornWorker`:使用异步 worker
+- `--max-requests`:防止内存泄漏,处理 N 个请求后重启 worker
+- `--preload`:预加载应用,减少启动时间
+
+#### 1.2 负载均衡(Nginx)
+
+```nginx
+upstream backend {
+    least_conn;  # 最少连接负载均衡
+    server 127.0.0.1:8000;
+    server 127.0.0.1:8001;
+    server 127.0.0.1:8002;
+    server 127.0.0.1:8003;
+    keepalive 32;  # 保持连接
+}
+
+server {
+    listen 443 ssl http2;
+    
+    # WebSocket 升级
+    location /ws {
+        proxy_pass http://backend;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+        proxy_set_header Host $host;
+        proxy_read_timeout 300s;
+        proxy_send_timeout 300s;
+    }
+    
+    # HTTP API
+    location / {
+        proxy_pass http://backend;
+        proxy_http_version 1.1;
+        proxy_set_header Connection "";
+        keepalive_requests 100;
+    }
+}
+```
+
+---
+
+### 2. 数据库优化
+
+#### 2.1 连接池调优
+
+**当前配置**(`config.py`):
+```python
+MYSQL_POOL_SIZE: int = 10        # 基础连接数
+MYSQL_MAX_OVERFLOW: int = 20     # 最大溢出连接数
+```
+
+**优化建议**:
+```python
+# 根据并发量调整(每个 worker 进程)
+MYSQL_POOL_SIZE: int = 20        # 增加到 20
+MYSQL_MAX_OVERFLOW: int = 40     # 增加到 40
+```
+
+**计算公式**:
+- 总连接数 = (POOL_SIZE + MAX_OVERFLOW) × Worker数量
+- 示例:4 workers × (20 + 40) = 240 个连接
+- ⚠️ 注意:MySQL `max_connections` 需 ≥ 总连接数
+
+#### 2.2 查询优化
+
+**索引优化**:
+```sql
+-- 用户表常用查询字段
+CREATE INDEX idx_user_id ON users(user_id);
+CREATE INDEX idx_phone ON users(phone);
+CREATE INDEX idx_email ON users(email);
+CREATE INDEX idx_wechat_openid ON users(wechat_openid);
+CREATE INDEX idx_qq_openid ON users(qq_openid);
+
+-- 消息表(按用户和轮次查询)
+CREATE INDEX idx_user_round ON messages(user_id, round_number);
+CREATE INDEX idx_user_created ON messages(user_id, created_at);
+
+-- 上下文表(按用户查询)
+CREATE INDEX idx_user_end_round ON conversation_summaries(user_id, end_round);
+```
+
+**批量操作优化**:
+```python
+# ❌ 避免:循环插入
+for item in items:
+    await session.add(item)
+
+# ✅ 推荐:批量插入
+session.add_all(items)
+await session.commit()
+```
+
+**查询优化**:
+```python
+# ❌ 避免:N+1 查询
+users = await session.execute(select(User))
+for user in users:
+    messages = await session.execute(select(Message).where(Message.user_id == user.id))
+
+# ✅ 推荐:使用 selectinload 预加载
+from sqlalchemy.orm import selectinload
+users = await session.execute(
+    select(User).options(selectinload(User.messages))
+)
+```
+
+---
+
+### 3. Redis 缓存优化
+
+#### 3.1 连接池调优
+
+**当前配置**:
+```python
+REDIS_MAX_CONNECTIONS: int = 50
+```
+
+**优化建议**:
+```python
+# 根据并发量调整(每个 worker 进程)
+REDIS_MAX_CONNECTIONS: int = 100  # 增加到 100
+```
+
+#### 3.2 缓存策略优化
+
+**热点数据缓存**:
+```python
+# 用户信息缓存(TTL: 5分钟)
+await redis.setex(f"user:{user_id}", 300, json.dumps(user_data))
+
+# 上下文缓存(TTL: 10分钟)
+await redis.setex(f"context:{user_id}", 600, json.dumps(context_data))
+
+# 使用 Pipeline 批量操作
+pipe = redis.pipeline()
+pipe.set(f"user:{user_id}", user_data, ex=300)
+pipe.set(f"context:{user_id}", context_data, ex=600)
+await pipe.execute()
+```
+
+**缓存预热**:
+```python
+# 应用启动时预热热点数据
+async def warmup_cache():
+    # 预加载活跃用户的上下文
+    active_users = await get_active_users(limit=100)
+    for user_id in active_users:
+        context = await get_context_from_db(user_id)
+        await redis.setex(f"context:{user_id}", 600, json.dumps(context))
+```
+
+---
+
+### 4. WebSocket 优化
+
+#### 4.1 连接管理
+
+**当前问题**:每个 WebSocket 连接占用一个事件循环任务
+
+**优化方案**:
+```python
+# 限制每个用户的并发连接数
+MAX_CONNECTIONS_PER_USER = 3
+
+class WebSocketManager:
+    def __init__(self):
+        self.user_connections: Dict[str, List[WebSocket]] = {}
+    
+    async def add_connection(self, user_id: str, websocket: WebSocket):
+        if user_id not in self.user_connections:
+            self.user_connections[user_id] = []
+        
+        # 限制连接数
+        if len(self.user_connections[user_id]) >= MAX_CONNECTIONS_PER_USER:
+            # 关闭最旧的连接
+            old_ws = self.user_connections[user_id].pop(0)
+            await old_ws.close(code=1008, reason="Too many connections")
+        
+        self.user_connections[user_id].append(websocket)
+```
+
+#### 4.2 消息队列优化
+
+**批量发送**:
+```python
+# ❌ 避免:逐个发送
+for chunk in audio_chunks:
+    await websocket.send_json(chunk)
+
+# ✅ 推荐:批量发送(减少网络往返)
+messages = [chunk.model_dump_json() for chunk in audio_chunks]
+await websocket.send_text("\n".join(messages))
+```
+
+---
+
+### 5. 异步任务队列
+
+#### 5.1 引入 Celery(可选)
+
+**适用场景**:
+- 对话总结生成(耗时 2-3 秒)
+- 用户数据统计
+- 日志持久化
+
+**实现示例**:
+```python
+# celery_app.py
+from celery import Celery
+
+celery_app = Celery(
+    "lyrria",
+    broker="redis://localhost:6379/0",
+    backend="redis://localhost:6379/0"
+)
+
+@celery_app.task
+def generate_summary_async(user_id: str, start_round: int, end_round: int):
+    # 异步生成总结(不阻塞主流程)
+    pass
+```
+
+#### 5.2 使用 asyncio 任务队列(轻量级方案)
+
+**当前已有实现**(`websocket_handler.py`):
+```python
+# 异步持久化队列
+self.pending_persistence = []
+self.persistence_task: Optional[asyncio.Task] = None
+```
+
+**优化建议**:使用 `asyncio.Queue` 替代列表
+```python
+from asyncio import Queue
+
+class WebSocketHandler:
+    def __init__(self):
+        self.persistence_queue = Queue(maxsize=1000)  # 限制队列大小
+    
+    async def _batch_persistence_loop(self):
+        batch = []
+        while True:
+            try:
+                # 批量收集(最多等待 1 秒或收集 100 条)
+                item = await asyncio.wait_for(
+                    self.persistence_queue.get(),
+                    timeout=1.0
+                )
+                batch.append(item)
+                
+                # 达到批量大小或超时,执行批量写入
+                if len(batch) >= 100:
+                    await self._persist_batch(batch)
+                    batch = []
+            except asyncio.TimeoutError:
+                if batch:
+                    await self._persist_batch(batch)
+                    batch = []
+```
+
+---
+
+### 6. 限流和熔断
+
+#### 6.1 API 限流(SlowAPI)
+
+```python
+# 安装:pip install slowapi
+from slowapi import Limiter, _rate_limit_exceeded_handler
+from slowapi.util import get_remote_address
+from slowapi.errors import RateLimitExceeded
+
+limiter = Limiter(key_func=get_remote_address)
+app.state.limiter = limiter
+app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
+
+@app.post("/api/users/create")
+@limiter.limit("10/minute")  # 每分钟 10 次
+async def create_user(request: Request):
+    pass
+```
+
+#### 6.2 WebSocket 限流
+
+```python
+class WebSocketHandler:
+    def __init__(self):
+        self.request_rate_limiter: Dict[str, float] = {}  # user_id -> last_request_time
+    
+    async def handle_audio_chunk(self, message: AudioChunkMessage):
+        # 限流:每个用户每秒最多 10 个请求
+        user_id = self.user_id
+        now = time.time()
+        
+        if user_id in self.request_rate_limiter:
+            time_since_last = now - self.request_rate_limiter[user_id]
+            if time_since_last < 0.1:  # 100ms 最小间隔
+                await self.send_error(message.requestId, "RATE_LIMIT", "请求过于频繁")
+                return
+        
+        self.request_rate_limiter[user_id] = now
+        # ... 处理逻辑
+```
+
+#### 6.3 熔断器(Circuit Breaker)
+
+```python
+from circuitbreaker import circuit
+
+@circuit(failure_threshold=5, recovery_timeout=60)
+async def call_llm_api(text: str):
+    # LLM API 调用
+    # 连续失败 5 次后熔断 60 秒
+    pass
+```
+
+---
+
+### 7. 监控和日志
+
+#### 7.1 性能监控(Prometheus + Grafana)
+
+```python
+# 安装:pip install prometheus-fastapi-instrumentator
+from prometheus_fastapi_instrumentator import Instrumentator
+
+instrumentator = Instrumentator()
+instrumentator.instrument(app).expose(app)
+```
+
+**监控指标**:
+- 请求延迟(P50/P95/P99)
+- 并发连接数
+- 数据库连接池使用率
+- Redis 连接数
+- WebSocket 连接数
+
+#### 7.2 结构化日志
+
+```python
+import structlog
+
+logger = structlog.get_logger()
+
+logger.info(
+    "request_completed",
+    user_id=user_id,
+    request_id=request_id,
+    duration_ms=duration_ms,
+    status_code=200
+)
+```
+
+---
+
+### 8. 配置优化建议
+
+#### 8.1 环境变量配置(`.env`)
+
+```bash
+# 数据库连接池(高并发)
+MYSQL_POOL_SIZE=20
+MYSQL_MAX_OVERFLOW=40
+MYSQL_POOL_RECYCLE=7200
+
+# Redis 连接池
+REDIS_MAX_CONNECTIONS=100
+
+# HTTP 连接池
+HTTPX_MAX_KEEPALIVE_CONNECTIONS=50
+HTTPX_MAX_CONNECTIONS=100
+HTTPX_KEEPALIVE_EXPIRY=600.0
+
+# 限流配置
+RATE_LIMIT_PER_MINUTE=60
+RATE_LIMIT_PER_USER=10
+
+# Worker 配置(Gunicorn)
+WORKERS=4
+MAX_REQUESTS=1000
+```
+
+#### 8.2 系统资源限制
+
+```bash
+# 增加文件描述符限制
+ulimit -n 65535
+
+# 增加 TCP 连接数
+echo "net.core.somaxconn = 65535" >> /etc/sysctl.conf
+sysctl -p
+```
+
+---
+
+## 性能测试
+
+### 压测工具(Locust)
+
+```python
+# locustfile.py
+from locust import HttpUser, task, between
+
+class WebSocketUser(HttpUser):
+    wait_time = between(1, 3)
+    
+    @task
+    def test_websocket(self):
+        # WebSocket 连接测试
+        pass
+    
+    @task(3)
+    def test_api(self):
+        self.client.get("/api/users/123")
+```
+
+**运行压测**:
+```bash
+locust -f locustfile.py --host=https://your-domain.com --users=100 --spawn-rate=10
+```
+
+---
+
+## 优化优先级
+
+### 🔥 高优先级(立即实施)
+1. **多进程部署**(Gunicorn + Workers)
+2. **数据库连接池调优**
+3. **Redis 连接池调优**
+4. **API 限流**
+
+### ⚡ 中优先级(1-2周内)
+5. **数据库索引优化**
+6. **缓存策略优化**
+7. **WebSocket 连接管理**
+
+### 📊 低优先级(长期优化)
+8. **异步任务队列**(Celery)
+9. **监控和告警**(Prometheus)
+10. **熔断器**(Circuit Breaker)
+
+---
+
+## 预期效果
+
+| 优化项 | 当前 | 优化后 | 提升 |
+|--------|------|--------|------|
+| 并发连接数 | ~100 | ~1000 | 10x |
+| 请求延迟(P95) | 500ms | 200ms | 2.5x |
+| 数据库连接数 | 10 | 240(4 workers) | 24x |
+| Redis 连接数 | 50 | 400(4 workers) | 8x |
+| 吞吐量(QPS) | ~100 | ~500 | 5x |
+
+---
+
+## 注意事项
+
+1. **MySQL max_connections**:确保 ≥ (POOL_SIZE + MAX_OVERFLOW) × Workers
+2. **Redis maxclients**:确保 ≥ MAX_CONNECTIONS × Workers
+3. **内存使用**:每个 worker 进程独立内存,注意总内存占用
+4. **WebSocket 状态**:多进程部署时,WebSocket 连接不能跨进程共享(需使用 Redis Pub/Sub 或消息队列)
+
+---
+
+## 参考资源
+
+- [FastAPI 部署文档](https://fastapi.tiangolo.com/deployment/)
+- [Gunicorn 配置指南](https://docs.gunicorn.org/en/stable/settings.html)
+- [SQLAlchemy 连接池](https://docs.sqlalchemy.org/en/14/core/pooling.html)
+- [Redis 性能优化](https://redis.io/docs/manual/optimization/)
+

+ 2 - 0
frontend/.env.production

@@ -18,3 +18,5 @@ VITE_WELCOME_VIDEO_URL=https://tolan.oss-cn-hangzhou.aliyuncs.com/Lyrria-video.m
 # SSL证书路径(可选,打包时不需要)
 # VITE_SSL_KEYFILE=/path/to/production/key.pem
 # VITE_SSL_CERTFILE=/path/to/production/cert.pem
+
+VITE_API_CLIENT_KEY=jdafdsfasfaakkkzxmfdxdd

+ 2 - 0
frontend/.env.production-cy

@@ -11,3 +11,5 @@ VITE_SSL_KEYFILE=/root/.acme.sh/chengyantek.cn_ecc/chengyantek.cn.key
 VITE_SSL_CERTFILE=/root/.acme.sh/chengyantek.cn_ecc/fullchain.cer
 
 VITE_WELCOME_VIDEO_URL=https://tolan.oss-cn-hangzhou.aliyuncs.com/Lyrria-video.mp4
+
+VITE_API_CLIENT_KEY=jdafdsfasfaakkkzxmfdxdd

+ 1 - 0
frontend/ENV_CONFIG.md

@@ -22,6 +22,7 @@ cp .env.example .env
 | `VITE_SSL_CERTFILE` | SSL 证书路径 | `../ssl/cert.pem` | 否 |
 | `VITE_WS_URL` | WebSocket URL | `wss://localhost:8000/ws` | 否 |
 | `VITE_API_BASE_URL` | API 基础 URL | - | 否 |
+| `VITE_API_CLIENT_KEY` | API Key,用于验证请求来源(必须与后端 `API_CLIENT_KEY` 一致) | - | 是 |
 | `VITE_APP_BASE_PATH` | 前端部署子路径(以 `/` 开头和结尾) | `/` | 否 |
 | `VITE_WELCOME_VIDEO_URL` | Welcome 页面视频 URL | `https://lyrria-pro.oss-cn-beijing.aliyuncs.com/Lyrria-video.mp4` | 否 |
 

+ 0 - 43
frontend/src/pages/BeginPage.tsx

@@ -33,7 +33,6 @@ export const BeginPage = () => {
   const hasNavigatedRef = useRef(false); // 是否已经跳转到 /naming,防止重复执行
   const checkIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null); // 检查定时器
   const fallbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); // 兜底定时器
-  const [showSkipButton, setShowSkipButton] = useState(false); // 是否展示"跳过动画"按钮(只供用户手动跳转)
   const hasShownTimeoutTipRef = useRef(false); // 是否已经提示过"响应超时"
 
   // 处理错误参数(如QQ登录时用户未注册)
@@ -447,16 +446,6 @@ export const BeginPage = () => {
         // 立即开始预加载(不阻塞视频播放)
         preloadUserData();
 
-        // 用户可控的"跳过动画"按钮:进入播放阶段一段时间后才出现,避免误触
-        setShowSkipButton(false);
-        const skipTimer = setTimeout(() => {
-          if (!hasNavigatedRef.current && viewStateRef.current === 'playing') {
-            console.log('[WelcomePage] 播放时间较长,展示跳过动画按钮');
-            setShowSkipButton(true);
-          }
-        }, 20000); // 播放 20 秒后才出现"跳过动画"
-        (player as any)._skipTimer = skipTimer;
-
         // 🚀 统一的安全跳转函数,避免多次 navigate
         const safeNavigateToNaming = () => {
           if (hasNavigatedRef.current) {
@@ -569,11 +558,6 @@ export const BeginPage = () => {
               (player as any)._timeUpdateHandler = null;
             }
           }
-          const skipTimer = (player as any)._skipTimer;
-          if (skipTimer) {
-            clearTimeout(skipTimer);
-            (player as any)._skipTimer = null;
-          }
         }
       };
     }
@@ -1069,33 +1053,6 @@ export const BeginPage = () => {
           </p>
         </video>
 
-        {/* 用户可控的跳过按钮:只在播放阶段且出现卡顿/播放时间较长时显示 */}
-        {viewState === 'playing' && showSkipButton && !hasNavigatedRef.current && (
-          <button
-            type="button"
-            onClick={() => {
-              if (!hasNavigatedRef.current) {
-                hasNavigatedRef.current = true;
-                navigate('/naming');
-              }
-            }}
-            style={{
-              position: 'absolute',
-              bottom: '20px',
-              left: '50%',
-              transform: 'translateX(-50%)',
-              padding: '10px 20px',
-              background: 'rgba(0, 0, 0, 0.6)',
-              color: '#fff',
-              border: '1px solid rgba(255, 255, 255, 0.3)',
-              borderRadius: '20px',
-              cursor: 'pointer',
-              zIndex: 100,
-            }}
-          >
-            跳过动画,进入命名
-          </button>
-        )}
       </div>
     </div>
     </>

+ 4 - 3
frontend/src/pages/FirstLoginRewardPage.tsx

@@ -653,8 +653,8 @@ export const FirstLoginRewardPage = () => {
                     border: 'none',
                     padding: 0,
                     background: 'none',
-                    // width: px2vmin(118),
-                    height: px2vmin(35),
+                    width: px2vmin(140),
+                    height: px2vmin(36),
                     cursor: 'pointer',
                   }}
                 >
@@ -662,7 +662,8 @@ export const FirstLoginRewardPage = () => {
                     src={claimGiftBtnImage}
                     alt="Claim Gift"
                     style={{
-                      height: '100%',
+                      width: '100%',
+                      height: 'auto',
                       display: 'block',
                     }}
                   />

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

@@ -36,6 +36,7 @@ export const LoginPage = () => {
   const [isChecking, setIsChecking] = useState(false);
   const [isAreaCodeOpen, setIsAreaCodeOpen] = useState(false);
   const areaCodeContainerRef = useRef<HTMLDivElement>(null);
+  const emailRef = useRef<HTMLInputElement>(null);
 
   const areaCodeOptions = [
     { code: '+86', label: '中国', flag: 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/china.png' },

+ 27 - 14
frontend/src/pages/ProfilePage.tsx

@@ -21,7 +21,21 @@ const getApiBaseUrl = (): string => {
   
   return apiUrl;
 };
-import { getUserId, getStorage, setStorage, STORAGE_KEYS, setCurrentAvatarId as setAvatarIdToStorage, setCurrentBackgroundImageId, syncBackgroundImageToStorage } from '@/utils/storage';
+
+// 获取统一的请求头(包含 API Key)
+const getApiHeaders = (): Record<string, string> => {
+  const headers: Record<string, string> = {
+    'Content-Type': 'application/json',
+  };
+  
+  const apiKey = import.meta.env.VITE_API_CLIENT_KEY || '';
+  if (apiKey) {
+    headers['X-Client-Key'] = apiKey;
+  }
+  
+  return headers;
+};
+import { getUserId, getStorage, setStorage, STORAGE_KEYS, setCurrentAvatarId as setAvatarIdToStorage, setCurrentBackgroundImageId as setBackgroundImageIdToStorage, getCurrentBackgroundImageId, syncBackgroundImageToStorage } from '@/utils/storage';
 import { getOssUrl } from '@/utils/oss';
 import { MissionEnergyStoneModal } from '@/components/MissionEnergyStoneModal';
 
@@ -33,7 +47,7 @@ const lumiNoCap = getOssUrl('Lumi-no-cap.png');
 const lumiCap = getOssUrl('Lumi-cap.png');
 const arrowRight = getOssUrl('arrow_right.png');
 const headCap = getOssUrl('head-cap.png');
-const bgHappyNewYear = getOssUrl('bg-happynewyear.png');
+const bgHappyNewYear = getOssUrl('bg-happynewyear-s.png');
 const starBg = getOssUrl('star-bg.png');
 const rocketIcon = 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/Rocket.gif';
 
@@ -78,9 +92,7 @@ export const ProfilePage = () => {
       const apiBaseUrl = getApiBaseUrl();
       const response = await fetch(`${apiBaseUrl}/api/users/${userId}/energy-stones/count`, {
         method: 'GET',
-        headers: {
-          'Content-Type': 'application/json',
-        },
+        headers: getApiHeaders(),
       });
 
       if (response.ok) {
@@ -107,9 +119,7 @@ export const ProfilePage = () => {
       const apiBaseUrl = getApiBaseUrl();
       const response = await fetch(`${apiBaseUrl}/api/users/${userId}/avatars`, {
         method: 'GET',
-        headers: {
-          'Content-Type': 'application/json',
-        },
+        headers: getApiHeaders(),
       });
 
       if (response.ok) {
@@ -138,9 +148,7 @@ export const ProfilePage = () => {
       const apiBaseUrl = getApiBaseUrl();
       const response = await fetch(`${apiBaseUrl}/api/users/${userId}/background-images`, {
         method: 'GET',
-        headers: {
-          'Content-Type': 'application/json',
-        },
+        headers: getApiHeaders(),
       });
 
       if (response.ok) {
@@ -184,6 +192,13 @@ export const ProfilePage = () => {
           setUsername(userInfo.data.username || '');
           setBotName(userInfo.data.bot_name || getStorage(STORAGE_KEYS.LUMI_NAME) || 'Lumi');
           setUserLevel(userInfo.data.user_level || 0);
+          // 同步当前背景图片状态(用于正确展示选中边框)
+          const backgroundIdFromUser = userInfo.data.current_background_image_id || getCurrentBackgroundImageId();
+          if (backgroundIdFromUser) {
+            setCurrentBackgroundImageId(backgroundIdFromUser);
+            // 存一份到 localStorage,保持与后端一致
+            setBackgroundImageIdToStorage(backgroundIdFromUser);
+          }
           setTotalSpeechDurationMs(userInfo.data.total_speech_duration_ms || 0);
         }
 
@@ -392,9 +407,7 @@ export const ProfilePage = () => {
       const apiBaseUrl = getApiBaseUrl();
       const response = await fetch(`${apiBaseUrl}/api/users/${userId}/feedback`, {
         method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-        },
+        headers: getApiHeaders(),
         body: JSON.stringify({ content: feedbackText.trim() }),
       });
 

+ 0 - 46
frontend/src/pages/WelcomePage.tsx

@@ -29,7 +29,6 @@ export const WelcomePage = () => {
   const hasNavigatedRef = useRef(false); // 是否已经跳转到 /naming,防止重复执行
   const checkIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null); // 检查定时器
   const fallbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); // 兜底定时器
-  const [showSkipButton, setShowSkipButton] = useState(false); // 是否展示"跳过动画"按钮(只供用户手动跳转)
   const hasShownTimeoutTipRef = useRef(false); // 是否已经提示过"响应超时"
 
   // 处理错误参数(如QQ登录时用户未注册)
@@ -443,16 +442,6 @@ export const WelcomePage = () => {
         // 立即开始预加载(不阻塞视频播放)
         preloadUserData();
 
-        // 用户可控的"跳过动画"按钮:进入播放阶段一段时间后才出现,避免误触
-        setShowSkipButton(false);
-        const skipTimer = setTimeout(() => {
-          if (!hasNavigatedRef.current && viewStateRef.current === 'playing') {
-            console.log('[WelcomePage] 播放时间较长,展示跳过动画按钮');
-            setShowSkipButton(true);
-          }
-        }, 20000); // 播放 20 秒后才出现"跳过动画"
-        (player as any)._skipTimer = skipTimer;
-
         // 🚀 统一的安全跳转函数,避免多次 navigate
         const safeNavigateToBegin = () => {
           if (hasNavigatedRef.current) {
@@ -565,11 +554,6 @@ export const WelcomePage = () => {
               (player as any)._timeUpdateHandler = null;
             }
           }
-          const skipTimer = (player as any)._skipTimer;
-          if (skipTimer) {
-            clearTimeout(skipTimer);
-            (player as any)._skipTimer = null;
-          }
         }
       };
     }
@@ -609,7 +593,6 @@ export const WelcomePage = () => {
     // 切换到加载状态
     setViewState('loading');
     setIsLoading(true);
-    setShowSkipButton(false);
     loadingStartTimeRef.current = Date.now();
 
     // 🔑 关键修复:在用户交互的直接调用链中立即尝试播放
@@ -786,7 +769,6 @@ export const WelcomePage = () => {
         style={{
           position: 'relative',
           width: '100%',
-          minHeight: '100dvh',
           minHeight: 'calc(var(--vh, 1vh) * 100)',
           overflow: 'hidden',
           paddingTop: 'env(safe-area-inset-top)',
@@ -801,7 +783,6 @@ export const WelcomePage = () => {
             style={{
               position: 'relative',
               width: '100%',
-              minHeight: '100dvh',
               minHeight: 'calc(var(--vh, 1vh) * 100)',
               margin: '0 auto',
               zIndex: 1,
@@ -986,33 +967,6 @@ export const WelcomePage = () => {
             </p>
           </video>
 
-          {/* 用户可控的跳过按钮:只在播放阶段且出现卡顿/播放时间较长时显示 */}
-          {viewState === 'playing' && showSkipButton && !hasNavigatedRef.current && (
-            <button
-              type="button"
-              onClick={() => {
-                if (!hasNavigatedRef.current) {
-                  hasNavigatedRef.current = true;
-                  navigate('/begin');
-                }
-              }}
-              style={{
-                position: 'absolute',
-                bottom: '20px',
-                left: '50%',
-                transform: 'translateX(-50%)',
-                padding: '10px 20px',
-                background: 'rgba(0, 0, 0, 0.6)',
-                color: '#fff',
-                border: '1px solid rgba(255, 255, 255, 0.3)',
-                borderRadius: '20px',
-                cursor: 'pointer',
-                zIndex: 100,
-              }}
-            >
-              跳过动画,进入命名
-            </button>
-          )}
         </div>
       </div>
     </>

+ 42 - 57
frontend/src/utils/api.ts

@@ -3,6 +3,29 @@
  * 统一管理后端 API 调用
  */
 
+/**
+ * 获取 API Key(从环境变量读取)
+ */
+const getApiKey = (): string => {
+  return import.meta.env.VITE_API_CLIENT_KEY || '';
+};
+
+/**
+ * 获取统一的请求头(包含 API Key)
+ */
+const getApiHeaders = (): Record<string, string> => {
+  const headers: Record<string, string> = {
+    'Content-Type': 'application/json',
+  };
+  
+  const apiKey = getApiKey();
+  if (apiKey) {
+    headers['X-Client-Key'] = apiKey;
+  }
+  
+  return headers;
+};
+
 /**
  * 获取后端 API 基础 URL
  */
@@ -59,9 +82,7 @@ export const createUser = async (username: string = ''): Promise<CreateUserRespo
   try {
     const response = await fetch(url, {
       method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
+      headers: getApiHeaders(),
       body: JSON.stringify({ username }),
     });
 
@@ -128,9 +149,7 @@ export const getUserInfo = async (user_id: string): Promise<GetUserResponse> =>
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/users/${user_id}`, {
     method: 'GET',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
   });
 
   if (!response.ok) {
@@ -163,9 +182,7 @@ export const updateUsername = async (user_id: string, username: string): Promise
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/username`, {
     method: 'PUT',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
     body: JSON.stringify({ username }),
   });
 
@@ -191,9 +208,7 @@ export const updateBackgroundImageId = async (
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/background-image-id`, {
     method: 'PUT',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
     body: JSON.stringify({ background_image_id }),
   });
 
@@ -216,9 +231,7 @@ export const updateBotName = async (user_id: string, bot_name: string): Promise<
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/bot-name`, {
     method: 'PUT',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
     body: JSON.stringify({ bot_name }),
   });
 
@@ -260,9 +273,7 @@ export const getUserUIState = async (user_id: string): Promise<GetUserUIStateRes
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/ui-state`, {
     method: 'GET',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
   });
 
   if (!response.ok) {
@@ -288,9 +299,7 @@ export const updateUserUIState = async (
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/ui-state`, {
     method: 'PUT',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
     body: JSON.stringify(payload),
   });
 
@@ -335,9 +344,7 @@ export const registerWithPhone = async (
   }
   const response = await fetch(`${apiBaseUrl}/api/auth/register/phone`, {
     method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
     body: JSON.stringify(body),
   });
 
@@ -370,9 +377,7 @@ export const registerWithEmail = async (email: string, username: string = '', us
   }
   const response = await fetch(`${apiBaseUrl}/api/auth/register/email`, {
     method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
     body: JSON.stringify(body),
   });
 
@@ -401,9 +406,7 @@ export const loginWithWechat = async (openid: string, username: string = ''): Pr
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/auth/login/wechat`, {
     method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
     body: JSON.stringify({ openid, username }),
   });
 
@@ -432,9 +435,7 @@ export const loginWithQQ = async (openid: string, username: string = ''): Promis
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/auth/login/qq`, {
     method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
     body: JSON.stringify({ openid, username }),
   });
 
@@ -464,9 +465,7 @@ export const loginWithPhone = async (phone: string, area_code?: string): Promise
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/auth/login/phone`, {
     method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
     body: JSON.stringify({ phone, area_code }),
   });
 
@@ -495,9 +494,7 @@ export const loginWithEmail = async (email: string): Promise<LoginEmailResponse>
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/auth/login/email`, {
     method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
     body: JSON.stringify({ email }),
   });
 
@@ -529,9 +526,7 @@ export const sendCode = async (request: SendCodeRequest): Promise<SendCodeRespon
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/auth/send-code`, {
     method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
     body: JSON.stringify(request),
   });
 
@@ -563,9 +558,7 @@ export const verifyCode = async (request: VerifyCodeRequest): Promise<VerifyCode
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/auth/verify-code`, {
     method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
     body: JSON.stringify(request),
   });
 
@@ -619,9 +612,7 @@ export const getAvatarGifs = async (): Promise<AvatarGifConfigResponse> => {
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/avatar-gifs`, {
     method: 'GET',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
   });
 
   if (!response.ok) {
@@ -651,9 +642,7 @@ export const getShareImages = async (): Promise<GetShareImagesResponse> => {
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/share-images`, {
     method: 'GET',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
   });
 
   if (!response.ok) {
@@ -681,9 +670,7 @@ export const claimShareReward = async (user_id: string): Promise<ClaimShareRewar
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/claim-share-reward`, {
     method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
   });
 
   if (!response.ok) {
@@ -733,9 +720,7 @@ export const getBackgroundImages = async (): Promise<GetBackgroundImagesResponse
   const apiBaseUrl = getApiBaseUrl();
   const response = await fetch(`${apiBaseUrl}/api/background-images`, {
     method: 'GET',
-    headers: {
-      'Content-Type': 'application/json',
-    },
+    headers: getApiHeaders(),
   });
 
   if (!response.ok) {