├── weixin
├── backends
│ ├── __init__.py
│ ├── fl.py
│ ├── dj.py
│ └── common.py
├── __init__.py
├── config.py
├── handler.py
├── lib.py
└── pay.py
├── example
└── django_example
│ ├── __init__.py
│ ├── question
│ ├── __init__.py
│ ├── admin.py
│ ├── models.py
│ ├── tests.py
│ ├── templates
│ │ ├── share.html
│ │ └── pay.html
│ └── views.py
│ ├── README.md
│ ├── manage.py
│ ├── urls.py
│ └── settings.py
├── .gitignore
└── README.md
/weixin/backends/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/django_example/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/django_example/question/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.so
3 | *~
4 | build
5 | dist/
6 | MANIFEST
--------------------------------------------------------------------------------
/example/django_example/README.md:
--------------------------------------------------------------------------------
1 | # weixin-knife django demo
2 |
3 | django 版本比较老(1.37)
4 |
5 |
6 |
--------------------------------------------------------------------------------
/weixin/__init__.py:
--------------------------------------------------------------------------------
1 | #coding=utf8
2 | from .lib import *
3 | from .config import WxPayConf_pub
4 | from .pay import *
--------------------------------------------------------------------------------
/example/django_example/question/admin.py:
--------------------------------------------------------------------------------
1 | #coding=utf8
2 |
3 | from django.contrib import admin
4 | from models import *
5 |
6 |
--------------------------------------------------------------------------------
/example/django_example/question/models.py:
--------------------------------------------------------------------------------
1 | #coding=utf8
2 |
3 | from django.db import models
4 |
5 | # Create your models here.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/django_example/question/tests.py:
--------------------------------------------------------------------------------
1 | """
2 | This file demonstrates writing tests using the unittest module. These will pass
3 | when you run "manage.py test".
4 |
5 | Replace this with more appropriate tests for your application.
6 | """
7 |
8 | from django.test import TestCase
9 |
10 |
11 | class SimpleTest(TestCase):
12 | def test_basic_addition(self):
13 | """
14 | Tests that 1 + 1 always equals 2.
15 | """
16 | self.assertEqual(1 + 1, 2)
17 |
--------------------------------------------------------------------------------
/example/django_example/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from django.core.management import execute_manager
3 | import imp
4 | try:
5 | imp.find_module('settings') # Assumed to be in the same directory.
6 | except ImportError:
7 | import sys
8 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__)
9 | sys.exit(1)
10 |
11 | import settings
12 |
13 | if __name__ == "__main__":
14 | execute_manager(settings)
15 |
--------------------------------------------------------------------------------
/example/django_example/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import patterns, include, url
2 |
3 | # Uncomment the next two lines to enable the admin:
4 | from django.contrib import admin
5 | admin.autodiscover()
6 |
7 | urlpatterns = patterns('',
8 | # Examples:
9 | # url(r'^$', 'brain.views.home', name='home'),
10 | # url(r'^brain/', include('brain.foo.urls')),
11 |
12 | # Uncomment the admin/doc line below to enable admin documentation:
13 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
14 |
15 | # Uncomment the next line to enable the admin:
16 | url(r'^admin/', include(admin.site.urls)),
17 | url(r'^do$', 'question.views.do'),
18 | url(r'^oauth', 'question.views.oauth'),
19 | url(r'^share', 'question.views.share'),
20 | url(r'^wxpay/', 'question.views.pay'),
21 | url(r'^paydetail', 'question.views.paydetail'),
22 | url(r'^payback', 'question.views.payback'),
23 | )
24 |
--------------------------------------------------------------------------------
/weixin/config.py:
--------------------------------------------------------------------------------
1 | #coding:utf-8
2 | '''
3 | Created on 2014-5-13
4 |
5 | @author: skycrab
6 | '''
7 |
8 | class WxPayConf_pub(object):
9 | """配置账号信息"""
10 |
11 | #=======【基本信息设置】=====================================
12 | #微信公众号身份的唯一标识。审核通过后,在微信发送的邮件中查看
13 | APPID = "wxxxxx"
14 | #JSAPI接口中获取openid,审核后在公众平台开启开发模式后可查看
15 | APPSECRET = "4e74fff7418b151fd345a5cdfd7075cy"
16 | #接口配置token
17 | TOKEN = "brain"
18 | #受理商ID,身份标识
19 | MCHID = "18883487"
20 | #商户支付密钥Key。审核通过后,在微信发送的邮件中查看
21 | KEY = "48888888888888888888888888888886"
22 |
23 |
24 | #=======【异步通知url设置】===================================
25 | #异步通知url,商户根据实际开发过程设定
26 | NOTIFY_URL = "http://******.com/payback"
27 |
28 | #=======【证书路径设置】=====================================
29 | #证书路径,注意应该填写绝对路径
30 | SSLCERT_PATH = "/******/cacert/apiclient_cert.pem"
31 | SSLKEY_PATH = "/******/cacert/apiclient_key.pem"
32 |
33 | #=======【curl超时设置】===================================
34 | CURL_TIMEOUT = 30
35 |
36 | #=======【HTTP客户端设置】===================================
37 | HTTP_CLIENT = "CURL" # ("URLLIB", "CURL", "REQUESTS")
38 |
39 |
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # weixin-knife 微信开发利器---微信瑞士小刀
2 |
3 |
4 | 封装了微信的基础操作,demo使用了django,核心功能都在目录weixin下
5 |
6 | demo django版本比较老(1.3.7), 抱歉抱歉
7 |
8 |
9 | 使用很方便, 很简单
10 |
11 | @HD.subscribe
12 | def subscribe(xml):
13 | """关注事件"""
14 | return "welcome to weixin-knife"
15 |
16 | @HD.unsubscribe
17 | def subscribe(xml):
18 | """取关事件"""
19 | print "leave weixin-knife"
20 | return "leave"
21 |
22 |
23 | @HD.text
24 | def text(xml):
25 | """文本消息"""
26 | content = xml.Content
27 | if content == "111":
28 | return {"Title":"风景", "Description":"美景", "PicUrl":"http://9smv.com/static/mm/uploads/150411/2-150411115450247.jpg", "Url":"http://9smv.com/beauty/list?category=5"}
29 | elif content == "222":
30 | return [
31 | ["壁纸", "桌面", "http://9smv.com/static/mm/uploads/150411/2-150411115450247.jpg", "http://9smv.com/beauty/list?category=5"],
32 | ["壁纸", "手机", "http://9smv.com/static/mm/uploads/150506/2-150506111A9648.jpg", "http://9smv.com/beauty/list?category=8"]
33 | ]
34 | return "hello world"
35 |
36 | @sns_userinfo
37 | def oauth(request):
38 | """网页授权获取用户信息"""
39 | return HttpResponse(request.openid)
40 |
41 |
42 | 新增jssdk(案列自定义分享), 微信支付,具体看demo
43 |
--------------------------------------------------------------------------------
/example/django_example/question/templates/share.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | JSSDK-自定义分享
5 |
6 |
7 |
8 |
9 |
10 |
48 |
--------------------------------------------------------------------------------
/weixin/backends/fl.py:
--------------------------------------------------------------------------------
1 | #coding=utf8
2 | '''
3 | Created on 2014-5-19
4 | flask 帮助函数
5 |
6 | @author: skycrab
7 |
8 | flask 没有提供Helper函数,可以仿照django的Helper实现
9 |
10 | @app.route('/login', methods=['GET', 'POST'])
11 | @sns_userinfo
12 | def oauth():
13 | openid = g.openid
14 |
15 | '''
16 |
17 | import json
18 | from functools import wraps
19 | from flask import request, redirect, g
20 |
21 | from .. import WeixinHelper
22 | from .common import CommonHelper
23 |
24 | def sns_userinfo_callback(callback=None):
25 | """网页授权获取用户信息装饰器
26 | callback(openid, userinfo):
27 | return user
28 | """
29 | def wrap(func):
30 | @wraps(func)
31 | def inner(*args, **kwargs):
32 | openid = request.cookies.get('openid')
33 | userinfo = None
34 | if not openid:
35 | code = request.args.get("code")
36 | if not code:
37 | return redirect(WeixinHelper.oauth2(request.url))
38 | else:
39 | data = json.loads(WeixinHelper.getAccessTokenByCode(code))
40 | access_token, openid, refresh_token = data["access_token"], data["openid"], data["refresh_token"]
41 | #WeixinHelper.refreshAccessToken(refresh_token)
42 | userinfo = json.loads(WeixinHelper.getSnsapiUserInfo(access_token, openid))
43 | else:
44 | ok, openid = CommonHelper.check_cookie(openid)
45 | if not ok:
46 | return redirect("/")
47 | g.openid = openid
48 | if callable(callback):
49 | g.user = callback(openid, userinfo)
50 | response = func()
51 | return response
52 | return inner
53 | return wrap
54 |
55 | sns_userinfo = sns_userinfo_callback()
--------------------------------------------------------------------------------
/weixin/backends/dj.py:
--------------------------------------------------------------------------------
1 | #coding=utf8
2 | '''
3 | Created on 2014-5-14
4 | django 帮助函数
5 |
6 | @author: skycrab
7 |
8 | @sns_userinfo
9 | def oauth(request):
10 | openid = request.openid
11 |
12 | '''
13 | import json
14 | from functools import wraps
15 | from django.conf import settings
16 | from django.core.cache import cache
17 | from django.shortcuts import redirect
18 |
19 | from .common import CommonHelper
20 | from .. import class_property, WeixinHelper
21 |
22 |
23 | class Helper(CommonHelper):
24 | """微信具体逻辑帮组类"""
25 |
26 | @class_property
27 | def cache(cls):
28 | """返回cache对象"""
29 | return cache
30 |
31 | @class_property
32 | def secret_key(cls):
33 | """返回cookie加密秘钥"""
34 | return settings.SECRET_KEY
35 |
36 |
37 |
38 | def sns_userinfo_callback(callback=None):
39 | """网页授权获取用户信息装饰器
40 | callback(openid, userinfo):
41 | return user
42 | """
43 | def wrap(func):
44 | @wraps(func)
45 | def inner(*args, **kwargs):
46 | request = args[0] #django第一个参数request
47 | openid = request.COOKIES.get('openid')
48 | userinfo = None
49 | if not openid:
50 | code = request.GET.get("code")
51 | if not code:
52 | current = "http://"+ request.get_host() + request.get_full_path()
53 | return redirect(WeixinHelper.oauth2(current))
54 | else:
55 | data = json.loads(WeixinHelper.getAccessTokenByCode(code))
56 | access_token, openid, refresh_token = data["access_token"], data["openid"], data["refresh_token"]
57 | #WeixinHelper.refreshAccessToken(refresh_token)
58 | userinfo = json.loads(WeixinHelper.getSnsapiUserInfo(access_token, openid))
59 | else:
60 | ok, openid = Helper.check_cookie(openid)
61 | if not ok:
62 | return redirect("/")
63 | request.openid = openid
64 | if callable(callback):
65 | request.user = callback(openid, userinfo)
66 | response = func(request)
67 | return response
68 | return inner
69 | return wrap
70 |
71 | sns_userinfo = sns_userinfo_callback()
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/weixin/backends/common.py:
--------------------------------------------------------------------------------
1 | #coding=utf8
2 | '''
3 | Created on 2014-5-19
4 | 通用帮助函数
5 |
6 | @author: skycrab
7 | '''
8 | import hmac
9 | import json
10 |
11 | from .. import WeixinHelper, class_property
12 |
13 |
14 | class CommonHelper(object):
15 |
16 | @class_property
17 | def expire(cls):
18 | """比真实过期减少时间"""
19 | return 300
20 |
21 | @class_property
22 | def cache(cls):
23 | """返回cache对象"""
24 |
25 |
26 | @class_property
27 | def access_token_key(cls):
28 | return "ACCESS_TOKEN"
29 |
30 | @class_property
31 | def jsapi_ticket_key(cls):
32 | return "JSAPI_TICKET"
33 |
34 |
35 | @class_property
36 | def access_token(cls):
37 | cache, key = cls.cache, cls.access_token_key
38 | token = cache.get(key)
39 | if not token:
40 | data = json.loads(WeixinHelper.getAccessToken())
41 | token, expire = data["access_token"], data["expires_in"]
42 | cache.set(key, token, expire-cls.expire)
43 | return token
44 |
45 |
46 | @class_property
47 | def jsapi_ticket(cls):
48 | cache, key = cls.cache, cls.jsapi_ticket_key
49 | ticket = cache.get(key)
50 | if not ticket:
51 | data = json.loads(WeixinHelper.getJsapiTicket(cls.access_token))
52 | ticket, expire = data["ticket"], data["expires_in"]
53 | cache.set(key, ticket, expire-cls.expire)
54 | return ticket
55 |
56 | @classmethod
57 | def send_text_message(cls, openid, message):
58 | """客服主动推送消息"""
59 | return WeixinHelper.sendTextMessage(openid, message, cls.access_token)
60 |
61 | @classmethod
62 | def jsapi_sign(cls, url):
63 | """jsapi_ticket 签名"""
64 | return WeixinHelper.jsapiSign(cls.jsapi_ticket, url)
65 |
66 |
67 | @classmethod
68 | def hmac_sign(cls, key):
69 | return hmac.new(cls.secret_key, key).hexdigest()
70 |
71 | @classmethod
72 | def sign_cookie(cls, key):
73 | """cookie签名"""
74 | return "{0}|{1}".format(key, cls.hmac_sign(key))
75 |
76 | @classmethod
77 | def check_cookie(cls, value):
78 | """验证cookie
79 | 成功返回True, key
80 | """
81 | code = value.split("|", 1)
82 | if len(code) != 2:
83 | return False, None
84 | key, signature = code
85 | if cls.hmac_sign(key) != signature:
86 | return False, None
87 | return True, key
88 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/example/django_example/question/templates/pay.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 砖石充值
9 |
10 |
11 |
12 |
13 |
14 |
15 |
砖石充值
16 |
17 |
w大神
18 |
余额:100砖石
19 |
20 |
21 |
22 |
100砖石
23 |
200砖石
24 |
300砖石
25 |
500砖石
26 |
1000砖石
27 |
28 |
29 |
30 |
31 |
35 |
36 |
砖石作用:
37 |
1.购买商城高级技能
38 |
39 |
40 |
41 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/weixin/handler.py:
--------------------------------------------------------------------------------
1 | #coding=utf8
2 | '''
3 | Created on 2014-5-13
4 |
5 | @author: skycrab
6 | '''
7 | import time
8 | from collections import defaultdict
9 | from .lib import WeixinHelper, ObjectDict
10 |
11 |
12 | class MessageHandle(object):
13 | """消息处理器"""
14 | handler = defaultdict(dict)
15 | def __init__(self, xml):
16 | self.xml = ObjectDict(WeixinHelper.xmlToArray(xml))
17 |
18 |
19 | def start(self):
20 | """开始消息处理"""
21 | msgtype = self.xml.MsgType
22 | if msgtype == "event":
23 | key = self.xml.Event
24 | elif msgtype == "text":
25 | key = "all"
26 | else:
27 | key = ""
28 |
29 | return self.call(msgtype, key)
30 |
31 |
32 | def call(self, type, key):
33 | """回调事件"""
34 | assert type in self.handler
35 | data = self.handler[type][key](self.xml)
36 | response = self.render(data)
37 | return response
38 |
39 |
40 | @classmethod
41 | def register(cls, type, key, func):
42 | """注册事件"""
43 | assert key not in cls.handler
44 | cls.handler[type][key] = func
45 |
46 | def render(self, data):
47 | """消息回复"""
48 | if not data:
49 | return ""
50 | reply = Reply(self.xml)
51 | if isinstance(data, str):
52 | res = reply.textResponse(data)
53 | elif isinstance(data, dict):
54 | res = reply.newsResponse([data])
55 | elif isinstance(data, list): #只有图片可多条消息
56 | data = [reply.newsKey(d) for d in data]
57 | res = reply.newsResponse(data)
58 | else:
59 | raise Exception("unknown message response")
60 |
61 | return res
62 |
63 |
64 | _TEXT = """\
65 |
66 |
67 |
68 | {CreateTime}
69 |
70 |
71 | """
72 |
73 | _ITEM = """\
74 | -
75 |
76 |
77 |
78 |
79 |
"""
80 |
81 | _NEWS = """\
82 |
83 |
84 |
85 | {CreateTime}
86 |
87 | {ArticleCount}
88 |
89 | {Items}
90 |
91 | """
92 |
93 | class Reply(object):
94 | """消息回复"""
95 | def __init__(self, xml):
96 | self.xml = xml
97 | self.xml["CreateTime"] = int(time.time())
98 |
99 | def textResponse(self, data):
100 | """文本消息回复"""
101 | self.xml["Content"] = data
102 | return _TEXT.format(**self.xml)
103 |
104 |
105 | def newsKey(self, ld):
106 | """图文消息列表转换为字典"""
107 | return dict(zip(["Title", "Description", "PicUrl", "Url"], ld))
108 |
109 | def newsResponse(self, data):
110 | """图文消息"""
111 | count = len(data)
112 | if count > 10:
113 | raise Exception("ArticleCount greater then 10")
114 | self.xml["Items"] = "".join([_ITEM.format(**d) for d in data])
115 | self.xml["ArticleCount"] = count
116 | return _NEWS.format(**self.xml)
117 |
118 |
119 |
120 | R = MessageHandle.register
121 |
122 | def subscribe(func):
123 | """关注事件"""
124 | R("event", "subscribe", func)
125 | return func
126 |
127 | def unsubscribe(func):
128 | """取消关注"""
129 | R("event", "unsubscribe", func)
130 | return func
131 |
132 | def click(func):
133 | """点击事件"""
134 | R("event", "CLICK", func)
135 | return func
136 |
137 |
138 | def text(func):
139 | """文本消息"""
140 | R("text", "all", func)
141 | return func
142 |
143 |
--------------------------------------------------------------------------------
/example/django_example/question/views.py:
--------------------------------------------------------------------------------
1 | #coding=utf8
2 | import time
3 | from django.shortcuts import HttpResponse, render_to_response, redirect
4 |
5 | from weixin import handler as HD
6 | from weixin.backends.dj import Helper, sns_userinfo
7 | from weixin import WeixinHelper, JsApi_pub, WxPayConf_pub, UnifiedOrder_pub, Notify_pub, catch
8 |
9 |
10 |
11 | def do(request):
12 | """公众平台对接"""
13 | signature = request.REQUEST.get("signature", "")
14 | timestamp = request.REQUEST.get("timestamp", "")
15 | nonce = request.REQUEST.get("nonce", "")
16 | if not any([signature, timestamp, nonce]) or not WeixinHelper.checkSignature(signature, timestamp, nonce):
17 | return HttpResponse("check failed")
18 |
19 | if request.method == "GET":
20 | return HttpResponse(request.GET.get("echostr"))
21 | elif request.method == "POST":
22 | #print request.raw_post_data
23 | handler = HD.MessageHandle(request.raw_post_data)
24 | response = handler.start()
25 | return HttpResponse(response)
26 | else:
27 | return HttpResponse("")
28 |
29 |
30 | @HD.subscribe
31 | def subscribe(xml):
32 | return "welcome to brain"
33 |
34 | @HD.unsubscribe
35 | def subscribe(xml):
36 | print "leave"
37 | return "leave brain"
38 |
39 |
40 | @HD.text
41 | def text(xml):
42 | content = xml.Content
43 | if content == "111":
44 | return {"Title":"美女", "Description":"比基尼美女", "PicUrl":"http://9smv.com/static/mm/uploads/150411/2-150411115450247.jpg", "Url":"http://9smv.com/beauty/list?category=5"}
45 | elif content == "222":
46 | return [
47 | ["比基尼美女", "比基尼美女", "http://9smv.com/static/mm/uploads/150411/2-150411115450247.jpg", "http://9smv.com/beauty/list?category=5"],
48 | ["长腿美女", "长腿美女", "http://9smv.com/static/mm/uploads/150506/2-150506111A9648.jpg", "http://9smv.com/beauty/list?category=8"]
49 | ]
50 | elif content == "push":
51 | Helper.send_text_message(xml.FromUserName, "推送消息测试")
52 | return "push ok"
53 |
54 | return "hello world"
55 |
56 | @sns_userinfo
57 | def oauth(request):
58 | """网页授权获取用户信息"""
59 | resp = HttpResponse(request.openid)
60 | resp.set_cookie("openid", Helper.sign_cookie(request.openid))
61 | return resp
62 |
63 |
64 |
65 | def share(request):
66 | """jssdk 分享"""
67 | url = "http://"+request.get_host() + request.path
68 | sign = Helper.jsapi_sign(url)
69 | sign["appId"] = WxPayConf_pub.APPID
70 | return render_to_response("share.html", {"jsapi":sign})
71 |
72 | @sns_userinfo
73 | def pay(request):
74 | response = render_to_response("pay.html")
75 | response.set_cookie("openid", Helper.sign_cookie(request.openid))
76 | return response
77 |
78 | @sns_userinfo
79 | @catch
80 | def paydetail(request):
81 | """获取支付信息"""
82 | openid = request.openid
83 | money = request.POST.get("money") or "0.01"
84 | money = int(float(money)*100)
85 |
86 | jsApi = JsApi_pub()
87 | unifiedOrder = UnifiedOrder_pub()
88 | unifiedOrder.setParameter("openid",openid) #商品描述
89 | unifiedOrder.setParameter("body","充值测试") #商品描述
90 | timeStamp = time.time()
91 | out_trade_no = "{0}{1}".format(WxPayConf_pub.APPID, int(timeStamp*100))
92 | unifiedOrder.setParameter("out_trade_no", out_trade_no) #商户订单号
93 | unifiedOrder.setParameter("total_fee", str(money)) #总金额
94 | unifiedOrder.setParameter("notify_url", WxPayConf_pub.NOTIFY_URL) #通知地址
95 | unifiedOrder.setParameter("trade_type", "JSAPI") #交易类型
96 | unifiedOrder.setParameter("attach", "6666") #附件数据,可分辨不同商家(string(127))
97 | try:
98 | prepay_id = unifiedOrder.getPrepayId()
99 | jsApi.setPrepayId(prepay_id)
100 | jsApiParameters = jsApi.getParameters()
101 | except Exception as e:
102 | print(e)
103 | else:
104 | print jsApiParameters
105 | return HttpResponse(jsApiParameters)
106 |
107 |
108 | FAIL, SUCCESS = "FAIL", "SUCCESS"
109 | @catch
110 | def payback(request):
111 | """支付回调"""
112 | xml = request.raw_post_data
113 | #使用通用通知接口
114 | notify = Notify_pub()
115 | notify.saveData(xml)
116 | print xml
117 | #验证签名,并回应微信。
118 | #对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,
119 | #微信会通过一定的策略(如30分钟共8次)定期重新发起通知,
120 | #尽可能提高通知的成功率,但微信不保证通知最终能成功
121 | if not notify.checkSign():
122 | notify.setReturnParameter("return_code", FAIL) #返回状态码
123 | notify.setReturnParameter("return_msg", "签名失败") #返回信息
124 | else:
125 | result = notify.getData()
126 |
127 | if result["return_code"] == FAIL:
128 | notify.setReturnParameter("return_code", FAIL)
129 | notify.setReturnParameter("return_msg", "通信错误")
130 | elif result["result_code"] == FAIL:
131 | notify.setReturnParameter("return_code", FAIL)
132 | notify.setReturnParameter("return_msg", result["err_code_des"])
133 | else:
134 | notify.setReturnParameter("return_code", SUCCESS)
135 | out_trade_no = result["out_trade_no"] #商户系统的订单号,与请求一致。
136 | ###检查订单号是否已存在,以及业务代码(业务代码注意重入问题)
137 |
138 | return HttpResponse(notify.returnXml())
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/example/django_example/settings.py:
--------------------------------------------------------------------------------
1 | # Django settings for brain project.
2 |
3 | import sys
4 | from os.path import dirname
5 | reload(sys)
6 | sys.setdefaultencoding("utf8")
7 | sys.path.append(dirname(dirname(dirname(__file__))))
8 |
9 |
10 | DEBUG = True
11 | TEMPLATE_DEBUG = DEBUG
12 |
13 | ADMINS = (
14 | # ('Your Name', 'your_email@example.com'),
15 | )
16 |
17 | MANAGERS = ADMINS
18 |
19 | DATABASES = {
20 | 'default': {
21 | 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
22 | 'NAME': 'brain', # Or path to database file if using sqlite3.
23 | 'USER': 'root', # Not used with sqlite3.
24 | 'PASSWORD': 'dajidan2bu2', # Not used with sqlite3.
25 | 'HOST': '10.9.28.162', # Set to empty string for localhost. Not used with sqlite3.
26 | 'PORT': '3306', # Set to empty string for default. Not used with sqlite3.
27 | }
28 | }
29 |
30 | # Hosts/domain names that are valid for this site; required if DEBUG is False
31 | # See https://docs.djangoproject.com/en/1.3/ref/settings/#allowed-hosts
32 | ALLOWED_HOSTS = []
33 |
34 | # Local time zone for this installation. Choices can be found here:
35 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
36 | # although not all choices may be available on all operating systems.
37 | # On Unix systems, a value of None will cause Django to use the same
38 | # timezone as the operating system.
39 | # If running in a Windows environment this must be set to the same as your
40 | # system time zone.
41 | TIME_ZONE = 'America/Chicago'
42 |
43 | # Language code for this installation. All choices can be found here:
44 | # http://www.i18nguy.com/unicode/language-identifiers.html
45 | LANGUAGE_CODE = 'zh-cn'
46 |
47 | SITE_ID = 1
48 |
49 | # If you set this to False, Django will make some optimizations so as not
50 | # to load the internationalization machinery.
51 | USE_I18N = True
52 |
53 | # If you set this to False, Django will not format dates, numbers and
54 | # calendars according to the current locale
55 | USE_L10N = True
56 |
57 | # Absolute filesystem path to the directory that will hold user-uploaded files.
58 | # Example: "/home/media/media.lawrence.com/media/"
59 | MEDIA_ROOT = ''
60 |
61 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a
62 | # trailing slash.
63 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
64 | MEDIA_URL = ''
65 |
66 | # Absolute path to the directory static files should be collected to.
67 | # Don't put anything in this directory yourself; store your static files
68 | # in apps' "static/" subdirectories and in STATICFILES_DIRS.
69 | # Example: "/home/media/media.lawrence.com/static/"
70 | STATIC_ROOT = ''
71 |
72 | # URL prefix for static files.
73 | # Example: "http://media.lawrence.com/static/"
74 | STATIC_URL = '/static/'
75 |
76 | # URL prefix for admin static files -- CSS, JavaScript and images.
77 | # Make sure to use a trailing slash.
78 | # Examples: "http://foo.com/static/admin/", "/static/admin/".
79 | ADMIN_MEDIA_PREFIX = '/static/admin/'
80 |
81 | # Additional locations of static files
82 | STATICFILES_DIRS = (
83 | # Put strings here, like "/home/html/static" or "C:/www/django/static".
84 | # Always use forward slashes, even on Windows.
85 | # Don't forget to use absolute paths, not relative paths.
86 | )
87 |
88 | # List of finder classes that know how to find static files in
89 | # various locations.
90 | STATICFILES_FINDERS = (
91 | 'django.contrib.staticfiles.finders.FileSystemFinder',
92 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
93 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
94 | )
95 |
96 | # Make this unique, and don't share it with anybody.
97 | SECRET_KEY = 'f7#_%g#qay@3xf9yg)6iz^4(_p3r)1^fznu&&8$swtml#twdc_'
98 |
99 | # List of callables that know how to import templates from various sources.
100 | TEMPLATE_LOADERS = (
101 | 'django.template.loaders.filesystem.Loader',
102 | 'django.template.loaders.app_directories.Loader',
103 | # 'django.template.loaders.eggs.Loader',
104 | )
105 |
106 | MIDDLEWARE_CLASSES = (
107 | 'django.middleware.common.CommonMiddleware',
108 | 'django.contrib.sessions.middleware.SessionMiddleware',
109 | #'django.middleware.csrf.CsrfViewMiddleware',
110 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
111 | 'django.contrib.messages.middleware.MessageMiddleware',
112 | )
113 |
114 | ROOT_URLCONF = 'django_example.urls'
115 |
116 | TEMPLATE_DIRS = (
117 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
118 | # Always use forward slashes, even on Windows.
119 | # Don't forget to use absolute paths, not relative paths.
120 | )
121 |
122 | INSTALLED_APPS = (
123 | 'django.contrib.auth',
124 | 'django.contrib.contenttypes',
125 | 'django.contrib.sessions',
126 | 'django.contrib.sites',
127 | 'django.contrib.messages',
128 | 'django.contrib.staticfiles',
129 | # Uncomment the next line to enable the admin:
130 | 'django.contrib.admin',
131 | # Uncomment the next line to enable admin documentation:
132 | # 'django.contrib.admindocs',
133 | 'django_example.question',
134 |
135 | )
136 |
137 | # A sample logging configuration. The only tangible logging
138 | # performed by this configuration is to send an email to
139 | # the site admins on every HTTP 500 error.
140 | # See http://docs.djangoproject.com/en/dev/topics/logging for
141 | # more details on how to customize your logging configuration.
142 | LOGGING = {
143 | 'version': 1,
144 | 'disable_existing_loggers': False,
145 | 'handlers': {
146 | 'mail_admins': {
147 | 'level': 'ERROR',
148 | 'class': 'django.utils.log.AdminEmailHandler'
149 | }
150 | },
151 | 'loggers': {
152 | 'django.request': {
153 | 'handlers': ['mail_admins'],
154 | 'level': 'ERROR',
155 | 'propagate': True,
156 | },
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/weixin/lib.py:
--------------------------------------------------------------------------------
1 | #coding=utf8
2 | '''
3 | Created on 2014-5-13
4 |
5 | @author: skycrab
6 | '''
7 | import json
8 | import time
9 | import random
10 | import string
11 | import urllib
12 | import urllib2
13 | import hashlib
14 | import threading
15 | import traceback
16 | import xml.etree.ElementTree as ET
17 |
18 | from functools import wraps
19 |
20 | from .config import WxPayConf_pub
21 |
22 | try:
23 | import pycurl
24 | from cStringIO import StringIO
25 | except ImportError:
26 | pycurl = None
27 |
28 | try:
29 | import requests
30 | except ImportError:
31 | requests = None
32 |
33 | def catch(func):
34 | @wraps(func)
35 | def wrap(*args,**kwargs):
36 | try:
37 | return func(*args,**kwargs)
38 | except Exception as e:
39 | print(traceback.format_exc())
40 | return None
41 | return wrap
42 |
43 | class ObjectDict(dict):
44 | """Makes a dictionary behave like an object, with attribute-style access.
45 | """
46 | def __getattr__(self, name):
47 | try:
48 | return self[name]
49 | except KeyError:
50 | raise AttributeError(name)
51 |
52 | def __setattr__(self, name, value):
53 | self[name] = value
54 |
55 |
56 | class Singleton(object):
57 | """可配置单例模式"""
58 |
59 | _instance_lock = threading.Lock()
60 |
61 | def __new__(cls, *args, **kwargs):
62 | if not hasattr(cls, "_instance"):
63 | with cls._instance_lock:
64 | if not hasattr(cls, "_instance"):
65 | impl = cls.configure() if hasattr(cls, "configure") else cls
66 | instance = super(Singleton, cls).__new__(impl, *args, **kwargs)
67 | if not isinstance(instance, cls):
68 | instance.__init__(*args, **kwargs)
69 | cls._instance = instance
70 | return cls._instance
71 |
72 |
73 | class class_property(object):
74 | """ A property can decorator class or instance
75 |
76 | class Foo(object):
77 | @class_property
78 | def foo(cls):
79 | return 42
80 |
81 |
82 | print(Foo.foo)
83 | print(Foo().foo)
84 |
85 | """
86 | def __init__(self, func, name=None, doc=None):
87 | self.__name__ = name or func.__name__
88 | self.__module__ = func.__module__
89 | self.__doc__ = doc or func.__doc__
90 | self.func = func
91 |
92 | def __get__(self, obj, type=None):
93 | value = self.func(type)
94 | return value
95 |
96 | class BaseHttpClient(object):
97 | include_ssl = False
98 |
99 | def get(self, url, second=30):
100 | if self.include_ssl:
101 | return self.postXmlSSL(None, url, second, False, False)
102 | else:
103 | return self.postXml(None, url, second)
104 |
105 | def postXml(self, xml, url, second=30):
106 | if self.include_ssl:
107 | return self.postXmlSSL(xml, url, second, cert=False)
108 | else:
109 | raise NotImplementedError("please implement postXML")
110 |
111 | def postXmlSSL(self, xml, url, second=30, cert=True, post=True):
112 | raise NotImplementedError("please implement postXMLSSL")
113 |
114 |
115 | class UrllibClient(BaseHttpClient):
116 | """使用urlib2发送请求"""
117 | def postXml(self, xml, url, second=30):
118 | """不使用证书"""
119 | data = urllib2.urlopen(url, xml, timeout=second).read()
120 | return data
121 |
122 |
123 | class CurlClient(BaseHttpClient):
124 | """使用Curl发送请求"""
125 | include_ssl = True
126 |
127 | def __init__(self):
128 | self.curl = pycurl.Curl()
129 | self.curl.setopt(pycurl.SSL_VERIFYHOST, False)
130 | self.curl.setopt(pycurl.SSL_VERIFYPEER, False)
131 | #设置不输出header
132 | self.curl.setopt(pycurl.HEADER, False)
133 |
134 | def postXmlSSL(self, xml, url, second=30, cert=True, post=True):
135 | """使用证书"""
136 | self.curl.setopt(pycurl.URL, url)
137 | self.curl.setopt(pycurl.TIMEOUT, second)
138 | #设置证书
139 | #使用证书:cert 与 key 分别属于两个.pem文件
140 | #默认格式为PEM,可以注释
141 | if cert:
142 | self.curl.setopt(pycurl.SSLKEYTYPE, "PEM")
143 | self.curl.setopt(pycurl.SSLKEY, WxPayConf_pub.SSLKEY_PATH)
144 | self.curl.setopt(pycurl.SSLCERTTYPE, "PEM")
145 | self.curl.setopt(pycurl.SSLCERT, WxPayConf_pub.SSLCERT_PATH)
146 | #post提交方式
147 | if post:
148 | self.curl.setopt(pycurl.POST, True)
149 | self.curl.setopt(pycurl.POSTFIELDS, xml)
150 | buff = StringIO()
151 | self.curl.setopt(pycurl.WRITEFUNCTION, buff.write)
152 |
153 | self.curl.perform()
154 | return buff.getvalue()
155 |
156 |
157 | class RequestsClient(BaseHttpClient):
158 | include_ssl = True
159 |
160 | def postXmlSSL(self, xml, url, second=30, cert=True, post=True):
161 | if cert:
162 | cert_config = (WxPayConf_pub.SSLCERT_PATH, WxPayConf_pub.SSLKEY_PATH)
163 | else:
164 | cert_config = None
165 | if post:
166 | res = requests.post(url, data=xml, second=30, cert=cert_config)
167 | else:
168 | res = requests.get(url, timeout=second, cert=cert_config)
169 | return res.content
170 |
171 | class HttpClient(Singleton, BaseHttpClient):
172 | @classmethod
173 | def configure(cls):
174 | config_client = WxPayConf_pub.HTTP_CLIENT
175 | client_cls = {"urllib": UrllibClient,
176 | "curl": CurlClient,
177 | "requests": RequestsClient}.get(config_client.lower(), None)
178 | if client_cls:
179 | return client_cls
180 |
181 | if pycurl is not None:
182 | print("HTTP_CLIENT config error, Use 'CURL'")
183 | return CurlClient
184 | if requests is not None:
185 | print("HTTP_CLIENT config error, Use 'REQUESTS'")
186 | return RequestsClient
187 | else:
188 | print("HTTP_CLIENT config error, Use 'URLLIB'")
189 | return UrllibClient
190 |
191 |
192 | class WeixinHelper(object):
193 | @classmethod
194 | def checkSignature(cls, signature, timestamp, nonce):
195 | """微信对接签名校验"""
196 | tmp = [WxPayConf_pub.TOKEN, timestamp, nonce]
197 | tmp.sort()
198 | code = hashlib.sha1("".join(tmp)).hexdigest()
199 | return code == signature
200 |
201 | @classmethod
202 | def nonceStr(cls, length):
203 | """随机数"""
204 | return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))
205 |
206 | @classmethod
207 | def xmlToArray(cls, xml):
208 | """将xml转为array"""
209 | return dict((child.tag, child.text) for child in ET.fromstring(xml))
210 |
211 | @classmethod
212 | def oauth2(cls, redirect_uri, scope="snsapi_userinfo", state="STATE"):
213 | """网页授权获取用户信息
214 | http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html
215 | """
216 | _OAUTH_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid={0}&redirect_uri={1}&response_type=code&scope={2}&state={3}#wechat_redirect"
217 | return _OAUTH_URL.format(WxPayConf_pub.APPID, urllib.quote(redirect_uri), scope, state)
218 |
219 | @classmethod
220 | def getAccessToken(cls):
221 | """获取access_token
222 | 需要缓存access_token,由于缓存方式各种各样,不在此提供
223 | http://mp.weixin.qq.com/wiki/11/0e4b294685f817b95cbed85ba5e82b8f.html
224 | """
225 | _ACCESS_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}"
226 | return HttpClient().get(_ACCESS_URL.format(WxPayConf_pub.APPID, WxPayConf_pub.APPSECRET))
227 |
228 |
229 | @classmethod
230 | def getUserInfo(cls, access_token, openid, lang="zh_CN"):
231 | """获取用户基本信息
232 | http://mp.weixin.qq.com/wiki/14/bb5031008f1494a59c6f71fa0f319c66.html
233 | """
234 | _USER_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token={0}&openid={1}&lang={2}"
235 | return HttpClient().get(_USER_URL.format(access_token, openid, lang))
236 |
237 | @classmethod
238 | def getAccessTokenByCode(cls, code):
239 | """通过code换取网页授权access_token, 该access_token与getAccessToken()返回是不一样的
240 | http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html
241 | """
242 | _CODEACCESS_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={0}&secret={1}&code={2}&grant_type=authorization_code"
243 | return HttpClient().get(_CODEACCESS_URL.format(WxPayConf_pub.APPID, WxPayConf_pub.APPSECRET, code))
244 |
245 | @classmethod
246 | def refreshAccessToken(cls, refresh_token):
247 | """刷新access_token, 使用getAccessTokenByCode()返回的refresh_token刷新access_token,可获得较长时间有效期
248 | http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html
249 | """
250 | _REFRESHTOKRN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid={0}&grant_type=refresh_token&refresh_token={1}"
251 | return HttpClient().get(_REFRESHTOKRN_URL.format(WxPayConf_pub.APPID, refresh_token))
252 |
253 |
254 | @classmethod
255 | def getSnsapiUserInfo(cls, access_token, openid, lang="zh_CN"):
256 | """拉取用户信息(通过网页授权)
257 | """
258 | _SNSUSER_URL = "https://api.weixin.qq.com/sns/userinfo?access_token={0}&openid={1}&lang={2}"
259 | return HttpClient().get(_SNSUSER_URL.format(access_token, openid, lang))
260 |
261 | @classmethod
262 | def send(cls, data, access_token):
263 | """发送客服消息
264 | http://mp.weixin.qq.com/wiki/1/70a29afed17f56d537c833f89be979c9.html
265 | """
266 | _SEND_URL ="https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={0}"
267 | data = json.dumps(data, ensure_ascii=False)
268 | return HttpClient().postXml(data, _SEND_URL.format(access_token))
269 |
270 | @classmethod
271 | def sendTextMessage(cls, openid, message, access_token):
272 | """发送文本消息
273 | """
274 | data = {
275 | "touser": openid,
276 | "msgtype":"text",
277 | "text":
278 | {
279 | "content": message
280 | }
281 | }
282 | return cls.send(data, access_token)
283 |
284 | @classmethod
285 | def getJsapiTicket(cls, access_token):
286 | """获取jsapi_tocket
287 | """
288 | _JSAPI_URL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=jsapi"
289 | return HttpClient().get(_JSAPI_URL.format(access_token))
290 |
291 |
292 | @classmethod
293 | def jsapiSign(cls, jsapi_ticket, url):
294 | """jsapi_ticket 签名"""
295 | sign = {
296 | 'nonceStr': cls.nonceStr(15),
297 | 'jsapi_ticket': jsapi_ticket,
298 | 'timestamp': int(time.time()),
299 | 'url': url
300 | }
301 | signature = '&'.join(['%s=%s' % (key.lower(), sign[key]) for key in sorted(sign)])
302 | sign["signature"] = hashlib.sha1(signature).hexdigest()
303 | return sign
304 |
--------------------------------------------------------------------------------
/weixin/pay.py:
--------------------------------------------------------------------------------
1 | #coding:utf-8
2 | """
3 | Created on 2014-11-24
4 |
5 | @author: http://blog.csdn.net/yueguanghaidao,yihaibo@longtugame.com
6 |
7 | * 微信支付帮助库
8 | * ====================================================
9 | * 接口分三种类型:
10 | * 【请求型接口】--Wxpay_client_
11 | * 统一支付接口类--UnifiedOrder
12 | * 订单查询接口--OrderQuery
13 | * 退款申请接口--Refund
14 | * 退款查询接口--RefundQuery
15 | * 对账单接口--DownloadBill
16 | * 短链接转换接口--ShortUrl
17 | * 【响应型接口】--Wxpay_server_
18 | * 通用通知接口--Notify
19 | * Native支付——请求商家获取商品信息接口--NativeCall
20 | * 【其他】
21 | * 静态链接二维码--NativeLink
22 | * JSAPI支付--JsApi
23 | * =====================================================
24 | * 【CommonUtil】常用工具:
25 | * trimString(),设置参数时需要用到的字符处理函数
26 | * createNoncestr(),产生随机字符串,不长于32位
27 | * formatBizQueryParaMap(),格式化参数,签名过程需要用到
28 | * getSign(),生成签名
29 | * arrayToXml(),array转xml
30 | * xmlToArray(),xml转 array
31 | * postXmlCurl(),以post方式提交xml到对应的接口url
32 | * postXmlSSLCurl(),使用证书,以post方式提交xml到对应的接口url
33 |
34 | """
35 |
36 | import json
37 | import time
38 | import random
39 | import hashlib
40 | from urllib import quote
41 |
42 | from .config import WxPayConf_pub
43 | from .lib import HttpClient, WeixinHelper
44 |
45 |
46 | ###############################支付接口############################
47 |
48 |
49 | class Common_util_pub(object):
50 | """所有接口的基类"""
51 |
52 | def trimString(self, value):
53 | if value is not None and len(value) == 0:
54 | value = None
55 | return value
56 |
57 | def createNoncestr(self, length = 32):
58 | """产生随机字符串,不长于32位"""
59 | chars = "abcdefghijklmnopqrstuvwxyz0123456789"
60 | strs = []
61 | for x in range(length):
62 | strs.append(chars[random.randrange(0, len(chars))])
63 | return "".join(strs)
64 |
65 | def formatBizQueryParaMap(self, paraMap, urlencode):
66 | """格式化参数,签名过程需要使用"""
67 | slist = sorted(paraMap)
68 | buff = []
69 | for k in slist:
70 | v = quote(paraMap[k]) if urlencode else paraMap[k]
71 | buff.append("{0}={1}".format(k, v))
72 |
73 | return "&".join(buff)
74 |
75 | def getSign(self, obj):
76 | """生成签名"""
77 | #签名步骤一:按字典序排序参数,formatBizQueryParaMap已做
78 | String = self.formatBizQueryParaMap(obj, False)
79 | #签名步骤二:在string后加入KEY
80 | String = "{0}&key={1}".format(String,WxPayConf_pub.KEY)
81 | #签名步骤三:MD5加密
82 | String = hashlib.md5(String).hexdigest()
83 | #签名步骤四:所有字符转为大写
84 | result_ = String.upper()
85 | return result_
86 |
87 | def arrayToXml(self, arr):
88 | """array转xml"""
89 | xml = [""]
90 | for k, v in arr.iteritems():
91 | if v.isdigit():
92 | xml.append("<{0}>{1}{0}>".format(k, v))
93 | else:
94 | xml.append("<{0}>{0}>".format(k, v))
95 | xml.append("")
96 | return "".join(xml)
97 |
98 | def xmlToArray(self, xml):
99 | """将xml转为array"""
100 | return WeixinHelper.xmlToArray(xml)
101 |
102 | def postXmlCurl(self, xml, url, second=30):
103 | """以post方式提交xml到对应的接口url"""
104 | return HttpClient().postXml(xml, url, second=second)
105 |
106 | def postXmlSSLCurl(self, xml, url, second=30):
107 | """使用证书,以post方式提交xml到对应的接口url"""
108 | return HttpClient().postXmlSSL(xml, url, second=second)
109 |
110 |
111 | class JsApi_pub(Common_util_pub):
112 | """JSAPI支付——H5网页端调起支付接口"""
113 | code = None #code码,用以获取openid
114 | openid = None #用户的openid
115 | parameters = None #jsapi参数,格式为json
116 | prepay_id = None #使用统一支付接口得到的预支付id
117 | curl_timeout = None #curl超时时间
118 |
119 | def __init__(self, timeout=WxPayConf_pub.CURL_TIMEOUT):
120 | self.curl_timeout = timeout
121 |
122 | def createOauthUrlForCode(self, redirectUrl):
123 | """生成可以获得code的url"""
124 | urlObj = {}
125 | urlObj["appid"] = WxPayConf_pub.APPID
126 | urlObj["redirect_uri"] = redirectUrl
127 | urlObj["response_type"] = "code"
128 | urlObj["scope"] = "snsapi_base"
129 | urlObj["state"] = "STATE#wechat_redirect"
130 | bizString = self.formatBizQueryParaMap(urlObj, False)
131 | return "https://open.weixin.qq.com/connect/oauth2/authorize?"+bizString
132 |
133 | def createOauthUrlForOpenid(self):
134 | """生成可以获得openid的url"""
135 | urlObj = {}
136 | urlObj["appid"] = WxPayConf_pub.APPID
137 | urlObj["secret"] = WxPayConf_pub.APPSECRET
138 | urlObj["code"] = self.code
139 | urlObj["grant_type"] = "authorization_code"
140 | bizString = self.formatBizQueryParaMap(urlObj, False)
141 | return "https://api.weixin.qq.com/sns/oauth2/access_token?"+bizString
142 |
143 | def getOpenid(self):
144 | """通过curl向微信提交code,以获取openid"""
145 | url = self.createOauthUrlForOpenid()
146 | data = HttpClient().get(url)
147 | self.openid = json.loads(data)["openid"]
148 | return self.openid
149 |
150 |
151 | def setPrepayId(self, prepayId):
152 | """设置prepay_id"""
153 | self.prepay_id = prepayId
154 |
155 | def setCode(self, code):
156 | """设置code"""
157 | self.code = code
158 |
159 | def getParameters(self):
160 | """设置jsapi的参数"""
161 | jsApiObj = {}
162 | jsApiObj["appId"] = WxPayConf_pub.APPID
163 | timeStamp = int(time.time())
164 | jsApiObj["timeStamp"] = "{0}".format(timeStamp)
165 | jsApiObj["nonceStr"] = self.createNoncestr()
166 | jsApiObj["package"] = "prepay_id={0}".format(self.prepay_id)
167 | jsApiObj["signType"] = "MD5"
168 | jsApiObj["paySign"] = self.getSign(jsApiObj)
169 | self.parameters = json.dumps(jsApiObj)
170 |
171 | return self.parameters
172 |
173 |
174 | class Wxpay_client_pub(Common_util_pub):
175 | """请求型接口的基类"""
176 | response = None #微信返回的响应
177 | url = None #接口链接
178 | curl_timeout = None #curl超时时间
179 |
180 | def __init__(self):
181 | self.parameters = {} #请求参数,类型为关联数组
182 | self.result = {} #返回参数,类型为关联数组
183 |
184 |
185 | def setParameter(self, parameter, parameterValue):
186 | """设置请求参数"""
187 | self.parameters[self.trimString(parameter)] = self.trimString(parameterValue)
188 |
189 | def createXml(self):
190 | """设置标配的请求参数,生成签名,生成接口参数xml"""
191 | self.parameters["appid"] = WxPayConf_pub.APPID #公众账号ID
192 | self.parameters["mch_id"] = WxPayConf_pub.MCHID #商户号
193 | self.parameters["nonce_str"] = self.createNoncestr() #随机字符串
194 | self.parameters["sign"] = self.getSign(self.parameters) #签名
195 | return self.arrayToXml(self.parameters)
196 |
197 | def postXml(self):
198 | """post请求xml"""
199 | xml = self.createXml()
200 | self.response = self.postXmlCurl(xml, self.url, self.curl_timeout)
201 | return self.response
202 |
203 | def postXmlSSL(self):
204 | """使用证书post请求xml"""
205 | xml = self.createXml()
206 | self.response = self.postXmlSSLCurl(xml, self.url, self.curl_timeout)
207 | return self.response
208 |
209 | def getResult(self):
210 | """获取结果,默认不使用证书"""
211 | self.postXml()
212 | self.result = self.xmlToArray(self.response)
213 | return self.result
214 |
215 |
216 | class UnifiedOrder_pub(Wxpay_client_pub):
217 | """统一支付接口类"""
218 |
219 | def __init__(self, timeout=WxPayConf_pub.CURL_TIMEOUT):
220 | #设置接口链接
221 | self.url = "https://api.mch.weixin.qq.com/pay/unifiedorder"
222 | #设置curl超时时间
223 | self.curl_timeout = timeout
224 | super(UnifiedOrder_pub, self).__init__()
225 |
226 |
227 | def createXml(self):
228 | """生成接口参数xml"""
229 | #检测必填参数
230 | if any(self.parameters[key] is None for key in ("out_trade_no", "body", "total_fee", "notify_url", "trade_type")):
231 | raise ValueError("missing parameter")
232 | if self.parameters["trade_type"] == "JSAPI" and self.parameters["openid"] is None:
233 | raise ValueError("JSAPI need openid parameters")
234 |
235 | self.parameters["appid"] = WxPayConf_pub.APPID #公众账号ID
236 | self.parameters["mch_id"] = WxPayConf_pub.MCHID #商户号
237 | self.parameters["spbill_create_ip"] = "127.0.0.1" #终端ip
238 | self.parameters["nonce_str"] = self.createNoncestr() #随机字符串
239 | self.parameters["sign"] = self.getSign(self.parameters) #签名
240 | return self.arrayToXml(self.parameters)
241 |
242 | def getPrepayId(self):
243 | """获取prepay_id"""
244 | self.postXml()
245 | self.result = self.xmlToArray(self.response)
246 | prepay_id = self.result["prepay_id"]
247 | return prepay_id
248 |
249 |
250 | class OrderQuery_pub(Wxpay_client_pub):
251 | """订单查询接口"""
252 |
253 | def __init__(self, timeout=WxPayConf_pub.CURL_TIMEOUT):
254 | #设置接口链接
255 | self.url = "https://api.mch.weixin.qq.com/pay/orderquery"
256 | #设置curl超时时间
257 | self.curl_timeout = timeout
258 | super(OrderQuery_pub, self).__init__()
259 |
260 | def createXml(self):
261 | """生成接口参数xml"""
262 |
263 | #检测必填参数
264 | if any(self.parameters[key] is None for key in ("out_trade_no", "transaction_id")):
265 | raise ValueError("missing parameter")
266 |
267 | self.parameters["appid"] = WxPayConf_pub.APPID #公众账号ID
268 | self.parameters["mch_id"] = WxPayConf_pub.MCHID #商户号
269 | self.parameters["nonce_str"] = self.createNoncestr() #随机字符串
270 | self.parameters["sign"] = self.getSign(self.parameters) #签名
271 | return self.arrayToXml(self.parameters)
272 |
273 |
274 | class Refund_pub(Wxpay_client_pub):
275 | """退款申请接口"""
276 |
277 | def __init__(self, timeout=WxPayConf_pub.CURL_TIMEOUT):
278 | #设置接口链接
279 | self.url = "https://api.mch.weixin.qq.com/secapi/pay/refund"
280 | #设置curl超时时间
281 | self.curl_timeout = timeout
282 | super(Refund_pub, self).__init__()
283 |
284 | def createXml(self):
285 | """生成接口参数xml"""
286 | if any(self.parameters[key] is None for key in ("out_trade_no", "out_refund_no", "total_fee", "refund_fee", "op_user_id")):
287 | raise ValueError("missing parameter")
288 |
289 | self.parameters["appid"] = WxPayConf_pub.APPID #公众账号ID
290 | self.parameters["mch_id"] = WxPayConf_pub.MCHID #商户号
291 | self.parameters["nonce_str"] = self.createNoncestr() #随机字符串
292 | self.parameters["sign"] = self.getSign(self.parameters) #签名
293 | return self.arrayToXml(self.parameters)
294 |
295 | def getResult(self):
296 | """ 获取结果,使用证书通信(需要双向证书)"""
297 | self.postXmlSSL()
298 | self.result = self.xmlToArray(self.response)
299 | return self.result
300 |
301 |
302 | class RefundQuery_pub(Wxpay_client_pub):
303 | """退款查询接口"""
304 |
305 | def __init__(self, timeout=WxPayConf_pub.CURL_TIMEOUT):
306 | #设置接口链接
307 | self.url = "https://api.mch.weixin.qq.com/pay/refundquery"
308 | #设置curl超时时间
309 | self.curl_timeout = timeout
310 | super(RefundQuery_pub, self).__init__()
311 |
312 | def createXml(self):
313 | """生成接口参数xml"""
314 | if any(self.parameters[key] is None for key in ("out_refund_no", "out_trade_no", "transaction_id", "refund_id")):
315 | raise ValueError("missing parameter")
316 | self.parameters["appid"] = WxPayConf_pub.APPID #公众账号ID
317 | self.parameters["mch_id"] = WxPayConf_pub.MCHID #商户号
318 | self.parameters["nonce_str"] = self.createNoncestr() #随机字符串
319 | self.parameters["sign"] = self.getSign(self.parameters) #签名
320 | return self.arrayToXml(self.parameters)
321 |
322 | def getResult(self):
323 | """ 获取结果,使用证书通信(需要双向证书)"""
324 | self.postXmlSSL()
325 | self.result = self.xmlToArray(self.response)
326 | return self.result
327 |
328 |
329 | class DownloadBill_pub(Wxpay_client_pub):
330 | """对账单接口"""
331 |
332 | def __init__(self, timeout=WxPayConf_pub.CURL_TIMEOUT):
333 | #设置接口链接
334 | self.url = "https://api.mch.weixin.qq.com/pay/downloadbill"
335 | #设置curl超时时间
336 | self.curl_timeout = timeout
337 | super(DownloadBill_pub, self).__init__()
338 |
339 | def createXml(self):
340 | """生成接口参数xml"""
341 | if any(self.parameters[key] is None for key in ("bill_date", )):
342 | raise ValueError("missing parameter")
343 |
344 | self.parameters["appid"] = WxPayConf_pub.APPID #公众账号ID
345 | self.parameters["mch_id"] = WxPayConf_pub.MCHID #商户号
346 | self.parameters["nonce_str"] = self.createNoncestr() #随机字符串
347 | self.parameters["sign"] = self.getSign(self.parameters) #签名
348 | return self.arrayToXml(self.parameters)
349 |
350 | def getResult(self):
351 | """获取结果,默认不使用证书"""
352 | self.postXml()
353 | self.result = self.xmlToArray(self.response)
354 | return self.result
355 |
356 |
357 | class ShortUrl_pub(Wxpay_client_pub):
358 | """短链接转换接口"""
359 |
360 | def __init__(self, timeout=WxPayConf_pub.CURL_TIMEOUT):
361 | #设置接口链接
362 | self.url = "https://api.mch.weixin.qq.com/tools/shorturl"
363 | #设置curl超时时间
364 | self.curl_timeout = timeout
365 | super(ShortUrl_pub, self).__init__()
366 |
367 | def createXml(self):
368 | """生成接口参数xml"""
369 | if any(self.parameters[key] is None for key in ("long_url", )):
370 | raise ValueError("missing parameter")
371 |
372 | self.parameters["appid"] = WxPayConf_pub.APPID #公众账号ID
373 | self.parameters["mch_id"] = WxPayConf_pub.MCHID #商户号
374 | self.parameters["nonce_str"] = self.createNoncestr() #随机字符串
375 | self.parameters["sign"] = self.getSign(self.parameters) #签名
376 | return self.arrayToXml(self.parameters)
377 |
378 | def getShortUrl(self):
379 | """获取prepay_id"""
380 | self.postXml()
381 | prepay_id = self.result["short_url"]
382 | return prepay_id
383 |
384 |
385 |
386 | class Wxpay_server_pub(Common_util_pub):
387 | """响应型接口基类"""
388 | SUCCESS, FAIL = "SUCCESS", "FAIL"
389 |
390 | def __init__(self):
391 | self.data = {} #接收到的数据,类型为关联数组
392 | self.returnParameters = {} #返回参数,类型为关联数组
393 |
394 | def saveData(self, xml):
395 | """将微信的请求xml转换成关联数组,以方便数据处理"""
396 | self.data = self.xmlToArray(xml)
397 |
398 | def checkSign(self):
399 | """校验签名"""
400 | tmpData = dict(self.data) #make a copy to save sign
401 | del tmpData['sign']
402 | sign = self.getSign(tmpData) #本地签名
403 | if self.data['sign'] == sign:
404 | return True
405 | return False
406 |
407 | def getData(self):
408 | """获取微信的请求数据"""
409 | return self.data
410 |
411 | def setReturnParameter(self, parameter, parameterValue):
412 | """设置返回微信的xml数据"""
413 | self.returnParameters[self.trimString(parameter)] = self.trimString(parameterValue)
414 |
415 | def createXml(self):
416 | """生成接口参数xml"""
417 | return self.arrayToXml(self.returnParameters)
418 |
419 | def returnXml(self):
420 | """将xml数据返回微信"""
421 | returnXml = self.createXml()
422 | return returnXml
423 |
424 |
425 | class Notify_pub(Wxpay_server_pub):
426 | """通用通知接口"""
427 |
428 |
429 |
430 | class NativeCall_pub(Wxpay_server_pub):
431 | """请求商家获取商品信息接口"""
432 |
433 | def createXml(self):
434 | """生成接口参数xml"""
435 | if self.returnParameters["return_code"] == self.SUCCESS:
436 | self.returnParameters["appid"] = WxPayConf_pub.APPID #公众账号ID
437 | self.returnParameters["mch_id"] = WxPayConf_pub.MCHID #商户号
438 | self.returnParameters["nonce_str"] = self.createNoncestr() #随机字符串
439 | self.returnParameters["sign"] = self.getSign(self.returnParameters) #签名
440 |
441 | return self.arrayToXml(self.returnParameters)
442 |
443 | def getProductId(self):
444 | """获取product_id"""
445 | product_id = self.data["product_id"]
446 | return product_id
447 |
448 |
449 | class NativeLink_pub(Common_util_pub):
450 | """静态链接二维码"""
451 |
452 | url = None #静态链接
453 |
454 | def __init__(self):
455 | self.parameters = {} #静态链接参数
456 |
457 | def setParameter(self, parameter, parameterValue):
458 | """设置参数"""
459 | self.parameters[self.trimString(parameter)] = self.trimString(parameterValue)
460 |
461 | def createLink(self):
462 | if any(self.parameters[key] is None for key in ("product_id", )):
463 | raise ValueError("missing parameter")
464 |
465 | self.parameters["appid"] = WxPayConf_pub.APPID #公众账号ID
466 | self.parameters["mch_id"] = WxPayConf_pub.MCHID #商户号
467 | time_stamp = int(time.time())
468 | self.parameters["time_stamp"] = "{0}".format(time_stamp) #时间戳
469 | self.parameters["nonce_str"] = self.createNoncestr() #随机字符串
470 | self.parameters["sign"] = self.getSign(self.parameters) #签名
471 | bizString = self.formatBizQueryParaMap(self.parameters, False)
472 | self.url = "weixin://wxpay/bizpayurl?"+bizString
473 |
474 | def getUrl(self):
475 | """返回链接"""
476 | self.createLink()
477 | return self.url
478 |
479 |
480 | def test():
481 | c = HttpClient()
482 | assert c.get("http://www.baidu.com")[:15] == ""
483 | c2 = HttpClient()
484 | assert id(c) == id(c2)
485 |
486 |
487 |
488 | if __name__ == "__main__":
489 | test()
--------------------------------------------------------------------------------