Merge remote-tracking branch 'origin/shiy-login' into Wren
This commit is contained in:
@@ -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<string, MenuItem[]> = {
|
||||
'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<Array<{id: number}>>("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<Array<{route_id: number}>>("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<RouteInfo[]>("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<number, MenuItem>();
|
||||
|
||||
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<string, string> = {
|
||||
'common': 'common',
|
||||
'developer': 'admin',
|
||||
'deptLeader': 'deptLeader',
|
||||
'groupLeader': 'groupLeader'
|
||||
};
|
||||
|
||||
return roleMapping[userRole] || 'common';
|
||||
}
|
||||
@@ -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<T>(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<Document, Partial<Document>>(
|
||||
'documents',
|
||||
{ audit_status: auditStatus },
|
||||
{ id: parseInt(id) }
|
||||
{
|
||||
id: parseInt(id),
|
||||
user_id: parseInt(userId) // 确保只能更新自己的文档
|
||||
}
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
// 并行执行获取数据和获取总数的请求
|
||||
@@ -296,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;
|
||||
@@ -308,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}` // 确保只能删除自己的文档
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -336,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;
|
||||
@@ -346,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<Document[]>(
|
||||
'documents',
|
||||
{
|
||||
filter: {
|
||||
'id': `eq.${id}`
|
||||
'id': `eq.${id}`,
|
||||
'user_id': `eq.${userId}`
|
||||
},
|
||||
limit: 1
|
||||
}
|
||||
@@ -419,7 +438,7 @@ export async function getFileDownloadUrl(filePath: string): Promise<{
|
||||
* @param document 部分文档数据
|
||||
* @returns 更新结果
|
||||
*/
|
||||
export async function updateDocument(id: string, document: Partial<DocumentUI> & { remark?: string }): Promise<{
|
||||
export async function updateDocument(id: string, document: Partial<DocumentUI> & { remark?: string }, userId: string): Promise<{
|
||||
data?: DocumentUI;
|
||||
error?: string;
|
||||
status?: number;
|
||||
@@ -429,6 +448,10 @@ export async function updateDocument(id: string, document: Partial<DocumentUI> &
|
||||
return { error: '文档ID不能为空', status: 400 };
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return { error: '用户身份验证失败', status: 401 };
|
||||
}
|
||||
|
||||
// 准备API数据 - 将UI数据转换为API格式
|
||||
const apiDocument: Partial<Document> = {};
|
||||
|
||||
@@ -457,7 +480,10 @@ export async function updateDocument(id: string, document: Partial<DocumentUI> &
|
||||
const response = await postgrestPut<Document, Partial<Document>>(
|
||||
'documents',
|
||||
apiDocument,
|
||||
{ id: parseInt(id) }
|
||||
{
|
||||
id: parseInt(id),
|
||||
user_id: parseInt(userId) // 确保只能更新自己的文档
|
||||
}
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
@@ -466,9 +492,9 @@ export async function updateDocument(id: string, document: Partial<DocumentUI> &
|
||||
}
|
||||
|
||||
// 获取更新后的完整文档数据
|
||||
const updatedResponse = await getDocument(id);
|
||||
const updatedResponse = await getDocument(id, userId);
|
||||
|
||||
return updatedResponse;
|
||||
return updatedResponse;
|
||||
} catch (error) {
|
||||
console.error('更新文档信息失败:', error);
|
||||
return {
|
||||
|
||||
@@ -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<SsoUser[]>("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<SsoUser> = {
|
||||
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<SsoUser[], Partial<SsoUser>>(
|
||||
"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<SsoUser[], SsoUser>("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<Array<{id: number, user_id: string, role_id: number}>>("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<Array<{id: number, user_id: string, role_id: number}>, {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<SsoUser[]>("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<SsoUser>
|
||||
) {
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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 : '未知错误'}`);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2261,15 +2264,13 @@ export function ReviewPointsList({
|
||||
|
||||
{/* 悬浮状态:横向排列,显示图标,数字放大 */}
|
||||
<div className="absolute top-0 right-0 opacity-0 scale-0 group-hover:opacity-100 group-hover:scale-100 flex items-center bg-blue-50 px-3 py-2 rounded-lg border border-blue-200 shadow-lg transition-all duration-300 origin-top-right">
|
||||
<button className="flex items-center" aria-label="点击查看详情">
|
||||
<div className="flex flex-col">
|
||||
<i className="ri-chat-1-line text-blue-600 text-base"></i>
|
||||
<span className="text-xl text-blue-600 font-bold">{scoringProposals.length}</span>
|
||||
<span className="text-xs text-blue-500 leading-tight whitespace-wrap">条</span>
|
||||
<span className="text-xs text-blue-500 leading-tight whitespace-wrap">意</span>
|
||||
<span className="text-xs text-blue-500 leading-tight whitespace-wrap">见</span>
|
||||
</div>
|
||||
</button>
|
||||
<div className="flex flex-col">
|
||||
<i className="ri-chat-1-line text-blue-600 text-base"></i>
|
||||
<span className="text-xl text-blue-600 font-bold">{scoringProposals.length}</span>
|
||||
<span className="text-xs text-blue-500 leading-tight whitespace-wrap">条</span>
|
||||
<span className="text-xs text-blue-500 leading-tight whitespace-wrap">意</span>
|
||||
<span className="text-xs text-blue-500 leading-tight whitespace-wrap">见</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -2459,7 +2460,7 @@ export function ReviewPointsList({
|
||||
</Modal>
|
||||
|
||||
|
||||
{/* 意见列表模态框 */}
|
||||
{/* 意见列表模态框 */}
|
||||
<Modal
|
||||
isOpen={isOpinionListModalOpen}
|
||||
onClose={handleCloseOpinionListModal}
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link, useLocation, useNavigate } from '@remix-run/react';
|
||||
import type { UserRole } from '~/root';
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
title: string;
|
||||
path: string;
|
||||
icon: string;
|
||||
hideBreadcrumb?: boolean;
|
||||
requiredRole?: UserRole;
|
||||
children?: MenuItem[];
|
||||
}
|
||||
import { getUserRoutesByRole, mapUserRoleToRoleKey, type MenuItem } from '~/api/auth/user-routes';
|
||||
|
||||
interface SidebarProps {
|
||||
onToggle: () => 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<Record<string, boolean>>({});
|
||||
const [currentApp, setCurrentApp] = useState<string>(''); // 初始设置为空字符串而不是selectedApp
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true); // 添加加载状态
|
||||
const [menuItems, setMenuItems] = useState<MenuItem[]>([]); // 动态菜单项
|
||||
const [isLoadingRoutes, setIsLoadingRoutes] = useState<boolean>(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<string, boolean> = {};
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
<div className="py-4 px-[10px]">
|
||||
{isLoading ? (
|
||||
{isLoading || isLoadingRoutes ? (
|
||||
// 加载中状态显示,保留菜单布局结构
|
||||
<div className="py-2">
|
||||
{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) => (
|
||||
<Link
|
||||
key={child.id}
|
||||
to={child.path}
|
||||
className={`sidebar-menu-item ${isActive(child.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`}
|
||||
onClick={(e) => handleSubMenuClick(child, e)}
|
||||
>
|
||||
<i className={`${child.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
|
||||
{!collapsed && <span>{child.title}</span>}
|
||||
</Link>
|
||||
))}
|
||||
{item.children.map((child) => (
|
||||
<Link
|
||||
key={child.id}
|
||||
to={child.path}
|
||||
className={`sidebar-menu-item ${isActive(child.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`}
|
||||
onClick={(e) => handleSubMenuClick(child, e)}
|
||||
>
|
||||
<i className={`${child.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
|
||||
{!collapsed && <span>{child.title}</span>}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
+13
-3
@@ -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: {
|
||||
|
||||
@@ -33,8 +33,12 @@ export const meta: MetaFunction = () => {
|
||||
];
|
||||
};
|
||||
|
||||
// 数据加载器
|
||||
// 数据加载器
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { userInfo } = await getUserSession(request);
|
||||
|
||||
// 获取URL查询参数,只保留必要的分页参数
|
||||
const url = new URL(request.url);
|
||||
const page = parseInt(url.searchParams.get("page") || "1", 10);
|
||||
@@ -55,6 +59,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
page,
|
||||
pageSize,
|
||||
documentTypeOptions,
|
||||
userInfo, // 传递用户信息到客户端
|
||||
initialLoad: true // 标记这是初始加载
|
||||
});
|
||||
};
|
||||
@@ -117,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 });
|
||||
@@ -134,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) {
|
||||
@@ -195,6 +209,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 +222,7 @@ export default function DocumentsIndex() {
|
||||
dateFrom: dateFrom || undefined,
|
||||
dateTo: dateTo || undefined,
|
||||
reviewType: storedReviewType || undefined,
|
||||
userId: userId, // 添加用户ID筛选
|
||||
page: currentPage,
|
||||
pageSize
|
||||
};
|
||||
@@ -238,7 +256,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(() => {
|
||||
@@ -643,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 || '未知错误'));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 || '未知错误'));
|
||||
|
||||
+26
-10
@@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 临时管理员登录区域 */}
|
||||
{/* 测试用户登录区域 */}
|
||||
<div className="temp-login-section">
|
||||
<div className="section-divider">
|
||||
<span>或</span>
|
||||
</div>
|
||||
|
||||
<Form method="post" className="temp-login-form">
|
||||
<input type="hidden" name="intent" value="temp_admin_login" />
|
||||
<input type="hidden" name="intent" value="test_user_login" />
|
||||
<button
|
||||
type="submit"
|
||||
className="temp-admin-login-button"
|
||||
>
|
||||
<i className="ri-admin-line"></i>
|
||||
临时管理员登录
|
||||
<i className="ri-user-line"></i>
|
||||
测试用户登录
|
||||
</button>
|
||||
<div className="temp-login-tips">
|
||||
<p>
|
||||
<i className="ri-alert-line"></i>
|
||||
仅供开发测试使用,将以管理员身份登录
|
||||
<i className="ri-information-line"></i>
|
||||
使用测试用户(testuser1)登录,默认普通权限
|
||||
</p>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -58,6 +58,10 @@ export const REVIEW_STATUS_LABELS: Record<string, string> = {
|
||||
|
||||
// 加载评查文件列表
|
||||
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<typeof loader>();
|
||||
const { files: initialFiles, documentTypes: allDocumentTypes, totalCount: initialTotal, currentPage, pageSize, userInfo, result, message } = useLoaderData<typeof loader>();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
@@ -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
|
||||
@@ -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;
|
||||
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;
|
||||
@@ -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");
|
||||
Reference in New Issue
Block a user