Files
gitlab-instance-0a899031_sa…/app/main.py
2026-03-16 20:08:32 -05:00

162 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
salon API 主入口
支付ZPAY/XorPay+ 报名、用户、小红书资料
路由:
- /salon/* 支付payh5、payh5/redirect、order_status、zpay_notify、xorpay_notify
- /api/salon/* 报名、check-user、ensure-user、complete-order、join、volunteers、xiaohongshu/profile
本地调试BASE_URL 需为 ZPAY 可访问地址(如 ngrok
线上部署BASE_URL=https://api.nomadyt.com必须为 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 salon_router, salon_meetup_router
app = FastAPI(
title="Salon API",
description="沙龙报名、支付、小红书资料",
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):
"""捕获未处理异常,记录日志后返回 500HTTPException 交由 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 "/salon" in path or "/api/salon" in path
# 启动时校验 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.nomadyt.com",
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 连通性(含 SSL、mapi 接口)"""
out = {"status": "ok", "service": "salonapi", "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"[salonapi] >>> {request.method} {path}\n")
resp = await call_next(request)
if _should_trace_request(path):
_safe_stderr_write(f"[salonapi] <<< {request.method} {path} {resp.status_code}\n")
return resp
# 挂载路由
app.include_router(salon_router)
app.include_router(salon_meetup_router)
@app.get("/")
async def root():
"""健康检查"""
return {
"status": "ok",
"service": "salonapi",
"routes": {
"salon": "/salon/payh5, /salon/payh5/redirect, /salon/order_status, /salon/zpay_notify, /salon/xorpay_notify",
"api": "/api/salon/check-user, /api/salon/ensure-user, /api/salon/complete-order, /api/salon/join, /api/salon/join/by-wechat, /api/salon/join/status, /api/salon/join/volunteers, /api/salon/xiaohongshu/profile",
},
}