<?php

namespace app\common\controller;

use think\Config;
use think\Db;
use think\Exception;
use think\exception\DbException;
use think\exception\PDOException;

/**
 * 微信公众平台基类
 * Class Wechat
 * @package app\common\controller
 * @version 1.0
 * @author fuyelk <fuyelk@fuyelk.com>
 */
class Wechat
{
    public $appid = 'wx81f7b48cef8b4cfb';
    public $appsecret = 'e5d87a1aed0a553838286012a5685800';
    private $access_token = '54_XqfLmdbsNdBLQlCbxMNto2vBcDnQjUf9Uxt_U6CHXiPgtdlwP2ju3KP5DqK0UT6wsxKAHhw0bGcI5og1K25RFGH9v0AT7hnN-WFcJa0vaQJgcXzqqE75zbWHn5qG6HdNq-rFbGn60AK9peTwLYReAGAVIB';
    private $expires_time = 72000;
    private $_error = '';

    /**
     * WeChat 构造函数
     * @param boolean $refreshToken 为真,则刷新token
     */
    public function __construct($refreshToken = false)
    {
        $config = $this->getConfig();
        if (empty($config)) {
            exit($this->_error);
        }
        //var_dump($config);die;
        if (empty($config['appid']) || empty($config['appsecret'])) {
            exit('微信配置为空');
        }

        $this->appid = $config['appid'];
        $this->appsecret = $config['appsecret'];

        // 检查access_token是否有效
        if ($refreshToken || empty($config['access_token']) || $config['access_token_expire_time'] < strtotime('+5 minute')) {
            if (!$this->refreshAccessToken()) {
                exit($this->_error);
            }
        } else {
            $this->access_token = $config['access_token'];
            $this->expires_time = $config['access_token_expire_time'];
        }
    }

    /**
     * 获取微信配置
     * @return array|bool
     * @author fuyelk <fuyelk@fuyelk.com>
     */
    private function getConfig()
    {
        // 检查有没有wechat表
        try {
            $config = Db::name('wechat')->column('value', 'name');
        } catch (PDOException $e) {
            // 表不存在
            if ('1146' == $e->getData()['PDO Error Info']['Driver Error Code']) {
                // 创建表
                $prefix = Config::get('database.prefix');
                $sqlCreateTable = "CREATE TABLE `{$prefix}wechat` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',`name` varchar(100) CHARACTER SET utf8mb4 NOT NULL COMMENT '数据名',`value` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '值',`description` text CHARACTER SET utf8mb4 COMMENT '描述',`updatetime` int(10) DEFAULT NULL COMMENT '更新时间',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='微信配置表';";
                $sqlInsertData = "insert into `{$prefix}wechat` (`id`, `name`, `value`, `description`, `updatetime`) values ('1','appid','','APPID',NULL),('2','appsecret','','APP SECRET',NULL),('3','access_token','','SCCESS TOKEN',NULL),('4','access_token_expire_time','','SCCESS TOKEN过期时间',NULL),('5','qrcode','','二维码',NULL);";
                try {
                    Db::query($sqlCreateTable);
                    Db::query($sqlInsertData);
                    return Db::name('wechat')->column('value', 'name');
                } catch (Exception $e) {
                    $this->_error = '微信配置表不存在,创建失败';
                    return false;
                }
            }

            $this->_error = $e->getMessage();
            return false;
        } catch (DbException $e) {
            $this->_error = $e->getMessage();
            return false;
        }

        return $config;
    }

    /**
     * 向微信服务器请求获取accessToken,并存储
     * return bool
     */
    private function refreshAccessToken()
    {
        // 向微信服务器发access_token请求
        $data = $this->wechatRequest('https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' . $this->appid . '&secret=' . $this->appsecret);
        if (empty($data['access_token'])) {
            $this->_error = 'access_token获取失败';
            return false;
        }

        $this->access_token = $data['access_token'];
        $this->expires_time = time() + $data['expires_in'];

        try {
            Db::name('wechat')->where('name', 'access_token')->update(['value' => $this->access_token, 'updatetime' => time()]);
            Db::name('wechat')->where('name', 'access_token_expire_time')->update(['value' => $this->expires_time, 'updatetime' => time()]);
        } catch (Exception $e) {
            $this->_error = $e->getMessage();
            return false;
        }

        return true;
    }

    /**
     * http请求
     * @param string $url http地址
     * @param string $method 请求方式
     * @param array $data 请求数据:
     * <pre>
     *  $data = [
     *      'image' => new \CURLFile($filePath),
     *      'access_token' => 'this-is-access-token'
     *       ...
     *  ]
     * </pre>
     * @return bool|string
     * @throws \Exception
     * @author fuyelk <fuyelk@fuyelk.com>
     */
    public function http_request($url, $method = 'GET', $data = [])
    {
        $addHeader = [];
        $curl = curl_init();
        curl_setopt_array($curl, [
            CURLOPT_ACCEPT_ENCODING => 'gzip,deflate',
            CURLOPT_URL => $url,
            CURLOPT_CUSTOMREQUEST => strtoupper($method), // 请求方式
            CURLOPT_USERAGENT => "Mozilla / 5.0 (Windows NT 10.0; Win64; x64)",// 模拟常用浏览器的useragent
            CURLOPT_RETURNTRANSFER => true, // 获取的信息以文件流的形式返回,而不是直接输出
            CURLOPT_SSL_VERIFYPEER => false, // https请求不验证证书
            CURLOPT_SSL_VERIFYHOST => false, // https请求不验证hosts
            CURLOPT_MAXREDIRS => 10, // 最深允许重定向级数
            CURLOPT_CONNECTTIMEOUT => 10,// 最长等待连接成功时间
            CURLOPT_TIMEOUT => 30, // 最长等待响应完成时间
        ]);

        // 发送请求数据
        if ($data) {
            $data = json_encode($data, JSON_UNESCAPED_UNICODE);
            array_push($addHeader, 'Content-type:application/json');
            curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
        }
        curl_setopt($curl, CURLOPT_HTTPHEADER, $addHeader);
        $response = curl_exec($curl);
        $err = curl_error($curl);
        curl_close($curl);
        if ($err) throw new \Exception($err);
        return $response;
    }

    /**
     * 获取错误信息
     * @return string
     * @author doyle <doyle@doywb.com>
     * @date 2021/4/11 15:49
     */
    public function getError()
    {
        return $this->_error;
    }

    /**
     * 请求微信接口,并检查请求结果
     * @param string $url 接口地址
     * @param null $data [请求数据]
     * @param bool $callback [是否是回调]
     * @return bool|mixed|string
     * @author fuyelk <fuyelk@fuyelk.com>
     * @date 2021/07/18 22:49
     */
    public function wechatRequest($url, $data = null, $callback = false)
    {
        $url = $url . (strpos($url, '?') ? '&' : '?') . 'access_token=' . $this->access_token;
        try {
            if (!empty($data)) {
                $res = $this->http_request($url, 'POST', $data);
            } else {
                $res = $this->http_request($url);
            }
        } catch (\Exception $e) {
            $this->_error = $e->getMessage();
            return false;
        }

        $res = json_decode($res, true);

        // 接口出错
        if (!empty($res['errcode'])) {
            $log = [
                'url' => $url,
                'data' => $data,
                'response' => $res
            ];

            // 非回调,则检查是否AccessToken错误
            if (!$callback && '40001' == $res['errcode'] && !empty($res['errmsg']) && strpos($res['errmsg'], 'access_token is invalid')) {
                // 刷新AccessToken
                if (!$this->refreshAccessToken()) {
                    return false;
                }

                dta($log, '微信接口请求出错,AccessToken失效所致,正在尝试修复');

                // 重新请求微信接口
                return $this->wechatRequest($url, $data, true);
            }

            dta($log, '微信接口请求出错');

            if (!empty($res['errmsg'])) {
                $this->_error = $res['errmsg'];
            }
            return false;
        }
        return $res;
    }

    /**
     * 在公网接口处重写此方法:验证服务器有效性
     * @return int|mixed
     */
    public function serverValidation()
    {
        $TOKEN = 'milingerfuyelkdoywb';
        $signature = $_GET["signature"] ?? "";
        $timestamp = $_GET["timestamp"] ?? "";
        $nonce = $_GET["nonce"] ?? "";
        $tmpArr = array($TOKEN, $timestamp, $nonce);
        sort($tmpArr, SORT_STRING);
        $tmpStr = implode($tmpArr);
        $tmpStr = sha1($tmpStr);

        if ($tmpStr == $signature) {
            return $_GET["echostr"] ?? '';
        }
        return 'error';
    }

    /**
     * 在公网接口处重写此方法:引导用户进入授权页,并跳转到目标页
     * @param string $url 授权后需要跳转的页面地址,必须进行base64编码
     * @author fuyelk <fuyelk@fuyelk.com>
     */
    public function bootToUrl($url = '')
    {
        $redirect_uri = request()->domain() . '/index/wechat/wechatRedirect?redirect=' . $url;
        $appid = $this->wechat->appid;
        $redirect_uri = urlencode($redirect_uri);
        $scope = 'snsapi_base'; // 静默授权
//        $scope  = 'snsapi_userinfo';
        $wechat_authorize = "http://2.ipfsfil168.com/get-weixin-code.html?appid={$appid}&redirect_uri={$redirect_uri}&response_type=code&scope={$scope}&state=123#wechat_redirect";
        $this->redirect($wechat_authorize);
    }

    /**
     * 在公网接口处重写此方法:用户授权后微信将用户重定向至此接口并携带code,
     * 使用code可获取access_token及用户openid,完成业务逻辑处理后拼接上openid将用户重定向指指定页面
     */
    public function wechatRedirect()
    {
        $code = input('code');
        $redirect = input('redirect');
        // 获取用户信息
        if ($code) {
            $wechat_url = 'https://api.weixin.qq.com/sns/oauth2/access_token' .
                '?appid=' . $this->wechat->appid .
                '&secret=' . $this->wechat->appsecret .
                '&code=' . input('code') .
                '&grant_type=authorization_code';

            try {
                $res = $this->wechat->http_request($wechat_url);
            } catch (\Exception $e) {
                $this->error($e->getMessage());
            }

            $res = json_decode($res, true);
            if (empty($res['openid'])) {
                $this->error('登录失败');
            }

            $redirect = base64_decode($redirect);
            $redirect = htmlspecialchars_decode($redirect);
            $query = parse_url(strstr($redirect, '?'), PHP_URL_QUERY);
            parse_str($query, $params);

            // 获取用户信息
            if ('snsapi_userinfo' == $res['scope']) {
                $userinfo = file_get_contents("https://api.weixin.qq.com/sns/userinfo?access_token={$res['access_token']}&openid={$res['openid']}&lang=zh_CN");
                $userinfo = json_decode($userinfo, true);
                if (!empty($userinfo['errcode']) && !empty($userinfo['errmsg'])) {
                    exit($userinfo['errmsg']);
                }
                //    [openid] => ou6NC6kBSloLS94gUhCyKeguIaYI
                //    [nickname] => fuyelk
                //    [sex] => 1 // 1=男,2=女,0=未知
                //    [language] => zh_CN
                //    [city] => 郑州
                //    [province] => 河南
                //    [country] => 中国
                //    [headimgurl] => https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKY3E1A2MFK46y1fNv7OElhtQFefO4mrBy8kcQoFCHP9diaULlpeINYB1FJ7KNh4fx5v3rzAJ2LzFg/132

                $invite_code = $params['sharecode'] ?? '';

                // TODO 业务处理
                //   ...

                $token = 'abcdefg';

                if (false === $token) {
                    exit($this->_error ?: '登录失败');
                }

            } else {
                exit('仅支持snsapi_userinfo');
            }

            $redirect = $redirect . (strpos($redirect, '?') ? '&' : '?') . 'token=' . $token;

            $this->redirect($redirect ? urldecode($redirect) : '/');
        }
        $this->error('授权失败');
    }

}