feat: 飞书登录支持多租户和用户工号管理

后端改动:
- FeishuService: 从飞书用户信息获取 tenant_key 和手机号
- FeishuService: 新增 user_sn 表工号分配机制
- FeishuService: 优先使用 enterprise_email,支持手机号
- AuthController: 飞书免登支持 company 参数

前端改动:
- FeishuAutoLogin: 支持多租户 company 参数
- login/index: 支持 redirect 参数和已登录检测
- QRCodeLogin: 支持多租户参数传递
- router: 新增路由守卫和参数处理
- api.ts: 优化飞书登录 API 调用

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-27 17:57:49 +08:00
parent b1895be50d
commit b1ca2c58b7
8 changed files with 241 additions and 41 deletions
+6 -3
View File
@@ -60,7 +60,10 @@ export default function FeishuAutoLogin() {
tryFeishuAutoLogin()
function tryFeishuAutoLogin() {
const appId = 'cli_a8033d607cba500d'
// 从 URL 获取 company 参数
const urlParams = new URLSearchParams(window.location.search)
const companyParam = urlParams.get('company')
console.log('🔍 URL 中的 company 参数:', companyParam)
// 打印 tt 对象的所有方法
if (window.tt) {
@@ -75,8 +78,8 @@ export default function FeishuAutoLogin() {
// 方式1: tt.requestAuthCode
if (window.tt && typeof window.tt.requestAuthCode === 'function') {
console.log('使用 tt.requestAuthCode')
// 不传 appId,让飞书自动选择(后端会根据 company 参数处理)
window.tt.requestAuthCode({
appId,
success: (res: any) => {
console.log('✅ 获取到授权码:', res.code?.substring(0, 20))
handleLogin(res.code)
@@ -117,7 +120,7 @@ export default function FeishuAutoLogin() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
auth_code: authCode,
tenant_key: '14e7bc0fe5985740',
company: urlParams.get('company') || undefined,
}),
})
+27 -3
View File
@@ -92,13 +92,37 @@ export default function QRCodeLogin({ visible, onClose }: QRCodeLoginProps) {
console.log('🔍 window.QRLogin:', !!window.QRLogin)
// 从 URL 获取 company 参数(支持两种方式:直接参数或从 redirect 中提取)
const urlParams = new URLSearchParams(window.location.search)
let companyParam = urlParams.get('company')
// 如果当前 URL 没有 company,尝试从 redirect 参数中获取
if (!companyParam) {
const redirect = urlParams.get('redirect')
if (redirect) {
try {
const redirectUrl = new URL(redirect, window.location.origin)
companyParam = redirectUrl.searchParams.get('company')
console.log('🔍 从 redirect URL 中提取的 company 参数:', companyParam)
} catch (e) {
console.log('⚠️ 无法解析 redirect URL:', redirect)
}
}
}
console.log('🔍 最终使用的 company 参数:', companyParam)
// 生成登录会话
const requestBody: any = {}
if (companyParam) {
// 如果有 company 参数,传递给后端
requestBody.company = companyParam
}
const response = await fetch('/api/v1/auth/qrcode-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tenant_key: '14e7bc0fe5985740',
}),
body: JSON.stringify(requestBody),
})
const result = await response.json()
+65 -7
View File
@@ -21,12 +21,38 @@ declare global {
export default function Login() {
const navigate = useNavigate()
const { login } = useUserStore()
const { login, user } = useUserStore()
const [loading, setLoading] = useState(true)
const [environment, setEnvironment] = useState<'feishu' | 'external' | null>(null)
const [showQRCode, setShowQRCode] = useState(false)
useEffect(() => {
// 首先检查是否已登录
const token = localStorage.getItem('token')
const urlParams = new URLSearchParams(window.location.search)
const redirect = urlParams.get('redirect')
console.log('🔐 登录页检查:', { hasToken: !!token, hasUser: !!user, redirect })
// 如果有 token,说明已登录
if (token) {
console.log('✅ 检测到 token,用户已登录')
// 如果有 redirect 参数,跳转到目标页面
if (redirect) {
console.log('✅ 跳转到 redirect 页面:', redirect)
window.location.href = redirect
return
}
// 否则跳转到 dashboard
console.log('✅ 跳转到 dashboard')
navigate('/dashboard')
return
}
// 没有token,继续登录流程
console.log('⚠️ 未检测到 token,显示登录页面')
setLoading(false)
// 监听飞书登录成功事件
const handleFeishuLoginSuccess = ((e: any) => {
console.log('🎉 收到飞书登录成功事件:', e.detail)
@@ -39,7 +65,7 @@ export default function Login() {
return () => {
window.removeEventListener('feishu-login-success', handleFeishuLoginSuccess)
}
}, [])
}, [user, login, navigate])
useEffect(() => {
// 打印详细的环境信息
@@ -103,7 +129,20 @@ export default function Login() {
console.log('🔍 window.tt:', window.tt)
console.log('🔍 window.tt 类型:', typeof window.tt)
const appId = 'cli_a8033d607cba500d'
// 从 URL 获取 company 参数(可能来自当前 URL 或 redirect URL)
const urlParams = new URLSearchParams(window.location.search)
let companyParam = urlParams.get('company')
console.log('🔍 URL 中的 company 参数:', companyParam)
// 如果当前 URL 没有 company,尝试从 redirect 参数中获取
if (!companyParam) {
const redirect = urlParams.get('redirect')
if (redirect) {
const redirectUrl = new URL(redirect, window.location.origin)
companyParam = redirectUrl.searchParams.get('company')
console.log('🔍 从 redirect URL 中提取的 company 参数:', companyParam)
}
}
// 详细检查 tt 对象
if (window.tt) {
@@ -137,10 +176,10 @@ export default function Login() {
// 尝试调用 requestAuthCode
try {
console.log('🔧 调用 tt.requestAuthCode,appId:', appId)
console.log('🔧 调用 tt.requestAuthCode,company:', companyParam)
// 不传 appId,让飞书自动选择(后端会根据 company 参数处理)
window.tt.requestAuthCode({
appId: appId,
success: (res: any) => {
console.log('✅ 成功获取授权码:', res)
console.log('✅ code:', res.code?.substring(0, 30))
@@ -198,11 +237,28 @@ export default function Login() {
// 处理飞书登录
const handleFeishuLogin = async (authCode: string) => {
try {
// 从 URL 获取 app_id 参数
// 从 URL 获取参数
const urlParams = new URLSearchParams(window.location.search)
let companyParam = urlParams.get('company')
const urlAppId = urlParams.get('app_id')
console.log('🔍 URL 中的 app_id:', urlAppId)
// 如果当前 URL 没有 company,尝试从 redirect 参数中获取
if (!companyParam) {
const redirect = urlParams.get('redirect')
if (redirect) {
try {
const redirectUrl = new URL(redirect, window.location.origin)
companyParam = redirectUrl.searchParams.get('company')
console.log('🔍 从 redirect URL 中提取的 company 参数:', companyParam)
} catch (e) {
console.log('⚠️ 无法解析 redirect URL:', redirect)
}
}
}
console.log('🔍 URL 中的参数:')
console.log(' - app_id:', urlAppId)
console.log(' - company:', companyParam)
const response = await fetch('/api/v1/auth/feishu-login', {
method: 'POST',
@@ -210,6 +266,7 @@ export default function Login() {
body: JSON.stringify({
auth_code: authCode,
app_id: urlAppId || undefined, // 如果 URL 有 app_id 就传递
company: companyParam || undefined, // 传递 company 参数
}),
})
@@ -224,6 +281,7 @@ export default function Login() {
const redirect = urlParams.get('redirect')
if (redirect) {
console.log('🔗 登录成功,跳转到:', redirect)
// redirect 参数可能包含 ?company=xxx,需要直接跳转
window.location.href = redirect
} else {
navigate('/dashboard')
+6
View File
@@ -12,6 +12,7 @@ import SelectCc from '@/pages/business-trip/select-cc'
import ApprovalList from '@/pages/approval/list'
import ApprovalDetail from '@/pages/approval/detail'
import QuickAction from '@/pages/approval/quick-action'
import RejectPage from '@/pages/approval/reject'
import Login from '@/pages/login'
import SimpleLogin from '@/pages/login-simple'
import DebugFeishu from '@/pages/debug-feishu'
@@ -30,15 +31,20 @@ const router = createBrowserRouter([
{ path: 'business-trip/apply', element: <BusinessTripApply /> },
{ path: 'business-trip/apply/select-approver', element: <SelectApprover /> },
{ path: 'business-trip/apply/select-cc', element: <SelectCc /> },
{ path: 'business-trip/detail/:id', element: <BusinessTripDetail /> },
{ path: 'business-trip/:id', element: <BusinessTripDetail /> },
{ path: 'approval', element: <ApprovalList /> },
{ path: 'approval/:id', element: <ApprovalDetail /> },
],
},
// 详情页移到顶层,支持未登录访问(页面自己处理登录检查)
{ path: '/approval/detail/:id', element: <ApprovalDetail /> },
{ path: '/business-trip/detail/:id', element: <BusinessTripDetail /> },
{ path: '/login', element: <Login /> },
{ path: '/login-simple', element: <SimpleLogin /> },
{ path: '/debug-feishu', element: <DebugFeishu /> },
{ path: '/approval/quick-action', element: <QuickAction /> },
{ path: '/approval/reject', element: <RejectPage /> },
{ path: '*', element: <Navigate to='/login' replace /> },
])
+17 -12
View File
@@ -11,22 +11,29 @@ const handleUnauthorized = () => {
if (isLoggingOut) return
isLoggingOut = true
// 保存当前页面信息,用于登录后跳转回来
const currentUrl = window.location.pathname + window.location.search
console.log('🚨 handleUnauthorized 被调用, currentUrl:', currentUrl)
sessionStorage.setItem('redirect_after_login', currentUrl)
// 清除本地存储
localStorage.removeItem('token')
localStorage.removeItem('user-storage')
// 显示提示
Toast.show({
content: '登录已过期,请重新登录',
duration: 2000,
content: '请先登录',
duration: 1500,
})
// 延迟跳转,确保提示显示
setTimeout(() => {
isLoggingOut = false
// 跳转到登录页,并清除历史记录
window.location.href = '/login'
}, 2000)
const redirectUrl = '/login?redirect=' + encodeURIComponent(currentUrl)
console.log('🚨 准备跳转到登录页, redirectUrl:', redirectUrl)
// 跳转到登录页,带上 redirect 参数
window.location.href = redirectUrl
}, 500)
}
// 创建 axios 实例
@@ -82,19 +89,17 @@ export const authApi = {
login: (username: string, password: string) =>
api.post('/auth/login', { username, password }),
// 飞书免登(支持 app_id 或 tenant_key)
feishuLogin: (authCode: string, options?: { appId?: string; tenantKey?: string }) =>
// 飞书免登(支持 app_id、tenant_key 或 company)
feishuLogin: (authCode: string, company?: string) =>
api.post('/auth/feishu-login', {
auth_code: authCode,
app_id: options?.appId,
tenant_key: options?.tenantKey,
company: company,
}),
// 生成二维码登录会话
qrcodeLogin: (appId?: string, tenantKey?: string) =>
qrcodeLogin: (company?: string) =>
api.post('/auth/qrcode-login', {
app_id: appId,
tenant_key: tenantKey,
company: company,
}),
// 轮询二维码登录状态