微信小程序 实现模板消息群发、发送给指定用户

1. 需求

最近在做一款拼课类小程序,大概需求就是分享课程页面给好友,好友参与达到一定数量后则拼课成功。

  • 好友参与后会给分享者发送一条模板消息
  • 参与人数满足后(拼课成功)会给分享者发送一条模板消息
  • 管理后台可以群发模板消息(给所有用户发消息)

按理说很平常的需求,微信公众号里边应该很容易实现,但是想在小程序里边实现这么个功能却有点蛋疼了。

2. 分析

为什么小程序实现起来比较费劲呢,那就要说下小程序发送模板消息的机制了,先看文档怎么说:

划重点,本人交互,也就是说这个模板消息,必须由用户手动来触发,你想后台定时给用户推个消息,洗洗睡吧你。
再来看下面:

这个重点你们自己划吧,发模板消息必须满足这两种情况中的一种,支付就不说了,用户付款后可以推送几条消息,重点是这个表单提交
意思就是我想给用户发个模板消息,第一要搞个表单,第二要让用户来提交这个表单(获取formId),而且这个模板消息还只能发给提交表单的用户本人,你想发给别的用户,呵呵。

献给我们伟大的TX

3. 原理

好了,说多了都是气,既然这样设计,也是有一定道理,但是道理都是讲给守规矩的人听的,至于不守规矩的,喂!说的就是你。
通过上面的分析我们知道,想发送一个基本的模板消息需要以下步骤:

  1. 构建一个form表单
  2. 设置表单的report-submit属性为true(用来获取formId发送模板消息)
  3. 用户提交表单,把openid和formId一块提交给后台(其实真正开发中一般不会提交openid,因为在用户登录或者访问小程序时候通常会把openid和当前用户在数据库中做个同步)
  4. 后台调用POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN来发送模板消息

模板消息接口 POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN有这么几个参数 :


其中touser(openid)form_id是重点,这两个参数的结合是用来确认和效验模板接收者的,因为用户提交表单微信会生成一个专属的formId,这个formId标识着用户的一个操作。所以可以这样来理解,要想发送一个模板消息给特定用户,那么必须要有该用户的有效formId(7天内有效)和openid,一旦我们有了用户大量的formId,你说我发个模板消息那还不跟玩的一样。

所以问题就来了

1. 我如何来收集用户的formId?
这个还没有什么特别有效的办法,因为微信不会给提供相关api,而且只有提交表单才能得到formId,所以只能让用户去主动的触发表单来生成formId,我们要做的就是修改原有的页面,把页面上高强度的交互都用form和button组件来替换,只是在外层套一个form组件而已,里边用button来触发操作(记得修改样式),比如:


像这些交互元素都可以外层套上form组件,用户点击后触发表单提交事件,得到formId,我们把formId和用户openid发送给后台特定接口,后台要做的就是把formId和openid存储下来,至于存数据库、文件、缓存、redis都行,主要是要把openid和formId关系对应好,而且每个formId都有一个过期时间。我是用laravel的redis缓存来存储,毕竟这块是一个高频的io操作。具体实现方式在后面。

2. 搞了一堆用户的formId后,我该怎么来用呢?
其实这个问题是多余的,就像给你了一个女朋友,你却不知道该干啥一样。当然是上...
前面已经说的很清楚了,想要给目标用户发模板消息需要formId和openid,当后台有一个发送模板消息事件被触发时,只需要获取目标用户的openid(这个你们自己数据库肯定有对应的啦),然后根据openid从数据库(或其他存储引擎)拉取一个有效的formId,请求POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN即可,完事了,记得删掉这个formId奥。

4. 实现

前面扯了一堆概念,下面我们来把这个功能具体的实现一遍吧,我这里后台用的是php laravel,原理都一样。

  • 小程序端业务

我这只写一个例子,一看就明白

// wxml
// reprt-submit属性记得写上
<form bindsubmit="clickFormView" report-submit="true" class="form-view">
    <button form-type="submit" class="form-view-button">
        <view>这里边才是我们正常的界面代码</view>
    </button>
</form>
// js
// 这块都可以封装的,毕竟很多交互的地方都需要
clickFormView(event) {
    let formId = event.detail.formId;
    // 忽略开发者工具里边的formId
    if (formId && formId !== 'the formId is a mock one') {
        wx.request({
            method: 'POST',
            url: '/api/collectFormId', // 该接口只用来收集formId
            data: { formId: formId } // 只传了一个formId,因为openid和当前用户通常会事先在后台做一个关联,看具体业务了
        });
    }
    // 然后可以干其他事了,比如跳转页面,其他业务逻辑 
    // TODO
}

有些时候用户操作频繁,可能会导致服务器收到大量请求,所以可以优化下,把formId先存到一个全局变量里边(数组),当达到一定数量后统一发给后台来保存。这块可以灵活运用。

  • 服务端实现

服务端的实现也就两个功能,收集发送
假设我们现在有这么一个类FormIdCollection,可以收集(save)和获取(get)某个openid的formId,那我们给前台暴露的api只需要简单的调用下就可以了,至于发消息,也只需要get一个formId,即可。

// 实例化一个对象
// $openid为目标用户openid
// $config是一个数组,微信小程序相关配置[app_id, secret]
$collection = new FormIdCollection($openid, $config);

// 收集一个formId
$collecton->save($formId);

//获取一个可用formId
$collection->get();

// 发送模板消息
// $data为模板消息相关参数 template_id等
$collecton->send($data);

下面是FormIdCollection类的一个具体实现,基于laravel(说实话,挺好用的),另外引入了一个微信开发包overtrue/wechat(这里主要是用来发模板消息、有点大材小用了),https://www.easywechat.com/

<?php

use Illuminate\Support\Facades\Cache;
use EasyWeChat\Factory;

class FormIdCollection
{
    private $openid;
    private $config;
    private $cache;
    private $cacheKey;

    public function __construct($openid, $config = []) 
    {
        $this->openid = $openid;
        $this->config = $config;
        $this->cache = Cache::store('redis');   // 用redis作为缓存驱动,记得要配置redis环境奥
        $this->cacheKey = $this->getCacheKey(); // 每个openid对应一个key
    }

    /**
     * 获取缓存key
     * 
     */
    public function getCacheKey() 
    {
        return 'mini_program_form_id_'.$this->openid;
    }
    
    /**
     * 发送模板消息
     * 
     * @param $data 模板消息参数
     */
    public function send($data)
    {
        $mina = Factory::miniProgram([
            'app_id' => $this->config['app_id'],
            'secret' => $this->config['secret'],
        ]);
        // 获取一个可用的formId,然后删除掉
        $formId = $this->get(true);
        
        if (!$formId) {
            throw new \Exception('no formId');
        } else {
            $data['touser'] = $this->openid;
            $data['form_id'] = $formId;
            
            // 用overtrue/wechat包来发送模板消息
            $res = $mina->template_message->send($data);
            return $res;
        }
    }
    
    /**
     * 存储formId
     * 
     * @param $formId
     */
    public function save($formId) 
    {
        $formIds = $this->gets();
        $formIds->push([
            'form_id' => $formId,
            'expire' => time() + 60 * 7 * 24 // formId过期时间
        ]);
        // 存储到redis缓存中
        $this->cache->forever($this->cacheKey, $formIds->toArray());
    }

    /**
     * 获取某个未过期的formId
     *
     * @param $delete 获取之后是否立即删除
     */
    public function get($delete = false) 
    {
        $formIds = $this->gets();
        if (!$formIds->count()) {
            return false;
        }
        // 筛选一个有效的formId,优先获取快过期的
        $formId = $formIds->where('expire', '>=', time())->sortBy('expire')->first()['form_id'];
        if ($delete && $formId) {
            $this->delete($formId);
        }
        return $formId;
    }

    /**
     * 获取formId集合
     * 
     * @return \Illuminate\Support\Collection
     */
    public function gets() 
    {
        $formIds = $this->cache->get($this->cacheKey);
        return collect($formIds ? $formIds : []);
    }

    /**
     * 删除某个formId
     * 
     * @param $formId
     */
    public function delete($formId) 
    {
        $formIds = $this->gets();
        $formIds = $formIds->filter(function($item) use($formId) {
            return $item['form_id'] != $formId;
        });
        $this->cache->forever($this->cacheKey, $formIds->toArray());
    }

    /**
     * 清理所有已过期的formId
     * 
     */
    public function clearExpireFormIds() 
    {
        $formIds = $this->gets();
        $time = time();
        $formIds = $formIds->filter(function($item) use($time) {
            return $item['expire'] > $time;
        });
        $this->cache->forever($this->cacheKey, $formIds->toArray());
    }
}

我已经封装了一个laravel扩展包,感兴趣的朋友可以上github上看下https://github.com/laravuel/laravel-wfc
至于非框架的php实现,小伙伴可以帮忙弄下。

觉得讲的不错的小伙伴可以点波关注奥~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容