2025-07-01 19:39:13 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace app\common\utils;
|
|
|
|
|
|
|
|
|
|
|
|
use GuzzleHttp\Client;
|
|
|
|
|
|
use GuzzleHttp\Exception\RequestException;
|
|
|
|
|
|
use support\Cache;
|
|
|
|
|
|
use Webman\Exception\BusinessException;
|
|
|
|
|
|
|
|
|
|
|
|
class Feishu
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
protected $appId = '';
|
|
|
|
|
|
protected $appSecret = '';
|
|
|
|
|
|
protected $baseUri = '';
|
|
|
|
|
|
protected $client = null;
|
|
|
|
|
|
protected $options = [];
|
|
|
|
|
|
protected $error = '';
|
2025-07-09 19:11:57 +08:00
|
|
|
|
protected $abbr = 'hesu';
|
2025-07-01 19:39:13 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 初始化
|
|
|
|
|
|
*/
|
2025-07-09 19:11:57 +08:00
|
|
|
|
public function __construct(string $appId = '', string $appSecret = '', string $abbr = 'hesu')
|
2025-07-01 19:39:13 +08:00
|
|
|
|
{
|
|
|
|
|
|
|
2025-07-09 19:11:57 +08:00
|
|
|
|
if ($abbr === 'mbaf') {
|
2025-07-01 19:39:13 +08:00
|
|
|
|
$this->baseUri = 'https://open.larksuite.com/open-apis/';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$this->baseUri = 'https://open.feishu.cn/open-apis/';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$this->appId = $appId;
|
|
|
|
|
|
$this->appSecret = $appSecret;
|
2025-07-09 19:11:57 +08:00
|
|
|
|
$this->abbr = $abbr;
|
2025-07-01 19:39:13 +08:00
|
|
|
|
|
|
|
|
|
|
$this->client = new Client([
|
|
|
|
|
|
'base_uri' => $this->baseUri
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$this->options = [
|
|
|
|
|
|
'headers' => [
|
|
|
|
|
|
'Content-Type' => 'application/json; charset=utf-8'
|
|
|
|
|
|
]
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 异常
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function error()
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->error;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 请求
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function requestResult($url = "", $method = "POST")
|
|
|
|
|
|
{
|
|
|
|
|
|
try {
|
|
|
|
|
|
$res = $this->client->request($method, $this->baseUri.$url, $this->options);
|
|
|
|
|
|
if ($res->getBody()) {
|
|
|
|
|
|
$result = json_decode($res->getBody(), true);
|
|
|
|
|
|
// var_dump($result);
|
|
|
|
|
|
if ($result["code"] === 0) {
|
|
|
|
|
|
return $result;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$this->error = $result["msg"];
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$this->error = "result error";
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (RequestException $e) {
|
|
|
|
|
|
if ($e->hasResponse()) {
|
|
|
|
|
|
var_dump('2 :'.$e->getResponse()->getBody());
|
|
|
|
|
|
throw new BusinessException($e->getResponse()->getBody());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
var_dump('3 :'.$e->getMessage());
|
|
|
|
|
|
throw new BusinessException($e->getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (\Throwable $th) {
|
|
|
|
|
|
var_dump('4 :'.$th->getFile()."|".$th->getLine()."|".$th->getMessage());
|
|
|
|
|
|
$this->error = $th->getFile()."|".$th->getLine()."|".$th->getMessage();
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 授权
|
|
|
|
|
|
*/
|
2026-01-04 16:27:20 +08:00
|
|
|
|
public function authorize($certificate,$mobile, $url = 'authen/v1/authorize')
|
2025-07-01 19:39:13 +08:00
|
|
|
|
{
|
|
|
|
|
|
$arr = [
|
2025-07-09 19:11:57 +08:00
|
|
|
|
'app_id' => $certificate['app_id'],
|
2026-01-04 16:27:20 +08:00
|
|
|
|
'redirect_uri' => $mobile == 1 ? $certificate['m_url'] : $certificate['r_url'],
|
2025-07-01 19:39:13 +08:00
|
|
|
|
'scope' => $certificate['scope'],
|
|
|
|
|
|
'state' => $certificate['state']
|
|
|
|
|
|
];
|
|
|
|
|
|
$queryData = http_build_query($arr);
|
|
|
|
|
|
return $this->baseUri.$url.'?'.$queryData;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 自建应用获取 tenant_access_token
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function tenantAccessToken()
|
|
|
|
|
|
{
|
2025-07-09 19:11:57 +08:00
|
|
|
|
$tenantAccessToken = Cache::get($this->abbr.'-tenantAccessToken');
|
2025-07-01 19:39:13 +08:00
|
|
|
|
if (!$tenantAccessToken) {
|
|
|
|
|
|
$this->options["json"] = [
|
|
|
|
|
|
"app_id" => $this->appId,
|
|
|
|
|
|
"app_secret" => $this->appSecret
|
|
|
|
|
|
];
|
|
|
|
|
|
$rst = $this->requestResult('auth/v3/tenant_access_token/internal', 'POST');
|
|
|
|
|
|
if ($rst) {
|
|
|
|
|
|
$tenantAccessToken = $rst['tenant_access_token'];
|
2025-07-09 19:11:57 +08:00
|
|
|
|
Cache::set($this->abbr.'-tenantAccessToken', $tenantAccessToken, $rst['expire']);
|
2025-07-01 19:39:13 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
throw new BusinessException('tenant_access_token 获取失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return $tenantAccessToken;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 通过手机号或邮箱获取用户 ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function getUserIdByPhoneOrEmail($phone = '', $userIdType = 'open_id')
|
|
|
|
|
|
{
|
|
|
|
|
|
$this->options["headers"] = [
|
|
|
|
|
|
"Authorization" => "Bearer " . $this->tenantAccessToken(),
|
|
|
|
|
|
"Content-Type" => "application/json; charset=utf-8"
|
|
|
|
|
|
];
|
|
|
|
|
|
$this->options["json"] = [
|
|
|
|
|
|
"emails" => [],
|
|
|
|
|
|
"mobiles" => [$phone],
|
|
|
|
|
|
"include_resigned" => false
|
|
|
|
|
|
];
|
|
|
|
|
|
$rst = $this->requestResult('contact/v3/users/batch_get_id?user_id_type='.$userIdType, 'POST');
|
|
|
|
|
|
return $rst;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function sendMessages($receiveId = '', $content = '', $msgType = 'text', $receiveIdtype = 'open_id')
|
|
|
|
|
|
{
|
|
|
|
|
|
$this->options["headers"] = [
|
|
|
|
|
|
"Authorization" => "Bearer " . $this->tenantAccessToken(),
|
|
|
|
|
|
"Content-Type" => "application/json; charset=utf-8"
|
|
|
|
|
|
];
|
|
|
|
|
|
$this->options["json"] = [
|
|
|
|
|
|
"receive_id" => $receiveId,
|
|
|
|
|
|
"msg_type" => $msgType,
|
|
|
|
|
|
"content" => $content
|
|
|
|
|
|
];
|
|
|
|
|
|
$rst = $this->requestResult('im/v1/messages?receive_id_type='.$receiveIdtype, 'POST');
|
|
|
|
|
|
return $rst;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 编辑消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function updateSendMessages($messageId = '', $content = '', $msgType = 'text')
|
|
|
|
|
|
{
|
|
|
|
|
|
$this->options["headers"] = [
|
|
|
|
|
|
"Authorization" => "Bearer " . $this->tenantAccessToken(),
|
|
|
|
|
|
"Content-Type" => "application/json; charset=utf-8"
|
|
|
|
|
|
];
|
|
|
|
|
|
$this->options["json"] = [
|
|
|
|
|
|
"msg_type" => $msgType,
|
|
|
|
|
|
"content" => $content
|
|
|
|
|
|
];
|
|
|
|
|
|
$rst = $this->requestResult('im/v1/messages/'.$messageId, 'PUT');
|
|
|
|
|
|
return $rst;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 用户 user_access_token
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function userAccessToken($code = "", $redirect_uri = "")
|
|
|
|
|
|
{
|
|
|
|
|
|
$this->options["headers"] = [
|
|
|
|
|
|
"Content-Type" => "application/json; charset=utf-8"
|
|
|
|
|
|
];
|
|
|
|
|
|
$this->options["json"] = [
|
|
|
|
|
|
"grant_type" => "authorization_code",
|
|
|
|
|
|
"client_id" => $this->appId,
|
|
|
|
|
|
"client_secret" => $this->appSecret,
|
|
|
|
|
|
"code" => $code,
|
|
|
|
|
|
"redirect_uri" => $redirect_uri
|
|
|
|
|
|
];
|
|
|
|
|
|
return $this->requestResult('authen/v2/oauth/token');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 用户信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function userInfo($userAccessToken = "")
|
|
|
|
|
|
{
|
|
|
|
|
|
$this->options["headers"] = [
|
|
|
|
|
|
"Authorization" => "Bearer " . $userAccessToken,
|
|
|
|
|
|
"Content-Type" => "application/json; charset=utf-8"
|
|
|
|
|
|
];
|
|
|
|
|
|
return $this->requestResult('authen/v1/user_info', "GET");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// public function user_access_token($code = "")
|
|
|
|
|
|
// {
|
|
|
|
|
|
// $url = "";
|
|
|
|
|
|
// $this->options["headers"] = [
|
|
|
|
|
|
// "Authorization" => "Bearer ".$this->tenantAccessToken(),
|
|
|
|
|
|
// "Content-Type" => "application/json; charset=utf-8"
|
|
|
|
|
|
// ];
|
|
|
|
|
|
// $this->options["json"] = [
|
|
|
|
|
|
// "grant_type" => "authorization_code",
|
|
|
|
|
|
// "code" => $code
|
|
|
|
|
|
// ];
|
|
|
|
|
|
// $this->options["form_params"] = [
|
|
|
|
|
|
// "app_id" => $this->app_id,
|
|
|
|
|
|
// "app_secret" => $this->app_secret
|
|
|
|
|
|
// ];
|
|
|
|
|
|
// return $this->requestResult($url);
|
|
|
|
|
|
// }
|
2026-02-26 10:46:16 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取 JSAPI 配置(签名)
|
|
|
|
|
|
* @param string $url 当前页面完整 URL(不包含 # 后面的内容)
|
|
|
|
|
|
* @return array
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function getJsApiConfig($url)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 1. 获取 tenant_access_token
|
|
|
|
|
|
//$accessToken = $this->getTenantAccessToken();
|
|
|
|
|
|
$accessToken = $this->tenantAccessToken();
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 获取 jsapi_ticket
|
|
|
|
|
|
$ticket = $this->getJsapiTicket($accessToken);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 生成签名参数
|
|
|
|
|
|
$timestamp = time();
|
|
|
|
|
|
$nonceStr = $this->generateNonceStr();
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 生成签名
|
|
|
|
|
|
$signature = $this->generateSignature($ticket, $nonceStr, $timestamp, $url);
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
'appId' => $this->appId,
|
|
|
|
|
|
'timestamp' => $timestamp,
|
|
|
|
|
|
'nonceStr' => $nonceStr,
|
|
|
|
|
|
'signature' => $signature
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取 jsapi_ticket
|
|
|
|
|
|
* @param string $accessToken
|
|
|
|
|
|
* @return string
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function getJsapiTicket($accessToken)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 检查缓存
|
|
|
|
|
|
$cachedTicket = Cache::get($this->abbr.'-feishu:jsapi_ticket');
|
|
|
|
|
|
// $cachedTicket = $this->getCachedData('feishu:jsapi_ticket');
|
|
|
|
|
|
if ($cachedTicket) {
|
|
|
|
|
|
return $cachedTicket;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$url = $this->baseUri . '/open-apis/jssdk/ticket/get';
|
|
|
|
|
|
|
|
|
|
|
|
$headers = [
|
|
|
|
|
|
'Authorization: Bearer ' . $accessToken,
|
|
|
|
|
|
'Content-Type: application/json'
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
$result = $this->httpGet($url, $headers);
|
|
|
|
|
|
|
|
|
|
|
|
if ($result['code'] !== 0) {
|
|
|
|
|
|
throw new BusinessException('获取 jsapi_ticket 失败: ' . json_encode($result));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$ticket = $result['ticket'];
|
|
|
|
|
|
$expire = $result['expire_in'] - 300; // 提前5分钟过期
|
|
|
|
|
|
|
|
|
|
|
|
// 缓存 ticket
|
|
|
|
|
|
Cache::set($this->abbr.'-feishu:jsapi_ticket', $ticket, $expire);
|
|
|
|
|
|
// $this->setCachedData('feishu:jsapi_ticket', $ticket, $expire);
|
|
|
|
|
|
|
|
|
|
|
|
return $ticket;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成签名
|
|
|
|
|
|
* @param string $ticket
|
|
|
|
|
|
* @param string $nonceStr
|
|
|
|
|
|
* @param int $timestamp
|
|
|
|
|
|
* @param string $url
|
|
|
|
|
|
* @return string
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function generateSignature($ticket, $nonceStr, $timestamp, $url)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 拼接待签名字符串(注意顺序)
|
|
|
|
|
|
$signStr = "jsapi_ticket={$ticket}&noncestr={$nonceStr}×tamp={$timestamp}&url={$url}";
|
|
|
|
|
|
|
|
|
|
|
|
return sha1($signStr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成随机字符串
|
|
|
|
|
|
* @param int $length
|
|
|
|
|
|
* @return string
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function generateNonceStr($length = 16)
|
|
|
|
|
|
{
|
|
|
|
|
|
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
|
|
|
|
$str = '';
|
|
|
|
|
|
for ($i = 0; $i < $length; $i++) {
|
|
|
|
|
|
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
return $str;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* HTTP GET 请求
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function httpGet($url, $headers = [])
|
|
|
|
|
|
{
|
|
|
|
|
|
$ch = curl_init();
|
|
|
|
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
|
|
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
|
|
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
|
|
|
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
|
|
|
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 不验证证书
|
|
|
|
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // 不验证主机名
|
|
|
|
|
|
|
|
|
|
|
|
$response = curl_exec($ch);
|
|
|
|
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
|
|
curl_close($ch);
|
|
|
|
|
|
|
|
|
|
|
|
if ($httpCode !== 200) {
|
|
|
|
|
|
throw new BusinessException("HTTP GET 请求失败: {$httpCode}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return json_decode($response, true);
|
|
|
|
|
|
}
|
2025-07-01 19:39:13 +08:00
|
|
|
|
}
|