wechatpy.pay.WeChatPay._handle_result()   A
last analyzed

Complexity

Conditions 4

Size

Total Lines 29
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4.2084

Importance

Changes 0
Metric Value
cc 4
eloc 25
nop 2
dl 0
loc 29
rs 9.28
c 0
b 0
f 0
ccs 13
cts 17
cp 0.7647
crap 4.2084
1
# -*- coding: utf-8 -*-
2 10
from __future__ import absolute_import, unicode_literals
3 10
import inspect
4 10
import logging
5
6 10
import requests
7 10
import xmltodict
8 10
from xml.parsers.expat import ExpatError
9 10
from optionaldict import optionaldict
10
11 10
from wechatpy.crypto import WeChatRefundCrypto
12 10
from wechatpy.utils import random_string
13 10
from wechatpy.exceptions import WeChatPayException, InvalidSignatureException
14 10
from wechatpy.pay.utils import (
15
    calculate_signature, calculate_signature_hmac, _check_signature, dict_to_xml
16
)
17 10
from wechatpy.pay.base import BaseWeChatPayAPI
18 10
from wechatpy.pay import api
19
20 10
logger = logging.getLogger(__name__)
21
22
23 10
def _is_api_endpoint(obj):
24 10
    return isinstance(obj, BaseWeChatPayAPI)
25
26
27 10
class WeChatPay(object):
28
    """
29
    微信支付接口
30
31
    :param appid: 微信公众号 appid
32
    :param sub_appid: 当前调起支付的小程序APPID
33
    :param api_key: 商户 key,不要在这里使用小程序的密钥
34
    :param mch_id: 商户号
35
    :param sub_mch_id: 可选,子商户号,受理模式下必填
36
    :param mch_cert: 必填,商户证书路径
37
    :param mch_key: 必填,商户证书私钥路径
38
    :param timeout: 可选,请求超时时间,单位秒,默认无超时设置
39
    :param sandbox: 可选,是否使用测试环境,默认为 False
40
    """
41
42 10
    redpack = api.WeChatRedpack()
43
    """红包接口"""
44 10
    transfer = api.WeChatTransfer()
45
    """企业付款接口"""
46 10
    coupon = api.WeChatCoupon()
47
    """代金券接口"""
48 10
    order = api.WeChatOrder()
49
    """订单接口"""
50 10
    refund = api.WeChatRefund()
51
    """退款接口"""
52 10
    micropay = api.WeChatMicroPay()
53
    """刷卡支付接口"""
54 10
    tools = api.WeChatTools()
55
    """工具类接口"""
56 10
    jsapi = api.WeChatJSAPI()
57
    """公众号网页 JS 支付接口"""
58 10
    withhold = api.WeChatWithhold()
59
    """代扣接口"""
60
61 10
    API_BASE_URL = 'https://api.mch.weixin.qq.com/'
62
63 10
    def __new__(cls, *args, **kwargs):
64 10
        self = super(WeChatPay, cls).__new__(cls)
65 10
        api_endpoints = inspect.getmembers(self, _is_api_endpoint)
66 10
        for name, _api in api_endpoints:
67 10
            api_cls = type(_api)
68 10
            _api = api_cls(self)
69 10
            setattr(self, name, _api)
70 10
        return self
71
72 10
    def __init__(self, appid, api_key, mch_id, sub_mch_id=None,
73
                 mch_cert=None, mch_key=None, timeout=None, sandbox=False, sub_appid=None):
74 10
        self.appid = appid
75 10
        self.sub_appid = sub_appid
76 10
        self.api_key = api_key
77 10
        self.mch_id = mch_id
78 10
        self.sub_mch_id = sub_mch_id
79 10
        self.mch_cert = mch_cert
80 10
        self.mch_key = mch_key
81 10
        self.timeout = timeout
82 10
        self.sandbox = sandbox
83 10
        self._sandbox_api_key = None
84 10
        self._http = requests.Session()
85
86 10
    def _fetch_sandbox_api_key(self):
87
        nonce_str = random_string(32)
88
        sign = calculate_signature({'mch_id': self.mch_id, 'nonce_str': nonce_str}, self.api_key)
89
        payload = dict_to_xml({
90
            'mch_id': self.mch_id,
91
            'nonce_str': nonce_str,
92
        }, sign=sign)
93
        headers = {'Content-Type': 'text/xml'}
94
        api_url = '{base}sandboxnew/pay/getsignkey'.format(base=self.API_BASE_URL)
95
        response = self._http.post(api_url, data=payload, headers=headers)
96
        return xmltodict.parse(response.text)['xml'].get('sandbox_signkey')
97
98 10
    def _request(self, method, url_or_endpoint, **kwargs):
99 10
        if not url_or_endpoint.startswith(('http://', 'https://')):
100 10
            api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL)
101 10
            if self.sandbox:
102
                api_base_url = '{url}sandboxnew/'.format(url=api_base_url)
103 10
            url = '{base}{endpoint}'.format(
104
                base=api_base_url,
105
                endpoint=url_or_endpoint
106
            )
107
        else:
108
            url = url_or_endpoint
109
110 10
        if isinstance(kwargs.get('data', ''), dict):
111 10
            data = kwargs['data']
112 10
            if 'mchid' not in data:
113
                # Fuck Tencent
114 10
                data.setdefault('mch_id', self.mch_id)
115 10
            data.setdefault('sub_mch_id', self.sub_mch_id)
116 10
            data.setdefault('nonce_str', random_string(32))
117 10
            data = optionaldict(data)
118
119 10
            if data.get('sign_type', 'MD5') == 'HMAC-SHA256':
120
                sign = calculate_signature_hmac(data, self.sandbox_api_key if self.sandbox else self.api_key)
121
            else:
122 10
                sign = calculate_signature(data, self.sandbox_api_key if self.sandbox else self.api_key)
123 10
            body = dict_to_xml(data, sign)
124 10
            body = body.encode('utf-8')
125 10
            kwargs['data'] = body
126
127
        # 商户证书
128 10
        if self.mch_cert and self.mch_key:
129
            kwargs['cert'] = (self.mch_cert, self.mch_key)
130
131 10
        kwargs['timeout'] = kwargs.get('timeout', self.timeout)
132 10
        logger.debug('Request to WeChat API: %s %s\n%s', method, url, kwargs)
133 10
        res = self._http.request(
134
            method=method,
135
            url=url,
136
            **kwargs
137
        )
138 10
        try:
139 10
            res.raise_for_status()
140
        except requests.RequestException as reqe:
141
            raise WeChatPayException(
142
                return_code=None,
143
                client=self,
144
                request=reqe.request,
145
                response=reqe.response
146
            )
147
148 10
        return self._handle_result(res)
149
150 10
    def _handle_result(self, res):
151 10
        res.encoding = 'utf-8'
152 10
        xml = res.text
153 10
        logger.debug('Response from WeChat API \n %s', xml)
154 10
        try:
155 10
            data = xmltodict.parse(xml)['xml']
156
        except (xmltodict.ParsingInterrupted, ExpatError):
157
            # 解析 XML 失败
158
            logger.debug('WeChat payment result xml parsing error', exc_info=True)
159
            return xml
160
161 10
        return_code = data['return_code']
162 10
        return_msg = data.get('return_msg')
163 10
        result_code = data.get('result_code')
164 10
        errcode = data.get('err_code')
165 10
        errmsg = data.get('err_code_des')
166 10
        if return_code != 'SUCCESS' or result_code != 'SUCCESS':
167
            # 返回状态码不为成功
168
            raise WeChatPayException(
169
                return_code,
170
                result_code,
171
                return_msg,
172
                errcode,
173
                errmsg,
174
                client=self,
175
                request=res.request,
176
                response=res
177
            )
178 10
        return data
179
180 10
    def get(self, url, **kwargs):
181
        return self._request(
182
            method='get',
183
            url_or_endpoint=url,
184
            **kwargs
185
        )
186
187 10
    def post(self, url, **kwargs):
188 10
        return self._request(
189
            method='post',
190
            url_or_endpoint=url,
191
            **kwargs
192
        )
193
194 10
    def check_signature(self, params):
195
        return _check_signature(params, self.api_key if not self.sandbox else self.sandbox_api_key)
196
197 10
    @classmethod
198
    def get_payment_data(cls, xml):
199
        """
200
        解析微信支付结果通知,获得appid, mch_id, out_trade_no, transaction_id
201
        如果你需要进一步判断,请先用appid, mch_id来生成WeChatPay,
202
        然后用`wechatpay.parse_payment_result(xml)`来校验支付结果
203
204
        使用示例::
205
206
            from wechatpy.pay import WeChatPay
207
            # 假设你已经获取了微信服务器推送的请求中的xml数据并存入xml变量
208
            data = WeChatPay.get_payment_appid(xml)
209
            {
210
                "appid": "公众号或者小程序的id",
211
                "mch_id": "商户id",
212
            }
213
214
        """
215
        try:
216
            data = xmltodict.parse(xml)
217
        except (xmltodict.ParsingInterrupted, ExpatError):
218
            raise ValueError("invalid xml")
219
        if not data or 'xml' not in data:
220
            raise ValueError("invalid xml")
221
        return {
222
            "appid": data["appid"],
223
            "mch_id": data["mch_id"],
224
            "out_trade_no": data["out_trade_no"],
225
            "transaction_id": data["transaction_id"],
226
        }
227
228 10
    def parse_payment_result(self, xml):
229
        """解析微信支付结果通知"""
230
        try:
231
            data = xmltodict.parse(xml)
232
        except (xmltodict.ParsingInterrupted, ExpatError):
233
            raise InvalidSignatureException()
234
235
        if not data or 'xml' not in data:
236
            raise InvalidSignatureException()
237
238
        data = data['xml']
239
        sign = data.pop('sign', None)
240
        real_sign = calculate_signature(data, self.api_key if not self.sandbox else self.sandbox_api_key)
241
        if sign != real_sign:
242
            raise InvalidSignatureException()
243
244
        for key in ('total_fee', 'settlement_total_fee', 'cash_fee', 'coupon_fee', 'coupon_count'):
245
            if key in data:
246
                data[key] = int(data[key])
247
        data['sign'] = sign
248
        return data
249
250 10
    def parse_refund_notify_result(self, xml):
251
        """解析微信退款结果通知"""
252
        refund_crypto = WeChatRefundCrypto(self.api_key if not self.sandbox else self.sandbox_api_key)
253
        data = refund_crypto.decrypt_message(xml, self.appid, self.mch_id)
254
        for key in ('total_fee', 'settlement_total_fee', 'refund_fee', 'settlement_refund_fee'):
255
            if key in data:
256
                data[key] = int(data[key])
257
        return data
258
259 10
    @property
260
    def sandbox_api_key(self):
261 10
        if self.sandbox and self._sandbox_api_key is None:
262
            self._sandbox_api_key = self._fetch_sandbox_api_key()
263
264
        return self._sandbox_api_key
265