""" 支付 API 主入口 支持多支付渠道(XorPay、ZPAY),通过 PAYMENT_PROVIDER 环境变量切换 路由模块化,按项目拆分: - nomadvip: /nomadvip/* - 固定 ZPAY - digital: /digital/* - 使用 PAYMENT_PROVIDER - cnomadcna: /cnomadcna/* - 使用 PAYMENT_PROVIDER - nomadlms: /nomadlms/* - 使用 PAYMENT_PROVIDER - legacy: /payh5、/zpay_notify 等(向后兼容) - common: /submit_meetup_application、/create_user 本地调试:BASE_URL 需为 ZPAY 可访问地址(如 ngrok) 线上部署:BASE_URL=https://api.hackrobot.cn(必须为 ZPAY 可公网访问的地址) """ import logging import requests from fastapi import FastAPI, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from .config import BASE_URL from .routers import ( nomadvip_router, digital_router, cnomadcna_router, nomadlms_router, legacy_router, common_router, meetup_router, ) app = FastAPI( title="PayJS API", description="nomadvip / digital / cnomadcna / nomadlms 支付接口", version="2.0", ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=False, allow_methods=["*"], allow_headers=["*"], ) @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): """捕获未处理异常,记录日志后返回 500(HTTPException 交由 FastAPI 默认处理)""" if isinstance(exc, HTTPException): raise exc logging.error(f"未处理异常: {request.url.path} - {exc}", exc_info=True) return JSONResponse(status_code=500, content={"detail": str(exc)}) # 日志(仅控制台,不写文件,避免 git 本地/线上不一致) logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(message)s", handlers=[logging.StreamHandler()], ) # 启动时校验 BASE_URL(线上部署必须为公网可访问地址,否则 ZPAY 无法回调) def _check_base_url(): url = (BASE_URL or "").strip().lower() if not url: logging.warning("BASE_URL 未设置,将使用默认值。线上部署请务必设置 BASE_URL 为公网地址") return if "localhost" in url or "127.0.0.1" in url or url.startswith("http://192.168.") or url.startswith("http://10."): logging.warning( "BASE_URL=%s 疑似内网地址,ZPAY 无法回调!线上部署请设置 BASE_URL 为公网地址,如 https://api.hackrobot.cn", BASE_URL, ) @app.on_event("startup") async def startup_event(): _check_base_url() @app.get("/health") async def health(check_zpay: str = ""): """健康检查。check_zpay=1 时额外检测 zpayz.cn 连通性""" out = {"status": "ok", "service": "payjsapi", "base_url": BASE_URL} if str(check_zpay).lower() in ("1", "true", "yes"): try: r = requests.get("https://zpayz.cn", timeout=5) out["zpayz_reachable"] = r.status_code in (200, 301, 302, 404) except Exception as e: out["zpayz_reachable"] = False out["zpayz_error"] = str(e) return out # 挂载路由 app.include_router(nomadvip_router) app.include_router(digital_router) app.include_router(cnomadcna_router) app.include_router(nomadlms_router) app.include_router(legacy_router) app.include_router(common_router) app.include_router(meetup_router) @app.get("/") async def root(): """健康检查""" return { "status": "ok", "service": "payjsapi", "routes": { "nomadvip": "/nomadvip/payh5, /nomadvip/payh5/redirect, /nomadvip/zpay_order_status, /nomadvip/zpay_notify, /nomadvip/xorpay_notify", "digital": "/digital/payh5, /digital/payh5/redirect, /digital/zpay_order_status, /digital/zpay_notify, /digital/xorpay_notify", "cnomadcna": "/cnomadcna/payh5, ...", "nomadlms": "/nomadlms/payh5, ...", "legacy": "/payh5, /payh5/redirect, /zpay_notify, /xorpay_notify, /zpay_order_status", "common": "/submit_meetup_application, /create_user", }, }