's'
This commit is contained in:
16
app/payment/__init__.py
Normal file
16
app/payment/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
支付模块
|
||||
支持多支付渠道,可通过配置切换
|
||||
"""
|
||||
from .base import PaymentProvider
|
||||
from .xorpay import XorPayProvider
|
||||
from .zpay import ZPayProvider
|
||||
from .factory import get_payment_provider, reset_provider
|
||||
|
||||
__all__ = [
|
||||
"PaymentProvider",
|
||||
"XorPayProvider",
|
||||
"ZPayProvider",
|
||||
"get_payment_provider",
|
||||
"reset_provider",
|
||||
]
|
||||
BIN
app/payment/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/payment/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/payment/__pycache__/base.cpython-312.pyc
Normal file
BIN
app/payment/__pycache__/base.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/payment/__pycache__/factory.cpython-312.pyc
Normal file
BIN
app/payment/__pycache__/factory.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/payment/__pycache__/xorpay.cpython-312.pyc
Normal file
BIN
app/payment/__pycache__/xorpay.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/payment/__pycache__/zpay.cpython-312.pyc
Normal file
BIN
app/payment/__pycache__/zpay.cpython-312.pyc
Normal file
Binary file not shown.
59
app/payment/base.py
Normal file
59
app/payment/base.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
支付渠道抽象基类
|
||||
定义统一的支付接口,便于切换不同支付渠道(XorPay、ZPAY 等)
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class PaymentProvider(ABC):
|
||||
"""支付渠道抽象基类"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""支付渠道名称"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_order(
|
||||
self,
|
||||
*,
|
||||
order_id: str,
|
||||
name: str,
|
||||
total_fee_yuan: str,
|
||||
pay_type: str,
|
||||
notify_url: str,
|
||||
return_url: Optional[str] = None,
|
||||
client_ip: Optional[str] = None,
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
创建支付订单
|
||||
:return: 包含支付跳转信息等,格式由各渠道自定义
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def verify_notify(self, data: Dict[str, Any]) -> bool:
|
||||
"""验证异步通知签名"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def parse_notify(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
解析异步通知,提取统一字段
|
||||
:return: {
|
||||
"order_id": str, # 商户订单号
|
||||
"trade_no": str, # 渠道订单号
|
||||
"pay_price": str, # 实付金额(元)
|
||||
"pay_time": str, # 支付时间
|
||||
"trade_status": str, # 支付状态
|
||||
}
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def success_response(self) -> str:
|
||||
"""异步通知成功时需返回的字符串(如 'ok' 或 'success')"""
|
||||
pass
|
||||
39
app/payment/factory.py
Normal file
39
app/payment/factory.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
支付渠道工厂
|
||||
根据配置返回对应的支付 Provider
|
||||
"""
|
||||
from ..config import (
|
||||
PAYMENT_PROVIDER,
|
||||
XORPAY_AID,
|
||||
XORPAY_SECRET,
|
||||
XORPAY_CASHIER_URL,
|
||||
ZPAY_PID,
|
||||
ZPAY_KEY,
|
||||
)
|
||||
from .base import PaymentProvider
|
||||
from .xorpay import XorPayProvider
|
||||
from .zpay import ZPayProvider
|
||||
|
||||
_provider_instance: PaymentProvider | None = None
|
||||
|
||||
|
||||
def get_payment_provider() -> PaymentProvider:
|
||||
"""获取当前配置的支付渠道实例(单例)"""
|
||||
global _provider_instance
|
||||
if _provider_instance is not None:
|
||||
return _provider_instance
|
||||
if PAYMENT_PROVIDER == "zpay":
|
||||
_provider_instance = ZPayProvider(pid=ZPAY_PID, key=ZPAY_KEY)
|
||||
else:
|
||||
_provider_instance = XorPayProvider(
|
||||
aid=XORPAY_AID,
|
||||
secret=XORPAY_SECRET,
|
||||
cashier_url=XORPAY_CASHIER_URL,
|
||||
)
|
||||
return _provider_instance
|
||||
|
||||
|
||||
def reset_provider():
|
||||
"""重置 Provider(用于测试或切换渠道)"""
|
||||
global _provider_instance
|
||||
_provider_instance = None
|
||||
82
app/payment/xorpay.py
Normal file
82
app/payment/xorpay.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
XorPay 支付渠道实现
|
||||
文档: https://xorpay.com
|
||||
"""
|
||||
import hashlib
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from .base import PaymentProvider
|
||||
|
||||
|
||||
class XorPayProvider(PaymentProvider):
|
||||
"""XorPay 支付"""
|
||||
|
||||
def __init__(self, aid: str, secret: str, cashier_url: str = "https://xorpay.com/api/cashier"):
|
||||
self.aid = aid
|
||||
self.secret = secret
|
||||
self.cashier_url = cashier_url.rstrip("/")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "xorpay"
|
||||
|
||||
def _sign(self, name: str, pay_type: str, price: str, order_id: str, notify_url: str) -> str:
|
||||
raw = name + pay_type + price + order_id + notify_url + self.secret
|
||||
return hashlib.md5(raw.encode("utf-8")).hexdigest().lower()
|
||||
|
||||
def create_order(
|
||||
self,
|
||||
*,
|
||||
order_id: str,
|
||||
name: str,
|
||||
total_fee_yuan: str,
|
||||
pay_type: str,
|
||||
notify_url: str,
|
||||
return_url: Optional[str] = None,
|
||||
client_ip: Optional[str] = None,
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
# XorPay 使用 jsapi 表示 H5/JSAPI 支付
|
||||
channel_pay_type = "jsapi"
|
||||
params = {
|
||||
"name": name,
|
||||
"pay_type": channel_pay_type,
|
||||
"price": total_fee_yuan,
|
||||
"order_id": order_id,
|
||||
"notify_url": notify_url,
|
||||
}
|
||||
params["sign"] = self._sign(
|
||||
params["name"],
|
||||
params["pay_type"],
|
||||
params["price"],
|
||||
params["order_id"],
|
||||
params["notify_url"],
|
||||
)
|
||||
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:
|
||||
received_sign = data.get("sign", "")
|
||||
aoid = data.get("aoid", "")
|
||||
order_id = data.get("order_id", "")
|
||||
pay_price = data.get("pay_price", "")
|
||||
pay_time = data.get("pay_time", "")
|
||||
raw = aoid + order_id + pay_price + pay_time + self.secret
|
||||
calculated = hashlib.md5(raw.encode("utf-8")).hexdigest().lower()
|
||||
return calculated == received_sign
|
||||
|
||||
def parse_notify(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
"order_id": data.get("order_id", ""),
|
||||
"trade_no": data.get("aoid", ""),
|
||||
"pay_price": data.get("pay_price", ""),
|
||||
"pay_time": data.get("pay_time", ""),
|
||||
"trade_status": "success", # XorPay 验签通过即视为成功
|
||||
}
|
||||
|
||||
def success_response(self) -> str:
|
||||
return "ok"
|
||||
153
app/payment/zpay.py
Normal file
153
app/payment/zpay.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
ZPAY 支付渠道实现
|
||||
文档: https://z-pay.cn/doc.html
|
||||
支持:页面跳转支付、API 接口支付、异步通知验签
|
||||
"""
|
||||
import hashlib
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from .base import PaymentProvider
|
||||
|
||||
|
||||
class ZPayProvider(PaymentProvider):
|
||||
"""ZPAY 支付(易支付兼容接口)"""
|
||||
|
||||
# API 地址
|
||||
SUBMIT_URL = "https://zpayz.cn/submit.php" # 页面跳转
|
||||
MAPI_URL = "https://zpayz.cn/mapi.php" # API 接口
|
||||
|
||||
def __init__(self, pid: str, key: str):
|
||||
self.pid = pid
|
||||
self.key = key
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "zpay"
|
||||
|
||||
def _md5_sign(self, params: Dict[str, Any], exclude: Optional[set] = None) -> str:
|
||||
"""
|
||||
ZPAY MD5 签名算法:
|
||||
1. 参数按 ASCII 排序,sign/sign_type/空值不参与
|
||||
2. 拼接为 a=b&c=d
|
||||
3. sign = md5(拼接串 + KEY),小写
|
||||
"""
|
||||
exclude = exclude or {"sign", "sign_type"}
|
||||
filtered = {k: v for k, v in params.items() if k not in exclude and v is not None and v != ""}
|
||||
sorted_keys = sorted(filtered.keys())
|
||||
parts = [f"{k}={filtered[k]}" for k in sorted_keys]
|
||||
raw = "&".join(parts) + self.key
|
||||
return hashlib.md5(raw.encode("utf-8")).hexdigest().lower()
|
||||
|
||||
def create_order(
|
||||
self,
|
||||
*,
|
||||
order_id: str,
|
||||
name: str,
|
||||
total_fee_yuan: str,
|
||||
pay_type: str,
|
||||
notify_url: str,
|
||||
return_url: Optional[str] = None,
|
||||
client_ip: Optional[str] = None,
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
# ZPAY: alipay / wxpay(pay_type 可为业务类型或支付方式,此处按支付方式解析)
|
||||
channel_type = "wxpay" if str(pay_type).lower() in ("wx", "wechat", "wxpay") else "alipay"
|
||||
params = {
|
||||
"pid": self.pid,
|
||||
"type": channel_type,
|
||||
"out_trade_no": order_id,
|
||||
"notify_url": notify_url,
|
||||
"name": name,
|
||||
"money": total_fee_yuan,
|
||||
"return_url": return_url or notify_url,
|
||||
}
|
||||
if client_ip:
|
||||
params["clientip"] = client_ip
|
||||
if extra and extra.get("param"):
|
||||
params["param"] = str(extra["param"])
|
||||
params["sign_type"] = "MD5"
|
||||
params["sign"] = self._md5_sign(params)
|
||||
|
||||
# 页面跳转方式:返回 form 提交 URL 和参数
|
||||
return {
|
||||
"status": "ok",
|
||||
"provider": self.name,
|
||||
"params": params,
|
||||
"pay_url": self.SUBMIT_URL,
|
||||
"submit_method": "POST",
|
||||
}
|
||||
|
||||
def create_order_api(
|
||||
self,
|
||||
*,
|
||||
order_id: str,
|
||||
name: str,
|
||||
total_fee_yuan: str,
|
||||
pay_type: str,
|
||||
notify_url: str,
|
||||
return_url: Optional[str] = None,
|
||||
client_ip: str = "127.0.0.1",
|
||||
device: str = "pc",
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
ZPAY API 接口支付(返回 payurl/qrcode 等)
|
||||
适用于需要二维码或 H5 跳转的场景
|
||||
"""
|
||||
channel_type = "wxpay" if pay_type.lower() in ("wx", "wechat", "wxpay") else "alipay"
|
||||
params = {
|
||||
"pid": self.pid,
|
||||
"type": channel_type,
|
||||
"out_trade_no": order_id,
|
||||
"notify_url": notify_url,
|
||||
"name": name,
|
||||
"money": total_fee_yuan,
|
||||
"clientip": client_ip or "127.0.0.1",
|
||||
"device": device,
|
||||
}
|
||||
if return_url:
|
||||
params["return_url"] = return_url
|
||||
if extra and extra.get("param"):
|
||||
params["param"] = str(extra["param"])
|
||||
params["sign_type"] = "MD5"
|
||||
params["sign"] = self._md5_sign(params)
|
||||
|
||||
resp = requests.post(self.MAPI_URL, data=params, timeout=15)
|
||||
result = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
|
||||
if isinstance(result, dict) and result.get("code") == 1:
|
||||
return {
|
||||
"status": "ok",
|
||||
"provider": self.name,
|
||||
"payurl": result.get("payurl"),
|
||||
"payurl2": result.get("payurl2"), # 微信 H5
|
||||
"qrcode": result.get("qrcode"),
|
||||
"img": result.get("img"),
|
||||
"O_id": result.get("O_id"),
|
||||
"trade_no": result.get("trade_no"),
|
||||
}
|
||||
return {
|
||||
"status": "error",
|
||||
"provider": self.name,
|
||||
"msg": result.get("msg", resp.text),
|
||||
}
|
||||
|
||||
def verify_notify(self, data: Dict[str, Any]) -> bool:
|
||||
received = data.get("sign", "")
|
||||
calculated = self._md5_sign(data)
|
||||
return calculated == received
|
||||
|
||||
def parse_notify(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# ZPAY 通知:trade_status 为 TRADE_SUCCESS 表示成功
|
||||
status = "success" if data.get("trade_status") == "TRADE_SUCCESS" else "pending"
|
||||
return {
|
||||
"order_id": data.get("out_trade_no", ""),
|
||||
"trade_no": data.get("trade_no", ""),
|
||||
"pay_price": data.get("money", ""),
|
||||
"pay_time": "", # ZPAY 通知未明确返回
|
||||
"trade_status": status,
|
||||
}
|
||||
|
||||
def success_response(self) -> str:
|
||||
return "success"
|
||||
Reference in New Issue
Block a user