微信小程序开发 后台开发详解, 二维码生成,推送消息,微信支付

Posted Tamic大白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微信小程序开发 后台开发详解, 二维码生成,推送消息,微信支付相关的知识,希望对你有一定的参考价值。

原文: https://blog.csdn.net/g8433373/article/details/80722001

前言

微信小程序已经是家喻户晓了,最近和同学一起刚上线了一款应用校园懒人邦,感兴趣的朋友可以搜索一下,一款基于校园最后一百米的概念开发的快递&外卖配送平台,我是负责后台开发部分,这里给朋友们介绍下相关开发经验,开发框架和方式有很多,这里给大家介绍一些快捷高效的方法,大家少走弯路!

开发环境

macOs 10.13.2
PyCharm 16.1
Python 2.7
vim 8.0

开发流程

项目整体结构

我们先来看下项目整体结构:

pjt: 整个项目的代码文件
api: 项目的所有接口信息
conf: 项目所有的配置信息
dal/dao: 数据维护层,所有的操作数据库的逻辑都要经过这里
db: 第一版使用的自己设计的orm框架,第二版废弃了
images: 图片缓存区,缓存存二维码还有用户头像上传七牛云
impl: 接口功能实现
key: 各种加密文件,微信支付,ssl证书等
log: 日志文件,目前天级别生成最新的一份
model: 数据模型, 第一版已废弃
pjt_data: 第二版使用Django QuerySet的orm框架,比自己写的方便多了大力推荐,具体学习可以查看这里,内部代码就不当做demo展示了,哈哈!
test_demo: 写一些功能的测试类,方便线上调试

接口开发

整个接口开发使用的是Flask框架,Flask是Python编写的轻量级Web应用框架,用过的人都知道简单快捷, 马上花一分钟上手下:

安装: sudo pip install flask
一个简单的demo:
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hi():
    return "hi!"

if __name__ == "__main__":
    app.run()

运行程序,浏览器输入, 可以看到页面返回了一个hi!,ok,这就可以了, 整个框架我们就搭建起来了,是不是很简单,后面我们来拓展一下!
http://127.0.0.1:5000/

详细的flask教程可以参考这里

项目部署

ip映射

个人选择的服务器是阿里云,性能不错,这里假设咱们的阿里云的Ip是1.2.3.4,那么首先我们要在服务器上建立映射关系,当我访问http://1.2.3.4:5000/时,不仅是本地能访问,任何地方都行,这个很简单,只要你买了服务器,里面有一项配置你服务器的ip地址就行。
nginx反向代理

现在我们可以使用http://1.2.3.4:5000/访问网站了,还缺少一些什么?当然,微信小程序是不允许接口暴露ip地址的,你必须要用自己的域名和自定义的接口名称,比如http://www.happypower.com/这样才行,当我访问这个网站时候,会先到阿里云服务器上做一下映射,确定我们的ip1.2.3.4,然后找到该Ip对应的服务器,之后怎么办? 我需要访问的是http://1.2.3.4:5000/这个啊,Nginx帮我们做了这个事,下面简单介绍下:
安装:具体看这里
安装完了之后咱们先配置下,详细配置可以参考这里,这里我简单说下我的配置,并给出具体demo:
进入目录/etc/nginx,每人安装目录可能不同,找到nginx.conf文件,vim进行编辑,vim的操作如果不熟可以查看这里,不行可以先本地测好再到线上用,下面给出我配置的信息,大家可以仿造,应该是比较精简的:

# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events 
    worker_connections 1024;


http 
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    #include /etc/nginx/conf.d/*.conf;

    server 
        listen       80  default_server;
        listen       [::]:80 default_server;
        server_name  _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / 
        return 404;
        

        error_page 404 /404.html;
            location = /40x.html 
        

        error_page 500 502 503 504 /50x.html;
            location = /50x.html 
        
    

# Settings for a TLS enabled server.
#
#    server 
#        listen       443 ssl http2 default_server;
#        listen       [::]:443 ssl http2 default_server;
#        server_name  _;
#        root         /usr/share/nginx/html;
#
#        ssl_certificate "/etc/pki/nginx/server.crt";
#        ssl_certificate_key "/etc/pki/nginx/private/server.key";
#        ssl_session_cache shared:SSL:1m;
#        ssl_session_timeout  10m;
#        ssl_ciphers HIGH:!aNULL:!MD5;
#        ssl_prefer_server_ciphers on;
#
#        # Load configuration files for the default server block.
#        include /etc/nginx/default.d/*.conf;
#
#        location / 
#        
#
#        error_page 404 /404.html;
#            location = /40x.html 
#        
#
#        error_page 500 502 503 504 /50x.html;
#            location = /50x.html 
#        
#    

# 自己主要要写的就下面这个server
server 
    # 443是https监听的端口,一般默认就好
    listen 443;
    # 自己接口的主域名,这就是当我们访问http://www.happypower.com时就会映射到这里
    server_name www.happypower.com;
    ssl on;
    # 这是默认主页,接口没有主页就注释掉了
    # root html;
    # index index.html index.htm;
    # 下面是阿里云下载的ssl证书,后面会说到ssl的配置,看不懂没关系
    ssl_certificate   ./xxx.pem;
    ssl_certificate_key  ./xxx.key;
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    location / 
    # 这里的意思是http://www.happypower.com会和http://0.0.0.0:8888做一个映射,这就解决了上面最开始说的那个问题了,这一步很关键!
    proxy_pass               http://0.0.0.0:8888;
    
    # 日志文件路径,有人访问你的网站时都会留下印记,有的话最好,不配置其实也行。
    access_log                 /root/xxx/pjt/log/nginx.log;


之后咱们启动nginx,就能通过http://www.happypower.com进行访问了,成功的话会返回hi!
gunicorn+super多进程开启服务+进程监控

这两个很简单,其实就是几条命令的事,主要说下作用:
1 gunicorn 可以让你的后台服务多进程方式开启,经过测试可以提升一定的qps(每秒的请求数),简单来说一定程度上防止你的服务器崩掉.

2 supervisor 的作用就是对你的进程进行监控,该框架提供了一个可视化界面,可以通过这个界面去开启,暂停和关闭你的服务进程,即使不动代码的人也能控制后台服务。

3 具体配置没啥好讲的,学会几个命令就行具体推荐看这里

ssl证书

由于小程序需要的接口都是需要https的连接,所以咱们还需要ssl证书才行,这里我使用的是阿里云服务器,具体配置可以先参考这里,可能有点难懂,下面简单说下我的配置:

我选择的是单域名免费型 DV SSL,其实一般的应用来说,单域名足够了。
官网下载ssl证书,一般是xxx.key和xxx.pem两个文件,上面Nginx反向代理配置就需要用到这个,可以返回上面的nginx.conf文件进行查看!
管理证书,这个就是配置问题,官方文档写的很详细
配置成功后,可以使用https://127.0.0.1:5000/ 或者https://www.happypower.com来访问你的网站,然后返回hi!,当然,也有可能访问不了,一般就是配置问题,https其实也是在访问http,只是中间多了一个验证的过程,感兴趣的可以到这里学习下http和https的区别。
ps.建议用什么服务器就用哪里的证书,腾讯云和阿里云推荐,其他的真的难配,个人遇到了很多坑!
小程序常用功能

微信支付

有些小程序涉及到微信支付的,就比如校园懒人邦,这个其实很头疼,做过就知道,难到不是很难,过程很繁琐,官方文档也很多坑,下面简单讲解下:

首先一定要注册公司,这个让运营或者产品的同学去做会好一些
一般来说支付都是单向的,也就是使用者对公司付款,如果是公司对使用者付款则需要9个月的申请时间才行,这个比较坑,当然,退款是不需要等的.
具体细节可以先看文档,这里面讲的还是非常清楚的,包括一些接口说明等.
这里我使用python写了个微信预支付的较为通用的类,大家改下参数拿去用就行,官方文档一个个试出来的,简直坑:

class WeiXinPay(object):
    """微信支付,返回回客户端需要参数
    """
    def __init__(self, uu_id, open_id, spbill_create_ip, total_fee, out_trade_no):
        """
        :param total_fee: 订单金额
        :param spbill_create_ip: 客户端请求IP地址
        """
        self.params = 
            'appid': '小程序的appid',
            'attach': u'你的应用名称,我这里是(校园懒人邦)',
            'body': u'校园懒人邦-代取费',
            'mch_id': '商户id,你企业的id',
            'nonce_str': '给个随机数,一般md5一下就好,时间戳啊或者别的什么',
            'notify_url': 'http://www.happypower.com/result(这个就是通知地址,会异步返回信息给你),你写一个接口接收就行',
            'openid': open_id,
            'out_trade_no': out_trade_no,
            'spbill_create_ip': spbill_create_ip,
            'total_fee': str(total_fee),
            'trade_type': 'JSAPI'
        
        # 官方给的接口
        self.url = 'https://api.mch.weixin.qq.com/pay/unifiedorder'
        self.error = None

    def key_value_url(self, value):
        """将将键值对转为 key1=value1&key2=value2
        """
        key_az = sorted(value.keys())
        pair_array = []
        for k in key_az:
            v = value.get(k, '').strip()
            v = v.encode('utf8')
            k = k.encode('utf8')
            pair_array.append('%s=%s' % (k, v))
        tmp = '&'.join(pair_array)
        return tmp

    def get_sign(self, params):
        """生成sign
        """
        stringA = self.key_value_url(params)
        stringSignTemp = stringA + '&key=' + 'xxx' # APIKEY, API密钥,需要在商户后台设置
        sign = (md5(stringSignTemp).hexdigest()).upper()
        params['sign'] = sign

    def get_req_xml(self):
        """拼接XML
        """
        self.get_sign(self.params)
        xml = "<xml>"
        for k, v in self.params.items():
            v = v.encode('utf8')
            k = k.encode('utf8')
            xml += '<' + k + '>' + v + '</' + k + '>'
        xml += "</xml>"
        print xml
        return xml

    def get_prepay_id(self):
        """
        请求获取prepay_id
        """
        xml = self.get_req_xml()
        headers = 'Content-Type': 'application/xml'
        r = requests.post(self.url, data=xml, headers=headers)
        re_xml = ElementTree.fromstring(r.text.encode('utf8'))
        xml_status = re_xml.getiterator('result_code')[0].text
        if xml_status != 'SUCCESS':
            self.error = u"连接微信出错啦!"
            logging.error(u"连接微信出错啦!")
            return
        prepay_id = re_xml.getiterator('prepay_id')[0].text
        self.params['package'] = 'prepay_id=%s' % prepay_id
        self.params['timestamp'] = str(int(time.time()))

    def re_finall(self):
        self.get_prepay_id()
        if self.error:
            return

        sign_again_params = 
            'appId': self.params['appid'],
            'timeStamp': self.params['timestamp'],
            'nonceStr': self.params['nonce_str'],
            'package': self.params['package'],
            'signType': 'MD5'
        
        self.get_sign(sign_again_params)
        sign_again_params['paySign'] = sign_again_params['sign']
        sign_again_params['total_fee'] = self.params['total_fee']
        sign_again_params['notify_url'] = self.params['notify_url']
        sign_again_params.pop('appId')
        sign_again_params.pop('sign')
        return json.dumps(sign_again_params)

5 这里最后返回的参数传给前台就能支付了,前台会拿到你的prepay_id,然后就能按照指定金额支付了。
6 退款的话有些不同,首先也是具体先查看文档,下面我也写了一个较为通用的类,大家觉得文档麻烦直接用也行:

class WeiXinReturn(object):
    def __init__(self, out_trade_no, total_fee, refund_fee):
        self.params_mach = 
            # 申请商户号的appid或商户号绑定的appid
            'appid': 'xxx',
            'mch_id': 'xxx',
            'nonce_str': '随机数',
            'out_trade_no': out_trade_no,
            'out_refund_no': '微信订单号',
            'total_fee': str(total_fee),
            'refund_fee': str(refund_fee),
            'notify_url': 'http://www.happypower.com/pay_return/result(和支付一样的意思,就是退款的通知地址,自己开发一个接口就好)'
        

    def pay_return(self):
        stringA = self.key_value_url(self.params_mach)
        stringSignTemp = stringA + '&key=' + 'xxx'  # APIKEY, API密钥,需要在商户后台设置
        sign = (md5(stringSignTemp).hexdigest()).upper()
        self.params_mach['sign'] = sign
        xml = "<xml>"
        for k, v in self.params_mach.items():
            v = v.encode('utf8')
            k = k.encode('utf8')
            xml += '<' + k + '>' + v + '</' + k + '>'
        xml += "</xml>"
        headers = 'Content-Type': 'application/xml;charset=UTF-8'
        # url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers'
        url = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
        # 请求中需要带有支付的证书,没有证书是无法申请的
        r = requests.post(url, data=xml, headers=headers, cert=('xxx.pem', 'xxx.pem'))
        result = r.text
        re_xml = ElementTree.fromstring(r.text.encode('utf8'))
        xml_status = re_xml.getiterator('result_code')[0].text
        if xml_status == 'SUCCESS':
            return True
        return False

    def key_value_url(self, value):
        """将将键值对转为 key1=value1&key2=value2
        """
        key_az = sorted(value.keys())
        pair_array = []
        for k in key_az:
            v = value.get(k, '').strip()
            v = v.encode('utf8')
            k = k.encode('utf8')
            pair_array.append('%s=%s' % (k, v))
        tmp = '&'.join(pair_array)
        return tmp

ps.这里需要商户证书,具体怎么弄看这里

生成二维码

这个功能比较常见了,同样也是先看文档,里面有三种二维码接口,开发阶段建议使用接口B:https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN, 项目上线了用接口A会更加灵活,因为可以通过二维码跳转页面且能够携带参数,校园懒人邦中的分享功能就是利用接口A进行开发,主要接口A上线才好测,而接口B开发阶段好测。
二维码是图片,涉及到存储问题,这里推荐七牛云,账号免费有10G的使用空间,很棒的,具体思路就是先到本地做一层缓存,然后本地上传到七牛云,相关代码可以参考下面的,基本已经封装好了:

   def upload_img(self, local_path, upload_name, bucket_name, ttl=7200):
        """
        上传图片到七牛云
        :param local_path: 本地文件路径
        :param upload_name: 上传文件名
        :param bucket_name: 七牛申请的存储空间名称
        :param ttl: 过期时间
        :return: 返回图片地址
        """
        from qiniu import Auth, put_file
        import re
        q = Auth(access_key=conf_test.AccessKey, secret_key=conf_test.SecretKey)
        token = q.upload_token(bucket_name, upload_name, ttl)
        ret, info = put_file(token, upload_name, local_path)
        pat_status = 'status_code:(.*?),'
        status_code = re.compile(pat_status, re.S).findall(str(info))
        if len(status_code) > 0:
            # 成功返回图片外链名称,失败返回原因以及状态码
            if int(status_code[0]) == 200:
                return "msg":conf_test.qiniu_domain + upload_name, "status":1
        return "msg":"failed upload img failed", "status":-1

相关的sdk文档可以参考这里
还有一个问题就是微信小程序只能识别https的图片外链地址,而刚开始上传七牛云生成的是http的链接图片,因此这里需要到七牛云上面配置下,具体的可以查看这里
二维码基本流程就是这么一套,当然缓存怎么做方式很多,这里仅供参考。

推送消息

给用户发送模板消息,提醒用户做一些事也是经常用到的功能,具体操作还是先查看文档,当然,微信规定了每天用户只能最多收到三条消息,当然还是有别的办法可以给用户多发一些消息,具体往下看.
看文档可以知道,其实只需要有足够的用户form_id咱们就能对用户无限制的发送消息,前提是用户没有屏蔽你的这个微信小程序,可以参考这里,当然这里是用java写的,我按照这个封装了一个python版本的,大家可以参考着用:

   # 刷新用户form_id 存入redis
    def fresh_formid(self, **kwargs):
        """
        接收前台传来的form_id存入redis
        :param kwargs: 
        :return: 
        """
       uu_id = kwargs.get("uu_id", "")
       if not uu_id:
           return "failed: uu_id cannot be null"
       open_id = kwargs.get("open_id", "")
       if not open_id:
           return "failed: open_id cannot be null"
       form_id = kwargs.get("form_id", "")
       if not form_id:
           return "failed: form_id cannot be null"
       if "invalid code" in open_id:
           return "failed: get openid err"
       msg = open_id: form_id
       self.redis_cli.sadd(uu_id, json.dumps(msg))
       return "success"
    # 发消息模板
    def send_template_msg(self, **kwargs):
        uu_id = kwargs.get("uu_id", "")
        if not uu_id:
            return "failed: uu_id cannot be null"
        data = kwargs.get("data", "")
        if not data:
            return "failed: data can not be null"
        # 这个token可以从文档中去查看怎么得到,内部源码不能提供
        token = self.get_refresh_token()
        if not token:
            return "failed: cannot find a token"
        send_msg = 
        # 从redis中取出用户对应的form_id
        redis_msg = self.redis_cli.spop(uu_id)
        if not redis_msg:
            return "failed: cannot find a open_id in redis"
        msg = json.loads(redis_msg.encode("utf8"))
        touser = msg.keys()[0]
        form_id = msg[touser]
        template_id = kwargs.get("template_id", "")
        if not template_id:
            return "failed: template_id cannot be null"
        # page是用户点开消息后跳转到的页面
        page = kwargs.get("page", "")
        # 下面这些参数的含义在文档中都能查到
        emphasis_keyword = kwargs.get("emphasis_keyword", "")
        send_msg["touser"] = touser
        send_msg["template_id"] = template_id
        send_msg["page"] = page
        send_msg["form_id"] = form_id
        send_msg["data"] = data
        send_msg["emphasis_keyword"] = emphasis_keyword
        # 调起发送消息接口
        api = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=%s" % token
        # post方式发起网络请求
        return self.get_html(url=api, data=send_msg)

测试

黑盒测试

应用上线之前肯定要各种测试才行,功能上黑盒测试可以交给不太懂代码的同学,也就是按照流程走通一遍,中途出现的问题记录下来然后交给程序哥哥。
qps测试

我这里主要介绍下qps的测试,因为我们是给用户用的,我们必须估计一下我们的服务器能承载多少用户同时在线,也就是平均来说能承受多少用户每秒的请求数量, 我们可以对最常用的几个接口做一个极限测试,然后计算出大致的qps, 最简单的做法就是多线程疯狂的call计算平均时间,这里给出一段测试代码:

class PressTest(object):
    def __init__(self):
        pass

    def get_html(self, url, headers=conf_test.HEADERS, data=None):
        if data:
            data = json.dumps(data)
        req = urllib2.Request(url=url, headers=headers, data=data)
        response = urllib2.urlopen(req)
        html = response.read()
        return html

    def get_test(self, api_url):
        result = self.get_html(url=api_url)
        if not result:
            print "url %s result is none" %api_url
            return
        return

    def post_test(self, api_url, **kwargs):
        import time
        start_time = time.time()
        result = self.get_html(url=api_url, data=kwargs)
        print result
        if not result:
            print "url %s result is none" % api_url
        end_time = time.time()
        all = end_time - start_time
        print all


if __name__ == '__main__':

    press_test = PressTest()
    print "requesting........."
    api_url = 'https://xxx接口1'
    api_url2 = 'https://xxx接口2'
    start = time.time()
    times = 400
    for i in range(times):
        t1 = threading.Thread(target=press_test.get_test, args=(api_url,))
        t2 = threading.Thread(target=press_test.get_test, args=(api_url2,))
        t1.start()
        t2.start()
        t1.join()
        t2.join()
    end = time.time()
    ave = (end - start) / 800.0
    print "count:%d, start_time :%s, now_time: %s, average_qps: %s" % (800, str(start), str(end), str(ave))

这里是开了400个线程并发call这两个常用接口,最后计算出qps(每秒的请求数),如果能达到100次/秒,基本上几千人同时在线没啥问题,哈哈
总结

emmmm……,整个开发流程差不多就这样,比较常用几个点就这些,还有很多东西没有介绍比如短信验证,数据库性能方面的问题,下回再和大家交流,有啥问题请留言,有时间都会回复,总的来说微信小程序开发还是挺有意思的, 觉得有帮助的朋友麻烦点个赞!

相关资料

[1] Flask入门
[2] Mac安装Nginx
[3] vim常用技巧
[4] nginx + gunicorn + supervisor部署技巧
[5] 阿里云ssl证书配置
[6] http和https的区别
[7] 微信支付文档
[8] 微信商户证书
[9]七牛云python sdk
[10]图片http转https
[11]小程序消息模板发送技巧
[12]Django QuerySet orm框架学习

更多阅读下篇:

微信小程序开发(二)图片上传+服务端接收
微信小程序开发(三) 微信小程序授权获取用户信息openid
https://tamic.blog.csdn.net/article/details/89284104

文/YXJ
地址:http://blog.csdn.net/sk719887916/article/details/53761107

更多原创关注开发者技术前线

以上是关于微信小程序开发 后台开发详解, 二维码生成,推送消息,微信支付的主要内容,如果未能解决你的问题,请参考以下文章

微信小程序开发 后台开发详解, 二维码生成,推送消息,微信支付

个人开发者无法生成微信小程序二维码

微信小程序开发--消息推送配置

微信小程序开发之普通链接二维码

微信小程序开发调试工具

微信小程序开发,怎么获取小程序场景值?