748 lines
26 KiB
PHP
748 lines
26 KiB
PHP
<?php
|
||
|
||
namespace app\controller;
|
||
|
||
use app\service\FeishuService;
|
||
use app\util\JwtUtil;
|
||
use app\model\User;
|
||
use app\model\Company;
|
||
use Webman\Http\Request;
|
||
use Webman\Http\Response;
|
||
|
||
class AuthController extends BaseController
|
||
{
|
||
/**
|
||
* 用户登录(邮箱/手机号 + 密码)
|
||
*/
|
||
public function login(Request $request): Response
|
||
{
|
||
$username = $request->post('username');
|
||
$password = $request->post('password');
|
||
|
||
if (!$username || !$password) {
|
||
return $this->error('用户名或密码不能为空');
|
||
}
|
||
|
||
// 通过邮箱或手机号查找用户
|
||
$user = User::where(function ($query) use ($username) {
|
||
$query->where('email', $username)
|
||
->orWhere('phone', $username);
|
||
})->where('status', 1)
|
||
->first();
|
||
|
||
if (!$user) {
|
||
return $this->error('用户不存在或已被禁用');
|
||
}
|
||
|
||
// 验证密码(如果没有设置密码,提示使用飞书登录)
|
||
if (!$user->password) {
|
||
return $this->error('该账号未设置密码,请使用飞书登录');
|
||
}
|
||
|
||
if (!password_verify($password, $user->password)) {
|
||
return $this->error('密码错误');
|
||
}
|
||
|
||
// 生成 token
|
||
$token = JwtUtil::encode([
|
||
'user_id' => $user->id,
|
||
'company_id' => $user->company_id,
|
||
'username' => $user->name,
|
||
]);
|
||
|
||
return $this->success([
|
||
'token' => $token,
|
||
'user' => $user->load('company'),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 飞书免登
|
||
*/
|
||
public function feishuLogin(Request $request): Response
|
||
{
|
||
$authCode = $request->post('auth_code');
|
||
$appId = $request->post('app_id');
|
||
$tenantKey = $request->post('tenant_key');
|
||
$companyParam = $request->post('company'); // 新增:company 参数
|
||
|
||
if (!$authCode) {
|
||
return $this->error('缺少授权码');
|
||
}
|
||
|
||
try {
|
||
$feishuService = new FeishuService();
|
||
$companyId = null;
|
||
$feishuApp = null;
|
||
|
||
// 方式1: 根据 company 参数查找公司配置(新增,用于多公司支持)
|
||
if ($companyParam) {
|
||
error_log("🔍 使用 company 参数查找配置: {$companyParam}");
|
||
|
||
// company 参数是 company_id
|
||
$company = \app\model\Company::find($companyParam);
|
||
if (!$company) {
|
||
return $this->error('公司不存在');
|
||
}
|
||
|
||
$companyId = $company->id;
|
||
$tenantKey = $company->tenant_key; // 从公司表获取 tenant_key
|
||
|
||
// 获取该公司的默认飞书应用
|
||
$feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId)
|
||
->where('system_id', 1)
|
||
->where('status', 1)
|
||
->first();
|
||
|
||
if (!$feishuApp) {
|
||
return $this->error('该公司未配置飞书应用');
|
||
}
|
||
|
||
error_log("✅ 通过 company 找到配置: company_id={$companyId}, tenant_key={$tenantKey}, feishu_app_id={$feishuApp->id}, app_id={$feishuApp->app_id}");
|
||
$feishuService->setCompany($companyId);
|
||
$appId = $feishuApp->id;
|
||
}
|
||
// 方式2: 根据 app_id 查找飞书应用配置
|
||
elseif ($appId) {
|
||
error_log("🔍 使用 app_id 查找配置: {$appId}");
|
||
|
||
// 根据 app_id 查找飞书应用
|
||
$feishuApp = \app\model\CompanyFeishuApp::where('app_id', $appId)
|
||
->where('status', 1)
|
||
->first();
|
||
|
||
if (!$feishuApp) {
|
||
return $this->error('飞书应用配置不存在');
|
||
}
|
||
|
||
$companyId = $feishuApp->company_id;
|
||
error_log("✅ 找到飞书应用配置: company_id={$companyId}, feishu_app_id={$feishuApp->id}");
|
||
|
||
$feishuService->setCompany($companyId);
|
||
}
|
||
// 方式3: 根据 tenant_key 查找(兼容旧版)
|
||
elseif ($tenantKey) {
|
||
error_log("🔍 使用 tenant_key 查找配置: {$tenantKey}");
|
||
|
||
$company = $feishuService->getCompanyByTenantKey($tenantKey);
|
||
if (!$company) {
|
||
return $this->error('公司不存在');
|
||
}
|
||
|
||
$companyId = $company->id;
|
||
|
||
// 获取默认飞书应用
|
||
$feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId)
|
||
->where('system_id', 1)
|
||
->where('status', 1)
|
||
->first();
|
||
|
||
if (!$feishuApp) {
|
||
return $this->error('未配置飞书应用');
|
||
}
|
||
|
||
error_log("✅ 找到公司配置: company_id={$companyId}, feishu_app_id={$feishuApp->id}");
|
||
$feishuService->setCompany($companyId);
|
||
|
||
// 设置应用ID
|
||
$appId = $feishuApp->id;
|
||
} else {
|
||
return $this->error('缺少 company、app_id 或 tenant_key 参数');
|
||
}
|
||
|
||
// 获取用户信息
|
||
$feishuData = $feishuService->getUserInfo($authCode);
|
||
error_log("👤 获取到用户信息: " . json_encode($feishuData));
|
||
|
||
// 创建或更新用户(传递应用ID)
|
||
$user = $feishuService->createOrUpdateUser($feishuData, $companyId, $appId);
|
||
|
||
// 生成 JWT Token
|
||
$token = JwtUtil::encode([
|
||
'user_id' => $user->id,
|
||
'company_id' => $user->company_id,
|
||
'feishu_app_id' => $user->feishu_app_id,
|
||
'union_id' => $user->union_id,
|
||
]);
|
||
|
||
error_log("✅ 登录成功: user_id={$user->id}, company_id={$user->company_id}");
|
||
|
||
return $this->success([
|
||
'token' => $token,
|
||
'user' => $user->load('company'),
|
||
]);
|
||
} catch (\Exception $e) {
|
||
error_log("❌ 登录失败: " . $e->getMessage());
|
||
return $this->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取当前用户信息
|
||
*/
|
||
public function me(Request $request): Response
|
||
{
|
||
$userId = $this->getUserId($request);
|
||
|
||
if (!$userId) {
|
||
return $this->error('未登录', 401);
|
||
}
|
||
|
||
$user = User::with('company')->find($userId);
|
||
|
||
if (!$user) {
|
||
return $this->error('用户不存在');
|
||
}
|
||
|
||
return $this->success($user);
|
||
}
|
||
|
||
/**
|
||
* 生成飞书扫码登录二维码
|
||
*/
|
||
public function qrcodeLogin(Request $request): Response
|
||
{
|
||
$tenantKey = $request->post('tenant_key');
|
||
$appId = $request->post('app_id');
|
||
$companyParam = $request->post('company'); // 新增:company 参数
|
||
|
||
error_log("📱 qrcodeLogin - tenant_key: {$tenantKey}, app_id: {$appId}, company: {$companyParam}");
|
||
|
||
try {
|
||
$feishuService = new \app\service\FeishuService();
|
||
$companyId = null;
|
||
$feishuApp = null;
|
||
|
||
// 方式1: 根据 company 参数查找公司配置(新增,用于多公司支持)
|
||
if ($companyParam) {
|
||
error_log("🔍 使用 company 参数查找配置: {$companyParam}");
|
||
|
||
// company 参数是 company_id
|
||
$company = \app\model\Company::find($companyParam);
|
||
if (!$company) {
|
||
return $this->error('公司不存在');
|
||
}
|
||
|
||
$companyId = $company->id;
|
||
$tenantKey = $company->tenant_key; // 从公司表获取 tenant_key
|
||
|
||
// 获取该公司的默认飞书应用
|
||
$feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId)
|
||
->where('system_id', 1)
|
||
->where('status', 1)
|
||
->first();
|
||
|
||
if (!$feishuApp) {
|
||
return $this->error('该公司未配置飞书应用');
|
||
}
|
||
|
||
error_log("✅ 通过 company 找到配置: company_id={$companyId}, tenant_key={$tenantKey}, feishu_app_id={$feishuApp->id}, app_id={$feishuApp->app_id}");
|
||
}
|
||
// 方式2: 根据 app_id 查找飞书应用配置(推荐)
|
||
elseif ($appId) {
|
||
$feishuApp = \app\model\CompanyFeishuApp::where('app_id', $appId)
|
||
->where('status', 1)
|
||
->first();
|
||
|
||
if (!$feishuApp) {
|
||
return $this->error('飞书应用配置不存在');
|
||
}
|
||
|
||
$companyId = $feishuApp->company_id;
|
||
error_log("✅ 通过 app_id 找到配置: company_id={$companyId}");
|
||
}
|
||
// 方式3: 根据 tenant_key 查找(兼容旧版)
|
||
elseif ($tenantKey) {
|
||
$company = $feishuService->getCompanyByTenantKey($tenantKey);
|
||
if (!$company) {
|
||
return $this->error('公司不存在');
|
||
}
|
||
|
||
$companyId = $company->id;
|
||
|
||
// 获取默认飞书应用
|
||
$feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId)
|
||
->where('system_id', 1)
|
||
->where('status', 1)
|
||
->first();
|
||
|
||
if (!$feishuApp) {
|
||
return $this->error('未配置飞书应用');
|
||
}
|
||
|
||
error_log("✅ 通过 tenant_key 找到配置: company_id={$companyId}");
|
||
} else {
|
||
return $this->error('缺少 company、app_id 或 tenant_key 参数');
|
||
}
|
||
|
||
$feishuService->setCompany($companyId);
|
||
|
||
// 生成登录会话
|
||
$loginSession = \app\model\FeishuLoginSession::create([
|
||
'company_id' => $companyId,
|
||
'tenant_key' => $tenantKey,
|
||
'feishu_app_id' => $feishuApp->id,
|
||
'status' => 'pending',
|
||
'expires_at' => date('Y-m-d H:i:s', time() + 300), // 5分钟过期
|
||
]);
|
||
|
||
// 构建扫码登录 URL(使用数据库中配置的回调地址)
|
||
if ($feishuApp->mobile_callback_url) {
|
||
$callbackUrl = $feishuApp->mobile_callback_url;
|
||
} else {
|
||
// 检查反向代理的协议头
|
||
$scheme = request()->header('x-forwarded-proto') ?? request()->header('x-scheme') ?? 'https';
|
||
$callbackUrl = $scheme . '://' . request()->host() . '/login/callback';
|
||
}
|
||
$redirectUri = urlencode($callbackUrl);
|
||
$authUrl = "https://passport.feishu.cn/suite/passport/oauth/authorize?client_id={$feishuApp->app_id}&redirect_uri={$redirectUri}&response_type=code&state={$loginSession->id}";
|
||
|
||
error_log("✅ 生成二维码会话: session_id={$loginSession->id}");
|
||
|
||
return $this->success([
|
||
'session_id' => $loginSession->id,
|
||
'auth_url' => $authUrl,
|
||
'expires_at' => $loginSession->expires_at,
|
||
]);
|
||
} catch (\Exception $e) {
|
||
error_log("❌ qrcodeLogin 失败: " . $e->getMessage());
|
||
return $this->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 轮询扫码登录状态
|
||
*/
|
||
public function qrcodeLoginStatus(Request $request): Response
|
||
{
|
||
$sessionId = $request->get('session_id');
|
||
|
||
if (!$sessionId) {
|
||
return $this->error('缺少会话ID');
|
||
}
|
||
|
||
$session = \app\model\FeishuLoginSession::find($sessionId);
|
||
|
||
if (!$session) {
|
||
return $this->error('登录会话不存在');
|
||
}
|
||
|
||
// 检查是否过期
|
||
if (strtotime($session->expires_at) < time()) {
|
||
$session->status = 'expired';
|
||
$session->save();
|
||
return $this->error('二维码已过期,请刷新重试');
|
||
}
|
||
|
||
// 返回当前状态
|
||
if ($session->status === 'success') {
|
||
// 生成 token
|
||
$user = \app\model\User::find($session->user_id);
|
||
if (!$user) {
|
||
return $this->error('用户不存在');
|
||
}
|
||
|
||
$token = \app\util\JwtUtil::encode([
|
||
'user_id' => $user->id,
|
||
'company_id' => $user->company_id,
|
||
'feishu_user_id' => $user->feishu_user_id,
|
||
'union_id' => $user->union_id,
|
||
]);
|
||
|
||
// 清除会话
|
||
$session->delete();
|
||
|
||
return $this->success([
|
||
'status' => 'success',
|
||
'token' => $token,
|
||
'user' => $user->load('company'),
|
||
]);
|
||
} else if ($session->status === 'expired') {
|
||
return $this->error('二维码已过期,请刷新重试');
|
||
} else {
|
||
// pending 或 scanning
|
||
return $this->success([
|
||
'status' => $session->status,
|
||
]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 飞书扫码登录回调(飞书授权后的回调)
|
||
*/
|
||
public function qrcodeLoginCallback(Request $request): Response
|
||
{
|
||
// 从 state 参数获取 session_id
|
||
$sessionId = $request->get('state');
|
||
// OAuth 2.0 标准参数是 code
|
||
$authCode = $request->get('code');
|
||
|
||
error_log("🔔 QRCode callback received - session_id: {$sessionId}, code: " . substr($authCode ?? 'null', 0, 20) . "...");
|
||
|
||
// 如果没有参数,直接返回空页面(可能是 QR SDK 的预检请求)
|
||
if (!$sessionId && !$authCode) {
|
||
error_log("ℹ️ Callback called without parameters, returning empty page");
|
||
return new \Webman\Http\Response(200, ['Content-Type' => 'text/html; charset=utf-8'], '');
|
||
}
|
||
|
||
if (!$sessionId) {
|
||
error_log("❌ Callback missing session_id");
|
||
return $this->error('缺少会话ID');
|
||
}
|
||
|
||
if (!$authCode) {
|
||
error_log("❌ Callback missing auth_code");
|
||
return $this->error('缺少授权码');
|
||
}
|
||
|
||
try {
|
||
$session = \app\model\FeishuLoginSession::find($sessionId);
|
||
|
||
error_log("🔍 Session found: " . ($session ? "id={$session->id}, status={$session->status}" : "null"));
|
||
|
||
if (!$session) {
|
||
error_log("❌ Session not found for id: {$sessionId}");
|
||
return $this->error('登录会话不存在');
|
||
}
|
||
|
||
// 检查是否过期
|
||
if (strtotime($session->expires_at) < time()) {
|
||
error_log("⏰ Session expired at: {$session->expires_at}");
|
||
$session->status = 'expired';
|
||
$session->save();
|
||
return $this->error('二维码已过期');
|
||
}
|
||
|
||
error_log("🔄 Processing login for company_id: {$session->company_id}, feishu_app_id: {$session->feishu_app_id}");
|
||
|
||
// 使用 session 的公司信息
|
||
$feishuService = new \app\service\FeishuService();
|
||
$feishuService->setCompany($session->company_id);
|
||
|
||
// 获取用户信息
|
||
$feishuData = $feishuService->getUserInfo($authCode);
|
||
error_log("👤 Got user info: " . json_encode($feishuData));
|
||
|
||
// 创建或更新用户(传递飞书应用ID)
|
||
$user = $feishuService->createOrUpdateUser($feishuData, $session->company_id, $session->feishu_app_id);
|
||
error_log("✅ User created/updated: user_id={$user->id}");
|
||
|
||
// 更新会话状态
|
||
$session->status = 'success';
|
||
$session->user_id = $user->id;
|
||
$saved = $session->save();
|
||
error_log("💾 Session save result: " . ($saved ? 'success' : 'failed') . ", new status: {$session->status}");
|
||
|
||
// 生成 JWT Token
|
||
$token = \app\util\JwtUtil::encode([
|
||
'user_id' => $user->id,
|
||
'company_id' => $user->company_id,
|
||
'feishu_user_id' => $user->feishu_user_id,
|
||
'union_id' => $user->union_id,
|
||
]);
|
||
error_log("🎫 Generated token for user: {$user->id}");
|
||
|
||
// 返回成功页面(包含 token,前端会保存到 localStorage)
|
||
$userName = htmlspecialchars($user->name ?? '用户');
|
||
// 准备用户数据JSON
|
||
$userJson = json_encode([
|
||
'id' => $user->id,
|
||
'name' => $user->name,
|
||
'email' => $user->email,
|
||
'avatar' => $user->avatar,
|
||
'phone' => $user->phone,
|
||
'company_id' => $user->company_id,
|
||
'status' => $user->status,
|
||
], JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE);
|
||
|
||
$html = <<<HTML
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>登录成功</title>
|
||
<style>
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 100vh;
|
||
margin: 0;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
}
|
||
.container {
|
||
text-align: center;
|
||
padding: 40px;
|
||
background: rgba(255,255,255,0.1);
|
||
border-radius: 20px;
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
.icon {
|
||
font-size: 60px;
|
||
margin-bottom: 20px;
|
||
}
|
||
h1 {
|
||
margin: 0 0 10px 0;
|
||
font-size: 24px;
|
||
}
|
||
p {
|
||
margin: 10px 0;
|
||
opacity: 0.9;
|
||
}
|
||
.spinner {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 4px solid rgba(255,255,255,0.3);
|
||
border-top-color: white;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin: 20px auto;
|
||
}
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="icon">✅</div>
|
||
<h1>登录成功!</h1>
|
||
<p>欢迎回来,{$userName}</p>
|
||
<p>正在跳转到应用...</p>
|
||
<div class="spinner"></div>
|
||
</div>
|
||
<script>
|
||
// 保存 token 到 localStorage
|
||
try {
|
||
localStorage.setItem('token', '$token');
|
||
localStorage.setItem('i18nextLng', 'zh');
|
||
console.log('✅ Token saved to localStorage');
|
||
|
||
// 保存用户信息到 zustand persist storage(正确的格式)
|
||
const userState = {
|
||
state: {
|
||
user: $userJson,
|
||
token: '$token',
|
||
currentCompany: null,
|
||
isLoggedIn: true
|
||
},
|
||
version: 0
|
||
};
|
||
localStorage.setItem('user-storage', JSON.stringify(userState));
|
||
console.log('✅ User state saved to localStorage:', userState);
|
||
|
||
// 通知父窗口登录成功(如果在弹窗中)
|
||
if (window.opener) {
|
||
window.opener.postMessage({
|
||
type: 'feishu-login-success',
|
||
data: { token: '$token', user: $userJson }
|
||
}, '*');
|
||
setTimeout(function() {
|
||
window.close();
|
||
}, 500);
|
||
} else {
|
||
// 否则跳转到首页
|
||
setTimeout(function() {
|
||
window.location.href = '/dashboard';
|
||
}, 1500);
|
||
}
|
||
} catch (e) {
|
||
console.error('❌ Failed to save data:', e);
|
||
// 如果 localStorage 不可用,使用 sessionStorage
|
||
sessionStorage.setItem('token', '$token');
|
||
setTimeout(function() {
|
||
window.location.href = '/dashboard';
|
||
}, 1500);
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|
||
HTML;
|
||
return new \Webman\Http\Response(200, ['Content-Type' => 'text/html; charset=utf-8'], $html);
|
||
} catch (\Exception $e) {
|
||
return $this->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取飞书配置(用于前端免登)
|
||
* 根据 company 参数返回对应的 appId
|
||
*/
|
||
public function feishuConfig(Request $request): Response
|
||
{
|
||
$companyParam = $request->get('company');
|
||
|
||
if (!$companyParam) {
|
||
return $this->error('缺少 company 参数');
|
||
}
|
||
|
||
try {
|
||
// company 参数是 company_id
|
||
$company = \app\model\Company::find($companyParam);
|
||
if (!$company) {
|
||
return $this->error('公司不存在');
|
||
}
|
||
|
||
$companyId = $company->id;
|
||
|
||
// 获取该公司的默认飞书应用
|
||
$feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId)
|
||
->where('system_id', 1)
|
||
->where('status', 1)
|
||
->first();
|
||
|
||
if (!$feishuApp) {
|
||
return $this->error('该公司未配置飞书应用');
|
||
}
|
||
|
||
error_log("✅ feishuConfig - company_id={$companyId}, app_id={$feishuApp->app_id}");
|
||
|
||
return $this->success([
|
||
'app_id' => $feishuApp->app_id,
|
||
'company_id' => $companyId,
|
||
]);
|
||
} catch (\Exception $e) {
|
||
error_log("❌ feishuConfig 失败: " . $e->getMessage());
|
||
return $this->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取飞书 JS API 鉴权签名
|
||
* 用于前端初始化飞书 JS SDK
|
||
*
|
||
* company 参数可选,如果未提供则从登录 token 中获取用户所属公司
|
||
*/
|
||
public function feishuJsapiSignature(Request $request): Response
|
||
{
|
||
$url = $request->get('url');
|
||
$companyParam = $request->get('company');
|
||
|
||
if (!$url) {
|
||
return $this->error('缺少 url 参数');
|
||
}
|
||
|
||
try {
|
||
$companyId = null;
|
||
|
||
// 优先使用传入的 company 参数
|
||
if ($companyParam) {
|
||
$company = \app\model\Company::find($companyParam);
|
||
if (!$company) {
|
||
return $this->error('公司不存在');
|
||
}
|
||
$companyId = $company->id;
|
||
error_log("✅ 使用参数中的 company: {$companyId}");
|
||
}
|
||
// 从 token 中获取用户所属公司
|
||
else {
|
||
$companyId = $this->getCompanyId($request);
|
||
if (!$companyId) {
|
||
return $this->error('未登录且未指定公司', 401);
|
||
}
|
||
error_log("✅ 从 token 获取 company: {$companyId}");
|
||
}
|
||
|
||
// 获取该公司的默认飞书应用
|
||
$feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId)
|
||
->where('system_id', 1)
|
||
->where('status', 1)
|
||
->first();
|
||
|
||
if (!$feishuApp) {
|
||
return $this->error('该公司未配置飞书应用');
|
||
}
|
||
|
||
// 生成 JS API 签名
|
||
$signatureData = $this->generateJsapiSignature($feishuApp->id, $url);
|
||
|
||
return $this->success([
|
||
'app_id' => $feishuApp->app_id,
|
||
'company_id' => $companyId,
|
||
'jsapi_ticket' => $signatureData['jsapi_ticket'],
|
||
'noncestr' => $signatureData['noncestr'],
|
||
'timestamp' => $signatureData['timestamp'],
|
||
'url' => $signatureData['url'],
|
||
'signature' => $signatureData['signature'],
|
||
]);
|
||
} catch (\Exception $e) {
|
||
error_log("❌ feishuJsapiSignature 失败: " . $e->getMessage());
|
||
return $this->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成 JS API 鉴权签名
|
||
*
|
||
* @param int $feishuAppId 飞书应用配置ID
|
||
* @param string $url 当前页面URL
|
||
* @return array 签名数据
|
||
*/
|
||
private function generateJsapiSignature(int $feishuAppId, string $url): array
|
||
{
|
||
// 获取 jsapi_ticket
|
||
$jsapiTicket = \app\util\FeishuUtil::getJsapiTicket($feishuAppId);
|
||
|
||
// 清理 URL:移除 # 和 ? 后面的参数
|
||
$url = $this->cleanUrl($url);
|
||
|
||
// 生成随机字符串
|
||
$noncestr = $this->generateNonceStr();
|
||
|
||
// 获取毫秒级时间戳
|
||
$timestamp = (int)round(microtime(true) * 1000);
|
||
|
||
// 拼接签名字符串
|
||
$verifyStr = "jsapi_ticket={$jsapiTicket}&noncestr={$noncestr}×tamp={$timestamp}&url={$url}";
|
||
|
||
// 生成 SHA1 签名
|
||
$signature = sha1($verifyStr);
|
||
|
||
error_log("✅ JS API 签名生成成功: url={$url}");
|
||
|
||
return [
|
||
'jsapi_ticket' => $jsapiTicket,
|
||
'noncestr' => $noncestr,
|
||
'timestamp' => $timestamp,
|
||
'url' => $url,
|
||
'signature' => $signature,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 清理 URL,移除 # 和 ? 后面的参数
|
||
*/
|
||
private function cleanUrl(string $url): string
|
||
{
|
||
// 移除 # 后面的内容
|
||
$pos = strpos($url, '#');
|
||
if ($pos !== false) {
|
||
$url = substr($url, 0, $pos);
|
||
}
|
||
|
||
// 移除 ? 后面的内容
|
||
$pos = strpos($url, '?');
|
||
if ($pos !== false) {
|
||
$url = substr($url, 0, $pos);
|
||
}
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* 生成随机字符串(由数字、字母组成)
|
||
*/
|
||
private function generateNonceStr(int $length = 16): string
|
||
{
|
||
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||
$noncestr = '';
|
||
for ($i = 0; $i < $length; $i++) {
|
||
$noncestr .= $chars[mt_rand(0, strlen($chars) - 1)];
|
||
}
|
||
return $noncestr;
|
||
}
|
||
}
|