class WeChatPay: """ 微信支付接口 :param appid: 微信公众号 appid :param sub_appid: 当前调起支付的小程序APPID :param api_key: 商户 key,不要在这里使用小程序的密钥 :param mch_id: 商户号 :param sub_mch_id: 可选,子商户号,受理模式下必填 :param mch_cert: 必填,商户证书路径 :param mch_key: 必填,商户证书私钥路径 :param timeout: 可选,请求超时时间,单位秒,默认无超时设置 :param sandbox: 可选,是否使用测试环境,默认为 False """ redpack = api.WeChatRedpack() """红包接口""" transfer = api.WeChatTransfer() """企业付款接口""" coupon = api.WeChatCoupon() """代金券接口""" order = api.WeChatOrder() """订单接口""" refund = api.WeChatRefund() """退款接口""" micropay = api.WeChatMicroPay() """刷卡支付接口""" tools = api.WeChatTools() """工具类接口""" jsapi = api.WeChatJSAPI() """公众号网页 JS 支付接口""" withhold = api.WeChatWithhold() """代扣接口""" app_auth = api.WeChatAppAuth() """实名认证接口""" API_BASE_URL = "https://api.mch.weixin.qq.com/" def __new__(cls, *args, **kwargs): self = super().__new__(cls) api_endpoints = inspect.getmembers(self, _is_api_endpoint) for name, _api in api_endpoints: api_cls = type(_api) _api = api_cls(self) setattr(self, name, _api) return self def __init__( self, appid, api_key, mch_id, sub_mch_id=None, mch_cert=None, mch_key=None, timeout=None, sandbox=False, sub_appid=None, ): self.appid = appid self.sub_appid = sub_appid self.api_key = api_key self.mch_id = mch_id self.sub_mch_id = sub_mch_id self.mch_cert = mch_cert self.mch_key = mch_key self._using_pkcs12_cert = False self.timeout = timeout self.sandbox = sandbox self._sandbox_api_key = None self._http = requests.Session() if mch_cert and mch_cert.endswith(".p12"): from requests_pkcs12 import Pkcs12Adapter # 商户 .p12 格式证书,证书密码默认为商户 ID self._http.mount( self.API_BASE_URL, Pkcs12Adapter(pkcs12_filename=self.mch_cert, pkcs12_password=self.mch_id)) self._using_pkcs12_cert = True def _fetch_sandbox_api_key(self): nonce_str = random_string(32) sign = calculate_signature( { "mch_id": self.mch_id, "nonce_str": nonce_str }, self.api_key) payload = dict_to_xml( { "mch_id": self.mch_id, "nonce_str": nonce_str, }, sign=sign, ) headers = {"Content-Type": "text/xml"} api_url = f"{self.API_BASE_URL}sandboxnew/pay/getsignkey" response = self._http.post(api_url, data=payload, headers=headers) return xmltodict.parse(response.text)["xml"].get("sandbox_signkey") def _request(self, method, url_or_endpoint, **kwargs): if not url_or_endpoint.startswith(("http://", "https://")): api_base_url = kwargs.pop("api_base_url", self.API_BASE_URL) if self.sandbox: api_base_url = f"{api_base_url}sandboxnew/" url = f"{api_base_url}{url_or_endpoint}" else: url = url_or_endpoint if isinstance(kwargs.get("data", ""), dict): data = kwargs["data"] if "mchid" not in data: # F**k Tencent data.setdefault("mch_id", self.mch_id) data.setdefault("sub_mch_id", self.sub_mch_id) data.setdefault("nonce_str", random_string(32)) data = optionaldict(data) if data.get("sign_type", "MD5") == "HMAC-SHA256": sign = calculate_signature_hmac( data, self.sandbox_api_key if self.sandbox else self.api_key) else: sign = calculate_signature( data, self.sandbox_api_key if self.sandbox else self.api_key) body = dict_to_xml(data, sign) body = body.encode("utf-8") kwargs["data"] = body # 商户 PEM 证书 if not self._using_pkcs12_cert and self.mch_cert and self.mch_key: kwargs["cert"] = (self.mch_cert, self.mch_key) kwargs["timeout"] = kwargs.get("timeout", self.timeout) logger.debug("Request to WeChat API: %s %s\n%s", method, url, kwargs) res = self._http.request(method=method, url=url, **kwargs) try: res.raise_for_status() except requests.RequestException as reqe: raise WeChatPayException( return_code=None, client=self, request=reqe.request, response=reqe.response, ) return self._handle_result(res) def _handle_result(self, res): res.encoding = "utf-8-sig" xml = res.text logger.debug("Response from WeChat API \n %s", xml) try: data = xmltodict.parse(xml)["xml"] except (xmltodict.ParsingInterrupted, ExpatError): # 解析 XML 失败 logger.debug("WeChat payment result xml parsing error", exc_info=True) return xml return_code = data["return_code"] return_msg = data.get("return_msg", data.get("retmsg")) result_code = data.get("result_code", data.get("retcode")) errcode = data.get("err_code") errmsg = data.get("err_code_des") if return_code != "SUCCESS" or result_code != "SUCCESS": # 返回状态码不为成功 raise WeChatPayException( return_code, result_code, return_msg, errcode, errmsg, client=self, request=res.request, response=res, ) return data def get(self, url, **kwargs): return self._request(method="get", url_or_endpoint=url, **kwargs) def post(self, url, **kwargs): return self._request(method="post", url_or_endpoint=url, **kwargs) def check_signature(self, params): return _check_signature( params, self.api_key if not self.sandbox else self.sandbox_api_key) @classmethod def get_payment_data(cls, xml): """ 解析微信支付结果通知,获得appid, mch_id, out_trade_no, transaction_id 如果你需要进一步判断,请先用appid, mch_id来生成WeChatPay, 然后用`wechatpay.parse_payment_result(xml)`来校验支付结果 使用示例:: from wechatpy.pay import WeChatPay # 假设你已经获取了微信服务器推送的请求中的xml数据并存入xml变量 data = WeChatPay.get_payment_appid(xml) { "appid": "公众号或者小程序的id", "mch_id": "商户id", } """ try: data = xmltodict.parse(xml) except (xmltodict.ParsingInterrupted, ExpatError): raise ValueError("invalid xml") if not data or "xml" not in data: raise ValueError("invalid xml") data = data["xml"] return { "appid": data["appid"], "mch_id": data["mch_id"], "out_trade_no": data["out_trade_no"], "transaction_id": data["transaction_id"], } def parse_payment_result(self, xml): """解析微信支付结果通知""" try: data = xmltodict.parse(xml) except (xmltodict.ParsingInterrupted, ExpatError): raise InvalidSignatureException() if not data or "xml" not in data: raise InvalidSignatureException() data = data["xml"] sign = data.pop("sign", None) real_sign = calculate_signature( data, self.api_key if not self.sandbox else self.sandbox_api_key) if sign != real_sign: raise InvalidSignatureException() for key in ( "total_fee", "settlement_total_fee", "cash_fee", "coupon_fee", "coupon_count", ): if key in data: data[key] = int(data[key]) data["sign"] = sign return data def parse_refund_notify_result(self, xml): """解析微信退款结果通知""" refund_crypto = WeChatRefundCrypto( self.api_key if not self.sandbox else self.sandbox_api_key) data = refund_crypto.decrypt_message(xml, self.appid, self.mch_id) for key in ( "total_fee", "settlement_total_fee", "refund_fee", "settlement_refund_fee", ): if key in data: data[key] = int(data[key]) return data @property def sandbox_api_key(self): if self.sandbox and self._sandbox_api_key is None: self._sandbox_api_key = self._fetch_sandbox_api_key() return self._sandbox_api_key
class WeChatPay(object): """ 微信红包接口 :param appid: 微信公众号 appid :param api_key: 商户 key :param mch_id: 商户号 :param sub_mch_id: 可选,子商户号,受理模式下必填 :param mch_cert: 必填,商户证书路径 :param mch_key: 必填,商户证书私钥路径 """ redpack = api.WeChatRedpack() """红包接口""" transfer = api.WeChatTransfer() """企业付款接口""" coupon = api.WeChatCoupon() """代金券接口""" order = api.WeChatOrder() """订单接口""" refund = api.WeChatRefund() """退款接口""" micropay = api.WeChatMicroPay() """刷卡支付接口""" tools = api.WeChatTools() """工具类接口""" jsapi = api.WeChatJSAPI() API_BASE_URL = 'https://api.mch.weixin.qq.com/' def __new__(cls, *args, **kwargs): self = super(WeChatPay, cls).__new__(cls) if sys.version_info[:2] == (2, 6): import copy # Python 2.6 inspect.gemembers bug workaround # http://bugs.python.org/issue1785 for name, _api in self.__class__.__dict__.items(): if isinstance(_api, BaseWeChatPayAPI): _api = copy.deepcopy(_api) _api._client = self setattr(self, name, _api) else: api_endpoints = inspect.getmembers(self, _is_api_endpoint) for name, _api in api_endpoints: api_cls = type(_api) _api = api_cls(self) setattr(self, name, _api) return self def __init__(self, appid, api_key, mch_id, sub_mch_id=None, mch_cert=None, mch_key=None): """ :param appid: 微信公众号 appid :param api_key: 商户 key :param mch_id: 商户号 :param sub_mch_id: 可选,子商户号,受理模式下必填 :param mch_cert: 商户证书路径 :param mch_key: 商户证书私钥路径 """ self.appid = appid self.api_key = api_key self.mch_id = mch_id self.sub_mch_id = sub_mch_id self.mch_cert = mch_cert self.mch_key = mch_key def _request(self, method, url_or_endpoint, **kwargs): if not url_or_endpoint.startswith(('http://', 'https://')): api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL) url = '{base}{endpoint}'.format(base=api_base_url, endpoint=url_or_endpoint) else: url = url_or_endpoint if isinstance(kwargs.get('data', ''), dict): data = optionaldict(kwargs['data']) if 'mchid' not in data: # F**k Tencent data.setdefault('mch_id', self.mch_id) data.setdefault('sub_mch_id', self.sub_mch_id) data.setdefault('nonce_str', random_string(32)) sign = calculate_signature(data, self.api_key) body = dict_to_xml(data, sign) body = body.encode('utf-8') kwargs['data'] = body # 商户证书 if self.mch_cert and self.mch_key: kwargs['cert'] = (self.mch_cert, self.mch_key) res = requests.request(method=method, url=url, **kwargs) try: res.raise_for_status() except requests.RequestException as reqe: raise WeChatPayException(return_code=None, client=self, request=reqe.request, response=reqe.response) return self._handle_result(res) def _handle_result(self, res): res.encoding = 'utf-8' xml = res.text try: data = xmltodict.parse(xml)['xml'] except (xmltodict.ParsingInterrupted, ExpatError): # 解析 XML 失败 logger.debug('WeChat payment result xml parsing error', exc_info=True) return xml return_code = data['return_code'] return_msg = data.get('return_msg') result_code = data.get('result_code') errcode = data.get('err_code') errmsg = data.get('err_code_des') if return_code != 'SUCCESS' or result_code != 'SUCCESS': # 返回状态码不为成功 raise WeChatPayException(return_code, result_code, return_msg, errcode, errmsg, client=self, request=res.request, response=res) return data def get(self, url, **kwargs): return self._request(method='get', url_or_endpoint=url, **kwargs) def post(self, url, **kwargs): return self._request(method='post', url_or_endpoint=url, **kwargs) def check_signature(self, params): return _check_signature(params, self.api_key) def parse_payment_result(self, xml): """解析微信支付结果通知""" try: data = xmltodict.parse(xml) except (xmltodict.ParsingInterrupted, ExpatError): raise InvalidSignatureException() if not data or 'xml' not in data: raise InvalidSignatureException() data = data['xml'] sign = data.pop('sign', None) real_sign = calculate_signature(data, self.api_key) if sign != real_sign: raise InvalidSignatureException() data['sign'] = sign return data
class WeChatPay(object): """ 微信支付接口 :param appid: 微信公众号 appid :param api_key: 商户 key :param mch_id: 商户号 :param sub_mch_id: 可选,子商户号,受理模式下必填 :param mch_cert: 必填,商户证书路径 :param mch_key: 必填,商户证书私钥路径 :param timeout: 可选,请求超时时间,单位秒,默认无超时设置 :param sandbox: 可选,是否使用测试环境,默认为 False """ _http = requests.Session() redpack = api.WeChatRedpack() """红包接口""" transfer = api.WeChatTransfer() """企业付款接口""" coupon = api.WeChatCoupon() """代金券接口""" order = api.WeChatOrder() """订单接口""" refund = api.WeChatRefund() """退款接口""" micropay = api.WeChatMicroPay() """刷卡支付接口""" tools = api.WeChatTools() """工具类接口""" jsapi = api.WeChatJSAPI() """公众号网页 JS 支付接口""" withhold = api.WeChatWithhold() """代扣接口""" API_BASE_URL = 'https://api.mch.weixin.qq.com/' def __new__(cls, *args, **kwargs): self = super(WeChatPay, cls).__new__(cls) api_endpoints = inspect.getmembers(self, _is_api_endpoint) for name, _api in api_endpoints: api_cls = type(_api) _api = api_cls(self) setattr(self, name, _api) return self def __init__(self, appid, api_key, mch_id, sub_mch_id=None, mch_cert=None, mch_key=None, timeout=None, sandbox=False): self.appid = appid self.api_key = api_key self.mch_id = mch_id self.sub_mch_id = sub_mch_id self.mch_cert = mch_cert self.mch_key = mch_key self.timeout = timeout self.sandbox = sandbox self.sandbox_api_key = None def _fetch_sanbox_api_key(self): nonce_str = random_string(32) sign = calculate_signature({'mch_id': self.mch_id, 'nonce_str': nonce_str}, self.api_key) payload = dict_to_xml({ 'mch_id': self.mch_id, 'nonce_str': nonce_str, }, sign=sign) headers = {'Content-Type': 'text/xml'} api_url = '{base}sandboxnew/pay/getsignkey'.format(base=self.API_BASE_URL) response = self._http.post(api_url, data=payload, headers=headers) return xmltodict.parse(response.text)['xml'].get('sandbox_signkey') def _request(self, method, url_or_endpoint, **kwargs): if not url_or_endpoint.startswith(('http://', 'https://')): api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL) if self.sandbox: api_base_url = '{url}sandboxnew/'.format(url=api_base_url) url = '{base}{endpoint}'.format( base=api_base_url, endpoint=url_or_endpoint ) else: url = url_or_endpoint if isinstance(kwargs.get('data', ''), dict): data = kwargs['data'] if 'mchid' not in data: # F**k Tencent data.setdefault('mch_id', self.mch_id) data.setdefault('sub_mch_id', self.sub_mch_id) data.setdefault('nonce_str', random_string(32)) data = optionaldict(data) if self.sandbox and self.sandbox_api_key is None: self.sandbox_api_key = self._fetch_sanbox_api_key() sign = calculate_signature(data, self.sandbox_api_key if self.sandbox else self.api_key) body = dict_to_xml(data, sign) body = body.encode('utf-8') kwargs['data'] = body # 商户证书 if self.mch_cert and self.mch_key: kwargs['cert'] = (self.mch_cert, self.mch_key) kwargs['timeout'] = kwargs.get('timeout', self.timeout) res = self._http.request( method=method, url=url, **kwargs ) try: res.raise_for_status() except requests.RequestException as reqe: raise WeChatPayException( return_code=None, client=self, request=reqe.request, response=reqe.response ) return self._handle_result(res) def _handle_result(self, res): res.encoding = 'utf-8' xml = res.text try: data = xmltodict.parse(xml)['xml'] except (xmltodict.ParsingInterrupted, ExpatError): # 解析 XML 失败 logger.debug('WeChat payment result xml parsing error', exc_info=True) return xml return_code = data['return_code'] return_msg = data.get('return_msg') result_code = data.get('result_code') errcode = data.get('err_code') errmsg = data.get('err_code_des') if return_code != 'SUCCESS' or result_code != 'SUCCESS': # 返回状态码不为成功 raise WeChatPayException( return_code, result_code, return_msg, errcode, errmsg, client=self, request=res.request, response=res ) return data def get(self, url, **kwargs): return self._request( method='get', url_or_endpoint=url, **kwargs ) def post(self, url, **kwargs): return self._request( method='post', url_or_endpoint=url, **kwargs ) def check_signature(self, params): return _check_signature(params, self.api_key if not self.sandbox else self.sandbox_api_key) def parse_payment_result(self, xml): """解析微信支付结果通知""" try: data = xmltodict.parse(xml) except (xmltodict.ParsingInterrupted, ExpatError): raise InvalidSignatureException() if not data or 'xml' not in data: raise InvalidSignatureException() data = data['xml'] sign = data.pop('sign', None) real_sign = calculate_signature(data, self.api_key if not self.sandbox else self.sandbox_api_key) if sign != real_sign: raise InvalidSignatureException() for key in ('total_fee', 'settlement_total_fee', 'cash_fee', 'coupon_fee', 'coupon_count'): if key in data: data[key] = int(data[key]) data['sign'] = sign return data