/** * 认证服务核心实现 * * 这个文件包含了用户认证、会话管理、Token 刷新等核心功能的具体实现。 * * 主要功能: * - 基于 Cookie 的会话管理 * - OAuth2.0 Token 自动刷新 * - 用户登录状态检查 * - 会话创建和销毁 * - 用户信息保存到数据库 * * 技术栈: * - Remix Session Storage (Cookie-based) * - OAuth2.0 Token Management * - TypeScript 类型安全 * */ import { createCookieSessionStorage } from "@remix-run/node"; import { postgrestGet, postgrestPost, postgrestPut } from "../postgrest-client"; import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt"; import { OAUTH_CONFIG, API_BASE_URL } from "~/config/api-config"; import axios from 'axios'; /** * 用户角色类型定义 * * @typedef {string} UserRole * @property {'common'} common - 普通用户,有基本的系统访问权限 * @property {'developer'} developer - 开发者/管理员,有完整的系统管理权限 */ export type UserRole = string; /** * 用户信息接口,对应 sso_users 表结构 */ export interface UserInfo { sub: string; // IDaaS用户唯一标识 username?: string; // 显示用户名称/工号 nick_name?: string; // 用户真实姓名 nickname?: string; // OAuth返回的昵称字段 name?: string; // 用户姓名(通常映射到 nick_name) phone_number?: string; // 手机号 email?: string; // 邮箱地址 ou_id?: string; // 所属组织单位ID ou_name?: string; // 所属部门名称 status?: number; // 账户状态: 0=正常, 1=禁用 is_leader?: boolean; // 是否为部门负责人 area?: string; // 用户所属地区 id?: string | number; // 临时的用户id user_id?: string | number; user_role?: string } /** * sso_users 表记录接口 */ export interface SsoUser { id?: string; sub: string; username: string; nick_name: string; phone_number?: string; email?: string; ou_id: string; ou_name: string; status: number; is_leader: boolean; area?: string; created_at?: string; updated_at?: string; deleted_at?: string; } /** * 会话存储配置 * * 使用 Remix 的 Cookie Session Storage 来管理用户会话。 * Cookie 存储方式的优势: * - 服务器端控制,安全性高 * - 自动处理过期时间 * - 支持 HttpOnly,防止 XSS 攻击 * * 配置说明: * - name: Cookie 名称,用于标识会话 * - httpOnly: 防止客户端 JavaScript 访问,提高安全性 * - sameSite: CSRF 保护 * - secrets: 用于签名和加密 Cookie 内容 * - maxAge: 会话有效期,与 OAuth Token 过期时间保持一致 */ export const sessionStorage = createCookieSessionStorage({ cookie: { name: "__lgsession", // 登录会话 Cookie 名称 httpOnly: true, // 仅服务器端可访问,防止 XSS path: "/", // Cookie 作用域为整个应用 sameSite: "lax", // CSRF 保护,允许顶级导航 secrets: ["s3cr3t"], // TODO: 应该从环境变量读取 maxAge: 60 * 60 * 8, // 8小时,确保大于等于JWT token最大有效期(通常为6小时) secure: false, // 开发环境中禁用 HTTPS 要求 }, }); /** * 获取会话对象 * * 从请求中提取 Cookie 并获取对应的会话对象。 * 这是一个底层函数,通常不直接使用,而是通过 getUserSession 使用。 * * @param request - Remix Request 对象,包含 HTTP 请求信息 * @returns 会话对象,可以用来读取和设置会话数据 */ export async function getSession(request: Request) { const cookie = request.headers.get("Cookie"); return sessionStorage.getSession(cookie); } /** * 获取用户登录状态(带自动Token刷新) * * 这是认证系统的核心函数,负责: * 1. 检查用户是否已登录 * 2. 验证 OAuth Token 是否有效 * 3. 自动刷新即将过期的 Token * 4. 返回用户信息和认证状态 * * Token 刷新机制: * - 检查 access_token 是否即将过期(距离过期时间 < 5 分钟) * - 如果即将过期,使用 refresh_token 自动获取新的 access_token * - 如果刷新失败,则认为用户未登录 * * @param request - Remix Request 对象 * @returns 包含以下字段的对象: * - isAuthenticated: 是否已认证 * - userRole: 用户角色 ('common' | 'developer') * - accessToken: OAuth 访问令牌 * - refreshToken: OAuth 刷新令牌 * - userInfo: 用户详细信息 * - isTokenExpired: Token 是否已过期 * - refreshedSession: 如果刷新了 Token,返回更新后的会话对象 */ /** * 生成前端JWT * @param userInfo 用户信息 * @param expiresIn OAuth token过期时间(秒) * @returns JWT字符串 */ // async function generateFrontendJWT(userInfo: UserInfo, savedUserData: SsoUser, userRole: UserRole, expiresIn: number): Promise { // const jwtUserInfo: UserInfoForJWT = { // sub: userInfo.sub, // user_id: savedUserData.id!, // username: savedUserData.username, // nick_name: savedUserData.nick_name, // email: savedUserData.email, // phone_number: savedUserData.phone_number, // ou_id: savedUserData.ou_id, // ou_name: savedUserData.ou_name, // is_leader: savedUserData.is_leader, // user_role: userRole // }; // return JWTUtils.generateJWT(jwtUserInfo, expiresIn); // } /** * 创建包含JWT的用户信息对象 * @param userInfo OAuth用户信息 * @param savedUserData 数据库中保存的用户数据 * @param userRole 用户角色 * @param frontendJWT 前端JWT * @returns 完整的用户信息对象 */ // function createUserInfoWithJWT(userInfo: UserInfo, savedUserData: SsoUser, userRole: UserRole, frontendJWT: string) { // return { // // 保持与callback.tsx中enhancedUserInfo相同的数据结构 // sub: userInfo.sub, // username: savedUserData.username, // nick_name: savedUserData.nick_name, // phone_number: savedUserData.phone_number, // email: savedUserData.email, // ou_id: savedUserData.ou_id, // ou_name: savedUserData.ou_name, // status: savedUserData.status, // is_leader: savedUserData.is_leader, // // 增强字段,与OAuth登录保持一致 // user_id: savedUserData.id, // user_role: userRole, // frontend_jwt: frontendJWT // }; // } export async function getUserSession(request: Request) { const session = await getSession(request); const isAuthenticated = session.get("isAuthenticated") === true; const userRole = session.get("userRole") as UserRole; const accessToken = session.get("accessToken"); const refreshToken = session.get("refreshToken"); const tokenIssuedAt = session.get("tokenIssuedAt"); let tokenExpiresIn = session.get("tokenExpiresIn"); const userInfo = session.get("userInfo"); const frontendJWT = session.get("frontendJWT"); // 🔑 检查是否是公共路径(不需要认证的路径) const url = new URL(request.url); const pathname = url.pathname; const publicPaths = ['/login', '/favicon.ico', '/callback']; const isPublicPath = publicPaths.some(path => pathname.startsWith(path)); let refreshedSession = null; // let shouldRegenerateJWT = false; // 🔑 新的统一过期检查逻辑 // 不区分 admin 和 OAuth 用户,所有用户都使用同样的过期检查 // 如果没有设置 tokenExpiresIn,给一个默认值(24小时) if (!tokenExpiresIn) { tokenExpiresIn = 86400; // 24小时(与后端登录接口返回的一致) session.set("tokenExpiresIn", tokenExpiresIn); refreshedSession = session; } // 🔑 简化:不再自动重新生成 JWT // JWT 即将过期时,直接让用户重新登录 // 🔑 关键:验证 session 完整性 // 如果 isAuthenticated 为 true,但缺少关键数据(userInfo),则认为 session 无效 let finalIsAuthenticated = isAuthenticated; if (finalIsAuthenticated) { // 检查是否有用户信息 if (!userInfo) { console.warn("⚠️ [getUserSession] Session 不完整:缺少 userInfo"); finalIsAuthenticated = false; } // 检查是否有前端 JWT if (!frontendJWT) { console.warn("⚠️ [getUserSession] Session 不完整:缺少 frontendJWT"); finalIsAuthenticated = false; } // 🔑 统一的 token 过期检查(所有用户类型) // 使用签发时间 + 有效期 - 5分钟缓冲来判断是否需要重新登录 if (tokenIssuedAt && tokenExpiresIn) { const now = Date.now(); // 当前时间(毫秒) const expiresAt = tokenIssuedAt + (tokenExpiresIn * 1000); // 过期时间(毫秒) const bufferTime = 5 * 60 * 1000; // 5分钟缓冲时间(毫秒) // 如果当前时间 >= 过期时间 - 5分钟,则认为 token 即将过期或已过期 if (now >= expiresAt - bufferTime) { const remainingSeconds = Math.floor((expiresAt - now) / 1000); console.error(`❌ [getUserSession] Token 即将过期或已过期 (剩余: ${remainingSeconds} 秒)`); finalIsAuthenticated = false; } } else if (!tokenIssuedAt) { // 如果没有签发时间,认为 session 无效(可能是旧版本数据) console.warn("⚠️ [getUserSession] Session 缺少 tokenIssuedAt,认为已过期"); finalIsAuthenticated = false; } } // 🚨 统一的认证检查和重定向逻辑 if (!finalIsAuthenticated) { // 如果是公共路径,不重定向,直接返回未认证状态 if (isPublicPath) { // console.log("ℹ️ [getUserSession] 公共路径,允许未认证访问:", pathname); return { isAuthenticated: false, userRole, accessToken, refreshToken, userInfo, refreshedSession, frontendJWT }; } // 非公共路径且未认证,重定向到登录页 if (isAuthenticated) { // Session 存在但已失效(token 过期或数据不完整) console.error("❌ [getUserSession] Session 已失效,清除 session 并重定向到登录页"); const { redirect } = await import("@remix-run/node"); const destroyedSession = await sessionStorage.destroySession(session); // 重定向到登录页,添加 expired=true 参数 throw redirect("/login?expired=true", { headers: { "Set-Cookie": destroyedSession } }); } else { // Session 不存在(首次访问或已登出) console.warn("⚠️ [getUserSession] 未登录,重定向到登录页"); const { redirect } = await import("@remix-run/node"); // 保存当前路径,登录后可以跳转回来 const redirectTo = pathname !== '/login' ? pathname : '/'; throw redirect(`/login?redirect=${encodeURIComponent(redirectTo)}`); } } return { isAuthenticated: finalIsAuthenticated, userRole, accessToken, refreshToken, userInfo, refreshedSession, // 如果刷新了token,返回更新后的session frontendJWT // 返回前端JWT }; } /** * 创建用户登录会话(完整版) * * 在用户成功登录后调用此函数来创建完整的会话并设置 Cookie。 * 这个函数支持完整的OAuth2.0登录流程,包括token管理和用户信息存储。 * * 处理流程: * 1. 创建新的会话对象 * 2. 设置认证状态、用户角色、token信息 * 3. 保存用户信息和前端JWT * 4. 生成签名的 Cookie * 5. 返回重定向响应并设置 Cookie * * @param params - 会话创建参数 * @returns HTTP 302 重定向响应,包含设置 Cookie 的头部 */ export async function createUserSession(params: { isAuthenticated: boolean; userRole: string; redirectTo: string; accessToken?: string; refreshToken?: string; tokenExpiresIn?: number; tokenIssuedAt?: number; // 🔑 新增:支持传递签发时间 userInfo?: UserInfo; frontendJWT?: string; }) { const session = await sessionStorage.getSession(); // 基础认证信息 session.set("isAuthenticated", params.isAuthenticated); session.set("userRole", params.userRole); // OAuth token信息 if (params.accessToken) { session.set("accessToken", params.accessToken); } if (params.refreshToken) { session.set("refreshToken", params.refreshToken); } // 🔑 Token 时间信息(与 accessToken 独立,支持管理员登录) // 只要有 tokenExpiresIn 或 tokenIssuedAt,就设置这些字段 if (params.tokenExpiresIn) { session.set("tokenExpiresIn", params.tokenExpiresIn); } if (params.tokenIssuedAt !== undefined) { // 优先使用传递的 tokenIssuedAt session.set("tokenIssuedAt", params.tokenIssuedAt); } else if (params.accessToken || params.frontendJWT) { // 如果没有传递 tokenIssuedAt,但有 token(accessToken 或 frontendJWT),则使用当前时间 session.set("tokenIssuedAt", Date.now()); } // 用户信息和JWT if (params.userInfo) { session.set("userInfo", params.userInfo); } if (params.frontendJWT) { session.set("frontendJWT", params.frontendJWT); } // 🔑 根据 tokenExpiresIn 动态设置 Cookie 的 maxAge // 如果有 tokenExpiresIn,使用它作为 Cookie 有效期;否则使用默认值(8小时) const cookieMaxAge = params.tokenExpiresIn || (60 * 60 * 8); // 默认8小时 // console.log("🍪 [createUserSession] Cookie maxAge:", cookieMaxAge, "秒 (", (cookieMaxAge / 3600).toFixed(2), "小时)"); const cookie = await sessionStorage.commitSession(session, { maxAge: cookieMaxAge // 🔑 动态设置 Cookie 有效期 }); return new Response(null, { status: 302, // HTTP 重定向状态码 headers: { Location: params.redirectTo, // 重定向目标 URL "Set-Cookie": cookie, // 设置会话 Cookie }, }); } /** * 创建用户登录会话(简化版) * * 在用户成功登录后调用此函数来创建会话并设置 Cookie。 * 这个函数通常在以下场景中使用: * - 临时管理员登录 * - 测试用户登录 * - 其他简单认证方式成功后 * * 处理流程: * 1. 创建新的会话对象 * 2. 设置认证状态和用户角色 * 3. 生成签名的 Cookie * 4. 返回重定向响应并设置 Cookie * * @param isAuthenticated - 是否已认证,通常为 true * @param userRole - 用户角色,决定用户的权限级别 * @param redirectTo - 登录成功后重定向的 URL,默认为首页 * @returns HTTP 302 重定向响应,包含设置 Cookie 的头部 */ export async function createSimpleUserSession(isAuthenticated: boolean, userRole: UserRole, redirectTo: string) { const session = await sessionStorage.getSession(); session.set("isAuthenticated", isAuthenticated); session.set("userRole", userRole); const cookie = await sessionStorage.commitSession(session); console.log("创建简化会话 - 设置Cookie:", !!cookie); console.log("创建简化会话 - 用户角色:", userRole); console.log("创建简化会话 - 重定向到:", redirectTo); return new Response(null, { status: 302, // HTTP 重定向状态码 headers: { Location: redirectTo, // 重定向目标 URL "Set-Cookie": cookie, // 设置会话 Cookie }, }); } /** * 销毁会话(用户登出) * * 当用户主动登出或会话失效时调用此函数。 * * 处理流程: * 1. 获取当前用户的会话和访问令牌 * 2. 调用 IDaaS 单点登出接口 * 3. 销毁本地会话数据(清除所有存储的信息) * 4. 清除客户端的会话 Cookie * 5. 重定向到登录页面 * * 注意事项: * - 这个函数会同时处理本地会话和 IDaaS 的单点登出 * - 即使 IDaaS 登出失败,也会清除本地会话 * - 销毁会话后,用户需要重新登录才能访问受保护的页面 * * @param request - Remix Request 对象,用于获取当前会话 * @returns HTTP 302 重定向响应,清除 Cookie 并跳转到登录页 */ export async function logout(request: Request) { const session = await getSession(request); // 获取访问令牌和应用ID,用于调用IDaaS单点登出 const accessToken = session.get("accessToken"); const appId = OAUTH_CONFIG.appId || 'idaasoauth2'; console.log("🚪 [Logout] 开始登出流程..."); console.log("🔑 [Logout] accessToken 存在:", !!accessToken); console.log("📱 [Logout] appId:", appId); // 如果存在访问令牌,调用IDaaS单点登出(仅 OAuth 登录用户) if (accessToken && appId) { console.log("🌐 [Logout] OAuth 用户,准备调用 IDaaS 单点登出..."); try { await callIDaaSLogout(accessToken, appId); console.log("✅ [Logout] IDaaS单点登出成功"); } catch (error) { console.error("❌ [Logout] IDaaS单点登出失败:"); console.error(" 错误详情:", error); if (error instanceof Error) { console.error(" 错误消息:", error.message); console.error(" 错误堆栈:", error.stack); } // 即使IDaaS登出失败,也继续清除本地会话 } } else { console.log("ℹ️ [Logout] 管理员登录用户,无需调用 IDaaS 登出"); } return new Response(null, { status: 302, // HTTP 重定向状态码 headers: { Location: "/login", // 重定向到登录页面 "Set-Cookie": await sessionStorage.destroySession(session), // 清除会话 Cookie }, }); } /** * 调用IDaaS单点登出接口 * * @param accessToken - 用户的访问令牌 * @param appId - 应用ID * @returns Promise */ async function callIDaaSLogout(accessToken: string, appId: string): Promise { const serverUrl = OAUTH_CONFIG.serverUrl || 'http://10.79.112.85'; const redirectUri = OAUTH_CONFIG.redirectUri || 'http://10.79.97.17/'; const logoutUrl = `${serverUrl}/public/sp/slo/${appId}`; console.log("📡 [callIDaaSLogout] 准备发送登出请求:"); console.log(" 登出URL:", logoutUrl); console.log(" 重定向URL:", redirectUri); console.log(" accessToken:", accessToken ? `${accessToken.substring(0, 20)}...` : 'null'); const formData = new URLSearchParams(); formData.append('access_token', accessToken); formData.append('redirect_url', encodeURIComponent(redirectUri)); try { const response = await axios.post(logoutUrl, formData.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); console.log("✅ [callIDaaSLogout] IDaaS单点登出请求成功"); console.log(" 响应状态:", response.status); console.log(" 响应数据:", response.data); } catch (error) { if (axios.isAxiosError(error)) { console.error("❌ [callIDaaSLogout] 调用IDaaS登出接口失败:"); console.error(" HTTP状态:", error.response?.status); console.error(" 状态文本:", error.response?.statusText); console.error(" 响应数据:", error.response?.data); console.error(" 请求配置:", error.config?.url, error.config?.method); throw new Error(`IDaaS登出失败: ${error.response?.status} ${error.response?.statusText}`); } console.error("❌ [callIDaaSLogout] 调用IDaaS登出接口失败(非HTTP错误):", error); throw error; } } /** * 保存用户信息到数据库 * * 此函数实现以下逻辑: * 1. 内部生成临时 JWT(user_id 为 'login',仅用于数据库操作) * 2. 根据 userInfo.sub 查询 sso_users 表中是否已存在该用户 * 3. 如果存在,则更新用户信息(如果用户已有 area 值则不更新) * 4. 如果不存在,则插入新的用户记录 * 5. 返回保存的用户数据和临时 JWT * * @param userInfo - 从 IDaaS 获取的用户信息 * @param userRole - 用户角色 * @param tokenExpiresIn - Token过期时间(秒) * @param area - 用户所属地区,根据端口号确定 * @returns Promise<{success: boolean, data?: SsoUser, tempToken?: string, error?: string}> */ export async function saveUserInfo( userInfo: UserInfo, userRole: UserRole, tokenExpiresIn: number, area?: string ): Promise<{success: boolean, data?: SsoUser, tempToken?: string, error?: string}> { try { console.log("开始保存用户信息", userInfo); // 验证必要字段 if (!userInfo.sub) { return { success: false, error: "用户唯一标识 sub 不能为空" }; } // 🔒 安全:在服务端生成临时 JWT,user_id 使用占位符 'login' // 这样客户端无法看到真实的 user_id const tempUserInfo: UserInfoForJWT = { sub: userInfo.sub, user_id: 'login', // 使用占位符,避免在客户端暴露真实ID username: 'login', nick_name: userInfo.nick_name || userInfo.nickname || userInfo.name || "未知用户", email: userInfo.email, phone_number: userInfo.phone_number, ou_id: userInfo.ou_id || "default", ou_name: userInfo.ou_name || "未知部门", is_leader: userInfo.is_leader || false, user_role: userRole }; const tempToken = JWTUtils.generateJWT(tempUserInfo, tokenExpiresIn); // 1. 根据 sub 查询是否已存在该用户 const existingUserResult = await postgrestGet("sso_users", { filter: { "sub": `eq.${userInfo.sub}`, "deleted_at": "is.null" // 只查询未删除的记录 }, token: tempToken }); if (existingUserResult.error) { console.error("查询用户失败:", existingUserResult.error); return { success: false, error: `查询用户失败: ${existingUserResult.error}` }; } const existingUsers = existingUserResult.data || []; const existingUser = existingUsers.length > 0 ? existingUsers[0] : null; // 准备要保存的用户数据 // 注意:OAuth返回的字段是nickname,而不是nick_name const userData: Partial = { sub: userInfo.sub, username: userInfo.username || userInfo.name || userInfo.sub, nick_name: userInfo.nick_name || userInfo.nickname || userInfo.name || "未知用户", phone_number: userInfo.phone_number || undefined, email: userInfo.email || undefined, ou_id: userInfo.ou_id || "default", ou_name: userInfo.ou_name || "未知部门", status: userInfo.status !== undefined ? userInfo.status : 0, is_leader: userInfo.is_leader || false, }; if (existingUser) { // 2. 用户已存在,执行更新操作 console.log("用户已存在,执行更新操作", existingUser.id); // 只有在现有用户没有 area 或 area 为空时,才更新 area if (area && !existingUser.area) { userData.area = area; console.log("用户原本无地区信息,更新地区为:", area); } else if (existingUser.area) { console.log("用户已有地区信息:", existingUser.area, "不更新"); } const updateResult = await postgrestPut>( "sso_users", userData, { id: existingUser.id! }, tempToken ); if (updateResult.error) { console.error("更新用户失败:", updateResult.error); return { success: false, error: `更新用户失败: ${updateResult.error}` }; } console.log("用户信息更新成功"); return { success: true, data: Array.isArray(updateResult.data) ? updateResult.data[0] : updateResult.data as unknown as SsoUser, tempToken // 返回临时 JWT }; } else { // 3. 用户不存在,执行插入操作,设置地区信息 console.log("用户不存在,执行插入操作"); // 新用户直接设置 area if (area) { userData.area = area; console.log("新用户,设置地区为:", area); } const insertResult = await postgrestPost("sso_users", userData as SsoUser, tempToken); if (insertResult.error) { console.error("插入用户失败:", insertResult.error); return { success: false, error: `插入用户失败: ${insertResult.error}` }; } console.log("用户信息插入成功"); // 4. 给这个用户默认添加一个角色,角色为common const userData_with_id = Array.isArray(insertResult.data) ? insertResult.data[0] : insertResult.data as unknown as SsoUser; if (userData_with_id?.id) { await addDefaultRole(userData_with_id.id, 2, tempToken); } return { success: true, data: userData_with_id, tempToken // 返回临时 JWT }; } } catch (error) { console.error("保存用户信息时发生错误:", error); return { success: false, error: `保存用户信息失败: ${error instanceof Error ? error.message : String(error)}` }; } } /** * 为用户添加默认角色 * * @param userId - 用户ID * @param roleId - 角色ID,默认为2(common角色) * @param token - JWT令牌,用于调用postgrest服务 * @returns 添加结果 */ export async function addDefaultRole(userId: string, roleId: number = 2, token?: string) { try { console.log(`为用户 ${userId} 添加默认角色 ${roleId}`); // 检查用户是否已经有此角色 const existingRoleResult = await postgrestGet>("user_role", { filter: { user_id: `eq.${userId}`, role_id: `eq.${roleId}` }, token }); if (existingRoleResult.error) { console.error("查询用户角色失败:", existingRoleResult.error); return { success: false, error: `查询用户角色失败: ${existingRoleResult.error}` }; } const existingRoles = existingRoleResult.data || []; if (existingRoles.length > 0) { console.log("用户已经拥有此角色,跳过添加"); return { success: true, data: existingRoles[0] }; } // 添加角色 const addRoleResult = await postgrestPost, {user_id: string, role_id: number}>("user_role", { user_id: userId, role_id: roleId }, token); if (addRoleResult.error) { console.error("添加用户角色失败:", addRoleResult.error); return { success: false, error: `添加用户角色失败: ${addRoleResult.error}` }; } console.log("用户角色添加成功"); return { success: true, data: Array.isArray(addRoleResult.data) ? addRoleResult.data[0] : addRoleResult.data }; } catch (error) { console.error("添加用户角色时发生错误:", error); return { success: false, error: `添加用户角色失败: ${error instanceof Error ? error.message : String(error)}` }; } } /** * 通过用户sub获取用户信息 * * @param sub - 用户的唯一标识 * @returns 用户信息 */ export async function getUserBySub(sub: string) { try { // console.log(`查询用户: ${sub}`); const userResult = await postgrestGet("sso_users", { filter: { sub: `eq.${sub}` } }); if (userResult.error) { console.error("查询用户失败:", userResult.error); return { success: false, error: `查询用户失败: ${userResult.error}` }; } const users = userResult.data || []; const user = users.length > 0 ? users[0] : null; if (!user) { return { success: false, error: "用户不存在" }; } return { success: true, data: user }; } catch (error) { console.error("查询用户时发生错误:", error); return { success: false, error: `查询用户失败: ${error instanceof Error ? error.message : String(error)}` }; } }