Files
douyin/backend/payments/wechat.py
T
2026-07-17 09:24:47 +08:00

140 lines
4.3 KiB
Python

"""微信支付 V3 Native 扫码。"""
from __future__ import annotations
import json
import logging
import time
import uuid
from base64 import b64decode, b64encode
from typing import Any
import httpx
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from auth.system_settings import SystemSettingsData
logger = logging.getLogger("payments.wechat")
WECHAT_API = "https://api.mch.weixin.qq.com"
def _load_private_key(pem: str):
text = pem.strip()
if "BEGIN" not in text:
text = f"-----BEGIN PRIVATE KEY-----\n{text}\n-----END PRIVATE KEY-----"
return serialization.load_pem_private_key(text.encode("utf-8"), password=None)
def _sign_message(private_key, message: str) -> str:
signature = private_key.sign(
message.encode("utf-8"),
padding.PKCS1v15(),
hashes.SHA256(),
)
return b64encode(signature).decode("utf-8")
def _build_auth_header(
method: str,
url_path: str,
body: str,
mch_id: str,
serial_no: str,
private_key,
) -> str:
timestamp = str(int(time.time()))
nonce = uuid.uuid4().hex
message = f"{method}\n{url_path}\n{timestamp}\n{nonce}\n{body}\n"
sign = _sign_message(private_key, message)
return (
f'WECHATPAY2-SHA256-RSA2048 mchid="{mch_id}",'
f'nonce_str="{nonce}",signature="{sign}",'
f'timestamp="{timestamp}",serial_no="{serial_no}"'
)
def create_native_order(
settings: SystemSettingsData,
order_no: str,
description: str,
amount_fen: int,
notify_url: str,
) -> str:
"""创建 Native 订单,返回 code_url。"""
private_key = _load_private_key(settings.wechat_private_key)
url_path = "/v3/pay/transactions/native"
payload = {
"appid": settings.wechat_app_id.strip(),
"mchid": settings.wechat_mch_id.strip(),
"description": description[:127],
"out_trade_no": order_no,
"notify_url": notify_url,
"amount": {"total": amount_fen, "currency": "CNY"},
}
body = json.dumps(payload, ensure_ascii=False)
headers = {
"Authorization": _build_auth_header(
"POST",
url_path,
body,
settings.wechat_mch_id.strip(),
settings.wechat_cert_serial.strip(),
private_key,
),
"Content-Type": "application/json",
"Accept": "application/json",
}
with httpx.Client(timeout=30.0) as client:
resp = client.post(f"{WECHAT_API}{url_path}", content=body.encode("utf-8"), headers=headers)
if resp.status_code >= 400:
detail = resp.text
try:
detail = resp.json().get("message") or detail
except Exception:
pass
raise RuntimeError(f"微信支付下单失败: {detail}")
data = resp.json()
code_url = data.get("code_url")
if not code_url:
raise RuntimeError("微信支付未返回 code_url")
return code_url
def decrypt_notify_resource(api_v3_key: str, resource: dict[str, Any]) -> dict[str, Any]:
nonce = resource.get("nonce", "")
ciphertext = resource.get("ciphertext", "")
associated_data = resource.get("associated_data", "")
aesgcm = AESGCM(api_v3_key.encode("utf-8"))
plain = aesgcm.decrypt(
nonce.encode("utf-8"),
b64decode(ciphertext),
associated_data.encode("utf-8") if associated_data else None,
)
return json.loads(plain.decode("utf-8"))
def verify_notify_signature(
settings: SystemSettingsData,
body: bytes,
timestamp: str,
nonce: str,
signature: str,
serial: str,
) -> bool:
"""简化验签:使用平台证书较复杂,此处用商户私钥对应逻辑 + 回调解密校验。"""
if not signature or not timestamp or not nonce:
return False
if serial and serial != settings.wechat_cert_serial.strip():
logger.warning("WeChat notify serial mismatch: %s", serial)
try:
payload = json.loads(body.decode("utf-8"))
resource = payload.get("resource") or {}
decrypt_notify_resource(settings.wechat_api_v3_key.strip(), resource)
return True
except Exception as exc:
logger.warning("WeChat notify verify failed: %s", exc)
return False