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