# 福利

转眼间,《ThinkPHP5.0+小程序商城构建全栈应用》(opens new window) (下简称:《零食商贩》)这门课在慕课网上线已经过去差不多3年了。这3年时间里,互联网很多事情都发生了改变,当然也包括微信小程序。回过头看这门课,有些功能因为微信小程序团队的调整已经废弃掉了,其中影响最广泛的就是关于获取用户信息和订阅消息(替换原来的模板消息)。这两个改动都曾在社区引起广泛的争议,但回过头看看这门课程的代码,得益于一开始的良好设计和封装,改动起来还是很轻松的,看着这年迈的代码依然发挥余热,瞬间留下了几滴欣慰的泪水。本章节作为临时增补的内容,以课程项目适配微信小程序新特性为实战给读者们在其他项目中提供参考。

# 获取用户信息

以前在小程序中我们想获取用户信息一般是在调用wx.login之后的回调函数里再调用wx.getUserInfo或者直接调用wx.getUserInfo,这样用户在小程序中会看到一个弹出窗口提示授权获取信息,但是在2018年4月的时候,微信小程序官方团队发出了调整的通知(点击查看(opens new window) ),调整之后你想获取用户信息只有两个办法:

  1. 使用 button 组件,并将 open-type 指定为 getUserInfo 类型,获取用户基本信息。
  2. 使用 open-data 展示用户基本信息。

这两种方法的区别在于第二种小程序是直接提供了原生组件,把组件放置在页面中就会显示当前访问用户的用户信息,而且无需授权。这种使用最简单,但是问题也很明显,就是开发拿不到这个用户的用户信息,因为是小程序内置组件直接帮你展示的,所以第二种方法只适合于纯展示用途,但是绝大多数情况下我们需要的是第一种方法。我们不单要在页面上显示用户的信息(微信头像、微信昵称),更要将这部分数据回传给我们的后端服务器保存到数据库中。

那么怎么适配这个改动呢?其实也不难,但是不知道为什么原本一件挺简单的事让官方团队解释得云里雾里,而且早期文档描述也是不够清晰。根据目前wx.getUserInfo(opens new window) 文档的描述:

在用户未授权过的情况下调用此接口,将不再出现授权弹窗,会直接进入 fail 回调(详见《公告》(opens new window) )。在用户已授权的情况下调用此接口,可成功获取用户信息。

通过这句话,其实我们就能很快的想到一个实现的思路,那就是调用wx.getUserInfo,根据进入了success回调还是fail回调来判断要不要授权,如果需要授权,那就在页面放置一个button来弹出授权窗让用户授权。

思路有了,让我们来改动一下代码,利用IDE打开小程序项目,接着打开根目录下的pages\my\my-model.js

/**
 * Created by jimmy on 17/3/24.
 */
import { Base } from '../../utils/base.js'

class My extends Base {
    constructor() {
        super();
    }

    //得到用户信息
    getUserInfo(cb) {
        var that = this;
        wx.login({
            success: function () {
                wx.getUserInfo({
                    success: function (res) {
                        const data = {
                            auth: true,
                            userInfo: res.userInfo
                        }
                        typeof cb == "function" && cb(data);

                        //将用户昵称 提交到服务器
                        if (!that.onPay) {
                            that._updateUserInfo(res.userInfo);
                        }

                    },
                    fail: function (res) {
                        const data = {
                            auth: false,
                            userInfo: {
                                avatarUrl: '../../imgs/icon/user@default.png',
                                nickName: '点击获取用户信息'
                            }
                        }
                        typeof cb == "function" && cb(data);
                    }
                })
            },
        })
    }

    /*更新用户信息到服务器*/
    _updateUserInfo(res) {...}
}

export { My }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

2020年的今年看到这种充满回调函数调用的语法真的有点难以接受,当然这主要是因为那时候小程序对ES6语法特性支持度不够导致的,更加简洁优雅的小程序开发可参考七月老师的全栈新课《Java全栈工程师:从Java后端到全栈,高级电商全栈系统大课》(opens new window)

这里我们对原来的代码稍加了修改,主要改动了返回的数据结构,我们增加了一个auth属性来标识用户是否授权过,接着点击同级目录下的my.js中,我们同样需要对调用处的代码稍加修改:

import {Address} from '../../utils/address.js';
import {Order} from '../order/order-model.js';
import {My} from '../my/my-model.js';

var address=new Address();
var order=new Order();
var my=new My();

Page({
    data: {
        pageIndex:1,
        isLoadedAll:false,
        loadingHidden:false,
        orderArr:[],
        addressInfo:null
    },
    onLoad:function(){
        this._loadData();
        this._getAddressInfo();
    },

    onShow:function(){...},

    _loadData:function(){
        var that=this;
        my.getUserInfo((data)=>{
            that.setData({
                showAuth: !data.auth,
                userInfo: data.userInfo
            })
        });

        this._getOrders();
        order.execSetStorageSync(false);  //更新标志位
    },

    /**
     * 获取用户信息按钮回调事件
     */
    getUserInfo(event) {
        const { nickName, avatarUrl } = event.detail.userInfo
        this.setData({
            showAuth: false,
            userInfo: { nickName, avatarUrl }
        })
    },

    /**地址信息**/
    _getAddressInfo:function(){...},
    
    // 省略一堆代码
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

这里我们同样对_loadData()方法里的代码做简单修改,我们新增绑定了一个变量showAuth,这个就是用来控制页面上是否显示授权按钮的,接着我们定义了一个新的方法getUserInfo(),这个方法是用于接收授权按钮的回调数据的,从方法体内的实现我们可以看出就是获取到我们想要的微信昵称和头像。到这里我们的代码逻辑部分就改动完毕了,非常简单的改动,接着我们就是要修改下wxml页面中的元素,按官方文档中的指引,我们需要在页面中放置一个Button按钮, 相信有过小程序开发经验的小伙伴都知道,官方原生的button有多难看,而且想要修改样式很麻烦,那么有什么办法可以解决呢?答案就是第三方的UI库了。这里向大家介绍并简单使用来自Lin UI(opens new window) Button组件。

从名字各位读者应该也可以猜到了,是的,Lin UI也是出自林间有风团队的开源项目之一。

通过使用Lin UI提供的各类组件,我们可以轻松、灵活的实现各种优雅美观的页面。要使用Lin UI必须先安装,首先使用任意一个命令行工具,在命令行工具中定位到小程序项目的根目录,执行以下命令:

npm init 
# 接着会有一些交互信息出现,让你填写信息,你可以认真填写,也可以一路回车
1
2

这时候你会发现根目录下生成了一个package.json文件,继续执行命令:

npm install lin-ui
1

执行成功后,会在根目录里生成项目依赖文件夹 node_modules/lin-ui (小程序IDE的目录结构里不会显示此文件夹)。然后用小程序官方IDE打开我们的小程序项目,找到工具选项,点击下拉选中构建npm,等待构建完成即可。这样就算成功把Lin UI安装到我们的项目中了,接着我们要来使用它。在使用之前,我们需要在小程序某个页面的.json文件中引入想要使用的组件,这里我们要在my.wxml页面中使用Button组件,所以在my.json中定义如下:

// /pages/my/my.json

{
  "navigationBarTitleText": "我的",
  "usingComponents": {
    "l-button": "/miniprogram_npm/lin-ui/button/index"
  }
}
1
2
3
4
5
6
7
8

定义完毕之后,让我们到my.wxml中稍作修改,尝试运用这个组件:

<!-- /pages/my/my.wxml -->

<view class="container my-container" hidden="{{!loadingHidden}}">
  <view class="my-header">
    <image src="{{userInfo.avatarUrl}}"></image>
    <!-- 根据授权的状态决定渲染什么内容 -->
    <text wx:if="{{!showAuth}}" class="name">{{userInfo.nickName}}</text>
    <view wx:else class="open-button">
      <!-- 使用Lin UI 的Button组件 -->
      <l-button special open-type="getUserInfo" bind:getuserinfo="getUserInfo">
        {{userInfo.nickName}}
      </l-button>
    </view>
  </view>
  <!-- 地址管理 -->
  <view class="my-address">
    <!-- 省略代码 -->
  </view>
  <view class="my-order">
    <!-- 省略代码 -->
  </view>
</view>
<loading hidden="{{loadingHidden}}">加载中...</loading>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

在wxml文件中,头像部分的显示我们不需要做改动,主要是昵称部分,我们通过逻辑渲染的语法,根据授权状态决定是直接渲染一个文本框显示昵称还是显示Lin UI提供的Button组件:

l-button 区域需要适当增加一个左边距,这里读者自行添加CSS样式即可, 这里作者已经在外层包裹了一个view并指定了样式类名open-button

这里可以看到,通过使用Lin UI的Button组件,除了支持原生的open-type类型使用,通过给组件声明一个special属性,就能实现图中的效果,你完全看不出来是一个按钮,而且你也不需要手动为按钮写任何样式。

Lin UI的Button组件还有更多灵活和强大的用法,有兴趣的读者可以点击查看相关文档(opens new window) 了解。

到这里我们关于获取用户信息的调整适配就已经完成了,总的来说改动的工作量并不大,主要是利用调整后的wx.getUserInfo运行机制特点来控制页面的交互逻辑,当然在一些更复杂的项目中,我们的适配工作可能不会像现在这般简单,但是核心的思路是差不多的,读者可以从这个最基础的示例中加以扩展、举一反三。

# 订阅消息

订阅消息(opens new window) 功能用于替代原有的模板消息(opens new window) 功能。其实这个模板消息功能也是一直很让人诟病的一个功能,比如每次发送都需要通过表单或者支付去收集一个的form_id、非支付类产生的不能重复使用、有效期时间、谁提交的form_id就只能发送给谁等等。这里面最难受的可能就是要收集form_id了,早期看到网上有很多人变着法子来收集。当然以上的问题如果不是业务特别复杂或者有啥奇思妙想的话并不会让你特别纠结,而这一次的订阅消息功能替代原来的模板消息功能我觉得更多是一种对功能定位和使用场景的明确,算是一种重构。接下来我们就要来动手适配一下这个新功能,从文档可以知道,要使用订阅消息功能需要三大步骤:

  1. 微信公众平台(opens new window) 添加一个官方提供的订阅模板或者可以申请添加新模板,审核通过后可使用。添加完模板之后,会有一个模板id,这一步和原来使用模板消息是一致的。

  2. 获取下发权限(wx.requestSubscribeMessage(opens new window) )。这一步其实就是在小程序端弹出一个授权窗,告知用户会有消息推送,是否订阅(这一步等同于原来收集form_id的过程)。

  3. 调用接口下发订阅消息(subscribeMessage.send(opens new window) )。这一步是在服务端做的,也就是我们的零食商贩后端服务里,前面第2步中的用户如果同意订阅,那么这里我们调用发送的时候这个用户在手机上就会收到消息通知。

这里建议读者点击相应步骤的链接看看官方文档的描述再跟着专栏实操或者直接自己实操。

总的来说实现的流程和原来模板消息其实并没有太大的变化,某种意义上来说还简化了,不需要再去费尽心思去收集form_id问题。

这里第1个步骤的过程就不演示了,大家可以自行操作,只需要保证你能拿到一个可以使用的消息模板id即可。

第2步则是需要我们动手写代码了,在《零食商贩》项目中,当后台对订单进行了发货操作时,会给用户发送一条模板消息,在新的订阅消息机制中,我们需要在小程序页面中埋点,在某个时机弹出一个订阅消息的授权窗获取消息下发的权限,按照文档的解释,wx.requestSubscribeMessage仅在点击事件或者支付成功后回调里触发才有效,那么这里我们选择在支付成功后的回调里调用这个小程序API。在IDE中打开pages/order/order.js文件,修改_execPay()中的实现逻辑:


// 省略一堆代码

  /*
  *开始支付
  * params:
  * id - {int}订单id
  */
  _execPay:function(id){
      if(!order.onPay) {
          this.showTips('支付提示','本产品仅用于演示,支付系统已屏蔽',true);//屏蔽支付,提示
          this.deleteProducts(); //将已经下单的商品从购物车删除
          return;
      }
      var that=this;
      order.execPay(id,(statusCode)=>{
          if(statusCode!=0){
              that.deleteProducts(); //将已经下单的商品从购物车删除 
              // 获取下发权限
              wx.requestSubscribeMessage({
                  // 填入模板id,可以传入多个
                  tmplIds: ['asdasd#2312k3'],
                  success: function (res) {
                    //成功
                    console.log(res)
                  },
                  fail(err) {
                    //失败
                    console.error(err);
                  }
              })
              var flag = statusCode == 2;
              wx.navigateTo({
                  url: '../pay-result/pay-result?id=' + id + '&flag=' + flag + '&from=order'
              });
          }
      });
  },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

这里我们在支付完成后的回调函数里增加了wx.requestSubscribeMessage()的调用,这个方法接收一个对象,对象中的tmplIds属性值是一个数组,在这个数组中传入我们在第1步中申请下来的模板Id,即我们想让用户订阅哪个模板的消息就把哪个模板Id传递进去,后面的授权弹窗会根据传入的Id获取一些展示信息给用户看,这里注意根据自己实际情况填写模板Id。

这里的模板Id在企业级开发中一般都是通过发起一个网络请求从后端动态获取的,这里仅做演示功能适配所以直接写死在小程序中。tmplIds可以同时接收多个模板id,让用户一次性授权订阅多个不同的模板。

修改完毕之后,我们可以尝试支付一笔订单,然后在支付完成之后小程序的页面上就会弹出一个订阅消息的授权窗:

点击允许,就代表后面我们可以向这个用户推送订阅消息了。到这里我们第2个步骤的工作就算完毕了,接下来就是最后一步,在服务端实现发送订阅消息。服务端部分的改动同样是非常简单,首先使用IDE打开后端项目根目录下application/api/service/WxMessage.php文件:

<?php
namespace app\api\service;

use think\Exception;

class WxMessage
{
//    private $sendUrl = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=%s";
    // 新的发送消息请求地址
    private $sendUrl = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s";
    private $touser;
    //不让子类控制颜色
    private $color = 'black';
    
    protected $tplID;
    protected $page;
    protected $formID;
    protected $data;
//    protected $emphasisKeyWord;

    function __construct()
    {
        $accessToken = new AccessToken();
        $token = $accessToken->get();
        $this->sendUrl = sprintf($this->sendUrl, $token);
    }
 
    // 开发工具中拉起的微信支付prepay_id是无效的,需要在真机上拉起支付
    protected function sendMessage($openID)
    {

        $data = [
            'touser' => $openID,
            'template_id' => $this->tplID,
            'page' => $this->page,
            // 'form_id' => $this->formID,
            'data' => $this->data,
            // 'color' => $this->color,
            // 'emphasis_keyword' => $this->emphasisKeyWord
        ];
        $result = curl_post($this->sendUrl, $data);
        $result = json_decode($result, true);
        if ($result['errcode'] == 0) {
            return true;
        } else {
            throw new Exception('模板消息发送失败,  ' . $result['errmsg']);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

WxMessage是我们原来模板消息发送的基类,基类里面封装了一些默认的行为,这里我们通过简单修改就可以完成适配订阅消息发送,主要做了两件事——第一修改发送消息的请求地址,改成官方文档中提供的订阅消息调用接口;第二就是注释掉一些已经不需要的请求参数。接着打开同级目录下的DeliveryMessage.php文件:

<?php
namespace app\api\service;


use app\api\model\User;
use app\lib\exception\OrderException;

class DeliveryMessage extends WxMessage
{
    // 订阅消息模板ID
    const DELIVERY_MSG_ID = '订阅消息模板Id';

    public function sendDeliveryMessage($order, $tplJumpPage = '')
    {
        if (!$order) {
            throw new OrderException();
        }
        $this->tplID = self::DELIVERY_MSG_ID;
        $this->formID = $order->prepay_id;
        $this->page = $tplJumpPage;
        $this->prepareMessageData($order);
        // 不需要了
//        $this->emphasisKeyWord='keyword2.DATA';
        return parent::sendMessage($this->getUserOpenID($order->user_id));
    }

    private function prepareMessageData($order)
    {
        $dt = new \DateTime();
        $data = [
            'thing1' => [
                'value' => $order->snap_name,
            ],
            'date2' => [
                'value' => $order->create_time,
            ],
            'thing3' => [
                'value' => '七月全栈新课已上线'
            ],
        ];
        $this->data = $data;
    }

    private function getUserOpenID($uid)
    {
        $user = User::get($uid);
        if (!$user) {
            throw new UserException();
        }
        return $user->openid;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

DeliveryMessage是具体发送模板消息的实现类,这里面有三个改动是必须的:第一个就是填写新的模板Id;第二个就是注释掉已经不需要了的参数;第三个也是最奇葩的一个,我们需要给模板里的字段赋值,这里我们还是在原来的prepareMessageData()方法中实现,要注意的是$data数组里每个元素的键名的命名方式:

这里不得不吐槽官方为什么会想出这种参数的命名方式,很难以理解和调用错误,上图出处点击查看(opens new window)

这里要注意由于作者小程序行业分类的原因,我没有申请到和原来课程中一样的模板,所以这里的$data赋值读者要根据自己实际申请到的模板内容来定义。通过这几处简单的修改,我们就让原来服务端的模板消息功能适配了新的订阅消息功能了。最后我们就要来联调测试一下了,前面作者已经在小程序中点击允许了订阅消息,这里作者再通过调用订单发货接口,接着微信就会收到一条服务通知:

到这里,我们的消息订阅功能适配就全部完成了,整个适配工作涉及前端小程序和服务端的逻辑代码,总的来说适配难度并不大的,唯一的难度可能就是要先仔细阅读官方的文档,搞清楚机(坑)制(点)。

最后更新: 2021-08-12 13:31:59
0/140
评论
0
暂无评论
  • 上一页
  • 首页
  • 1
  • 尾页
  • 下一页
  • 总共1页