WxworkAuth.class.php 11.4 KB
<?php
/**
 * wxworkAuth.class.php
 * 企业微信获取用户基本信息(临时解决方案),只用于前端网页授权(CODE),属于应用层面的授权
 * @author Deepseath
 * @version $Id$
 */
namespace Com;

/**
 * 企业微信获取用户基本信息
 */
class WxworkAuth
{

    /**
     * 获取应用 access_token
     */
    const WXWORK_API_GET_ACCESS_TOKEN = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s';

    /**
     * 根据 CODE 获取用户基本信息
     */
    const WXWORK_API_GET_USER_INFO = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s';

    /**
     * 企业 corpId
     */
    protected $_wxworkCorpId = '';

    /**
     * 所属应用的 secret
     */
    protected $_wxworkAppSecret = '';

    /**
     * 所属应用 appId
     */
    protected $_wxworkAppId = '';

    /**
     * 当前应用 token
     */
    private $_accessToken = '';

    /**
     * 网络请求对象
     */
    private $_request;

    /**
     * 构造方法:企业微信获取用户基本信息
     */
    public function __construct()
    {
        $this->_request = \VcySDK\Service::instance();
    }

    /**
     * 自配置文件验证当前企业是否采用企业微信新的应用 secret 方式获取身份
     * @return bool
     */
    public function useWXWorkGetUserInfo()
    {
        $appSecret = cfg('WXWORK_APP_SECRET');
        if (empty($appSecret)) {
            // 未配置则不使用
            return false;
        }

        // appid、secret 配置信息
        $setting = false;
        // 默认配置信息
        $defaults = [];
        // 查找当前应用所对应的配置信息
        foreach ($appSecret as $_app_dir => $_setting) {
            $_app_dir = strtolower($_app_dir);
            if ($_app_dir == 'default') {
                $defaults = $_setting;
                continue;
            }

            if ($_app_dir == strtolower(APP_DIR)) {
                $setting = $_setting;
                continue;
            }
        }

        // 如果没有应用对应则使用默认配置
        if ($setting === false) {
            $setting = $defaults;
        }

        $this->_wxworkAppId = $setting['app_id'];
        $this->_wxworkAppSecret = $setting['app_secret'];
        $this->_wxworkCorpId = $setting['corpid'];

        return true;
    }

    /**
     * 获取当前应用的 token
     */
    public function getAccessToken()
    {
        $appToken = $this->_cache($this->_wxworkAppId);
        if ($appToken !== false) {
            // 缓存存在;
            $this->_accessToken = $appToken['access_token'];
            return true;
        }

        $params = [
            self::WXWORK_API_GET_ACCESS_TOKEN,
            $this->_wxworkCorpId,
            $this->_wxworkAppSecret
        ];

        $token = '';
        $data = [];

        try {
            $this->_request($data, $this->apiUrl($params));
        } catch (\VcySDK\Exception $e) {
            \Think\Log::record('#getAccessToken#get app token::' . print_r($e, true));
            throw new \VcySDK\Exception($e);
        }

        // \Think\Log::record("#getAccessToken#AAA." . print_r($data, true));

        $data['expires_time'] = time() + $data['expires_in'];

        $this->_cache($this->_wxworkAppId, $data);
        // \Think\Log::record('#getAccessToken#BBB.' . print_r($data, true));

        return $data['access_token'];
    }

    /**
     * 根据 Code 值获取用户基本信息
     * @param string $code
     */
    public function getUserInfo($code)
    {
        $token = $this->getAccessToken();

        if (empty($token)) {
            return [];
        }

        $params = [
            self::WXWORK_API_GET_USER_INFO,
            $this->getAccessToken(),
            $code
        ];

        $data = [];

        try {
            $this->_request($data, $this->apiUrl($params));
        } catch (\VcySDK\Exception $e) {
            // 如果是未找到用户
            \Think\Log::record('get user info::' . print_r($e, true));
            throw new \VcySDK\Exception($e);
        }

        // \Think\Log::record('#getUserInfo#AAA.' . print_r($data, true));

        // 默认初始化为外部成员
        $user = [
            'source' => \Common\Common\User::SOURCE_TYPE_GUEST,
            'memUserid' => isset($data['OpenId']) ? $data['OpenId'] : (isset($data['UserId']) ? $data['UserId'] : '')
        ];

        if (isset($data['UserId'])) {
            // 企业内部成员
            $userService = new \Common\Common\User();
            $userData = $userService->getByUserId($data['UserId']);
            if (empty($userData)) {
                \Think\Log::record('获取内部成员数据失败!' . print_r($data, true));
                return $user;
            }
            // \Think\Log::record('#getUserInfo#CCC.' . print_r($user, true));
            $user['source'] = isset($user['source']) ? $user['source'] : \Common\Common\User::SOURCE_TYPE_MEMBER;
        }

        return $user;
    }

    /**
     * 解析接口的参数变量
     * @param array $params
     * @return string
     */
    public function apiUrl($params = array())
    {
        return call_user_func_array('sprintf', $params);
    }

    /**
     * 缓存操作方法
     * @param string $appId 待获取/设置的应用ID
     * @param array $value 待设置缓存值的数据,未空则是获取数据
     */
    private function _cache($appId, $value = null)
    {
        // 缓存名称
        $cacheName = 'wxworkAccessToken';
        // 缓存数据类型
        settype($cacheName, 'array');
        // 获取缓存数据
        $list = S($cacheName);
        if (! empty($list)) {
            $list = unserialize($list);
        }
        // \Think\Log::record('#_cache#AAA.' . print_r($list, true));
        if (empty($list) || ! is_array($list)) {
            // 初始化缓存值
            $list = [];
        }
        if (! isset($list[$appId])) {
            // 初始化应用缓存信息
            $list[$appId] = [];
        }
        if ($value !== null) {
            // 更新缓存
            $list[$appId] = $value;
            S($cacheName, serialize($value));
            return $value;
        }

        $appTokenCache = $list[$appId];
        if (empty($appTokenCache) || empty($appTokenCache['expires_time'])) {
            // 不存在该应用的缓存
            return false;
        }
        if (time() - $appTokenCache['expires_time'] > $appTokenCache['expires_in'] - 1800) {
            // 缓存已过期,为避免 token 失效,取 token 实际过期周期偏小一些(1800)
            return false;
        }

        return $appTokenCache;
    }

    /**
     * 请求接口数据
     * @param mixed &$data 返回值
     * @param string $url 请求URL
     * @param string $reqParams 请求数据
     * @param mixed $headers 请求头部
     * @param string $method 请求方式, 如: GET/POST/DELETE/PUT
     * @param mixed $files 文件路径
     * @param bool $retry 是否重试
     * @return boolean
     * @throws \VcySDK\Exception
     */
    private function _request(&$data, $url, $reqParams = [], $headers = [], $method = 'GET', $files = null, $retry = false)
    {
        // 载入 Snoopy 类
        $snoopy = new \VcySDK\Net\Snoopy();
        // 使用自定义的头字段,格式为 array(字段名 => 值, ... ...)
        $headers = array();

        // \Think\Log::record("#_request#AAA.URL: {$url} Method: {$method} Post: " . var_export($reqParams, true) . " Header: " . var_export($headers, true));
        $snoopy->rawheaders = $headers;
        $method = rstrtoupper($method);
        // 非 GET 协议, 需要设置
        $methods = array(
            'POST',
            'PUT',
            'DELETE'
        );
        if (! in_array($method, $methods)) {
            $method = 'GET';
        }

        // 设置协议
        if (! empty($files)) {
            // 如果需要传文件
            $method = 'POST';
            $snoopy->set_submit_multipart();
        } else {
            $snoopy->set_submit_normal('');
        }

        // 判断协议
        $snoopy->set_submit_method($method);
        switch (rstrtoupper($method)) {
            case 'POST':
            case 'PUT':
            case 'DELETE':
                $result = $snoopy->submit($url, $reqParams, $files);
                break;
            default:
                // 如果有请求数据
                $this->_buildGetQuery($url, $reqParams);
                $result = $snoopy->fetch($url);
                break;
        }

        // 如果读取错误
        if (! $result || 200 != $snoopy->status) {
            \Think\Log::record('#_request#BBB.$snoopy[' . $method . '] error, url: ' . $url . '|post:' . var_export($reqParams, true) . '|result: ' . var_export($result, true) . '|status: ' . $snoopy->status . '|error: ' . $snoopy->error);
            // 出错时, 返回 $snoopy 对象
            $data = $snoopy;
            throw new \VcySDK\Exception(\VcySDK\Error::API_REQUEST_ERROR);
            return false;
        }

        // 获取返回数据
        $data = $snoopy->results;
        // 如果返回的是 JSON, 则解析 JSON
        if ($this->_isJson($snoopy->headers)) {
            $data = json_decode($data, true);
            // 如果接口返回错误, 则直接抛异常
            if (0 != $data['errcode']) {
                throw new \VcySDK\Exception($data['errmsg'] . '##' . $this->_wxworkAppId . '#' . $this->_wxworkAppSecret . '#' . $this->_wxworkCorpId . '#' . print_r($data, true) . '#' . $url, $data['errcode']);
                return false;
            }
        }

        // \Think\Log::record("#_request#CCC.result: " . var_export($data, true) . "|post: " . var_export($reqParams, true));
        // 如果返回的数据为空, 则
        if (empty($data)) {
            \Think\Log::record('#_request#DDD.$snoopy[' . $method . '] error, url: ' . $url . '|result: ' . var_export($result, true) . '|status: ' . $snoopy->status);
            // 出错时, 返回 $snoopy 对象
            $data = $snoopy;
            throw new \VcySDK\Exception(\VcySDK\Error::API_RESPONSE_DATA_EMPTY);
            return false;
        }

        return true;
    }

    /**
     * 拼凑请求URL
     * @param string $url URL地址
     * @param mixed $params 请求参数
     * @return boolean
     */
    private function _buildGetQuery(&$url, $params)
    {

        // 如果请求数据为空
        if (empty($params)) {
            return true;
        }

        // 拼凑 GET 字串
        if (is_array($params)) {
            $get_data = http_build_query($params);
        } else {
            $get_data = $params;
        }

        // 判断 URL 是否有参数
        if (false === strpos($url, '?')) {
            $url .= '?';
        } else {
            $url .= '&';
        }

        $url .= $get_data;

        return true;
    }

    /**
     * 判断返回值是否为Json数据
     * @param array $headers 头部数据
     * @return boolean
     */
    private function _isJson($headers)
    {

        // 如果头部信息已经解析出来, 则
        if (isset($headers['Content-Type'])) {
            return 0 === strpos($headers['Content-Type'], 'application/json');
        }

        // 遍历返回的头部信息
        foreach ($headers as $_header) {
            // 如果匹配到 json 头
            if (preg_match('/^Content-Type:\s*application\/json/i', $_header)) {
                return true;
            }
        }

        return false;
    }
}