197 lines
6.7 KiB
Python
Executable File
197 lines
6.7 KiB
Python
Executable File
"""
|
||
支付 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 sys
|
||
|
||
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,
|
||
chatbot_router,
|
||
)
|
||
from .routers.salon_meetup import router as salon_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=["*"],
|
||
)
|
||
|
||
|
||
class _SafeStderrProxy:
|
||
def write(self, message: str) -> int:
|
||
try:
|
||
return sys.stderr.write(message)
|
||
except (OSError, ValueError):
|
||
return 0
|
||
|
||
def flush(self) -> None:
|
||
try:
|
||
sys.stderr.flush()
|
||
except (OSError, ValueError):
|
||
pass
|
||
|
||
|
||
def _safe_log(level: int, message: str, *args, **kwargs) -> None:
|
||
try:
|
||
logging.log(level, message, *args, **kwargs)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
@app.exception_handler(Exception)
|
||
async def global_exception_handler(request: Request, exc: Exception):
|
||
"""捕获未处理异常,记录日志后返回 500(HTTPException 交由 FastAPI 默认处理)"""
|
||
if isinstance(exc, HTTPException):
|
||
raise exc
|
||
_safe_log(logging.ERROR, "未处理异常: %s - %s", request.url.path, exc, exc_info=True)
|
||
return JSONResponse(status_code=500, content={"detail": str(exc)})
|
||
|
||
logging.raiseExceptions = False
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||
handlers=[logging.StreamHandler(_SafeStderrProxy())],
|
||
force=True,
|
||
)
|
||
|
||
|
||
def _safe_stderr_write(message: str) -> None:
|
||
try:
|
||
sys.stderr.write(message)
|
||
sys.stderr.flush()
|
||
except (OSError, ValueError):
|
||
pass
|
||
|
||
|
||
def _should_trace_request(path: str) -> bool:
|
||
return any(
|
||
segment in path for segment in ("/nomadvip", "/digital", "/cnomadcna", "/nomadlms")
|
||
)
|
||
|
||
# 启动时校验 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()
|
||
# 预热 PocketBase 连接,减少首个业务请求冷启动延迟
|
||
try:
|
||
from .services.pb import get_pb_client
|
||
|
||
get_pb_client()
|
||
logging.info("PocketBase client warmup done")
|
||
except Exception as error:
|
||
logging.warning("PocketBase client warmup failed: %s", error)
|
||
|
||
|
||
@app.get("/health")
|
||
async def health(check_zpay: str = ""):
|
||
"""健康检查。check_zpay=1 时额外检测 zpayz.cn 连通性(含 SSL、mapi 接口)"""
|
||
out = {"status": "ok", "service": "payjsapi", "base_url": BASE_URL}
|
||
if str(check_zpay).lower() in ("1", "true", "yes"):
|
||
# 1) 检测 zpayz.cn 首页
|
||
try:
|
||
r = requests.get("https://zpayz.cn", timeout=5)
|
||
out["zpayz_reachable"] = r.status_code in (200, 301, 302, 404)
|
||
except requests.exceptions.SSLError as e:
|
||
out["zpayz_reachable"] = False
|
||
out["zpayz_error"] = f"SSL 错误: {e}(Ubuntu 请执行: apt install ca-certificates)"
|
||
except requests.RequestException as e:
|
||
out["zpayz_reachable"] = False
|
||
out["zpayz_error"] = str(e)
|
||
# 2) 检测 mapi 接口(实际支付调用地址)
|
||
if out.get("zpayz_reachable"):
|
||
try:
|
||
r2 = requests.post("https://zpayz.cn/mapi.php", data={"pid": "test"}, timeout=5)
|
||
out["zpayz_mapi_reachable"] = True
|
||
out["zpayz_mapi_status"] = r2.status_code
|
||
except requests.exceptions.SSLError as e:
|
||
out["zpayz_mapi_reachable"] = False
|
||
out["zpayz_mapi_error"] = f"SSL: {e}"
|
||
except requests.RequestException as e:
|
||
out["zpayz_mapi_reachable"] = False
|
||
out["zpayz_mapi_error"] = str(e)
|
||
return out
|
||
|
||
|
||
# 请求日志中间件:每个请求都打印到 stderr,确保终端可见
|
||
@app.middleware("http")
|
||
async def log_requests(request, call_next):
|
||
path = request.url.path
|
||
if _should_trace_request(path):
|
||
_safe_stderr_write(f"[payjsapi] >>> {request.method} {path}\n")
|
||
resp = await call_next(request)
|
||
if _should_trace_request(path):
|
||
_safe_stderr_write(f"[payjsapi] <<< {request.method} {path} {resp.status_code}\n")
|
||
return resp
|
||
|
||
# 挂载路由
|
||
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.include_router(salon_meetup_router)
|
||
app.include_router(chatbot_router)
|
||
|
||
|
||
@app.get("/")
|
||
async def root():
|
||
"""健康检查"""
|
||
return {
|
||
"status": "ok",
|
||
"service": "payjsapi",
|
||
"routes": {
|
||
"nomadvip": "/nomadvip/payh5, /nomadvip/payh5/redirect, /nomadvip/check_vip, /nomadvip/check_vip_by_email, /nomadvip/order_status, /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",
|
||
},
|
||
}
|