172 lines
7.1 KiB
Python
172 lines
7.1 KiB
Python
"""nomadvip 项目支付接口:/nomadvip/*,固定 ZPAY"""
|
||
import logging
|
||
import random
|
||
import string
|
||
import time
|
||
|
||
from fastapi import APIRouter, Request, HTTPException
|
||
from fastapi.responses import RedirectResponse, JSONResponse
|
||
|
||
import requests
|
||
|
||
from ..config import BASE_URL, ZPAY_PID, ZPAY_KEY
|
||
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)
|
||
|
||
|
||
@router.post("/payh5")
|
||
async def nomadvip_payh5(item: dict):
|
||
"""nomadvip 专用:固定 ZPAY,表单提交支付"""
|
||
print("nomadvip/payh5 item:", item)
|
||
total_fee = int(item.get("total_fee", 8800))
|
||
total_fee_yuan = f"{total_fee / 100:.2f}"
|
||
user_id = item.get("user_id", "unknown")
|
||
pay_type = (item.get("type") or "vip").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, "VIP会员充值")
|
||
notify_url = f"{BASE_URL.rstrip('/')}/nomadvip/zpay_notify"
|
||
channel = item.get("channel", "alipay")
|
||
result = _nomadvip_zpay.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"),
|
||
)
|
||
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",
|
||
"submit_method": "POST",
|
||
}
|
||
|
||
|
||
@router.post("/payh5/redirect")
|
||
async def nomadvip_payh5_redirect(item: dict, request: Request):
|
||
"""nomadvip 专用:ZPAY mapi 接口"""
|
||
logging.info(f"nomadvip payh5/redirect 请求: item={item}")
|
||
try:
|
||
total_fee = float(item.get("total_fee", 8800))
|
||
except (TypeError, ValueError):
|
||
logging.warning(f"nomadvip payh5/redirect 无效 total_fee: {item.get('total_fee')}")
|
||
total_fee = 8800.0
|
||
total_fee_yuan = f"{total_fee / 100:.2f}"
|
||
user_id = item.get("user_id", "unknown")
|
||
pay_type = (item.get("type") or "vip").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, "VIP会员充值")
|
||
notify_url = f"{BASE_URL.rstrip('/')}/nomadvip/zpay_notify"
|
||
result = _nomadvip_zpay.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":
|
||
err_msg = result.get("msg", "ZPAY mapi 创建订单失败")
|
||
logging.error(f"nomadvip payh5/redirect ZPAY 创建订单失败: {err_msg}, result={result}")
|
||
raise HTTPException(status_code=500, detail=err_msg)
|
||
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)
|
||
logging.error(f"nomadvip payh5/redirect ZPAY 未返回支付链接: device={device}, channel={channel}, result keys={list(result.keys())}")
|
||
raise HTTPException(status_code=500, detail="ZPAY 未返回支付链接")
|
||
|
||
|
||
@router.get("/zpay_order_status")
|
||
async def nomadvip_zpay_order_status(out_trade_no: str):
|
||
"""nomadvip 专用:查询 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"nomadvip 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.get("/zpay_notify")
|
||
@router.post("/zpay_notify")
|
||
async def nomadvip_zpay_notify(request: Request):
|
||
"""nomadvip 专用: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"nomadvip ZPAY 异步通知: {data}")
|
||
order_id = data.get("out_trade_no", "")
|
||
if order_id in processed_orders:
|
||
logging.info(f"nomadvip 订单已处理,跳过: {order_id}")
|
||
return "success"
|
||
if not _nomadvip_zpay.verify_notify(data):
|
||
logging.error(f"nomadvip ZPAY 验签失败: {data}")
|
||
raise HTTPException(status_code=400, detail="sign error")
|
||
parsed = _nomadvip_zpay.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}")
|
||
return "success"
|
||
pay_price = parsed.get("pay_price") or data.get("money", "")
|
||
try:
|
||
handle_payment_success(order_id, pay_price, "nomadvip-zpay", 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 _nomadvip_zpay.success_response()
|