微信公众号开发

微信公众号开发

# 微信公众号开发

# 前期准备

# 具有公网IP的服务器

只有具有公网IP的服务器,微信服务器才能与其建立连接,作为中介转发请求。同时为具有公网IP的服务器绑定域名,该域名随后会作为参数填入微信公众号的后台。

# 申请微信公众号

微信公众号网址: https://mp.weixin.qq.com 我使用的是未认证的订阅号。

# 配置应用服务器与数据库

# 应用服务器

微信公众号开发只能使用80或443端口,也就是我们常说的http或https,但是我部署的博客也使用的这个端口。不过公众号配置的时候,url路径可以进行灵活的配置。这样,我们就可以配置代理服务器,根据不同的路径,将请求转发到相应的应用服务器上。 本节以nginx + uwsgi + supervisor为例,进行介绍。其中nginx为代理服务器,uwsgi为应用服务器,supervisor为进程管理程序。

# nginx

关于nginx的安装,可以在博客中搜索关键字"nginx"。

nginx的配置,主要是添加一个新的访问路径,进行对应请求的转发:

location /mpserver/ {
        include uwsgi_params;
        uwsgi_pass 127.0.0.1:9090;
    }

有关nginx的配置,参考自如下链接:

关于nginx,建议引入https,提升传输数据的安全性。关于https的配置,可以采用certbot进行自动化配置。具体过程可以在博客内搜索关键词"certbot",或者自行参考网上资料。

# uwsgi

uwsgi的安装,可以通过pip进行。首先进入虚拟环境,然后:

pip install uwsgi

uwsgi,作为应用服务器,其主要功能就是将相关请求转发到对应的处理逻辑上,并将结果返回,配置文件可以放在/var/www/mpserver/下,并命名为uwsgi.ini:

[uwsgi]
socket = 127.0.0.1:9090
chdir = /var/www/mpserver
virtualenv = /var/www/mpserver/venv/
manage-script-name
mount = /mpserver=mpserver:app
processes = 4
threads = 2

由于引入了虚拟环境,所以需要添加virtualenv这个配置选项。为每一个项目创建一个虚拟环境,有利于进行项目的包管理。

此处: nginx中的uwsgi_pass参数,要和uwsgi中的socket保持一致; chdir为公众号项目的根目录; mount = /mpserver=mpserver:app,/mpserver为挂载的路径,对应nginx的location,mpserver:app中的mpserver为flask的启动文件名称,app为flask启动文件中初始化的Flask对象名称,例如:app = Flask(name)。此处需要结合flask的官方文档,新建一个最小的测试demo,用于验证配置的正确性。

有关uwsgi的配置,参考自如下链接:

http://flask.pocoo.org/docs/0.12/deploying/uwsgi/#starting-your-app-with-uwsgi
# supervisor

supervisor的安装,可以通过pip进行。首先进入虚拟环境,然后:

pip install supervisor

supervisor作为进程管理程序,主要用于监控uwsgi的运行,如果uwsgi意外退出,可以自动启动uwsgi,配置文件可以放置在/var/www/mpserver/venv/下,并命名为supervisord.conf:

[program:mpserver]
command = /var/www/mpserver/venv/bin/uwsgi --ini /var/www/mpserver/uwsgi.ini
autorestart = true
stopasgroup = true
redirect_stderr=true
stdout_logfile=/tmp/mpserver.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10

由于我们之前配置了uwsgi,每次启动4个进程,所以在使用supervisor进行管理的时候,要设置stopasgroup为true。这样在终止supervisor进程的时候,四个uwsgi子进程都可以被终止,否则只会有一个子进程被终止。

此时,我们只能通过指定supervisor的路径进行启动,所以,还需要将其放入systemctl系统服务中,这样,就可以通过sudo systemctl start/stop/restart supervisor对supervisor进行控制。

自定义服务配置可以通过github进行下载,然后将其放入/lib/systemd/system/下,并命名为supervisor.service。对自定义服务配置文件的编辑如下:

# supervisord service for systemd (CentOS 7.0+)
# by ET-CS (https://github.com/ET-CS)
[Unit]
Description=Supervisor daemon

[Service]
Type=forking
ExecStart=/var/www/mpserver/venv/bin/supervisord
ExecStop=/var/www/mpserver/venv/bin/supervisorctl $OPTIONS shutdown
ExecReload=/var/www/mpserver/venv/bin/supervisorctl $OPTIONS reload
KillMode=process
Restart=on-failure
RestartSec=42s

[Install]
WantedBy=multi-user.target

此时,建议输入 sudo systemctl daemon-reload 重新加载一遍所有的配置文件 如果输入 sudo systemctl start supervisor 可以启动supervisor,则配置无误。此时可以通过sudo systemctl enable supervisor设置supervisor为开机自动启动。 然后,继续输入ps -aux | grep uwsgi查看,是否有4个uwsgi的进程,如果有,则输入sudo killall uwsgi,然后再次通过ps -aux | grep uwsgi查看是否有4个uwsgi进程,如果有,则配置完成。出错的话,重点盘查supervisord.conf和uwsgi.ini文件。

有关supervisor的配置,参考自如下链接:

http://www.supervisord.org/
http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html

# 数据库

关于数据库的配置,除了安装,就是为项目新建数据库用户,避免直接使用root账户连接数据库。

安装的话,可以通过sudo yum install mariadb-server mariadb-client。执行该命令前,记得提前配置好mariadb的仓库。

安装结束以后,输入mysql -u root -p,以root账户登录mariadb; 执行以下命令,新建数据库用户:

create user 'username'@'%' identified by 'password';
grant all on mpserver.* to 'username'@'%';

以上步骤新建了用户username,其密码为password,能够操作的数据库为mpserver。

关于数据库的安装和配置,可以参考如下链接:

# 框架引入

注意:所有的pip操作都要提前进入虚拟环境。

# 安装

通过pip安装flask,flask-restplus,flask-sqlalchemy,pymysql,werobot:

pip install flask flask-restplus flask-sqlalchemy pymysql werobot

# 配置

在项目文件夹根目录新建两个空文件,如mpserver.py、config.py;然后在根目录下新建文件夹,如mpcode;然后在mpcode文件夹中,分别新建空文件__init__.py、models.py、weapis.py、robot.py。

mpserver.py内容如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask
from mpcode.robot import myrobot
from mpcode.weapis import api
from mpcode.models import db
from werobot.contrib.flask import make_view

app = Flask(__name__)

# 配置flask-sqlalchemy使用pymysql作为连接数据库的中间件
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://user:[email protected]:3306/dbname'

# 将flask-sqlalchemy与flask整合
db.init_app(app)
# 在数据库创建models中定义的表
db.create_all(app = app)

# 将werobot与flask整合
app.add_url_rule(
	rule = '/',
	endpoint = 'werobot',
	view_func = make_view(myrobot),
	methods = ['GET', 'POST'],
)
api.init_app(app)

config.py内容如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

TOKEN = 'yourtoken'
APP_ID = 'yourappid'
APP_SECRET = 'yourappsecret'
ENCODING_AES_KEY = 'yourencodingaeskey'
ENABLE_SESSION = False
SESSION_STORAGE = False

models.py内容如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

weapis.py内容如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask_restplus import Api, Resource

api = Api(doc = False)

robot.py内容如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from werobot import WeRoBot

myrobot = WeRoBot()
myrobot.config.from_pyfile('config.py')

# myrobot.add_filter(func=article, rules=[re.compile(u"[A-Za-z]|[0-9]|[\u4e00-\u9fa5]|[\uFB00-\uFFFD]")])

至此,几个框架已经得到整合,只需要按照需求在对应的文件中填充代码即可。

关于框架的整合,包括公众号相关参数的填写参考自以下文档:

http://flask.pocoo.org/
http://flask-sqlalchemy.pocoo.org

# 相关知识

# HTTP常用请求类型

未认证的订阅号,开放的接口有限,本文主要利用的是公众号被动回复的功能。这个在微信官方文档-消息管理-被动回复用户消息中提到,“严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复”。所以,对于只利用到被动消息功能的应用,发现数据库中没有写入accesstoken是正常的,因为调用接口的时候才会进入获取accesstoken并保存的逻辑。

本公众号的多数功能都是基于接口。用户在公众号内主动发送消息,微信服务器将消息转发给自建服务器,自建服务器根据过滤条件,触发不同的处理逻辑,处理逻辑通过参数向相关的接口发送请求,然后将返回值返回给微信服务器,最后由微信服务器将消息回复给用户。

因此,有必要了解常用的几种HTTP请求类型。本文只介绍四种请求类型,分别为get、post、put、delete,其含义如下: get:获取        post:提交        put:更新        delete:删除

关于HTTP请求的具体信息,参考如下链接:

# python decorator

在执行某些函数之前,都要执行某段代码,无疑增加了整个项目的冗余,而且对于提前执行的代码段,维护量也比较大、比较繁琐。但是python的装饰器可以很好的解决这个问题。对于需要提前执行的代码段,抽离出来,形成装饰器。然后在每一个需要执行它的函数之前,通过@“装饰器”的方法实现提前执行特定代码段的目的。

decorator的关键就是将A函数作为参数传入B函数,在B函数中调用A函数。

示例代码如下:

def tagcheck(func):
    def wrapper(args):
        url4p = "https://yoururl/weapis/user/" + weapitoken
        url4g = url4p + '?uid=' + args.source
        r = requests.get(url4g).json()
        if (r['tag'] > 0):
            r['tag'] += 1
            r = requests.put(url4p, json = {'uid': args.source, 'count': r['count'], 'tag': r['tag']})
        else:
            return func(args)
    return wrapper

def apiauth(func):
    def wrapper(*args, **kwargs):
        if (kwargs['weapitoken'] == weapistoken):
            return func(*args, **kwargs)
        else:
            return {'code': '403'}
    return wrapper

# 使用方法:
@tagcheck
funcA(argA)

@apiauth
funcB(argb, argc)

其中,tagcheck传入的func参数即为需要在tagcheck中调用的函数,本例为funcA;wrapper传入的args参数即为funcA要接收的参数argA。 apiauth说明了如果函数的参数个数不固定,应该如何写对应的装饰器。此处需要注意*args与**kwagrs的区别。*args表示任意多个无名参数,是一个tuple,**kwargs表示有关键字的参数,是一个dictionary。

关于python的装饰器,可以参考以下链接:

# 功能点示例

# 事件过滤

代码示例:

# 当用户关注公众号以后,会触发一个subscribe事件
# 然后,用户会在公众号内收到“欢迎关注”的消息

@myrobot.subscribe
def subscribe(message):
    return "欢迎关注"

# 关键词过滤

代码示例:

@tagcheck
def count(message):
    url4p = "https://yoururl/weapis/user/" + weapitoken
    url4g = url4p + '?uid=' + message.source
    r = requests.get(url4g).json()
    if (r['uid'] == 'null'):
        r = requests.post(url4p, json = {'uid': message.source}).json()
    return "你在Yunerself已留言%s次,有你的回应,我很幸福!" % r['count']

# 触发关键词为:C, c, count
myrobot.add_filter(func=count, rules=["C", "c", "count"])

在我的公众号内,有一个功能是统计用户在公众号里面留言的次数,而这个功能的实现就是依赖于上面的函数。函数将用户的OpenID(message.source)作为参数,向接口请求该用户的留言次数。tagcheck用于检查用户是否处于留言模式,当用户进入留言模式以后,count对应的触发关键词会当做留言内容,不会执行count函数。

对于werobot,基于文字的过滤有一个filter装饰器,例:

@robot.filter("a")
def a():
    return "正文为 a "

# 如果用户在公众号内输入“a”,则会收到“正文为a”的回复

当我需要在函数内部调用message参数的时候,使用filter始终获取不到值,即使是在a()中传入参数名message,但是通过上面的add_filter即可。

# 获取语音识别内容

代码示例:

@myrobot.voice
def voicedispatch(message):
    # 公众号开启语音识别以后,微信服务器会将识别结果一并传入自建服务器
    # 但是微信的识别结果最后会加一个标点符号,貌似还能基于语义来判断
    # “哈喽、只识别中文啊、哎呦”,这些词,都会加感叹号!,而“微信、帮助、我”,这些词,都会加句号。
    # 因此,我们需要将最后的标点符号位截取掉,然后进行匹配
    keyword = message.recognition[:-1]
    if (keyword == u"简介"):
	return about(message)
    elif (keyword == u"统计"):
	return count(message)
    elif (keyword == u"帮助"):
	return help(message)
    elif (keyword == u"魔法"):
	return magic(message)
    elif (keyword == u"结束留言"):
	return over(message)
    elif (keyword == u"我要留言"):
	return wish(message)
    else:
	return article(message)

关于语音识别的处理,重点关注上面注释提及的部分即可。貌似微信的语音识别只支持中文和数字,但是也没有在官方文档中看到明确的内容。

我的项目源码已经上传到了github,但是对部分细节做了处理,隐藏了密码、路径之类的信息,部署测试的时候请注意将其修改为正确的内容。