This commit is contained in:
eric
2026-03-13 05:15:00 -05:00
parent 90378502cf
commit 53cacfac46
24 changed files with 1059 additions and 141 deletions

View File

@@ -10,11 +10,23 @@ import time
from fastapi import APIRouter, Request, HTTPException
from ..config import BASE_URL
from ..payment import get_payment_provider
from ..payment import (
get_payment_provider,
get_payment_provider_by_name,
resolve_provider,
resolve_channel,
)
from ..payment.zpay import ZPayProvider
from ..services import handle_payment_success, processed_orders
def _safe_log(level: int, message: str, *args, **kwargs) -> None:
try:
logging.log(level, message, *args, **kwargs)
except Exception:
pass
def create_digital_router(prefix: str, project_name: str, site_id: str | None = None) -> APIRouter:
"""
创建 digital/cnomadcna/nomadlms 的支付路由
@@ -31,12 +43,29 @@ def create_digital_router(prefix: str, project_name: str, site_id: str | None =
return f"{base}{path}"
@router.post("/payh5")
async def payh5(item: dict):
"""创建 H5 支付订单"""
print(f"{project_name}/payh5 item:", item)
total_fee_yuan = f"{item['total_fee'] / 100:.2f}"
async def payh5(item: dict, request: Request):
"""创建 H5 支付订单。默认微信支付PC/手机用 zpay微信内用 xorpay支付宝暂不提供"""
req_provider = (item.get("provider") or "").strip().lower() or None
device = (item.get("device") or "pc").lower()
ua = (request.headers.get("user-agent") or "").strip()
provider = resolve_provider(device, req_provider, ua)
channel = resolve_channel(item.get("channel"))
user_id = item.get("user_id", "unknown")
pay_type = item.get("type", "unknown").lower()
pay_type = (item.get("type") or "unknown").lower()
_safe_log(
logging.INFO,
"[payjsapi] %s/payh5 user_id=%s type=%s provider=%s channel=%s",
project_name,
user_id,
pay_type,
provider.name,
channel,
)
try:
total_fee = int(item.get("total_fee", 80))
except (TypeError, ValueError):
total_fee = 80
total_fee_yuan = f"{total_fee / 100:.2f}"
random_suffix = "".join(random.choices(string.hexdigits.lower(), k=8))
timestamp = int(time.time())
order_id = f"{user_id}_{pay_type}_order_{timestamp}_{random_suffix}"
@@ -47,10 +76,8 @@ def create_digital_router(prefix: str, project_name: str, site_id: str | None =
"vip": "VIP会员充值",
}
name = type_map.get(pay_type, "商品支付")
provider = get_payment_provider()
notify_path = "/xorpay_notify" if provider.name == "xorpay" else "/zpay_notify"
notify_url = _notify_path(notify_path)
channel = item.get("channel", "alipay")
result = provider.create_order(
order_id=order_id,
name=name,
@@ -60,9 +87,19 @@ def create_digital_router(prefix: str, project_name: str, site_id: str | None =
return_url=item.get("return_url"),
client_ip=item.get("client_ip"),
)
_safe_log(
logging.INFO,
"[payjsapi] %s/payh5 created order_id=%s provider=%s notify_url=%s return_url=%s",
project_name,
order_id,
provider.name,
notify_url,
item.get("return_url") or "",
)
if provider.name == "xorpay":
return {
"status": "ok",
"order_id": order_id,
"xorpay_params": result.get("params", {}),
"xorpay_url": result.get("pay_url", ""),
"provider": provider.name,
@@ -79,17 +116,27 @@ def create_digital_router(prefix: str, project_name: str, site_id: str | None =
@router.post("/payh5/redirect")
async def payh5_redirect(item: dict, request: Request):
"""ZPAY mapi 接口,返回 302 或二维码"""
"""ZPAY mapi 接口,返回 302 或二维码。PC/手机用 zpay默认微信支付支付宝暂不提供"""
from fastapi.responses import RedirectResponse, JSONResponse
provider = get_payment_provider()
if provider.name != "zpay":
raise HTTPException(status_code=400, detail="payh5/redirect 仅支持 zpay 渠道")
total_fee = float(item.get("total_fee", 80))
provider = get_payment_provider_by_name("zpay")
channel = resolve_channel(item.get("channel"))
_safe_log(
logging.INFO,
"[payjsapi] %s/payh5/redirect user_id=%s type=%s device=%s channel=%s",
project_name,
item.get("user_id", ""),
item.get("type", ""),
item.get("device", "pc"),
channel,
)
try:
total_fee = float(item.get("total_fee", 80))
except (TypeError, ValueError):
total_fee = 80.0
total_fee_yuan = f"{total_fee / 100:.2f}"
user_id = item.get("user_id", "unknown")
pay_type = item.get("type", "unknown").lower()
channel = item.get("channel", "alipay")
return_url = item.get("return_url")
device = item.get("device", "pc")
client_ip = item.get("client_ip") or (request.client.host if request.client else "127.0.0.1")
@@ -107,8 +154,6 @@ def create_digital_router(prefix: str, project_name: str, site_id: str | None =
}
name = type_map.get(pay_type, "商品支付")
notify_url = _notify_path("/zpay_notify")
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,
@@ -121,6 +166,14 @@ def create_digital_router(prefix: str, project_name: str, site_id: str | None =
)
if result.get("status") != "ok":
raise HTTPException(status_code=500, detail=result.get("msg", "ZPAY mapi 创建订单失败"))
_safe_log(
logging.INFO,
"[payjsapi] %s/payh5/redirect created order_id=%s device=%s channel=%s",
project_name,
order_id,
device,
channel,
)
payurl = result.get("payurl")
payurl2 = result.get("payurl2")
img = result.get("img")
@@ -146,23 +199,66 @@ def create_digital_router(prefix: str, project_name: str, site_id: str | None =
raise HTTPException(status_code=400, detail="仅 zpay 渠道支持")
result = provider.query_order_status(out_trade_no, timeout=15)
if result.get("error"):
logging.warning(f"{project_name} ZPAY 订单查询失败: {result.get('error')}")
_safe_log(logging.WARNING, "%s ZPAY 订单查询失败: %s", project_name, result.get("error"))
return result
@router.get("/order_status")
async def order_status(order_id: str = "", out_trade_no: str = ""):
order_id = (order_id or out_trade_no or "").strip()
if not order_id:
return {"paid": False, "error": "missing order_id"}
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}"'}
)
if records.items:
return {"paid": True, "source": "pocketbase"}
except Exception as error:
_safe_log(logging.WARNING, "%s order_status PocketBase failed: %s", project_name, error)
try:
xorpay = get_payment_provider_by_name("xorpay")
xorpay_result = xorpay.query_order_status(order_id, timeout=8)
if xorpay_result.get("paid"):
return {
"paid": True,
"source": "xorpay",
"status": xorpay_result.get("status", ""),
}
except Exception as error:
_safe_log(logging.WARNING, "%s order_status XorPay failed: %s", project_name, error)
try:
zpay = get_payment_provider_by_name("zpay")
if isinstance(zpay, ZPayProvider):
zpay_result = zpay.query_order_status(order_id, timeout=8)
if zpay_result.get("paid"):
return {
"paid": True,
"source": "zpay",
"status": zpay_result.get("status", ""),
}
except Exception as error:
_safe_log(logging.WARNING, "%s order_status ZPAY failed: %s", project_name, error)
return {"paid": False}
@router.post("/xorpay_notify")
async def xorpay_notify(request: Request):
"""XorPay 异步通知"""
form_data = await request.form()
data = {k: v for k, v in form_data.items()}
logging.info(f"{project_name} XorPay 异步通知: {data}")
_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()
if provider.name != "xorpay":
raise HTTPException(status_code=400, detail="xorpay_notify only for xorpay")
provider = get_payment_provider_by_name("xorpay")
if not provider.verify_notify(data):
logging.error(f"{project_name} XorPay 验签失败: {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":
@@ -185,15 +281,13 @@ def create_digital_router(prefix: str, project_name: str, site_id: str | None =
else:
form_data = await request.form()
data = {k: v for k, v in form_data.items()}
logging.info(f"{project_name} ZPAY 异步通知: {data}")
_safe_log(logging.INFO, "%s ZPAY 异步通知: %s", project_name, data)
order_id = data.get("out_trade_no", "")
if order_id in processed_orders:
return "success"
provider = get_payment_provider()
if provider.name != "zpay":
raise HTTPException(status_code=400, detail="zpay_notify only for zpay")
provider = get_payment_provider_by_name("zpay")
if not provider.verify_notify(data):
logging.error(f"{project_name} ZPAY 验签失败: {data}")
_safe_log(logging.ERROR, "%s ZPAY 验签失败: %s", project_name, data)
raise HTTPException(status_code=400, detail="sign error")
parsed = provider.parse_notify(data)
if parsed.get("trade_status") != "success":