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:
@@ -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}`
|
||||
|
||||
Reference in New Issue
Block a user