diff --git a/backend/app/controller/AuthController.php b/backend/app/controller/AuthController.php
index e79b9c9..5c094f9 100644
--- a/backend/app/controller/AuthController.php
+++ b/backend/app/controller/AuthController.php
@@ -64,6 +64,7 @@ class AuthController extends BaseController
$authCode = $request->post('auth_code');
$appId = $request->post('app_id');
$tenantKey = $request->post('tenant_key');
+ $companyParam = $request->post('company'); // 新增:company 参数
if (!$authCode) {
return $this->error('缺少授权码');
@@ -71,9 +72,38 @@ class AuthController extends BaseController
try {
$feishuService = new FeishuService();
+ $companyId = null;
+ $feishuApp = null;
- // 方式1: 根据 app_id 查找飞书应用配置(推荐)
- if ($appId) {
+ // 方式1: 根据 company 参数查找公司配置(新增,用于多公司支持)
+ if ($companyParam) {
+ error_log("🔍 使用 company 参数查找配置: {$companyParam}");
+
+ // company 参数是 company_id
+ $company = \app\model\Company::find($companyParam);
+ if (!$company) {
+ return $this->error('公司不存在');
+ }
+
+ $companyId = $company->id;
+ $tenantKey = $company->tenant_key; // 从公司表获取 tenant_key
+
+ // 获取该公司的默认飞书应用
+ $feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId)
+ ->where('system_id', 1)
+ ->where('status', 1)
+ ->first();
+
+ if (!$feishuApp) {
+ return $this->error('该公司未配置飞书应用');
+ }
+
+ error_log("✅ 通过 company 找到配置: company_id={$companyId}, tenant_key={$tenantKey}, feishu_app_id={$feishuApp->id}, app_id={$feishuApp->app_id}");
+ $feishuService->setCompany($companyId);
+ $appId = $feishuApp->id;
+ }
+ // 方式2: 根据 app_id 查找飞书应用配置
+ elseif ($appId) {
error_log("🔍 使用 app_id 查找配置: {$appId}");
// 根据 app_id 查找飞书应用
@@ -90,7 +120,7 @@ class AuthController extends BaseController
$feishuService->setCompany($companyId);
}
- // 方式2: 根据 tenant_key 查找(兼容旧版)
+ // 方式3: 根据 tenant_key 查找(兼容旧版)
elseif ($tenantKey) {
error_log("🔍 使用 tenant_key 查找配置: {$tenantKey}");
@@ -117,7 +147,7 @@ class AuthController extends BaseController
// 设置应用ID
$appId = $feishuApp->id;
} else {
- return $this->error('缺少 app_id 或 tenant_key 参数');
+ return $this->error('缺少 company、app_id 或 tenant_key 参数');
}
// 获取用户信息
@@ -174,16 +204,42 @@ class AuthController extends BaseController
{
$tenantKey = $request->post('tenant_key');
$appId = $request->post('app_id');
+ $companyParam = $request->post('company'); // 新增:company 参数
- error_log("📱 qrcodeLogin - tenant_key: {$tenantKey}, app_id: {$appId}");
+ error_log("📱 qrcodeLogin - tenant_key: {$tenantKey}, app_id: {$appId}, company: {$companyParam}");
try {
$feishuService = new \app\service\FeishuService();
$companyId = null;
$feishuApp = null;
- // 方式1: 根据 app_id 查找飞书应用配置(推荐)
- if ($appId) {
+ // 方式1: 根据 company 参数查找公司配置(新增,用于多公司支持)
+ if ($companyParam) {
+ error_log("🔍 使用 company 参数查找配置: {$companyParam}");
+
+ // company 参数是 company_id
+ $company = \app\model\Company::find($companyParam);
+ if (!$company) {
+ return $this->error('公司不存在');
+ }
+
+ $companyId = $company->id;
+ $tenantKey = $company->tenant_key; // 从公司表获取 tenant_key
+
+ // 获取该公司的默认飞书应用
+ $feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId)
+ ->where('system_id', 1)
+ ->where('status', 1)
+ ->first();
+
+ if (!$feishuApp) {
+ return $this->error('该公司未配置飞书应用');
+ }
+
+ error_log("✅ 通过 company 找到配置: company_id={$companyId}, tenant_key={$tenantKey}, feishu_app_id={$feishuApp->id}, app_id={$feishuApp->app_id}");
+ }
+ // 方式2: 根据 app_id 查找飞书应用配置(推荐)
+ elseif ($appId) {
$feishuApp = \app\model\CompanyFeishuApp::where('app_id', $appId)
->where('status', 1)
->first();
@@ -195,7 +251,7 @@ class AuthController extends BaseController
$companyId = $feishuApp->company_id;
error_log("✅ 通过 app_id 找到配置: company_id={$companyId}");
}
- // 方式2: 根据 tenant_key 查找(兼容旧版)
+ // 方式3: 根据 tenant_key 查找(兼容旧版)
elseif ($tenantKey) {
$company = $feishuService->getCompanyByTenantKey($tenantKey);
if (!$company) {
@@ -216,7 +272,7 @@ class AuthController extends BaseController
error_log("✅ 通过 tenant_key 找到配置: company_id={$companyId}");
} else {
- return $this->error('缺少 app_id 或 tenant_key 参数');
+ return $this->error('缺少 company、app_id 或 tenant_key 参数');
}
$feishuService->setCompany($companyId);
diff --git a/backend/app/service/FeishuService.php b/backend/app/service/FeishuService.php
index 20cbd11..16c30ef 100644
--- a/backend/app/service/FeishuService.php
+++ b/backend/app/service/FeishuService.php
@@ -134,9 +134,18 @@ class FeishuService
$unionId = $feishuData['union_id'] ?? null;
$feishuUserId = $feishuData['user_id'] ?? null;
$openId = $feishuData['open_id'] ?? null;
+ $tenantKey = $feishuData['tenant_key'] ?? null; // 从飞书用户信息获取 tenant_key
$name = $feishuData['name'] ?? '未知';
$avatar = $feishuData['avatar_url'] ?? null;
- $email = $feishuData['email'] ?? null;
+
+ // 邮箱:优先使用 enterprise_email,否则使用 email
+ $email = $feishuData['enterprise_email'] ?? $feishuData['email'] ?? null;
+ // 手机号:飞书返回的是 mobile 字段
+ $phone = $feishuData['mobile'] ?? null;
+ // 去掉手机号中的 +86 前缀
+ if ($phone && str_starts_with($phone, '+86')) {
+ $phone = substr($phone, 3);
+ }
if (!$unionId) {
throw new Exception('缺少 union_id');
@@ -160,20 +169,25 @@ class FeishuService
if (!$user) {
error_log("✨ 创建新用户: company={$companyId}, union_id={$unionId}, name={$name}");
- // 初始密码使用 sn(工号),如果没有 sn 则使用 union_id 的前6位
- $initialPassword = $feishuData['employee_no'] ?? substr($unionId, 0, 6);
+ // 从 user_sn 表中随机获取一个未分配的 sn
+ $userSn = $this->allocateUserSn();
$user = User::create([
'company_id' => $companyId,
+ 'sn' => $userSn,
'union_id' => $unionId,
'name' => $name,
'email' => $email,
+ 'phone' => $phone,
'avatar' => $avatar,
- 'password' => password_hash($initialPassword, PASSWORD_DEFAULT), // 初始密码加密
+ 'password' => password_hash($userSn, PASSWORD_DEFAULT), // 使用 sn 作为初始密码
'feishu_app_id' => $appId, // 记录登录来源应用
'status' => 1, // 新用户默认在职
]);
- error_log("🔑 初始密码设置为 sn: {$initialPassword}");
+ error_log("🔑 用户 sn: {$userSn}, 初始密码: {$userSn}");
+
+ // 更新 user_sn 表,标记 sn 已分配给新用户
+ $this->markUserSnAllocated($userSn, $user->id);
} else {
error_log("🔄 更新用户: id={$user->id}, company={$companyId}, union_id={$unionId}, 当前状态={$user->status}");
@@ -188,16 +202,47 @@ class FeishuService
'name' => $name,
'avatar' => $avatar,
'email' => $email,
+ 'phone' => $phone,
'feishu_app_id' => $appId,
]);
}
// ⭐ 保存或更新用户飞书应用绑定关系
- $this->saveUserFeishuBinding($user->id, $companyId, $appId, $feishuUserId, $openId, $app->tenant_key);
+ $this->saveUserFeishuBinding($user->id, $companyId, $appId, $feishuUserId, $openId, $tenantKey);
return $user;
}
+ /**
+ * 从 user_sn 表中随机获取一个未分配的 sn
+ */
+ private function allocateUserSn(): string
+ {
+ // 随机获取一个未分配的 sn(user_id = 0)
+ $snRecord = \support\Db::table('user_sn')
+ ->where('user_id', 0)
+ ->inRandomOrder()
+ ->first();
+
+ if (!$snRecord) {
+ throw new Exception('没有可用的用户编号,请联系管理员');
+ }
+
+ return (string) $snRecord->user_sn;
+ }
+
+ /**
+ * 标记 user_sn 已分配给指定用户
+ */
+ private function markUserSnAllocated(string $userSn, int $userId): void
+ {
+ \support\Db::table('user_sn')
+ ->where('user_sn', $userSn)
+ ->update(['user_id' => $userId]);
+
+ error_log("✅ 用户编号 {$userSn} 已分配给用户 {$userId}");
+ }
+
/**
* 保存或更新用户飞书应用绑定关系
*/
diff --git a/backend/config/route.php b/backend/config/route.php
index c7153cd..ac648d2 100644
--- a/backend/config/route.php
+++ b/backend/config/route.php
@@ -14,12 +14,15 @@ Route::group('/api/v1', function () {
Route::get('/auth/qrcode-login/status', [app\controller\AuthController::class, 'qrcodeLoginStatus']);
// 测试接口
- Route::get('/test/notification', [app\controller\TestController::class, 'testNotification']);
+ Route::get('/test/notification', [app\controller\TestController::class, 'notification']);
})->middleware($apiMiddleware);
// 飞书登录回调(不在 /api/v1 下,供飞书重定向使用)
Route::get('/login/callback', [app\controller\AuthController::class, 'qrcodeLoginCallback'])->middleware($apiMiddleware);
+// 飞书卡片回调(不需要认证,飞书服务器直接调用)
+Route::post('/feishu/card/callback', [app\controller\FeishuCardController::class, 'callback']);
+
// 用户路由(需要认证)
Route::group('/api/v1/user', function () {
Route::get('/info', [app\controller\UserController::class, 'info']);
diff --git a/frontend/src/components/FeishuAutoLogin.tsx b/frontend/src/components/FeishuAutoLogin.tsx
index 56eb9f9..702c2e4 100644
--- a/frontend/src/components/FeishuAutoLogin.tsx
+++ b/frontend/src/components/FeishuAutoLogin.tsx
@@ -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,
}),
})
diff --git a/frontend/src/pages/login/QRCodeLogin.tsx b/frontend/src/pages/login/QRCodeLogin.tsx
index 436fd69..ec9a1c2 100644
--- a/frontend/src/pages/login/QRCodeLogin.tsx
+++ b/frontend/src/pages/login/QRCodeLogin.tsx
@@ -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()
diff --git a/frontend/src/pages/login/index.tsx b/frontend/src/pages/login/index.tsx
index 7d01f49..60d4cb8 100644
--- a/frontend/src/pages/login/index.tsx
+++ b/frontend/src/pages/login/index.tsx
@@ -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')
diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx
index fe88d9a..489450a 100644
--- a/frontend/src/router/index.tsx
+++ b/frontend/src/router/index.tsx
@@ -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: },
{ path: 'business-trip/apply/select-approver', element: },
{ path: 'business-trip/apply/select-cc', element: },
+ { path: 'business-trip/detail/:id', element: },
{ path: 'business-trip/:id', element: },
{ path: 'approval', element: },
{ path: 'approval/:id', element: },
],
},
+ // 详情页移到顶层,支持未登录访问(页面自己处理登录检查)
+ { path: '/approval/detail/:id', element: },
+ { path: '/business-trip/detail/:id', element: },
{ path: '/login', element: },
{ path: '/login-simple', element: },
{ path: '/debug-feishu', element: },
{ path: '/approval/quick-action', element: },
+ { path: '/approval/reject', element: },
{ path: '*', element: },
])
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index 31076e2..6353fb3 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -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,
}),
// 轮询二维码登录状态