<?php
/**
 * tpshop
 * ============================================================================
 * 版权所有 2015-2027 深圳搜豹网络科技有限公司，并保留所有权利。
 * 网站地址: http://www.tp-shop.cn
 * ----------------------------------------------------------------------------
 * 这不是一个自由软件！您只能在不用于商业目的的前提下对程序代码进行修改和使用 .
 * 不允许对程序代码以任何形式任何目的的再发布。
 * 如果商业用途务必到官方购买正版授权, 以免引起不必要的法律纠纷.
 * ============================================================================
 * @author lhb
 */

namespace app\common\logic\saas\wechat;

use app\common\model\saas\Wx3rd;
use app\common\util\XML;

/**
 * 微信小程序第三方平台操作类
 * 单例模式
 */
class Wx3rdPlatform extends WxCommon
{
    static private $install = null; //实例
    private $config = []; //第三方平台配置

    //授权事件类型
    const AUTH_EVENT_ALL = 0; //全局事件
    const AUTH_EVENT_VERIFY_TICKET = 1; //开放平台的ticket推送通知,微信会10分钟推一次
    const AUTH_EVENT_UNAUTHORIZED = 2; //取消授权事件
    const AUTH_EVENT_AUTHORIZED = 3; //授权事件
    const AUTH_EVENT_UPDATE_AUTHORIZED = 4; //更新授权事件

    //普通事件类型
    const COMMON_EVENT_ALL = 0; //全局事件
    const COMMON_EVENT_WEAPP_AUDIT_SUCCESS = 1; //小程序审核成功事件
    const COMMON_EVENT_WEAPP_AUDIT_FAIL = 2; //小程序审核失败事件


    private function __construct($config = null)
    {
        if (!$config && !$this->config) {
            $config = Wx3rd::get(1);
        }
        $this->config = $config ?: $this->config;
    }
    
    /**
     * 获取实例
     * @param array $config 如果为空，则自动获取
     * @return self
     */
    static public function getInstance($config = null)
    {
        if (!self::$install) {
            self::$install = new self($config);
        }
        return self::$install;
    }
    
    public function getConfig()
    {
        return $this->config;
    }
    
    public function setConfig($config)
    {
        $this->config = $config;
    }
    
    /**
     * 获取第三方平台令牌（组件方令牌）
     * 详见：https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=&lang=zh_CN
     * @return string
     */
    public function getComponentAccessToken()
    {
        $config = $this->config;
        if (empty($config)) {
            $this->setError("第三方平台信息未配置");
            return false;
        }

        //判断是否过了缓存期
        if ($config['access_token'] && $config['access_token_expires'] > time()) {
           return $config['access_token'];
        }
        
        $post = $this->toJson([
            'component_appid'       => $config['appid'] ,
            'component_appsecret'   => $config['appsecret'], 
            'component_verify_ticket' => $config['verify_ticket']
        ]);
        $url = "https://api.weixin.qq.com/cgi-bin/component/api_component_token";
        $return = $this->requestAndCheck($url, 'POST', $post);
        if ($return === false) {
            $this->config['access_token_expires'] = 0;
            Wx3rd::update(['access_token_expires' => 0], ['id' => $config['id']]);//token错误
            return false;
        }
        
        $expires = time() + $return['expires_in'] - 200; // 提前200秒过期
        Wx3rd::update([
            'access_token' => $return['component_access_token'],
            'access_token_expires'=> $expires
        ], ['id' => $config['id']]);
        $this->config['access_token'] = $return['component_access_token'];
        $this->config['access_token_expires'] = $expires;
        
        return $return['component_access_token'];
    }
    
    /**
     * 获取预授权码pre_auth_code
     * 详见：https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=&lang=zh_CN
     * @return mixed 预授权码
     */
    public function getPreAuthCode()
    {
        $accessToken = $this->getComponentAccessToken();
        if (!$accessToken) {
            return false;
        }
        
        $url ="https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token={$accessToken}";        
        $post = $this->toJson([
            'component_appid' => $this->config['appid'],
        ]);
        
        $wxdata = $this->requestAndCheck($url, 'POST', $post);
        if ($wxdata === false) {
            return false;
        }
        
        return $wxdata['pre_auth_code'];
    }
    
    /**
     * 使用授权码换取公众号或小程序的接口调用凭据和授权信息
     * 详见：https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=&lang=zh_CN
     * @param string $authCode 授权码
     * @return mixed 授权数组
     */
    public function getAuthInfo($authCode)
    {
        $accessToken = $this->getComponentAccessToken();
        if (!$accessToken) {
            return false;
        }
        
        $url ="https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token={$accessToken}";        
        $post = $this->toJson([
            'component_appid'    => $this->config['appid'],
            'authorization_code' => $authCode
        ]);
        
        $wxdata = $this->requestAndCheck($url, 'POST', $post);
        if ($wxdata === false) {
            return false;
        }
        
        //返回数据结构
        //{ 
        //    "authorization_info": {
        //        "authorizer_appid": "wxf8b4f85f3a794e77", 
        //        "authorizer_access_token": "QXjUqNqfYVH0yBE1iI_7vuN_9gQbpjfK7hYwJ3P7xOa88a89-Aga5x1NMYJyB8G2yKt1KCl0nPC3W9GJzw0Zzq_dBxc8pxIGUNi_bFes0qM", 
        //        "expires_in": 7200, 
        //        "authorizer_refresh_token": "dTo-YCXPL4llX-u1W1pPpnp8Hgm4wpJtlR6iV0doKdY", 
        //        "func_info": [
        //            {
        //                "funcscope_category": {
        //                    "id": 1
        //                }
        //            }
        //        ]
        //    }
        //}
        return $wxdata['authorization_info'];
    }

    /**
     * 获取（刷新）授权公众号或小程序的接口调用凭据（令牌）
     * 详见：https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=&lang=zh_CN
     * @param string $authorizerAppid  授权方appid
     * @param string $refreshTokenValue 授权方的刷新令牌
     * @return mixed 授权数组
     */
    public function getAuthorizerToken($authorizerAppid, $refreshTokenValue)
    {
        $accessToken = $this->getComponentAccessToken();
        if (!$accessToken) {
            return false;
        }
        
        $url ="https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token={$accessToken}";        
        $post = $this->toJson([
            'component_appid'   => $this->config['appid'],
            'authorizer_appid'  => $authorizerAppid,
            'authorizer_refresh_token' => $refreshTokenValue,
        ]);
        
        $wxdata = $this->requestAndCheck($url, 'POST', $post);
        if ($wxdata === false) {
            return false;
        }
        
        //返回数据结构
        //{
        //    "authorizer_access_token": "aaUl5s6kAByLwgV0BhXNuIFFUqfrR8vTATsoSHukcIGqJgrc4KmMJ-JlKoC_-NKCLBvuU1cWPv4vDcLN8Z0pn5I45mpATruU0b51hzeT1f8", 
        //    "expires_in": 7200, 
        //    "authorizer_refresh_token": "BstnRqgTJBXb9N2aJq6L5hzfJwP406tpfahQeLNxX0w"
        //}
        return $wxdata;
    }

    /**
     * 获取授权方的帐号基本信息
     * @param string $authorizerAppid 授权公众号或小程序的appid
     * @return mixed
     */
    public function getAuthorizerInfo($authorizerAppid)
    {
        $accessToken = $this->getComponentAccessToken();
        if (!$accessToken) {
            return false;
        }
        
        $url ="https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token={$accessToken}";        
        $post = $this->toJson([
            'component_appid'   => $this->config['appid'],
            'authorizer_appid'  => $authorizerAppid,
        ]);
        
        $wxdata = $this->requestAndCheck($url, 'POST', $post);
        if ($wxdata === false) {
            return false;
        }
        
        //返回数据结构（小程序）
        //{
        //    "authorizer_info": {
        //        "nick_name": "微信SDK Demo Special", 
        //        "head_img": "http://wx.qlogo.cn/mmopen/GPy", 
        //        "service_type_info": { "id": 2 }, 
        //        "verify_type_info": { "id": 0 },
        //        "user_name":"gh_eb5e3a772040",
        //        "principal_name":"腾讯计算机系统有限公司",
        //        "business_info": {"open_store": 0, "open_scan": 0, "open_pay": 0, "open_card": 0, "open_shake": 0},
        //        "qrcode_url":"URL",
        //        "signature": "时间的水缓缓流去",
        //        "MiniProgramInfo": {
        //            "network": {
        //                "RequestDomain":["https://www.qq.com","https://www.qq.com"],
        //                "WsRequestDomain":["wss://www.qq.com","wss://www.qq.com"],
        //                "UploadDomain":["https://www.qq.com","https://www.qq.com"],
        //                "DownloadDomain":["https://www.qq.com","https://www.qq.com"],
        //            },
        //            "categories":[{"first":"资讯","second":"文娱"},{"first":"工具","second":"天气"}],
        //            "visit_status": 0,
        //        }
        //    },
        //    "authorization_info": {
        //        "appid": "wxf8b4f85f3a794e77", 
        //        "func_info": [
        //            { "funcscope_category": { "id": 17 } }, 
        //        ]
        //    }
        //}
        
        //返回数据结构（公众号）
        //{
        //    "authorizer_info": {
        //        "nick_name": "微信SDK Demo Special", 
        //        "head_img": "http://wx.qlogo.cn/mmopen/GPy", 
        //        "service_type_info": { "id": 2 }, 
        //        "verify_type_info": { "id": 0 },
        //        "user_name":"gh_eb5e3a772040",
        //        "principal_name":"腾讯计算机系统有限公司",
        //        "business_info": {"open_store": 0, "open_scan": 0, "open_pay": 0, "open_card": 0, "open_shake": 0},
        //        "alias":"paytest01"
        //        "qrcode_url":"URL",
        //    },     
        //    "authorization_info": {
        //        "appid": "wxf8b4f85f3a794e77", 
        //        "func_info": [
        //            { "funcscope_category": { "id": 1 } }, 
        //        ]
        //    }
        //}
        return $wxdata;
    }

    /**
     * 获取授权方的选项设置信息
     * 详见：https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=&lang=zh_CN
     * @param string $authorizerAppid 授权公众号或小程序的appid
     * @param string $optionName 选项名称
     * @return boolean
     */
    public function getAuthorizerOption($authorizerAppid, $optionName)
    {
        $accessToken = $this->getComponentAccessToken();
        if (!$accessToken) {
            return false;
        }
        
        $url ="https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token={$accessToken}";        
        $post = $this->toJson([
            'component_appid'   => $this->config['appid'],
            'authorizer_appid'  => $authorizerAppid,
            "option_name"       => $optionName
        ]);
        
        $wxdata = $this->requestAndCheck($url, 'POST', $post);
        if ($wxdata === false) {
            return false;
        }
        
        //返回数据结构
        //{
        //    "authorizer_appid":"wx7bc5ba58cabd00f4",
        //    "option_name":"voice_recognize", //选项名称
        //    "option_value":"1" //选项值
        //}
        return $wxdata;
    }
    
    /**
     * 设置授权方的选项信息
     * 详见：https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=&lang=zh_CN
     * @param string $authorizerAppid 授权公众号或小程序的appid
     * @param string $optionName 选项名称
     * @param string $optionValue 设置的选项值
     * @return boolean
     */
    public function setAuthorizerOption($authorizerAppid, $optionName, $optionValue)
    {
        $accessToken = $this->getComponentAccessToken();
        if (!$accessToken) {
            return false;
        }
        
        $url ="https://api.weixin.qq.com/cgi-bin/component/api_set_authorizer_option?component_access_token={$accessToken}";        
        $post = $this->toJson([
            'component_appid'   => $this->config['appid'],
            'authorizer_appid'  => $authorizerAppid,
            "option_name"       => $optionName,
            "option_value"      => $optionValue
        ]);
        
        $wxdata = $this->requestAndCheck($url, 'POST', $post);
        return $wxdata !== false;
    }
    
    /**
     * 获取授权的页面url
     * @return string
     */
    public function getAuthUrl()
    {
        $preAuthCode = $this->getPreAuthCode();
        if ($preAuthCode === false) {
            return false;
        }
        //回调url
        $redirectUri = SITE_URL.'/admin/wx3rd/authorization';
        $url = 'https://mp.weixin.qq.com/cgi-bin/componentloginpage'
                . '?component_appid='.$this->config['appid']
                . '&pre_auth_code='.urlencode($preAuthCode)
                . '&redirect_uri='.urlencode($redirectUri);
        return $url;
    }
    
    /**
     * 获取推送解密的消息
     * @return mixed 消息数组
     */
    public function getDecryptPushMessage()
    {
        $content = file_get_contents('php://input');
        if (empty($content)) {
            $this->setError('推送消息为空！');
            return false;
        }
        
        $this->logDebugFile($content);
        
        $decryptMsg = $this->decryptPushMsg($content);
        
        $this->logDebugFile($decryptMsg);

        $message = XML::parse($decryptMsg);
        if (empty($message)) {
            $this->setError('推送消息内容为空！');
            return false;
        }
        $this->logDebugFile($message);
        
        return $message;
    }
    
    /**
     * 解密推送消息
     * @param string $encryptMsg
     */
    public function decryptPushMsg($encryptMsg)
    {
        vendor('wechat.msgencrypt.WXBizMsgCrypt');

        $xmlTree = new \DOMDocument();
        $xmlTree->loadXML($encryptMsg);
        $arrayEnc = $xmlTree->getElementsByTagName('Encrypt');
        $encrypt = $arrayEnc->item(0)->nodeValue;

        $format = "<xml><AppId><![CDATA[%s]]></AppId><Encrypt><![CDATA[%s]]></Encrypt></xml>";
        $fromXml = sprintf($format, $this->config['appid'], $encrypt);

        $msgSignature = input('msg_signature', '');
        $timeStamp = input('timestamp', '');
        $nonce = input('nonce', '');
        $this->logDebugFile(compact('msgSignature', 'timeStamp', 'nonce'));

        $wxBizMsgCrypt = new \WXBizMsgCrypt($this->config['verify_token'], $this->config['encoding_aes_key'], $this->config['appid']);
        $errCode = $wxBizMsgCrypt->decryptMsg($msgSignature, $timeStamp, $nonce, $fromXml, $msg);
        if ($errCode != 0) {
            $this->logDebugFile('解密错误码：'.$errCode);
            $this->setError('解密错误码：'.$errCode);
            return false;
        } 
        
        return $msg;
    }

    /**
     * 获取普通推送消息
     * @return array|bool|\SimpleXMLElement
     */
    public function getPushMessage()
    {
        $content = file_get_contents('php://input');
        if (empty($content)) {
            $this->setError('推送消息为空！');
            return false;
        }

        $this->logDebugFile($content);

        $message = \app\common\util\XML::parse($content);
        if (empty($message)) {
            $this->setError('推送消息内容为空！');
            return false;
        }
        $this->logDebugFile($message);

        return $message;
    }

    /**
     * 获取授权的用户列表
     * @param int $count 一次获取的数量，最大500
     * @param int $offset 获取数量的偏移值
     * @return bool|mixed|string
     */
    public function getAuthorizerList($count = 500, $offset = 0)
    {
        $accessToken = $this->getComponentAccessToken();
        if (!$accessToken) {
            return false;
        }

        $url ="https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_list?component_access_token={$accessToken}";
        $post = $this->toJson([
            'component_appid'   => $this->config['appid'],
            'offset'  => $offset,
            "count"   => $count
        ]);

        $wxdata = $this->requestAndCheck($url, 'POST', $post);
        if ($wxdata === false) {
            return false;
        }

        //返回数据结构
        //{
        //    "total_count":33,
        //    list:[
        //        {
        //          "authorizer_appid": "authorizer_appid_1",
        //          "refresh_token": "refresh_token_1",
        //          "auth_time": auth_time_1
        //        },
        //    ]
        //}
        return $wxdata;
    }

    /**
     * 处理授权消息事件
     */
    public function handleAuthEvent($event, $callback)
    {
        static $msg = null;
        if (!$msg) {
            $msg = $this->getDecryptPushMessage();
            if (!$msg) {
                exit($this->getError());
            }
        }

        // 先处理全局事件
        if ($event == self::AUTH_EVENT_ALL) {
            is_callable($callback) && $callback($msg);
            return;
        }

        static $eventParse = [
            self::AUTH_EVENT_VERIFY_TICKET => ['InfoType' => 'component_verify_ticket'],
            self::AUTH_EVENT_UNAUTHORIZED => ['InfoType' => 'unauthorized'],
            self::AUTH_EVENT_AUTHORIZED => ['InfoType' => 'authorized'],
            self::AUTH_EVENT_UPDATE_AUTHORIZED => ['InfoType' => 'updateauthorized'],
        ];

        $this->callEventHandle($eventParse, $event, $callback, $msg);
    }

    /**
     * 处理普通消息事件
     */
    public function handleCommonEvent($event, $callback)
    {
        static $msg = null;
        if (!$msg) {
            $msg = $this->getDecryptPushMessage();
            if (!$msg) {
                exit($this->getError());
            }
        }

        // 先处理全局事件
        if ($event == self::COMMON_EVENT_ALL) {
            is_callable($callback) && $callback($msg);
            return;
        }

        static $eventParse = [
            self::COMMON_EVENT_WEAPP_AUDIT_SUCCESS => ['MsgType' => 'event', 'Event' => 'weapp_audit_success'],
            self::COMMON_EVENT_WEAPP_AUDIT_FAIL => ['MsgType' => 'event', 'Event' => 'weapp_audit_fail'],
        ];

        $this->callEventHandle($eventParse, $event, $callback, $msg);
    }

    /**
     * 回调事件处理
     * @param $eventParse
     * @param $event
     * @param $callback
     * @param $msg
     */
    private function callEventHandle($eventParse, $event, $callback, $msg)
    {
        if (!isset($eventParse[$event])) {
            return;
        }

        $findEvent = true;
        foreach ($eventParse[$event] as $key => $word) {
            if ($msg[$key] !== $word) {
                $findEvent = false;
                break;
            }
        }
        if (!$findEvent) {
            return;
        }

        is_callable($callback) && $callback($msg);
    }
}