wechat_pay.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. """微信支付工具模块
  2. 支持微信支付 V3 API
  3. 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.html
  4. 使用方式:
  5. from app.core.wechat_pay import wechat_pay, WechatPayClient
  6. # 统一下单
  7. result = wechat_pay.unified_order(out_trade_no, total_amount, description, notify_url)
  8. # 申请退款
  9. result = wechat_pay.refund(transaction_id, refund_amount, total_amount)
  10. """
  11. import hashlib
  12. import time
  13. import random
  14. import string
  15. import json
  16. import base64
  17. import xml.etree.ElementTree as ET
  18. from typing import Optional, Dict, Any
  19. from datetime import datetime
  20. from urllib.parse import urljoin
  21. import ssl
  22. import urllib.request
  23. import urllib.error
  24. from app.core.config import settings
  25. class WechatPayClient:
  26. """微信支付客户端"""
  27. def __init__(self):
  28. self.mchid = settings.WXPAY_MCHID
  29. self.appid = settings.WXPAY_APPID
  30. self.api_key = settings.WXPAY_API_KEY
  31. self.cert_path = settings.WXPAY_CERT_PATH
  32. self.private_key_path = settings.WXPAY_PRIVATE_KEY_PATH
  33. self.sandbox = settings.WXPAY_SANDBOX
  34. # API 版本
  35. self.api_base = "https://api.mchpay.qpic.cn/v3" if self.sandbox else "https://api.mchpay.qpic.cn/v3"
  36. def _generate_nonce_str(self, length: int = 32) -> str:
  37. """生成随机字符串"""
  38. return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
  39. def _generate_sign(self, data: Dict[str, Any]) -> str:
  40. """生成签名 (V2 API)"""
  41. # V2 签名算法
  42. sorted_keys = sorted(data.keys())
  43. sign_str = "&".join([f"{k}={data[k]}" for k in sorted_keys if data[k]])
  44. sign_str += f"&key={self.api_key}"
  45. return hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
  46. def _generate_sign_v3(self, method: str, url: str, timestamp: str, nonce: str, body: str = "") -> str:
  47. """生成签名 V3 (SHA256 with RSA)"""
  48. try:
  49. from cryptography.hazmat.primitives import hashes, serialization
  50. from cryptography.hazmat.primitives.asymmetric import padding
  51. from cryptography.hazmat.backends import default_backend
  52. from cryptography.x509 import load_pem_x509_certificate
  53. with open(self.private_key_path, 'r') as f:
  54. private_key = f.read()
  55. # 构建签名串
  56. sign_str = f"{method}\n{url}\n{timestamp}\n{nonce}\n{body}\n"
  57. # 使用私钥签名
  58. from cryptography.hazmat.primitives import hashes
  59. from cryptography.hazmat.primitives.asymmetric import padding
  60. key = serialization.load_pem_private_key(
  61. private_key.encode(),
  62. password=None,
  63. backend=default_backend()
  64. )
  65. signature = key.sign(
  66. sign_str.encode('utf-8'),
  67. padding.PKCS1v15(),
  68. hashes.SHA256()
  69. )
  70. return base64.b64encode(signature).decode('utf-8')
  71. except ImportError:
  72. raise ImportError("请安装 cryptography: pip install cryptography")
  73. def _make_request(self, url: str, method: str = "GET", data: Dict = None, need_cert: bool = False) -> Dict:
  74. """发送HTTP请求"""
  75. headers = {
  76. 'Content-Type': 'application/json',
  77. 'User-Agent': 'SQXP/1.0'
  78. }
  79. body = json.dumps(data) if data else ""
  80. try:
  81. if need_cert and self.cert_path and self.private_key_path:
  82. import ssl
  83. context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  84. context.load_cert_chain(self.cert_path, self.private_key_path)
  85. req = urllib.request.Request(url, data=body.encode(), headers=headers, method=method)
  86. with urllib.request.urlopen(req, context=context) as response:
  87. return json.loads(response.read().decode())
  88. else:
  89. req = urllib.request.Request(url, data=body.encode(), headers=headers, method=method)
  90. with urllib.request.urlopen(req) as response:
  91. return json.loads(response.read().decode())
  92. except urllib.error.HTTPError as e:
  93. error_body = e.read().decode() if e.fp else ""
  94. try:
  95. return json.loads(error_body)
  96. except:
  97. return {"error": error_body, "code": e.code}
  98. except Exception as e:
  99. return {"error": str(e), "code": -1}
  100. def unified_order(
  101. self,
  102. out_trade_no: str,
  103. total_amount: int,
  104. description: str,
  105. notify_url: str,
  106. openid: str = None
  107. ) -> Dict[str, Any]:
  108. """
  109. 统一下单接口 (V3)
  110. Args:
  111. out_trade_no: 商户订单号
  112. total_amount: 订单金额(分)
  113. description: 商品描述
  114. notify_url: 回调通知地址
  115. openid: 用户openid (JSAPI支付必需)
  116. Returns:
  117. 返回支付参数,用于调起微信支付
  118. """
  119. timestamp = str(int(time.time()))
  120. nonce_str = self._generate_nonce_str()
  121. url = f"{self.api_base}/transactions/jsapi"
  122. data = {
  123. "appid": self.appid,
  124. "mchid": self.mchid,
  125. "description": description,
  126. "out_trade_no": out_trade_no,
  127. "time_expire": datetime.now().strftime("%Y-%m-%dT%H:%M:%S+08:00"),
  128. "attach": "sqxp_order",
  129. "notify_url": notify_url,
  130. "amount": {
  131. "total": total_amount,
  132. "currency": "CNY"
  133. },
  134. "payer": {
  135. "openid": openid
  136. } if openid else None
  137. }
  138. # 移除None值
  139. data = {k: v for k, v in data.items() if v is not None}
  140. body = json.dumps(data)
  141. sign = self._generate_sign_v3("POST", f"/v3/transactions/jsapi", timestamp, nonce_str, body)
  142. headers = {
  143. "Authorization": f'WECHATPAY2-SHA256-RSA2048 mchid="{self.mchid}",nonce_str="{nonce_str}",signature="{sign}",timestamp="{timestamp}",serial_no="{settings.WXPAY_SERIAL_NO}"',
  144. "Content-Type": "application/json",
  145. "Accept": "application/json"
  146. }
  147. try:
  148. import ssl
  149. context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  150. context.check_hostname = False
  151. context.verify_mode = ssl.CERT_NONE
  152. if self.cert_path and self.private_key_path:
  153. context.load_cert_chain(self.cert_path, self.private_key_path)
  154. req = urllib.request.Request(url, data=body.encode(), headers=headers, method="POST")
  155. with urllib.request.urlopen(req, context=context, timeout=30) as response:
  156. result = json.loads(response.read().decode())
  157. if "prepay_id" in result:
  158. # 返回调起支付的参数
  159. pay_sign = self._generate_pay_sign(result["prepay_id"])
  160. return {
  161. "success": True,
  162. "prepay_id": result["prepay_id"],
  163. "pay_sign": pay_sign,
  164. "timestamp": timestamp,
  165. "nonce_str": nonce_str
  166. }
  167. else:
  168. return {"success": False, "error": result.get("message", "下单失败")}
  169. except Exception as e:
  170. return {"success": False, "error": str(e)}
  171. def _generate_pay_sign(self, prepay_id: str) -> str:
  172. """生成支付签名 (小程序调起支付用)"""
  173. timestamp = str(int(time.time()))
  174. nonce_str = self._generate_nonce_str()
  175. sign_str = f"{self.appid}\n{timestamp}\n{nonce_str}\nprepay_id={prepay_id}\n"
  176. return hashlib.sha256(sign_str.encode('utf-8')).hexdigest()
  177. def query_order(self, out_trade_no: str = None, transaction_id: str = None) -> Dict:
  178. """查询订单"""
  179. if not out_trade_no and not transaction_id:
  180. return {"success": False, "error": "缺少订单号"}
  181. timestamp = str(int(time.time()))
  182. nonce_str = self._generate_nonce_str()
  183. if out_trade_no:
  184. url = f"{self.api_base}/v3/pay/transactions/out-trade-no/{out_trade_no}?mchid={self.mchid}"
  185. else:
  186. url = f"{self.api_base}/v3/pay/transactions/id/{transaction_id}?mchid={self.mchid}"
  187. headers = {
  188. "Authorization": f'WECHATPAY2-SHA256-RSA2048 mchid="{self.mchid}",nonce_str="{nonce_str}",signature="",timestamp="{timestamp}",serial_no=""',
  189. "Accept": "application/json"
  190. }
  191. try:
  192. import ssl
  193. context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  194. context.check_hostname = False
  195. context.verify_mode = ssl.CERT_NONE
  196. req = urllib.request.Request(url, headers=headers, method="GET")
  197. with urllib.request.urlopen(req, context=context, timeout=30) as response:
  198. result = json.loads(response.read().decode())
  199. return {"success": True, "data": result}
  200. except urllib.error.HTTPError as e:
  201. return {"success": False, "error": f"HTTP {e.code}: {e.read().decode()}"}
  202. except Exception as e:
  203. return {"success": False, "error": str(e)}
  204. def refund(
  205. self,
  206. out_trade_no: str,
  207. total_amount: int,
  208. refund_amount: int,
  209. reason: str = None
  210. ) -> Dict:
  211. """
  212. 申请退款
  213. Args:
  214. out_trade_no: 商户订单号
  215. total_amount: 订单总金额(分)
  216. refund_amount: 退款金额(分)
  217. reason: 退款原因
  218. Returns:
  219. 退款结果
  220. """
  221. if refund_amount > total_amount:
  222. return {"success": False, "error": "退款金额不能大于订单金额"}
  223. timestamp = str(int(time.time()))
  224. nonce_str = self._generate_nonce_str()
  225. out_refund_no = f"REF{datetime.now().strftime('%Y%m%d%H%M%S')}{random.randint(1000, 9999)}"
  226. url = f"{self.api_base}/v3/refund/domestic/refunds"
  227. data = {
  228. "out_trade_no": out_trade_no,
  229. "out_refund_no": out_refund_no,
  230. "reason": reason or "用户申请退款",
  231. "notify_url": settings.WXPAY_REFUND_NOTIFY_URL,
  232. "amount": {
  233. "refund": refund_amount,
  234. "total": total_amount,
  235. "currency": "CNY"
  236. }
  237. }
  238. body = json.dumps(data)
  239. sign = self._generate_sign_v3("POST", "/v3/refund/domestic/refunds", timestamp, nonce_str, body)
  240. headers = {
  241. "Authorization": f'WECHATPAY2-SHA256-RSA2048 mchid="{self.mchid}",nonce_str="{nonce_str}",signature="{sign}",timestamp="{timestamp}",serial_no="{settings.WXPAY_SERIAL_NO}"',
  242. "Content-Type": "application/json",
  243. "Accept": "application/json"
  244. }
  245. try:
  246. import ssl
  247. context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  248. context.check_hostname = False
  249. context.verify_mode = ssl.CERT_NONE
  250. if self.cert_path and self.private_key_path:
  251. context.load_cert_chain(self.cert_path, self.private_key_path)
  252. req = urllib.request.Request(url, data=body.encode(), headers=headers, method="POST")
  253. with urllib.request.urlopen(req, context=context, timeout=30) as response:
  254. result = json.loads(response.read().decode())
  255. return {
  256. "success": True,
  257. "refund_id": result.get("refund_id"),
  258. "out_refund_no": out_refund_no,
  259. "refund_status": result.get("status")
  260. }
  261. except urllib.error.HTTPError as e:
  262. error_body = e.read().decode()
  263. return {"success": False, "error": f"HTTP {e.code}: {error_body}"}
  264. except Exception as e:
  265. return {"success": False, "error": str(e)}
  266. def close_order(self, out_trade_no: str) -> Dict:
  267. """关闭订单"""
  268. timestamp = str(int(time.time()))
  269. nonce_str = self._generate_nonce_str()
  270. url = f"{self.api_base}/v3/transactions/out-trade-no/{out_trade_no}/close"
  271. data = {"mchid": self.mchid}
  272. body = json.dumps(data)
  273. sign = self._generate_sign_v3("POST", f"/v3/transactions/out-trade-no/{out_trade_no}/close", timestamp, nonce_str, body)
  274. headers = {
  275. "Authorization": f'WECHATPAY2-SHA256-RSA2048 mchid="{self.mchid}",nonce_str="{nonce_str}",signature="{sign}",timestamp="{timestamp}",serial_no="{settings.WXPAY_SERIAL_NO}"',
  276. "Content-Type": "application/json"
  277. }
  278. try:
  279. import ssl
  280. context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  281. context.check_hostname = False
  282. context.verify_mode = ssl.CERT_NONE
  283. if self.cert_path and self.private_key_path:
  284. context.load_cert_chain(self.cert_path, self.private_key_path)
  285. req = urllib.request.Request(url, data=body.encode(), headers=headers, method="POST")
  286. with urllib.request.urlopen(req, context=context, timeout=30) as response:
  287. return {"success": True}
  288. except urllib.error.HTTPError as e:
  289. return {"success": False, "error": f"HTTP {e.code}: {e.read().decode()}"}
  290. except Exception as e:
  291. return {"success": False, "error": str(e)}
  292. def get_wechat_pay_client() -> WechatPayClient:
  293. """获取微信支付客户端实例"""
  294. return WechatPayClient()
  295. # 全局实例 (延迟初始化)
  296. _wechat_pay_client: Optional[WechatPayClient] = None
  297. def wechat_pay() -> WechatPayClient:
  298. """获取微信支付客户端"""
  299. global _wechat_pay_client
  300. if _wechat_pay_client is None:
  301. _wechat_pay_client = WechatPayClient()
  302. return _wechat_pay_client
  303. class WechatPayV2Client:
  304. """微信支付 V2 客户端 (兼容旧版)"""
  305. def __init__(self):
  306. self.mchid = settings.WXPAY_MCHID
  307. self.appid = settings.WXPAY_APPID
  308. self.api_key = settings.WXPAY_API_KEY
  309. self.appid_mini = settings.WXPAY_MINI_APPID
  310. self.notify_url = settings.WXPAY_NOTIFY_URL
  311. if settings.WXPAY_SANDBOX:
  312. self.pay_url = "https://sandbox.mch.weixin.qq.com/pay/unifiedorder"
  313. else:
  314. self.pay_url = "https://api.mch.weixin.qq.com/pay/unifiedorder"
  315. def _generate_sign(self, params: Dict) -> str:
  316. """生成签名"""
  317. sorted_keys = sorted(params.keys())
  318. sign_str = "&".join([f"{k}={params[k]}" for k in sorted_keys if params[k]])
  319. sign_str += f"&key={self.api_key}"
  320. return hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
  321. def _dict_to_xml(self, params: Dict) -> str:
  322. """字典转XML"""
  323. xml_str = "<xml>"
  324. for k, v in params.items():
  325. xml_str += f"<{k}><![CDATA[{v}]]></{k}>"
  326. xml_str += "</xml>"
  327. return xml_str
  328. def _xml_to_dict(self, xml_str: str) -> Dict:
  329. """XML转字典"""
  330. try:
  331. root = ET.fromstring(xml_str)
  332. return {child.tag: child.text for child in root}
  333. except:
  334. return {}
  335. def build_miniprogram_pay_params(self, prepay_id: str) -> Dict[str, str]:
  336. """小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致)"""
  337. appid = self.appid_mini or self.appid
  338. time_stamp = str(int(time.time()))
  339. nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
  340. package = f"prepay_id={prepay_id}"
  341. sign_type = "MD5"
  342. pay_fields = {
  343. "appId": appid,
  344. "nonceStr": nonce_str,
  345. "package": package,
  346. "signType": sign_type,
  347. "timeStamp": time_stamp,
  348. }
  349. sign_str = "&".join([f"{k}={pay_fields[k]}" for k in sorted(pay_fields.keys())])
  350. sign_str += f"&key={self.api_key}"
  351. pay_sign = hashlib.md5(sign_str.encode("utf-8")).hexdigest().upper()
  352. return {
  353. "appId": appid,
  354. "timeStamp": time_stamp,
  355. "nonceStr": nonce_str,
  356. "package": package,
  357. "signType": sign_type,
  358. "paySign": pay_sign,
  359. }
  360. def unified_order(
  361. self,
  362. out_trade_no: str,
  363. total_fee: int,
  364. body: str,
  365. openid: str = None,
  366. trade_type: str = "JSAPI"
  367. ) -> Dict:
  368. """
  369. 统一下单 (V2 API)
  370. Args:
  371. out_trade_no: 商户订单号
  372. total_fee: 订单金额(分)
  373. body: 商品描述
  374. openid: 用户openid
  375. trade_type: 交易类型 JSAPI/NATIVE/APP
  376. Returns:
  377. 返回支付参数
  378. """
  379. params = {
  380. "appid": self.appid,
  381. "mch_id": self.mchid,
  382. "nonce_str": ''.join(random.choices(string.ascii_letters + string.digits, k=32)),
  383. "body": body,
  384. "out_trade_no": out_trade_no,
  385. "total_fee": total_fee,
  386. "spbill_create_ip": "127.0.0.1",
  387. "notify_url": self.notify_url,
  388. "trade_type": trade_type
  389. }
  390. if openid:
  391. params["openid"] = openid
  392. # 小程序 JSAPI 必须使用小程序 AppID 与商户号绑定关系一致
  393. if trade_type == "JSAPI" and self.appid_mini:
  394. params["appid"] = self.appid_mini
  395. # 生成签名
  396. params["sign"] = self._generate_sign(params)
  397. # 发送请求
  398. try:
  399. import ssl
  400. context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  401. context.check_hostname = False
  402. context.verify_mode = ssl.CERT_NONE
  403. req = urllib.request.Request(
  404. self.pay_url,
  405. data=self._dict_to_xml(params).encode('utf-8'),
  406. headers={"Content-Type": "text/xml"}
  407. )
  408. with urllib.request.urlopen(req, context=context, timeout=30) as response:
  409. result = self._xml_to_dict(response.read().decode())
  410. if result.get("return_code") == "SUCCESS" and result.get("result_code") == "SUCCESS":
  411. if trade_type == "JSAPI":
  412. prepay = result.get("prepay_id")
  413. pay_extra = self.build_miniprogram_pay_params(prepay) if prepay else {}
  414. return {
  415. "success": True,
  416. "prepay_id": prepay,
  417. "package": pay_extra.get("package"),
  418. "pay_params": pay_extra,
  419. }
  420. elif trade_type == "NATIVE":
  421. return {
  422. "success": True,
  423. "code_url": result.get("code_url")
  424. }
  425. elif trade_type == "APP":
  426. return {
  427. "success": True,
  428. "prepay_id": result.get("prepay_id")
  429. }
  430. else:
  431. return {
  432. "success": False,
  433. "error": result.get("err_code_des", result.get("return_msg", "下单失败"))
  434. }
  435. except Exception as e:
  436. return {"success": False, "error": str(e)}
  437. def query_order(self, out_trade_no: str = None, transaction_id: str = None) -> Dict:
  438. """查询订单"""
  439. if not out_trade_no and not transaction_id:
  440. return {"success": False, "error": "缺少订单号"}
  441. params = {
  442. "appid": self.appid,
  443. "mch_id": self.mchid,
  444. "nonce_str": ''.join(random.choices(string.ascii_letters + string.digits, k=32))
  445. }
  446. if out_trade_no:
  447. params["out_trade_no"] = out_trade_no
  448. else:
  449. params["transaction_id"] = transaction_id
  450. params["sign"] = self._generate_sign(params)
  451. query_url = "https://api.mch.weixin.qq.com/pay/orderquery"
  452. try:
  453. import ssl
  454. context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  455. context.check_hostname = False
  456. context.verify_mode = ssl.CERT_NONE
  457. req = urllib.request.Request(
  458. query_url,
  459. data=self._dict_to_xml(params).encode('utf-8'),
  460. headers={"Content-Type": "text/xml"}
  461. )
  462. with urllib.request.urlopen(req, context=context, timeout=30) as response:
  463. result = self._xml_to_dict(response.read().decode())
  464. return {"success": True, "data": result}
  465. except Exception as e:
  466. return {"success": False, "error": str(e)}
  467. def refund(
  468. self,
  469. out_trade_no: str,
  470. total_fee: int,
  471. refund_fee: int,
  472. refund_desc: str = None
  473. ) -> Dict:
  474. """申请退款 (V2)"""
  475. out_refund_no = f"REF{datetime.now().strftime('%Y%m%d%H%M%S')}{random.randint(1000, 9999)}"
  476. params = {
  477. "appid": self.appid,
  478. "mch_id": self.mchid,
  479. "nonce_str": ''.join(random.choices(string.ascii_letters + string.digits, k=32)),
  480. "out_trade_no": out_trade_no,
  481. "out_refund_no": out_refund_no,
  482. "total_fee": total_fee,
  483. "refund_fee": refund_fee,
  484. "refund_desc": refund_desc or "用户申请退款"
  485. }
  486. params["sign"] = self._generate_sign(params)
  487. refund_url = "https://api.mch.weixin.qq.com/secapi/pay/refund"
  488. try:
  489. import ssl
  490. context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  491. context.check_hostname = False
  492. context.verify_mode = ssl.CERT_NONE
  493. # 需要双向证书
  494. if settings.WXPAY_SSL_CERT and settings.WXPAY_SSL_KEY:
  495. cert_file = settings.WXPAY_SSL_CERT
  496. key_file = settings.WXPAY_SSL_KEY
  497. req = urllib.request.Request(
  498. refund_url,
  499. data=self._dict_to_xml(params).encode('utf-8'),
  500. headers={"Content-Type": "text/xml"}
  501. )
  502. with urllib.request.urlopen(req, context=context, timeout=30) as response:
  503. result = self._xml_to_dict(response.read().decode())
  504. if result.get("return_code") == "SUCCESS":
  505. return {
  506. "success": True,
  507. "out_refund_no": out_refund_no,
  508. "refund_id": result.get("refund_id")
  509. }
  510. return {"success": False, "error": result.get("return_msg", "退款失败")}
  511. else:
  512. return {"success": False, "error": "缺少SSL证书配置"}
  513. except Exception as e:
  514. return {"success": False, "error": str(e)}
  515. def parse_notify(self, xml_data: str) -> Dict:
  516. """解析支付回调"""
  517. result = self._xml_to_dict(xml_data)
  518. if result.get("return_code") == "SUCCESS":
  519. # 验证签名
  520. sign = result.pop("sign", None)
  521. if sign and sign == self._generate_sign(result):
  522. return {"success": True, "data": result}
  523. return {"success": False, "error": "签名验证失败"}
  524. def get_wechat_pay_v2() -> WechatPayV2Client:
  525. """获取微信支付V2客户端"""
  526. return WechatPayV2Client()