├── .gitignore ├── README.md └── weixin_pay ├── clean.sh ├── requirements.txt ├── tests.py ├── utils.py └── weixin_pay.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | #local files 57 | local_settings.py 58 | local_tests.py 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 微信支付接口(V3.3.7)类库 2 | ========== 3 | 4 | ####weixin_pay 5 | 6 | ##支持接口: 7 | 此类库目前只提供了三种接口的操作类: 8 | 9 | * 统一支付接口 10 | * 订单查询接口 11 | * JSAPI 支付 12 | 13 | ##使用方法 14 | 15 | ####1. 统一支付接口 16 | 17 | 用于获取预支付ID和查询支付链接地址,可以使用二维码类库将支付链接地址生成二维码后供付款人扫描付款。 18 | 19 | 注意:同一个out_trade_no发送不同的数据内容(如金额、body发生变化,但是out_trade_no未变)时会报OUT_TRADE_NO_USED(商户订单号重复)错误。 20 | 21 | ```python 22 | from weixin_pay.weixin_pay import UnifiedOrderPay 23 | 24 | 25 | pay = UnifiedOrderPay("WXPAY_APPID", "WXPAY_MCHID", "WXPAY_API_KEY") 26 | response = pay.post("body", "out_trade_no", "total_fee", "127.0.0.1", "http://www.xxxx.com/pay/notify/url/") 27 | if response and response["return_code"] == "SUCCESS" and response["result_code"] == "SUCCESS": 28 | prepay_id = response["prepay_id"] #预支付ID 29 | code_url = response["code_url"] #二维码链接 30 | else: 31 | if response["return_code"] == "FAIL": 32 | err_code_des = response["return_msg"] 33 | #通信失败处理 34 | if response["result_code"] == "FAIL": 35 | err_code = response["err_code"] 36 | err_code_des = pay.get_error_code_desc(response["err_code"]) 37 | #交易失败处理 38 | ``` 39 | 40 | ####2. 订单查询接口 41 | 42 | 使用外部订单号查询订单的支付状态。 43 | 44 | ```python 45 | from weixin_pay.weixin_pay import OrderQuery 46 | 47 | 48 | pay = OrderQuery("WXPAY_APPID", "WXPAY_MCHID", "WXPAY_API_KEY") 49 | response = wx_pay.post("out_trade_no") 50 | if response and response["return_code"] == "SUCCESS" and response["result_code"] == "SUCCESS": 51 | trade_state = response["trade_state"] 52 | if trade_state == "SUCCESS": #支付成功 53 | pass #处理支付成功的情况 54 | else: 55 | pass #处理支付未完成的情况,trade_state的枚举值参见微信官方文档说明 56 | ``` 57 | 58 | ####3. JSAPI 支付 59 | 60 | 在微信浏览器里面打开 H5 网页中执行 JS 调起支付。接口输入输出数据格式为 JSON。 61 | 62 | 注意:同一个out_trade_no发送不同的数据内容(如金额、body发生变化,但是out_trade_no未变)时会报OUT_TRADE_NO_USED(商户订单号重复)错误。 63 | 64 | ```python 65 | from weixin_pay.weixin_pay import JsAPIOrderPay, UnifiedOrderPay 66 | 67 | 68 | pay = JsAPIOrderPay("WXPAY_APPID", "WXPAY_MCHID", "WXPAY_API_KEY", "WXPAY_API_SECRET") 69 | 70 | #先判断request.GET中是否有code参数,如果没有,需要使用create_oauth_url_for_code函数获取OAuth2授权地址后重定向到该地址并取得code值 71 | oauth_url = pay.create_oauth_url_for_code("http://www.xxxx.com/pay/url/") 72 | #重定向到oauth_url后,获得code值 73 | code = request.GET('code', None) 74 | 75 | if code: 76 | #使用code获取H5页面JsAPI所需的所有参数,类型为字典 77 | josn_pay_info = pay.post("body", "out_trade_no", "total_fee", "127.0.0.1", "http://www.xxxx.com/pay/notify/url/", code) 78 | ``` 79 | 80 | 例如在Django模板中可以这样调用: 81 | 82 | ```javascript 83 | function jsApiCall(){ 84 | WeixinJSBridge.invoke( 85 | 'getBrandWCPayRequest', 86 | { 87 | "appId":"{{ app_id }}", 88 | "timeStamp": "{{ time_stamp }}", 89 | "nonceStr": "{{ nonce_str }}", 90 | "package": "{{ package }}", 91 | "signType": "MD5", 92 | "paySign": "{{ sign }}" 93 | }, 94 | function(res){ 95 | WeixinJSBridge.log(res.err_msg); 96 | //以下对调用结果进行处理 97 | } 98 | ); 99 | } 100 | 101 | function callpay() { 102 | if (typeof WeixinJSBridge == "undefined"){ 103 | if( document.addEventListener ) { 104 | document.addEventListener('WeixinJSBridgeReady', jsApiCall, false); 105 | } else if ( document.attachEvent ) { 106 | document.attachEvent('WeixinJSBridgeReady', jsApiCall); 107 | document.attachEvent('onWeixinJSBridgeReady', jsApiCall); 108 | } 109 | } else { 110 | jsApiCall(); 111 | } 112 | } 113 | //其余部分参见官方的JsAPI调用示例 114 | ``` 115 | 116 | ##说明 117 | 118 | 目前还是开发版本,只是在公司网站上应用了一下,还不是一个完全稳定的版本,希望有能力的同学可以贡献代码,一起完善这个类库。 119 | 120 | 如有问题请联系 ddkangfu(AT)gmail.com 121 | 122 | -------------------------------------------------------------------------------- /weixin_pay/clean.sh: -------------------------------------------------------------------------------- 1 | find . -name '*.pyc' -type f -print -exec rm -rf {} \; 2 | -------------------------------------------------------------------------------- /weixin_pay/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.4.3 2 | wsgiref==0.1.2 3 | -------------------------------------------------------------------------------- /weixin_pay/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding=utf-8 -*- 2 | 3 | import unittest 4 | import hashlib 5 | 6 | from utils import dict_to_xml, calculate_sign, random_str, smart_str, xml_to_dict, format_url 7 | from local_settings import appid, mch_id, api_key 8 | 9 | 10 | class TestUtils(unittest.TestCase): 11 | def test_format_url(self): 12 | params = {"123": "123"} 13 | key = "12345678901234567890" 14 | url = format_url(params) 15 | expect_url = "123=123" 16 | self.assertEqual(url, expect_url) 17 | 18 | params = {"abc": "abc", "123": "123"} 19 | url = format_url(params) 20 | expect_url = "123=123&abc=abc" 21 | self.assertEqual(url, expect_url) 22 | 23 | url = format_url(params, key) 24 | expect_url = "123=123&abc=abc&key=12345678901234567890" 25 | self.assertEqual(url, expect_url) 26 | 27 | def test_calculate_sign(self): 28 | params = {"123": "123"} 29 | key = "12345678901234567890" 30 | sign = calculate_sign(params, key) 31 | expect_sign = hashlib.md5("123=123&key=%s"%key).hexdigest().upper() 32 | self.assertEqual(sign, expect_sign) 33 | 34 | params = {"abc": "abc", "123": "123"} 35 | sign = calculate_sign(params, key) 36 | expect_sign = hashlib.md5("123=123&abc=abc&key=%s" % key).hexdigest().upper() 37 | print sign 38 | self.assertEqual(sign, expect_sign) 39 | 40 | def test_dict_to_xml(self): 41 | params = {"123": "123"} 42 | sign = '1234567890' 43 | result = dict_to_xml(params, sign) 44 | expect_result = "<123>123" % sign 45 | self.assertEqual(result, expect_result) 46 | 47 | params = {"123": "xyz123"} 48 | result = dict_to_xml(params, sign) 49 | expect_result = "<123>" % sign 50 | self.assertEqual(result, expect_result) 51 | 52 | params = {"123": "123", "abc": "abc", } 53 | result = dict_to_xml(params, sign) 54 | expect_result = "<123>123" % sign 55 | self.assertEqual(result, expect_result) 56 | 57 | params = {"c": "123", "a": "abc", } 58 | result = dict_to_xml(params, sign) 59 | expect_result = "123" % sign 60 | self.assertEqual(result, expect_result) 61 | 62 | def test_random_str(self): 63 | result = random_str() 64 | self.assertEqual(len(result), 8) 65 | 66 | result1 = random_str(32) 67 | self.assertEqual(len(result1), 32) 68 | 69 | result2 = random_str(32) 70 | self.assertEqual(len(result2), 32) 71 | 72 | self.assertNotEqual(result1, result2) 73 | 74 | def test_xml_to_dict(self): 75 | xml = "xxx" 76 | sign, result = xml_to_dict(xml) 77 | self.assertEqual(sign, None) 78 | self.assertEqual(result, None) 79 | 80 | xml = "xxx" 81 | sign, result = xml_to_dict(xml) 82 | self.assertEqual(sign, None) 83 | self.assertEqual(len(result), 1) 84 | self.assertEqual(result["a"], "xxx") 85 | 86 | xml = "xxxyyy" 87 | sign, result = xml_to_dict(xml) 88 | self.assertEqual(len(result), 2) 89 | self.assertEqual(result["a"], "xxx") 90 | self.assertEqual(result["b"], "yyy") 91 | 92 | xml = "xxxyyy" 93 | sign, result = xml_to_dict(xml) 94 | self.assertEqual(len(result), 3) 95 | self.assertEqual(result["a"], "xxx") 96 | self.assertEqual(result["b"], "yyy") 97 | self.assertEqual(result["c"], "zzz") 98 | 99 | xml = """ 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | """ 110 | 111 | sign, result = xml_to_dict(xml) 112 | self.assertEqual(sign, "0C638718BE0316E9B16E57DC869D2CD1") 113 | self.assertEqual(len(result), 9) 114 | self.assertEqual(result["return_code"], "SUCCESS") 115 | self.assertEqual(result["return_msg"], "OK") 116 | self.assertEqual(result["result_code"], "SUCCESS") 117 | self.assertEqual(result["code_url"], "weixin://wxpay/bizpayurl?sr=GnZnlWr") 118 | self.assertEqual(result.get("device_info", None), None) 119 | 120 | 121 | if __name__ == "__main__": 122 | unittest.main() 123 | -------------------------------------------------------------------------------- /weixin_pay/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding=utf-8 -*- 2 | 3 | import hashlib 4 | import re 5 | from random import Random 6 | import requests 7 | 8 | 9 | def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'): 10 | """ 11 | Returns a bytestring version of 's', encoded as specified in 'encoding'. 12 | 13 | If strings_only is True, don't convert (some) non-string-like objects. 14 | """ 15 | if strings_only and isinstance(s, (types.NoneType, int)): 16 | return s 17 | if not isinstance(s, basestring): 18 | try: 19 | return str(s) 20 | except UnicodeEncodeError: 21 | if isinstance(s, Exception): 22 | # An Exception subclass containing non-ASCII data that doesn't 23 | # know how to print itself properly. We shouldn't raise a 24 | # further exception. 25 | return ' '.join([smart_str(arg, encoding, strings_only, 26 | errors) for arg in s]) 27 | return unicode(s).encode(encoding, errors) 28 | elif isinstance(s, unicode): 29 | return s.encode(encoding, errors) 30 | elif s and encoding != 'utf-8': 31 | return s.decode('utf-8', errors).encode(encoding, errors) 32 | else: 33 | return s 34 | 35 | 36 | def format_url(params, api_key=None): 37 | url = "&".join(['%s=%s'%(key, smart_str(params[key])) for key in sorted(params)]) 38 | if api_key: 39 | url = '%s&key=%s' % (url, api_key) 40 | return url 41 | 42 | 43 | def calculate_sign(params, api_key): 44 | #签名步骤一:按字典序排序参数, 在string后加入KEY 45 | url = format_url(params, api_key) 46 | #签名步骤二:MD5加密, 所有字符转为大写 47 | return hashlib.md5(url).hexdigest().upper() 48 | 49 | 50 | def dict_to_xml(params, sign): 51 | xml = ["",] 52 | for (k, v) in params.items(): 53 | if (v.isdigit()): 54 | xml.append('<%s>%s' % (k, v, k)) 55 | else: 56 | xml.append('<%s>' % (k, v, k)) 57 | xml.append('' % sign) 58 | return ''.join(xml) 59 | 60 | 61 | def xml_to_dict(xml): 62 | if xml[0:5].upper() != "" and xml[-6].upper() != "": 63 | return None, None 64 | 65 | result = {} 66 | sign = None 67 | content = ''.join(xml[5:-6].strip().split('\n')) 68 | 69 | pattern = re.compile(r"<(?P.+)>(?P.+)") 70 | m = pattern.match(content) 71 | while(m): 72 | key = m.group("key").strip() 73 | value = m.group("value").strip() 74 | if value != "": 75 | pattern_inner = re.compile(r".+)\]\]>") 76 | inner_m = pattern_inner.match(value) 77 | if inner_m: 78 | value = inner_m.group("inner_val").strip() 79 | if key == "sign": 80 | sign = value 81 | else: 82 | result[key] = value 83 | 84 | next_index = m.end("value") + len(key) + 3 85 | if next_index >= len(content): 86 | break 87 | content = content[next_index:] 88 | m = pattern.match(content) 89 | 90 | return sign, result 91 | 92 | 93 | def validate_post_xml(xml, appid, mch_id, api_key): 94 | sign, params = xml_to_dict(xml) 95 | if (not sign) or (not params): 96 | return None 97 | 98 | remote_sign = calculate_sign(params, api_key) 99 | if sign != remote_sign: 100 | return None 101 | 102 | if params["appid"] != appid or params["mch_id"] != mch_id: 103 | return None 104 | 105 | return params 106 | 107 | 108 | def random_str(randomlength=8): 109 | chars = 'abcdefghijklmnopqrstuvwxyz0123456789' 110 | random = Random() 111 | return "".join([chars[random.randint(0, len(chars) - 1)] for i in range(randomlength)]) 112 | 113 | 114 | def post_xml(url, xml): 115 | return requests.post(url, data=xml) 116 | -------------------------------------------------------------------------------- /weixin_pay/weixin_pay.py: -------------------------------------------------------------------------------- 1 | # -*- coding=utf-8 -*- 2 | 3 | import time 4 | import json 5 | import hashlib 6 | import requests 7 | 8 | from utils import (smart_str, dict_to_xml, calculate_sign, random_str, 9 | post_xml, xml_to_dict, validate_post_xml, format_url) 10 | #from local_settings import appid, mch_id, api_key 11 | 12 | 13 | OAUTH2_AUTHORIZE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?%s" 14 | OAUTH2_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?%s" 15 | 16 | 17 | class WeiXinPay(object): 18 | def __init__(self, appid, mch_id, api_key): 19 | self.appid = appid #微信公众号身份的唯一标识。审核通过后,在微信发送的邮件中查看 20 | self.mch_id = mch_id #受理商ID,身份标识 21 | self.api_key = api_key #商户支付密钥Key。审核通过后,在微信发送的邮件中查看 22 | self.common_params = { 23 | "appid": self.appid, 24 | "mch_id": self.mch_id, 25 | } 26 | self.params = {} 27 | self.url = "" 28 | self.trade_type = "" 29 | 30 | def set_params(self, **kwargs): 31 | self.params = {} 32 | for (k, v) in kwargs.items(): 33 | self.params[k] = smart_str(v) 34 | 35 | self.params["nonce_str"] = random_str(32) 36 | if self.trade_type: 37 | self.params["trade_type"] = self.trade_type 38 | self.params.update(self.common_params) 39 | 40 | def post_xml(self): 41 | sign = calculate_sign(self.params, self.api_key) 42 | xml = dict_to_xml(self.params, sign) 43 | response = post_xml(self.url, xml) 44 | return xml_to_dict(response.text) 45 | 46 | def valiate_xml(self, xml): 47 | return validate_post_xml(xml, self.appid, self.mch_id, self.api_key) 48 | 49 | def get_error_code_desc(self, error_code): 50 | error_desc = { 51 | "SYSTEMERROR": u"接口后台错误", 52 | "INVALID_TRANSACTIONID": u"无效 transaction_id", 53 | "PARAM_ERROR": u"提交参数错误", 54 | "ORDERPAID": u"订单已支付", 55 | "OUT_TRADE_NO_USED": u"商户订单号重复", 56 | "NOAUTH": u"商户无权限", 57 | "NOTENOUGH": u"余额丌足", 58 | "NOTSUPORTCARD": u"不支持卡类型", 59 | "ORDERCLOSED": u"订单已关闭", 60 | "BANKERROR": u"银行系统异常", 61 | "REFUND_FEE_INVALID": u"退款金额大亍支付金额", 62 | "ORDERNOTEXIST": u"订单不存在", 63 | } 64 | return error_desc.get(error_code.strip().upper(), u"未知错误") 65 | 66 | 67 | class UnifiedOrderPay(WeiXinPay): 68 | """发送预支付单""" 69 | def __init__(self, appid, mch_id, api_key): 70 | super(UnifiedOrderPay, self).__init__(appid, mch_id, api_key) 71 | self.url = "https://api.mch.weixin.qq.com/pay/unifiedorder" 72 | self.trade_type = "NATIVE" 73 | 74 | def post(self, body, out_trade_no, total_fee, spbill_create_ip, notify_url, **kwargs): 75 | tmp_kwargs = { 76 | "body": body, 77 | "out_trade_no": out_trade_no, 78 | "total_fee": total_fee, 79 | "spbill_create_ip": spbill_create_ip, 80 | "notify_url": notify_url, 81 | } 82 | tmp_kwargs.update(**kwargs) 83 | self.set_params(**tmp_kwargs) 84 | return self.post_xml()[1] 85 | 86 | 87 | class OrderQuery(WeiXinPay): 88 | """订单状态查询""" 89 | def __init__(self, appid, mch_id, api_key): 90 | super(OrderQuery, self).__init__(appid, mch_id, api_key) 91 | self.url = "https://api.mch.weixin.qq.com/pay/orderquery" 92 | 93 | def post(self, out_trade_no): 94 | self.set_params(out_trade_no=out_trade_no) 95 | return self.post_xml()[1] 96 | 97 | 98 | class JsAPIOrderPay(UnifiedOrderPay): 99 | """H5页面的Js调用类""" 100 | def __init__(self, appid, mch_id, api_key, app_secret): 101 | super(JsAPIOrderPay, self).__init__(appid, mch_id, api_key) 102 | self.app_secret = app_secret 103 | self.trade_type = "JSAPI" 104 | 105 | def create_oauth_url_for_code(self, redirect_uri): 106 | url_params = { 107 | "appid": self.appid, 108 | "redirect_uri": redirect_uri, #一般是回调当前页面 109 | "response_type": "code", 110 | "scope": "snsapi_base", 111 | "state": "STATE#wechat_redirect" 112 | } 113 | url = format_url(url_params) 114 | return OAUTH2_AUTHORIZE_URL % url 115 | 116 | def _create_oauth_url_for_openid(self, code): 117 | url_params = { 118 | "appid": self.appid, 119 | "secret": self.app_secret, 120 | "code": code, 121 | "grant_type": "authorization_code", 122 | } 123 | url = format_url(url_params) 124 | return OAUTH2_ACCESS_TOKEN_URL % url 125 | 126 | def _get_oauth_info(self, code): 127 | """ 128 | 获取OAuth2的信息:access_token、expires_in、refresh_token、openid、scope 129 | 返回结果为字典,可使用["xxx"]或.get("xxx", None)的方式进行读取 130 | """ 131 | url = self._create_oauth_url_for_openid(code) 132 | response = requests.get(url) 133 | return response.json() if response else None 134 | 135 | def _get_openid(self, code): 136 | oauth_info = self._get_oauth_info(code) 137 | if oauth_info: 138 | return oauth_info.get("openid", None) 139 | return None 140 | 141 | def _get_json_js_api_params(self, prepay_id): 142 | js_params = { 143 | "appId": self.appid, 144 | "timeStamp": "%d" % time.time(), 145 | "nonceStr": random_str(32), 146 | "package": "prepay_id=%s" % prepay_id, 147 | "signType": "MD5", 148 | } 149 | js_params["paySign"] = calculate_sign(js_params, self.api_key) 150 | return js_params 151 | 152 | def post(self, body, out_trade_no, total_fee, spbill_create_ip, notify_url, code): 153 | if code: 154 | open_id = self._get_openid(code) 155 | if open_id: 156 | #直接调用基类的post方法查询prepay_id,如果成功,返回一个字典 157 | unified_order = super(JsAPIOrderPay, self).post(body, out_trade_no, total_fee, spbill_create_ip, notify_url, open_id=open_id) 158 | if unified_order: 159 | prepay_id = unified_order.get("prepay_id", None) 160 | if prepay_id: 161 | return self._get_json_js_api_params(prepay_id) 162 | return None 163 | 164 | 165 | #if __name__ == "__main__": 166 | # pay = UnifiedOrderPay(appid, mch_id, api_key) 167 | #pay.post_unified_order("贡献一分钱", "wx983e4a34aa76e3c41416107999", "http://www.xxxxxx.com/demo/notify_url.php", "1") 168 | # print pay.post(body="贡献一分钱", out_trade_no="wx983e4a34aa76e3c41416149262", total_fee="1", 169 | # spbill_create_ip="127.0.0.1", notify_url="http://www.xxxxxx.com/demo/notify_url.php") 170 | #print pay.post() 171 | 172 | #if __name__ == "__main__": 173 | # pay = JsAPIOrderPay(appid, mch_id, api_key) 174 | # print pay.post_unified_order("贡献一分钱", "wx983e4a34aa76e3c41416107999", "http://www.xxxxxx.com/demo/notify_url.php", "1") 175 | --------------------------------------------------------------------------------