feat: 实现多端飞书登录功能
1. 飞书客户端内:静默自动登录
- 检测飞书环境 (userAgent + ttjsBridge)
- 调用 ttjsBridge.getAuthCode() 获取授权码
- 自动登录到后台
2. 飞书客户端外:扫码登录
- 后端新增三个接口:
* POST /api/v1/auth/qrcode-login - 生成登录会话和二维码
* GET /api/v1/auth/qrcode-login/status - 轮询登录状态
* GET /login/qrcode/{id} - 飞书授权回调
- 前端新增 QRCodeLogin 组件:
* 使用 qrcode.react 生成二维码
* 每 2 秒轮询登录状态
* 状态:loading → pending → scanning → success/expired
3. 数据库表:feishu_login_session
- 存储 session_id, auth_url, status, user_id
- 状态:pending, scanning, success, expired
- 2 分钟过期机制
4. 其他优化
- 移除审批详情页的 header
- 更新 PROJECT_ISSUES.md 文档
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -108,4 +108,160 @@ class AuthController extends BaseController
|
||||
|
||||
return $this->success($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成飞书扫码登录二维码
|
||||
*/
|
||||
public function qrcodeLogin(Request $request): Response
|
||||
{
|
||||
$tenantKey = $request->post('tenant_key');
|
||||
|
||||
if (!$tenantKey) {
|
||||
return $this->error('缺少租户密钥');
|
||||
}
|
||||
|
||||
try {
|
||||
$feishuService = new \app\service\FeishuService();
|
||||
|
||||
// 根据 tenant_key 获取公司
|
||||
$company = $feishuService->getCompanyByTenantKey($tenantKey);
|
||||
|
||||
if (!$company) {
|
||||
return $this->error('公司不存在');
|
||||
}
|
||||
|
||||
$feishuService->setCompany($company->id);
|
||||
|
||||
// 生成登录会话
|
||||
$loginSession = \app\model\FeishuLoginSession::create([
|
||||
'company_id' => $company->id,
|
||||
'tenant_key' => $tenantKey,
|
||||
'status' => 'pending',
|
||||
'expires_at' => date('Y-m-d H:i:s', time() + 300), // 5分钟过期
|
||||
]);
|
||||
|
||||
// 构建扫码登录 URL
|
||||
$appId = 'cli_a8033d607cba500d';
|
||||
$redirectUri = urlencode("https://xsm.dev.hnxinyouhe.com/login/qrcode/{$loginSession->id}");
|
||||
$authUrl = "https://open.feishu.cn/open-apis/authen/v1/oidc/authorize?app_id={$appId}&redirect_uri={$redirectUri}&state={$loginSession->id}";
|
||||
|
||||
// 生成二维码内容(返回 URL,前端生成二维码)
|
||||
return $this->success([
|
||||
'session_id' => $loginSession->id,
|
||||
'auth_url' => $authUrl,
|
||||
'expires_at' => $loginSession->expires_at,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
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
|
||||
{
|
||||
$sessionId = $this->getRouteParam($request, 'id');
|
||||
$authCode = $request->get('auth_code');
|
||||
|
||||
if (!$sessionId) {
|
||||
return $this->error('缺少会话ID');
|
||||
}
|
||||
|
||||
if (!$authCode) {
|
||||
return $this->error('缺少授权码');
|
||||
}
|
||||
|
||||
try {
|
||||
$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('二维码已过期');
|
||||
}
|
||||
|
||||
// 使用 session 的公司信息
|
||||
$feishuService = new \app\service\FeishuService();
|
||||
$feishuService->setCompany($session->company_id);
|
||||
|
||||
// 获取用户信息
|
||||
$feishuData = $feishuService->getUserInfo($authCode);
|
||||
|
||||
// 创建或更新用户
|
||||
$user = $feishuService->createOrUpdateUser($feishuData, $session->company_id);
|
||||
|
||||
// 更新会话状态
|
||||
$session->status = 'success';
|
||||
$session->user_id = $user->id;
|
||||
$session->save();
|
||||
|
||||
// 重定向到前端页面
|
||||
return \Webman\Http\Response::redirect('https://xsm.dev.hnxinyouhe.com/login');
|
||||
} catch (\Exception $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class FeishuLoginSession extends Model
|
||||
{
|
||||
protected $table = 'feishu_login_session';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'tenant_key',
|
||||
'status',
|
||||
'user_id',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => 'string',
|
||||
'user_id' => 'integer',
|
||||
'company_id' => 'integer',
|
||||
'expires_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
const STATUS_PENDING = 'pending'; // 待扫码
|
||||
const STATUS_SCANNING = 'scanning'; // 已扫码,待授权
|
||||
const STATUS_SUCCESS = 'success'; // 登录成功
|
||||
const STATUS_EXPIRED = 'expired'; // 已过期
|
||||
|
||||
/**
|
||||
* 关联公司
|
||||
*/
|
||||
public function company()
|
||||
{
|
||||
return $this->belongsTo(Company::class, 'company_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联用户
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ $authMiddleware = array_merge($apiMiddleware, [app\middleware\AuthMiddleware::cl
|
||||
Route::group('/api/v1', function () {
|
||||
Route::post('/auth/login', [app\controller\AuthController::class, 'login']);
|
||||
Route::post('/auth/feishu-login', [app\controller\AuthController::class, 'feishuLogin']);
|
||||
Route::post('/auth/qrcode-login', [app\controller\AuthController::class, 'qrcodeLogin']);
|
||||
Route::get('/auth/qrcode-login/status', [app\controller\AuthController::class, 'qrcodeLoginStatus']);
|
||||
Route::get('/login/qrcode/{id}', [app\controller\AuthController::class, 'qrcodeLoginCallback']);
|
||||
})->middleware($apiMiddleware);
|
||||
|
||||
// 用户路由(需要认证)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use support\Db;
|
||||
|
||||
return new class
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Db::statement('
|
||||
CREATE TABLE IF NOT EXISTS `feishu_login_session` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`company_id` bigint unsigned NOT NULL COMMENT '公司ID',
|
||||
`tenant_key` varchar(64) NOT NULL COMMENT '租户密钥',
|
||||
`status` varchar(20) NOT NULL DEFAULT \'pending\' COMMENT \'pending: 待扫码, scanning: 已扫码待授权, success: 登录成功, expired: 已过期\'',
|
||||
`user_id` bigint unsigned DEFAULT NULL COMMENT \'用户ID(登录成功后填充)\'',
|
||||
`expires_at` datetime NOT NULL COMMENT \'过期时间\'',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_company_status` (`company_id`, `status`),
|
||||
KEY `idx_expires_at` (`expires_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=\'飞书扫码登录会话表\'
|
||||
');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Db::statement('DROP TABLE IF EXISTS `feishu_login_session`');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user