├── 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 |
32 |

01折)

33 | 34 |
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 | <![CDATA[{Title}]]> 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}".format(k, v)) 93 | else: 94 | xml.append("<{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() --------------------------------------------------------------------------------