Python+Tornado開發微信公眾號

sufaith發表於2019-03-28

本教程針對的是已掌握Python語言基本用法並且掌握其任一Web框架的使用者。

本教程使用的Python版本為3.5.0, Web框架為Tornado, IDE開發工具為PyCharm,整個開發過程是在Windows環境下測試開發,最終上線部署至centos伺服器。

備註:(1) 如果您是python小白,建議參考 Python入門教程

(2) 對tornado框架還不熟悉的同學,建議參考 Tornado中文文件

本教程整體框架如下:

1. Python開發環境和專案的初始化搭建;

2. 微信公眾號註冊及開發模式校驗配置;

3. 接收關注/取關事件推送和自動回覆;

4. IOLoop定時獲取access_token和jsapi_ticket;

5. 自定義選單及點選選單時獲取openid;

6. 選單中網頁的開發, JS-SDK的使用;

7. 完成測試,釋出上線,部署至centos伺服器。

思維導圖如下:

Python+Tornado開發微信公眾號

整體專案結構如下:


Python+Tornado開發微信公眾號

下面我們正式進入詳細的開發流程

一. Python開發環境和專案的初始化搭建


1. 安裝python及pip,並配置環境變數,安裝tornado框架
Python及pip安裝參考教程windows下面安裝Python和pip終極教程
(1) 下載Python包並安裝 點此下載
(2) 將python配置到系統環境變數
(3) 下載pip包並安裝 點此下載
(4) 將pip配置到系統環境變數
(5) 使用pip安裝tornado框架 指令為:


pip install tornado
複製程式碼

2. 選擇一款開發Python的IDE
本教程使用的是PyCharm點選下載
附帶: PyCharm 2016.2.3專業版註冊碼

3. 選擇一個程式碼託管平臺
本教程使用的是開源中國Git@osc程式碼託管平臺 碼雲 - 開源中國程式碼託管平臺,請自行註冊,並配置賬戶下的SSH金鑰,關於Git的使用,請參考教程 Git教程 - 廖雪峰的官方網站
4. 建立Web專案
使用Tornado搭建專案入口,埠號為8000,專案搭建至完成微信校驗所需的基本程式碼如下:
專案整體目錄

Python+Tornado開發微信公眾號


  • 為了方便除錯,編寫了日誌管理檔案 logger_helper.py

備註: 為防止日誌輸出報錯, 請各位同學注意修改日誌輸出目錄為自己定義的檔案目錄


import logging
from logging import Logger
from logging.handlers import TimedRotatingFileHandler

'''日誌管理類'''


def init_logger(logger_name):
    if logger_name not in Logger.manager.loggerDict:
        logger1 = logging.getLogger(logger_name)
        logger1.setLevel(logging.INFO)  # 設定最低階別
        # logger1.setLevel(logging.DEBUG)  # 設定最低階別
        df = '%Y-%m-%d %H:%M:%S'
        format_str = '[%(asctime)s]: %(name)s %(levelname)s %(lineno)s %(message)s'
        formatter = logging.Formatter(format_str, df)
        # handler all
        try:
            handler1 = TimedRotatingFileHandler('/usr/web_wx/log/all.log', when='D', interval=1, backupCount=7)
        except Exception:
            handler1 = TimedRotatingFileHandler('F:\program\web_wx\core\log\/all.log', when='D', interval=1, backupCount=7)
        handler1.setFormatter(formatter)
        handler1.setLevel(logging.DEBUG)
        logger1.addHandler(handler1)
        # handler error
        try:
            handler2 = TimedRotatingFileHandler('/usr/web_wx/log/error.log', when='D', interval=1, backupCount=7)
        except Exception:
            handler2 = TimedRotatingFileHandler('F:\program\web_wx\core\log\error.log', when='D', interval=1, backupCount=7)
        handler2.setFormatter(formatter)
        handler2.setLevel(logging.ERROR)
        logger1.addHandler(handler2)

        # console
        console = logging.StreamHandler()
        console.setLevel(logging.DEBUG)
        # 設定日誌列印格式
        console.setFormatter(formatter)
        # 將定義好的console日誌handler新增到root logger
        logger1.addHandler(console)

    logger1 = logging.getLogger(logger_name)
    return logger1


logger = init_logger('runtime-log')

if __name__ == '__main__':
    logger.debug('test-debug')
    logger.info('test-info')
    logger.warn('test-warn')
    logger.error('test-error')
複製程式碼
  • 微信服務端校驗的介面檔案 wxauthorize.py


from core.logger_helper import logger
import hashlib
import tornado.web


class WxSignatureHandler(tornado.web.RequestHandler):
    """
    微信伺服器簽名驗證, 訊息回覆

    check_signature: 校驗signature是否正確
    """

    def data_received(self, chunk):
        pass

    def get(self):
        try:
            signature = self.get_argument('signature')
            timestamp = self.get_argument('timestamp')
            nonce = self.get_argument('nonce')
            echostr = self.get_argument('echostr')
            logger.debug('微信sign校驗,signature='+signature+',&timestamp='+timestamp+'&nonce='+nonce+'&echostr='+echostr)
            result = self.check_signature(signature, timestamp, nonce)
            if result:
                logger.debug('微信sign校驗,返回echostr='+echostr)
                self.write(echostr)
            else:
                logger.error('微信sign校驗,---校驗失敗')
        except Exception as e:
            logger.error('微信sign校驗,---Exception' + str(e))

    def check_signature(self, signature, timestamp, nonce):
        """校驗token是否正確"""
        token = 'test12345'
        L = [timestamp, nonce, token]
        L.sort()
        s = L[0] + L[1] + L[2]
        sha1 = hashlib.sha1(s.encode('utf-8')).hexdigest()
        logger.debug('sha1=' + sha1 + '&signature=' + signature)
        return sha1 == signature
複製程式碼
  • 配置Tornado的url路由規則 url.py


from core.server.wxauthorize import WxSignatureHandler
import tornado.web


'''web解析規則'''

urlpatterns = [
    (r'/wxsignature', WxSignatureHandler),  # 微信簽名
   ]
複製程式碼
  • 基本配置檔案 run.py


import os
import tornado.httpserver
import tornado.ioloop
import tornado.web
from tornado.options import define, options
from core.url import urlpatterns


define('port', default=8000, help='run on the given port', type=int)


class Application(tornado.web.Application):
    def __init__(self):
        settings = dict(
            template_path=os.path.join(os.path.dirname(__file__), "core/template"),
            static_path=os.path.join(os.path.dirname(__file__), "core/static"),
            debug=True,
            login_url='/login',
            cookie_secret='MuG7xxacQdGPR7Svny1OfY6AymHPb0H/t02+I8rIHHE=',
        )
        super(Application, self).__init__(urlpatterns, **settings)


def main():
    tornado.options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(Application())
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()

if __name__ == '__main__':
    main()
複製程式碼


(5) 同步專案檔案至Git託管平臺
專案入口檔案及微信校驗檔案已編寫好,使用Git同步程式碼至託管平臺,接下來需要配置埠對映,使外網能訪問到我們的本地專案,便於完成微信服務端校驗.

5. 使用花生殼,配置本地測試所需埠對映
微信公眾號開發需要配置服務端URL, 驗證URL的有效性,這個URL必須以http://或https://開頭,分別支援80埠和443埠,我們目前測試階段都是在自己電腦上測試(本地測試),為了滿足不斷修改程式碼能夠即時生效, 因此需要一個外網ip埠對映到本地(內網穿透),我本人使用的是花生殼內網穿透服務,下面是花生殼的使用流程:
(1) 花生殼的賬戶註冊 花生殼軟體-內網也能用!內網動態域名,註冊成功後,會贈送一個免費域名,這個域名同時也配備了一個公網ip
(2) 進入到花生殼管理介面, 選擇內網穿透選單,進入到配置介面

Python+Tornado開發微信公眾號


(3) 選擇 右邊的"編輯"操作,彈出編輯對映皮膚,在"內網主機"一項,填上自己本地電腦的ip地址,埠填寫自己將要建立的web應用埠,我本地專案用的埠號為8000,此處填寫8000即可

Python+Tornado開發微信公眾號


二. 微信公眾號註冊及開發模式校驗配置


1. 微信公眾號註冊
官網連結mp.weixin.qq.com/,依次填寫資訊進行註冊
2.微信公眾開發模式校驗配置
(1)登入微信公眾號後, 進入基本配置,如下:

Python+Tornado開發微信公眾號
Python+Tornado開發微信公眾號

URL 填寫為: 花生殼的域名+我們專案中微信校驗的介面名:
XXXXXXX.imwork.net/wxsignature
token 填寫為我們專案中自定義的token: test12345
EncodingAESKey 點選"隨機生成"按鈕即可,訊息加密方式使用明文模式

填寫完畢後,先啟動我們的專案,執行python run.py指令後, 保證我們的伺服器是執行著的, 然後點選"提交",如果你是按照以上流程操作的話,會提示提交成功,否則校驗失敗,需要我們通過日誌檢查是哪一塊出了問題.
(2) 接下來,校驗成功後,點選啟用,即可啟用開發者模式

Python+Tornado開發微信公眾號
Python+Tornado開發微信公眾號
Python+Tornado開發微信公眾號


三.接收關注/取關事件推送和自動回覆


1. 接收關注/取關事件推送
在開發模式中,有新使用者關注我們的公眾號時,微信公眾平臺會使用http協議的Post方式推送資料至我們的後臺微信校驗的介面,在接收到訊息後,我們後臺傳送一條歡迎語給該使用者,關於微信公眾平臺推送訊息的具體內容和資料格式,詳見微信開發文件

  • wxauthorize.py


以下是在該檔案中增加的post方法,用來接收事件推送


def post(self):
        body = self.request.body
        logger.debug('微信訊息回覆中心】收到使用者訊息' + str(body.decode('utf-8')))
        data = ET.fromstring(body)
        ToUserName = data.find('ToUserName').text
        FromUserName = data.find('FromUserName').text
        MsgType = data.find('MsgType').text
        if MsgType == 'event':
            '''接收事件推送'''
            try:
                Event = data.find('Event').text
                if Event == 'subscribe':
                    # 關注事件
                    CreateTime = int(time.time())
                    reply_content = '歡迎關注我的公眾號~'
                    out = self.reply_text(FromUserName, ToUserName, CreateTime, reply_content)
                    self.write(out)
            except:
                pass

def reply_text(self, FromUserName, ToUserName, CreateTime, Content):
        """回覆文字訊息模板"""
        textTpl = """<xml> <ToUserName><![CDATA[%s]]></ToUserName> <FromUserName><![CDATA[%s]]></FromUserName> <CreateTime>%s</CreateTime> <MsgType><![CDATA[%s]]></MsgType> <Content><![CDATA[%s]]></Content></xml>"""
        out = textTpl % (FromUserName, ToUserName, CreateTime, 'text', Content)
        return out
複製程式碼

2. 自動回覆

(1) 同接收關注/取關事件推送訊息一樣,使用者給我們公眾號傳送訊息時,微信公眾平臺也會推送資料至我們的後臺微信校驗的介面,在接收到訊息後,我們取出自定義的關鍵字進行匹配,匹配到了就執行自動回覆
(2) 微信公眾平臺也提供了語音識別功能, 將使用者傳送的語音內容識別轉化為文字,傳送給我們後臺,在使用該功能時需要在介面許可權中開啟語音識別功能.

Python+Tornado開發微信公眾號
Python+Tornado開發微信公眾號
Python+Tornado開發微信公眾號


  • wxauthorize.py

以下是在該檔案中post方法中增加的一個判斷,用來匹配使用者文字訊息和語音訊息中的關鍵字


def post(self):
        body = self.request.body
        logger.debug('微信訊息回覆中心】收到使用者訊息' + str(body.decode('utf-8')))
        data = ET.fromstring(body)
        ToUserName = data.find('ToUserName').text
        FromUserName = data.find('FromUserName').text
        MsgType = data.find('MsgType').text
        if MsgType == 'text' or MsgType == 'voice':
            '''文字訊息 or 語音訊息'''
            try:
                MsgId = data.find("MsgId").text
                if MsgType == 'text':
                    Content = data.find('Content').text  # 文字訊息內容
                elif MsgType == 'voice':
                    Content = data.find('Recognition').text  # 語音識別結果,UTF8編碼
                if Content == u'你好':
                    reply_content = '您好,請問有什麼可以幫助您的嗎?'
                else:
                    # 查詢不到關鍵字,預設回覆
                    reply_content = "客服小兒智商不夠用啦~"
                if reply_content:
                    CreateTime = int(time.time())
                    out = self.reply_text(FromUserName, ToUserName, CreateTime, reply_content)
                    self.write(out)
            except:
                pass

        elif MsgType == 'event':
            '''接收事件推送'''
            try:
                Event = data.find('Event').text
                if Event == 'subscribe':
                    # 關注事件
                    CreateTime = int(time.time())
                    reply_content = self.sys_order_reply
                    out = self.reply_text(FromUserName, ToUserName, CreateTime, reply_content)
                    self.write(out)
            except:
                pass

    def reply_text(self, FromUserName, ToUserName, CreateTime, Content):
        """回覆文字訊息模板"""
        textTpl = """<xml> <ToUserName><![CDATA[%s]]></ToUserName> <FromUserName><![CDATA[%s]]></FromUserName> <CreateTime>%s</CreateTime> <MsgType><![CDATA[%s]]></MsgType> <Content><![CDATA[%s]]></Content></xml>"""
        out = textTpl % (FromUserName, ToUserName, CreateTime, 'text', Content)
        return out
複製程式碼

四. IOLoop定時獲取access_token和jsapi_ticket


1. access_token
access_token是公眾號的全域性唯一票據,公眾號呼叫各介面時都需使用access_token。開發者需要進行妥善儲存。access_token的儲存至少要保留512個字元空間。access_token的有效期目前為2個小時,需定時重新整理,重複獲取將導致上次獲取的access_token失效。以下是文件中的說明 詳見微信開發文件

Python+Tornado開發微信公眾號


2. jsapi_ticket
jsapi_ticket是公眾號用於呼叫微信JS介面的臨時票據。正常情況下,jsapi_ticket的有效期為7200秒,通過access_token來獲取。由於獲取jsapi_ticket的api呼叫次數非常有限,頻繁重新整理jsapi_ticket會導致api呼叫受限,影響自身業務,開發者必須在自己的服務全域性快取jsapi_ticket 。參考文件JS-SDK使用許可權簽名演算法

Python+Tornado開發微信公眾號

3. Redis資料庫
如果有對Redis不瞭解的同學,可參考Redis快速入門

  • basecache.py
import redis

"""快取伺服器"""
CACHE_SERVER = {
    'host': '127.0.0.1',
    'port': 6379,
    'database': 0,
    'password': '',
}

class BaseCache(object):
    """
    快取類父類

    redis_ctl:                          redis控制控制程式碼
    """

    _host = CACHE_SERVER.get('host', '')
    _port = CACHE_SERVER.get('port', '')
    _database = CACHE_SERVER.get('database', '')
    _password = CACHE_SERVER.get('password', '')

    @property
    def redis_ctl(self):
        """redis控制控制程式碼"""
        redis_ctl = redis.Redis(host=self._host, port=self._port, db=self._database, password=self._password)
        return redis_ctl
複製程式碼
  • tokencache.py
from core.cache.basecache import BaseCache
from core.logger_helper import logger


class TokenCache(BaseCache):
    """
    微信token快取

    set_cache               新增redis
    get_cache               獲取redis
    """
    _expire_access_token = 7200  # 微信access_token過期時間, 2小時
    _expire_js_token = 30 * 24 * 3600   # 微信js網頁授權過期時間, 30天
    KEY_ACCESS_TOKEN = 'access_token'  # 微信全域性唯一票據access_token
    KEY_JSAPI_TICKET = 'jsapi_ticket'  # JS_SDK許可權簽名的jsapi_ticket

    def set_access_cache(self, key, value):
        """新增微信access_token驗證相關redis"""
        res = self.redis_ctl.set(key, value)
        self.redis_ctl.expire(key, self._expire_access_token)
        logger.debug('【微信token快取】setCache>>>key[' + key + '],value[' + value + ']')
        return res

    def set_js_cache(self, key, value):
        """新增網頁授權相關redis"""
        res = self.redis_ctl.set(key, value)
        self.redis_ctl.expire(key, self._expire_js_token)
        logger.debug('【微信token快取】setCache>>>key[' + key + '],value[' + value + ']')
        return res

    def get_cache(self, key):
        """獲取redis"""
        try:
            v = (self.redis_ctl.get(key)).decode('utf-8')
            logger.debug(v)
            logger.debug('【微信token快取】getCache>>>key[' + key + '],value[' + v + ']')
            return v
        except Exception:
            return None
複製程式碼

4. 使用tornado的 Ioloop 實現定時獲取access_token和 jsapi_ticket,並將獲取到的access_token和 jsapi_ticket儲存在Redis資料庫中

  • wxconfig.py
class WxConfig(object):
    """
    微信開發--基礎配置

    """
    AppID = 'wxxxxxxxxxxxxxxxx'  # AppID(應用ID)
    AppSecret = '024a7fcxxxxxxxxxxxxxxxxxxxx'  # AppSecret(應用金鑰)

    '''獲取access_token'''
    config_get_access_token_url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s' % (AppID, AppSecret)
複製程式碼
  • wxshedule.py
from core.logger_helper import logger
import tornado.ioloop
import requests
import json
from core.server.wxconfig import WxConfig
from core.cache.tokencache import TokenCache


class WxShedule(object):
    """
    定時任務排程器

    excute                      執行定時器任務
    get_access_token            獲取微信全域性唯一票據access_token
    get_jsapi_ticket           獲取JS_SDK許可權簽名的jsapi_ticket
    """
    _token_cache = TokenCache()  # 微信token快取例項
    _expire_time_access_token = 7000 * 1000  # token過期時間

    def excute(self):
        """執行定時器任務"""
        logger.info('【獲取微信全域性唯一票據access_token】>>>執行定時器任務')
        tornado.ioloop.IOLoop.instance().call_later(0, self.get_access_token)
        tornado.ioloop.PeriodicCallback(self.get_access_token, self._expire_time_access_token).start()
        # tornado.ioloop.IOLoop.current().start()

    def get_access_token(self):
        """獲取微信全域性唯一票據access_token"""
        url = WxConfig.config_get_access_token_url
        r = requests.get(url)
        logger.info('【獲取微信全域性唯一票據access_token】Response[' + str(r.status_code) + ']')
        if r.status_code == 200:
            res = r.text
            logger.info('【獲取微信全域性唯一票據access_token】>>>' + res)
            d = json.loads(res)
            if 'access_token' in d.keys():
                access_token = d['access_token']
                # 新增至redis中
                self._token_cache.set_access_cache(self._token_cache.KEY_ACCESS_TOKEN, access_token)
                # 獲取JS_SDK許可權簽名的jsapi_ticket
                self.get_jsapi_ticket()
                return access_token
            elif 'errcode' in d.keys():
                errcode = d['errcode']
                logger.info(
                    '【獲取微信全域性唯一票據access_token-SDK】errcode[' + errcode + '] , will retry get_access_token() method after 10s')
                tornado.ioloop.IOLoop.instance().call_later(10, self.get_access_token)
        else:
            logger.error('【獲取微信全域性唯一票據access_token】request access_token error, will retry get_access_token() method after 10s')
            tornado.ioloop.IOLoop.instance().call_later(10, self.get_access_token)

    def get_jsapi_ticket(self):
        """獲取JS_SDK許可權簽名的jsapi_ticket"""
        access_token = self._token_cache.get_cache(self._token_cache.KEY_ACCESS_TOKEN)
        if access_token:
            url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi' % access_token
            r = requests.get(url)
            logger.info('【微信JS-SDK】獲取JS_SDK許可權簽名的jsapi_ticket的Response[' + str(r.status_code) + ']')
            if r.status_code == 200:
                res = r.text
                logger.info('【微信JS-SDK】獲取JS_SDK許可權簽名的jsapi_ticket>>>>' + res)
                d = json.loads(res)
                errcode = d['errcode']
                if errcode == 0:
                    jsapi_ticket = d['ticket']
                    # 新增至redis中
                    self._token_cache.set_access_cache(self._token_cache.KEY_JSAPI_TICKET, jsapi_ticket)
                else:
                    logger.info('【微信JS-SDK】獲取JS_SDK許可權簽名的jsapi_ticket>>>>errcode[' + errcode + ']')
                    logger.info('【微信JS-SDK】request jsapi_ticket error, will retry get_jsapi_ticket() method after 10s')
                    tornado.ioloop.IOLoop.instance().call_later(10, self.get_jsapi_ticket)
            else:
                logger.info('【微信JS-SDK】request jsapi_ticket error, will retry get_jsapi_ticket() method after 10s')
                tornado.ioloop.IOLoop.instance().call_later(10, self.get_jsapi_ticket)
        else:
            logger.error('【微信JS-SDK】獲取JS_SDK許可權簽名的jsapi_ticket時,access_token獲取失敗, will retry get_access_token() method after 10s')
            tornado.ioloop.IOLoop.instance().call_later(10, self.get_access_token)

if __name__ == '__main__':

    wx_shedule = WxShedule()
    """執行定時器"""
    wx_shedule.excute()
複製程式碼
  • run.py 將定時器的啟動放在主程式入口處,保證每次啟動伺服器時,重新啟動定時器
import os
import tornado.httpserver
import tornado.ioloop
import tornado.web
from tornado.options import define, options
from core.url import urlpatterns
from core.server.wxshedule import WxShedule

define('port', default=8000, help='run on the given port', type=int)


class Application(tornado.web.Application):
    def __init__(self):
        settings = dict(
            template_path=os.path.join(os.path.dirname(__file__), "core/template"),
            static_path=os.path.join(os.path.dirname(__file__), "core/static"),
            debug=True,
            login_url='/login',
            cookie_secret='MuG7xxacQdGPR7Svny1OfY6AymHPb0H/t02+I8rIHHE=',
        )
        super(Application, self).__init__(urlpatterns, **settings)


def main():
    tornado.options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(Application())
    http_server.listen(options.port)
    # 執行定時任務
    wx_shedule = WxShedule()
    wx_shedule.excute()
    tornado.ioloop.IOLoop.current().start()

if __name__ == '__main__':
    main()
複製程式碼

五. 自定義選單及點選選單時獲取openid

1. 編寫選單對應的html頁面

  • 先在template模板資料夾下製作一個html頁面,用於點選自定義選單時跳轉到的網頁.
Python+Tornado開發微信公眾號
  • 編寫一個頁面處理類,用於接收tornado.web.RequestHandler請求
Python+Tornado開發微信公眾號
  • 給PageHandler新增url規則


Python+Tornado開發微信公眾號

2.建立一個選單,並給選單新增獲取授權code的URL

以下是微信公眾平臺官方文件給出的具體流程,詳見 網頁授權獲取使用者基本資訊

Python+Tornado開發微信公眾號

我們希望在使用者點選自定義選單時,需要先獲取使用者的openid,以便從我們自己的後臺中通過該openid獲取這個使用者更多的資訊,比如它對應的我們後臺中的uid等, 如果我們後臺中沒有這個使用者,則需要執行繫結等操作.

因此我們需要給這個自定義選單按鈕新增一個對應的URL,點選這個選單,跳轉到這個URL,這個URL會觸發獲取code操作,獲取到code後,通過獲取授權的access_token介面,獲取openid及access_token

(1) 給選單新增url,及state對映關係

  • state為自定義字串,可以用來標識是使用者點選了哪一個選單,放在一個dict字典中,當前我們製作的第一個選單就對應 /page/index 對映
Python+Tornado開發微信公眾號
Python+Tornado開發微信公眾號
Python+Tornado開發微信公眾號

(2) 點選選單時,觸發獲取code介面,微信公眾平臺攜帶code和state請求訪問我們後臺的 /wx/wxauthor 介面,根據state欄位獲取 /page/index 對映,用來做重定向用.通過code換取網頁授權access_token及openid,拿到openid後我們就可以重定向跳轉到 /page/index對映對應的頁面 index.html

Python+Tornado開發微信公眾號

附:涉及到的主要程式程式碼如下:

  • wxconfig.py
class WxConfig(object):
    """
    微信開發--基礎配置

    """
    AppID = 'wxxxxxxxxxxxxxxxx'  # AppID(應用ID)
    AppSecret = '024a7fcxxxxxxxxxxxxxxxxxxxx'  # AppSecret(應用金鑰)

    """微信網頁開發域名"""
    AppHost = 'http://xxxxxx.com'

    '''獲取access_token'''
    config_get_access_token_url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s' % (AppID, AppSecret)

    '''自定義選單建立介面'''
    menu_create_url = 'https://api.weixin.qq.com/cgi-bin/menu/create?access_token='

    '''自定義選單查詢介面'''
    menu_get_url = 'https://api.weixin.qq.com/cgi-bin/menu/get?access_token='

    '''自定義選單刪除介面'''
    menu_delete_url = 'https://api.weixin.qq.com/cgi-bin/menu/delete?access_token='

    '''微信公眾號選單對映資料'''
    """重定向後會帶上state引數,開發者可以填寫a-zA-Z0-9的引數值,最多128位元組"""
    wx_menu_state_map = {
        'menuIndex0': '%s/page/index' % AppHost,  # 測試選單1
    }
複製程式碼
  • wxauthorize.py 中的 WxAuthorServer類
class WxAuthorServer(object):
    """
    微信網頁授權server

    get_code_url                            獲取code的url
    get_auth_access_token                   通過code換取網頁授權access_token
    refresh_token                           重新整理access_token
    check_auth                              檢驗授權憑證(access_token)是否有效
    get_userinfo                            拉取使用者資訊
    """

    """授權後重定向的回撥連結地址,請使用urlencode對連結進行處理"""
    REDIRECT_URI = '%s/wx/wxauthor' % WxConfig.AppHost

    """
    應用授權作用域
    snsapi_base (不彈出授權頁面,直接跳轉,只能獲取使用者openid)
    snsapi_userinfo (彈出授權頁面,可通過openid拿到暱稱、性別、所在地。並且,即使在未關注的情況下,只要使用者授權,也能獲取其資訊)
    """
    SCOPE = 'snsapi_base'
    # SCOPE = 'snsapi_userinfo'

    """通過code換取網頁授權access_token"""
    get_access_token_url = 'https://api.weixin.qq.com/sns/oauth2/access_token?'

    """拉取使用者資訊"""
    get_userinfo_url = 'https://api.weixin.qq.com/sns/userinfo?'


    def get_code_url(self, state):
        """獲取code的url"""
        dict = {'redirect_uri': self.REDIRECT_URI}
        redirect_uri = urllib.parse.urlencode(dict)
        author_get_code_url = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&%s&response_type=code&scope=%s&state=%s#wechat_redirect' % (WxConfig.AppID, redirect_uri, self.SCOPE, state)
        logger.debug('【微信網頁授權】獲取網頁授權的code的url>>>>' + author_get_code_url)
        return author_get_code_url

    def get_auth_access_token(self, code):
        """通過code換取網頁授權access_token"""
        url = self.get_access_token_url + 'appid=%s&secret=%s&code=%s&grant_type=authorization_code' % (WxConfig.AppID, WxConfig.AppSecret, code)
        r = requests.get(url)
        logger.debug('【微信網頁授權】通過code換取網頁授權access_token的Response[' + str(r.status_code) + ']')
        if r.status_code == 200:
            res = r.text
            logger.debug('【微信網頁授權】通過code換取網頁授權access_token>>>>' + res)
            json_res = json.loads(res)
            if 'access_token' in json_res.keys():
                return json_res
            elif 'errcode' in json_res.keys():
                errcode = json_res['errcode']
複製程式碼
  • wxmenu.py
import requests
import json
from core.server.wxconfig import WxConfig
from core.cache.tokencache import TokenCache
from core.logger_helper import logger
from core.server.wxauthorize import WxAuthorServer


class WxMenuServer(object):
    """
    微信自定義選單

    create_menu                     自定義選單建立介面
    get_menu                        自定義選單查詢介面
    delete_menu                     自定義選單刪除介面
    create_menu_data                建立選單資料
    """

    _token_cache = TokenCache()  # 微信token快取
    _wx_author_server = WxAuthorServer()  # 微信網頁授權server

    def create_menu(self):
        """自定義選單建立介面"""
        access_token = self._token_cache.get_cache(self._token_cache.KEY_ACCESS_TOKEN)
        if access_token:
            url = WxConfig.menu_create_url + access_token
            data = self.create_menu_data()
            r = requests.post(url, data.encode('utf-8'))
            logger.debug('【微信自定義選單】自定義選單建立介面Response[' + str(r.status_code) + ']')
            if r.status_code == 200:
                res = r.text
                logger.debug('【微信自定義選單】自定義選單建立介面' + res)
                json_res = json.loads(res)
                if 'errcode' in json_res.keys():
                    errcode = json_res['errcode']
                    return errcode
        else:
            logger.error('【微信自定義選單】自定義選單建立介面獲取不到access_token')

    def get_menu(self):
        """自定義選單查詢介面"""
        access_token = self._token_cache.get_cache(self._token_cache.KEY_ACCESS_TOKEN)
        if access_token:
            url = WxConfig.menu_get_url + access_token
            r = requests.get(url)
            logger.debug('【微信自定義選單】自定義選單查詢介面Response[' + str(r.status_code) + ']')
            if r.status_code == 200:
                res = r.text
                logger.debug('【微信自定義選單】自定義選單查詢介面' + res)
                json_res = json.loads(res)
                if 'errcode' in json_res.keys():
                    errcode = json_res['errcode']
                    return errcode
        else:
            logger.error('【微信自定義選單】自定義選單查詢介面獲取不到access_token')

    def delete_menu(self):
        """自定義選單刪除介面"""
        access_token = self._token_cache.get_cache(self._token_cache.KEY_ACCESS_TOKEN)
        if access_token:
            url = WxConfig.menu_delete_url + access_token
            r = requests.get(url)
            logger.debug('【微信自定義選單】自定義選單刪除介面Response[' + str(r.status_code) + ']')
            if r.status_code == 200:
                res = r.text
                logger.debug('【微信自定義選單】自定義選單刪除介面' + res)
                json_res = json.loads(res)
                if 'errcode' in json_res.keys():
                    errcode = json_res['errcode']
                    return errcode
        else:
            logger.error('【微信自定義選單】自定義選單刪除介面獲取不到access_token')

    def create_menu_data(self):
        """建立選單資料"""
        menu_data = {'button': []}  # 大選單
        menu_Index0 = {
            'type': 'view',
            'name': '測試選單1',
            'url': self._wx_author_server.get_code_url('menuIndex0')
        }
        menu_data['button'].append(menu_Index0)
        MENU_DATA = json.dumps(menu_data, ensure_ascii=False)
        logger.debug('【微信自定義選單】建立選單資料MENU_DATA[' + str(MENU_DATA) + ']')
        return MENU_DATA

if __name__ == '__main__':
    wx_menu_server = WxMenuServer()
    '''建立選單資料'''
    # wx_menu_server.create_menu_data()
    # '''自定義選單建立介面'''
    wx_menu_server.create_menu()
    '''自定義選單查詢介面'''
    # wx_menu_server.get_menu()
    '''自定義選單刪除介面'''
    # wx_menu_server.delete_menu()
複製程式碼

wx_handler.py


import tornado.web
from core.logger_helper import logger
from core.server.wxauthorize import WxConfig
from core.server.wxauthorize import WxAuthorServer
from core.cache.tokencache import TokenCache


class WxHandler(tornado.web.RequestHandler):
    """
    微信handler處理類
    """

    '''微信配置檔案'''
    wx_config = WxConfig()
    '''微信網頁授權server'''
    wx_author_server = WxAuthorServer()
    '''redis服務'''
    wx_token_cache = TokenCache()

    def post(self, flag):

        if flag == 'wxauthor':
            '''微信網頁授權'''
            code = self.get_argument('code')
            state = self.get_argument('state')
            # 獲取重定向的url
            redirect_url = self.wx_config.wx_menu_state_map[state]
            logger.debug('【微信網頁授權】將要重定向的地址為:redirct_url[' + redirect_url + ']')
            logger.debug('【微信網頁授權】使用者同意授權,獲取code>>>>code[' + code + ']state[' + state + ']')
            if code:
                # 通過code換取網頁授權access_token
                data = self.wx_author_server.get_auth_access_token(code)
                openid = data['openid']
                logger.debug('【微信網頁授權】openid>>>>openid[' + openid + ']')
                if openid:
                    # 跳到自己的業務介面
                    self.redirect(redirect_url)
                else:
                    # 獲取不到openid
                    logger.debug('獲取不到openid')
複製程式碼

六. 選單中網頁的開發, JS-SDK的使用

在完成自定義選單後,我們就可以開發自己的網頁了,在網頁中涉及到獲取使用者地理位置,微信支付等,都需要使用微信公眾平臺提供的JS-SDK,詳見 微信JS-SDK說明文件

1. 獲取JS-SDK許可權簽名

  • wxsign.py


import time
import random
import string
import hashlib
from core.server.weixin.wxconfig import WxConfig
from core.server.cache.tokencache import TokenCache
from core.logger_helper import logger


class WxSign:
    """\
    微信開發--獲取JS-SDK許可權簽名

    __create_nonce_str              隨機字串
    __create_timestamp              時間戳
    sign                            生成JS-SDK使用許可權簽名
    """

    def __init__(self, jsapi_ticket, url):
        self.ret = {
            'nonceStr': self.__create_nonce_str(),
            'jsapi_ticket': jsapi_ticket,
            'timestamp': self.__create_timestamp(),
            'url': url
        }

    def __create_nonce_str(self):
        return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(15))

    def __create_timestamp(self):
        return int(time.time())

    def sign(self):
        string = '&'.join(['%s=%s' % (key.lower(), self.ret[key]) for key in sorted(self.ret)])
        self.ret['signature'] = hashlib.sha1(string.encode('utf-8')).hexdigest()
        logger.debug('【微信JS-SDK】獲取JS-SDK許可權簽名>>>>dict[' + str(self.ret) + ']')
        return self.ret

if __name__ == '__main__':

    token_cache = TokenCache()

    jsapi_ticket = token_cache.get_cache(token_cache.KEY_JSAPI_TICKET)
    # 注意 URL 一定要動態獲取,不能 hardcode
    url = '%s/order/index' % WxConfig.AppHost
    sign = WxSign(jsapi_ticket, url)
    print(sign.sign())
複製程式碼

七. 完成測試,釋出上線,部署至centos伺服器

本測試專案釋出上線時使用的伺服器為阿里雲的centos 6.5伺服器,系統python版本為2.7,為保證多個Python版本共存,及當前專案環境的純淨,需要使用pyenv及虛擬環境virtualenv

同時我們採用 nginx 做負載均衡和靜態檔案伺服,supervisor做守護程式管理

1. 安裝pyenv

(1) 下載


$ git clone git://github.com/yyuu/pyenv.git ~/.pyenv
複製程式碼

(2) 配置


$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(pyenv init -)"' >> ~/.bashrc
複製程式碼

(3) 重新載入shell


$ exec $SHELL -l
複製程式碼

2.通過pyenv安裝多個版本的python

(1) 安裝相關依賴


yum install readline readline-devel readline-static -y
yum install openssl openssl-devel openssl-static -y
yum install sqlite-devel -y
yum install bzip2-devel bzip2-libs -y
複製程式碼

(2) 安裝需要的python版本


pyenv install 3.5.0
然後重新整理python版本
pyenv rehash
複製程式碼
Python+Tornado開發微信公眾號

(3) 設定全域性Python版本


$ pyenv global 3.5.0
複製程式碼
Python+Tornado開發微信公眾號

3.使用虛擬環境virtualenv
(1) 安裝


$ pip install virtualenv
複製程式碼

(2) 使用方法
(a)進入專案目錄


$ virtualenv --no-site-packages wx_env
複製程式碼

(b)用source進入該環境


$ source wx_env/bin/activate
複製程式碼

注意到命令提示符變了,有個(venv)字首,表示當前環境是一個名為venv的Python環境

4.匯出本地專案的關聯關係,並在centos上安裝


pip freeze > ./requirements.txt
pip install -r requirements.txt 
複製程式碼

5.安裝redis服務

(1)下載redis Redis
(2) 上傳至 /usr/local資料夾
(3) 解壓 tar -xzvf redis-3.2.3.tar.gz
(4) 重新命名redis-3.2.3檔名為redis
(4) 進入目錄 cd /usr/local/redis
(5) 編譯安裝


make 
make install
複製程式碼

(6) 修改配置檔案


vi /etc/redis/redis.conf
複製程式碼

僅修改: daemonize yes (no-->yes)
(7) 啟動


/usr/local/bin/redis-server /usr/local/redis/redis.conf
複製程式碼

(8) 檢視啟動


ps -ef | grep redis
複製程式碼

6.安裝nginx
(1) 下載安裝包:


wget http://nginx.org/download/nginx-1.10.0.tar.gz
複製程式碼

(2) 解壓Nginx的tar包,並進入解壓好的目錄


tar -zxvf nginx-1.10.0.tar.gz
cd nginx-1.10.0/
複製程式碼

(3) 安裝zlib和pcre庫


yum -y install zlib zlib-devel
yum -y install pcre pcre-devel
複製程式碼

(4) 配置、編譯並安裝


./configure
複製程式碼
Python+Tornado開發微信公眾號


make
make install
複製程式碼
Python+Tornado開發微信公眾號

(5) 啟動nginx


/usr/local/nginx/sbin/nginx
複製程式碼

訪問伺服器後如下圖顯示說明Nginx運正常。

Python+Tornado開發微信公眾號

7.配置nginx


user nobody;
worker_processes 1; 
#error_log logs/error.log; 
#error_log logs/error.log notice; 
#error_log logs/error.log info; 
#pid logs/nginx.pid; 
events {
    worker_connections 1024;
}

http {
    include mime.types;
    default_type application/octet-stream;

    upstream web_wx {
        server 127.0.0.1:8001;
		server 127.0.0.1:8002;
		server 127.0.0.1:8003;
		server 127.0.0.1:8004;
        
    }

    sendfile on; #tcp_nopush on; keepalive_timeout 65;
    proxy_read_timeout 200;
    tcp_nopush on;
    tcp_nodelay on;
    gzip on;
    gzip_min_length 1000;
    gzip_proxied any;

    server {
        listen 80;
        server_name localhost; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root html;
        }

        location / {
            proxy_pass_header Server;
            proxy_set_header Host $http_host; #	proxy_redirect false; proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Scheme $scheme;
            proxy_pass http://web_wx; }

    }

}
複製程式碼

8.配置Supervisord

(1) 安裝


yum install supervisor
複製程式碼

(2) 設定開機自啟


wget -O /etc/rc.d/init.d/supervisord https://gist.githubusercontent.com/gracece/21e5719b234929799eeb/raw/supervisord
複製程式碼

(3)將守護程式新增為服務


chmod +x /etc/rc.d/init.d/supervisord
chkconfig --add supervisord #加為服務
ntsysv    #執行ntsysv,選中supervisord啟動系統時跟著啟動。
複製程式碼

(4) 設定 /etc/supervisord.conf檔案


[unix_http_server]
file=/tmp/supervisor.sock   ; (the path to the socket file)
;chmod=0700                 ; socket file mode (default 0700)
;chown=nobody:nogroup       ; socket file uid:gid owner
;username=user              ; (default is no username (open server))
;password=123               ; (default is no password (open server))

;[inet_http_server]         ; inet (TCP) server disabled by default
;port=127.0.0.1:9001        ; (ip_address:port specifier, *:port for all iface)
;username=user              ; (default is no username (open server))
;password=123               ; (default is no password (open server))

[supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB        ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10           ; (num of main logfile rotation backups;default 10)
loglevel=info                ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false               ; (start in foreground if true;default false)
minfds=1024                  ; (min. avail startup file descriptors;default 1024)
minprocs=200                 ; (min. avail process descriptors;default 200)
;umask=022                   ; (process file creation umask;default 022)
;user=chrism                 ; (default is current user, required if root)
;identifier=supervisor       ; (supervisord identifier, default is 'supervisor')
;directory=/tmp              ; (default is not to cd during start)
;nocleanup=true              ; (don't clean up tempfiles at start;default false)
;childlogdir=/tmp            ; ('AUTO' child log dir, default $TEMP)
;environment=KEY="value"     ; (key value pairs to add to environment)
;strip_ansi=false            ; (strip ansi escape codes in logs; def. false)


[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket
;serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket
;username=grace             ; should be same as http_username if set
;password=grace               ; should be same as http_password if set
;prompt=mysupervisor         ; cmd line prompt (default "supervisor")
;history_file=~/.sc_history  ; use readline history if available


[group:tornadoApp]
programs=web_wx
[program:web_wx
]
command=python /var/web_wx/run.py --port=80%(process_num)02d
directory=/var/web_wx/
process_name = %(program_name)s%(process_num)d
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/tornado.log
stdout_logfile_maxbytes=500MB
stdout_logfile_backups=50
stderr_logfile=/var/log/tornado-error.log
loglevel=info
numprocs = 4
numprocs_start = 1
複製程式碼

(5) 開啟守護程式服務

supervisord
supervisorctl reload all
supervisorctl status複製程式碼

福利: 本文已同步到我的個人技術網站 IT乾貨-sufaith 該網站包括Python, Linux, Nodejs, 前端開發等模組, 專注於程式開發中的技術、經驗總結與分享, 歡迎訪問.


相關文章