nomadweb/backend/app/services/payment_providers.py
eric ff7b721acb feat: NomadCNA community layer, social matching, payments, and ebook
Port meetups, discussions, gigs, dating/chat, VIP pay (ZPay/XorPay), MiroTalk live, digital academy, and ebook reader. Add persistent community_store, git-first deploy docs, and env template for production secrets.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 09:38:13 -05:00

238 lines
7.4 KiB
Python

"""ZPay / XorPay providers — ported from NomadCNA backend/payment.py."""
from __future__ import annotations
import hashlib
import json
import os
from typing import Any
import httpx
def _md5(value: str) -> str:
return hashlib.md5(value.encode("utf-8")).hexdigest().lower()
def _is_internal_ip(ip: str | None) -> bool:
if not ip:
return True
value = ip.strip().lower()
if value in {"127.0.0.1", "localhost", "::1"}:
return True
if value.startswith("10.") or value.startswith("192.168."):
return True
if value.startswith("172."):
parts = value.split(".")
if len(parts) > 1 and parts[1].isdigit():
return 16 <= int(parts[1]) <= 31
return False
def resolve_channel(channel: str | None) -> str:
return "wxpay" if str(channel or "").lower() in {"", "wx", "wechat", "wxpay"} else "alipay"
class ZPayProvider:
name = "zpay"
def __init__(self) -> None:
self.pid = os.getenv("ZPAY_PID", "2025121809351743")
self.key = os.getenv("ZPAY_KEY", "tpEi7wWIWI2kXiYVTpIG6j7it0mjVW89")
self.submit_url = os.getenv("ZPAY_SUBMIT_URL", "https://zpayz.cn/submit.php")
self.mapi_url = os.getenv("ZPAY_MAPI_URL", "https://zpayz.cn/mapi.php")
self.query_url = os.getenv("ZPAY_QUERY_URL", "https://zpayz.cn/api.php")
def sign(self, params: dict[str, Any]) -> str:
filtered = {
key: value
for key, value in params.items()
if key not in {"sign", "sign_type"} and value is not None and str(value) != ""
}
raw = "&".join(f"{key}={filtered[key]}" for key in sorted(filtered)) + self.key
return _md5(raw)
def create_order(
self,
*,
order_id: str,
name: str,
amount_yuan: str,
channel: str,
notify_url: str,
return_url: str,
client_ip: str = "",
) -> dict[str, Any]:
params: dict[str, Any] = {
"pid": self.pid,
"type": resolve_channel(channel),
"out_trade_no": order_id,
"notify_url": notify_url,
"return_url": return_url,
"name": name,
"money": amount_yuan,
"sign_type": "MD5",
}
if client_ip and not _is_internal_ip(client_ip):
params["clientip"] = client_ip
params["sign"] = self.sign(params)
return {
"status": "ok",
"provider": self.name,
"params": params,
"pay_url": self.submit_url,
"submit_method": "POST",
}
async def create_order_api(
self,
*,
order_id: str,
name: str,
amount_yuan: str,
channel: str,
notify_url: str,
return_url: str,
client_ip: str = "",
device: str = "pc",
) -> dict[str, Any]:
params: dict[str, Any] = {
"pid": self.pid,
"type": resolve_channel(channel),
"out_trade_no": order_id,
"notify_url": notify_url,
"return_url": return_url,
"name": name,
"money": amount_yuan,
"device": device,
"sign_type": "MD5",
}
if client_ip and not _is_internal_ip(client_ip):
params["clientip"] = client_ip
params["sign"] = self.sign(params)
try:
async with httpx.AsyncClient(timeout=15.0, trust_env=False) as client:
res = await client.post(self.mapi_url, data=params)
except Exception as exc:
return {"status": "error", "provider": self.name, "msg": str(exc)}
result: Any
try:
result = res.json()
except Exception:
result = {}
if isinstance(result, str):
try:
result = json.loads(result)
except Exception:
result = {}
if not isinstance(result, dict):
result = {}
msg = result.get("msg")
if isinstance(msg, str) and msg.strip().startswith("{"):
try:
inner = json.loads(msg)
if isinstance(inner, dict):
result = inner
except Exception:
pass
try:
code = int(float(result.get("code", 0)))
except (TypeError, ValueError):
code = 0
if code == 1:
return {
"status": "ok",
"provider": self.name,
"payurl": result.get("payurl"),
"payurl2": result.get("payurl2"),
"qrcode": result.get("qrcode"),
"img": result.get("img"),
"trade_no": result.get("trade_no"),
}
return {"status": "error", "provider": self.name, "msg": result.get("msg") or res.text[:500]}
def verify_notify(self, data: dict[str, Any]) -> bool:
return self.sign(data) == str(data.get("sign", ""))
async def query_order_status(self, order_id: str, timeout: float = 8.0) -> dict[str, Any]:
try:
async with httpx.AsyncClient(timeout=timeout, trust_env=False) as client:
res = await client.get(
self.query_url,
params={"act": "order", "pid": self.pid, "key": self.key, "out_trade_no": order_id},
)
data = res.json() if res.is_success else {}
except Exception as exc:
return {"paid": False, "error": str(exc)}
try:
code = int(float(data.get("code", 0)))
status = int(float(data.get("status", 0)))
except (TypeError, ValueError):
return {"paid": False, "msg": data.get("msg", "查询失败")}
pay_price = data.get("money") or data.get("pay_price") or ""
return {"paid": code == 1 and status == 1, "status": status, "pay_price": str(pay_price)}
class XorPayProvider:
name = "xorpay"
def __init__(self) -> None:
self.aid = os.getenv("XORPAY_AID", "8220")
self.secret = os.getenv("XORPAY_SECRET", "afcacd99570945f88de62624aaa3578e")
self.cashier_url = os.getenv("XORPAY_CASHIER_URL", "https://xorpay.com/api/cashier").rstrip("/")
self.query_url = os.getenv("XORPAY_QUERY_URL", "https://xorpay.com/api/query2").rstrip("/")
def create_order(self, *, order_id: str, name: str, amount_yuan: str, notify_url: str) -> dict[str, Any]:
params = {
"name": name,
"pay_type": "jsapi",
"price": amount_yuan,
"order_id": order_id,
"notify_url": notify_url,
}
params["sign"] = _md5(
params["name"] + params["pay_type"] + params["price"] + params["order_id"] + params["notify_url"] + self.secret
)
return {"status": "ok", "provider": self.name, "params": params, "pay_url": f"{self.cashier_url}/{self.aid}"}
def verify_notify(self, data: dict[str, Any]) -> bool:
raw = (
str(data.get("aoid", ""))
+ str(data.get("order_id", ""))
+ str(data.get("pay_price", ""))
+ str(data.get("pay_time", ""))
+ self.secret
)
return _md5(raw) == str(data.get("sign", ""))
async def query_order_status(self, order_id: str, timeout: float = 8.0) -> dict[str, Any]:
sign = _md5(order_id + self.secret)
try:
async with httpx.AsyncClient(timeout=timeout, trust_env=False) as client:
res = await client.get(f"{self.query_url}/{self.aid}", params={"order_id": order_id, "sign": sign})
data = res.json() if res.is_success else {}
status = str(data.get("status", "")).lower()
return {
"paid": status in {"payed", "success"},
"status": status,
"pay_price": data.get("pay_price") or data.get("price") or "",
}
except Exception as exc:
return {"paid": False, "error": str(exc)}
def provider_for(device: str = "pc", requested: str = "", user_agent: str = "") -> ZPayProvider | XorPayProvider:
req = requested.strip().lower()
default = os.getenv("PAYMENT_PROVIDER", "zpay").lower()
if req == "xorpay":
return XorPayProvider()
if req == "zpay":
return ZPayProvider()
if default == "xorpay":
return XorPayProvider()
if device.lower() == "wechat" or "micromessenger" in user_agent.lower():
return XorPayProvider()
return ZPayProvider()