""" __init__.py - auther: tanlie date: 2026/8/30 """ import logging import os from logging.handlers import RotatingFileHandler import traceback import re from fastapi import FastAPI, Request, HTTPException from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse 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) def _clean_message(s: str) -> str: """Normalize exception messages by removing wrapper prefixes like "ValueError,".""" if not s: return s return re.sub(r'^\s*(?:ValueError|Value\s*error)[:,]?\s*', '', str(s), flags=re.I) def create_app(): app = FastAPI() register_mobile_routes(app) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): # Build a friendly, JSON-serializable summary of validation errors errors = [] for e in exc.errors(): loc = e.get("loc", []) # remove leading location indicators like 'body', 'query', 'path', 'header' if loc and len(loc) > 0 and str(loc[0]) in ("body", "query", "path", "header"): loc_parts = loc[1:] else: loc_parts = loc # join remaining location parts into a dotted path for readability loc_str = ".".join([str(p) for p in loc_parts if p is not None]) if loc_parts else "" cleaned_msg = _clean_message(e.get("msg", "")) errors.append({"field": loc_str, "message": cleaned_msg}) # Log validation details for debugging logger.info("Validation error for %s: %s", request.url.path, errors) return JSONResponse( status_code=422, content=error_response(msg="参数校验失败", code=422, data=errors), ) @app.exception_handler(HTTPException) async def http_exception_handler(request: Request, exc: HTTPException): # Use HTTPException's status_code and detail detail = exc.detail if exc.detail is not None else "" logger.warning("HTTPException for %s: %s", request.url.path, detail) return JSONResponse( status_code=getattr(exc, "status_code", 500), content=error_response(msg=str(detail), code=getattr(exc, "status_code", 500)), ) @app.exception_handler(Exception) async def generic_exception_handler(request: Request, exc: Exception): # Map common ValueError to a friendlier 400 parameter error if isinstance(exc, ValueError): # Log with stack for debugging tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) raw_msg = str(exc) or "" cleaned_msg = _clean_message(raw_msg) logger.warning("ValueError on %s: %s\n%s", request.url.path, cleaned_msg, tb) return JSONResponse( status_code=400, content=error_response(msg=cleaned_msg or "参数错误", code=400), ) # Generic fallback to avoid leaking internal stack traces # Log full stack trace for internal debugging tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) logger.error("Unhandled exception on %s: %s\n%s", request.url.path, str(exc), tb) return JSONResponse( status_code=500, content=error_response(msg="服务器内部错误", code=500), ) return app