tanlie 1 month ago
parent
commit
5348a49381

+ 3 - 25
FastApi/app/__init__.py

@@ -12,33 +12,11 @@ from fastapi import FastAPI, Request, HTTPException
 from fastapi.exceptions import RequestValidationError
 from fastapi.responses import JSONResponse
 
+from app.core.redis_lifespan_fixed import lifespan
 from app.mobile import register_mobile_routes
 from app.core.response import error_response
 
-# Configure logging: write to logs/app.log (rotating) and also to console. Avoid duplicate handlers.
-logger = logging.getLogger("app")
-if not logger.handlers:
-    # Determine logs directory relative to this file (project-root/app/../logs)
-    base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
-    log_dir = os.path.join(base_dir, "logs")
-    os.makedirs(log_dir, exist_ok=True)
-    log_file = os.path.join(log_dir, "app.log")
-
-    formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
-
-    # Rotating file handler
-    file_handler = RotatingFileHandler(log_file, maxBytes=10 * 1024 * 1024, backupCount=7, encoding="utf-8")
-    file_handler.setFormatter(formatter)
-    file_handler.setLevel(logging.INFO)
-    logger.addHandler(file_handler)
-
-    # Console handler
-    console_handler = logging.StreamHandler()
-    console_handler.setFormatter(formatter)
-    console_handler.setLevel(logging.INFO)
-    logger.addHandler(console_handler)
-
-    logger.setLevel(logging.INFO)
+from app.core.logger import logger
 
 
 def _clean_message(s: str) -> str:
@@ -49,7 +27,7 @@ def _clean_message(s: str) -> str:
 
 
 def create_app():
-    app = FastAPI()
+    app = FastAPI(lifespan=lifespan)
 
     # Attach a DB session to each request via middleware so handlers can use request.state.db
     from app.core.db import SessionLocal

+ 0 - 5
FastApi/app/admim/__init__.py

@@ -1,5 +0,0 @@
-"""
-__init__.py -
-auther: tanlie
-date: 2026/8/30
-"""

+ 46 - 0
FastApi/app/core/logger.py

@@ -0,0 +1,46 @@
+"""Centralized logger configuration for the application.
+
+Configures the 'app' logger with a rotating file handler and console handler.
+Importing `logger` from this module gives a configured logger instance.
+"""
+import logging
+import os
+from logging.handlers import RotatingFileHandler
+
+# Determine project base directory (app/..)
+base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+log_dir = os.path.join(base_dir, "logs")
+os.makedirs(log_dir, exist_ok=True)
+log_file = os.path.join(log_dir, "app.log")
+
+logger = logging.getLogger("app")
+# Avoid reconfiguration if imported multiple times
+if not logger.handlers:
+    formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
+
+    # Resolve log level from environment (default INFO)
+    log_level_name = os.getenv("LOG_LEVEL", "INFO").upper()
+    try:
+        log_level = getattr(logging, log_level_name)
+    except Exception:
+        log_level = logging.INFO
+
+    # Rotating file handler
+    file_handler = RotatingFileHandler(log_file, maxBytes=10 * 1024 * 1024, backupCount=7, encoding="utf-8")
+    file_handler.setFormatter(formatter)
+    file_handler.setLevel(log_level)
+    logger.addHandler(file_handler)
+
+    # Console handler
+    console_handler = logging.StreamHandler()
+    console_handler.setFormatter(formatter)
+    console_handler.setLevel(log_level)
+    logger.addHandler(console_handler)
+
+    logger.setLevel(log_level)
+
+# Expose a helper to get child loggers if needed
+def get_logger(name: str = None):
+    if name:
+        return logging.getLogger(f"app.{name}")
+    return logger

+ 67 - 0
FastApi/app/core/redis_lifespan.py

@@ -0,0 +1,67 @@
+"""Async lifespan helper to initialize an asyncio Redis client and attach to app.state.redis.
+This separates the async redis client lifecycle from synchronous redis_client/redis_manager modules.
+"""
+import os
+from contextlib import asynccontextmanager
+
+import redis.asyncio as aioredis
+import redis as redis_sync
+from fastapi import FastAPI
+import logging
+
+logger = logging.getLogger("app.redis")
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    redis_url = os.getenv("REDIS_URL")
+    redis_host = os.getenv("REDIS_HOST", "127.0.0.1")
+    redis_port = int(os.getenv("REDIS_PORT", "6379"))
+    redis_db = int(os.getenv("REDIS_DB", "0"))
+    redis_password = os.getenv("REDIS_PASSWORD", None)
+
+    if redis_url:
+        async_client = aioredis.from_url(redis_url, decode_responses=True)
+        sync_client = redis_sync.from_url(redis_url, decode_responses=True)
+    else:
+        client = redis.Redis(host=redis_host, port=redis_port, db=redis_db, password=redis_password, decode_responses=True)
+
+    try:
+        # Verify connectivity for both clients
+        await async_client.ping()
+        try:
+            sync_client.ping()
+        except Exception:
+            logger.warning("Sync Redis ping failed during startup", exc_info=True)
+
+        app.state.redis = async_client
+        app.state.redis_sync = sync_client
+        logger.info("Redis clients initialized and attached to app.state.redis (async) and app.state.redis_sync (sync) (host=%s port=%s db=%s)", redis_host, redis_port, redis_db)
+    except Exception as e:
+        logger.error("Failed to initialize Redis clients: %s", e, exc_info=True)
+        app.state.redis = None
+        app.state.redis_sync = None
+
+    try:
+        yield
+    finally:
+        try:
+                if getattr(app.state, "redis", None) is not None:
+                    await app.state.redis.close()
+                    logger.info("Async Redis client closed on shutdown")
+            except Exception:
+                logger.error("Error closing async Redis client", exc_info=True)
+
+            # Close sync client
+            try:
+                if getattr(app.state, "redis_sync", None) is not None:
+                    try:
+                        app.state.redis_sync.close()
+                    except Exception:
+                        try:
+                            app.state.redis_sync.connection_pool.disconnect()
+                        except Exception:
+                            pass
+                    logger.info("Sync Redis client closed on shutdown")
+            except Exception:
+                logger.error("Error closing sync Redis client", exc_info=True)

+ 73 - 0
FastApi/app/core/redis_lifespan_fixed.py

@@ -0,0 +1,73 @@
+"""Async lifespan helper to initialize both an asyncio Redis client and a synchronous Redis client
+and attach them to app.state.redis (async) and app.state.redis_sync (sync).
+This lets synchronous request handlers use the sync client while async handlers use the asyncio client.
+"""
+import os
+from contextlib import asynccontextmanager
+
+import redis.asyncio as aioredis
+import redis as redis_sync
+from fastapi import FastAPI
+import logging
+
+logger = logging.getLogger("app.redis")
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    redis_url = os.getenv("REDIS_URL")
+    redis_host = os.getenv("REDIS_HOST", "127.0.0.1")
+    redis_port = int(os.getenv("REDIS_PORT", "6379"))
+    redis_db = int(os.getenv("REDIS_DB", "0"))
+    redis_password = os.getenv("REDIS_PASSWORD", None)
+
+    # Create both async and sync clients
+    if redis_url:
+        async_client = aioredis.from_url(redis_url, decode_responses=True)
+        sync_client = redis_sync.from_url(redis_url, decode_responses=True)
+    else:
+        async_client = aioredis.Redis(host=redis_host, port=redis_port, db=redis_db, password=redis_password, decode_responses=True)
+        sync_client = redis_sync.Redis(host=redis_host, port=redis_port, db=redis_db, password=redis_password, decode_responses=True)
+
+    try:
+        # Verify connectivity for both clients
+        await async_client.ping()
+        try:
+            sync_client.ping()
+        except Exception:
+            # If sync ping fails, log but still keep async client if it's ok
+            logger.warning("Sync Redis ping failed during startup", exc_info=True)
+
+        app.state.redis = async_client
+        app.state.redis_sync = sync_client
+        logger.info("Redis clients initialized and attached to app.state.redis (async) and app.state.redis_sync (sync) (host=%s port=%s db=%s)", redis_host, redis_port, redis_db)
+    except Exception as e:
+        logger.error("Failed to initialize Redis clients: %s", e, exc_info=True)
+        app.state.redis = None
+        app.state.redis_sync = None
+
+    try:
+        yield
+    finally:
+        # Close async client
+        try:
+            if getattr(app.state, "redis", None) is not None:
+                await app.state.redis.close()
+                logger.info("Async Redis client closed on shutdown")
+        except Exception:
+            logger.error("Error closing async Redis client", exc_info=True)
+
+        # Close sync client
+        try:
+            if getattr(app.state, "redis_sync", None) is not None:
+                try:
+                    app.state.redis_sync.close()
+                except Exception:
+                    # Older redis-py may not have close(); disconnect pool as fallback
+                    try:
+                        app.state.redis_sync.connection_pool.disconnect()
+                    except Exception:
+                        pass
+                logger.info("Sync Redis client closed on shutdown")
+        except Exception:
+            logger.error("Error closing sync Redis client", exc_info=True)

+ 9 - 1
FastApi/app/mobile/views/user.py

@@ -17,7 +17,15 @@ logger = logging.getLogger("app")
 
 
 @api.get("/login/captcha", summary="获取验证码")
-def get_captcha(type: int = Query(default=1, description="验证码类型,1:登录验证码,2:注册验证码")):
+def get_captcha(request: Request, type: int = Query(default=1, description="验证码类型,1:登录验证码,2:注册验证码")):
+    # Use the synchronous Redis client attached as app.state.redis_sync by the lifespan
+    redis_client = getattr(request.app.state, "redis_sync", None)
+    if redis_client is None:
+        logger.warning("redis_sync not initialized")
+        return error_response(msg="Redis 未初始化", code=500)
+
+    # Synchronous set is safe in sync endpoint
+    redis_client.set("key", "value", ex=60)
     logger.info("获取验证码,type=%s", type)
     return success_response(data={"type": type}, msg="获取验证码成功")
 

+ 0 - 1
FastApi/main.py

@@ -4,7 +4,6 @@ python version api for wishing tree
 author: tanlie
 date: 2026/8/30
 """
-from http.cookiejar import debug
 
 from app import create_app