小程序消息推送怎么做?模板消息开发全解析

小程序消息推送怎么做?模板消息开发全解析

在移动互联网的下半场,用户留存比拉新更重要。对于微信小程序而言,如何在不打扰用户的前提下,将关键信息(如订单状态、审核结果、物流进度)精准触达用户,是提升用户体验和复购率的关键。

虽然微信官方已逐步用“订阅消息”取代了传统的“模板消息”,但其核心逻辑——基于模板ID、通过后端服务向指定用户发送结构化数据——依然未变。本文将基于 ThinkPHP 5 (TP5) 框架,带你从零构建一套稳定、高效的小程序消息推送服务。


一、 核心原理与前置准备

在写代码之前,我们需要理清消息推送的三个核心要素:

  1. Access Token:接口调用的凭证,由 AppID 和 AppSecret 换取,有效期2小时,需缓存。
  2. Template ID:在微信公众平台后台配置的模板唯一标识。
  3. OpenID:用户的唯一标识,必须通过用户授权登录获取。
注意:自2020年起,微信逐步下线模板消息,转为订阅消息。本文以 TP5 实现通用推送逻辑为主,代码结构兼容两者,仅需调整 API 地址和参数结构即可。

1.1 公众平台配置

  1. 登录 微信公众平台
  2. 进入“功能” -> “订阅消息”或“模板消息”。
  3. 选用或创建模板,获取 template_id
  4. 记录 AppID 和 AppSecret(位于“开发” -> “开发设置”)。

二、 ThinkPHP 5 项目架构设计

为了保持代码的整洁与可维护性,我们不建议将微信逻辑散落在 Controller 中。建议采用以下结构:

application/
├── common/
│   └── service/
│       └── WechatService.php  # 微信核心服务类
├── index/
│   ├── controller/
│   │   └── Message.php        # 消息推送控制器
│   └── model/
│       └── User.php           # 用户模型
└── config/
    └── wechat.php             # 微信配置文件

2.1 配置文件 (config/wechat.php)

将敏感配置独立出来,便于多环境管理。

<?php
return [
    'app_id'     => 'wx1234567890abcdef',
    'app_secret' => 'your_app_secret_here',
    // 模板ID映射,方便业务调用
    'templates'  => [
        'order_pay'    => 'TEMPLATE_ID_FOR_ORDER_PAY',
        'ship_notice'  => 'TEMPLATE_ID_FOR_SHIP_NOTICE',
        'audit_result' => 'TEMPLATE_ID_FOR_AUDIT_RESULT',
    ],
    // Redis缓存前缀
    'cache_prefix' => 'wechat_',
];

三、 核心服务层开发

这是本文的重点。我们将所有微信交互逻辑封装在 WechatService 中,利用 TP5 的缓存机制解决 Access Token 过期问题。

3.1 安装 HTTP 客户端

TP5 本身没有内置强大的 HTTP 客户端,建议使用 guzzlehttp/guzzle 或 TP5 自带的 curl 封装。这里为了演示清晰,使用原生 curl 封装,不依赖额外扩展。

3.2 编写 WechatService.php

<?php
namespace app\common\service;

use think\Cache;
use think\Config;

class WechatService
{
    private $appId;
    private $appSecret;
    
    public function __construct()
    {
        $this->appId     = Config::get('wechat.app_id');
        $this->appSecret = Config::get('wechat.app_secret');
    }

    /**
     * 获取 Access Token (带缓存)
     * @return string
     */
    public function getAccessToken()
    {
        $cacheKey = Config::get('wechat.cache_prefix') . 'access_token';
        $token = Cache::get($cacheKey);

        if ($token) {
            return $token;
        }

        $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$this->appId}&secret={$this->appSecret}";
        $res = $this->httpGet($url);
        $result = json_decode($res, true);

        if (isset($result['access_token'])) {
            // 微信默认有效期7200秒,我们缓存7000秒以确保安全
            Cache::set($cacheKey, $result['access_token'], 7000);
            return $result['access_token'];
        }

        throw new \Exception("获取AccessToken失败: " . ($result['errmsg'] ?? '未知错误'));
    }

    /**
     * 发送订阅消息/模板消息
     * @param string $openid 用户openid
     * @param string $templateId 模板ID
     * @param array $data 模板数据
     * @param string $page 点击消息跳转页面
     * @return array
     */
    public function sendSubscribeMessage($openid, $templateId, $data, $page = '')
    {
        $accessToken = $this->getAccessToken();
        
        // 订阅消息接口地址 (如果是旧版模板消息,地址不同,参数结构也略有差异)
        $url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={$accessToken}";

        $postData = [
            'touser'      => $openid,
            'template_id' => $templateId,
            'page'        => $page,
            'data'        => $data,
            // 'miniprogram_state' => 'formal', // 可选: developer/trial/formal
        ];

        $res = $this->httpPost($url, json_encode($postData, JSON_UNESCAPED_UNICODE));
        $result = json_decode($res, true);

        if ($result['errcode'] != 0) {
            // 记录日志,方便排查
            \think\Log::error("微信消息推送失败: openid={$openid}, errcode={$result['errcode']}, errmsg={$result['errmsg']}");
            return ['status' => 0, 'msg' => $result['errmsg']];
        }

        return ['status' => 1, 'msg' => '发送成功'];
    }

    /**
     * GET 请求
     */
    private function httpGet($url)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        $output = curl_exec($ch);
        curl_close($ch);
        return $output;
    }

    /**
     * POST 请求
     */
    private function httpPost($url, $data)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        // 设置Header
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
        $output = curl_exec($ch);
        curl_close($ch);
        return $output;
    }
}

四、 业务场景实战:订单支付成功通知

假设用户支付成功后,我们需要推送一条“支付成功通知”。

4.1 控制器调用 (index/controller/Message.php)

<?php
namespace app\index\controller;

use think\Controller;
use app\common\service\WechatService;
use think\Config;

class Message extends Controller
{
    /**
     * 模拟订单支付成功回调触发推送
     */
    public function notifyOrderPay()
    {
        // 1. 获取参数 (实际场景中应从订单表或支付回调中获取)
        $openid = input('post.openid');
        $orderId = input('post.order_id');
        
        if (!$openid || !$orderId) {
            return json(['code' => 400, 'msg' => '参数缺失']);
        }

        // 2. 准备模板数据
        // 注意:订阅消息的数据格式为 { "thing1": { "value": "xxx" } }
        // 具体字段名(thing1, date2等)取决于你在公众平台创建的模板详情
        $templateData = [
            'character_string1' => ['value' => $orderId], // 订单编号
            'amount2'           => ['value' => '99.00元'], // 支付金额
            'time3'             => ['value' => date('Y-m-d H:i:s')], // 支付时间
            'thing4'            => ['value' => '微信支付'], // 支付方式
        ];

        // 3. 获取对应的 Template ID
        $templateId = Config::get('wechat.templates.order_pay');

        // 4. 调用服务发送
        try {
            $wechatService = new WechatService();
            $result = $wechatService->sendSubscribeMessage(
                $openid, 
                $templateId, 
                $templateData, 
                '/pages/order/detail?id=' . $orderId // 点击跳转路径
            );

            if ($result['status'] == 1) {
                return json(['code' => 200, 'msg' => '推送成功']);
            } else {
                return json(['code' => 500, 'msg' => '推送失败: ' . $result['msg']]);
            }

        } catch (\Exception $e) {
            return json(['code' => 500, 'msg' => $e->getMessage()]);
        }
    }
}

五、 关键难点与避坑指南

在实际开发中,很多开发者会遇到“推送失败”或“收不到消息”的问题,以下是常见坑点:

5.1 订阅消息 vs 模板消息

  • 模板消息(旧):无需用户每次授权,但有行业限制,且已逐步停用。
  • 订阅消息(新)

    • 一次性订阅:用户每点击一次授权按钮,只能发送一条消息。适合支付成功、审核结果等低频场景。
    • 长期订阅:仅对政务、医疗、交通等特定行业开放。
    • 关键点:前端必须先调用 wx.requestSubscribeMessage 接口,用户点击“允许”后,后端才能发送。否则接口会返回 43101 (用户拒绝接受消息)。

5.2 数据格式严格匹配

微信对模板数据的长度和内容有严格限制:

  • thing (事物): 不超过20个字符。
  • character_string (字符串): 不超过32个字符。
  • number (数字): 不超过32位。
  • time (时间): 格式需符合 ISO 8601 或特定要求。
  • 建议:在后端发送前,对数据进行 mb_substr 截断处理,避免因超长导致发送失败。

5.3 Access Token 并发问题

在高并发场景下,多个请求同时发现 Token 过期,会同时去微信服务器请求新 Token,可能导致冲突。

  • 解决方案:使用 Redis 的 setnx 或文件锁机制,确保同一时间只有一个进程去刷新 Token。上述代码中的 Cache::set 在 TP5 默认文件缓存下基本够用,高并发建议切换为 Redis 驱动。

六、 总结

基于 ThinkPHP 5 实现小程序消息推送,核心在于分层架构缓存策略

  1. 封装服务层:将 getAccessTokensendMessage 封装在 Service 层,避免代码重复。
  2. 合理缓存:利用 TP5 的 Cache 机制管理 Token,减少 API 调用次数。
  3. 遵循规范:严格遵守微信订阅消息的数据格式和用户授权流程。

通过这套方案,你可以轻松集成订单通知、物流提醒、审核结果等多种业务场景,有效提升小程序的用户活跃度和转化率。

温馨提示:代码仅为核心逻辑演示,生产环境请补充完善的异常处理、日志记录以及前端授权逻辑配合。

0 条评论

还没有人发表评论

发表评论 取消回复

记住我的信息,方便下次评论
有人回复时邮件通知我