更新
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""微信支付 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
|
||||
Reference in New Issue
Block a user