From d8f3d98c70bb2654018107640cc15837d1c74a84 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Sun, 20 Jul 2025 21:49:40 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=88=97=E8=A1=A8documen?= =?UTF-8?q?ts=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7id=E7=9A=84=E9=99=90?= =?UTF-8?q?=E5=88=B6=EF=BC=8C=E6=B7=BB=E5=8A=A0=E9=80=9A=E8=BF=87=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E8=AE=A4=E8=AF=81=E4=B9=8B=E5=90=8E=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E4=B8=AD=E7=94=A8=E6=88=B7=E6=95=B0=E6=8D=AE=E7=9A=84?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=92=8C=E8=A7=92=E8=89=B2=E7=9A=84=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=EF=BC=8C=E6=B7=BB=E5=8A=A0Sidebar=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E9=80=9A=E8=BF=87=E6=95=B0=E6=8D=AE=E5=BA=93=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/auth/user-routes.ts | 613 ++++++++++++++++++ app/api/files/documents.ts | 10 +- app/api/login/auth.server.ts | 282 ++++++++ .../cross-checking/ReviewPointsList.tsx | 33 +- app/components/layout/Sidebar.tsx | 225 ++----- app/routes/callback.tsx | 16 +- app/routes/documents._index.tsx | 10 + app/routes/login.tsx | 36 +- sql/role_route.sql | 61 ++ sql/role_route_insert.sql | 63 ++ sql/roles.sql | 65 ++ sql/sso_users.sql | 84 +++ sql/sys_routes.sql | 59 ++ sql/sys_routes_insert.sql | 93 +++ sql/test_user_setup.sql | 46 ++ sql/update_get_documents_with_filters.sql | 86 +++ sql/user_role.sql | 47 ++ 17 files changed, 1630 insertions(+), 199 deletions(-) create mode 100644 app/api/auth/user-routes.ts create mode 100644 sql/role_route.sql create mode 100644 sql/role_route_insert.sql create mode 100644 sql/roles.sql create mode 100644 sql/sso_users.sql create mode 100644 sql/sys_routes.sql create mode 100644 sql/sys_routes_insert.sql create mode 100644 sql/test_user_setup.sql create mode 100644 sql/update_get_documents_with_filters.sql create mode 100644 sql/user_role.sql diff --git a/app/api/auth/user-routes.ts b/app/api/auth/user-routes.ts new file mode 100644 index 0000000..5d0324e --- /dev/null +++ b/app/api/auth/user-routes.ts @@ -0,0 +1,613 @@ +import { toastService } from '~/components/ui'; +import { postgrestGet } from '../postgrest-client'; + +// 路由数据接口 +export interface RouteInfo { + id: number; + path: string; + name: string; + meta: { + title: string; + icon: string; + order: number; + requiredRole?: string; + }; + parent_id: number; + is_menu: number; +} + +// 用户路由权限接口 +export interface UserRoutePermission { + route_id: number; + role_id: number; + permission: string; + route: RouteInfo; +} + +// MenuItem结构接口 +export interface MenuItem { + id: string; + title: string; + path: string; + icon: string; + order: number; + hideBreadcrumb?: boolean; + requiredRole?: string; + children?: MenuItem[]; +} + +// 静态菜单数据作为后备 (保留用于开发和紧急情况,当前不使用) +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const FALLBACK_MENU_DATA: Record = { + 'admin': [ + { + id: 'home', + title: '系统概览', + path: '/home', + icon: 'ri-home-line', + order: 1 + }, + { + id: 'chat-with-llm', + title: 'AI对话', + path: '/chat-with-llm', + icon: 'ri-chat-smile-2-line', + order: 2 + }, + { + id: 'file-management', + title: '文件管理', + path: '/files', + icon: 'ri-folder-line', + order: 3, + children: [ + { + id: 'file-upload', + title: '文件上传', + path: '/files/upload', + icon: 'ri-upload-cloud-line', + order: 1 + }, + { + id: 'documents', + title: '文档列表', + path: '/documents', + icon: 'ri-file-list-3-line', + order: 2 + } + ] + }, + { + id: 'rule-management', + title: '评查规则库', + path: '/rules', + icon: 'ri-book-3-line', + order: 4, + children: [ + { + id: 'rule-groups', + title: '评查点分组', + path: '/rule-groups', + icon: 'ri-folder-open-line', + order: 1 + }, + { + id: 'rules-list', + title: '评查点列表', + path: '/rules', + icon: 'ri-list-check-3', + order: 2 + }, + { + id: 'rules-file', + title: '评查文件列表', + path: '/rules-files', + icon: 'ri-list-check-2', + order: 3 + } + ] + }, + { + id: 'contract-template', + title: '合同模板', + path: '/contract-template', + icon: 'ri-file-search-line', + order: 5, + children: [ + { + id: 'contract-search-ai', + title: '智能搜索', + path: '/contract-template/search', + icon: 'ri-search-line', + order: 1 + }, + { + id: 'contract-list', + title: '合同列表', + path: '/contract-template/list', + icon: 'ri-folder-line', + order: 2 + } + ] + }, + { + id: 'system-settings', + title: '系统设置', + path: '/settings', + icon: 'ri-settings-4-line', + order: 6, + requiredRole: 'developer', + children: [ + { + id: 'config-lists', + title: '配置列表', + path: '/config-lists', + icon: 'ri-list-check-3', + order: 1, + requiredRole: 'developer' + }, + { + id: 'document-types', + title: '文档类型', + path: '/document-types', + icon: 'ri-file-list-line', + order: 2, + requiredRole: 'developer' + }, + { + id: 'prompt-management', + title: '提示词管理', + path: '/prompts', + icon: 'ri-chat-1-line', + order: 3, + requiredRole: 'developer' + } + ] + }, + { + id: 'cross-checking', + title: '交叉评查', + path: '/cross-checking', + icon: 'ri-color-filter-line', + order: 7 + } + ], + 'common': [ + { + id: 'home', + title: '系统概览', + path: '/home', + icon: 'ri-home-line', + order: 1 + }, + { + id: 'file-management', + title: '文件管理', + path: '/files', + icon: 'ri-folder-line', + order: 3, + children: [ + { + id: 'file-upload', + title: '文件上传', + path: '/files/upload', + icon: 'ri-upload-cloud-line', + order: 1 + }, + { + id: 'documents', + title: '文档列表', + path: '/documents', + icon: 'ri-file-list-3-line', + order: 2 + } + ] + }, + { + id: 'rule-management', + title: '评查规则库', + path: '/rules', + icon: 'ri-book-3-line', + order: 4, + children: [ + { + id: 'rule-groups', + title: '评查点分组', + path: '/rule-groups', + icon: 'ri-folder-open-line', + order: 1 + }, + { + id: 'rules-list', + title: '评查点列表', + path: '/rules', + icon: 'ri-list-check-3', + order: 2 + }, + { + id: 'rules-file', + title: '评查文件列表', + path: '/rules-files', + icon: 'ri-list-check-2', + order: 3 + } + ] + }, + { + id: 'contract-template', + title: '合同模板', + path: '/contract-template', + icon: 'ri-file-search-line', + order: 5, + children: [ + { + id: 'contract-search-ai', + title: '智能搜索', + path: '/contract-template/search', + icon: 'ri-search-line', + order: 1 + }, + { + id: 'contract-list', + title: '合同列表', + path: '/contract-template/list', + icon: 'ri-folder-line', + order: 2 + } + ] + }, + { + id: 'cross-checking', + title: '交叉评查', + path: '/cross-checking', + icon: 'ri-color-filter-line', + order: 7 + } + ], + 'deptLeader': [ + { + id: 'home', + title: '系统概览', + path: '/home', + icon: 'ri-home-line', + order: 1 + }, + { + id: 'chat-with-llm', + title: 'AI对话', + path: '/chat-with-llm', + icon: 'ri-chat-smile-2-line', + order: 2 + }, + { + id: 'file-management', + title: '文件管理', + path: '/files', + icon: 'ri-folder-line', + order: 3, + children: [ + { + id: 'file-upload', + title: '文件上传', + path: '/files/upload', + icon: 'ri-upload-cloud-line', + order: 1 + }, + { + id: 'documents', + title: '文档列表', + path: '/documents', + icon: 'ri-file-list-3-line', + order: 2 + } + ] + }, + { + id: 'rule-management', + title: '评查规则库', + path: '/rules', + icon: 'ri-book-3-line', + order: 4, + children: [ + { + id: 'rule-groups', + title: '评查点分组', + path: '/rule-groups', + icon: 'ri-folder-open-line', + order: 1 + }, + { + id: 'rules-list', + title: '评查点列表', + path: '/rules', + icon: 'ri-list-check-3', + order: 2 + }, + { + id: 'rules-file', + title: '评查文件列表', + path: '/rules-files', + icon: 'ri-list-check-2', + order: 3 + } + ] + }, + { + id: 'contract-template', + title: '合同模板', + path: '/contract-template', + icon: 'ri-file-search-line', + order: 5, + children: [ + { + id: 'contract-search-ai', + title: '智能搜索', + path: '/contract-template/search', + icon: 'ri-search-line', + order: 1 + }, + { + id: 'contract-list', + title: '合同列表', + path: '/contract-template/list', + icon: 'ri-folder-line', + order: 2 + } + ] + }, + { + id: 'cross-checking', + title: '交叉评查', + path: '/cross-checking', + icon: 'ri-color-filter-line', + order: 7 + } + ], + 'groupLeader': [ + { + id: 'home', + title: '系统概览', + path: '/home', + icon: 'ri-home-line', + order: 1 + }, + { + id: 'file-management', + title: '文件管理', + path: '/files', + icon: 'ri-folder-line', + order: 3, + children: [ + { + id: 'file-upload', + title: '文件上传', + path: '/files/upload', + icon: 'ri-upload-cloud-line', + order: 1 + }, + { + id: 'documents', + title: '文档列表', + path: '/documents', + icon: 'ri-file-list-3-line', + order: 2 + } + ] + }, + { + id: 'rule-management', + title: '评查规则库', + path: '/rules', + icon: 'ri-book-3-line', + order: 4, + children: [ + { + id: 'rule-groups', + title: '评查点分组', + path: '/rule-groups', + icon: 'ri-folder-open-line', + order: 1 + }, + { + id: 'rules-list', + title: '评查点列表', + path: '/rules', + icon: 'ri-list-check-3', + order: 2 + }, + { + id: 'rules-file', + title: '评查文件列表', + path: '/rules-files', + icon: 'ri-list-check-2', + order: 3 + } + ] + }, + { + id: 'contract-template', + title: '合同模板', + path: '/contract-template', + icon: 'ri-file-search-line', + order: 5, + children: [ + { + id: 'contract-search-ai', + title: '智能搜索', + path: '/contract-template/search', + icon: 'ri-search-line', + order: 1 + }, + { + id: 'contract-list', + title: '合同列表', + path: '/contract-template/list', + icon: 'ri-folder-line', + order: 2 + } + ] + }, + { + id: 'cross-checking', + title: '交叉评查', + path: '/cross-checking', + icon: 'ri-color-filter-line', + order: 7 + } + ] +}; + +/** + * 根据角色获取用户可访问的路由 + * @param roleKey 角色标识 (如: 'admin', 'common', 'deptLeader', 'groupLeader') + * @returns 用户可访问的路由列表 + */ +export async function getUserRoutesByRole(roleKey: string): Promise<{ success: boolean; data?: MenuItem[]; error?: string; shouldRedirectToHome?: boolean }> { + try { + console.log(`获取角色 ${roleKey} 的路由权限`); + + // 首先获取角色ID + const roleResult = await postgrestGet>("roles", { + filter: { + "role_key": `eq.${roleKey}` + } + }); + + if (roleResult.error || !roleResult.data || roleResult.data.length === 0) { + console.error("角色不存在:", roleKey); + toastService.error("角色不存在,请联系管理员配置权限后重新登录"); + return { success: false, error: "角色不存在", shouldRedirectToHome: true }; + } + + const roleId = roleResult.data[0].id; + + // 查询角色的路由权限 + const roleRoutesResult = await postgrestGet>("role_route", { + filter: { + "role_id": `eq.${roleId}` + } + }); + + if (roleRoutesResult.error) { + console.error("查询角色路由关联失败:", roleRoutesResult.error); + toastService.error("查询角色路由关联失败,请稍后再试"); + return { success: false, error: "查询角色路由关联失败", shouldRedirectToHome: true }; + } + + const roleRoutes = roleRoutesResult.data || []; + const routeIds = roleRoutes.map(item => item.route_id); + + if (routeIds.length === 0) { + console.log(`角色 ${roleKey} 没有分配任何路由权限`); + toastService.error("您的角色没有分配任何路由权限,请联系管理员配置权限"); + return { success: false, error: "角色没有分配任何路由权限", shouldRedirectToHome: true }; + } + + // 查询具体的路由信息 + const routesResult = await postgrestGet("sys_routes", { + filter: { + "id": `in.(${routeIds.join(',')})`, + "is_menu": "eq.1" + }, + order: "parent_id,meta->>order" + }); + + if (routesResult.error) { + console.error("查询路由信息失败:", routesResult.error); + toastService.error("查询路由信息失败,请稍后再试"); + return { success: false, error: "查询路由信息失败", shouldRedirectToHome: true }; + } + + const routes = routesResult.data || []; + + // 构建菜单树 + const menuItems = buildMenuTreeFromRoutes(routes); + + console.log(`角色 ${roleKey} 可访问 ${menuItems.length} 个路由`); + return { success: true, data: menuItems }; + + } catch (error) { + console.error("获取用户路由时发生错误:", error); + toastService.error("获取用户路由时发生错误,请稍后再试"); + return { + success: false, + error: `获取用户路由失败: ${error instanceof Error ? error.message : String(error)}`, + shouldRedirectToHome: true + }; + } +} + +/** + * 从路由信息构建菜单树结构 + * @param routes 路由信息数组 + * @returns 菜单树结构 + */ +function buildMenuTreeFromRoutes(routes: RouteInfo[]): MenuItem[] { + // 转换为MenuItem格式 + const menuMap = new Map(); + + routes.forEach(route => { + const menuItem: MenuItem = { + id: route.name, + title: route.meta.title, + path: route.path, + icon: route.meta.icon, + order: route.meta.order || 0, + requiredRole: route.meta.requiredRole + }; + + menuMap.set(route.id, menuItem); + }); + + // 构建父子关系 + const rootItems: MenuItem[] = []; + const itemsWithParent: Array<{ item: MenuItem; parentId: number }> = []; + + routes.forEach(route => { + const menuItem = menuMap.get(route.id); + if (!menuItem) return; + + if (route.parent_id === 0) { + rootItems.push(menuItem); + } else { + itemsWithParent.push({ item: menuItem, parentId: route.parent_id }); + } + }); + + // 添加子菜单 + itemsWithParent.forEach(({ item, parentId }) => { + const parent = menuMap.get(parentId); + if (parent) { + if (!parent.children) { + parent.children = []; + } + parent.children.push(item); + } + }); + + // 排序 + rootItems.sort((a, b) => a.order - b.order); + rootItems.forEach(item => { + if (item.children) { + item.children.sort((a, b) => a.order - b.order); + } + }); + + return rootItems; +} + +/** + * 根据用户角色映射到权限系统的角色标识 + * @param userRole 前端用户角色 ('common' | 'developer') + * @returns 数据库中的角色标识 + */ +export function mapUserRoleToRoleKey(userRole: string): string { + const roleMapping: Record = { + 'common': 'common', + 'developer': 'admin', + 'deptLeader': 'deptLeader', + 'groupLeader': 'groupLeader' + }; + + return roleMapping[userRole] || 'common'; +} \ No newline at end of file diff --git a/app/api/files/documents.ts b/app/api/files/documents.ts index 213f9a4..7b7ee4f 100644 --- a/app/api/files/documents.ts +++ b/app/api/files/documents.ts @@ -38,6 +38,7 @@ export interface DocumentSearchParams { page?: number; pageSize?: number; reviewType?: string; + userId?: string; // 添加用户ID筛选 } /** @@ -214,7 +215,8 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro fileStatus, dateFrom, dateTo, - reviewType + reviewType, + userId } = searchParams; let documentTypes: number[] | undefined; @@ -228,6 +230,11 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro } } + // 确保userId必须存在,如果不存在则抛出错误 + if (!userId) { + return { error: '用户身份验证失败,无法获取文档列表', status: 401 }; + } + const rpcParams = { search_name: name, search_document_number: documentNumber, @@ -236,6 +243,7 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro search_file_status: fileStatus, search_date_from: dateFrom, search_date_to: dateTo, + search_user_id: parseInt(userId, 10), // 强制要求传递用户ID }; // 并行执行获取数据和获取总数的请求 diff --git a/app/api/login/auth.server.ts b/app/api/login/auth.server.ts index 955d9e4..068d2fa 100644 --- a/app/api/login/auth.server.ts +++ b/app/api/login/auth.server.ts @@ -8,6 +8,7 @@ * - OAuth2.0 Token 自动刷新 * - 用户登录状态检查 * - 会话创建和销毁 + * - 用户信息保存到数据库 * * 技术栈: * - Remix Session Storage (Cookie-based) @@ -18,6 +19,7 @@ import { createCookieSessionStorage } from "@remix-run/node"; import { tokenManager } from "./token-manager.server"; +import { postgrestGet, postgrestPost, postgrestPut } from "../postgrest-client"; /** * 用户角色类型定义 @@ -28,6 +30,42 @@ import { tokenManager } from "./token-manager.server"; */ export type UserRole = 'common' | 'developer'; +/** + * 用户信息接口,对应 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; // 是否为部门负责人 +} + +/** + * 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; + created_at?: string; + updated_at?: string; + deleted_at?: string; +} + /** * 会话存储配置 * @@ -230,4 +268,248 @@ export async function logout(request: Request) { "Set-Cookie": await sessionStorage.destroySession(session), // 清除会话 Cookie }, }); +} + +/** + * 保存用户信息到数据库 + * + * 此函数实现以下逻辑: + * 1. 根据 userInfo.sub 查询 sso_users 表中是否已存在该用户 + * 2. 如果存在,则更新用户信息 + * 3. 如果不存在,则插入新的用户记录 + * + * @param userInfo - 从 IDaaS 获取的用户信息 + * @returns Promise<{success: boolean, data?: SsoUser, error?: string}> + */ +export async function saveUserInfo(userInfo: UserInfo): Promise<{success: boolean, data?: SsoUser, error?: string}> { + try { + console.log("开始保存用户信息", userInfo); + + // 验证必要字段 + if (!userInfo.sub) { + return { success: false, error: "用户唯一标识 sub 不能为空" }; + } + + // 1. 根据 sub 查询是否已存在该用户 + const existingUserResult = await postgrestGet("sso_users", { + filter: { + "sub": `eq.${userInfo.sub}`, + "deleted_at": "is.null" // 只查询未删除的记录 + } + }); + + 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); + + const updateResult = await postgrestPut>( + "sso_users", + userData, + { id: existingUser.id! } + ); + + 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 + }; + } else { + // 3. 用户不存在,执行插入操作 同时需要给这个用户默认添加一个角色,角色为common + console.log("用户不存在,执行插入操作"); + + const insertResult = await postgrestPost("sso_users", userData as SsoUser); + + 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); + } + + return { + success: true, + data: userData_with_id + }; + } + } catch (error) { + console.error("保存用户信息时发生错误:", error); + return { + success: false, + error: `保存用户信息失败: ${error instanceof Error ? error.message : String(error)}` + }; + } +} + +/** + * 为用户添加默认角色 + * + * @param userId - 用户ID + * @param roleId - 角色ID,默认为2(common角色) + * @returns 添加结果 + */ +export async function addDefaultRole(userId: string, roleId: number = 2) { + try { + console.log(`为用户 ${userId} 添加默认角色 ${roleId}`); + + // 检查用户是否已经有此角色 + const existingRoleResult = await postgrestGet>("user_role", { + filter: { + user_id: `eq.${userId}`, + role_id: `eq.${roleId}` + } + }); + + 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 + }); + + 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)}` + }; + } +} + +/** + * 创建用户登录会话(支持用户信息) + * + * @param isAuthenticated - 是否已认证 + * @param userRole - 用户角色 + * @param redirectTo - 重定向URL + * @param userInfo - 可选的用户信息 + * @returns HTTP重定向响应 + */ +export async function createUserSessionWithInfo( + isAuthenticated: boolean, + userRole: UserRole, + redirectTo: string, + userInfo?: Partial +) { + const session = await sessionStorage.getSession(); + session.set("isAuthenticated", isAuthenticated); + session.set("userRole", userRole); + + // 如果提供了用户信息,也保存到session中 + if (userInfo) { + session.set("userInfo", { + sub: userInfo.sub, + user_id: userInfo.id, + username: userInfo.username, + nick_name: userInfo.nick_name, + email: userInfo.email, + ou_name: userInfo.ou_name, + is_leader: userInfo.is_leader, + user_role: userRole + }); + } + + const cookie = await sessionStorage.commitSession(session); + console.log("创建会话 - 设置Cookie:", !!cookie); + console.log("创建会话 - 用户角色:", userRole); + console.log("创建会话 - 用户信息:", userInfo?.username || "无"); + console.log("创建会话 - 重定向到:", redirectTo); + + return new Response(null, { + status: 302, + headers: { + Location: redirectTo, + "Set-Cookie": cookie, + }, + }); } \ No newline at end of file diff --git a/app/components/cross-checking/ReviewPointsList.tsx b/app/components/cross-checking/ReviewPointsList.tsx index ed5e8ed..f7483d8 100644 --- a/app/components/cross-checking/ReviewPointsList.tsx +++ b/app/components/cross-checking/ReviewPointsList.tsx @@ -516,14 +516,16 @@ export function ReviewPointsList({ /** * 加载意见列表数据 */ - const loadOpinionListData = async (page: number = 1, pageSize: number = 10) => { - console.log('加载意见列表数据', selectedReviewPoint); - if (!selectedReviewPoint?.documentId) return; + const loadOpinionListData = async (page: number = 1, pageSize: number = 10, documentId?: string | number) => { + // 使用传入的documentId或者从selectedReviewPoint获取 + const targetDocumentId = documentId || selectedReviewPoint?.documentId; + console.log('加载意见列表数据', targetDocumentId); + if (!targetDocumentId) return; setOpinionListLoading(true); try { - console.log('加载意见列表数据', selectedReviewPoint.documentId, page, pageSize); - const response = await getCrossCheckingOpinions(selectedReviewPoint.documentId, page, pageSize); + console.log('加载意见列表数据', targetDocumentId, page, pageSize); + const response = await getCrossCheckingOpinions(targetDocumentId, page, pageSize); console.log('意见列表数据', response); if (response.error) { @@ -555,7 +557,8 @@ export function ReviewPointsList({ setSelectedReviewPoint(reviewPoint); setIsOpinionListModalOpen(true); console.log('打开意见列表模态框'); - loadOpinionListData(1, 10); + // 直接传递reviewPoint的documentId,避免依赖状态更新 + loadOpinionListData(1, 10, reviewPoint.documentId); }; /** @@ -2253,15 +2256,13 @@ export function ReviewPointsList({ {/* 悬浮状态:横向排列,显示图标,数字放大 */}
- +
+ + {scoringProposals.length} + + + +
@@ -2451,7 +2452,7 @@ export function ReviewPointsList({ - {/* 意见列表模态框 */} + {/* 意见列表模态框 */} void; @@ -21,8 +12,8 @@ interface SidebarProps { // 定义不同应用模块下显示的菜单项ID const APP_MENU_MAP = { - 'contract': ['home', 'contract-template', 'file-management', 'rule-management', 'system-settings'], - 'record': ['home', 'file-management', 'rule-management', 'system-settings'], + 'contract': ['home', 'contract-template', 'file-management', 'rule-management', 'cross-checking', 'system-settings'], + 'record': ['home', 'file-management', 'rule-management', 'cross-checking', 'system-settings'], 'model': ['chat-with-llm'] }; @@ -45,8 +36,47 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid const [expandedMenus, setExpandedMenus] = useState>({}); const [currentApp, setCurrentApp] = useState(''); // 初始设置为空字符串而不是selectedApp const [isLoading, setIsLoading] = useState(true); // 添加加载状态 + const [menuItems, setMenuItems] = useState([]); // 动态菜单项 + const [isLoadingRoutes, setIsLoadingRoutes] = useState(true); // 路由加载状态 const navigate = useNavigate(); + // 获取用户路由权限 + useEffect(() => { + const fetchUserRoutes = async () => { + setIsLoadingRoutes(true); + try { + const roleKey = mapUserRoleToRoleKey(userRole); + const result = await getUserRoutesByRole(roleKey); + + if (result.success && result.data) { + setMenuItems(result.data); + console.log('用户路由权限加载成功:', result.data); + } else { + console.error('获取用户路由权限失败:', result.error); + + // 如果需要重定向到首页 + if (result.shouldRedirectToHome) { + console.log('重定向到首页'); + navigate('/'); + return; + } + + // 其他错误情况,使用空数组 + setMenuItems([]); + } + } catch (error) { + console.error('获取用户路由权限时发生错误:', error); + // 发生异常时也重定向到首页 + navigate('/'); + return; + } finally { + setIsLoadingRoutes(false); + } + }; + + fetchUserRoutes(); + }, [userRole, navigate]); + // 组件挂载后从 sessionStorage 读取初始 reviewType useEffect(() => { let mounted = true; @@ -135,142 +165,6 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid } }, [selectedApp, currentApp]); - const menuItems: MenuItem[] = [ - { - id: 'home', - title: '系统概览', - path: '/home', - icon: 'ri-home-line' - }, - { - id: 'chat-with-llm', - title: 'AI对话', - path: '/chat-with-llm', - icon: 'ri-chat-smile-2-line' - }, - { - id: 'file-management', - title: '文件管理', - path: '/files', - icon: 'ri-folder-line', - children: [ - { - id: 'file-upload', - title: '文件上传', - path: '/files/upload', - icon: 'ri-upload-cloud-line' - }, - { - id: 'documents', - title: '文档列表', - path: '/documents', - icon: 'ri-file-list-3-line' - } - ] - }, - { - id: 'rule-management', - title: '评查规则库', - path: '/rules', - icon: 'ri-book-3-line', - children: [ - { - id: 'rule-groups', - title: '评查点分组', - path: '/rule-groups', - icon: 'ri-folder-open-line' - }, - { - id: 'rules-list', - title: '评查点列表', - path: '/rules', - icon: 'ri-list-check-3' - }, - { - id: 'rules-file', - title: '评查文件列表', - path: '/rules-files', - icon: 'ri-list-check-2' - }, - { - id: 'cross-checking', - title: '交叉评查', - path: '/cross-checking', - icon: 'ri-color-filter-line' - }, - // { - // id: 'rule-new', - // title: '新增评查点', - // path: '/rules-new', - // requiredRole: 'developer', - // icon: 'ri-add-circle-line' - // }, - // { - // id: 'review-detail', - // title: '评查详情', - // path: '/reviews', - // icon: 'ri-file-chart-line' - // } - ] - }, - { - id: 'contract-template', - title: '合同模板', - path: '/contract-template', - icon: 'ri-file-search-line', - children: [ - { - id: 'contract-search-ai', - title: '智能搜索', - path: '/contract-template/search', - icon: 'ri-search-line' - }, - { - id: 'contract-list', - title: '合同列表', - path: '/contract-template/list', - icon: 'ri-folder-line' - } - ] - }, - { - id: 'system-settings', - title: '系统设置', - path: '/settings', - icon: 'ri-settings-4-line', - requiredRole: 'developer', - children: [ - { - id: 'config-lists', - title: '配置列表', - path: '/config-lists', - icon: 'ri-list-check-3', - requiredRole: 'developer' - }, - // { - // id: 'basic-settings', - // title: '基础设置', - // path: '/settings', - // icon: 'ri-equalizer-line' - // }, - { - id: 'document-types', - title: '文档类型', - path: '/document-types', - icon: 'ri-file-list-line', - requiredRole: 'developer' - }, - { - id: 'prompt-management', - title: '提示词管理', - path: '/prompts', - icon: 'ri-chat-1-line', - requiredRole: 'developer' - } - ] - } - ]; - // 初始化展开状态,默认全部展开 useEffect(() => { const initialExpandedState: Record = {}; @@ -280,7 +174,7 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid } }); setExpandedMenus(initialExpandedState); - }, []); + }, [menuItems]); const toggleMenu = (id: string, e: React.MouseEvent) => { // 我们只防止事件冒泡,不阻止默认行为 @@ -318,13 +212,8 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid // const visibleMenuIds = APP_MENU_MAP[currentApp as keyof typeof APP_MENU_MAP] // console.log('当前应用模式:', currentApp, '可见菜单ID:', visibleMenuIds); - // 根据用户角色和当前应用模式过滤菜单项 + // 根据当前应用模式过滤菜单项 const filteredMenuItems = menuItems.filter(item => { - // 如果菜单项需要特定角色但用户没有 - if (item.requiredRole && item.requiredRole !== userRole) { - return false; - } - // 检查当前菜单是否在所选应用模式中显示 if (!visibleMenuIds.includes(item.id)) { return false; @@ -382,7 +271,7 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid )}
- {isLoading ? ( + {isLoading || isLoadingRoutes ? ( // 加载中状态显示,保留菜单布局结构
{Array(5).fill(0).map((_, index) => ( @@ -444,19 +333,17 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid className={`submenu-container ${collapsed ? 'border-l-0 pl-0' : 'border-l border-gray-100 ml-4 pl-3'} z-20`} id={`submenu-${item.id}`} > - {item.children - .filter(child => !child.requiredRole || child.requiredRole === userRole) - .map((child) => ( - handleSubMenuClick(child, e)} - > - - {!collapsed && {child.title}} - - ))} + {item.children.map((child) => ( + handleSubMenuClick(child, e)} + > + + {!collapsed && {child.title}} + + ))}
)} diff --git a/app/routes/callback.tsx b/app/routes/callback.tsx index c26308a..59d0fd5 100644 --- a/app/routes/callback.tsx +++ b/app/routes/callback.tsx @@ -1,7 +1,7 @@ import { type LoaderFunctionArgs, redirect } from "@remix-run/node"; import { OAuthClient } from "~/api/login/oauth-client"; import { OAUTH_CONFIG } from "~/config/api-config"; -import { sessionStorage } from "~/api/login/auth.server"; +import { sessionStorage, saveUserInfo } from "~/api/login/auth.server"; import { toastService } from "~/components/ui"; export async function loader({ request }: LoaderFunctionArgs) { @@ -58,14 +58,24 @@ export async function loader({ request }: LoaderFunctionArgs) { session.set("tokenExpiresIn", tokenResponse.expires_in); session.set("userInfo", userInfo.data); - // 根据用户信息判断用户角色,这里可以根据实际业务逻辑调整 - const userRole = userInfo.data.username === "admin" ? "developer" : "common"; + // TODO 根据用户信息判断用户角色,这里可以根据实际业务逻辑调整 暂定都是common + // const userRole = userInfo.data.username === "admin" ? "developer" : "common"; + const userRole = "common"; session.set("userRole", userRole); // 获取重定向URL const redirectTo = url.searchParams.get("redirect") || "/"; const cookie = await sessionStorage.commitSession(session); + + // 成功获取用户信息之后通过auth.server.ts中的saveUserInfo方法去写入自己的数据库中,通过sub作为唯一值去添加数据 + const saveResult = await saveUserInfo(userInfo.data); + if (!saveResult.success) { + console.error("保存用户信息到数据库失败:", saveResult.error); + // 注意:即使保存到数据库失败,我们仍然继续登录流程,因为用户已经通过了身份验证 + } else { + console.log("用户信息已成功保存到数据库"); + } return redirect(redirectTo, { headers: { diff --git a/app/routes/documents._index.tsx b/app/routes/documents._index.tsx index 682b33b..8de6281 100644 --- a/app/routes/documents._index.tsx +++ b/app/routes/documents._index.tsx @@ -35,6 +35,11 @@ export const meta: MetaFunction = () => { // 数据加载器 export const loader = async ({ request }: LoaderFunctionArgs) => { + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { userInfo } = await getUserSession(request); + console.log(userInfo); + // 获取URL查询参数,只保留必要的分页参数 const url = new URL(request.url); const page = parseInt(url.searchParams.get("page") || "1", 10); @@ -55,6 +60,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { page, pageSize, documentTypeOptions, + userInfo, // 传递用户信息到客户端 initialLoad: true // 标记这是初始加载 }); }; @@ -195,6 +201,9 @@ export default function DocumentsIndex() { loadingBarService.show(); try { + // 从loader data中获取用户ID + const userId = loaderData.userInfo?.user_id?.toString(); + // 构建搜索参数 const searchParams = { name: search || undefined, @@ -205,6 +214,7 @@ export default function DocumentsIndex() { dateFrom: dateFrom || undefined, dateTo: dateTo || undefined, reviewType: storedReviewType || undefined, + userId: userId, // 添加用户ID筛选 page: currentPage, pageSize }; diff --git a/app/routes/login.tsx b/app/routes/login.tsx index 6914467..3cf1179 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -3,7 +3,7 @@ import { useSearchParams, Form } from "@remix-run/react"; import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs, redirect } from "@remix-run/node"; import { OAuthClient } from "~/api/login/oauth-client"; import { OAUTH_CONFIG } from "~/config/api-config"; -import { getUserSession, getSession, createUserSession } from "~/api/login/auth.server"; +import { getUserSession, getSession, createUserSessionWithInfo, getUserBySub, addDefaultRole } from "~/api/login/auth.server"; import styles from "~/styles/pages/login.css?url"; export const links = () => [ @@ -44,13 +44,29 @@ export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const intent = formData.get("intent"); - if (intent === "temp_admin_login") { + if (intent === "test_user_login") { // 获取重定向目标 const session = await getSession(request); const redirectTo = session.get("redirectTo") || "/"; - // 创建管理员会话 - return createUserSession(true, 'developer', redirectTo); + // 使用测试用户登录 + const testUserSub = "001"; // 测试用户的sub + const userResult = await getUserBySub(testUserSub); + + if (userResult.success && userResult.data) { + const user = userResult.data; + + // 确保用户有默认角色 + if (user.id) { + await addDefaultRole(user.id, 2); // 添加common角色 + } + + // 创建用户会话,默认角色为common,并保存用户信息 + return createUserSessionWithInfo(true, 'common', redirectTo, user); + } else { + // 如果用户不存在,重定向到登录页面并显示错误 + return redirect(`/login?error=${encodeURIComponent("测试用户不存在")}`); + } } return null; @@ -149,25 +165,25 @@ export default function Login() {
- {/* 临时管理员登录区域 */} + {/* 测试用户登录区域 */}
- +

- - 仅供开发测试使用,将以管理员身份登录 + + 使用测试用户(testuser1)登录,默认普通权限

diff --git a/sql/role_route.sql b/sql/role_route.sql new file mode 100644 index 0000000..a339b29 --- /dev/null +++ b/sql/role_route.sql @@ -0,0 +1,61 @@ +/* + Navicat Premium Data Transfer + + Source Server : 智慧法务 + Source Server Type : PostgreSQL + Source Server Version : 170005 + Source Host : nas.7bm.co:54302 + Source Catalog : docauditai + Source Schema : public + + Target Server Type : PostgreSQL + Target Server Version : 170005 + File Encoding : 65001 + + Date: 20/07/2025 19:51:40 +*/ + + +-- ---------------------------- +-- Table structure for role_route +-- ---------------------------- +DROP TABLE IF EXISTS "public"."role_route"; +CREATE TABLE "public"."role_route" ( + "id" int4 NOT NULL DEFAULT nextval('role_route_id_seq'::regclass), + "role_id" int4 NOT NULL, + "route_id" int4 NOT NULL, + "permission" varchar(10) COLLATE "pg_catalog"."default" DEFAULT 'RW'::character varying, + "created_at" timestamptz(6) DEFAULT now(), + "updated_at" timestamptz(6) DEFAULT now() +) +; +COMMENT ON COLUMN "public"."role_route"."id" IS '主键ID'; +COMMENT ON COLUMN "public"."role_route"."role_id" IS '角色ID'; +COMMENT ON COLUMN "public"."role_route"."route_id" IS '路由ID'; +COMMENT ON COLUMN "public"."role_route"."permission" IS '权限类型(R=读, W=写, RW=读写)'; +COMMENT ON COLUMN "public"."role_route"."created_at" IS '创建时间'; +COMMENT ON COLUMN "public"."role_route"."updated_at" IS '更新时间'; +COMMENT ON TABLE "public"."role_route" IS '角色-路由权限关联表'; + +-- ---------------------------- +-- Triggers structure for table role_route +-- ---------------------------- +CREATE TRIGGER "update_role_route_updated_at" BEFORE UPDATE ON "public"."role_route" +FOR EACH ROW +EXECUTE PROCEDURE "public"."update_updated_at"(); + +-- ---------------------------- +-- Uniques structure for table role_route +-- ---------------------------- +ALTER TABLE "public"."role_route" ADD CONSTRAINT "role_route_role_id_route_id_key" UNIQUE ("role_id", "route_id"); + +-- ---------------------------- +-- Primary Key structure for table role_route +-- ---------------------------- +ALTER TABLE "public"."role_route" ADD CONSTRAINT "role_route_pkey" PRIMARY KEY ("id"); + +-- ---------------------------- +-- Foreign Keys structure for table role_route +-- ---------------------------- +ALTER TABLE "public"."role_route" ADD CONSTRAINT "fk_role_route_role" FOREIGN KEY ("role_id") REFERENCES "public"."roles" ("id") ON DELETE CASCADE ON UPDATE NO ACTION; +ALTER TABLE "public"."role_route" ADD CONSTRAINT "fk_role_route_route" FOREIGN KEY ("route_id") REFERENCES "public"."sys_routes" ("id") ON DELETE CASCADE ON UPDATE NO ACTION; diff --git a/sql/role_route_insert.sql b/sql/role_route_insert.sql new file mode 100644 index 0000000..7a279b7 --- /dev/null +++ b/sql/role_route_insert.sql @@ -0,0 +1,63 @@ +-- 角色路由权限数据插入脚本 +-- 根据sys_routes_insert.sql中的路由数据和roles.sql中的角色数据进行关联 + +-- 清理现有数据(可选) +-- DELETE FROM role_route WHERE id > 0; + +-- ---------------------------- +-- 为admin角色分配全部路由权限 +-- ---------------------------- +INSERT INTO role_route (role_id, route_id, permission) +SELECT 1, id, 'RW' FROM sys_routes; + +-- ---------------------------- +-- 为common角色分配基础路由权限 +-- ---------------------------- +INSERT INTO role_route (role_id, route_id, permission) +SELECT 2, id, 'RW' FROM sys_routes WHERE name IN ( + 'home', + 'file-management', 'file-upload', 'documents', + 'rule-management', 'rule-groups', 'rules-list', 'rules-file', + 'contract-template', 'contract-search-ai', 'contract-list', + 'cross-checking' +) ON CONFLICT (role_id, route_id) DO NOTHING; + +-- ---------------------------- +-- 为deptLeader角色分配扩展权限 +-- ---------------------------- +INSERT INTO role_route (role_id, route_id, permission) +SELECT 3, id, 'RW' FROM sys_routes WHERE name IN ( + 'home', + 'chat-with-llm', + 'file-management', 'file-upload', 'documents', + 'rule-management', 'rule-groups', 'rules-list', 'rules-file', + 'contract-template', 'contract-search-ai', 'contract-list', + 'cross-checking' +) ON CONFLICT (role_id, route_id) DO NOTHING; + +-- ---------------------------- +-- 为groupLeader角色分配小组权限 +-- ---------------------------- +INSERT INTO role_route (role_id, route_id, permission) +SELECT 4, id, 'RW' FROM sys_routes WHERE name IN ( + 'home', + 'file-management', 'file-upload', 'documents', + 'rule-management', 'rule-groups', 'rules-list', 'rules-file', + 'contract-template', 'contract-search-ai', 'contract-list', + 'cross-checking' +) ON CONFLICT (role_id, route_id) DO NOTHING; + +-- ---------------------------- +-- 查询角色权限分配结果 +-- ---------------------------- +SELECT + r.role_name, + sr.name as route_name, + sr.path, + sr.meta->>'title' as route_title, + rr.permission +FROM role_route rr +JOIN roles r ON rr.role_id = r.id +JOIN sys_routes sr ON rr.route_id = sr.id +WHERE sr.parent_id = 0 -- 只显示一级菜单 +ORDER BY r.id, (sr.meta->>'order')::int; \ No newline at end of file diff --git a/sql/roles.sql b/sql/roles.sql new file mode 100644 index 0000000..5f0292c --- /dev/null +++ b/sql/roles.sql @@ -0,0 +1,65 @@ +/* + Navicat Premium Data Transfer + + Source Server : 智慧法务 + Source Server Type : PostgreSQL + Source Server Version : 170005 + Source Host : nas.7bm.co:54302 + Source Catalog : docauditai + Source Schema : public + + Target Server Type : PostgreSQL + Target Server Version : 170005 + File Encoding : 65001 + + Date: 18/07/2025 20:16:41 +*/ + + +-- ---------------------------- +-- Table structure for roles +-- ---------------------------- +DROP TABLE IF EXISTS "public"."roles"; +CREATE TABLE "public"."roles" ( + "id" int4 NOT NULL DEFAULT nextval('user_role_id_seq'::regclass), + "role_key" varchar(30) COLLATE "pg_catalog"."default" NOT NULL, + "role_name" varchar(50) COLLATE "pg_catalog"."default" NOT NULL, + "data_scope" varchar(20) COLLATE "pg_catalog"."default" DEFAULT 'SELF'::character varying, + "description" varchar(200) COLLATE "pg_catalog"."default" DEFAULT ''::character varying, + "created_at" timestamptz(6) DEFAULT now(), + "updated_at" timestamptz(6) DEFAULT now() +) +; +COMMENT ON COLUMN "public"."roles"."id" IS '角色ID'; +COMMENT ON COLUMN "public"."roles"."role_key" IS '角色标识(如:admin, common, deptLeader, groupLeader)'; +COMMENT ON COLUMN "public"."roles"."role_name" IS '角色名称(如:系统管理员, 普通员工)'; +COMMENT ON COLUMN "public"."roles"."data_scope" IS '数据权限范围(SELF=仅自己, GROUP=本组, DEPT=本部门, ALL=全部)'; +COMMENT ON COLUMN "public"."roles"."description" IS '角色描述'; +COMMENT ON COLUMN "public"."roles"."created_at" IS '创建时间'; +COMMENT ON COLUMN "public"."roles"."updated_at" IS '更新时间'; +COMMENT ON TABLE "public"."roles" IS '系统角色表'; + +-- ---------------------------- +-- Records of roles +-- ---------------------------- +INSERT INTO "public"."roles" VALUES (1, 'admin', '系统管理员', 'ALL', '拥有系统全部操作权限', '2025-07-18 02:35:39.367459+00', '2025-07-18 02:35:39.367459+00'); +INSERT INTO "public"."roles" VALUES (2, 'common', '普通员工', 'SELF', '仅能操作自己的数据', '2025-07-18 02:35:39.367459+00', '2025-07-18 02:35:39.367459+00'); +INSERT INTO "public"."roles" VALUES (3, 'deptLeader', '部门主管', 'DEPT', '可操作本部门的数据', '2025-07-18 02:35:39.367459+00', '2025-07-18 02:35:39.367459+00'); +INSERT INTO "public"."roles" VALUES (4, 'groupLeader', '小组组长', 'GROUP', '可操作本小组的数据', '2025-07-18 02:35:39.367459+00', '2025-07-18 02:35:39.367459+00'); + +-- ---------------------------- +-- Triggers structure for table roles +-- ---------------------------- +CREATE TRIGGER "update_user_role_updated_at" BEFORE UPDATE ON "public"."roles" +FOR EACH ROW +EXECUTE PROCEDURE "public"."update_updated_at"(); + +-- ---------------------------- +-- Uniques structure for table roles +-- ---------------------------- +ALTER TABLE "public"."roles" ADD CONSTRAINT "roles_role_key_key" UNIQUE ("role_key"); + +-- ---------------------------- +-- Primary Key structure for table roles +-- ---------------------------- +ALTER TABLE "public"."roles" ADD CONSTRAINT "user_role_pkey" PRIMARY KEY ("id"); diff --git a/sql/sso_users.sql b/sql/sso_users.sql new file mode 100644 index 0000000..c1d8c34 --- /dev/null +++ b/sql/sso_users.sql @@ -0,0 +1,84 @@ +/* + Navicat Premium Data Transfer + + Source Server : 智慧法务 + Source Server Type : PostgreSQL + Source Server Version : 170005 + Source Host : nas.7bm.co:54302 + Source Catalog : docauditai + Source Schema : public + + Target Server Type : PostgreSQL + Target Server Version : 170005 + File Encoding : 65001 + + Date: 17/07/2025 21:07:07 +*/ + + +-- ---------------------------- +-- Table structure for sso_users +-- ---------------------------- +DROP TABLE IF EXISTS "public"."sso_users"; +CREATE TABLE "public"."sso_users" ( + "id" uuid NOT NULL DEFAULT gen_random_uuid(), + "sub" varchar(255) COLLATE "pg_catalog"."default" NOT NULL, + "username" varchar(255) COLLATE "pg_catalog"."default" NOT NULL, + "nick_name" varchar(255) COLLATE "pg_catalog"."default" NOT NULL, + "phone_number" varchar(20) COLLATE "pg_catalog"."default", + "email" varchar(255) COLLATE "pg_catalog"."default", + "ou_id" varchar(255) COLLATE "pg_catalog"."default" NOT NULL, + "ou_name" varchar(255) COLLATE "pg_catalog"."default" NOT NULL, + "status" int2 DEFAULT 0, + "is_leader" bool DEFAULT false, + "created_at" timestamp(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp(6) DEFAULT CURRENT_TIMESTAMP, + "deleted_at" timestamp(6) +) +; +COMMENT ON COLUMN "public"."sso_users"."id" IS '主键,数据库自增或全局唯一'; +COMMENT ON COLUMN "public"."sso_users"."sub" IS 'IDaaS用户唯一标识(身份证号级别)'; +COMMENT ON COLUMN "public"."sso_users"."username" IS '显示用户名称/工号'; +COMMENT ON COLUMN "public"."sso_users"."nick_name" IS '用户真实姓名'; +COMMENT ON COLUMN "public"."sso_users"."phone_number" IS '手机号'; +COMMENT ON COLUMN "public"."sso_users"."email" IS '邮箱地址(可为空)'; +COMMENT ON COLUMN "public"."sso_users"."ou_id" IS '所属组织单位ID(部门ID)'; +COMMENT ON COLUMN "public"."sso_users"."ou_name" IS '所属部门名称'; +COMMENT ON COLUMN "public"."sso_users"."status" IS '账户状态: 0=正常, 1=禁用'; +COMMENT ON COLUMN "public"."sso_users"."is_leader" IS '是否为部门负责人'; +COMMENT ON COLUMN "public"."sso_users"."created_at" IS '创建时间'; +COMMENT ON COLUMN "public"."sso_users"."updated_at" IS '更新时间'; +COMMENT ON COLUMN "public"."sso_users"."deleted_at" IS '删除时间(软删除)'; +COMMENT ON TABLE "public"."sso_users" IS '统一登录用户表'; + +-- ---------------------------- +-- Indexes structure for table sso_users +-- ---------------------------- +CREATE INDEX "idx_sso_users_deleted_at" ON "public"."sso_users" USING btree ( + "deleted_at" "pg_catalog"."timestamp_ops" ASC NULLS LAST +); +CREATE INDEX "idx_sso_users_is_leader" ON "public"."sso_users" USING btree ( + "is_leader" "pg_catalog"."bool_ops" ASC NULLS LAST +); +CREATE INDEX "idx_sso_users_ou_id" ON "public"."sso_users" USING btree ( + "ou_id" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST +); +CREATE INDEX "idx_sso_users_status" ON "public"."sso_users" USING btree ( + "status" "pg_catalog"."int2_ops" ASC NULLS LAST +); +CREATE INDEX "idx_sso_users_sub" ON "public"."sso_users" USING btree ( + "sub" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST +); +CREATE INDEX "idx_sso_users_username" ON "public"."sso_users" USING btree ( + "username" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST +); + +-- ---------------------------- +-- Uniques structure for table sso_users +-- ---------------------------- +ALTER TABLE "public"."sso_users" ADD CONSTRAINT "sso_users_sub_key" UNIQUE ("sub"); + +-- ---------------------------- +-- Primary Key structure for table sso_users +-- ---------------------------- +ALTER TABLE "public"."sso_users" ADD CONSTRAINT "sso_users_pkey" PRIMARY KEY ("id"); diff --git a/sql/sys_routes.sql b/sql/sys_routes.sql new file mode 100644 index 0000000..ef1c222 --- /dev/null +++ b/sql/sys_routes.sql @@ -0,0 +1,59 @@ +/* + Navicat Premium Data Transfer + + Source Server : 智慧法务 + Source Server Type : PostgreSQL + Source Server Version : 170005 + Source Host : nas.7bm.co:54302 + Source Catalog : docauditai + Source Schema : public + + Target Server Type : PostgreSQL + Target Server Version : 170005 + File Encoding : 65001 + + Date: 18/07/2025 15:16:47 +*/ + + +-- ---------------------------- +-- Table structure for sys_routes +-- ---------------------------- +DROP TABLE IF EXISTS "public"."sys_routes"; +CREATE TABLE "public"."sys_routes" ( + "id" int4 NOT NULL DEFAULT nextval('sys_route_id_seq'::regclass), + "path" varchar(100) COLLATE "pg_catalog"."default" NOT NULL, + "name" varchar(50) COLLATE "pg_catalog"."default" NOT NULL, + "meta" json NOT NULL, + "parent_id" int4 DEFAULT 0, + "is_menu" int2 DEFAULT 1, + "create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP, + "update_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP +) +; +COMMENT ON COLUMN "public"."sys_routes"."id" IS '路由ID'; +COMMENT ON COLUMN "public"."sys_routes"."path" IS '路由路径(如:/user/list)'; +COMMENT ON COLUMN "public"."sys_routes"."name" IS '路由唯一标识(前端组件名)'; +COMMENT ON COLUMN "public"."sys_routes"."meta" IS '元数据(存储图标/排序/隐藏等配置)'; +COMMENT ON COLUMN "public"."sys_routes"."parent_id" IS '父路由ID(0=根节点)'; +COMMENT ON COLUMN "public"."sys_routes"."is_menu" IS '是否菜单项(0=接口权限, 1=菜单项)'; +COMMENT ON COLUMN "public"."sys_routes"."create_time" IS '创建时间'; +COMMENT ON COLUMN "public"."sys_routes"."update_time" IS '更新时间'; +COMMENT ON TABLE "public"."sys_routes" IS '系统路由权限表'; + +-- ---------------------------- +-- Indexes structure for table sys_routes +-- ---------------------------- +CREATE INDEX "idx_parent_id" ON "public"."sys_routes" USING btree ( + "parent_id" "pg_catalog"."int4_ops" ASC NULLS LAST +); + +-- ---------------------------- +-- Uniques structure for table sys_routes +-- ---------------------------- +ALTER TABLE "public"."sys_routes" ADD CONSTRAINT "uniq_route_name" UNIQUE ("name"); + +-- ---------------------------- +-- Primary Key structure for table sys_routes +-- ---------------------------- +ALTER TABLE "public"."sys_routes" ADD CONSTRAINT "sys_route_pkey" PRIMARY KEY ("id"); diff --git a/sql/sys_routes_insert.sql b/sql/sys_routes_insert.sql new file mode 100644 index 0000000..c7b8011 --- /dev/null +++ b/sql/sys_routes_insert.sql @@ -0,0 +1,93 @@ +-- 根据 Sidebar.tsx 菜单项生成的 sys_routes 表插入语句 +-- 智慧法务系统路由权限数据 + +-- 清理现有数据(可选) +-- DELETE FROM sys_routes WHERE id > 0; +-- ALTER SEQUENCE sys_route_id_seq RESTART WITH 1; + +-- 插入一级菜单项 +INSERT INTO sys_routes (path, name, meta, parent_id, is_menu) VALUES +-- 系统概览 +('/home', 'home', '{"title": "系统概览", "icon": "ri-home-line", "order": 1}', 0, 1), + +-- AI对话 +('/chat-with-llm', 'chat-with-llm', '{"title": "AI对话", "icon": "ri-chat-smile-2-line", "order": 2}', 0, 1), + +-- 文件管理(父菜单) +('/files', 'file-management', '{"title": "文件管理", "icon": "ri-folder-line", "order": 3}', 0, 1), + +-- 评查规则库(父菜单) +('/rules', 'rule-management', '{"title": "评查规则库", "icon": "ri-book-3-line", "order": 4}', 0, 1), + +-- 合同模板(父菜单) +('/contract-template', 'contract-template', '{"title": "合同模板", "icon": "ri-file-search-line", "order": 5}', 0, 1), + +-- 系统设置(父菜单,需要developer权限) +('/settings', 'system-settings', '{"title": "系统设置", "icon": "ri-settings-4-line", "order": 6, "requiredRole": "developer"}', 0, 1); + +-- 交叉评查(父菜单) +('/cross-checking', 'cross-checking', '{"title": "交叉评查", "icon": "ri-color-filter-line", "order": 7}', 0, 1); + +-- 插入文件管理的子菜单项 +INSERT INTO sys_routes (path, name, meta, parent_id, is_menu) VALUES +-- 文件上传(文件管理的子菜单,parent_id需要根据实际的file-management记录ID调整) +('/files/upload', 'file-upload', '{"title": "文件上传", "icon": "ri-upload-cloud-line", "order": 1}', + (SELECT id FROM sys_routes WHERE name = 'file-management'), 1), + +-- 文档列表(文件管理的子菜单) +('/documents', 'documents', '{"title": "文档列表", "icon": "ri-file-list-3-line", "order": 2}', + (SELECT id FROM sys_routes WHERE name = 'file-management'), 1); + +-- 插入评查规则库的子菜单项 +INSERT INTO sys_routes (path, name, meta, parent_id, is_menu) VALUES +-- 评查点分组(评查规则库的子菜单) +('/rule-groups', 'rule-groups', '{"title": "评查点分组", "icon": "ri-folder-open-line", "order": 1}', + (SELECT id FROM sys_routes WHERE name = 'rule-management'), 1), + +-- 评查点列表(评查规则库的子菜单) +('/rules', 'rules-list', '{"title": "评查点列表", "icon": "ri-list-check-3", "order": 2}', + (SELECT id FROM sys_routes WHERE name = 'rule-management'), 1), + +-- 评查文件列表(评查规则库的子菜单) +('/rules-files', 'rules-file', '{"title": "评查文件列表", "icon": "ri-list-check-2", "order": 3}', + (SELECT id FROM sys_routes WHERE name = 'rule-management'), 1), + +-- 插入合同模板的子菜单项 +INSERT INTO sys_routes (path, name, meta, parent_id, is_menu) VALUES +-- 智能搜索(合同模板的子菜单) +('/contract-template/search', 'contract-search-ai', '{"title": "智能搜索", "icon": "ri-search-line", "order": 1}', + (SELECT id FROM sys_routes WHERE name = 'contract-template'), 1), + +-- 合同列表(合同模板的子菜单) +('/contract-template/list', 'contract-list', '{"title": "合同列表", "icon": "ri-folder-line", "order": 2}', + (SELECT id FROM sys_routes WHERE name = 'contract-template'), 1); + +-- 插入系统设置的子菜单项(需要developer权限) +INSERT INTO sys_routes (path, name, meta, parent_id, is_menu) VALUES +-- 配置列表(系统设置的子菜单,需要developer权限) +('/config-lists', 'config-lists', '{"title": "配置列表", "icon": "ri-list-check-3", "order": 1, "requiredRole": "developer"}', + (SELECT id FROM sys_routes WHERE name = 'system-settings'), 1), + +-- 文档类型(系统设置的子菜单,需要developer权限) +('/document-types', 'document-types', '{"title": "文档类型", "icon": "ri-file-list-line", "order": 2, "requiredRole": "developer"}', + (SELECT id FROM sys_routes WHERE name = 'system-settings'), 1), + +-- 提示词管理(系统设置的子菜单,需要developer权限) +('/prompts', 'prompt-management', '{"title": "提示词管理", "icon": "ri-chat-1-line", "order": 3, "requiredRole": "developer"}', + (SELECT id FROM sys_routes WHERE name = 'system-settings'), 1); + +-- 查询插入结果 +SELECT + r1.id, + r1.path, + r1.name, + r1.meta, + r1.parent_id, + r2.name as parent_name, + r1.is_menu +FROM sys_routes r1 +LEFT JOIN sys_routes r2 ON r1.parent_id = r2.id +ORDER BY + CASE WHEN r1.parent_id = 0 THEN r1.id ELSE r1.parent_id END, + r1.parent_id, + (r1.meta->>'order')::int; \ No newline at end of file diff --git a/sql/test_user_setup.sql b/sql/test_user_setup.sql new file mode 100644 index 0000000..2da1b7f --- /dev/null +++ b/sql/test_user_setup.sql @@ -0,0 +1,46 @@ +-- 为测试用户添加默认角色的脚本 +-- 测试用户信息:sub='001', username='testuser1' + +-- 首先查找测试用户 +DO $$ +DECLARE + test_user_id uuid; + existing_role_count integer; +BEGIN + -- 查找测试用户的ID + SELECT id INTO test_user_id + FROM sso_users + WHERE sub = '001' AND username = 'testuser1'; + + IF test_user_id IS NOT NULL THEN + -- 检查用户是否已经有common角色(role_id = 2) + SELECT COUNT(*) INTO existing_role_count + FROM user_role + WHERE user_id = test_user_id AND role_id = 2; + + IF existing_role_count = 0 THEN + -- 为用户添加common角色 + INSERT INTO user_role (user_id, role_id) + VALUES (test_user_id, 2); + + RAISE NOTICE '已为测试用户(%)添加默认角色(common)', test_user_id; + ELSE + RAISE NOTICE '测试用户(%)已经拥有默认角色(common)', test_user_id; + END IF; + ELSE + RAISE NOTICE '测试用户不存在,请先创建用户'; + END IF; +END $$; + +-- 验证结果 +SELECT + u.id, + u.sub, + u.username, + u.nick_name, + r.role_key, + r.role_name +FROM sso_users u +LEFT JOIN user_role ur ON u.id = ur.user_id +LEFT JOIN roles r ON ur.role_id = r.id +WHERE u.sub = '001' AND u.username = 'testuser1'; \ No newline at end of file diff --git a/sql/update_get_documents_with_filters.sql b/sql/update_get_documents_with_filters.sql new file mode 100644 index 0000000..9301640 --- /dev/null +++ b/sql/update_get_documents_with_filters.sql @@ -0,0 +1,86 @@ +CREATE OR REPLACE FUNCTION "public"."get_documents_with_filters"("search_name" text=NULL::text, "search_document_number" text=NULL::text, "search_document_types" _int4=NULL::integer[], "search_audit_status" int4=NULL::integer, "search_file_status" text=NULL::text, "search_date_from" date=NULL::date, "search_date_to" date=NULL::date, "search_user_id" int4=NULL::integer, "page" int4=1, "page_size" int4=10) + RETURNS TABLE("id" int4, "name" varchar, "document_number" varchar, "type_id" int4, "audit_status" int4, "status" varchar, "upload_time" timestamp, "updated_at" timestamptz, "file_size" int4, "path" varchar, "is_test_document" bool, "ocr_result" jsonb, "type_name" varchar, "false_count" int8) AS $BODY$ +DECLARE + offset_val integer; +BEGIN + offset_val := (page - 1) * page_size; + + -- 如果search_user_id为NULL,直接返回空结果 + IF search_user_id IS NULL THEN + RETURN; + END IF; + + RETURN QUERY + WITH paginated_documents AS ( + SELECT d.id + FROM documents d + WHERE (search_name IS NULL OR d.name ILIKE '%' || search_name || '%') + AND (search_document_number IS NULL OR d.document_number ILIKE '%' || search_document_number || '%') + AND (search_document_types IS NULL OR d.type_id = ANY(search_document_types)) + AND (search_audit_status IS NULL OR + (CASE WHEN search_audit_status = 0 THEN d.audit_status IS NULL OR d.audit_status = 0 + ELSE d.audit_status = search_audit_status END)) + AND (search_file_status IS NULL OR d.status = search_file_status) + AND (search_date_from IS NULL OR d.updated_at >= search_date_from) + AND (search_date_to IS NULL OR d.updated_at < (search_date_to + INTERVAL '1 day')) + AND d.user_id = search_user_id -- 强制要求匹配用户ID + ORDER BY d.updated_at DESC + LIMIT page_size OFFSET offset_val + ) + SELECT + d.id, + d.name, + d.document_number, + d.type_id, + d.audit_status, + d.status, + d.upload_time, + d.updated_at, + d.file_size, + d.path, + d.is_test_document, + d.ocr_result, + dt.name AS type_name, + COUNT(er.id) FILTER (WHERE (er.evaluated_results ->> 'result')::text = 'false') AS false_count + FROM documents d + JOIN paginated_documents pd ON d.id = pd.id + LEFT JOIN document_types dt ON d.type_id = dt.id + LEFT JOIN evaluation_results er ON d.id = er.document_id + GROUP BY d.id, dt.name + ORDER BY d.updated_at DESC; +END; +$BODY$ + LANGUAGE plpgsql STABLE + COST 100 + ROWS 1000 + +-- 同时也需要更新 count_documents_with_filters 函数 +CREATE OR REPLACE FUNCTION "public"."count_documents_with_filters"("search_name" text=NULL::text, "search_document_number" text=NULL::text, "search_document_types" _int4=NULL::integer[], "search_audit_status" int4=NULL::integer, "search_file_status" text=NULL::text, "search_date_from" date=NULL::date, "search_date_to" date=NULL::date, "search_user_id" int4=NULL::integer) + RETURNS int4 AS $BODY$ +DECLARE + total_count integer; +BEGIN + -- 如果search_user_id为NULL,直接返回0 + IF search_user_id IS NULL THEN + RETURN 0; + END IF; + + SELECT COUNT(*) + INTO total_count + FROM documents d + WHERE (search_name IS NULL OR d.name ILIKE '%' || search_name || '%') + AND (search_document_number IS NULL OR d.document_number ILIKE '%' || search_document_number || '%') + AND (search_document_types IS NULL OR d.type_id = ANY(search_document_types)) + AND (search_audit_status IS NULL OR + (CASE WHEN search_audit_status = 0 THEN d.audit_status IS NULL OR d.audit_status = 0 + ELSE d.audit_status = search_audit_status END)) + AND (search_file_status IS NULL OR d.status = search_file_status) + AND (search_date_from IS NULL OR d.updated_at >= search_date_from) + AND (search_date_to IS NULL OR d.updated_at < (search_date_to + INTERVAL '1 day')) + AND d.user_id = search_user_id; -- 强制要求匹配用户ID + + RETURN total_count; +END; +$BODY$ + LANGUAGE plpgsql STABLE + COST 100 \ No newline at end of file diff --git a/sql/user_role.sql b/sql/user_role.sql new file mode 100644 index 0000000..b2c74f8 --- /dev/null +++ b/sql/user_role.sql @@ -0,0 +1,47 @@ +/* + Navicat Premium Data Transfer + + Source Server : 智慧法务 + Source Server Type : PostgreSQL + Source Server Version : 170005 + Source Host : nas.7bm.co:54302 + Source Catalog : docauditai + Source Schema : public + + Target Server Type : PostgreSQL + Target Server Version : 170005 + File Encoding : 65001 + + Date: 18/07/2025 20:13:33 +*/ + + +-- ---------------------------- +-- Table structure for user_role +-- ---------------------------- +DROP TABLE IF EXISTS "public"."user_role"; +CREATE TABLE "public"."user_role" ( + "id" int4 NOT NULL DEFAULT nextval('user_role_id_seq'::regclass), + "user_id" int4 NOT NULL, + "role_id" int4 NOT NULL, + "created_at" timestamptz(6) DEFAULT now(), + "updated_at" timestamptz(6) DEFAULT now() +) +; + +-- ---------------------------- +-- Triggers structure for table user_role +-- ---------------------------- +CREATE TRIGGER "update_user_role_updated_at" BEFORE UPDATE ON "public"."user_role" +FOR EACH ROW +EXECUTE PROCEDURE "public"."update_updated_at"(); + +-- ---------------------------- +-- Uniques structure for table user_role +-- ---------------------------- +ALTER TABLE "public"."user_role" ADD CONSTRAINT "user_role_user_id_role_id_key" UNIQUE ("user_id", "role_id"); + +-- ---------------------------- +-- Primary Key structure for table user_role +-- ---------------------------- +ALTER TABLE "public"."user_role" ADD CONSTRAINT "sys_user_role_pkey" PRIMARY KEY ("id"); From e80b6b7da30fe991eda5a29324a87ae7a4727626 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Sun, 20 Jul 2025 22:03:02 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/documents._index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/routes/documents._index.tsx b/app/routes/documents._index.tsx index 8de6281..e2e2109 100644 --- a/app/routes/documents._index.tsx +++ b/app/routes/documents._index.tsx @@ -38,7 +38,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); const { userInfo } = await getUserSession(request); - console.log(userInfo); // 获取URL查询参数,只保留必要的分页参数 const url = new URL(request.url); @@ -248,7 +247,7 @@ export default function DocumentsIndex() { setIsLoadingData(false); loadingBarService.hide(); } - }, [search, documentNumber, documentType, auditStatus, fileStatus, dateFrom, dateTo, currentPage, pageSize]); + }, [search, documentNumber, documentType, auditStatus, fileStatus, dateFrom, dateTo, currentPage, pageSize, loaderData.userInfo]); // 在组件挂载时从 sessionStorage 获取 reviewType 并加载数据 useEffect(() => { From e7ffbe875ec7d6690da711cdde93e6da133bce28 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Mon, 21 Jul 2025 09:41:20 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E8=AF=84=E6=9F=A5=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=9A=84=E6=9F=A5=E8=AF=A2=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E3=80=81=E6=96=87=E6=A1=A3=E5=88=97=E8=A1=A8=E7=9A=84=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E6=9F=A5=E7=9C=8B=E4=BF=AE=E6=94=B9=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E9=83=BD=E6=B7=BB=E5=8A=A0=E4=BA=86user=5Fid=E5=8E=BB=E9=99=90?= =?UTF-8?q?=E5=88=B6=E7=94=A8=E6=88=B7=E6=93=8D=E4=BD=9C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/evaluation_points/rules-files.ts | 79 +++++-------------- app/api/files/documents.ts | 32 ++++++-- .../cross-checking/DocumentListModal.tsx | 13 +-- app/routes/documents._index.tsx | 22 +++++- app/routes/documents.edit.tsx | 24 +++++- app/routes/files.upload.tsx | 25 +++++- app/routes/rules-files.tsx | 26 +++++- sql/update_get_review_files_with_details.sql | 44 ++++++++++- 8 files changed, 177 insertions(+), 88 deletions(-) diff --git a/app/api/evaluation_points/rules-files.ts b/app/api/evaluation_points/rules-files.ts index ee1f865..ec7f06c 100644 --- a/app/api/evaluation_points/rules-files.ts +++ b/app/api/evaluation_points/rules-files.ts @@ -1,9 +1,4 @@ import { postgrestPut, postgrestPost } from '../postgrest-client'; -// import dayjs from 'dayjs'; -// import { getDocumentTypes } from '../document-types/document-types'; -// import type { DocumentTypeUI } from '../document-types/document-types'; -// import weekday from 'dayjs/plugin/weekday'; -// import updateLocale from 'dayjs/plugin/updateLocale'; import { formatDate } from '../../utils'; // 文档数据库表接口 @@ -107,58 +102,6 @@ export interface DocumentSearchParams { pageSize?: number; // 每页条数 } -// 添加评查结果和评查点类型定义 -// 评查结果类型 -// interface EvaluationResult { -// id: string | number; -// document_id: string | number; -// evaluation_point_id: string | number; -// evaluated_results?: { -// result?: boolean; -// message?: string; -// data?: string; -// [key: string]: unknown; -// }; -// [key: string]: unknown; -// } - -// 评查点类型 -// interface EvaluationPoint { -// id: string | number; -// post_action?: string; -// score?: number; -// [key: string]: unknown; -// } - -// 文档评查状态结果 -// interface DocumentReviewResult { -// status: number; -// issueCount: number; -// passCount: number; -// warningCount: number; -// failCount: number; -// manualCount: number; -// } - -// /** -// * 从不同格式的 API 响应中提取数据 -// * @param responseData API 响应数据 -// * @returns 提取后的数据或 null -// */ -// function extractApiData(responseData: unknown): T | null { -// if (!responseData) return null; - -// // 格式1: { code: number, msg: string, data: T } -// if (typeof responseData === 'object' && responseData !== null && -// 'code' in responseData && -// 'data' in responseData && -// (responseData as { data: unknown }).data) { -// return (responseData as { data: T }).data; -// } - -// // 格式2: 直接是数据对象 -// return responseData as T; -// } /** * 将评查状态代码映射到UI状态 @@ -202,14 +145,21 @@ export function getFileExtension(fileName: string): string { /** * 获取评查文件列表 * @param searchParams 搜索参数 + * @param documentIds 文档ID数组(可选) + * @param userId 用户ID * @returns 评查文件列表和总数 */ -export async function getReviewFiles(searchParams: DocumentSearchParams = {}, documentIds: number[] | null = null): Promise<{ +export async function getReviewFiles(searchParams: DocumentSearchParams = {}, documentIds: number[] | null = null, userId?: string): Promise<{ data?: { files: ReviewFileUI[], total: number }; error?: string; status?: number; }> { try { + // 确保userId必须存在,如果不存在则抛出错误 + if (!userId) { + return { error: '用户身份验证失败,无法获取评查文件列表', status: 401 }; + } + const { page = 1, pageSize = 10, @@ -242,6 +192,7 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}, do p_date_from: dateFrom || null, p_date_to: dateTo || null, p_document_ids: documentIds || null, + p_user_id: parseInt(userId, 10), // 强制要求传递用户ID }; const listParams = { @@ -364,9 +315,10 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}, do * 更新文件的审核状态 * @param id 文件ID * @param auditStatus 审核状态 + * @param userId 用户ID * @returns 更新结果 */ -export async function updateDocumentAuditStatus(id: string, auditStatus: number): Promise<{ +export async function updateDocumentAuditStatus(id: string, auditStatus: number, userId: string): Promise<{ success?: boolean; error?: string; status?: number; @@ -376,10 +328,17 @@ export async function updateDocumentAuditStatus(id: string, auditStatus: number) return { error: '文件ID不能为空', status: 400 }; } + if (!userId) { + return { error: '用户身份验证失败', status: 401 }; + } + const response = await postgrestPut>( 'documents', { audit_status: auditStatus }, - { id: parseInt(id) } + { + id: parseInt(id), + user_id: parseInt(userId) // 确保只能更新自己的文档 + } ); if (response.error) { diff --git a/app/api/files/documents.ts b/app/api/files/documents.ts index 7b7ee4f..6ac1455 100644 --- a/app/api/files/documents.ts +++ b/app/api/files/documents.ts @@ -304,9 +304,10 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro /** * 删除文档 * @param id 文档ID + * @param userId 用户ID * @returns 删除结果 */ -export async function deleteDocument(id: string): Promise<{ +export async function deleteDocument(id: string, userId: string): Promise<{ success?: boolean; error?: string; status?: number; @@ -316,11 +317,16 @@ export async function deleteDocument(id: string): Promise<{ return { error: '文档ID不能为空', status: 400 }; } + if (!userId) { + return { error: '用户身份验证失败', status: 401 }; + } + const response = await postgrestDelete( 'documents', { filter: { - 'id': `eq.${id}` + 'id': `eq.${id}`, + 'user_id': `eq.${userId}` // 确保只能删除自己的文档 } } ); @@ -344,7 +350,7 @@ export async function deleteDocument(id: string): Promise<{ * @param id 文档ID * @returns 文档详情 */ -export async function getDocument(id: string): Promise<{ +export async function getDocument(id: string, userId: string): Promise<{ data?: DocumentUI; error?: string; status?: number; @@ -354,11 +360,16 @@ export async function getDocument(id: string): Promise<{ return { error: '文档ID不能为空', status: 400 }; } + if (!userId) { + return { error: '用户身份验证失败', status: 401 }; + } + const response = await postgrestGet( 'documents', { filter: { - 'id': `eq.${id}` + 'id': `eq.${id}`, + 'user_id': `eq.${userId}` }, limit: 1 } @@ -427,7 +438,7 @@ export async function getFileDownloadUrl(filePath: string): Promise<{ * @param document 部分文档数据 * @returns 更新结果 */ -export async function updateDocument(id: string, document: Partial & { remark?: string }): Promise<{ +export async function updateDocument(id: string, document: Partial & { remark?: string }, userId: string): Promise<{ data?: DocumentUI; error?: string; status?: number; @@ -437,6 +448,10 @@ export async function updateDocument(id: string, document: Partial & return { error: '文档ID不能为空', status: 400 }; } + if (!userId) { + return { error: '用户身份验证失败', status: 401 }; + } + // 准备API数据 - 将UI数据转换为API格式 const apiDocument: Partial = {}; @@ -465,7 +480,10 @@ export async function updateDocument(id: string, document: Partial & const response = await postgrestPut>( 'documents', apiDocument, - { id: parseInt(id) } + { + id: parseInt(id), + user_id: parseInt(userId) // 确保只能更新自己的文档 + } ); if (response.error) { @@ -474,7 +492,7 @@ export async function updateDocument(id: string, document: Partial & } // 获取更新后的完整文档数据 - const updatedResponse = await getDocument(id); + const updatedResponse = await getDocument(id, userId); return updatedResponse; } catch (error) { diff --git a/app/components/cross-checking/DocumentListModal.tsx b/app/components/cross-checking/DocumentListModal.tsx index 3d5a9f3..6a16268 100644 --- a/app/components/cross-checking/DocumentListModal.tsx +++ b/app/components/cross-checking/DocumentListModal.tsx @@ -8,7 +8,7 @@ import { StatusBadge } from '../ui/StatusBadge'; import { Pagination } from '../ui/Pagination'; import { LoadingIndicator } from '../ui/SkeletonScreen'; import type { ReviewFileUI } from '~/api/evaluation_points/rules-files'; -import { updateDocumentAuditStatus } from '~/api/evaluation_points/rules-files'; +// import { updateDocumentAuditStatus } from '~/api/evaluation_points/rules-files'; import { toastService } from '../ui/Toast'; // 导出样式链接 @@ -49,10 +49,13 @@ export function DocumentListModal({ // 检查audit_status是否为0,如果是则更新为2 if (auditStatus === 0 || auditStatus === null) { try { - const response = await updateDocumentAuditStatus(fileId, 2); - if (response.error) { - throw new Error(response.error); - } + // TODO: 这里需要从父组件传递 userId,或者重新设计这个函数的调用方式 + // 暂时跳过状态更新,直接进入查看 + // const response = await updateDocumentAuditStatus(fileId, 2, userId); + // if (response.error) { + // throw new Error(response.error); + // } + console.warn('DocumentListModal: 跳过审核状态更新,需要传递 userId 参数'); } catch (error) { console.error('更新文件审核状态时出错:', error); toastService.error(`更新文件审核状态时出错:${error instanceof Error ? error.message : '未知错误'}`); diff --git a/app/routes/documents._index.tsx b/app/routes/documents._index.tsx index e2e2109..b303f0c 100644 --- a/app/routes/documents._index.tsx +++ b/app/routes/documents._index.tsx @@ -122,12 +122,21 @@ const formatFileSize = (bytes: number) => { // 处理表单提交和删除等操作 export const action = async ({ request }: ActionFunctionArgs) => { try { + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { userInfo } = await getUserSession(request); + + if (!userInfo?.user_id) { + return Response.json({ result: false, message: "用户身份验证失败" }, { status: 401 }); + } + + const userId = userInfo.user_id.toString(); const formData = await request.formData(); const action = formData.get("_action"); if (action === "delete") { const id = formData.get("id") as string; - const response = await deleteDocument(id); + const response = await deleteDocument(id, userId); if (response.error) { return Response.json({ result: false, message: response.error }, { status: response.status || 500 }); @@ -139,7 +148,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { const ids = formData.getAll("ids") as string[]; // 批量删除处理 - const results = await Promise.all(ids.map(id => deleteDocument(id))); + const results = await Promise.all(ids.map(id => deleteDocument(id, userId))); const failures = results.filter(r => r.error); if (failures.length > 0) { @@ -652,8 +661,15 @@ export default function DocumentsIndex() { // 检查audit_status是否为0,如果是则更新为2 if (auditStatus === 0 || auditStatus === null) { try { + // 从loader data中获取用户ID + const userId = loaderData.userInfo?.user_id?.toString(); + if (!userId) { + toastService.error('用户身份验证失败'); + return; + } + // console.log('开始审核',fileId,auditStatus) - const response = await updateDocumentAuditStatus(fileId.toString(), 2); + const response = await updateDocumentAuditStatus(fileId.toString(), 2, userId); if (response.error) { console.error('更新文件审核状态失败:', response.error); toastService.error('更新文件审核状态失败:' + (response.error || '未知错误')); diff --git a/app/routes/documents.edit.tsx b/app/routes/documents.edit.tsx index 366151d..3353d9d 100644 --- a/app/routes/documents.edit.tsx +++ b/app/routes/documents.edit.tsx @@ -80,6 +80,16 @@ function formatFileSize(bytes: number): string { // Loader函数 export async function loader({ request }: LoaderFunctionArgs) { try { + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { userInfo } = await getUserSession(request); + + if (!userInfo?.user_id) { + throw new Response("用户身份验证失败", { status: 401 }); + } + + const userId = userInfo.user_id.toString(); + // 从URL查询参数获取文档ID const url = new URL(request.url); const id = url.searchParams.get("id"); @@ -90,7 +100,7 @@ export async function loader({ request }: LoaderFunctionArgs) { // 并行获取文档详情和文档类型列表 const [documentResponse, documentTypesResponse] = await Promise.all([ - getDocument(id), + getDocument(id, userId), getDocumentTypes({ pageSize: 500 }) ]); @@ -114,6 +124,16 @@ export async function loader({ request }: LoaderFunctionArgs) { // Action函数处理表单提交 export async function action({ request }: ActionFunctionArgs) { + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { userInfo } = await getUserSession(request); + + if (!userInfo?.user_id) { + return Response.json({ error: "用户身份验证失败" }, { status: 401 }); + } + + const userId = userInfo.user_id.toString(); + // 从URL查询参数获取文档ID const url = new URL(request.url); const id = url.searchParams.get("id"); @@ -153,7 +173,7 @@ export async function action({ request }: ActionFunctionArgs) { auditStatus, isTest, remark - }); + }, userId); if (updateResponse.error) { console.error('更新文档失败:', updateResponse.error); diff --git a/app/routes/files.upload.tsx b/app/routes/files.upload.tsx index bec42f9..7ad3e5e 100644 --- a/app/routes/files.upload.tsx +++ b/app/routes/files.upload.tsx @@ -214,11 +214,21 @@ type LoaderData = { documents: Document[]; documentTypes: DocumentType[]; mode: string; + userInfo?: { + user_id?: number; + username?: string; + nick_name?: string; + [key: string]: unknown; + } | null; }; // 添加 loader 函数 export async function loader({ request }: LoaderFunctionArgs) { try { + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { userInfo } = await getUserSession(request); + // console.log('loader: 开始加载数据...'); const url = new URL(request.url); const mode = url.searchParams.get("mode") || "create"; @@ -240,13 +250,15 @@ export async function loader({ request }: LoaderFunctionArgs) { return Response.json({ mode, documents: documentsResponse.data || [], - documentTypes: typesResponse.data || [] + documentTypes: typesResponse.data || [], + userInfo // 传递用户信息到客户端 }); } catch (error) { console.error('loader: 加载数据失败:', error); return Response.json({ documents: [], - documentTypes: [] + documentTypes: [], + userInfo: null }); } } @@ -1439,8 +1451,15 @@ export default function FilesUpload() { // 检查audit_status是否为0,如果是则更新为2 if (record.audit_status === 0 || record.audit_status === null) { try { + // 从loader data中获取用户ID + const userId = loaderData.userInfo?.user_id?.toString(); + if (!userId) { + toastService.error('用户身份验证失败'); + return; + } + // console.log('【调试-handleViewFile】更新文件审核状态,文件ID:', record.id); - const response = await updateDocumentAuditStatus(record.id.toString(), 2); + const response = await updateDocumentAuditStatus(record.id.toString(), 2, userId); if (response.error) { console.error('【调试-handleViewFile】更新文件审核状态失败:', response.error); toastService.error('更新文件审核状态失败:' + (response.error || '未知错误')); diff --git a/app/routes/rules-files.tsx b/app/routes/rules-files.tsx index ed02863..a8f525f 100644 --- a/app/routes/rules-files.tsx +++ b/app/routes/rules-files.tsx @@ -58,6 +58,10 @@ export const REVIEW_STATUS_LABELS: Record = { // 加载评查文件列表 export async function loader({ request }: LoaderFunctionArgs) { + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { userInfo } = await getUserSession(request); + // 获取分页参数 const url = new URL(request.url); const currentPage = parseInt(url.searchParams.get("page") || "1", 10); @@ -75,6 +79,7 @@ export async function loader({ request }: LoaderFunctionArgs) { totalCount: 0, currentPage, pageSize, + userInfo, // 传递用户信息到客户端 initialLoad: true }); } catch (error) { @@ -85,7 +90,7 @@ export async function loader({ request }: LoaderFunctionArgs) { export default function RulesFiles() { const navigate = useNavigate(); - const { files: initialFiles, documentTypes: allDocumentTypes, totalCount: initialTotal, currentPage, pageSize, result, message } = useLoaderData(); + const { files: initialFiles, documentTypes: allDocumentTypes, totalCount: initialTotal, currentPage, pageSize, userInfo, result, message } = useLoaderData(); const [searchParams, setSearchParams] = useSearchParams(); const dateFrom = searchParams.get('dateFrom') || ''; const dateTo = searchParams.get('dateTo') || ''; @@ -134,8 +139,11 @@ export default function RulesFiles() { searchParams.fileType = params.fileType; } + // 从loader data中获取用户ID + const userId = userInfo?.user_id?.toString(); + // 获取文件列表 - const filesResponse = await getReviewFiles(searchParams); + const filesResponse = await getReviewFiles(searchParams, null, userId); if (filesResponse.error) { throw new Error(filesResponse.error); } @@ -198,8 +206,11 @@ export default function RulesFiles() { // 设置加载状态 setIsLoading(true); + // 从loader data中获取用户ID + const userId = userInfo?.user_id?.toString(); + // 获取文件列表 - getReviewFiles(apiSearchParams) + getReviewFiles(apiSearchParams, null, userId) .then(filesResponse => { if (filesResponse.error) { throw new Error(filesResponse.error); @@ -281,7 +292,14 @@ export default function RulesFiles() { // 检查audit_status是否为0,如果是则更新为2 if (auditStatus === 0 || auditStatus === null) { try { - const response = await updateDocumentAuditStatus(fileId, 2); + // 从loader data中获取用户ID + const userId = userInfo?.user_id?.toString(); + if (!userId) { + toastService.error('用户身份验证失败'); + return; + } + + const response = await updateDocumentAuditStatus(fileId, 2, userId); if (response.error) { throw new Error(response.error); } diff --git a/sql/update_get_review_files_with_details.sql b/sql/update_get_review_files_with_details.sql index 9760037..bb5e8d5 100644 --- a/sql/update_get_review_files_with_details.sql +++ b/sql/update_get_review_files_with_details.sql @@ -1,4 +1,4 @@ -CREATE OR REPLACE FUNCTION "public"."get_review_files_with_details"("p_keyword" text=NULL::text, "p_typeid" _int4=NULL::integer[], "p_evaluations_status" int4=NULL::integer, "p_date_from" date=NULL::date, "p_date_to" date=NULL::date, "p_sort_order" text='created_at_desc'::text, "p_page" int4=1, "p_page_size" int4=10) +CREATE OR REPLACE FUNCTION "public"."get_review_files_with_details"("p_keyword" text=NULL::text, "p_typeid" _int4=NULL::integer[], "p_evaluations_status" int4=NULL::integer, "p_date_from" date=NULL::date, "p_date_to" date=NULL::date, "p_sort_order" text='created_at_desc'::text, "p_page" int4=1, "p_page_size" int4=10, "p_document_ids" _int4=NULL::integer[], "p_user_id" int4=NULL::integer) RETURNS TABLE("id" int4, "status" varchar, "path" varchar, "file_name" varchar, "file_code" varchar, "file_type_name" varchar, "file_type_id" int4, "file_size" int4, "upload_time" timestamptz, "created_at" timestamptz, "evaluations_status" int4, "audit_status" int4, "created_by_user_id" int4, "issue_count" int8, "total_score" numeric, "pass_count" int8, "warning_count" int8, "fail_count" int8, "manual_count" int8, "issues" jsonb) AS $BODY$ DECLARE offset_val integer; @@ -6,6 +6,11 @@ DECLARE sort_direction text; BEGIN offset_val := (p_page - 1) * p_page_size; + + -- 如果p_user_id为NULL,直接返回0 + IF p_user_id IS NULL THEN + RETURN; + END IF; SELECT CASE @@ -70,13 +75,44 @@ BEGIN ($2 IS NULL OR d.type_id = ANY($2)) AND ($3 IS NULL OR d.evaluations_status = $3) AND ($4 IS NULL OR d.created_at >= $4) AND - ($5 IS NULL OR d.created_at < ($5 + INTERVAL ''1 day'')) + ($5 IS NULL OR d.created_at < ($5 + INTERVAL ''1 day'')) AND + ($8 IS NULL OR d.id = ANY($8)) AND + ($9 d.user_id = $9) ORDER BY %I %s LIMIT $6 OFFSET $7 ', sort_column, sort_direction) - USING p_keyword, p_typeid, p_evaluations_status, p_date_from, p_date_to, p_page_size, offset_val; + USING p_keyword, p_typeid, p_evaluations_status, p_date_from, p_date_to, p_page_size, offset_val, p_document_ids, p_user_id; END; $BODY$ LANGUAGE plpgsql VOLATILE COST 100 - ROWS 1000; \ No newline at end of file + ROWS 1000; + +-- 同时创建或更新 count_review_files 函数 +CREATE OR REPLACE FUNCTION "public"."count_review_files"("p_keyword" text=NULL::text, "p_typeid" _int4=NULL::integer[], "p_evaluations_status" int4=NULL::integer, "p_date_from" date=NULL::date, "p_date_to" date=NULL::date, "p_document_ids" _int4=NULL::integer[], "p_user_id" int4=NULL::integer) + RETURNS int4 AS $BODY$ +DECLARE + total_count integer; +BEGIN + -- 如果p_user_id为NULL,直接返回0 + IF p_user_id IS NULL THEN + RETURN 0; + END IF; + + SELECT COUNT(*) + INTO total_count + FROM documents d + WHERE + (p_keyword IS NULL OR (d.name ILIKE '%' || p_keyword || '%' OR d.document_number ILIKE '%' || p_keyword || '%')) AND + (p_typeid IS NULL OR d.type_id = ANY(p_typeid)) AND + (p_evaluations_status IS NULL OR d.evaluations_status = p_evaluations_status) AND + (p_date_from IS NULL OR d.created_at >= p_date_from) AND + (p_date_to IS NULL OR d.created_at < (p_date_to + INTERVAL '1 day')) AND + (p_document_ids IS NULL OR d.id = ANY(p_document_ids)) AND + d.user_id = p_user_id; + + RETURN total_count; +END; +$BODY$ + LANGUAGE plpgsql STABLE + COST 100; \ No newline at end of file From 8a09c37dd76c9df89d71c411cbb10129cae4f921 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Mon, 21 Jul 2025 12:03:38 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=9B=B8=E5=85=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/files/documents.ts | 2 +- app/routes/documents._index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/files/documents.ts b/app/api/files/documents.ts index 6ac1455..9c9ec5f 100644 --- a/app/api/files/documents.ts +++ b/app/api/files/documents.ts @@ -494,7 +494,7 @@ export async function updateDocument(id: string, document: Partial & // 获取更新后的完整文档数据 const updatedResponse = await getDocument(id, userId); - return updatedResponse; + return updatedResponse; } catch (error) { console.error('更新文档信息失败:', error); return { diff --git a/app/routes/documents._index.tsx b/app/routes/documents._index.tsx index b303f0c..1e0abc3 100644 --- a/app/routes/documents._index.tsx +++ b/app/routes/documents._index.tsx @@ -33,7 +33,7 @@ export const meta: MetaFunction = () => { ]; }; -// 数据加载器 +// 数据加载器 export const loader = async ({ request }: LoaderFunctionArgs) => { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server");