"""微信支付 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