"""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") or "").strip() self.key = (os.getenv("ZPAY_KEY") or "").strip() if not self.pid or not self.key: raise RuntimeError("ZPAY_PID / ZPAY_KEY 未配置,无法发起支付") 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") or "").strip() self.secret = (os.getenv("XORPAY_SECRET") or "").strip() if not self.aid or not self.secret: raise RuntimeError("XORPAY_AID / XORPAY_SECRET 未配置,无法发起支付") 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()