"""微信支付渠道(V3):App 下单(服务端签名) + 回调验签/AES-GCM 解密。 密钥未配置(开发环境)时:下单返回 dev_mock 占位,回调一律拒绝入账。 生产接入需配置:WECHAT_APP_ID / WECHAT_MCHID / WECHAT_API_V3_KEY / WECHAT_PRIVATE_KEY(商户 API 私钥) / WECHAT_CERT_SERIAL_NO(商户证书序列号)/ WECHAT_PLATFORM_CERT(微信支付平台证书 PEM)/ WECHAT_NOTIFY_URL。 """ import json import time import uuid from app.config import settings from app.payment.base import PaymentConfigError, rsa2_sign, rsa2_verify, wechat_decrypt_resource APP_PATH = "/v3/pay/transactions/app" APP_PACKAGE = "Sign=WXPay" DEFAULT_SUBJECT = "积分充值" HTTP_TIMEOUT = 10 def is_configured() -> bool: return bool( settings.wechat_mchid and settings.wechat_app_id and settings.wechat_api_v3_key and settings.wechat_private_key ) def _authorization(method: str, url_path: str, body: str) -> str: """构造 V3 请求签名的 Authorization 头(WECHATPAY2-SHA256-RSA2048)""" timestamp = str(int(time.time())) nonce = uuid.uuid4().hex message = f"{method}\n{url_path}\n{timestamp}\n{nonce}\n{body}\n" signature = rsa2_sign(message, settings.wechat_private_key) return ( f'WECHATPAY2-SHA256-RSA2048 mchid="{settings.wechat_mchid}",' f'nonce_str="{nonce}",signature="{signature}",' f'timestamp="{timestamp}",serial_no="{settings.wechat_cert_serial_no}"' ) async def _create_prepay_id(order_no: str, amount_fen: int) -> str: """V3 下单(App 支付)→ prepay_id""" import httpx # 延迟导入:未配置渠道时无需依赖 body = json.dumps({ "appid": settings.wechat_app_id, "mchid": settings.wechat_mchid, "description": DEFAULT_SUBJECT, "out_trade_no": order_no, "notify_url": settings.wechat_notify_url, "amount": {"total": amount_fen, "currency": "CNY"}, }, ensure_ascii=False, separators=(",", ":")) headers = { "Authorization": _authorization("POST", APP_PATH, body), "Content-Type": "application/json", "Accept": "application/json", "User-Agent": "video-server/1.0", } async with httpx.AsyncClient(base_url=settings.wechat_api_base, timeout=HTTP_TIMEOUT) as client: resp = await client.post(APP_PATH, content=body.encode("utf-8"), headers=headers) if resp.status_code != 200: raise PaymentConfigError(f"微信下单失败: {resp.status_code} {resp.text[:200]}") prepay_id = (resp.json() or {}).get("prepay_id") or "" if not prepay_id: raise PaymentConfigError("微信下单未返回 prepay_id") return prepay_id async def create_app_pay_params(order_no: str, amount_fen: int) -> dict: """生成 App SDK 调起参数(未配置密钥时返回 dev 占位)""" if not is_configured(): return {"dev_mock": True, "prepayid": f"mock-{order_no}"} prepay_id = await _create_prepay_id(order_no, amount_fen) # App 调起需二次签名(不同于接口签名串) timestamp = str(int(time.time())) noncestr = uuid.uuid4().hex sign_str = ( f"{settings.wechat_app_id}\n{settings.wechat_mchid}\n{prepay_id}\n" f"{APP_PACKAGE}\n{noncestr}\n{timestamp}\n" ) return { "appid": settings.wechat_app_id, "partnerid": settings.wechat_mchid, "prepayid": prepay_id, "package": APP_PACKAGE, "noncestr": noncestr, "timestamp": timestamp, "sign": rsa2_sign(sign_str, settings.wechat_private_key), } def verify_callback(headers: dict, body: bytes) -> dict | None: """V3 回调验签 + AES-GCM 解密,成功返回明文报文 dict,失败返回 None(未配置一律 None)""" if not is_configured() or not settings.wechat_platform_cert: return None timestamp = headers.get("wechatpay-timestamp", "") nonce = headers.get("wechatpay-nonce", "") signature = headers.get("wechatpay-signature", "") if not (timestamp and nonce and signature): return None # 验签串:timestamp\nnonce\nbody\n(body 为原始报文,勿用解析后结果) message = f"{timestamp}\n{nonce}\n{body.decode('utf-8')}\n" if not rsa2_verify(message, signature, settings.wechat_platform_cert): return None try: payload = json.loads(body.decode("utf-8")) except Exception: # noqa: BLE001 return None resource = payload.get("resource") or {} return wechat_decrypt_resource(resource, settings.wechat_api_v3_key)