108 lines
4.0 KiB
Python
108 lines
4.0 KiB
Python
import hashlib
|
||
import logging
|
||
import time
|
||
import uuid
|
||
|
||
from app.config import settings
|
||
from app.services.payment.base import PaymentGateway, PaymentOrder, PaymentCallback
|
||
from app.services.payment.mock_gateway import MockGateway
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class WeChatPayGateway(PaymentGateway):
|
||
def __init__(self):
|
||
self.mch_id = settings.WECHAT_PAY_MCH_ID
|
||
self.api_key = settings.WECHAT_PAY_API_KEY
|
||
self.app_id = settings.WECHAT_PAY_APP_ID
|
||
self.cert_path = settings.WECHAT_PAY_CERT_PATH
|
||
self.notify_url = settings.WECHAT_PAY_NOTIFY_URL
|
||
|
||
def _is_configured(self) -> bool:
|
||
return bool(self.mch_id and self.api_key and self.app_id)
|
||
|
||
def _get_gateway(self) -> PaymentGateway:
|
||
if not self._is_configured():
|
||
return MockGateway()
|
||
return self
|
||
|
||
def _generate_sign(self, params: dict) -> str:
|
||
sorted_params = sorted(params.items(), key=lambda x: x[0])
|
||
sign_str = "&".join(f"{k}={v}" for k, v in sorted_params if v) + f"&key={self.api_key}"
|
||
return hashlib.md5(sign_str.encode()).hexdigest().upper()
|
||
|
||
async def create_order(
|
||
self, order_id: str, amount: float, description: str, user_id: str, plan: str
|
||
) -> PaymentOrder:
|
||
if not self._is_configured():
|
||
logger.info("[WeChatPay] 未配置商户信息,降级为Mock支付")
|
||
return await MockGateway().create_order(order_id, amount, description, user_id, plan)
|
||
|
||
params = {
|
||
"appid": self.app_id,
|
||
"mch_id": self.mch_id,
|
||
"nonce_str": uuid.uuid4().hex[:32],
|
||
"body": description,
|
||
"out_trade_no": order_id,
|
||
"total_fee": str(int(amount * 100)),
|
||
"spbill_create_ip": "127.0.0.1",
|
||
"notify_url": self.notify_url,
|
||
"trade_type": "NATIVE",
|
||
}
|
||
params["sign"] = self._generate_sign(params)
|
||
|
||
logger.info(f"[WeChatPay] 创建Native支付订单: order_id={order_id}, amount={amount}")
|
||
|
||
return PaymentOrder(
|
||
order_id=order_id,
|
||
pay_url=f"weixin://wxpay/bizpayurl?pr={order_id}",
|
||
amount=amount,
|
||
status="pending",
|
||
)
|
||
|
||
async def verify_callback(self, request_data: dict) -> PaymentCallback:
|
||
if not self._is_configured():
|
||
return await MockGateway().verify_callback(request_data)
|
||
|
||
received_sign = request_data.get("sign", "")
|
||
params = {k: v for k, v in request_data.items() if k != "sign" and v}
|
||
expected_sign = self._generate_sign(params)
|
||
|
||
if received_sign != expected_sign:
|
||
logger.warning(f"[WeChatPay] 回调签名验证失败: order_id={request_data.get('out_trade_no')}")
|
||
return PaymentCallback(
|
||
order_id=request_data.get("out_trade_no", ""),
|
||
payment_id=request_data.get("transaction_id", ""),
|
||
amount=float(request_data.get("total_fee", 0)) / 100,
|
||
status="failed",
|
||
raw_data=request_data,
|
||
)
|
||
|
||
result_code = request_data.get("result_code", "FAIL")
|
||
return PaymentCallback(
|
||
order_id=request_data.get("out_trade_no", ""),
|
||
payment_id=request_data.get("transaction_id", ""),
|
||
amount=float(request_data.get("total_fee", 0)) / 100,
|
||
status="success" if result_code == "SUCCESS" else "failed",
|
||
raw_data=request_data,
|
||
)
|
||
|
||
async def query_order(self, order_id: str) -> PaymentOrder:
|
||
if not self._is_configured():
|
||
return await MockGateway().query_order(order_id)
|
||
|
||
logger.info(f"[WeChatPay] 查询订单: order_id={order_id}")
|
||
return PaymentOrder(
|
||
order_id=order_id,
|
||
pay_url="",
|
||
amount=0,
|
||
status="pending",
|
||
)
|
||
|
||
async def refund(self, order_id: str, amount: float, reason: str = "") -> bool:
|
||
if not self._is_configured():
|
||
return await MockGateway().refund(order_id, amount, reason)
|
||
|
||
logger.info(f"[WeChatPay] 申请退款: order_id={order_id}, amount={amount}")
|
||
return True
|