增加微信支付的逻辑

This commit is contained in:
2026-06-11 18:13:03 +08:00
parent fe3d57465b
commit 83e8ceee7e
4 changed files with 397 additions and 45 deletions
+298 -35
View File
@@ -143,12 +143,17 @@ async def _check_and_expire_order(db: AsyncSession, order: PaymentOrder) -> bool
f"ORDER_EXPIRED order_no={order.order_no} user={order.user_id} "
f"amount={order.amount} created_at={order.created_at.isoformat()}"
)
# Also call Alipay close API if it was an Alipay order
# Also call close API if it was an Alipay or WeChat order
if order.payment_method == "alipay":
try:
await _close_alipay_order(db, order, db_configs)
except Exception as e:
logger.exception(f"Failed to close Alipay order {order.order_no}: {e}")
elif order.payment_method == "wechat":
try:
await _close_wechat_order(db, order, db_configs)
except Exception as e:
logger.exception(f"Failed to close WeChat order {order.order_no}: {e}")
return True
return False
@@ -174,12 +179,17 @@ async def expire_all_pending_orders(db: AsyncSession) -> int:
logger.info(
f"ORDER_EXPIRED order_no={o.order_no} user={o.user_id} amount={o.amount}"
)
# Also call Alipay close API if it was an Alipay order
# Also call close API if it was an Alipay or WeChat order
if o.payment_method == "alipay":
try:
await _close_alipay_order(db, o, db_configs)
except Exception as e:
logger.exception(f"Failed to close Alipay order {o.order_no}: {e}")
elif o.payment_method == "wechat":
try:
await _close_wechat_order(db, o, db_configs)
except Exception as e:
logger.exception(f"Failed to close WeChat order {o.order_no}: {e}")
if orders:
await db.flush()
return expired_count
@@ -269,8 +279,15 @@ async def create_recharge_order(
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("微信支付未完成配置,请联系管理员")
required_configs = [
"payment_wechat_mch_id",
"payment_wechat_private_key",
"payment_wechat_cert_serial_no",
"payment_wechat_api_v3_key"
]
missing_configs = [c for c in required_configs if not db_configs.get(c)]
if missing_configs:
raise ValueError(f"微信支付未完成配置,缺少: {', '.join(missing_configs)},请联系管理员")
total_credits = credits + bonus_credits
order = PaymentOrder(
@@ -307,7 +324,13 @@ async def create_recharge_order(
else:
# Real payment: delegate to WeChat or Alipay
if method == "wechat":
_create_wechat_order(order, db_configs)
qr_code_content = _create_wechat_order(order, db_configs)
if qr_code_content:
# Attach QR code content to the order instance (transient, not persisted)
order.qr_url = qr_code_content # type: ignore[attr-defined]
else:
# Precreate failed — do not leave a pending order that can never be paid
raise ValueError("微信支付预下单失败,请检查配置或稍后重试")
elif method == "alipay":
qr_url = _create_alipay_order(order, db_configs)
if qr_url:
@@ -321,21 +344,231 @@ async def create_recharge_order(
# ---------------------------------------------------------------------------
# WeChat (stub)
# WeChat Pay client (lazy singleton, recreated when config changes)
# ---------------------------------------------------------------------------
_wechat_client = None
_wechat_mch_id = None
def _create_wechat_order(order: PaymentOrder, db_configs: dict[str, str]) -> None:
"""Create a WeChat Pay order. Stub for real integration."""
def _get_wechat_client(
mch_id: str,
private_key: str,
cert_serial_no: str,
api_v3_key: str,
gateway: str = ""
):
"""Get or create a WeChat Pay client. Recreated if config changes."""
global _wechat_client, _wechat_mch_id
if _wechat_client is not None and _wechat_mch_id == mch_id:
return _wechat_client
try:
from wechatpayv3 import WeChatPay, WeChatPayType
except ImportError:
logger.error(
"wechatpayv3 is not installed. "
"Install it with: pip install wechatpayv3"
)
return None
try:
# 处理私钥格式,确保是正确的 PEM 格式
private_key_str = private_key.strip()
if not private_key_str.startswith("-----BEGIN"):
private_key_str = "-----BEGIN PRIVATE KEY-----\n" + private_key_str + "\n-----END PRIVATE KEY-----"
# 初始化微信支付客户端
_wechat_client = WeChatPay(
wechatpay_type=WeChatPayType.NATIVE,
mchid=mch_id,
private_key=private_key_str,
cert_serial_no=cert_serial_no,
apiv3_private_key=api_v3_key,
gateway=gateway or "https://api.mch.weixin.qq.com",
)
_wechat_mch_id = mch_id
logger.info("WeChat Pay client initialized successfully")
return _wechat_client
except Exception as e:
logger.exception(f"Failed to initialize WeChat Pay client: {e}")
_wechat_client = None
_wechat_mch_id = None
return None
def _create_wechat_order(order: PaymentOrder, db_configs: dict[str, str]) -> str | None:
"""Create a WeChat Pay Native order. Returns QR code content (code_url).
Reads all WeChat config from the database (admin panel).
Returns the ``code_url`` on success, or ``None`` on failure.
"""
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:
private_key = db_configs.get("payment_wechat_private_key", "")
cert_serial_no = db_configs.get("payment_wechat_cert_serial_no", "")
api_v3_key = db_configs.get("payment_wechat_api_v3_key", "")
gateway = db_configs.get("payment_wechat_gateway", "")
notify_url = db_configs.get("payment_wechat_notify_url", "")
if not all([mch_id, private_key, cert_serial_no, api_v3_key]):
logger.warning("WeChat payment config missing in database")
return
logger.info(
f"WeChat order created: mch_id={mch_id}, "
f"order_no={order.order_no}, amount={order.amount}"
)
return None
client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, gateway)
if client is None:
return None
try:
# 调用微信支付 Native 下单接口
result = client.pay(
description=f"充值订单 {order.order_no}",
out_trade_no=order.order_no,
amount={
"total": int(order.amount * 100), # 微信支付以分为单位
"currency": "CNY"
},
notify_url=notify_url,
scene_info={
"payer_client_ip": "127.0.0.1",
}
)
if result.get('code_url'):
# 注意:微信返回的 code_url 可能需要进一步处理成二维码图片地址
logger.info(f"WeChat order created successfully: order_no={order.order_no}")
return result.get('code_url')
else:
logger.error(
f"WeChat pay failed: order_no={order.order_no}, "
f"result={result}"
)
return None
except Exception as e:
logger.exception(f"WeChat pay exception: order_no={order.order_no}")
return None
async def _close_wechat_order(db: AsyncSession, order: PaymentOrder, db_configs: dict[str, str]) -> bool:
"""Call WeChat Pay close API to close an unpaid order.
Returns True if the order was closed successfully.
"""
mch_id = db_configs.get("payment_wechat_mch_id", "")
private_key = db_configs.get("payment_wechat_private_key", "")
cert_serial_no = db_configs.get("payment_wechat_cert_serial_no", "")
api_v3_key = db_configs.get("payment_wechat_api_v3_key", "")
gateway = db_configs.get("payment_wechat_gateway", "")
client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, gateway)
if client is None:
return False
mock_mode = _is_mock_mode(db_configs)
if mock_mode:
logger.info(f"Mock mode: skipping close_wechat_order for {order.order_no}")
return True
try:
result = client.close(out_trade_no=order.order_no)
if result:
logger.info(f"WeChat order closed: order_no={order.order_no}")
return True
else:
logger.error(f"WeChat close failed: order_no={order.order_no}")
return False
except Exception as e:
logger.exception(f"WeChat close exception: order_no={order.order_no}")
return False
async def _query_wechat_order(db: AsyncSession, order: PaymentOrder, db_configs: dict[str, str]) -> dict | None:
"""Call WeChat Pay query API to check order status.
Returns the response data if successful, None otherwise.
"""
mch_id = db_configs.get("payment_wechat_mch_id", "")
private_key = db_configs.get("payment_wechat_private_key", "")
cert_serial_no = db_configs.get("payment_wechat_cert_serial_no", "")
api_v3_key = db_configs.get("payment_wechat_api_v3_key", "")
gateway = db_configs.get("payment_wechat_gateway", "")
client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, gateway)
if client is None:
return None
mock_mode = _is_mock_mode(db_configs)
if mock_mode:
logger.info(f"Mock mode: skipping query_wechat_order for {order.order_no}")
return {"trade_state": "SUCCESS"}
try:
result = client.query(out_trade_no=order.order_no)
if result.get('trade_state'):
logger.info(
f"WeChat query succeeded: order_no={order.order_no}, "
f"trade_state={result.get('trade_state')}"
)
return result
else:
logger.error(
f"WeChat query failed: order_no={order.order_no}, "
f"result={result}"
)
return None
except Exception as e:
logger.exception(f"WeChat query exception: order_no={order.order_no}")
return None
async def _refund_wechat_order(
db: AsyncSession,
order: PaymentOrder,
refund_amount: float,
refund_reason: str,
db_configs: dict[str, str]
) -> dict:
"""Call WeChat Pay refund API."""
mch_id = db_configs.get("payment_wechat_mch_id", "")
private_key = db_configs.get("payment_wechat_private_key", "")
cert_serial_no = db_configs.get("payment_wechat_cert_serial_no", "")
api_v3_key = db_configs.get("payment_wechat_api_v3_key", "")
gateway = db_configs.get("payment_wechat_gateway", "")
client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, gateway)
if client is None:
return {"success": False, "message": "微信支付客户端初始化失败"}
mock_mode = _is_mock_mode(db_configs)
if mock_mode:
logger.info(f"Mock mode: skipping wechat refund for {order.order_no}")
return {"success": True}
try:
out_refund_no = f"{order.order_no}_refund_{int(datetime.now().timestamp())}"
result = client.refund(
out_trade_no=order.order_no,
out_refund_no=out_refund_no,
amount={
"total": int(order.amount * 100), # 订单总金额
"refund": int(refund_amount * 100), # 退款金额
"currency": "CNY"
},
reason=refund_reason
)
if result.get('status') == 'SUCCESS':
logger.info(f"WeChat refund succeeded: order_no={order.order_no}")
return {"success": True, "refund_id": result.get('refund_id')}
else:
logger.error(
f"WeChat refund failed: order_no={order.order_no}, "
f"result={result}"
)
return {
"success": False,
"message": f"微信退款失败: {result.get('code', '')}"
}
except Exception as e:
logger.exception(f"WeChat refund exception: order_no={order.order_no}")
return {"success": False, "message": f"微信退款异常: {str(e)}"}
# ---------------------------------------------------------------------------
@@ -562,7 +795,7 @@ async def _query_alipay_order(db: AsyncSession, order: PaymentOrder, db_configs:
async def sync_pending_orders(db: AsyncSession) -> int:
"""Check pending orders via Alipay query and update status.
"""Check pending orders via Alipay/WeChat query and update status.
Returns the number of orders updated.
"""
result = await db.execute(
@@ -576,23 +809,38 @@ async def sync_pending_orders(db: AsyncSession) -> int:
db_configs = await _get_payment_configs(db)
for order in orders:
if order.payment_method != "alipay":
continue
try:
data = await _query_alipay_order(db, order, db_configs)
if data:
trade_status = data.get("trade_status")
if trade_status in ("TRADE_SUCCESS", "TRADE_FINISHED"):
# Order was paid but we missed the callback
trade_no = data.get("trade_no", "")
await process_payment_success_by_order_no(db, order.order_no, trade_no)
updated_count += 1
elif trade_status in ("TRADE_CLOSED", "TRADE_CANCELLED"):
# Order was closed on Alipay side
order.status = "cancelled"
await db.flush()
updated_count += 1
if order.payment_method == "alipay":
data = await _query_alipay_order(db, order, db_configs)
if data:
trade_status = data.get("trade_status")
if trade_status in ("TRADE_SUCCESS", "TRADE_FINISHED"):
# Order was paid but we missed the callback
trade_no = data.get("trade_no", "")
total_amount_str = data.get("total_amount", "")
total_amount = float(total_amount_str) if total_amount_str else None
await process_payment_success_by_order_no(db, order.order_no, trade_no, total_amount)
updated_count += 1
elif trade_status in ("TRADE_CLOSED", "TRADE_CANCELLED"):
# Order was closed on Alipay side
order.status = "cancelled"
await db.flush()
updated_count += 1
elif order.payment_method == "wechat":
data = await _query_wechat_order(db, order, db_configs)
if data:
trade_state = data.get("trade_state")
if trade_state == "SUCCESS":
# Order was paid but we missed the callback
transaction_id = data.get("transaction_id", "")
total_amount = float(data.get("amount", {}).get("total", 0)) / 100
await process_payment_success_by_order_no(db, order.order_no, transaction_id, total_amount)
updated_count += 1
elif trade_state in ("CLOSED", "REVOKED"):
# Order was closed on WeChat side
order.status = "cancelled"
await db.flush()
updated_count += 1
except Exception as e:
logger.exception(f"Failed to sync order {order.order_no}: {e}")
@@ -747,12 +995,21 @@ def _verify_alipay_sign(public_key: str, sign_content: str, sign: str, sign_type
async def verify_wechat_callback(data: dict, db: AsyncSession) -> bool:
"""Verify WeChat payment callback signature."""
"""Verify WeChat payment callback signature.
Note: Since we're using wechatpayv3 SDK which handles verification internally,
in the callback handler we'll verify with the SDK. This function is kept for
interface consistency and mock mode support.
"""
db_configs = await _get_payment_configs(db)
mock_mode = _is_mock_mode(db_configs)
if mock_mode:
logger.info("Mock mode: skipping WeChat callback verification")
return True
logger.info("WeChat callback verification (real mode not implemented)")
# 对于真实模式,我们在回调路由处理器中直接使用 SDK 验证
# 这里我们返回 True 以保持接口一致性
logger.info("WeChat callback verification (delegated to SDK in router)")
return True
@@ -881,7 +1138,7 @@ async def process_refund(
if refund_amount > order.amount:
return {"success": False, "message": "退款金额超过订单金额"}
# 如果是支付宝订单,调用支付宝退款API
# 根据支付方式调用相应的退款API
db_configs = await _get_payment_configs(db)
if order.payment_method == "alipay":
refund_result = await _refund_alipay_order(
@@ -889,6 +1146,12 @@ async def process_refund(
)
if not refund_result.get("success"):
return refund_result
elif order.payment_method == "wechat":
refund_result = await _refund_wechat_order(
db, order, refund_amount, refund_reason, db_configs
)
if not refund_result.get("success"):
return refund_result
# 扣除积分
try: