├── .gitignore ├── LICENSE ├── README.md ├── demo ├── demo │ ├── __init__.py │ ├── celery.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py └── wechat │ ├── __init__.py │ ├── admin.py │ ├── caches.py │ ├── choices.py │ ├── consts.py │ ├── managers.py │ ├── migrations │ └── __init__.py │ ├── models.py │ ├── tasks.py │ ├── tests.py │ ├── urls.py │ ├── utils.py │ └── views.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 wechatpy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-wechat-example 2 | 3 | 本项目使用django、celery、wechatpy开发微信公众号第三方平台的demo。 4 | 5 | 6 | 操作说明 7 | ----------- 8 | 1. 假设第三方平台有如下配置 9 | 10 | - 登录授权的发起页域名 somewebsite.cn 11 | - 授权事件接收URL http://www.somewebsite.cn/wechat/callback 12 | - 公众号消息与事件接收URL http://www.somewebsite.cn/wechat/server/$APPID$ 13 | - 网页开发域名 www.somewebsite.cn 14 | 15 | 2. 修改 demo/settings.py 16 | - COMPONENT_APP_ID = 'app_id' 17 | - COMPONENT_APP_SECRET = '0c79eferferfeferf0cc0be99b20a18faeb' 18 | - COMPONENT_APP_TOKEN = 'srgewgegerferf' 19 | - COMPONENT_ENCODINGAESKEY = 'bz5LSXhcaIBIBKJWZpk2tRl4fiBVbfPN5VlYgwXKTwp' 20 | - AUTH_REDIRECT_URI = 'http://www.somewebsite.cn/wechat' 21 | 22 | 3. 初始化Django项目 23 | - python manage.py makemigrations 24 | - python manage.py migrate 25 | - \# 在 www.somewebsite.com主机上运行开发服务器 26 | - sudo python manage.py runserver 0.0.0.0:80 27 | 28 | 4. 打开浏览器测试一下 29 | - 打开http://www.somewebsite.cn/wechat/auth,获得预授权链接 30 | - 点击预授权链接,页面跳转到微信授权页面 31 | - 用微信扫描页面上的二维码 32 | - 在手机上选择要授权的公众号 33 | - 授权成功,浏览器跳转到http://www.somewebsite.cn/wechat 34 | - 授权过程完成 35 | 36 | 5. 开始写自己的逻辑 37 | 38 | 39 | 开发说明 40 | -------- 41 | 42 | 1. component和所有公众号的token信息会自动放入`caches['wechat']`中。要获取`component`对象,使用`wechat.utils.get_component`即可。 43 | 2. 授权成功后,微信服务器会调用`AUTH_REDIRECT_URI`,将授权码带过来,`AUTH_REDIRECT_URI`会获得公众号的信息,并保存到Wechat模型中。 44 | 3. 需要启动celery定时任务,以保证已授权的公众号的token不会失效。 45 | -------------------------------------------------------------------------------- /demo/demo/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .celery import app 4 | -------------------------------------------------------------------------------- /demo/demo/celery.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, unicode_literals 3 | import os 4 | import logging 5 | from celery import Celery 6 | 7 | 8 | # wechatpy依赖requests。这里提高requests日志输出的级别 9 | logging.getLogger("requests").setLevel(logging.WARNING) 10 | 11 | # set the default Django settings module for the 'celery' program. 12 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo.settings') 13 | 14 | from django.conf import settings 15 | app = Celery('demo') 16 | 17 | # Using a string here means the worker will not have to 18 | # pickle the object when using Windows. 19 | app.config_from_object('django.conf:settings') 20 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 21 | -------------------------------------------------------------------------------- /demo/demo/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | """ 4 | Django settings for demo project. 5 | 6 | Generated by 'django-admin startproject' using Django 1.8.4. 7 | 8 | For more information on this file, see 9 | https://docs.djangoproject.com/en/1.8/topics/settings/ 10 | 11 | For the full list of settings and their values, see 12 | https://docs.djangoproject.com/en/1.8/ref/settings/ 13 | """ 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | import os 17 | from celery.schedules import crontab 18 | 19 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 20 | 21 | 22 | # Quick-start development settings - unsuitable for production 23 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 24 | 25 | # SECURITY WARNING: keep the secret key used in production secret! 26 | SECRET_KEY = 'm+8@0kna^t-#0sb45uv8n^_jz%e9myq(i2=oi)h_dn94$z&e@s' 27 | 28 | # SECURITY WARNING: don't run with debug turned on in production! 29 | DEBUG = True 30 | 31 | ALLOWED_HOSTS = [] 32 | 33 | 34 | # Application definition 35 | 36 | INSTALLED_APPS = ( 37 | 'django.contrib.admin', 38 | 'django.contrib.auth', 39 | 'django.contrib.contenttypes', 40 | 'django.contrib.sessions', 41 | 'django.contrib.messages', 42 | 'django.contrib.staticfiles', 43 | 'rest_framework', 44 | 'wechat', 45 | ) 46 | 47 | MIDDLEWARE_CLASSES = ( 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | 'django.middleware.security.SecurityMiddleware', 56 | ) 57 | 58 | ROOT_URLCONF = 'demo.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'demo.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 86 | } 87 | } 88 | 89 | 90 | # Internationalization 91 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 92 | 93 | LANGUAGE_CODE = 'en-us' 94 | 95 | TIME_ZONE = 'UTC' 96 | 97 | USE_I18N = True 98 | 99 | USE_L10N = True 100 | 101 | USE_TZ = True 102 | 103 | 104 | # Static files (CSS, JavaScript, Images) 105 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 106 | 107 | STATIC_URL = '/static/' 108 | 109 | CACHES = { 110 | 'default': { 111 | 'BACKEND': 'redis_cache.RedisCache', 112 | 'LOCATION': ['127.0.0.1:6379'], 113 | 'OPTIONS': { 114 | 'DB': 1, 115 | 'PARSER_CLASS': 'redis.connection.HiredisParser', 116 | 'CONNECTION_POOL_CLASS': 'redis.BlockingConnectionPool', 117 | 'CONNECTION_POOL_CLASS_KWARGS': { 118 | 'max_connections': 50, 119 | 'timeout': 20, 120 | }, 121 | 'MAX_CONNECTIONS': 1000, 122 | 'PICKLE_VERSION': -1, 123 | }, 124 | }, 125 | # 存放微信公众号 token 126 | 'wechat': { 127 | 'BACKEND': 'redis_cache.RedisCache', 128 | 'LOCATION': ['127.0.0.1:6379'], 129 | 'OPTIONS': { 130 | 'DB': 2, 131 | 'PARSER_CLASS': 'redis.connection.HiredisParser', 132 | 'CONNECTION_POOL_CLASS': 'redis.BlockingConnectionPool', 133 | 'CONNECTION_POOL_CLASS_KWARGS': { 134 | 'max_connections': 50, 135 | 'timeout': 20, 136 | }, 137 | 'MAX_CONNECTIONS': 1000, 138 | 'PICKLE_VERSION': -1, 139 | }, 140 | } 141 | } 142 | 143 | #============================================================================== 144 | # 微信第三方平台配置 145 | #============================================================================== 146 | COMPONENT_APP_ID = 'app_id' 147 | COMPONENT_APP_SECRET = '0c79eferferfeferf0cc0be99b20a18faeb' 148 | COMPONENT_APP_TOKEN = 'srgewgegerferf' 149 | COMPONENT_ENCODINGAESKEY = 'bz5LSXhcaIBIBKJWZpk2tRl4fiBVbfPN5VlYgwXKTwp' 150 | # 公众号授权链接,大括号中是需要替换的部分 151 | AUTH_URL = ( 152 | "https://mp.weixin.qq.com/cgi-bin/componentloginpage" 153 | "?component_appid={component_appid}&pre_auth_code=" 154 | "{pre_auth_code}&redirect_uri={redirect_uri}" 155 | ) 156 | # 授权成功之后返回链接。此链接应该调用 /wechat/authorized/ API。 157 | AUTH_REDIRECT_URI = 'http://www.somewebsite.com/wechat/authorized_successful' 158 | # 开放平台发布前测试 159 | TEST_APPID = 'wx570bc396a51b8ff8' 160 | 161 | 162 | #============================================================================== 163 | # Celery配置 164 | #============================================================================== 165 | CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/1' 166 | BROKER_URL = "redis://127.0.0.1:6379/10" 167 | CELERY_TASK_RESULT_EXPIRES = 10 168 | CELERY_TIMEZONE = "Asia/Shanghai" 169 | CELERY_ENABLE_UTC = False 170 | UTC_ENABLE = False 171 | CELERY_ACCEPT_CONTENT = ['pickle', 'json', 'msgpack', 'yaml'] 172 | CELERY_TASK_SERIALIZER = 'pickle' 173 | CELERYD_MAX_TASKS_PER_CHILD = 2000 174 | CELERY_TIMEZONE = 'UTC' 175 | CELERYD_TASK_LOG_LEVEL = 'INFO' 176 | CELERY_DEFAULT_EXCHANGE = 'default' 177 | # 定时任务配置 178 | CELERYBEAT_SCHEDULE = { 179 | # 每小时刷新所有公众号 token 180 | "refresh_all_wechat_token": { 181 | 'task': 'wechat.tasks.refresh_all_wechat_token', 182 | 'schedule': crontab(hour='*', minute=10), 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | """demo URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.8/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Add an import: from blog import urls as blog_urls 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 15 | """ 16 | from django.conf.urls import include, url 17 | from django.contrib import admin 18 | from wechat import urls as wechat_urls 19 | 20 | urlpatterns = [ 21 | url(r'^admin/', include(admin.site.urls)), 22 | url(r'^wechat/', include(wechat_urls)), 23 | ] 24 | -------------------------------------------------------------------------------- /demo/demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /demo/wechat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechatpy/django-wechat-example/ab080029e990d4e23558b409673175d42145d51d/demo/wechat/__init__.py -------------------------------------------------------------------------------- /demo/wechat/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /demo/wechat/caches.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | # key(wechat_id): value(wechat_secret) 3 | CACHE_WECHAT_ID = 'wechat_id:{0}' 4 | CACHE_WECHAT_ACCESS_CODE = '{0}_access_token' 5 | CACHE_WECHAT_REFRESH_CODE = '{0}_refresh_token' 6 | COMPONENT_VERIFY_TICKET = 'component_verify_ticket' 7 | -------------------------------------------------------------------------------- /demo/wechat/choices.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from wechat import consts 4 | 5 | #============================================================================= 6 | # 公众号类型 7 | #============================================================================= 8 | WECHAT_TYPE_CHOICES = ( 9 | (consts.WECHAT_TYPE_SUB, '订阅号'), 10 | (consts.WECHAT_TYPE_SUB2, '升级后的订阅号'), 11 | (consts.WECHAT_TYPE_SERVICE, '服务号'), 12 | ) 13 | 14 | 15 | #============================================================================= 16 | # 公众号认证类型 17 | #============================================================================= 18 | VERIFY_TYPE_CHOICES = ( 19 | (consts.VERIFY_TYPE_NONE, '未认证'), 20 | (consts.VERIFY_TYPE_WECHAT, '微信认证'), 21 | (consts.VERIFY_TYPE_WEIBO, '新浪微博认证'), 22 | (consts.VERIFY_TYPE_TENCENT_WEIBO, '腾讯微博认证'), 23 | (consts.VERIFY_TYPE_BASE_VERIFY, '已资质认证通过但还未通过名称认证'), 24 | (consts.VERIFY_TYPE_3_1, '已资质认证通过、还未通过名称认证,但通过了新浪微博认证'), 25 | (consts.VERIFY_TYPE_3_2, '已资质认证通过、还未通过名称认证,但通过了腾讯微博认证'), 26 | ) 27 | -------------------------------------------------------------------------------- /demo/wechat/consts.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | 3 | 4 | #============================================================================= 5 | # 公众号类型 6 | #============================================================================= 7 | # 订阅号 8 | WECHAT_TYPE_SUB = 0 9 | # 升级后的订阅号 10 | WECHAT_TYPE_SUB2 = 1 11 | # 服务号 12 | WECHAT_TYPE_SERVICE = 2 13 | 14 | 15 | #============================================================================= 16 | # 开放平台授权方认证类型 17 | #============================================================================= 18 | VERIFY_TYPE_NONE = -1 19 | VERIFY_TYPE_WECHAT = 0 20 | VERIFY_TYPE_WEIBO = 1 21 | VERIFY_TYPE_TENCENT_WEIBO = 2 22 | # 已资质认证通过但还未通过名称认证 23 | VERIFY_TYPE_BASE_VERIFY = 3 24 | # 已资质认证通过、还未通过名称认证,但通过了新浪微博认证 25 | VERIFY_TYPE_3_1 = 4 26 | # 已资质认证通过、还未通过名称认证,但通过了腾讯微博认证 27 | VERIFY_TYPE_3_2 = 5 28 | -------------------------------------------------------------------------------- /demo/wechat/managers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from logging import getLogger 4 | from django.db import models 5 | from wechat import consts 6 | 7 | class WechatManager(models.Manager): 8 | """ 9 | 创建公众号时生成 token 10 | """ 11 | def create(self, **kwargs): 12 | obj = self.model(**kwargs) 13 | self._for_write = True 14 | obj.save(force_insert=True, using=self.db) 15 | return obj 16 | 17 | def create_from_api_result(self, kwargs): 18 | """ 19 | 用 API 结果创建对象 20 | """ 21 | info = {} 22 | info['appid'] = kwargs['authorization_info']['authorizer_appid'] 23 | info['alias'] = kwargs['authorizer_info']['alias'] 24 | info['user_name'] = kwargs['authorizer_info']['user_name'] 25 | info['head_img'] = kwargs['authorizer_info']['head_img'] 26 | info['nick_name'] = kwargs['authorizer_info']['nick_name'] 27 | info['qrcode_url'] = kwargs['authorizer_info']['qrcode_url'] 28 | info['service_type'] = kwargs['authorizer_info']['service_type_info']['id'] # flake8 noqa 29 | info['verify_type'] = kwargs['authorizer_info']['verify_type_info']['id'] # flake8 noqa 30 | info['authorized'] = True 31 | return self.create(**info) 32 | -------------------------------------------------------------------------------- /demo/wechat/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechatpy/django-wechat-example/ab080029e990d4e23558b409673175d42145d51d/demo/wechat/migrations/__init__.py -------------------------------------------------------------------------------- /demo/wechat/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | import os 4 | import logging 5 | from django.db import models 6 | from wechat.utils import get_component 7 | from wechat import consts 8 | from wechat import choices 9 | from wechat import managers 10 | 11 | 12 | model_logger = logging.getLogger('django.db.wechat') 13 | 14 | 15 | class Wechat(models.Model): 16 | """ 17 | 公众号 18 | """ 19 | appid = models.CharField('公众号 ID', max_length=20, default='') 20 | alias = models.CharField('公众号名称', max_length=20, null=True, blank=True) 21 | service_type = models.IntegerField( 22 | '公众号类型', choices=choices.WECHAT_TYPE_CHOICES, 23 | default=consts.WECHAT_TYPE_SUB 24 | ) 25 | nick_name = models.CharField('昵称', max_length=32, null=True, blank=True) 26 | head_img = models.URLField('头像', max_length=256, null=True, blank=True) 27 | user_name = models.CharField('内部名称', max_length=32) 28 | qrcode_url = models.URLField( 29 | '二维码URL', max_length=256, null=True, blank=True) 30 | authorized = models.BooleanField('授权') 31 | verify_type = models.PositiveIntegerField( 32 | '认证类型', choices=choices.VERIFY_TYPE_CHOICES) 33 | funcscope_categories = models.CommaSeparatedIntegerField( 34 | '权限集', max_length=64) 35 | join_time = models.DateTimeField('授权时间', auto_now_add=True) 36 | 37 | class Meta: 38 | get_latest_by = 'join_time' 39 | verbose_name = '公众号' 40 | verbose_name_plural = '公众号' 41 | 42 | def __unicode__(self): 43 | return '公众号 {0}'.format(self.alias) 44 | 45 | def __str__(self): 46 | return self.__unicode__().encode('utf-8') 47 | 48 | def is_valid(self): 49 | return self.authorized 50 | 51 | @property 52 | def client(self): 53 | component = get_component() 54 | return component.get_client_by_appid(self.appid) 55 | -------------------------------------------------------------------------------- /demo/wechat/tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from __future__ import unicode_literals 3 | from django.conf import settings 4 | from celery.utils.log import get_task_logger 5 | from demo import app 6 | from wechat.models import Wechat 7 | from wechat.utils import get_component 8 | 9 | 10 | @app.task 11 | def process_wechat_query_auth_code_test(FromUserName, query_auth_code): 12 | """ 13 | 处理发布前微信的自动化测试query_auth_code 14 | """ 15 | logger = get_task_logger('process_wechat_query_auth_code_test') 16 | logger.info(FromUserName) 17 | logger.info(query_auth_code) 18 | component = get_component() 19 | client = component.get_client_by_authorization_code(query_auth_code) 20 | client.message.send_text(FromUserName, query_auth_code+'_from_api') 21 | 22 | 23 | @app.task(bind=True) 24 | def refresh_all_wechat_token(self): 25 | """ 26 | 定时1小时,刷新所有已授权公众号 27 | """ 28 | logger = get_task_logger('refresh_all_wechat_token') 29 | for wechat in Wechat.objects.exclude(appid=settings.TEST_APPID).all(): 30 | if not wechat.authorized: 31 | logger.error('公众号{0}失去授权'.format(wechat.appid)) 32 | continue 33 | refresh_wechat_token.delay(wechat.appid) 34 | 35 | 36 | @app.task(bind=True) 37 | def refresh_wechat_token(self, appid): 38 | """ 39 | 刷新已授权公众号 40 | """ 41 | logger = get_task_logger('refresh_wechat_token') 42 | wechat = Wechat.objects.get(appid=appid) 43 | if not wechat.authorized: 44 | logger.error('公众号{0}失去授权'.format(wechat.appid)) 45 | return None 46 | try: 47 | result = wechat.client.fetch_access_token() 48 | logger.info(result) 49 | except Exception as e: 50 | logger.error(u'刷新已授权公众号{0}失败:{1}'.format(appid, str(e))) 51 | -------------------------------------------------------------------------------- /demo/wechat/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /demo/wechat/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from django.conf.urls import url 4 | from wechat import views 5 | 6 | urlpatterns = [ 7 | # 处理发给公众号的消息和事件 8 | url( 9 | r'^server/(?P[0-9a-z_]+)/?$', 10 | views.ProcessServerEventView.as_view(), 11 | name='wechat-server_messages' 12 | ), 13 | # 获取授权链接 14 | url( 15 | r'^auth/?$', 16 | views.WechatAuthPageView.as_view(), 17 | name='wechat-auth' 18 | ), 19 | # 公众号授权成功后由微信服务器调用 20 | url( 21 | r'^authorized/?$', 22 | views.WechatAuthSuccessPageView.as_view(), 23 | name='wechat-authorized' 24 | ), 25 | # 授权事件接收URL 26 | url( 27 | r'^callback/?$', 28 | views.AuthEventProcessView.as_view(), 29 | name='wechat-component-verify-ticket' 30 | ) 31 | ] 32 | -------------------------------------------------------------------------------- /demo/wechat/utils.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | from __future__ import unicode_literals 3 | from wechatpy import WeChatComponent 4 | from django.conf import settings 5 | from django.core.cache import caches 6 | 7 | 8 | def get_component(): 9 | """ 10 | 获取开放平台API对象 11 | """ 12 | component = WeChatComponent( 13 | settings.COMPONENT_APP_ID, 14 | settings.COMPONENT_APP_SECRET, 15 | settings.COMPONENT_APP_TOKEN, 16 | settings.COMPONENT_ENCODINGAESKEY, 17 | session=caches['wechat'] 18 | ) 19 | return component 20 | -------------------------------------------------------------------------------- /demo/wechat/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from datetime import datetime 4 | import hashlib 5 | import json 6 | import xmltodict 7 | from logging import getLogger 8 | from django.core.cache import caches 9 | from django.conf import settings 10 | from django.http import HttpResponse 11 | from wechatpy.replies import TextReply 12 | from wechatpy.utils import to_text 13 | from rest_framework.views import APIView 14 | from rest_framework import generics 15 | from rest_framework.response import Response 16 | from wechat.utils import get_component 17 | from wechat.models import Wechat 18 | from wechat import consts 19 | from wechat import caches as wechat_caches 20 | from wechat.tasks import process_wechat_query_auth_code_test 21 | 22 | 23 | common_logger = getLogger('django.request.common') 24 | 25 | 26 | class WechatDetailView(APIView): 27 | model = Wechat 28 | queryset = Wechat.objects.all() 29 | lookup_field = 'alias' 30 | 31 | def get_serializer_class(self): 32 | """ 33 | 匿名用户显示简化的信息 34 | """ 35 | if self.request.user.is_anonymous(): 36 | return wechat_serializers.WechatLiteSerializer 37 | else: 38 | return self.serializer_class 39 | 40 | def retrieve(self, request, *args, **kwargs): 41 | instance = self.get_object() 42 | serializer = self.get_serializer(instance) 43 | return Response(serializer.data) 44 | 45 | 46 | class AuthEventProcessView(APIView): 47 | """ 48 | 处理授权事件 49 | """ 50 | permission_classes = () 51 | 52 | def post(self, request, *args, **kwargs): 53 | """ 54 | 处理微信服务器提交的数据 55 | """ 56 | logger = getLogger('django.request.AuthEventProcessView') 57 | message = self.preprocess_message(request) 58 | logger.info('收到事件:{0}'.format(message)) 59 | component = get_component() 60 | # 推送component_verify_ticket协议 61 | if message['InfoType'].lower() == 'component_verify_ticket': 62 | component.cache_component_verify_ticket( 63 | request.body, 64 | request.query_params['msg_signature'], 65 | request.query_params['timestamp'], 66 | request.query_params['nonce'] 67 | ) 68 | logger.info('成功获取component_verify_ticket') 69 | return HttpResponse('success') 70 | # 取消授权通知 71 | elif message['InfoType'].lower() == 'unauthorized': 72 | authorizer_appid = message['AuthorizerAppid'] 73 | try: 74 | wechat = Wechat.objects.get(appid=authorizer_appid) 75 | except Wechat.DoesNotExist: 76 | return HttpResponse('success') 77 | wechat.authorized = False 78 | wechat.save() 79 | return HttpResponse('success') 80 | else: 81 | pass 82 | 83 | def preprocess_message(self, request): 84 | ''' 85 | 将消息转换成字典 86 | ''' 87 | component = get_component() 88 | content = component.crypto.decrypt_message( 89 | request.body, 90 | request.query_params['msg_signature'], 91 | int(request.query_params['timestamp']), 92 | int(request.query_params['nonce']) 93 | ) 94 | message = xmltodict.parse(to_text(content))['xml'] 95 | cc = json.loads(json.dumps(message)) 96 | cc['CreateTime'] = int(cc['CreateTime']) 97 | cc['CreateTime'] = datetime.fromtimestamp(cc['CreateTime']) 98 | if 'MsgId' in cc: 99 | cc['MsgId'] = int(cc['MsgId']) 100 | return cc 101 | 102 | 103 | class ProcessServerEventView(APIView): 104 | """ 105 | 公众号消息与事件接收 106 | """ 107 | permission_classes = () 108 | 109 | def post(self, request, *args, **kwargs): 110 | logger = getLogger('django.request.ProcessServerEventView') 111 | appid = kwargs.get('appid', '') 112 | wechat = Wechat.objects.get(appid=appid) 113 | message = self.preprocess_message(request) 114 | # 发布应用时,被开放平台调用 115 | if appid == settings.TEST_APPID: 116 | return self.test(request, message, wechat) 117 | # 发布以后 118 | else: 119 | if message.get('MsgType').lower() == 'event': 120 | return self.process_event(message, wechat) 121 | elif message.get('MsgType').lower() in consts.MESSAGE_TYPES: 122 | # 保存消息到数据库 123 | message_obj = self.save_message(message, wechat) 124 | # 默认的消息回应 125 | reply_content = '欢迎!请您稍等,马上给您安排服务人员。' 126 | return self.reply_message(message_obj, reply_content) 127 | else: 128 | return HttpResponse('') 129 | 130 | def test(self, request, message, wechat): 131 | """ 132 | 发布中测试 133 | """ 134 | logger = getLogger('django.request.test_ProcessServerEventView') 135 | logger.info(message) 136 | if message.get('MsgType').lower() == 'event': 137 | reply = TextReply() 138 | reply.target = message['FromUserName'] 139 | reply.source = message['ToUserName'] 140 | reply.content = message['Event'] + 'from_callback' 141 | xml_str = reply.render() 142 | headers = {'CONTENT_TYPE': request.META['CONTENT_TYPE']} 143 | return Response(xml_str, headers=headers) 144 | elif message.get('MsgType').lower() in consts.MESSAGE_TYPES: 145 | if message.get('Content') == 'TESTCOMPONENT_MSG_TYPE_TEXT': 146 | reply = TextReply() 147 | reply.target = message['FromUserName'] 148 | reply.source = message['ToUserName'] 149 | reply.content = 'TESTCOMPONENT_MSG_TYPE_TEXT_callback' 150 | xml_str = reply.render() 151 | headers = {'CONTENT_TYPE': request.META['CONTENT_TYPE']} 152 | return Response(xml_str, headers=headers) 153 | elif message.get('Content').startswith('QUERY_AUTH_CODE'): 154 | from datetime import timedelta 155 | now = datetime.utcnow() + timedelta(seconds=2) 156 | query_auth_code = message.get('Content').split(':')[1] 157 | process_wechat_query_auth_code_test.apply_async( 158 | (message['FromUserName'], query_auth_code), eta=now) 159 | return Response('') 160 | 161 | 162 | def reply_message(self, message, content): 163 | """ 164 | 回复公众号消息 165 | """ 166 | reply = TextReply() 167 | reply.target = message.FromUserName 168 | reply.source = message.ToUserName 169 | reply.content = content 170 | xml_str = reply.render() 171 | headers = {'CONTENT_TYPE': self.request.META['CONTENT_TYPE']} 172 | return Response(xml_str, headers=headers) 173 | 174 | def preprocess_message(self, request): 175 | component = get_component() 176 | content = component.crypto.decrypt_message( 177 | request.body, 178 | request.query_params['msg_signature'], 179 | int(request.query_params['timestamp']), 180 | int(request.query_params['nonce']) 181 | ) 182 | message = xmltodict.parse(to_text(content))['xml'] 183 | cc = json.loads(json.dumps(message)) 184 | cc['CreateTime'] = int(cc['CreateTime']) 185 | cc['CreateTime'] = datetime.fromtimestamp(cc['CreateTime']) 186 | if 'MsgId' in cc: 187 | cc['MsgId'] = int(cc['MsgId']) 188 | return cc 189 | 190 | 191 | class WechatAuthPageView(APIView): 192 | """ 193 | 生成授权页面链接 194 | """ 195 | 196 | def get(self, request, *args, **kwargs): 197 | logger = getLogger('django.request.WechatAuthPageView') 198 | component = get_component() 199 | result = component.create_preauthcode() 200 | auth_url = settings.AUTH_URL.format( 201 | component_appid=settings.COMPONENT_APP_ID, 202 | pre_auth_code=result['pre_auth_code'], 203 | redirect_uri=settings.AUTH_REDIRECT_URI 204 | ) 205 | return Response({'auth_url': auth_url}) 206 | 207 | 208 | class WechatAuthSuccessPageView(APIView): 209 | """ 210 | 授权成功时回调视图 211 | """ 212 | 213 | def post(self, request, *args, **kwargs): 214 | logger = getLogger('django.request.WechatAuthSuccessPageView') 215 | auth_code = request.data['auth_code'] 216 | component = get_component() 217 | # 拿到授权公众号的信息 218 | result = component.query_auth(auth_code) 219 | authorizer_appid = result['authorization_info']['authorizer_appid'] 220 | expires_in = result['authorization_info']['expires_in'] 221 | access_token_key = wechat_caches.CACHE_WECHAT_ACCESS_CODE.format(authorizer_appid) 222 | refresh_token_key = wechat_caches.CACHE_WECHAT_REFRESH_CODE.format(authorizer_appid) 223 | app_info = component.get_authorizer_info(authorizer_appid) 224 | if not Wechat.objects.filter(appid=authorizer_appid).exists(): 225 | wechat = Wechat.objects.create_from_api_result(app_info) 226 | # wechat.owner = request.user 227 | # wechat.save() 228 | else: 229 | wechat = Wechat.objects.get(appid=authorizer_appid) 230 | if not wechat.authorized: 231 | wechat.authorized = True 232 | wechat.save() 233 | caches['wechat'].set( 234 | access_token_key, 235 | result['authorization_info']['authorizer_access_token'], 236 | expires_in 237 | ) 238 | caches['wechat'].set( 239 | refresh_token_key, 240 | result['authorization_info']['authorizer_refresh_token'], 241 | expires_in 242 | ) 243 | return HttpResponse('success') 244 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.8.4 2 | wechatpy==1.2.5 3 | celery==3.1.23 4 | hiredis==0.2.0 5 | redis==2.10.5 6 | djangorestframework==3.3.2 7 | django-redis-cache==1.6.5 8 | --------------------------------------------------------------------------------