"""FastAPI 入口。 通过 `uvicorn app.main:app --reload` 启动。 """ from __future__ import annotations import logging from collections.abc import AsyncIterator from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from app.api.v1.ad import router as ad_router from app.api.v1.auth import router as auth_router from app.api.v1.compare import router as compare_router from app.api.v1.compare_milestone import router as compare_milestone_router from app.api.v1.compare_record import router as compare_record_router from app.api.v1.coupon import router as coupon_router from app.api.v1.cps_redirect import router as cps_redirect_router from app.api.internal.price import router as internal_price_router from app.api.internal.store import router as internal_store_router from app.api.v1.feedback import router as feedback_router from app.api.v1.invite import router as invite_router from app.api.v1.meituan import router as meituan_router from app.api.v1.order import router as order_router from app.api.v1.platform import router as platform_router from app.api.v1.report import router as report_router from app.api.v1.savings import router as savings_router from app.api.v1.signin import router as signin_router from app.api.v1.tasks import router as tasks_router from app.api.v1.user import router as user_router from app.api.v1.wallet import router as wallet_router from app.api.v1.wxpay import router as wxpay_router from app.core.config import settings from app.core.logging import setup_logging from app.core.withdraw_reconcile_worker import ( start_withdraw_reconcile_worker, stop_withdraw_reconcile_worker, ) setup_logging(debug=settings.APP_DEBUG) logger = logging.getLogger("shagua.main") @asynccontextmanager async def lifespan(_: FastAPI) -> AsyncIterator[None]: # 提示而非强制建表:生产用 alembic upgrade head,本地 dev 也建议先跑一次 migration。 # 这里只 log,不自动 create_all,避免生产环境意外建出过时 schema。 logger.info( "started env=%s debug=%s db=%s", settings.APP_ENV, settings.APP_DEBUG, settings.DATABASE_URL.split("://", 1)[0], ) reconcile_task = start_withdraw_reconcile_worker() try: yield finally: await stop_withdraw_reconcile_worker(reconcile_task) logger.info("shutting down") app = FastAPI( title=settings.APP_NAME, version="0.1.0", docs_url="/docs" if not settings.is_prod else None, redoc_url="/redoc" if not settings.is_prod else None, lifespan=lifespan, ) if settings.cors_origins_list: app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins_list, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/health", tags=["meta"]) def health() -> dict[str, str]: return {"status": "ok"} app.include_router(auth_router) app.include_router(user_router) app.include_router(feedback_router) app.include_router(invite_router) app.include_router(coupon_router) app.include_router(compare_router) app.include_router(compare_record_router) app.include_router(compare_milestone_router) app.include_router(meituan_router) app.include_router(wallet_router) app.include_router(wxpay_router) app.include_router(signin_router) app.include_router(tasks_router) app.include_router(savings_router) app.include_router(ad_router) app.include_router(order_router) app.include_router(report_router) # 内部(server→server)端点:pricebot 上报价格观测 / 店铺映射,靠共享密钥头校验,不对客户端开放。 app.include_router(internal_price_router) app.include_router(internal_store_router) app.include_router(platform_router) # CPS 群发短链跳转 /c/{code}(公网无鉴权:记点击 → 302 跳美团) app.include_router(cps_redirect_router) # 用户上传文件(头像)静态服务。生产可改由 nginx 直接 serve MEDIA_ROOT。 _media_root = Path(settings.MEDIA_ROOT) _media_root.mkdir(parents=True, exist_ok=True) app.mount( settings.MEDIA_URL_PREFIX, StaticFiles(directory=str(_media_root)), name="media", )