zlwu před 11 měsíci
rodič
revize
f8ad0138c6

+ 4 - 7
backend/main.py

@@ -10,7 +10,6 @@ from contextlib import asynccontextmanager
 from config import settings, get_dashscope_domain
 from websocket_handler import WebSocketHandler
 from database import init_database, close_database
-import redis.asyncio as redis
 from services.redis_cache_service import RedisCacheService
 from services.user_service import user_service
 
@@ -31,18 +30,16 @@ async def lifespan(app: FastAPI):
     try:
         await init_database()
         
-        # 获取Redis客户端(已配置健康检查)
+        # 获取Redis客户端(已配置健康检查),用于验证连通性
         from database import db_manager
-        redis_client = await db_manager.get_redis()
+        await db_manager.get_redis()
         
-        # 创建Redis缓存服务
-        app.state.redis_cache = RedisCacheService(redis_client)
-        app.state.redis_client = redis_client
+        # 创建Redis缓存服务,按需获取最新连接
+        app.state.redis_cache = RedisCacheService(db_manager.get_redis)
         
     except Exception as e:
         logger.error(f"❌ 数据库初始化失败: {e}")
         app.state.redis_cache = None
-        app.state.redis_client = None
     
     # 🔥 启动HTTP连接保活任务(保持连接活跃,避免长时间不交互后响应变慢)
     try:

+ 15 - 5
backend/services/context_service.py

@@ -3,8 +3,9 @@
 """
 import json
 import asyncio
-from typing import List, Dict, Optional
+from typing import Awaitable, Callable, List, Dict, Optional
 import logging
+import redis.asyncio as aioredis
 from sqlalchemy import select
 
 logger = logging.getLogger(__name__)
@@ -22,19 +23,28 @@ class ContextService:
         """
         self.db = db_manager
         self.redis = None
+        self._redis_provider: Optional[Callable[[], Awaitable[aioredis.Redis]]] = None
         if db_manager:
             # 🔥 延迟获取Redis(在首次使用时)
-            pass
+            self._redis_provider = db_manager.get_redis
 
     async def set_db_manager(self, db_manager):
         """设置数据库管理器(用于延迟初始化)"""
         self.db = db_manager
-        self.redis = await db_manager.get_redis()
+        self._redis_provider = db_manager.get_redis
+        self.redis = None
     
     async def _ensure_redis(self):
         """确保Redis连接可用"""
-        if not self.redis and self.db:
-            self.redis = await self.db.get_redis()
+        if not self._redis_provider:
+            if not self.db:
+                raise RuntimeError("Redis 未初始化,缺少数据库管理器")
+            self._redis_provider = self.db.get_redis
+        redis_client = await self._redis_provider()
+        if not redis_client:
+            raise RuntimeError("Redis 客户端不可用")
+        self.redis = redis_client
+        return redis_client
     
     async def get_context_for_round(
         self, 

+ 17 - 6
backend/services/message_service.py

@@ -5,12 +5,13 @@ import uuid
 import json
 import asyncio
 from datetime import datetime
-from typing import Dict, Optional
+from typing import Awaitable, Callable, Dict, Optional
 import logging
 from sqlalchemy import select, update, func
 from sqlalchemy.orm import selectinload
 from database import db_manager
 from models_db import Message, User
+import redis.asyncio as aioredis
 
 logger = logging.getLogger(__name__)
 
@@ -27,19 +28,28 @@ class MessageService:
         """
         self.db = db_manager
         self.redis = None
+        self._redis_provider: Optional[Callable[[], Awaitable[aioredis.Redis]]] = None
         if db_manager:
             # 🔥 延迟获取Redis(在首次使用时)
-            pass
+            self._redis_provider = db_manager.get_redis
 
     async def set_db_manager(self, db_manager):
         """设置数据库管理器(用于延迟初始化)"""
         self.db = db_manager
-        self.redis = await db_manager.get_redis()
+        self._redis_provider = db_manager.get_redis
+        self.redis = None
     
     async def _ensure_redis(self):
         """确保Redis连接可用"""
-        if not self.redis and self.db:
-            self.redis = await self.db.get_redis()
+        if not self._redis_provider:
+            if not self.db:
+                raise RuntimeError("Redis 未初始化,缺少数据库管理器")
+            self._redis_provider = self.db.get_redis
+        redis_client = await self._redis_provider()
+        if not redis_client:
+            raise RuntimeError("Redis 客户端不可用")
+        self.redis = redis_client
+        return redis_client
     
     async def save_message(
         self,
@@ -466,8 +476,9 @@ class MessageService:
             当前轮次(从1开始)
         """
         try:
+            redis = await self._ensure_redis()
             meta_key = f"user:{user_id}:meta"
-            current_round = await self.redis.hget(meta_key, 'current_round')
+            current_round = await redis.hget(meta_key, 'current_round')
             
             if current_round:
                 return int(current_round)

+ 44 - 23
backend/services/redis_cache_service.py

@@ -4,9 +4,9 @@ Redis缓存管理服务
 """
 import json
 import time
-import asyncio
-from typing import Dict, List, Optional
+from typing import Awaitable, Callable, Dict, List, Optional
 import logging
+import redis.asyncio as aioredis
 
 logger = logging.getLogger(__name__)
 
@@ -14,14 +14,16 @@ logger = logging.getLogger(__name__)
 class RedisCacheService:
     """Redis缓存服务"""
     
-    def __init__(self, redis_client):
+    def __init__(self, redis_provider: Callable[[], Awaitable[aioredis.Redis]]):
         """
         初始化Redis缓存服务
         
         Args:
-            redis_client: redis.asyncio.Redis 实例
+            redis_provider: 一个可调用对象,返回 redis.asyncio.Redis 实例
         """
-        self.redis = redis_client
+        if not callable(redis_provider):
+            raise ValueError("redis_provider 必须可调用")
+        self._redis_provider = redis_provider
         
         # 缓存Key设计
         self.KEY_USER_CONTEXT = "context:{user_id}"           # 用户上下文
@@ -34,6 +36,13 @@ class RedisCacheService:
         self.TTL_MESSAGES = 86400     # 24小时
         self.TTL_SUMMARY = 604800     # 7天
     
+    async def _get_redis(self) -> aioredis.Redis:
+        """获取最新的Redis客户端,确保连接有效"""
+        redis_client = await self._redis_provider()
+        if not redis_client:
+            raise RuntimeError("Redis 客户端不可用")
+        return redis_client
+    
     async def get_context(self, user_id: str) -> Optional[Dict]:
         """
         从Redis获取用户上下文(零延迟)
@@ -47,8 +56,9 @@ class RedisCacheService:
             }
         """
         try:
+            redis = await self._get_redis()
             key = self.KEY_USER_CONTEXT.format(user_id=user_id)
-            cached = await self.redis.get(key)
+            cached = await redis.get(key)
             
             if cached:
                 context = json.loads(cached)
@@ -77,10 +87,11 @@ class RedisCacheService:
             ttl: 过期时间(秒),默认24小时
         """
         try:
+            redis = await self._get_redis()
             key = self.KEY_USER_CONTEXT.format(user_id=user_id)
             ttl = ttl or self.TTL_CONTEXT
             
-            await self.redis.setex(
+            await redis.setex(
                 key,
                 ttl,
                 json.dumps(context, ensure_ascii=False)
@@ -106,6 +117,7 @@ class RedisCacheService:
         使用Redis List结构,支持高效追加和范围查询
         """
         try:
+            redis = await self._get_redis()
             key = self.KEY_USER_MESSAGES.format(user_id=user_id)
             
             message = {
@@ -116,13 +128,13 @@ class RedisCacheService:
             }
             
             # 追加到List尾部
-            await self.redis.rpush(key, json.dumps(message, ensure_ascii=False))
+            await redis.rpush(key, json.dumps(message, ensure_ascii=False))
             
             # 设置过期时间
-            await self.redis.expire(key, self.TTL_MESSAGES)
+            await redis.expire(key, self.TTL_MESSAGES)
             
             # 限制List长度(最多保留200条消息)
-            await self.redis.ltrim(key, -200, -1)
+            await redis.ltrim(key, -200, -1)
             
             logger.debug(f"✅ Redis追加消息: user={user_id}, round={round_number}, role={role}")
             return True
@@ -144,10 +156,11 @@ class RedisCacheService:
             count: 消息数量(默认30条,即15轮)
         """
         try:
+            redis = await self._get_redis()
             key = self.KEY_USER_MESSAGES.format(user_id=user_id)
             
             # 获取最后N条消息
-            messages_raw = await self.redis.lrange(key, -count, -1)
+            messages_raw = await redis.lrange(key, -count, -1)
             
             messages = []
             for msg_bytes in messages_raw:
@@ -169,8 +182,9 @@ class RedisCacheService:
     async def get_current_round(self, user_id: str) -> int:
         """获取当前轮次"""
         try:
+            redis = await self._get_redis()
             key = self.KEY_USER_ROUND.format(user_id=user_id)
-            round_bytes = await self.redis.get(key)
+            round_bytes = await redis.get(key)
             
             if round_bytes:
                 round_str = round_bytes.decode('utf-8') if isinstance(round_bytes, bytes) else round_bytes
@@ -184,8 +198,9 @@ class RedisCacheService:
     async def set_current_round(self, user_id: str, round_number: int) -> bool:
         """设置当前轮次"""
         try:
+            redis = await self._get_redis()
             key = self.KEY_USER_ROUND.format(user_id=user_id)
-            await self.redis.setex(key, self.TTL_CONTEXT, str(round_number))
+            await redis.setex(key, self.TTL_CONTEXT, str(round_number))
             return True
         except Exception as e:
             logger.error(f"❌ Redis设置轮次失败: {e}")
@@ -194,14 +209,15 @@ class RedisCacheService:
     async def increment_round(self, user_id: str) -> int:
         """轮次自增"""
         try:
+            redis = await self._get_redis()
             key = self.KEY_USER_ROUND.format(user_id=user_id)
-            new_round = await self.redis.incr(key)
-            await self.redis.expire(key, self.TTL_CONTEXT)
+            new_round = await redis.incr(key)
+            await redis.expire(key, self.TTL_CONTEXT)
             
             # 同时更新 user:{user_id}:meta 中的 current_round(保持一致性)
             meta_key = f"user:{user_id}:meta"
-            await self.redis.hset(meta_key, 'current_round', new_round)
-            await self.redis.expire(meta_key, self.TTL_CONTEXT)
+            await redis.hset(meta_key, 'current_round', new_round)
+            await redis.expire(meta_key, self.TTL_CONTEXT)
             
             logger.debug(f"✅ Redis轮次自增: user={user_id}, new_round={new_round}")
             return new_round
@@ -213,8 +229,9 @@ class RedisCacheService:
     async def get_summary(self, user_id: str, end_round: int) -> Optional[str]:
         """获取历史总结"""
         try:
+            redis = await self._get_redis()
             key = self.KEY_USER_SUMMARY.format(user_id=user_id, end_round=end_round)
-            summary_bytes = await self.redis.get(key)
+            summary_bytes = await redis.get(key)
             
             if summary_bytes:
                 summary = summary_bytes.decode('utf-8') if isinstance(summary_bytes, bytes) else summary_bytes
@@ -235,8 +252,9 @@ class RedisCacheService:
     ) -> bool:
         """保存历史总结"""
         try:
+            redis = await self._get_redis()
             key = self.KEY_USER_SUMMARY.format(user_id=user_id, end_round=end_round)
-            await self.redis.setex(key, self.TTL_SUMMARY, summary)
+            await redis.setex(key, self.TTL_SUMMARY, summary)
             
             logger.info(f"✅ Redis保存总结: user={user_id}, end_round={end_round}, 长度={len(summary)}字")
             return True
@@ -394,8 +412,9 @@ class RedisCacheService:
     async def invalidate_context(self, user_id: str) -> bool:
         """清除用户上下文缓存(强制刷新)"""
         try:
+            redis = await self._get_redis()
             key = self.KEY_USER_CONTEXT.format(user_id=user_id)
-            await self.redis.delete(key)
+            await redis.delete(key)
             logger.info(f"🗑️ Redis清除缓存: user={user_id}")
             return True
         except Exception as e:
@@ -412,13 +431,14 @@ class RedisCacheService:
             current_round: 当前轮次
         """
         try:
+            redis = await self._get_redis()
             # 清空旧数据
             messages_key = self.KEY_USER_MESSAGES.format(user_id=user_id)
-            await self.redis.delete(messages_key)
+            await redis.delete(messages_key)
             
             # 批量写入消息
             if messages:
-                pipeline = self.redis.pipeline()
+                pipeline = redis.pipeline()
                 for msg in messages:
                     pipeline.rpush(
                         messages_key,
@@ -440,7 +460,8 @@ class RedisCacheService:
     async def health_check(self) -> bool:
         """健康检查"""
         try:
-            await self.redis.ping()
+            redis = await self._get_redis()
+            await redis.ping()
             return True
         except Exception as e:
             logger.error(f"❌ Redis健康检查失败: {e}")

+ 19 - 6
backend/services/summary_service.py

@@ -2,9 +2,10 @@
 对话总结服务(SQLAlchemy ORM 版本)
 """
 import uuid
-from typing import List, Dict
+from typing import Awaitable, Callable, List, Dict, Optional
 import logging
 from sqlalchemy import select
+import redis.asyncio as aioredis
 
 logger = logging.getLogger(__name__)
 
@@ -21,11 +22,21 @@ class SummaryService:
         """
         self.db = db_manager
         self.redis = None  # 🔥 延迟获取Redis
+        self._redis_provider: Optional[Callable[[], Awaitable[aioredis.Redis]]] = (
+            db_manager.get_redis if db_manager else None
+        )
     
     async def _ensure_redis(self):
         """确保Redis连接可用"""
-        if not self.redis and self.db:
-            self.redis = await self.db.get_redis()
+        if not self._redis_provider:
+            if not self.db:
+                raise RuntimeError("Redis 未初始化,缺少数据库管理器")
+            self._redis_provider = self.db.get_redis
+        redis_client = await self._redis_provider()
+        if not redis_client:
+            raise RuntimeError("Redis 客户端不可用")
+        self.redis = redis_client
+        return redis_client
     
     async def generate_summary(
         self,
@@ -328,8 +339,9 @@ class SummaryService:
             summary_text: 总结文本
         """
         try:
+            redis = await self._ensure_redis()
             cache_key = f"user:{user_id}:latest_summary"
-            await self.redis.setex(cache_key, 86400, summary_text)
+            await redis.setex(cache_key, 86400, summary_text)
             logger.debug(f"✅ 总结已缓存: user={user_id}")
         except Exception as e:
             logger.warning(f"⚠️ 缓存总结失败: user={user_id}, error={e}")
@@ -345,9 +357,10 @@ class SummaryService:
             总结文本或空字符串
         """
         try:
+            redis = await self._ensure_redis()
             # 先查Redis
             cache_key = f"user:{user_id}:latest_summary"
-            cached = await self.redis.get(cache_key)
+            cached = await redis.get(cache_key)
             
             if cached:
                 return cached
@@ -368,7 +381,7 @@ class SummaryService:
                 if summary:
                     summary_text = summary.summary_text
                 # 回填缓存
-                await self.redis.setex(cache_key, 86400, summary_text)
+                await redis.setex(cache_key, 86400, summary_text)
                 return summary_text
             
             return ""

+ 16 - 7
backend/文档/Redis框架选择说明.md

@@ -4,25 +4,34 @@
 
 ### 当前方案
 - **客户端库**: `redis[asyncio]` (官方异步 Redis 客户端)
-- **封装层**: `RedisCacheService` (自定义服务层)
-- **状态**: ✅ 已实现连接池、健康检查、自动重连
+- **连接管理**: `DatabaseManager` 统一维护连接池、健康检查、后台重连
+- **服务接入**: `RedisCacheService/MessageService/ContextService/SummaryService` 通过 `redis_provider` **按需获取最新连接**,避免复用已关闭的句柄
+- **状态**: ✅ 已实现连接池、健康检查、自动重连、连接刷新
 
 ### 代码示例
 ```python
-# database.py - 连接管理
+# database.py - 连接管理 & 健康检查
 self.redis_client = await aioredis.from_url(
     redis_url,
-    encoding="utf-8",
-    decode_responses=True,
     max_connections=settings.REDIS_MAX_CONNECTIONS,
     health_check_interval=30,
 )
 
-# redis_cache_service.py - 业务封装
+# services/redis_cache_service.py - 业务封装(关键:实时获取连接)
 class RedisCacheService:
+    def __init__(self, redis_provider: Callable[[], Awaitable[aioredis.Redis]]):
+        self._redis_provider = redis_provider
+
+    async def _get_redis(self) -> aioredis.Redis:
+        redis_client = await self._redis_provider()
+        if not redis_client:
+            raise RuntimeError("Redis 客户端不可用")
+        return redis_client
+
     async def get_context(self, user_id: str) -> Optional[Dict]:
+        redis = await self._get_redis()
         key = self.KEY_USER_CONTEXT.format(user_id=user_id)
-        cached = await self.redis.get(key)
+        cached = await redis.get(key)
         return json.loads(cached) if cached else None
 ```
 

+ 2 - 0
frontend/SSL证书配置.md

@@ -119,6 +119,8 @@ ssl/
 
 
 
+
+
 
 
 

binární
frontend/src/assets/02.GIF


+ 7 - 2
frontend/src/pages/ChatBotPage.tsx

@@ -15,6 +15,7 @@ import '@/styles/chatbot-fix.css';
 import avatarC from '@/assets/Avatar-C.svg';
 import treesImage from '@/assets/Group-3465183.svg';
 import animaZoneAvatar from '@/assets/Avatar-anima-zone.png';
+import animaZoneAvatarGif from '@/assets/02.GIF';
 import audioButton from '@/assets/audio-button.png';
 import { AudioWave } from '@/components/AudioWave';
 import microphoneIcon from '@/assets/microphone-icon.svg';
@@ -658,6 +659,10 @@ export const ChatBotPage = () => {
     remainingTimeMs !== null &&
     remainingTimeMs <= COUNTDOWN_THRESHOLD_MS;
 
+  const isAvatarAnimating = currentState === 'playing';
+  const currentAvatarSrc = isAvatarAnimating ? animaZoneAvatarGif : animaZoneAvatar;
+  const currentAvatarAlt = isAvatarAnimating ? 'AnimaZone Avatar Animated' : 'AnimaZone Avatar';
+
   return (
     <div 
       className={`chatbot-container relative w-full ${
@@ -726,8 +731,8 @@ export const ChatBotPage = () => {
 
         {/* AnimaZone 头像 - 设计稿:top 248px (树木175px + 73px), 尺寸 343×340, 左右居中 */}
         <img
-          src={animaZoneAvatar}
-          alt="AnimaZone Avatar"
+          src={currentAvatarSrc}
+          alt={currentAvatarAlt}
           className="chatbot-anima-zone-avatar absolute"
           style={{
             width: `${layout.mainAvatarWidth}px`,

+ 1 - 0
frontend/vite.config.ts

@@ -38,6 +38,7 @@ export default defineConfig({
       '@': path.resolve(__dirname, './src'),
     },
   },
+  assetsInclude: ['**/*.GIF'],
   server: {
     port: 3000,
     host: '0.0.0.0',  // 允许外部访问