Files
gitlab-instance-0a899031_pa…/app/routers/_digital_base.py
2026-03-10 04:42:05 -05:00

214 lines
9.1 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.
"""
digital/cnomadcna/nomadlms 共享的支付逻辑
根据 prefix 生成对应 notify_path业务互不影响
"""
import logging
import random
import string
import time
import requests
from fastapi import APIRouter, Request, HTTPException
from ..config import BASE_URL, ZPAY_PID, ZPAY_KEY
from ..payment import get_payment_provider
from ..payment.zpay import ZPayProvider
from ..services import handle_payment_success, processed_orders
def create_digital_router(prefix: str, project_name: str) -> APIRouter:
"""
创建 digital/cnomadcna/nomadlms 的支付路由
prefix: 如 "/digital", "/cnomadcna", "/nomadlms"""legacy
project_name: 用于日志,如 "digital", "cnomadcna", "nomadlms", "legacy"
"""
router = APIRouter(prefix=prefix, tags=[project_name])
def _notify_path(path: str) -> str:
base = BASE_URL.rstrip("/")
if prefix:
return f"{base}{prefix}{path}"
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}"
user_id = item.get("user_id", "unknown")
pay_type = item.get("type", "unknown").lower()
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}"
type_map = {
"book": "购买书籍",
"meetup": "数字游民社区报名",
"video": "视频解锁",
"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,
total_fee_yuan=total_fee_yuan,
pay_type=channel,
notify_url=notify_url,
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": provider.name,
"submit_method": "POST",
}
@router.post("/payh5/redirect")
async def payh5_redirect(item: dict, request: Request):
"""ZPAY mapi 接口,返回 302 或二维码"""
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))
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")
forwarded = request.headers.get("x-forwarded-for")
if forwarded:
client_ip = forwarded.split(",")[0].strip()
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}"
type_map = {
"book": "购买书籍",
"meetup": "数字游民社区报名",
"video": "视频解锁",
"vip": "VIP会员充值",
}
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,
total_fee_yuan=total_fee_yuan,
pay_type=channel,
notify_url=notify_url,
return_url=return_url,
client_ip=client_ip,
device=device,
)
if result.get("status") != "ok":
raise HTTPException(status_code=500, detail=result.get("msg", "ZPAY mapi 创建订单失败"))
payurl = result.get("payurl")
payurl2 = result.get("payurl2")
img = result.get("img")
if device == "pc" and img:
return JSONResponse(status_code=200, content={
"status": "ok", "qrcode_mode": True, "order_id": order_id,
"img": img, "payurl": payurl, "return_url": return_url, "channel": channel,
})
if device == "wechat" and channel.lower() in ("wxpay", "wx", "wechat"):
return JSONResponse(status_code=200, content={"use_form": True, "order_id": order_id})
headers = {"X-Pay-Order-Id": order_id}
if device == "h5" and channel.lower() in ("wxpay", "wx", "wechat") and payurl2:
return RedirectResponse(url=payurl2, status_code=302, headers=headers)
if payurl:
return RedirectResponse(url=payurl, status_code=302, headers=headers)
raise HTTPException(status_code=500, detail="ZPAY 未返回支付链接")
@router.get("/zpay_order_status")
async def zpay_order_status(out_trade_no: str):
"""查询 ZPAY 订单支付状态"""
provider = get_payment_provider()
if provider.name != "zpay":
raise HTTPException(status_code=400, detail="仅 zpay 渠道支持")
url = f"https://zpayz.cn/api.php?act=order&pid={ZPAY_PID}&key={ZPAY_KEY}&out_trade_no={out_trade_no}"
try:
resp = requests.get(url, timeout=10)
result = resp.json() if resp.ok else {}
except Exception as e:
logging.warning(f"ZPAY 订单查询失败: {e}")
return {"paid": False, "error": str(e)}
try:
code = result.get("code", 0)
code = int(code) if isinstance(code, (int, float)) or str(code).isdigit() else 0
except (ValueError, TypeError):
code = 0
if code != 1:
return {"paid": False, "msg": result.get("msg", "查询失败")}
try:
status = int(result.get("status", 0))
except (ValueError, TypeError):
status = 0
return {"paid": status == 1, "status": status}
@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}")
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")
if not provider.verify_notify(data):
logging.error(f"{project_name} XorPay 验签失败: {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)
return provider.success_response()
@router.get("/zpay_notify")
@router.post("/zpay_notify")
async def zpay_notify(request: Request):
"""ZPAY 异步通知"""
if request.method == "GET":
data = dict(request.query_params)
else:
form_data = await request.form()
data = {k: v for k, v in form_data.items()}
logging.info(f"{project_name} ZPAY 异步通知: {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")
if not provider.verify_notify(data):
logging.error(f"{project_name} ZPAY 验签失败: {data}")
raise HTTPException(status_code=400, detail="sign error")
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)
return provider.success_response()
return router