469 lines
22 KiB
Python
469 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""微信支付服务商 · 分账 / 资金托管客户端(懒加载单例)。
|
||
|
||
覆盖能力(服务商模式 partner_mode=True,mchid=服务商商户号):
|
||
- 添加分账接收方(MERCHANT_ID 子商户号 / PERSONAL_OPENID 个人微信零钱)
|
||
- 请求分账(按订单分账:OPC 收款 95% + 平台佣金 5%)
|
||
- 查询分账 / 分账回退
|
||
- 分账结果回调验签解密(复用微信平台证书 + APIv3 密钥)
|
||
|
||
设计原则:
|
||
- 与 ``wxpay.py`` 直连收单客户端并存:收单继续走现有商户,分账用服务商商户号发起;
|
||
- 懒加载:缺服务商配置时 ``profitsharing_enabled()==False``,所有调用返回不可用,业务降级;
|
||
- 所有失败统一抛 RuntimeError(携带微信报文摘要),由调用方决定降级/重试。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
from typing import Optional
|
||
|
||
from wechatpayv3.async_ import AsyncWeChatPay, WeChatPayType
|
||
from wechatpayv3.async_.utils import aes_decrypt
|
||
|
||
from . import config as pay_config
|
||
|
||
# 进件申请状态枚举(微信 applyment_state)
|
||
APPLYMENT_STATE = {
|
||
"EDITTING": "资料编辑中",
|
||
"AUDITING": "微信审核中",
|
||
"REJECTED": "已驳回",
|
||
"TO_BE_CONFIRMED": "待账户验证(需超管扫码完成打款/法人验证)",
|
||
"TO_BE_SIGNED": "待签约(需超管扫码完成绑定+签约授权)",
|
||
"SIGNING": "开通权限中(签约后平台开通分账等产品权限)",
|
||
"FINISHED": "已完成(子商户号已下发)",
|
||
"CANCELED": "已作废",
|
||
}
|
||
# 待平台侧人工/超管介入的状态(前端应展示 sign_url 二维码引导超管扫码)
|
||
APPLYMENT_ACTION_STATES = ("TO_BE_CONFIRMED", "TO_BE_SIGNED", "SIGNING")
|
||
|
||
logger = logging.getLogger("pay.profitsharing")
|
||
|
||
_sp: Optional[AsyncWeChatPay] = None
|
||
_init_lock = False
|
||
_init_error: str = ""
|
||
|
||
|
||
async def _ensure_split_pay() -> bool:
|
||
"""确保服务商分账客户端已初始化(懒加载 + 双检锁)。"""
|
||
global _sp, _init_lock, _init_error
|
||
if _sp is not None:
|
||
return True
|
||
if not pay_config.profitsharing_enabled():
|
||
_init_error = "服务商分账配置不完整(PINEAGENTS_WX_SP_MCHID / 证书 / APIv3 / notify)"
|
||
return False
|
||
if _init_lock:
|
||
import asyncio
|
||
for _ in range(50):
|
||
if _sp is not None:
|
||
return True
|
||
await asyncio.sleep(0.1)
|
||
return False
|
||
_init_lock = True
|
||
try:
|
||
with open(pay_config.WECHATPAY_SP_PRIVATE_KEY_PATH, mode="r") as f:
|
||
private_key = f.read()
|
||
import os
|
||
public_key_path = pay_config.WECHATPAY_SP_PUBLIC_KEY_PATH
|
||
# 服务商模式:mchid=服务商商户号,appid=服务商 AppID;证书/密钥用服务商独立配置
|
||
# (若未单独配置则回退直连证书,仅适用于服务商与直连为同一商户号的场景)
|
||
_sp = AsyncWeChatPay(
|
||
wechatpay_type=WeChatPayType.NATIVE,
|
||
mchid=pay_config.WECHATPAY_SP_MCHID,
|
||
private_key=private_key,
|
||
cert_serial_no=pay_config.WECHATPAY_SP_CERT_SERIAL_NO,
|
||
appid=pay_config.WECHATPAY_SP_APPID or pay_config.WECHATPAY_NATIVE_APPID,
|
||
apiv3_key=pay_config.WECHATPAY_SP_APIV3_KEY,
|
||
notify_url=pay_config.WECHATPAY_SPLIT_NOTIFY_URL,
|
||
cert_dir=pay_config.WECHATPAY_SP_CERT_DIR,
|
||
logger=logger,
|
||
partner_mode=True,
|
||
public_key=(
|
||
open(public_key_path).read()
|
||
if public_key_path and os.path.exists(public_key_path)
|
||
else None
|
||
),
|
||
public_key_id=pay_config.WECHATPAY_SP_PUBLIC_KEY_ID or None,
|
||
)
|
||
await _sp.__aenter__()
|
||
logger.info("微信服务商分账客户端懒加载初始化成功 sp_mchid=%s", pay_config.WECHATPAY_SP_MCHID)
|
||
return True
|
||
except Exception as exc: # noqa: BLE001
|
||
_init_error = str(exc)
|
||
logger.error("微信服务商分账客户端初始化失败: %s", exc, exc_info=True)
|
||
return False
|
||
finally:
|
||
_init_lock = False
|
||
|
||
|
||
def init_error() -> str:
|
||
"""最近一次初始化失败原因(供接口 503 提示)。"""
|
||
return _init_error
|
||
|
||
|
||
def _parse(data, code: int, result) -> dict:
|
||
"""统一把 wechatpayv3 返回解析为 dict;非 200 抛 RuntimeError。"""
|
||
if code != 200:
|
||
raise RuntimeError(f"微信分账接口失败 http={code}: {str(result)[:300]}")
|
||
return result if isinstance(result, dict) else json.loads(result)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 分账接收方
|
||
# ---------------------------------------------------------------------------
|
||
async def add_receiver(*, account_type: str, account: str, name: str = "",
|
||
relation_type: str = "SERVICE_PROVIDER",
|
||
sub_mchid: str = "") -> dict:
|
||
"""添加分账接收方。
|
||
|
||
- account_type: MERCHANT_ID(商户号) | PERSONAL_OPENID(个人微信零钱)
|
||
- account: 子商户号 或 小程序 openid
|
||
- name: 个人实名(PERSONAL_OPENID 时传,微信校验实名一致)
|
||
- sub_mchid: 【服务商模式必填】分账的出资特约商户号(接收方被添加到该出资方名下)。
|
||
"""
|
||
if not await _ensure_split_pay():
|
||
raise RuntimeError(f"服务商分账未就绪: {_init_error or '未配置'}")
|
||
code, result = await _sp.profitsharing_add_receiver(
|
||
account_type=account_type, account=account,
|
||
relation_type=relation_type,
|
||
name=name or None,
|
||
appid=pay_config.WECHATPAY_SP_APPID or pay_config.WECHATPAY_NATIVE_APPID,
|
||
sub_mchid=sub_mchid or None,
|
||
)
|
||
return _parse(result, code, result)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 请求分账 / 查询 / 回退
|
||
# ---------------------------------------------------------------------------
|
||
async def create_split(*, transaction_id: str, out_order_no: str,
|
||
receivers: list[dict], sub_mchid: str = "",
|
||
unfreeze_unsplit: bool = True) -> dict:
|
||
"""请求分账。
|
||
|
||
- transaction_id: 微信支付单号(订单支付成功回调带回)
|
||
- out_order_no: 平台分账单号(幂等键,PS_<ts>_<hex>)
|
||
- receivers: [{type, account, amount(分), description}]
|
||
- sub_mchid: 服务商模式下收单子商户号(分账方)
|
||
- unfreeze_unsplit: True=分账同时解冻剩余;False=部分分账,后续可再分账/单独解冻
|
||
"""
|
||
if not await _ensure_split_pay():
|
||
raise RuntimeError(f"服务商分账未就绪: {_init_error or '未配置'}")
|
||
code, result = await _sp.profitsharing_order(
|
||
transaction_id=transaction_id,
|
||
out_order_no=out_order_no,
|
||
receivers=receivers,
|
||
unfreeze_unsplit=unfreeze_unsplit,
|
||
sub_mchid=sub_mchid or None,
|
||
)
|
||
return _parse(result, code, result)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 分账生命周期补充:剩余待分金额 / 分账比例 / 完结分账解冻剩余
|
||
# ---------------------------------------------------------------------------
|
||
async def query_remaining_amount(*, transaction_id: str) -> dict | None:
|
||
"""查询订单剩余待分金额(分)。支付成功且订单标记 profit_sharing 后可用。
|
||
|
||
返回 {"transaction_id": ..., "unsplit_amount": int(分), "split_fee": int(分), ...}。
|
||
用于"资金冻结确认":unsplit_amount 即该订单当前可发起分账的冻结资金。
|
||
"""
|
||
if not await _ensure_split_pay():
|
||
return None
|
||
try:
|
||
code, result = await _sp.profitsharing_amount_query(transaction_id=transaction_id)
|
||
except Exception as exc: # noqa: BLE001
|
||
logger.warning("查询剩余待分金额异常 tx=%s: %s", transaction_id, exc)
|
||
return None
|
||
if code != 200:
|
||
return None
|
||
return result if isinstance(result, dict) else json.loads(result)
|
||
|
||
|
||
async def query_split_config(*, sub_mchid: str) -> dict | None:
|
||
"""查询子商户分账配置(允许服务商分账的最大比例)。
|
||
|
||
对应微信接口:GET /v3/profitsharing/merchant-configs/{sub_mchid}。
|
||
返回 {"sub_mchid": ..., "max_ratio": int(万分比)}:
|
||
- max_ratio 单位为万分比(2000 = 20%),为子商户在商户平台设置的允许服务商分账的最大比例,
|
||
未授权/未开通时微信返回 403 NO_AUTH(此处记 None,由调用方提示需子商户在商户平台开通分账)。
|
||
注意:该接口仅返回 max_ratio,不返回分账开关;分账是否可用以能否成功 add_receiver/查询比例为准。
|
||
"""
|
||
if not await _ensure_split_pay():
|
||
return None
|
||
try:
|
||
code, result = await _sp.profitsharing_config_query(sub_mchid=sub_mchid)
|
||
except Exception as exc: # noqa: BLE001
|
||
logger.warning("查询分账配置异常 sub_mchid=%s: %s", sub_mchid, exc)
|
||
return None
|
||
if code != 200:
|
||
return None
|
||
data = result if isinstance(result, dict) else json.loads(result)
|
||
if isinstance(data, dict) and "max_ratio" in data:
|
||
# 万分比 → 百分比(保留一位小数),便于前端展示
|
||
data["max_ratio_percent"] = round(int(data["max_ratio"]) / 100.0, 1)
|
||
return data
|
||
|
||
|
||
async def unfreeze_remaining(*, transaction_id: str, out_order_no: str,
|
||
sub_mchid: str = "", description: str = "解冻剩余资金") -> dict:
|
||
"""完结分账,解冻订单剩余未分资金回出资方商户。
|
||
|
||
对应微信接口:POST /v3/profitsharing/orders/unfreeze。
|
||
- out_order_no: 分账完结单号,**不能与请求分账时的 out_order_no 相同**(需新生成);
|
||
- 解冻后该订单不能再发起分账。
|
||
"""
|
||
if not await _ensure_split_pay():
|
||
raise RuntimeError(f"服务商分账未就绪: {_init_error or '未配置'}")
|
||
code, result = await _sp.profitsharing_unfreeze(
|
||
transaction_id=transaction_id, out_order_no=out_order_no,
|
||
description=description, sub_mchid=sub_mchid or None,
|
||
)
|
||
return _parse(result, code, result)
|
||
|
||
|
||
def split_finished(result: dict) -> tuple[bool, str]:
|
||
"""判断分账结果是否全部成功:state=FINISHED 且所有 receivers.result=SUCCESS。
|
||
|
||
微信分账为异步:state=FINISHED 仅代表动账执行完毕,须逐接收方看 result。
|
||
返回 (全部成功?, 失败原因摘要)。
|
||
"""
|
||
if not isinstance(result, dict):
|
||
return False, "分账结果为空"
|
||
if result.get("state") == "PROCESSING":
|
||
return False, "分账处理中,请稍后查询"
|
||
if result.get("state") != "FINISHED":
|
||
return False, f"分账未完成 state={result.get('state')}"
|
||
receivers = result.get("receivers") or []
|
||
if not receivers:
|
||
return False, "分账结果缺少接收方明细"
|
||
failed = [r.get("fail_reason") or f"result={r.get('result')}" for r in receivers
|
||
if r.get("result") != "SUCCESS"]
|
||
return (not failed), (";".join(failed) if failed else "")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 服务商代子商户收单(partner transactions + profit_sharing 标记)
|
||
# ---------------------------------------------------------------------------
|
||
async def create_partner_order(*, client_type: str, out_trade_no: str, total_fen: int,
|
||
description: str, sub_mchid: str,
|
||
payer_sp_openid: str = "",
|
||
notify_url: str = "", profit_sharing: bool = True) -> dict:
|
||
"""服务商代子商户收单(资金直接进子商户账户并可分账)。
|
||
|
||
对应微信接口(partner transactions 系列):
|
||
- native: POST /v3/pay/partner/transactions/native → {code_url}
|
||
- jsapi : POST /v3/pay/partner/transactions/jsapi → {pay_params}(需 payer_sp_openid)
|
||
关键:profit_sharing=True 时订单带分账标记,支付成功后资金在子商户账户冻结,可分账。
|
||
"""
|
||
if not await _ensure_split_pay():
|
||
raise RuntimeError(f"服务商分账未就绪: {_init_error or '未配置'}")
|
||
if client_type == "jsapi" and not payer_sp_openid:
|
||
raise RuntimeError("JSAPI 收单缺少用户 openid(payer_sp_openid)")
|
||
notify = notify_url or pay_config.WECHATPAY_SPLIT_NOTIFY_URL
|
||
appid = pay_config.WECHATPAY_SP_APPID or pay_config.WECHATPAY_NATIVE_APPID
|
||
pay_type = WeChatPayType.MINIPROG if client_type == "jsapi" else WeChatPayType.NATIVE
|
||
payer = {"sp_openid": payer_sp_openid} if client_type == "jsapi" else None
|
||
settle_info = {"profit_sharing": True} if profit_sharing else None
|
||
try:
|
||
code, result = await _sp.pay(
|
||
description=description, out_trade_no=out_trade_no,
|
||
amount={"total": int(total_fen), "currency": "CNY"},
|
||
payer=payer, pay_type=pay_type, appid=appid,
|
||
sub_mchid=sub_mchid, settle_info=settle_info,
|
||
notify_url=notify,
|
||
)
|
||
except Exception as exc: # noqa: BLE001
|
||
raise RuntimeError(f"微信服务商收单失败: {str(exc)[:300]}") from exc
|
||
if code != 200:
|
||
raise RuntimeError(f"微信服务商收单失败 http={code}: {str(result)[:300]}")
|
||
data = result if isinstance(result, dict) else json.loads(result)
|
||
if client_type == "jsapi":
|
||
prepay_id = data.get("prepay_id") or ""
|
||
if not prepay_id:
|
||
raise RuntimeError(f"微信服务商收单无 prepay_id: {str(data)[:300]}")
|
||
time_stamp = str(int(__import__("time").time()))
|
||
import uuid
|
||
nonce_str = uuid.uuid4().hex
|
||
package = f"prepay_id={prepay_id}"
|
||
# 服务商模式:签名主体为服务商商户私钥 + 服务商 AppID
|
||
pay_sign = _sp.sign([appid, time_stamp, nonce_str, package])
|
||
return {"appid": appid, "prepay_id": prepay_id,
|
||
"pay_params": {"timeStamp": time_stamp, "nonceStr": nonce_str,
|
||
"package": package, "signType": "RSA", "paySign": pay_sign}}
|
||
code_url = data.get("code_url") or ""
|
||
if not code_url:
|
||
raise RuntimeError(f"微信服务商收单无 code_url: {str(data)[:300]}")
|
||
return {"appid": appid, "code_url": code_url}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 结算账户查询/修改(子商户进件后对公/法人结算账户管理)
|
||
# ---------------------------------------------------------------------------
|
||
async def query_settlement(*, sub_mchid: str) -> dict | None:
|
||
"""查询子商户结算账户(开户行/账号脱敏/结算类型等)。"""
|
||
if not await _ensure_split_pay():
|
||
return None
|
||
try:
|
||
code, result = await _sp.applyment_settlement_query(sub_mchid=sub_mchid)
|
||
except Exception as exc: # noqa: BLE001
|
||
logger.warning("查询结算账户异常 sub_mchid=%s: %s", sub_mchid, exc)
|
||
return None
|
||
if code != 200:
|
||
return None
|
||
return result if isinstance(result, dict) else json.loads(result)
|
||
|
||
|
||
async def query_split(*, transaction_id: str, out_order_no: str,
|
||
sub_mchid: str = "") -> Optional[dict]:
|
||
"""查询分账结果;无结果返回 None。"""
|
||
if not await _ensure_split_pay():
|
||
return None
|
||
try:
|
||
code, result = await _sp.profitsharing_order_query(
|
||
transaction_id=transaction_id, out_order_no=out_order_no,
|
||
sub_mchid=sub_mchid or None,
|
||
)
|
||
except Exception as exc: # noqa: BLE001
|
||
logger.warning("查询分账异常 order=%s: %s", out_order_no, exc)
|
||
return None
|
||
if code != 200:
|
||
return None
|
||
return result if isinstance(result, dict) else json.loads(result)
|
||
|
||
|
||
async def split_return(*, out_order_no: str, return_mchid: str, amount: int,
|
||
description: str = "分账回退", sub_mchid: str = "") -> dict:
|
||
"""分账回退(退款/纠错场景)。"""
|
||
if not await _ensure_split_pay():
|
||
raise RuntimeError(f"服务商分账未就绪: {_init_error or '未配置'}")
|
||
out_return_no = f"RT_{out_order_no}_{int(__import__('time').time())}"
|
||
code, result = await _sp.profitsharing_return(
|
||
out_return_no=out_return_no, return_mchid=return_mchid, amount=amount,
|
||
description=description, out_order_no=out_order_no, sub_mchid=sub_mchid or None,
|
||
)
|
||
return _parse(result, code, result)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 特约商户进件(applyment4sub:OPC 商户绑定前置,微信审核 1–3 工作日)
|
||
# ---------------------------------------------------------------------------
|
||
async def submit_applyment(*, business_code: str, contact_info: dict,
|
||
subject_info: dict, business_info: dict,
|
||
settlement_info: dict, bank_account_info: dict,
|
||
addition_info: dict | None = None) -> dict:
|
||
"""提交特约商户进件申请单。
|
||
|
||
敏感字段(联系人姓名/证件/手机/邮箱、法人、银行账户)由 wechatpayv3 内部
|
||
使用服务商商户 API 证书公钥加密后传输,不落平台明文。
|
||
返回 {applyment_id, applyment_state, ...};进件为异步审核(1–3 工作日)。
|
||
"""
|
||
if not await _ensure_split_pay():
|
||
raise RuntimeError(f"服务商分账未就绪: {_init_error or '未配置'}")
|
||
code, result = await _sp.applyment_submit(
|
||
business_code=business_code,
|
||
contact_info=contact_info or None,
|
||
subject_info=subject_info or None,
|
||
business_info=business_info or None,
|
||
settlement_info=settlement_info or None,
|
||
bank_account_info=bank_account_info or None,
|
||
addition_info=addition_info or None,
|
||
)
|
||
return _parse(result, code, result)
|
||
|
||
|
||
def normalize_applyment_state(state: str) -> str:
|
||
"""把微信 applyment_state 规范化为裸枚举(去掉 APPLYMENT_STATE_ 前缀)。
|
||
|
||
官方查询接口返回形如 ``APPLYMENT_STATE_FINISHED``;SDK 原样透传不剥前缀。
|
||
统一转成裸枚举(FINISHED / TO_BE_SIGNED / ...),供业务层与前端一致使用。
|
||
兼容已剥前缀的返回值。
|
||
"""
|
||
if not state:
|
||
return state
|
||
if state.startswith("APPLYMENT_STATE_"):
|
||
return state[len("APPLYMENT_STATE_"):]
|
||
return state
|
||
|
||
|
||
async def query_applyment(*, business_code: str = "", applyment_id: str = "") -> dict | None:
|
||
"""查询特约商户进件申请状态;失败返回 None。
|
||
|
||
applyment_state 枚举(见模块头 APPLYMENT_STATE,已规范化去掉前缀):
|
||
EDITTING / AUDITING / REJECTED / TO_BE_CONFIRMED(待账户验证) /
|
||
TO_BE_SIGNED(待签约) / SIGNING(开通权限中) / FINISHED / CANCELED。
|
||
关键返回字段:
|
||
- sign_url:超级管理员签约链接(查询即返回),超管用微信扫码→关注"微信支付商家助手"
|
||
→根据公众号指引完成【核对联系信息 + 账户验证(打款/法人) + 签约授权】;
|
||
- account_validation:账户验证信息(打款验证收款账户、金额区间、验证截止时间);
|
||
- sub_mchid:TO_BE_SIGNED / SIGNING / FINISHED 时返回(特约商户号);
|
||
- audit_detail:REJECTED 时返回 [{field, field_name, reject_reason}](驳回原因详情)。
|
||
"""
|
||
if not await _ensure_split_pay():
|
||
return None
|
||
try:
|
||
if business_code:
|
||
code, result = await _sp.applyment_query(business_code=business_code)
|
||
elif applyment_id:
|
||
code, result = await _sp.applyment_query(applyment_id=applyment_id)
|
||
else:
|
||
return None
|
||
except Exception as exc: # noqa: BLE001
|
||
logger.warning("查询进件异常 applyment_id=%s: %s", applyment_id, exc)
|
||
return None
|
||
if code != 200:
|
||
return None
|
||
data = result if isinstance(result, dict) else json.loads(result)
|
||
if isinstance(data, dict) and data.get("applyment_state"):
|
||
data["applyment_state"] = normalize_applyment_state(data["applyment_state"])
|
||
return data
|
||
|
||
|
||
async def upload_image(*, filepath: str, filename: str = "") -> dict:
|
||
"""上传进件素材(营业执照/法人证件/银行账户证明等)→ 返回微信 media_id。
|
||
|
||
微信素材 media_id 有效期为 3 天,需在提交进件前上传并立即使用。
|
||
"""
|
||
if not await _ensure_split_pay():
|
||
raise RuntimeError(f"服务商分账未就绪: {_init_error or '未配置'}")
|
||
code, result = await _sp.image_upload(filepath=filepath, filename=filename or None)
|
||
if code != 200:
|
||
raise RuntimeError(f"微信素材上传失败 http={code}: {str(result)[:300]}")
|
||
return result if isinstance(result, dict) else json.loads(result)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 分账结果回调验签 + 解密(复用同一平台证书 / APIv3 密钥体系)
|
||
# ---------------------------------------------------------------------------
|
||
async def verify_split_notify(headers, body) -> Optional[dict]:
|
||
"""验证微信分账回调签名并解密 resource;失败返回 None。
|
||
|
||
分账回调与支付回调同用微信平台证书 + APIv3 密钥;若直连客户端已初始化,
|
||
直接复用 wxpay.verify_and_decrypt,否则用服务商客户端验签。
|
||
"""
|
||
from . import wxpay
|
||
if await _ensure_split_pay():
|
||
body_str = body.decode("UTF-8") if isinstance(body, bytes) else body
|
||
if await _sp._core._verify_signature_async(headers, body_str):
|
||
data = json.loads(body_str)
|
||
resource = data.get("resource")
|
||
if not resource:
|
||
logger.error("分账回调缺少 resource 字段")
|
||
return None
|
||
try:
|
||
decrypted = aes_decrypt(
|
||
nonce=resource.get("nonce"),
|
||
ciphertext=resource.get("ciphertext"),
|
||
associated_data=resource.get("associated_data") or "",
|
||
apiv3_key=_sp._core._apiv3_key,
|
||
)
|
||
except Exception as exc: # noqa: BLE001
|
||
logger.error("分账回调解密异常: %s", exc)
|
||
return None
|
||
if not decrypted:
|
||
return None
|
||
data.update({"resource": json.loads(decrypted)})
|
||
return data
|
||
# 回退:直连客户端验签
|
||
return await wxpay.verify_and_decrypt(headers, body)
|