__init__.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """
  2. __init__.py -
  3. auther: tanlie
  4. date: 2026/8/30
  5. """
  6. import logging
  7. import os
  8. from logging.handlers import RotatingFileHandler
  9. import traceback
  10. import re
  11. from fastapi import FastAPI, Request, HTTPException
  12. from fastapi.exceptions import RequestValidationError
  13. from fastapi.responses import JSONResponse
  14. from app.mobile import register_mobile_routes
  15. from app.core.response import error_response
  16. # Configure logging: write to logs/app.log (rotating) and also to console. Avoid duplicate handlers.
  17. logger = logging.getLogger("app")
  18. if not logger.handlers:
  19. # Determine logs directory relative to this file (project-root/app/../logs)
  20. base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
  21. log_dir = os.path.join(base_dir, "logs")
  22. os.makedirs(log_dir, exist_ok=True)
  23. log_file = os.path.join(log_dir, "app.log")
  24. formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
  25. # Rotating file handler
  26. file_handler = RotatingFileHandler(log_file, maxBytes=10 * 1024 * 1024, backupCount=7, encoding="utf-8")
  27. file_handler.setFormatter(formatter)
  28. file_handler.setLevel(logging.INFO)
  29. logger.addHandler(file_handler)
  30. # Console handler
  31. console_handler = logging.StreamHandler()
  32. console_handler.setFormatter(formatter)
  33. console_handler.setLevel(logging.INFO)
  34. logger.addHandler(console_handler)
  35. logger.setLevel(logging.INFO)
  36. def _clean_message(s: str) -> str:
  37. """Normalize exception messages by removing wrapper prefixes like "ValueError,"."""
  38. if not s:
  39. return s
  40. return re.sub(r'^\s*(?:ValueError|Value\s*error)[:,]?\s*', '', str(s), flags=re.I)
  41. def create_app():
  42. app = FastAPI()
  43. # Attach a DB session to each request via middleware so handlers can use request.state.db
  44. from app.core.db import SessionLocal
  45. @app.middleware("http")
  46. async def db_session_middleware(request: Request, call_next):
  47. # Provide a request-scoped SQLAlchemy session
  48. request.state.db = SessionLocal()
  49. try:
  50. response = await call_next(request)
  51. return response
  52. except Exception:
  53. # If an unhandled exception occurred, rollback any pending transaction
  54. try:
  55. request.state.db.rollback()
  56. except Exception:
  57. pass
  58. raise
  59. finally:
  60. request.state.db.close()
  61. register_mobile_routes(app)
  62. @app.exception_handler(RequestValidationError)
  63. async def validation_exception_handler(request: Request, exc: RequestValidationError):
  64. # Build a friendly, JSON-serializable summary of validation errors
  65. errors = []
  66. for e in exc.errors():
  67. loc = e.get("loc", [])
  68. # remove leading location indicators like 'body', 'query', 'path', 'header'
  69. if loc and len(loc) > 0 and str(loc[0]) in ("body", "query", "path", "header"):
  70. loc_parts = loc[1:]
  71. else:
  72. loc_parts = loc
  73. # join remaining location parts into a dotted path for readability
  74. loc_str = ".".join([str(p) for p in loc_parts if p is not None]) if loc_parts else ""
  75. cleaned_msg = _clean_message(e.get("msg", ""))
  76. errors.append({"field": loc_str, "message": cleaned_msg})
  77. # Log validation details for debugging
  78. logger.info("Validation error for %s: %s", request.url.path, errors)
  79. return JSONResponse(
  80. status_code=422,
  81. content=error_response(msg="参数校验失败", code=422, data=errors),
  82. )
  83. @app.exception_handler(HTTPException)
  84. async def http_exception_handler(request: Request, exc: HTTPException):
  85. # Use HTTPException's status_code and detail
  86. detail = exc.detail if exc.detail is not None else ""
  87. logger.warning("HTTPException for %s: %s", request.url.path, detail)
  88. return JSONResponse(
  89. status_code=getattr(exc, "status_code", 500),
  90. content=error_response(msg=str(detail), code=getattr(exc, "status_code", 500)),
  91. )
  92. @app.exception_handler(Exception)
  93. async def generic_exception_handler(request: Request, exc: Exception):
  94. # Map common ValueError to a friendlier 400 parameter error
  95. if isinstance(exc, ValueError):
  96. # Log with stack for debugging
  97. tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
  98. raw_msg = str(exc) or ""
  99. cleaned_msg = _clean_message(raw_msg)
  100. logger.warning("ValueError on %s: %s\n%s", request.url.path, cleaned_msg, tb)
  101. return JSONResponse(
  102. status_code=400,
  103. content=error_response(msg=cleaned_msg or "参数错误", code=400),
  104. )
  105. # Generic fallback to avoid leaking internal stack traces
  106. # Log full stack trace for internal debugging
  107. tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
  108. logger.error("Unhandled exception on %s: %s\n%s", request.url.path, str(exc), tb)
  109. return JSONResponse(
  110. status_code=500,
  111. content=error_response(msg="服务器内部错误", code=500),
  112. )
  113. return app