's'
This commit is contained in:
131
app/main.py
131
app/main.py
@@ -15,13 +15,14 @@ from pydantic import BaseModel
|
||||
from pocketbase import PocketBase
|
||||
|
||||
from .config import (
|
||||
PAYMENT_PROVIDER,
|
||||
BASE_URL,
|
||||
PB_URL,
|
||||
PB_ADMIN_EMAIL,
|
||||
PB_ADMIN_PASSWORD,
|
||||
MEMOS_API,
|
||||
MEMOS_TOKEN,
|
||||
ZPAY_PID,
|
||||
ZPAY_KEY,
|
||||
)
|
||||
from .payment import get_payment_provider
|
||||
|
||||
@@ -220,15 +221,139 @@ async def payh5(item: dict):
|
||||
"xorpay_url": result.get("pay_url", ""),
|
||||
"provider": provider.name,
|
||||
}
|
||||
# ZPAY:确保所有参数为字符串,避免前端传参问题
|
||||
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": result.get("params", {}),
|
||||
"params": params_str,
|
||||
"pay_url": result.get("pay_url", ""),
|
||||
"provider": provider.name,
|
||||
**{k: v for k, v in result.items() if k not in ("status", "params", "pay_url", "provider")},
|
||||
"submit_method": "POST",
|
||||
}
|
||||
|
||||
|
||||
@app.post("/payh5/redirect")
|
||||
async def payh5_redirect(item: dict, request: Request):
|
||||
"""
|
||||
创建支付订单,使用 ZPAY API接口支付(mapi.php),返回 302 重定向。
|
||||
根据 device 区分:PC 跳转 payurl,H5/微信 跳转 payurl 或 payurl2(微信H5)。
|
||||
文档:https://z-pay.cn/doc.html#d3
|
||||
"""
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
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") # pc | h5 | wechat,微信内传 wechat 走收银台
|
||||
|
||||
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 = f"{BASE_URL.rstrip('/')}/zpay_notify"
|
||||
|
||||
from .payment.zpay import ZPayProvider
|
||||
|
||||
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")
|
||||
|
||||
# PC 端:payurl 为 wap 链接,在 PC 浏览器会显示错误页面。支付宝/微信均需展示二维码
|
||||
if device == "pc" and img:
|
||||
from fastapi.responses import JSONResponse
|
||||
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,
|
||||
},
|
||||
)
|
||||
# 微信内:payurl2 为 checkmweb,会提示「请外微信外打开」。改用收银台表单(submit.php)由 Z-Pay 识别 UA
|
||||
if device == "wechat" and channel.lower() in ("wxpay", "wx", "wechat"):
|
||||
from fastapi.responses import JSONResponse
|
||||
return JSONResponse(status_code=200, content={"use_form": True, "order_id": order_id})
|
||||
# 返回 302 时附带 order_id,供 digital 写入 cookie 用于轮询检测支付状态(微信 H5 可能不跳转)
|
||||
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 未返回支付链接")
|
||||
|
||||
|
||||
@app.get("/zpay_order_status")
|
||||
async def zpay_order_status(out_trade_no: str):
|
||||
"""
|
||||
查询 ZPAY 订单支付状态,用于 PC 二维码页轮询。
|
||||
文档:https://z-pay.cn/doc.html 查询单个订单
|
||||
"""
|
||||
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)}
|
||||
# ZPAY 错误时可能返回 code='error',需安全解析
|
||||
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}
|
||||
|
||||
|
||||
@app.post("/xorpay_notify")
|
||||
async def xorpay_notify(request: Request):
|
||||
"""XorPay 异步通知(POST form)"""
|
||||
|
||||
Reference in New Issue
Block a user