332 lines
14 KiB
PHP
Executable File
332 lines
14 KiB
PHP
Executable File
<?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',
|
||
],
|
||
],
|
||
];
|
||
}
|
||
} |