├── .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>123123>" % sign
45 | self.assertEqual(result, expect_result)
46 |
47 | params = {"123": "xyz123"}
48 | result = dict_to_xml(params, sign)
49 | expect_result = "<123>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>123123>" % 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%s>' % (k, v, k))
55 | else:
56 | xml.append('<%s>%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.+)(?P=key)>")
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 |
--------------------------------------------------------------------------------