caipan_shop_admin/app/service/wx/WechatApplets.php

332 lines
14 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace app\service\wx;
use app\exception\RepositoryException;
use app\model\SubscribeMessageLog;
use EasyWeChat\Factory;
use EasyWeChat\Kernel\Exceptions\HttpException;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
use EasyWeChat\Kernel\Exceptions\RuntimeException;
use EasyWeChat\MiniProgram\Application;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use think\facade\Config;
use think\facade\Log;
use think\Model;
/**
* 微信小程序
* Class WechatApplets
* @package app\service\wx
*/
class WechatApplets
{
private static $app = null;
/**
* TODO 正式上线时需要替换模板配置信息和相关小程序配置信息
*/
// 订阅消息模板:预约通知
public const SUBSCRIBE_TPL_APPOINTMENT = 'uvGd7RqaegheGU-uVxR-uM3y2MadZeMOHdQaNiiWm8U';
// 订阅消息模版:新活动通知
public const SUBSCRIBE_TPL_ACTIVITY = 'd0efR-Ga27c6eIvx9mAwJcnAqzhM_Sq68XiFvjvlBJM';
// 未读消息通知:自然流量待分配通知管理员、新顾客通知员工、新客户通知客服 后台分配客服、邀请的新客户绑定手机号通知相应客服
public const SUBSCRIBE_TPL_NEW_INFO_NOTICE = 'eyxvInLLF3L_wmcSQc_O7XLKF7RoGK1dM3OwKj5fHio';
private function __construct()
{
}
private function __clone()
{
}
//微信小程序实例 单例模式
public static function getInstance(): ?Application
{
if (self::$app == null) {
Config::load('extra/wechat', 'wechat');
$conf = config('wechat');
$config = [
// 必要配置
'app_id' => $conf['applets_appId'],
'secret' => $conf['applets_appSecret'],
// 返回数据类型 array | xml
'response_type' => 'array',
];
self::$app = Factory::miniProgram($config);
}
return self::$app;
}
/**
* 生成微信小程序链接
*
* @param string $path
* @param string $sourceCode
* @param bool $tokenRefresh 是否强制刷新access_token 默认false 非特殊条件请勿设为true
* @return string
* @throws GuzzleException
* @throws HttpException
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws RuntimeException
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public static function generateActivityUrl(string $path, string $sourceCode, bool $tokenRefresh = false): string
{
$accessToken = self::getInstance()->access_token;
$client = new Client();
$url = 'https://api.weixin.qq.com/wxa/generate_urllink?access_token=';
$params = [];
$query = '';
$params['path'] = 'pages/tabbar/pagehome/pagehome';//首页路径
if (!empty($path)) {
$pathArr = explode('?', $path);
$query = isset($pathArr[1]) ? $pathArr[1].'&' : '';
$params['path'] = $pathArr[0];
}
$params['query'] = $query.'channel=activity&source_code='.$sourceCode;
$jsonStr = !empty($params) ? json_encode($params, JSON_UNESCAPED_UNICODE) : '';
Log::info('【小程序链接生成请求参数】'.$jsonStr);
$response = $client->request('POST', $url.($accessToken->getToken($tokenRefresh)['access_token'] ?? ''), [
'body' => $jsonStr
]);
$res = json_decode($response->getBody(), true);
Log::info('【小程序链接生成响应】'.json_encode($res, JSON_UNESCAPED_UNICODE));
if ($res['errcode'] == 0) {
return $res['url_link'] ?? '';
}
if ($res['errcode'] == 40001 && $tokenRefresh == false) {
// 可能是token过期 刷新一次
// tokenRefresh 防止无限循环
return self::generateActivityUrl($path, $sourceCode, true);
}
throw new Exception('链接生成失败 '.$res['errmsg'] ?? '');
}
/**
* 发送订阅消息
*
* 注1订阅消息需要在用户预约后订阅成功后才能发送每次订阅只能推送一条订阅消息;
* 注2用户取消订阅后无法下发订阅消息
*
* @param string $openid 接收者openid
* @param array $data ['template_id' => '模版ID', 'msg' => msgBody, 'page' => '小程序跳转页面', 'state' => '小程序类型']
* 消息体 样例msgBody=['date1'=>date('Y年m月d日'), 'thing2'=>'植发', 'character_string12'=>'8:30 ~ 9:30']
* state 跳转小程序类型。 developer为开发版trial=体验版formal为正式版默认为正式版
* @return boolean
* @throws RepositoryException|GuzzleException
*/
public static function sendSubscribeMessage(string $openid, array $data): bool
{
try {
$wxApp = self::getInstance();
$msgBody = $data['msg'] ?? [];
$templateId = $data['template_id'] ?? '';
$page = $data['page'] ?? '';
$state = $data['state'] ?? 'formal';
$state = in_array($state, ['developer', 'trial', 'formal']) ? $state : '';
if (empty($msgBody)) {
throw new RepositoryException('订阅消息不能为空');
}
if (empty($templateId)) {
throw new RepositoryException('消息模版不能为空');
}
$data = [
// 所需下发的订阅模板id
'template_id' => $templateId,
// 接收者(用户)的 openid
'touser' => $openid,
// 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,示例index?foo=bar。该字段不填则模板无跳转。
'page' => $page,
//跳转小程序类型developer=开发版trial=体验版formal=正式版;默认为正式版
'miniprogram_state' => $state,
// 模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } }
'data' => $msgBody
];
Log::info('[发送订阅消息]'.json_encode($data, JSON_UNESCAPED_UNICODE));
$res = $wxApp->subscribe_message->send($data);
Log::info('[发送回执]'.json_encode($res, JSON_UNESCAPED_UNICODE));
if (isset($res['errcode']) && $res['errcode'] == 0) {
return true;
}
//40003 touser字段openid为空或者不正确
//40037 订阅模板id为空不正确
//43101 用户拒绝接受消息,如果用户之前曾经订阅过,则表示用户取消了订阅关系
//47003 模板参数不准确可能为空或者不满足规则errmsg会提示具体是哪个字段出错
//41030 page路径不正确需要保证在现网版本小程序中存在与app.json保持一致
if ($res['errcode'] == '43101') {
throw new RepositoryException('用户未订阅或订阅次数已用完');
}
throw new RepositoryException('【订阅消息发送失败】code:'.$res['errcode'].' msg:'.$res['errmsg']);
} catch (RepositoryException $e) {
throw $e;
} catch (Exception $e) {
Log::error('【订阅消息发送失败】:'.$e->getMessage());
return false;
}
}
/**
* 批量发送订阅消息
*
* 注1订阅消息需要在用户预约后订阅成功后才能发送每次订阅只能推送一条订阅消息;
* 注2用户取消订阅后无法下发订阅消息
*
* @param array $openidList 接收者openid列表
* @param array $data ['template_id' => '模版ID', 'msg' => msgBody, 'page' => '小程序跳转页面', 'state' => '小程序类型', 'message_id' => '消息ID']
* 消息体 样例msgBody=['date1'=>date('Y年m月d日'), 'thing2'=>'植发', 'character_string12'=>'8:30 ~ 9:30']
* state 跳转小程序类型。 developer为开发版trial=体验版formal为正式版默认为正式版
* @throws GuzzleException
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws RepositoryException
*/
public static function sendBatchSubscribeMessage(int $messageId, array $openidList, array $data)
{
try {
$wxApp = self::getInstance();
$msgBody = $data['msg'] ?? [];
$templateId = $data['template_id'] ?? '';
$page = $data['page'] ?? '';
$state = $data['state'] ?? 'formal';
$state = in_array($state, ['developer', 'trial', 'formal']) ? $state : '';
if (empty($msgBody)) {
throw new RepositoryException('订阅消息不能为空');
}
if (empty($templateId)) {
throw new RepositoryException('消息模版不能为空');
}
if (empty($openidList)) {
throw new RepositoryException('消息模版接受人列表不能为空');
}
$resData = [];
$now = date('Y-m-d H:i:s');
$resInfo = [
'openid' => '',
'status' => SubscribeMessageLog::STATUS_SUCCESS,
'message_id' => $messageId,
'template_id' => $templateId,
'created_at' => $now,
'data' => json_encode($msgBody, JSON_UNESCAPED_UNICODE),
];
foreach ($openidList as $openid) {
$resInfo['openid'] = $openid;
$sendData = [
// 所需下发的订阅模板id
'template_id' => $templateId,
// 接收者(用户)的 openid
'touser' => $openid,
// 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,示例index?foo=bar。该字段不填则模板无跳转。
'page' => $page,
//跳转小程序类型developer=开发版trial=体验版formal=正式版;默认为正式版
'miniprogram_state' => $state,
// 模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } }
'data' => $msgBody
];
Log::info('[发送订阅消息]'.json_encode($data, JSON_UNESCAPED_UNICODE));
$res = $wxApp->subscribe_message->send($sendData);
Log::info('[发送回执]'.json_encode($res, JSON_UNESCAPED_UNICODE));
if (isset($res['errcode']) && $res['errcode'] == 0) {
$resData[] = $resInfo;
continue;
}
$resInfo['status'] = SubscribeMessageLog::STATUS_FAIL;
//40003 touser字段openid为空或者不正确
//40037 订阅模板id为空不正确
//43101 用户拒绝接受消息,如果用户之前曾经订阅过,则表示用户取消了订阅关系
//47003 模板参数不准确可能为空或者不满足规则errmsg会提示具体是哪个字段出错
//41030 page路径不正确需要保证在现网版本小程序中存在与app.json保持一致
if ($res['errcode'] == '43101') {
$resInfo['remarks'] = '用户未订阅或订阅次数已用完';
$resData[] = $resInfo;
continue;
}
$resInfo['remarks'] = $res['errcode'].' msg:'.$res['errmsg'];
$resData[] = $resInfo;
(new SubscribeMessageLog())->saveAll($resData);
throw new RepositoryException('【订阅消息发送失败】code:'.$res['errcode'].' msg:'.$res['errmsg']);
}
(new SubscribeMessageLog())->saveAll($resData);
} catch (RepositoryException $e) {
throw $e;
} catch (Exception $e) {
Log::error('【订阅消息发送失败】:'.$e->getMessage());
throw $e;
}
}
// 订阅消息模版
public static function msgTemplateList(): array
{
return [
['name' => '新活动通知', 'value' => self::SUBSCRIBE_TPL_ACTIVITY],
['name' => '未读消息通知', 'value' => self::SUBSCRIBE_TPL_NEW_INFO_NOTICE],
];
}
// 订阅消息参数列表
public static function msgTemplateParams(): array
{
return [
self::SUBSCRIBE_TPL_ACTIVITY => [
[
'name' => '活动名称',
'type' => 'string',
'value' => 'thing1',
],
[
'name' => '活动时间',
'type' => 'datetime',
'value' => 'time4',
],
[
'name' => '温馨提示',
'type' => 'string',
'value' => 'thing5',
],
],
self::SUBSCRIBE_TPL_NEW_INFO_NOTICE => [
[
'name' => '消息内容',
'type' => 'string',
'value' => 'thing2',
],
[
'name' => '时间',
'type' => 'datetime',
'value' => 'time3',
],
[
'name' => '备注',
'type' => 'string',
'value' => 'thing5',
],
],
];
}
}