"""支付宝当面付扫码(precreate)。""" from __future__ import annotations import json import logging from base64 import b64decode, b64encode from datetime import datetime from typing import Any from urllib.parse import quote_plus, urlencode import httpx from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding from auth.system_settings import SystemSettingsData logger = logging.getLogger("payments.alipay") ALIPAY_GATEWAY = "https://openapi.alipay.com/gateway.do" ALIPAY_SANDBOX_GATEWAY = "https://openapi-sandbox.dl.alipaydev.com/gateway.do" def _normalize_pem(text: str, label: str) -> str: body = text.strip() if "BEGIN" in body: return body return f"-----BEGIN {label}-----\n{body}\n-----END {label}-----" def _load_private_key(pem: str): return serialization.load_pem_private_key( _normalize_pem(pem, "RSA PRIVATE KEY").encode("utf-8"), password=None, ) def _load_public_key(pem: str): return serialization.load_pem_public_key( _normalize_pem(pem, "PUBLIC KEY").encode("utf-8"), ) def _sign_content(content: str, private_key) -> str: signature = private_key.sign( content.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256(), ) return b64encode(signature).decode("utf-8") def _gateway(settings: SystemSettingsData) -> str: return ALIPAY_SANDBOX_GATEWAY if settings.alipay_sandbox else ALIPAY_GATEWAY def create_precreate_order( settings: SystemSettingsData, order_no: str, subject: str, amount_yuan: str, notify_url: str, ) -> str: """创建当面付预下单,返回 qr_code 字符串。""" private_key = _load_private_key(settings.alipay_private_key) biz_content = { "out_trade_no": order_no, "total_amount": amount_yuan, "subject": subject[:256], } params = { "app_id": settings.alipay_app_id.strip(), "method": "alipay.trade.precreate", "format": "JSON", "charset": "utf-8", "sign_type": "RSA2", "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "version": "1.0", "notify_url": notify_url, "biz_content": json.dumps(biz_content, ensure_ascii=False, separators=(",", ":")), } unsigned = "&".join( f"{k}={quote_plus(str(v), safe='')}" for k, v in sorted(params.items()) if v is not None and str(v) != "" ) params["sign"] = _sign_content(unsigned, private_key) with httpx.Client(timeout=30.0) as client: resp = client.post(_gateway(settings), data=params) resp.raise_for_status() data = resp.json() key = "alipay_trade_precreate_response" result = data.get(key) or {} if result.get("code") != "10000": raise RuntimeError(result.get("sub_msg") or result.get("msg") or "支付宝下单失败") qr_code = result.get("qr_code") if not qr_code: raise RuntimeError("支付宝未返回 qr_code") return qr_code def verify_notify(params: dict[str, Any], settings: SystemSettingsData) -> bool: sign = params.get("sign") if not sign: return False verify_params = {k: v for k, v in params.items() if k not in ("sign", "sign_type") and v is not None} unsigned = "&".join( f"{k}={v}" for k, v in sorted(verify_params.items()) if str(v) != "" ) try: public_key = _load_public_key(settings.alipay_public_key) public_key.verify( b64decode(sign), unsigned.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256(), ) return True except Exception as exc: logger.warning("Alipay notify verify failed: %s", exc) return False