This commit is contained in:
eric
2026-03-16 20:08:32 -05:00
parent e9898ab372
commit dc0f1dd9ec
15 changed files with 273 additions and 1714 deletions

View File

@@ -18,7 +18,8 @@ from ..payment import (
resolve_channel,
)
from ..payment.zpay import ZPayProvider
from ..services import handle_payment_success, processed_orders
from ..payment.xorpay import XorPayProvider
from ..services import handle_payment_success, try_add_processed_order, remove_processed_order
def _safe_log(level: int, message: str, *args, **kwargs) -> None:
@@ -28,6 +29,45 @@ def _safe_log(level: int, message: str, *args, **kwargs) -> None:
pass
async def _poll_xorpay_until_paid(
order_id: str,
project_name: str,
site_id: str,
max_attempts: int = 45,
interval_sec: float = 2.0,
) -> None:
"""创建 XorPay 订单后后台轮询,支付成功即更新 DB不依赖 notify 回调"""
provider = get_payment_provider_by_name("xorpay")
if not isinstance(provider, XorPayProvider):
return
for attempt in range(max_attempts):
if attempt > 0:
await asyncio.sleep(interval_sec)
try:
result = await asyncio.to_thread(provider.query_order_status, order_id, 6)
if not result.get("paid"):
continue
pay_price = str(result.get("pay_price", "") or result.get("price", "") or "1")
if not try_add_processed_order(order_id):
return
try:
await asyncio.to_thread(
handle_payment_success,
order_id,
pay_price,
f"{project_name}-xorpay-poll",
use_memos=False,
site_id=site_id,
)
_safe_log(logging.INFO, "%s XorPay 轮询检测到支付 order_id=%s", project_name, order_id)
except Exception as e:
remove_processed_order(order_id)
_safe_log(logging.ERROR, "%s XorPay 轮询更新失败 order_id=%s: %s", project_name, order_id, e)
return
except Exception as e:
_safe_log(logging.WARNING, "%s XorPay 轮询异常 order_id=%s attempt=%s: %s", project_name, order_id, attempt + 1, e)
def create_digital_router(prefix: str, project_name: str, site_id: str | None = None) -> APIRouter:
"""
创建 digital/cnomadcna/nomadlms 的支付路由
@@ -77,6 +117,7 @@ def create_digital_router(prefix: str, project_name: str, site_id: str | None =
type_map = {
"book": "购买书籍",
"meetup": "数字游民社区报名",
"salon": "沙龙报名茶歇费",
"video": "视频解锁",
"vip": "VIP会员充值",
}
@@ -101,6 +142,8 @@ def create_digital_router(prefix: str, project_name: str, site_id: str | None =
notify_url,
return_url or "",
)
if provider.name == "xorpay" and site_id and pay_type in ("salon", "meetup"):
asyncio.create_task(_poll_xorpay_until_paid(order_id, project_name, site_id))
if provider.name == "xorpay":
return {
"status": "ok",
@@ -261,27 +304,39 @@ def create_digital_router(prefix: str, project_name: str, site_id: str | None =
@router.post("/xorpay_notify")
async def xorpay_notify(request: Request):
"""XorPay 异步通知"""
"""XorPay 异步通知:先返回 200 避免超时重试,后台异步更新 solanRed/site_vip"""
form_data = await request.form()
data = {k: v for k, v in form_data.items()}
_safe_log(logging.INFO, "%s XorPay 异步通知: %s", project_name, data)
order_id = data.get("order_id", "")
if order_id in processed_orders:
return "ok"
provider = get_payment_provider_by_name("xorpay")
if not try_add_processed_order(order_id):
return provider.success_response()
if not provider.verify_notify(data):
_safe_log(logging.ERROR, "%s XorPay 验签失败: %s", project_name, data)
raise HTTPException(status_code=400, detail="sign error")
parsed = provider.parse_notify(data)
if parsed.get("trade_status") != "success":
return "ok"
handle_payment_success(
order_id,
parsed.get("pay_price", data.get("pay_price", "")),
f"{project_name}-xorpay",
use_memos=False,
site_id=site_id,
)
return provider.success_response()
# 先返回 200后台异步更新 DB用户再次打开首页时 by-wechat 可直接查到 solanRed
pay_price = parsed.get("pay_price", data.get("pay_price", ""))
provider_name = f"{project_name}-xorpay"
def _do_update():
try:
handle_payment_success(
order_id,
pay_price,
provider_name,
use_memos=False,
site_id=site_id,
)
except Exception as e:
_safe_log(logging.ERROR, "%s XorPay 后台更新失败 order_id=%s: %s", project_name, order_id, e)
remove_processed_order(order_id)
asyncio.create_task(asyncio.to_thread(_do_update))
return provider.success_response()
@router.get("/zpay_notify")
@@ -295,7 +350,7 @@ def create_digital_router(prefix: str, project_name: str, site_id: str | None =
data = {k: v for k, v in form_data.items()}
_safe_log(logging.INFO, "%s ZPAY 异步通知: %s", project_name, data)
order_id = data.get("out_trade_no", "")
if order_id in processed_orders:
if not try_add_processed_order(order_id):
return "success"
provider = get_payment_provider_by_name("zpay")
if not provider.verify_notify(data):
@@ -304,13 +359,18 @@ def create_digital_router(prefix: str, project_name: str, site_id: str | None =
parsed = provider.parse_notify(data)
if parsed.get("trade_status") != "success":
return "success"
handle_payment_success(
order_id,
parsed.get("pay_price", data.get("money", "")),
f"{project_name}-zpay",
use_memos=False,
site_id=site_id,
)
try:
await asyncio.to_thread(
handle_payment_success,
order_id,
parsed.get("pay_price", data.get("money", "")),
f"{project_name}-zpay",
use_memos=False,
site_id=site_id,
)
except Exception:
remove_processed_order(order_id)
raise
return provider.success_response()
return router