diff --git a/app/main.py b/app/main.py index b909e0d..e2eaf3f 100755 --- a/app/main.py +++ b/app/main.py @@ -108,7 +108,7 @@ async def root(): "status": "ok", "service": "payjsapi", "routes": { - "nomadvip": "/nomadvip/payh5, /nomadvip/payh5/redirect, /nomadvip/zpay_order_status, /nomadvip/zpay_notify", + "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, ...", diff --git a/app/routers/nomadvip.py b/app/routers/nomadvip.py index 4df916f..be1af23 100644 --- a/app/routers/nomadvip.py +++ b/app/routers/nomadvip.py @@ -1,4 +1,4 @@ -"""nomadvip 项目支付接口:/nomadvip/*,固定 ZPAY""" +"""nomadvip 项目支付接口:/nomadvip/*,支持 xorpay/zpay(PAYMENT_PROVIDER)""" import logging import random import string @@ -7,18 +7,23 @@ import time from fastapi import APIRouter, Request, HTTPException from fastapi.responses import RedirectResponse, JSONResponse -from ..config import BASE_URL, ZPAY_PID, ZPAY_KEY +from ..config import BASE_URL +from ..payment import get_payment_provider from ..payment.zpay import ZPayProvider from ..services import handle_payment_success, processed_orders router = APIRouter(prefix="/nomadvip", tags=["nomadvip"]) -_nomadvip_zpay = ZPayProvider(pid=ZPAY_PID, key=ZPAY_KEY) + + +def _get_notify_path(provider_name: str) -> str: + return f"{BASE_URL.rstrip('/')}/nomadvip/xorpay_notify" if provider_name == "xorpay" else f"{BASE_URL.rstrip('/')}/nomadvip/zpay_notify" @router.post("/payh5") async def nomadvip_payh5(item: dict): - """nomadvip 专用:固定 ZPAY,表单提交支付""" + """nomadvip 专用:创建支付订单,xorpay 直接返回收银台参数,zpay 返回表单参数""" print("nomadvip/payh5 item:", item) + provider = get_payment_provider() total_fee = int(item.get("total_fee", 8800)) total_fee_yuan = f"{total_fee / 100:.2f}" user_id = item.get("user_id", "unknown") @@ -33,9 +38,9 @@ async def nomadvip_payh5(item: dict): "vip": "VIP会员充值", } name = type_map.get(pay_type, "VIP会员充值") - notify_url = f"{BASE_URL.rstrip('/')}/nomadvip/zpay_notify" + notify_url = _get_notify_path(provider.name) channel = item.get("channel", "alipay") - result = _nomadvip_zpay.create_order( + result = provider.create_order( order_id=order_id, name=name, total_fee_yuan=total_fee_yuan, @@ -44,20 +49,30 @@ async def nomadvip_payh5(item: dict): return_url=item.get("return_url"), client_ip=item.get("client_ip"), ) + if provider.name == "xorpay": + return { + "status": "ok", + "xorpay_params": result.get("params", {}), + "xorpay_url": result.get("pay_url", ""), + "provider": provider.name, + } params = result.get("params", {}) params_str = {k: str(v) for k, v in params.items() if v is not None and str(v).strip() != ""} return { "status": "ok", "params": params_str, "pay_url": result.get("pay_url", ""), - "provider": "zpay", + "provider": provider.name, "submit_method": "POST", } @router.post("/payh5/redirect") async def nomadvip_payh5_redirect(item: dict, request: Request): - """nomadvip 专用:ZPAY mapi 接口""" + """nomadvip 专用:ZPAY mapi 接口(xorpay 无需 redirect,直接 payh5 即可)""" + provider = get_payment_provider() + if provider.name != "zpay": + raise HTTPException(status_code=400, detail="payh5/redirect 仅支持 zpay,xorpay 请直接使用 payh5") logging.info(f"nomadvip payh5/redirect 请求: item={item}") try: total_fee = float(item.get("total_fee", 8800)) @@ -80,7 +95,9 @@ async def nomadvip_payh5_redirect(item: dict, request: Request): type_map = {"book": "购买书籍", "meetup": "数字游民社区报名", "video": "视频解锁", "vip": "VIP会员充值"} name = type_map.get(pay_type, "VIP会员充值") notify_url = f"{BASE_URL.rstrip('/')}/nomadvip/zpay_notify" - result = _nomadvip_zpay.create_order_api( + if not isinstance(provider, ZPayProvider): + raise HTTPException(status_code=400, detail="payh5/redirect 仅支持 zpay") + result = provider.create_order_api( order_id=order_id, name=name, total_fee_yuan=total_fee_yuan, @@ -115,17 +132,73 @@ async def nomadvip_payh5_redirect(item: dict, request: Request): @router.get("/zpay_order_status") async def nomadvip_zpay_order_status(out_trade_no: str): - """nomadvip 专用:查询 ZPAY 订单支付状态""" - result = _nomadvip_zpay.query_order_status(out_trade_no, timeout=15) + """nomadvip 专用:查询 ZPAY 订单支付状态(xorpay 无此接口)""" + provider = get_payment_provider() + if not isinstance(provider, ZPayProvider): + raise HTTPException(status_code=400, detail="zpay_order_status 仅支持 zpay") + result = provider.query_order_status(out_trade_no, timeout=15) if result.get("error"): logging.warning(f"nomadvip ZPAY 订单查询失败: {result.get('error')}") return result +@router.get("/order_status") +async def nomadvip_order_status(order_id: str): + """nomadvip 通用:查询订单支付状态(xorpay 查 PocketBase,zpay 查 ZPAY)""" + provider = get_payment_provider() + if isinstance(provider, ZPayProvider): + result = provider.query_order_status(order_id, timeout=15) + if result.get("error"): + logging.warning(f"nomadvip ZPAY 订单查询失败: {result.get('error')}") + return result + # xorpay:查 PocketBase + try: + from ..services.pb import get_pb_client + + pb = get_pb_client() + records = pb.collection("payments").get_list(1, 1, {"filter": f'order_id = "{order_id}"'}) + return {"paid": len(records.items) > 0} + except Exception as e: + logging.warning(f"nomadvip PocketBase 查询失败: {e}") + return {"paid": False, "error": str(e)} + + +@router.post("/xorpay_notify") +async def nomadvip_xorpay_notify(request: Request): + """nomadvip 专用:XorPay 异步通知""" + form_data = await request.form() + data = {k: v for k, v in form_data.items()} + logging.info(f"nomadvip XorPay 异步通知: {data}") + order_id = data.get("order_id", "") + if order_id in processed_orders: + logging.info(f"nomadvip 订单已处理,跳过: {order_id}") + return "ok" + provider = get_payment_provider() + if provider.name != "xorpay": + raise HTTPException(status_code=400, detail="xorpay_notify only for xorpay") + if not provider.verify_notify(data): + logging.error(f"nomadvip XorPay 验签失败: {data}") + raise HTTPException(status_code=400, detail="sign error") + parsed = provider.parse_notify(data) + if parsed.get("trade_status") != "success": + return "ok" + pay_price = parsed.get("pay_price", data.get("pay_price", "")) + try: + handle_payment_success(order_id, pay_price, "nomadvip-xorpay", use_memos=True) + logging.info(f"nomadvip 业务处理完成: order_id={order_id}") + except Exception as e: + logging.error(f"nomadvip PocketBase/Memos 处理异常: {e}", exc_info=True) + raise + return provider.success_response() + + @router.get("/zpay_notify") @router.post("/zpay_notify") async def nomadvip_zpay_notify(request: Request): """nomadvip 专用:ZPAY 异步通知""" + provider = get_payment_provider() + if provider.name != "zpay": + raise HTTPException(status_code=400, detail="zpay_notify only for zpay") if request.method == "GET": data = dict(request.query_params) else: @@ -136,10 +209,10 @@ async def nomadvip_zpay_notify(request: Request): if order_id in processed_orders: logging.info(f"nomadvip 订单已处理,跳过: {order_id}") return "success" - if not _nomadvip_zpay.verify_notify(data): + if not provider.verify_notify(data): logging.error(f"nomadvip ZPAY 验签失败: {data}") raise HTTPException(status_code=400, detail="sign error") - parsed = _nomadvip_zpay.parse_notify(data) + parsed = provider.parse_notify(data) trade_status = parsed.get("trade_status") or data.get("trade_status", "") if trade_status != "success" and trade_status != "TRADE_SUCCESS": logging.info(f"nomadvip 非成功状态,跳过: trade_status={trade_status}") @@ -151,4 +224,4 @@ async def nomadvip_zpay_notify(request: Request): except Exception as e: logging.error(f"nomadvip PocketBase/Memos 处理异常: {e}", exc_info=True) raise - return _nomadvip_zpay.success_response() + return provider.success_response()