1
This commit is contained in:
@@ -4,11 +4,6 @@ import certifi
|
|||||||
import ssl
|
import ssl
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
# 先定义 logger
|
|
||||||
import time as _time
|
|
||||||
logger = logging.getLogger("payment")
|
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
# 尝试禁用 SSL 验证(用于解决证书问题)
|
# 尝试禁用 SSL 验证(用于解决证书问题)
|
||||||
try:
|
try:
|
||||||
_create_unverified_https_context = ssl._create_unverified_context
|
_create_unverified_https_context = ssl._create_unverified_context
|
||||||
@@ -17,100 +12,6 @@ except AttributeError:
|
|||||||
else:
|
else:
|
||||||
ssl._create_default_https_context = _create_unverified_https_context
|
ssl._create_default_https_context = _create_unverified_https_context
|
||||||
|
|
||||||
# 猴子补丁修复 alipay-sdk-python 的 ResponseException 错误
|
|
||||||
# 修复 WebUtils.py 中 bytes 和 str 拼接的问题
|
|
||||||
def fix_alipay_web_utils():
|
|
||||||
try:
|
|
||||||
from alipay.aop.api.util import WebUtils
|
|
||||||
|
|
||||||
# 保存原始的 do_post 函数
|
|
||||||
original_do_post = WebUtils.do_post
|
|
||||||
|
|
||||||
def patched_do_post(url, query_string, headers, params, charset, timeout):
|
|
||||||
try:
|
|
||||||
return original_do_post(url, query_string, headers, params, charset, timeout)
|
|
||||||
except Exception as e:
|
|
||||||
# 如果是 ResponseException,尝试修复错误信息
|
|
||||||
error_msg = str(e)
|
|
||||||
if "invalid http status" in error_msg or "ResponseException" in error_msg or "can only concatenate str (not \"bytes\") to str" in error_msg:
|
|
||||||
# 重新实现一个更安全的版本
|
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
import ssl
|
|
||||||
from alipay.aop.api.exception.ResponseException import ResponseException
|
|
||||||
|
|
||||||
logger.warning("Alipay SDK ResponseException detected, using fallback request")
|
|
||||||
|
|
||||||
# 构造请求
|
|
||||||
if query_string:
|
|
||||||
full_url = url + "?" + query_string
|
|
||||||
else:
|
|
||||||
full_url = url
|
|
||||||
|
|
||||||
# 准备 headers
|
|
||||||
req_headers = {}
|
|
||||||
if headers:
|
|
||||||
for key, value in headers.items():
|
|
||||||
req_headers[key] = value
|
|
||||||
|
|
||||||
req_headers["Content-type"] = "application/x-www-form-urlencoded;charset=" + charset
|
|
||||||
req_headers["Connection"] = "Keep-Alive"
|
|
||||||
req_headers["Cache-Control"] = "no-cache"
|
|
||||||
req_headers["User-Agent"] = "Mozilla/4.0 (compatible; MSIE 6.0; Windows XP; SV1)"
|
|
||||||
|
|
||||||
# 准备 post data
|
|
||||||
post_data = ""
|
|
||||||
if params:
|
|
||||||
post_params = []
|
|
||||||
for key, value in params.items():
|
|
||||||
post_params.append(
|
|
||||||
"%s=%s" % (key, urllib.parse.quote(str(value), encoding=charset))
|
|
||||||
)
|
|
||||||
post_data = "&".join(post_params)
|
|
||||||
|
|
||||||
logger.debug(f"Request URL: {full_url}")
|
|
||||||
logger.debug(f"Request data: {post_data[:200]}...")
|
|
||||||
|
|
||||||
# 发送请求
|
|
||||||
try:
|
|
||||||
req = urllib.request.Request(full_url, data=post_data.encode(charset) if post_data else None, headers=req_headers)
|
|
||||||
|
|
||||||
# 创建不验证证书的 context
|
|
||||||
ctx = ssl.create_default_context()
|
|
||||||
ctx.check_hostname = False
|
|
||||||
ctx.verify_mode = ssl.CERT_NONE
|
|
||||||
|
|
||||||
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as response:
|
|
||||||
response_body = response.read().decode(charset)
|
|
||||||
logger.debug(f"Response: {response_body[:500]}...")
|
|
||||||
return response_body
|
|
||||||
|
|
||||||
except urllib.error.HTTPError as e:
|
|
||||||
try:
|
|
||||||
error_body = e.read().decode(charset)
|
|
||||||
except:
|
|
||||||
error_body = str(e)
|
|
||||||
logger.error(f"Alipay HTTP error: {e.code}, body: {error_body}")
|
|
||||||
raise ResponseException(str(e.code) + "," + error_body)
|
|
||||||
except Exception as e2:
|
|
||||||
logger.exception(f"Alipay request failed")
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
# 应用补丁
|
|
||||||
WebUtils.do_post = patched_do_post
|
|
||||||
logger.info("Alipay WebUtils monkey patch applied successfully")
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# alipay 模块还没安装,跳过
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to apply Alipay WebUtils monkey patch")
|
|
||||||
|
|
||||||
# 应用补丁
|
|
||||||
fix_alipay_web_utils()
|
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -123,6 +24,10 @@ from app.utils.id_gen import generate_id, generate_order_no
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Payment logger → log/payment/YYYY-MM-DD.log (one file per day, no cleanup)
|
# Payment logger → log/payment/YYYY-MM-DD.log (one file per day, no cleanup)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
import time as _time
|
||||||
|
|
||||||
|
logger = logging.getLogger("payment")
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
_log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "log", "payment")
|
_log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "log", "payment")
|
||||||
os.makedirs(_log_dir, exist_ok=True)
|
os.makedirs(_log_dir, exist_ok=True)
|
||||||
@@ -237,7 +142,7 @@ _alipay_client = None
|
|||||||
_alipay_client_app_id = None
|
_alipay_client_app_id = None
|
||||||
|
|
||||||
|
|
||||||
def _get_alipay_client(app_id: str, private_key: str, public_key: str, gateway: str = "", notify_url: str = ""):
|
def _get_alipay_client(app_id: str, private_key: str, public_key: str, gateway: str = ""):
|
||||||
"""Get or create an Alipay client. Recreated if app_id changes."""
|
"""Get or create an Alipay client. Recreated if app_id changes."""
|
||||||
global _alipay_client, _alipay_client_app_id
|
global _alipay_client, _alipay_client_app_id
|
||||||
|
|
||||||
@@ -261,8 +166,6 @@ def _get_alipay_client(app_id: str, private_key: str, public_key: str, gateway:
|
|||||||
config.alipay_public_key = public_key
|
config.alipay_public_key = public_key
|
||||||
config.sign_type = "RSA2"
|
config.sign_type = "RSA2"
|
||||||
config.charset = "utf-8"
|
config.charset = "utf-8"
|
||||||
config.notify_url = notify_url
|
|
||||||
config.notify_type = "json"
|
|
||||||
# 先尝试使用 certifi 证书
|
# 先尝试使用 certifi 证书
|
||||||
config.ca_certificates = certifi.where()
|
config.ca_certificates = certifi.where()
|
||||||
|
|
||||||
@@ -408,7 +311,7 @@ def _create_alipay_order(order: PaymentOrder, db_configs: dict[str, str]) -> str
|
|||||||
logger.warning("Alipay config missing in database (app_id / private_key)")
|
logger.warning("Alipay config missing in database (app_id / private_key)")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
client = _get_alipay_client(app_id, private_key, public_key, gateway, notify_url)
|
client = _get_alipay_client(app_id, private_key, public_key, gateway)
|
||||||
if client is None:
|
if client is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -439,23 +342,29 @@ def _create_alipay_order(order: PaymentOrder, db_configs: dict[str, str]) -> str
|
|||||||
# 构造请求
|
# 构造请求
|
||||||
request = AlipayTradePrecreateRequest(biz_model=model)
|
request = AlipayTradePrecreateRequest(biz_model=model)
|
||||||
|
|
||||||
# 执行API调用
|
# 设置 notify_url 在 request 上
|
||||||
logger.info(f"Calling Alipay trade.precreate for order: {order.order_no}")
|
if notify_url:
|
||||||
response_content = client.execute(request)
|
try:
|
||||||
|
if hasattr(request, 'set_notify_url'):
|
||||||
|
request.set_notify_url(notify_url)
|
||||||
|
elif hasattr(request, 'notify_url'):
|
||||||
|
request.notify_url = notify_url
|
||||||
|
logger.info(f"Set notify_url for order {order.order_no}: {notify_url}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to set notify_url: {e}")
|
||||||
|
|
||||||
|
# 执行API调用
|
||||||
|
response_content = client.execute(request)
|
||||||
if not response_content:
|
if not response_content:
|
||||||
logger.error(f"Alipay precreate failed: empty response, order_no={order.order_no}")
|
logger.error(f"Alipay precreate failed: empty response, order_no={order.order_no}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.info(f"Alipay response received: {response_content[:500]}...")
|
|
||||||
|
|
||||||
# 解析响应结果
|
# 解析响应结果
|
||||||
response = AlipayTradePrecreateResponse()
|
response = AlipayTradePrecreateResponse()
|
||||||
response.parse_response_content(response_content)
|
response.parse_response_content(response_content)
|
||||||
|
|
||||||
if response.is_success():
|
if response.is_success():
|
||||||
qr_url = response.qr_code
|
qr_url = response.qr_code
|
||||||
logger.info(f"Alipay precreate success, qr_url: {qr_url}")
|
|
||||||
return qr_url
|
return qr_url
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -466,96 +375,13 @@ def _create_alipay_order(order: PaymentOrder, db_configs: dict[str, str]) -> str
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 特殊处理 bytes 和 str 拼接的错误
|
# 处理 SDK 内部的 bytes/str 错误
|
||||||
error_str = str(e)
|
if "TypeError" in str(e) and ("bytes" in str(e) or "str" in str(e)):
|
||||||
if "can only concatenate str (not \"bytes\") to str" in error_str:
|
|
||||||
logger.error(f"Alipay SDK bytes/str concat error, order_no={order.order_no}")
|
|
||||||
# 尝试直接用 urllib 发送请求作为备选方案
|
|
||||||
return _fallback_alipay_request(order, db_configs)
|
|
||||||
else:
|
|
||||||
logger.exception(f"Alipay precreate exception: order_no={order.order_no}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _fallback_alipay_request(order: PaymentOrder, db_configs: dict[str, str]) -> str | None:
|
|
||||||
"""
|
|
||||||
备选方案:直接使用 urllib 发送支付宝请求,绕过 SDK 的 WebUtils
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
import ssl
|
|
||||||
from alipay.aop.api.util.Signature import sign_with_rsa
|
|
||||||
from alipay.aop.api.response.AlipayTradePrecreateResponse import (
|
|
||||||
AlipayTradePrecreateResponse,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Using fallback Alipay request method")
|
|
||||||
|
|
||||||
app_id = db_configs.get("payment_alipay_app_id", "")
|
|
||||||
private_key = db_configs.get("payment_alipay_private_key", "")
|
|
||||||
gateway = db_configs.get("payment_alipay_gateway", "https://openapi.alipay.com/gateway.do")
|
|
||||||
charset = "utf-8"
|
|
||||||
|
|
||||||
# 构造请求参数
|
|
||||||
biz_content = {
|
|
||||||
"out_trade_no": order.order_no,
|
|
||||||
"total_amount": f"{order.amount:.2f}",
|
|
||||||
"subject": f"充值订单 {order.order_no}",
|
|
||||||
"product_code": "QR_CODE_OFFLINE"
|
|
||||||
}
|
|
||||||
|
|
||||||
params = {
|
|
||||||
"app_id": app_id,
|
|
||||||
"method": "alipay.trade.precreate",
|
|
||||||
"format": "json",
|
|
||||||
"charset": charset,
|
|
||||||
"sign_type": "RSA2",
|
|
||||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
"version": "1.0",
|
|
||||||
"biz_content": str(biz_content).replace("'", "\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
# 排序并签名
|
|
||||||
sorted_keys = sorted(params.keys())
|
|
||||||
sign_content = "&".join([f"{k}={params[k]}" for k in sorted_keys])
|
|
||||||
sign = sign_with_rsa(private_key.encode(charset), sign_content.encode(charset))
|
|
||||||
params["sign"] = sign
|
|
||||||
|
|
||||||
# 构造请求
|
|
||||||
query_string = urllib.parse.urlencode(params)
|
|
||||||
full_url = gateway
|
|
||||||
|
|
||||||
# 创建不验证证书的 context
|
|
||||||
ctx = ssl.create_default_context()
|
|
||||||
ctx.check_hostname = False
|
|
||||||
ctx.verify_mode = ssl.CERT_NONE
|
|
||||||
|
|
||||||
# 发送请求
|
|
||||||
req = urllib.request.Request(full_url, data=query_string.encode(charset))
|
|
||||||
req.add_header("Content-type", f"application/x-www-form-urlencoded;charset={charset}")
|
|
||||||
|
|
||||||
with urllib.request.urlopen(req, timeout=30, context=ctx) as response:
|
|
||||||
response_body = response.read().decode(charset)
|
|
||||||
logger.info(f"Fallback Alipay response: {response_body[:500]}...")
|
|
||||||
|
|
||||||
# 解析响应
|
|
||||||
resp = AlipayTradePrecreateResponse()
|
|
||||||
resp.parse_response_content(response_body)
|
|
||||||
|
|
||||||
if resp.is_success():
|
|
||||||
return resp.qr_code
|
|
||||||
else:
|
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Fallback Alipay precreate failed: code={resp.code}, "
|
f"Alipay SDK TypeError (bytes/str issue): order_no={order.order_no}, "
|
||||||
f"msg={resp.msg}, sub_code={resp.sub_code}, "
|
f"error={str(e)}"
|
||||||
f"sub_msg={resp.sub_msg}"
|
|
||||||
)
|
)
|
||||||
return None
|
logger.exception(f"Alipay precreate exception: order_no={order.order_no}")
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Fallback Alipay request also failed")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user