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:
root
2026-02-26 09:58:19 +08:00
parent 8110b1e5e6
commit df07b16c7a
12 changed files with 892 additions and 51 deletions
+161
View File
@@ -535,3 +535,164 @@ if (in_array($userId, $approverIds)) {
### 数据库
- `approval_node` 表 - 删除重复节点
- `approval_order` 表 - 修正 current_nodes 字段
---
## 24. 多端飞书登录功能实现
### 需求背景
支持两种登录场景:
1. **飞书客户端内**:静默自动登录,无需用户操作
2. **飞书客户端外**:邮箱密码登录 + 飞书扫码登录
### 实现方案
#### 1. 飞书客户端内 - 静默授权
**环境检测**:
```typescript
const ua = navigator.userAgent.toLowerCase()
const isInFeishu = ua.includes('feishu') || ua.includes('lark') ||
(window as any).ttjsBridge !== undefined
```
**调用 ttjsBridge 获取授权码**:
```typescript
if ((window as any).ttjsBridge) {
(window as any).ttjsBridge.getAuthCode({
success: (authCode: string) => {
handleFeishuLogin(authCode)
},
fail: (error: any) => {
Toast.show('获取授权失败,请重试')
},
})
}
```
**相关文件**:`frontend/src/pages/login/index.tsx`
#### 2. 飞书客户端外 - 扫码登录
**后端接口**:
1. `POST /api/v1/auth/qrcode-login` - 生成登录会话和二维码 URL
2. `GET /api/v1/auth/qrcode-login/status?session_id={id}` - 轮询登录状态
3. `GET /login/qrcode/{id}` - 飞书授权回调地址
**数据库表**:`feishu_login_session`
- `session_id` - 会话 ID(UUID)
- `status` - pending | scanning | success | expired
- `auth_url` - 飞书授权 URL
- `expires_at` - 过期时间(2分钟)
**前端实现**:
- 使用 `qrcode.react` 生成二维码
- 每 2 秒轮询一次登录状态
- 状态更新:loading → pending → scanning → success
**相关文件**:
- `frontend/src/pages/login/QRCodeLogin.tsx` - 二维码登录组件
- `backend/app/controller/AuthController.php` - 后端接口
- `backend/app/model/FeishuLoginSession.php` - 会话模型
#### 3. 登录流程图
```
飞书内:
┌─────────┐ ttjsBridge ┌──────────┐ auth_code ┌─────────┐
│ 用户 │ ───────────────> │ 飞书API │ ──────────────> │ 后端 │
└─────────┘ getAuthCode() └──────────┘ 静默授权 └─────────┘
│
v
返回 token + user
自动登录成功
飞书外(扫码):
┌─────────┐ ┌──────────┐ ┌─────────┐
│ 用户 │ ──> 显示二维码 ──> │ 手机扫码 │ ──> 飞书授权 ──> │ 后端 │
└─────────┘ └──────────┘ └─────────┘
│ │
│ 每2秒轮询 status │
│<─────────────────────────────────────────────────────────────│
│ v
│ 返回 token + user
│<─────────────────────────────────────────────────────────────│
│
v
自动登录成功
```
### 关键技术点
1. **ttjsBridge 类型声明**:
```typescript
declare global {
interface Window {
ttjsBridge?: {
getAuthCode: (params: any) => void
}
}
}
```
2. **登录成功事件通知**:
```typescript
// QRCodeLogin 组件触发事件
window.dispatchEvent(new CustomEvent('feishu-login-success', {
detail: { user, token }
}))
// 主登录页监听事件
useEffect(() => {
const handleFeishuLoginSuccess = (e: any) => {
const { user, token } = e.detail
login(user, token)
}
window.addEventListener('feishu-login-success', handleFeishuLoginSuccess)
return () => {
window.removeEventListener('feishu-login-success', handleFeishuLoginSuccess)
}
}, [])
```
3. **二维码状态管理**:
```typescript
const [status, setStatus] = useState<'loading' | 'pending' | 'scanning' | 'success' | 'expired'>('loading')
const getStatusColor = () => {
switch (status) {
case 'pending': return '#1677ff' // 蓝色 - 待扫码
case 'scanning': return '#faad14' // 橙色 - 已扫码待确认
case 'success': return '#52c41a' // 绿色 - 成功
case 'expired': return '#ff4d4f' // 红色 - 过期
}
}
```
### 依赖安装
```bash
npm install qrcode.react
```
### 数据库迁移
```sql
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(登录成功后填充)',
`session_id` varchar(64) NOT NULL COMMENT '会话ID',
`auth_url` text COMMENT '飞书授权URL',
`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`),
UNIQUE KEY `uk_session_id` (`session_id`),
KEY `idx_company_status` (`company_id`, `status`),
KEY `idx_expires_at` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='飞书扫码登录会话表';
```
### 配置要求
- 飞书应用 `tenant_key`: `14e7bc0fe5985740`
- 飞书授权回调地址: `https://xsm.dev.hnxinyouhe.com/login/qrcode/{id}`