This commit is contained in:
2026-06-15 14:45:15 +08:00
2 changed files with 158 additions and 56 deletions
+129 -41
View File
@@ -105,54 +105,118 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)):
logger.exception(f"Mock WeChat callback error: {e}")
return {"code": "SUCCESS", "message": "OK"} # 微信要求即使处理失败也返回成功
# 真实模式:使用 wechatpayv3 SDK 验证回调并解析数据
# 真实模式:使用 wechatpayv3 SDK 工具验证回调并解析数据
try:
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", "")
appid = db_configs.get("payment_wechat_appid", "")
public_key = db_configs.get("payment_wechat_public_key", "")
public_key_id = db_configs.get("payment_wechat_public_key_id", "")
notify_url = db_configs.get("payment_wechat_notify_url", "")
client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, appid, notify_url, public_key, public_key_id)
if not client:
logger.error("WeChat client not initialized for callback")
return {"code": "SUCCESS", "message": "OK"}
# 从请求头获取必要信息
headers = dict(request.headers)
timestamp = headers.get("Wechatpay-Timestamp", "")
nonce = headers.get("Wechatpay-Nonce", "")
signature = headers.get("Wechatpay-Signature", "")
serial_no = headers.get("Wechatpay-Serial", "")
# 验证签名
is_verified = client.verify(
timestamp=timestamp,
nonce=nonce,
body=body_str,
signature=signature,
serial_no=serial_no
from wechatpayv3.utils import (
rsa_verify, load_public_key, sha256, b64decode,
AESGCM, InvalidTag
)
if not is_verified:
logger.warning("WeChat callback signature verification failed")
raise HTTPException(status_code=400, detail="签名验证失败")
mch_id = db_configs.get("payment_wechat_mch_id", "")
api_v3_key = db_configs.get("payment_wechat_api_v3_key", "")
public_key = db_configs.get("payment_wechat_public_key", "")
# 解密回调数据
decrypted_data = client.decrypt(body_str)
if not decrypted_data:
logger.error("WeChat callback decryption failed")
if not all([mch_id, api_v3_key]):
logger.error("WeChat payment config missing for callback")
return {"code": "SUCCESS", "message": "OK"}
# 从请求头获取必要信息(不区分大小写)
headers = {k.lower(): v for k, v in dict(request.headers).items()}
timestamp = headers.get("wechatpay-timestamp", "")
nonce = headers.get("wechatpay-nonce", "")
signature = headers.get("wechatpay-signature", "")
serial_no = headers.get("wechatpay-serial", "")
# 验证签名:使用平台公钥验证
if public_key and serial_no:
try:
# 构造签名串:timestamp + "\n" + nonce + "\n" + body + "\n"
# 符合微信支付官方文档规范:https://pay.weixin.qq.com/doc/v3/merchant/4013053249
is_verified = rsa_verify(
timestamp=timestamp,
nonce=nonce,
body=body_str,
signature=signature,
public_key=load_public_key(public_key)
)
if not is_verified:
logger.warning(f"WeChat callback signature verification failed: serial={serial_no}")
raise HTTPException(status_code=400, detail="签名验证失败")
except Exception as e:
logger.warning(f"WeChat signature verification error: {e}, serial={serial_no}")
raise HTTPException(status_code=400, detail="签名验证失败")
else:
if not public_key:
logger.warning("WeChat platform public key not configured, skipping signature verification")
if not serial_no:
logger.warning("Wechatpay-Serial header missing, skipping signature verification")
# 解密回调数据:使用 API v3 key
# 官方文档:https://pay.weixin.qq.com/doc/v3/merchant/4012071382
import json
body_data = json.loads(body_str) if body_str else {}
resource = body_data.get("resource", {})
if not resource:
logger.error("WeChat callback resource not found")
raise HTTPException(status_code=400, detail="数据格式错误")
# 验证加密算法(官方文档要求固定为 AEAD_AES_256_GCM
algorithm = resource.get("algorithm", "")
if algorithm != "AEAD_AES_256_GCM":
logger.error(f"WeChat callback unsupported algorithm: {algorithm}")
raise HTTPException(status_code=400, detail="不支持的加密算法")
ciphertext = resource.get("ciphertext", "")
associated_data = resource.get("associated_data", "")
nonce_str = resource.get("nonce", "")
# 参数验证
if not ciphertext:
logger.error("WeChat callback ciphertext is empty")
raise HTTPException(status_code=400, detail="密文为空")
if not nonce_str:
logger.error("WeChat callback nonce is empty")
raise HTTPException(status_code=400, detail="随机数为空")
# 使用 AES-GCM 解密(符合官方文档规范)
# 官方文档:https://pay.weixin.qq.com/doc/v3/merchant/4012071382
try:
# API v3 key 需要转换为字节串
api_v3_key_bytes = api_v3_key.encode('utf-8')
# ciphertext 是 Base64 编码的,需要解码
ciphertext_bytes = b64decode(ciphertext)
# nonce 直接使用字符串编码(官方文档方式)
nonce_bytes = nonce_str.encode('utf-8')
# associated_data 是字符串,直接编码
associated_data_bytes = associated_data.encode('utf-8') if associated_data else b''
aesgcm = AESGCM(api_v3_key_bytes)
decrypted_str = aesgcm.decrypt(nonce_bytes, ciphertext_bytes, associated_data_bytes)
except InvalidTag:
logger.error("WeChat callback decryption failed: Invalid tag (key or data mismatch)")
raise HTTPException(status_code=400, detail="数据解密失败(密钥或数据不匹配)")
except Exception as e:
logger.error(f"WeChat callback decryption failed: {e}")
raise HTTPException(status_code=400, detail="数据解密失败")
if not decrypted_str:
logger.error("WeChat callback decryption returned empty")
raise HTTPException(status_code=400, detail="数据解密失败")
decrypted_data = json.loads(decrypted_str)
event_type = body_data.get("event_type", "")
# 处理支付成功回调
if decrypted_data.get("event_type") == "TRANSACTION.SUCCESS":
resource = decrypted_data.get("resource", {})
order_no = resource.get("out_trade_no", "")
transaction_id = resource.get("transaction_id", "")
amount_info = resource.get("amount", {})
if event_type == "TRANSACTION.SUCCESS":
order_no = decrypted_data.get("out_trade_no", "")
transaction_id = decrypted_data.get("transaction_id", "")
amount_info = decrypted_data.get("amount", {})
total_amount = amount_info.get("total", 0) / 100 # 转换为元
if order_no:
@@ -162,6 +226,30 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)):
f"transaction_id={transaction_id}, amount={total_amount}"
)
# 处理退款回调
elif event_type == "REFUND.SUCCESS":
order_no = decrypted_data.get("out_trade_no", "")
refund_id = decrypted_data.get("refund_id", "")
refund_status = decrypted_data.get("status", "")
if order_no and refund_status == "SUCCESS":
# 更新订单状态为已退款
from app.models import PaymentOrder
from sqlalchemy import select
result = await db.execute(select(PaymentOrder).where(PaymentOrder.order_no == order_no))
order = result.scalar_one_or_none()
if order and order.status == "refunding":
order.status = "refunded"
order.transaction_id = refund_id
await db.commit()
logger.info(
f"WeChat refund callback processed: order_no={order_no}, "
f"refund_id={refund_id}, status={refund_status}"
)
return {"code": "SUCCESS", "message": "OK"}
except Exception as e:
logger.exception(f"WeChat callback processing error: {e}")
+29 -15
View File
@@ -452,7 +452,7 @@ def _create_wechat_order(order: PaymentOrder, db_configs: dict[str, str]) -> str
try:
# 调用微信支付 Native 下单接口
code, result = client.pay(
description=f"充值订单 {order.order_no}",
description=f"充值积分{order.credits}订单 {order.order_no}",
out_trade_no=order.order_no,
amount={
"total": int(order.amount * 100), # 微信支付以分为单位
@@ -565,7 +565,10 @@ async def _refund_wechat_order(
refund_reason: str,
db_configs: dict[str, str]
) -> dict:
"""Call WeChat Pay refund API."""
"""Call WeChat Pay refund API.
微信退款是异步的,调用后会返回PROCESSING状态,实际退款结果通过回调通知。
"""
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", "")
@@ -599,18 +602,29 @@ async def _refund_wechat_order(
data = _parse_wechat_result(result)
if code == 200 and data.get('status') == 'SUCCESS':
logger.info(f"WeChat refund succeeded: order_no={order.order_no}")
return {"success": True, "refund_id": data.get('refund_id')}
else:
logger.error(
f"WeChat refund failed: order_no={order.order_no}, "
f"code={code}, result={result}"
)
return {
"success": False,
"message": f"微信退款失败: code={code}, {data.get('code', '')}"
}
# 微信退款是异步的,PROCESSING是正常状态,表示退款已受理
if code == 200:
status = data.get('status')
if status in ('SUCCESS', 'PROCESSING', 'REFUNDCLOSE'):
logger.info(
f"WeChat refund initiated: order_no={order.order_no}, "
f"status={status}, refund_id={data.get('refund_id')}"
)
return {
"success": True,
"refund_id": data.get('refund_id'),
"status": status,
"message": "退款申请已提交,等待微信处理"
}
logger.error(
f"WeChat refund failed: order_no={order.order_no}, "
f"code={code}, result={result}"
)
return {
"success": False,
"message": f"微信退款失败: code={code}, {data.get('message', '')}"
}
except Exception as e:
logger.exception(f"WeChat refund exception: order_no={order.order_no}")
return {"success": False, "message": f"微信退款异常: {str(e)}"}
@@ -656,7 +670,7 @@ def _create_alipay_order(order: PaymentOrder, db_configs: dict[str, str]) -> str
model = AlipayTradePrecreateModel()
model.out_trade_no = order.order_no
model.total_amount = f"{order.amount:.2f}"
model.subject = f"充值订单 {order.order_no}"
model.subject = f"充值积分{order.credits}订单 {order.order_no}"
model.product_code = "QR_CODE_OFFLINE"
body_parts = []