Files
x-server-mobile/backend/app/controller/AuthController.php
T
root 2e5f570b40 feat: 完善鉴权和密码功能,添加飞书通知模块
- 完善Token过期/不存在时跳转登录重新认证
- 实现正常的账号密码登录验证(邮箱/手机号+密码)
- 用户初始化密码设为工号(sn)
- 添加修改密码功能(API+前端页面)
- 修复扫码登录后跳转问题(zustand persist格式)
- 创建飞书通知服务 FeishuNotificationService
- 设置页面添加修改密码入口

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:19:37 +08:00

503 lines
17 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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');
if (!$authCode) {
return $this->error('缺少授权码');
}
try {
$feishuService = new FeishuService();
// 方式1: 根据 app_id 查找飞书应用配置(推荐)
if ($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}");
$feishuService->setCompany($companyId);
}
// 方式2: 根据 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}");
$feishuService->setCompany($companyId);
} else {
return $this->error('缺少 app_id 或 tenant_key 参数');
}
// 获取用户信息
$feishuData = $feishuService->getUserInfo($authCode);
error_log("👤 获取到用户信息: " . json_encode($feishuData));
// 创建或更新用户
$user = $feishuService->createOrUpdateUser($feishuData, $companyId);
// 生成 JWT Token
$token = JwtUtil::encode([
'user_id' => $user->id,
'company_id' => $user->company_id,
'feishu_user_id' => $user->feishu_user_id,
'union_id' => $user->feishu_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');
error_log("📱 qrcodeLogin - tenant_key: {$tenantKey}, app_id: {$appId}");
try {
$feishuService = new \app\service\FeishuService();
$companyId = null;
$feishuApp = null;
// 方式1: 根据 app_id 查找飞书应用配置(推荐)
if ($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}");
}
// 方式2: 根据 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('缺少 app_id 或 tenant_key 参数');
}
$feishuService->setCompany($companyId);
// 生成登录会话
$loginSession = \app\model\FeishuLoginSession::create([
'company_id' => $companyId,
'tenant_key' => $tenantKey,
'status' => 'pending',
'expires_at' => date('Y-m-d H:i:s', time() + 300), // 5分钟过期
]);
// 构建扫码登录 URL
$redirectUri = urlencode("https://xsm.dev.hnxinyouhe.com/login/callback");
$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->feishu_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}");
// 使用 session 的公司信息
$feishuService = new \app\service\FeishuService();
$feishuService->setCompany($session->company_id);
// 获取用户信息
$feishuData = $feishuService->getUserInfo($authCode);
error_log("👤 Got user info: " . json_encode($feishuData));
// 创建或更新用户
$user = $feishuService->createOrUpdateUser($feishuData, $session->company_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->feishu_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());
}
}
}