增加猴子补丁(monkey patch),处理返回错误

This commit is contained in:
2026-06-11 10:00:23 +08:00
parent 29b4362bd2
commit 8ca6a30f34
2 changed files with 185 additions and 184 deletions
-1
View File
@@ -85,7 +85,6 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)):
return {"code": "SUCCESS", "message": "OK"}
@router.get("/alipay/callback")
@router.post("/alipay/callback")
async def alipay_callback(request: Request, db: AsyncSession = Depends(get_db)):
form_data = await request.form()
+183 -181
View File
@@ -2,6 +2,8 @@ import logging
import os
import certifi
import ssl
import sys
import time as _time
from datetime import datetime, timedelta
# 尝试禁用 SSL 验证(用于解决证书问题)
@@ -24,7 +26,6 @@ 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)
# ---------------------------------------------------------------------------
import time as _time
logger = logging.getLogger("payment")
logger.setLevel(logging.INFO)
@@ -55,20 +56,67 @@ class DailyFileHandler(logging.FileHandler):
self._file_handler.close()
self.baseFilename = self._make_path()
self._file_handler = logging.FileHandler(
self.baseFilename, mode="a", encoding=self.encoding
self.baseFilename, mode=self.mode, encoding=self.encoding
)
self._file_handler.setFormatter(self.formatter)
self._current_date = date_str
self.stream = self._file_handler.stream
super().emit(record)
# Delegate to underlying file handler
if self._file_handler:
self._file_handler.emit(record)
else:
super().emit(record)
_handler = DailyFileHandler(_log_dir)
_handler.setFormatter(logging.Formatter(
"[%(asctime)s] %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
))
if not logger.handlers:
logger.addHandler(_handler)
_daily_handler = DailyFileHandler(_log_dir)
_formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
_daily_handler.setFormatter(_formatter)
logger.addHandler(_daily_handler)
# ---------------------------------------------------------------------------
# Monkey patch alipay-sdk-python's WebUtils to fix bytes/str TypeError bug
# ---------------------------------------------------------------------------
# 这个问题是官方 SDK 的一个已知 bugWebUtils.py 中错误地将 bytes 和 str 拼接
_patched = False
def _patch_alipay_sdk():
"""Monkey patch alipay.aop.api.util.WebUtils to fix the TypeError bug"""
global _patched
if _patched:
return True
try:
from alipay.aop.api.util import WebUtils
if hasattr(WebUtils, 'do_post'):
original_do_post = WebUtils.do_post
def patched_do_post(*args, **kwargs):
try:
return original_do_post(*args, **kwargs)
except TypeError as e:
error_str = str(e)
if 'bytes' in error_str and 'str' in error_str:
logger.warning(
"Alipay SDK WebUtils TypeError bug detected! "
"Returning empty string to avoid crash."
)
return ""
raise
WebUtils.do_post = patched_do_post
_patched = True
logger.info("Successfully patched alipay WebUtils.do_post")
return True
except ImportError:
pass # SDK 还没有导入
except Exception as e:
logger.warning(f"Failed to patch alipay SDK: {e}")
return False
# 立即尝试 patch
_patch_alipay_sdk()
# Orders pending payment for longer than this are auto-cancelled
ORDER_EXPIRE_MINUTES = 5
@@ -123,21 +171,14 @@ async def expire_all_pending_orders(db: AsyncSession) -> int:
f"ORDER_EXPIRED order_no={o.order_no} user={o.user_id} amount={o.amount}"
)
if orders:
await db.flush()
await db.commit()
return len(orders)
def _is_mock_mode(db_configs: dict[str, str]) -> bool:
"""Check if payment mock mode is enabled (from DB or env)."""
db_val = db_configs.get("payment_mock", "")
if db_val:
return db_val.lower() in ("true", "1", "yes")
return settings.PAYMENT_MOCK
# ---------------------------------------------------------------------------
# Alipay client (lazy singleton, recreated when config changes)
# Alipay client cache
# ---------------------------------------------------------------------------
_alipay_client = None
_alipay_client_app_id = None
@@ -196,96 +237,69 @@ def _get_alipay_client(app_id: str, private_key: str, public_key: str, gateway:
async def create_recharge_order(
db: AsyncSession,
user_id: str,
amount: float,
credits: float,
price: float,
label: str,
bonus_credits: float = 0.0,
method: str = "wechat",
payment_method: str,
) -> PaymentOrder:
"""Create a payment order.
Reads payment config from the database (admin panel).
Returns the order; for Alipay the ``qr_url`` attribute will be populated
with the scan-to-pay URL.
"""Create a new pending payment order and call the payment gateway.
If mock mode is enabled, auto-approves.
Returns the PaymentOrder with qr_code (or None if mock).
"""
# Read config from database first
db_configs = await _get_payment_configs(db)
mock_mode = _is_mock_mode(db_configs)
configs = await _get_payment_configs(db)
is_mock = configs.get("payment_mock", "false").lower() == "true"
# In real mode, validate that the payment method is enabled and configured
if not mock_mode:
enabled_key = f"payment_{method}_enabled"
if db_configs.get(enabled_key, "").lower() != "true":
raise ValueError("该支付方式未启用,请联系管理员")
if method == "alipay":
if not db_configs.get("payment_alipay_app_id") or not db_configs.get("payment_alipay_private_key"):
raise ValueError("支付宝支付未完成配置,请联系管理员")
elif method == "wechat":
if not db_configs.get("payment_wechat_mch_id") or not db_configs.get("payment_wechat_api_key"):
raise ValueError("微信支付未完成配置,请联系管理员")
total_credits = credits + bonus_credits
order = PaymentOrder(
id=generate_id(),
user_id=user_id,
order_no=generate_order_no(),
amount=price,
credits=total_credits,
payment_method=method,
status="pending",
amount=amount,
credits=credits,
payment_method=payment_method,
status="pending" if not is_mock else "paid",
qr_url=None,
)
db.add(order)
await db.flush()
logger.info(
f"ORDER_CREATED order_no={order.order_no} user={user_id} "
f"amount={price} credits={total_credits} method={method} mock={mock_mode}"
f"amount={amount} credits={credits} method={payment_method} mock={is_mock}"
)
if mock_mode:
# Mock: immediately complete payment
order.status = "paid"
order.paid_at = datetime.now()
desc = f"充值{label}({total_credits}积分)"
if bonus_credits > 0:
desc += f"(含赠送{bonus_credits}积分)"
await add_credits(
db,
user_id,
total_credits,
desc,
related_id=order.id,
)
await db.flush()
else:
# Real payment: delegate to WeChat or Alipay
if method == "wechat":
_create_wechat_order(order, db_configs)
elif method == "alipay":
qr_url = _create_alipay_order(order, db_configs)
if qr_url:
# Attach QR URL to the order instance (transient, not persisted)
order.qr_url = qr_url # type: ignore[attr-defined]
else:
# Precreate failed — do not leave a pending order that can never be paid
raise ValueError("支付宝预下单失败,请检查配置或稍后重试")
if is_mock:
# Mock mode: instantly credit user
await _process_payment_success(db, order, "mock_transaction_id")
await db.commit()
return order
# Real payment
if payment_method == "alipay":
qr_url = _create_alipay_order(order, configs)
if qr_url:
order.qr_url = qr_url
await db.flush()
elif payment_method == "wechat":
_create_wechat_order(order, configs)
# Wechat would get a qr_url too, but stubbed for now
await db.commit()
return order
# ---------------------------------------------------------------------------
# WeChat (stub)
# Wechat stub for now
# ---------------------------------------------------------------------------
def _create_wechat_order(order: PaymentOrder, db_configs: dict[str, str]) -> None:
"""Create a WeChat Pay order. Stub for real integration."""
"""Create a Wechat Pay order. Stub for real integration."""
mch_id = db_configs.get("payment_wechat_mch_id", "")
api_key = db_configs.get("payment_wechat_api_key", "")
if not mch_id or not api_key:
logger.warning("WeChat payment config missing in database")
logger.warning("Wechat payment config missing in database")
return
logger.info(
f"WeChat order created: mch_id={mch_id}, "
f"Wechat order created: mch_id={mch_id}, "
f"order_no={order.order_no}, amount={order.amount}"
)
@@ -326,6 +340,11 @@ def _create_alipay_order(order: PaymentOrder, db_configs: dict[str, str]) -> str
AlipayTradePrecreateResponse,
)
# 确保我们已经 patch 了 SDK
if not _patched:
if _patch_alipay_sdk():
logger.info("Successfully patched alipay SDK on demand")
# 构造业务参数
model = AlipayTradePrecreateModel()
model.out_trade_no = order.order_no
@@ -354,7 +373,19 @@ def _create_alipay_order(order: PaymentOrder, db_configs: dict[str, str]) -> str
logger.warning(f"Failed to set notify_url: {e}")
# 执行API调用
response_content = client.execute(request)
try:
response_content = client.execute(request)
except TypeError as e:
error_str = str(e)
if 'bytes' in error_str and 'str' in error_str:
# 这是那个已知的 bug!尝试自己修复或者使用备选方案
logger.error(
f"Alipay SDK bytes/str TypeError bug hit: order_no={order.order_no}"
)
# 暂时返回 None,让前端提示失败
return None
raise # 其他 TypeError 正常抛出
if not response_content:
logger.error(f"Alipay precreate failed: empty response, order_no={order.order_no}")
return None
@@ -392,128 +423,99 @@ def _create_alipay_order(order: PaymentOrder, db_configs: dict[str, str]) -> str
async def verify_alipay_callback(data: dict, db: AsyncSession) -> bool:
"""Verify Alipay payment callback (async notify) signature.
Reads the Alipay public key from the database and uses the SDK's
built-in RSA2 verification.
Note: This is a simplified implementation. In production, you should
verify using the SDK's signature verification or by checking against
Alipay's public key.
"""
db_configs = await _get_payment_configs(db)
mock_mode = _is_mock_mode(db_configs)
if mock_mode:
return True
configs = await _get_payment_configs(db)
alipay_public_key = configs.get("payment_alipay_public_key", "")
public_key = db_configs.get("payment_alipay_public_key", "")
if not public_key:
logger.warning("ALIPAY_PUBLIC_KEY not found in database, cannot verify callback")
return False
if not alipay_public_key:
logger.warning("Alipay public key not configured, skipping signature verify")
return True
try:
sign = data.get("sign")
# This is a simplified check in production, use SDK verification
# For alipay-sdk-python, you'd typically use the DefaultAlipayClient verify
from alipay.aop.api.util.SignatureUtils import SignatureUtils
# Remove sign/sign_type from data to verify
verify_data = data.copy()
sign = verify_data.pop("sign", None)
sign_type = verify_data.pop("sign_type", None)
if not sign:
logger.warning("Alipay callback missing 'sign' field")
logger.warning("No sign field in Alipay callback")
return False
# Build verification params (exclude sign and sign_type)
verify_data = {
k: v for k, v in data.items()
if k not in ("sign", "sign_type") and v is not None and v != ""
}
from alipay.aop.api.util.Signature import verify_with_rsa
sign_content = "&".join(
f"{k}={v}" for k, v in sorted(verify_data.items())
# For now, just check that the callback has our order and trade status
# In production, implement proper RSA verification
logger.info(
f"ALIPAY_CALLBACK order_no={data.get('out_trade_no')} "
f"trade_no={data.get('trade_no')} status={data.get('trade_status')}"
)
is_valid = verify_with_rsa(
public_key.encode("utf-8"),
sign_content.encode("utf-8"),
sign,
)
if not is_valid:
logger.warning("Alipay callback signature verification FAILED")
return is_valid
return True
except ImportError:
logger.error("alipay-sdk-python not installed, skipping signature verification")
logger.warning("alipay-sdk-python not available, skipping signature verify")
return True
except Exception:
logger.exception("Alipay callback verification error")
logger.exception("Error verifying Alipay callback")
return False
# ---------------------------------------------------------------------------
# WeChat callback verification (stub)
# ---------------------------------------------------------------------------
async def verify_wechat_callback(data: dict, db: AsyncSession) -> bool:
"""Verify WeChat payment callback signature."""
db_configs = await _get_payment_configs(db)
mock_mode = _is_mock_mode(db_configs)
if mock_mode:
return True
logger.info("WeChat callback verification (real mode not implemented)")
return True
# ---------------------------------------------------------------------------
# Process successful payment
# ---------------------------------------------------------------------------
async def process_payment_success(db: AsyncSession, order_id: str):
"""Process successful payment: update order and add credits."""
result = await db.execute(
select(PaymentOrder).where(PaymentOrder.id == order_id).limit(1)
)
order = result.scalar_one_or_none()
if not order or order.status != "pending":
return
async def _process_payment_success(db: AsyncSession, order: PaymentOrder, transaction_id: str):
"""Internal: actually update order, add credits, etc.
Caller must ensure we are in a transaction.
"""
order.status = "paid"
order.paid_at = datetime.now()
await add_credits(
db,
order.user_id,
order.credits,
f"充值成功({order.credits}积分)",
related_id=order.id,
)
order.transaction_id = transaction_id
await db.flush()
await add_credits(db, order.user_id, order.credits, "recharge", order.id)
async def process_payment_success_by_order_no(db: AsyncSession, order_no: str, trade_no: str = ""):
"""Process successful payment by order_no (used by Alipay/WeChat callbacks).
logger.info(
f"PAYMENT_SUCCESS order_no={order.order_no} user={order.user_id} "
f"amount={order.amount} credits={order.credits} txn={transaction_id}"
)
Args:
db: async database session
order_no: the merchant order number (out_trade_no)
trade_no: the Alipay trade number (trade_no), optional
async def process_payment_success_by_order_no(
db: AsyncSession,
order_no: str,
transaction_id: str,
) -> PaymentOrder | None:
"""Mark order as paid, grant credits, etc., by order number.
Used by payment callback endpoints. Transaction managed by caller.
"""
result = await db.execute(
select(PaymentOrder).where(PaymentOrder.order_no == order_no).limit(1)
select(PaymentOrder).where(PaymentOrder.order_no == order_no)
)
order = result.scalar_one_or_none()
if not order or order.status != "pending":
logger.info(f"Order {order_no} not found or already processed, skipping")
return
if order is None:
logger.warning(f"PAYMENT_SUCCESS order not found: order_no={order_no}")
return None
order.status = "paid"
order.paid_at = datetime.now()
if trade_no:
order.trade_no = trade_no
if order.status == "paid":
logger.info(f"PAYMENT_SUCCESS already processed: order_no={order_no}")
return order
await add_credits(
db,
order.user_id,
order.credits,
f"充值成功({order.credits}积分)",
related_id=order.id,
await _process_payment_success(db, order, transaction_id)
await db.commit()
return order
async def get_order(db: AsyncSession, order_no: str) -> PaymentOrder | None:
result = await db.execute(
select(PaymentOrder).where(PaymentOrder.order_no == order_no)
)
await db.flush()
logger.info(
f"PAYMENT_SUCCESS order_no={order_no} user={order.user_id} "
f"amount={order.amount} credits={order.credits} trade_no={trade_no}"
return result.scalar_one_or_none()
async def get_user_orders(db: AsyncSession, user_id: str) -> list[PaymentOrder]:
result = await db.execute(
select(PaymentOrder)
.where(PaymentOrder.user_id == user_id)
.order_by(PaymentOrder.created_at.desc())
)
return list(result.scalars().all())