;s
This commit is contained in:
269
backend/payment.py
Normal file
269
backend/payment.py
Normal file
@@ -0,0 +1,269 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from .settings import get_settings
|
||||
|
||||
|
||||
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:
|
||||
settings = get_settings()
|
||||
self.pid = settings.zpay_pid
|
||||
self.key = settings.zpay_key
|
||||
self.submit_url = settings.zpay_submit_url
|
||||
self.mapi_url = settings.zpay_mapi_url
|
||||
self.query_url = settings.zpay_query_url
|
||||
|
||||
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", ""))
|
||||
|
||||
def parse_notify(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"order_id": str(data.get("out_trade_no", "")),
|
||||
"trade_no": str(data.get("trade_no", "")),
|
||||
"pay_price": str(data.get("money", "")),
|
||||
"trade_status": "success" if data.get("trade_status") == "TRADE_SUCCESS" else "pending",
|
||||
}
|
||||
|
||||
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)}
|
||||
|
||||
def success_response(self) -> str:
|
||||
return "success"
|
||||
|
||||
|
||||
class XorPayProvider:
|
||||
name = "xorpay"
|
||||
|
||||
def __init__(self) -> None:
|
||||
settings = get_settings()
|
||||
self.aid = settings.xorpay_aid
|
||||
self.secret = settings.xorpay_secret
|
||||
self.cashier_url = settings.xorpay_cashier_url.rstrip("/")
|
||||
self.query_url = settings.xorpay_query_url.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", ""))
|
||||
|
||||
def parse_notify(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"order_id": str(data.get("order_id", "")),
|
||||
"trade_no": str(data.get("aoid", "")),
|
||||
"pay_price": str(data.get("pay_price", "")),
|
||||
"trade_status": "success",
|
||||
}
|
||||
|
||||
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 success_response(self) -> str:
|
||||
return "ok"
|
||||
|
||||
|
||||
def provider_for(device: str = "pc", requested: str = "", user_agent: str = "") -> ZPayProvider | XorPayProvider:
|
||||
req = requested.strip().lower()
|
||||
if req == "xorpay":
|
||||
return XorPayProvider()
|
||||
if req == "zpay":
|
||||
return ZPayProvider()
|
||||
if device.lower() == "wechat" or "micromessenger" in user_agent.lower():
|
||||
return XorPayProvider()
|
||||
return ZPayProvider()
|
||||
Reference in New Issue
Block a user