feat: 1. 完善全局路由的访问权限的验证。 2. 完善接口返回的树形路由结构 3.优化评查点列表的查询,改用表连接的方式,废弃使用数据库的rpc函数,同时进行地区隔离和权限隔离。

4. 删除冗余的评查文件列表。      5.完善上传文档 页面初始化查询数据的时候 查询文件类型(改成动态指定)  6. 添加获取入口模块的查询接口。    7.完善服务端中判断token的有效性,失效则跳转到登录页。
8. 重构layout和sidebar的页面,改成由动态权限路由来渲染对应的菜单栏。       9.重构入口页面,通过动态查询根据不同地区的人返回不同的入口。
This commit is contained in:
2025-11-20 01:35:30 +08:00
parent adfb84a31d
commit 2edde8a8ab
23 changed files with 1201 additions and 2154 deletions
+71 -10
View File
@@ -493,7 +493,7 @@ const FALLBACK_MENU_DATA: Record<string, MenuItem[]> = {
*/
export async function getUserRoutesByRole(roleKey: string, jwt?: string, includeHidden: boolean = false): Promise<{ success: boolean; data?: MenuItem[]; error?: string; shouldRedirectToHome?: boolean }> {
try {
console.log(`🔍 [User Routes] 获取用户路由,角色: ${roleKey}`);
// console.log(`🔍 [User Routes] 获取用户路由,角色: ${roleKey}`);
if (!jwt) {
console.error('❌ [User Routes] JWT token 未提供');
@@ -554,13 +554,15 @@ export async function getUserRoutesByRole(roleKey: string, jwt?: string, include
return { success: false, error: "用户没有分配任何路由权限", shouldRedirectToHome: true };
}
// console.log('📋 [User Routes] 菜单数据:', routes);
// console.log('🔍 [User Routes] 后端返回的原始路由数据:', JSON.stringify(routes, null, 2));
// console.log('🔍 [User Routes] 检查第一个路由是否有children:', routes[0]?.children);
// 将后端路由格式转换为前端 MenuItem 格式
const menuItems = convertBackendRoutesToMenuItems(routes, includeHidden);
// console.log(`✅ [User Routes] 成功获取 ${menuItems.length} 个路由 (includeHidden: ${includeHidden})`);
// console.log('📋 [User Routes] 菜单数据:', menuItems);
// console.log(`✅ [User Routes] 转换后得到 ${menuItems.length} 个菜单项 (includeHidden: ${includeHidden})`);
// console.log('🔍 [User Routes] 转换后的菜单数据:', JSON.stringify(menuItems, null, 2));
// console.log('🔍 [User Routes] 检查第一个菜单项是否有children:', menuItems[0]?.children);
return { success: true, data: menuItems };
@@ -608,12 +610,40 @@ function convertIcon(elementIcon: string | null): string {
return ICON_MAPPING[elementIcon] || 'ri-file-line';
}
/**
* 递归提取所有路由(包括嵌套的子路由)为平铺数组
* @param routes 路由数组(可能包含嵌套的 children)
* @returns 平铺的路由数组
*/
function flattenRoutes(routes: BackendRouteInfo[]): BackendRouteInfo[] {
const flattened: BackendRouteInfo[] = [];
function traverse(routeList: BackendRouteInfo[]) {
for (const route of routeList) {
// 添加当前路由(不带 children,因为我们要重新构建)
const { children, ...routeWithoutChildren } = route;
flattened.push(routeWithoutChildren);
// 递归处理子路由
if (children && children.length > 0) {
traverse(children);
}
}
}
traverse(routes);
// console.log('🔄 [flattenRoutes] 平铺后的路由数量:', flattened.length);
return flattened;
}
/**
* 将平铺的路由数组构建为树形结构
* @param routes 平铺的路由数组
* @returns 树形结构的路由数组
*/
function buildRouteTree(routes: BackendRouteInfo[]): BackendRouteInfo[] {
// console.log('🌲 [buildRouteTree] 开始构建树,接收到的路由数量:', routes.length);
// 创建路由映射
const routeMap = new Map<number, BackendRouteInfo>();
const rootRoutes: BackendRouteInfo[] = [];
@@ -641,12 +671,13 @@ function buildRouteTree(routes: BackendRouteInfo[]): BackendRouteInfo[] {
parentRoute.children.push(currentRoute);
} else {
// 如果找不到父路由,当作顶级路由处理
// console.warn(`⚠️ [User Routes] 找不到父路由 (parent_id: ${route.parent_id}) for route: ${route.route_name}`);
// console.warn(`⚠️ [buildRouteTree] 找不到父路由 (parent_id: ${route.parent_id}) for route: ${route.route_name}`);
rootRoutes.push(currentRoute);
}
}
});
// console.log('🌲 [buildRouteTree] 构建完成,根路由数量:', rootRoutes.length);
return rootRoutes;
}
@@ -659,20 +690,43 @@ function buildRouteTree(routes: BackendRouteInfo[]): BackendRouteInfo[] {
* @returns MenuItem 数组
*/
function convertBackendRoutesToMenuItems(backendRoutes: BackendRouteInfo[], includeHidden: boolean = false): MenuItem[] {
// console.log('🔄 [convertBackendRoutesToMenuItems] 开始转换,接收到的路由数量:', backendRoutes.length);
// console.log('🔄 [convertBackendRoutesToMenuItems] includeHidden:', includeHidden);
// console.log('🔄 [convertBackendRoutesToMenuItems] 接收到的路由数据:', JSON.stringify(backendRoutes, null, 2));
// 检查是否需要构建树形结构
// 如果存在 parent_id 不为 null/0 但没有对应的父路由在 children 中,说明是平铺数组
// 如果存在 parent_id 不为 null/0 但没有对应的父路由在 children 中,说明需要重建树
const needsBuildTree = backendRoutes.some(route =>
route.parent_id !== null &&
route.parent_id !== 0 &&
!backendRoutes.some(r => r.children?.some(c => c.id === route.id))
);
// 如果是平铺数组,先构建树形结构
const treeRoutes = needsBuildTree ? buildRouteTree(backendRoutes) : backendRoutes;
// console.log('🔄 [convertBackendRoutesToMenuItems] needsBuildTree:', needsBuildTree);
return treeRoutes
.filter(route => includeHidden || !route.is_hidden) // 根据 includeHidden 决定是否过滤隐藏路由
let treeRoutes: BackendRouteInfo[];
if (needsBuildTree) {
// 🔑 关键修复:先平铺所有路由(包括嵌套的 children),再重新构建树
// console.log('🔄 [convertBackendRoutesToMenuItems] 检测到混合格式,先平铺再重建树...');
const flattenedRoutes = flattenRoutes(backendRoutes);
treeRoutes = buildRouteTree(flattenedRoutes);
} else {
// 后端已经返回正确的树形结构,直接使用
// console.log('🔄 [convertBackendRoutesToMenuItems] 后端返回的是完整树形结构,直接使用');
treeRoutes = backendRoutes;
}
// console.log('🔄 [convertBackendRoutesToMenuItems] 构建树后的路由数据:', JSON.stringify(treeRoutes, null, 2));
const result = treeRoutes
.filter(route => {
const shouldInclude = includeHidden || !route.is_hidden;
// console.log(`🔄 [convertBackendRoutesToMenuItems] 过滤路由 ${route.route_name}: is_hidden=${route.is_hidden}, includeHidden=${includeHidden}, 结果=${shouldInclude}`);
return shouldInclude;
})
.map(route => {
// console.log(`🔄 [convertBackendRoutesToMenuItems] 处理路由 ${route.route_name}, children数量: ${route.children?.length || 0}`);
const menuItem: MenuItem = {
id: route.route_name || `route-${route.id}`,
title: route.route_title,
@@ -684,12 +738,19 @@ function convertBackendRoutesToMenuItems(backendRoutes: BackendRouteInfo[], incl
// 递归处理子路由,传递 includeHidden 参数
if (route.children && route.children.length > 0) {
// console.log(`🔄 [convertBackendRoutesToMenuItems] ${route.route_name} 有 ${route.children.length} 个子路由,递归处理...`);
menuItem.children = convertBackendRoutesToMenuItems(route.children, includeHidden);
// console.log(`🔄 [convertBackendRoutesToMenuItems] ${route.route_name} 转换后的children:`, menuItem.children);
}
return menuItem;
})
.sort((a, b) => a.order - b.order); // 按 sort_order 排序
// console.log('🔄 [convertBackendRoutesToMenuItems] 转换完成,返回的菜单项数量:', result.length);
// console.log('🔄 [convertBackendRoutesToMenuItems] 返回的菜单数据:', JSON.stringify(result, null, 2));
return result;
}
+16 -2
View File
@@ -106,6 +106,13 @@ axiosInstance.interceptors.response.use(
if (isAxiosError(error) && error.response?.status === 401) {
// Token 过期或无效
console.warn('⚠️ Token 已过期或无效,请重新登录');
console.warn('⚠️ 401 错误详情:', {
url: error.config?.url,
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
headers: error.response?.headers
});
if (typeof window !== 'undefined') {
// 🌐 客户端环境:清除 localStorage 并跳转
@@ -328,13 +335,20 @@ export async function apiRequest<T>(
// 确保使用默认超时时间
timeout: options.timeout || DEFAULT_TIMEOUT
};
// 🔍 调试:打印 Authorization 头
if (headers['Authorization']) {
// console.log('🔑 [apiRequest] 请求包含 Authorization 头:', headers['Authorization'].substring(0, 20) + '...');
} else {
console.warn('⚠️ [apiRequest] 请求缺少 Authorization 头!headers:', Object.keys(headers));
}
// console.log(`📦 axios-client.ts->请求配置: \n${JSON.stringify(config)}`);
// 删除body属性,避免axios警告
if ('body' in config) {
delete (config as ExtendedAxiosRequestConfig).body;
}
// 使用带重试功能的请求方法
const response: AxiosResponse = await axiosRetry(config);
+1 -204
View File
@@ -1,5 +1,4 @@
import { postgrestPut, postgrestPost } from '../postgrest-client';
import { formatDate } from '../../utils';
import { postgrestPut } from '../postgrest-client';
// 文档数据库表接口
export interface Document {
@@ -62,32 +61,6 @@ export interface ReviewFileUI {
manualCount: number;
}
// 数据库函数返回的评查文件结构
interface ReviewFileFromSQL {
id: number;
status: string;
path: string;
file_name: string;
file_code: string;
file_type_name: string;
file_type_id: number;
file_size: number;
upload_time: string;
created_at: string;
evaluations_status: number;
audit_status: number | null;
created_by_user_id: number | null;
issue_count: number;
total_score: number;
pass_count: number;
warning_count: number;
fail_count: number;
manual_count: number;
issues: Array<{
severity: 'info' | 'warning' | 'error' | 'critical';
message: string;
}> | null;
}
// 文件列表搜索参数
export interface DocumentSearchParams {
@@ -133,183 +106,7 @@ export function mapUIToReviewStatus(status: string): number {
}
}
/**
* 获取文件扩展名
* @param fileName 文件名
* @returns 文件扩展名
*/
export function getFileExtension(fileName: string): string {
return fileName.split('.').pop()?.toLowerCase() || '';
}
/**
* 获取评查文件列表
* @param searchParams 搜索参数
* @param documentIds 文档ID数组(可选)
* @param userId 用户ID
* @returns 评查文件列表和总数
*/
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,
keyword,
fileType, // sessionStorage.getItem('reviewType')
reviewStatus,
dateFrom,
dateTo,
sortOrder = 'upload_time_desc'
} = searchParams;
let p_typeid: number[] | null = null;
if (fileType) {
if (fileType === 'record') {
p_typeid = [2, 3, 155];
} else if (fileType === 'contract') {
p_typeid = [1];
} else {
const typeId = parseInt(fileType, 10);
if (!isNaN(typeId)) {
p_typeid = [typeId];
}
}
}
const rpcParams = {
p_keyword: keyword || null,
p_typeid: p_typeid,
p_evaluations_status: reviewStatus ? mapUIToReviewStatus(reviewStatus) : null,
p_date_from: dateFrom || null,
p_date_to: dateTo || null,
p_document_ids: documentIds || null,
p_user_id: parseInt(userId, 10), // 强制要求传递用户ID
};
const listParams = {
...rpcParams,
p_page: page,
p_page_size: pageSize,
p_sort_order: sortOrder
};
// 并行执行获取数据和获取总数的请求
const [filesResponse, countResponse] = await Promise.all([
postgrestPost<ReviewFileFromSQL[]>('rpc/get_review_files_with_details', listParams),
postgrestPost<number>('rpc/count_review_files', rpcParams)
]);
// 处理获取文档列表的错误
if (filesResponse.error || !filesResponse.data) {
return { error: filesResponse.error || '获取文档数据失败', status: filesResponse.status || 500 };
}
// 处理获取总数的错误
if (countResponse.error || typeof countResponse.data !== 'number') {
console.error('获取文档总数失败:', countResponse.error);
}
const totalCount = typeof countResponse.data === 'number' ? countResponse.data : 0;
// 将SQL返回的数据转换为UI格式
const reviewFiles: ReviewFileUI[] = filesResponse.data.map((file: ReviewFileFromSQL) => ({
id: file.id.toString(),
status: file.status,
path: file.path,
fileName: file.file_name,
fileCode: file.file_code,
fileType: file.file_type_name,
fileTypeId: file.file_type_id,
fileSize: file.file_size,
uploadTime: formatDate(file.created_at),
reviewStatus: mapReviewStatusToUI(file.evaluations_status),
reviewStatusCode: file.evaluations_status,
issueCount: file.issue_count,
score: file.total_score,
auditStatus: file.audit_status,
issues: file.issues || [],
createdBy: file.created_by_user_id?.toString() || '系统',
passCount: file.pass_count,
warningCount: file.warning_count,
failCount: file.fail_count,
manualCount: file.manual_count,
}));
return {
data: {
files: reviewFiles,
total: totalCount
}
};
} catch (error) {
console.error('获取评查文件列表失败:', error);
return {
error: error instanceof Error ? error.message : '获取评查文件列表失败',
status: 500
};
}
}
/**
* 更新文件的评查状态
* @param id 文件ID
* @param status 评查状态
* @returns 更新后的文件信息
*/
// export async function updateReviewStatus(id: string, status: string): Promise<{
// data?: ReviewFileUI;
// error?: string;
// status?: number;
// }> {
// try {
// if (!id) {
// return { error: '文件ID不能为空', status: 400 };
// }
// const statusValue = mapUIToReviewStatus(status);
// const response = await postgrestPut<Document, Partial<Document>>(
// 'documents',
// { evaluations_status: statusValue },
// { id: parseInt(id) }
// );
// if (response.error) {
// return { error: response.error, status: response.status };
// }
// const extractedData = extractApiData<Document>(response.data);
// if (!extractedData) {
// return { error: '更新评查状态失败', status: 500 };
// }
// // 获取文档类型,用于查找文档类型名称
// const documentTypesResponse = await getDocumentTypes({pageSize: 500});
// const documentTypes = documentTypesResponse.data?.types || [];
// // 查找文档类型名称
// const docType = documentTypes.find((type: DocumentTypeUI) => type.id === extractedData.type_id);
// const typeName = docType ? docType.name : '未知类型';
// return { data: convertToReviewFileUI(extractedData, typeName) };
// } catch (error) {
// console.error('更新评查状态失败:', error);
// return {
// error: error instanceof Error ? error.message : '更新评查状态失败',
// status: 500
// }
// }
// }
/**
* 更新文件的审核状态
+212 -201
View File
@@ -32,9 +32,10 @@ export interface RulesQueryParams {
groupId?: string; // 规则组ID
isActive?: boolean;
keyword?: string;
area?: string; // 地区过滤
orderBy?: string;
orderDirection?: 'asc' | 'desc';
reviewType?: string; // 添加 reviewType 参数,值为 contract 或 record
userRole?: string; // 用户角色
token?: string; // JWT token
}
@@ -53,6 +54,7 @@ export interface ApiRule {
id: number;
code: string;
name: string;
area?: string; // 地区
evaluation_point_groups_id: number | null;
risk: string;
description: string;
@@ -61,6 +63,15 @@ export interface ApiRule {
first_name: string;
second_name: string;
};
// 🆕 PostgREST 双连接查询返回的字段
child_group?: {
id: number;
name: string;
} | null;
parent_group?: {
id: number;
name: string;
} | null;
references_laws: Record<string, unknown>;
extraction_config: {
type: string;
@@ -118,27 +129,42 @@ function mapApiRuleToFrontendModel(apiRule: ApiRule): Rule {
'低': 'low'
};
//评查点类型映射
// 🆕 优先使用 PostgREST 双连接查询返回的数据
let ruleType = '';
let groupName = '';
let groupId = '';
// 规则类型映射(这里根据实际业务逻辑设置一个默认值)
// const ruleType = 'essential'; // 实际应用中可能需要从其他字段推断
// console.log("apiRule.evaluation_point_groups",apiRule);
// 如果evaluation_point_groups_id为null或空,则不显示ruleType和groupName
const isGroupIdEmpty = !apiRule.evaluation_point_groups_id;
// 规则类型只在有分组ID时才显示
const ruleType = isGroupIdEmpty ? '' : (apiRule.evaluation_point_groups?.first_name || '');
if (apiRule.child_group || apiRule.parent_group) {
// 有 PostgREST 双连接查询数据(外键约束方式)
groupId = apiRule.child_group?.id.toString() || '';
groupName = apiRule.child_group?.name || '';
ruleType = apiRule.parent_group?.name || '';
} else if (apiRule.evaluation_point_groups) {
// 兼容旧的查询方式(RPC 函数或手动拼接)
const isGroupIdEmpty = !apiRule.evaluation_point_groups_id;
ruleType = isGroupIdEmpty ? '' : (apiRule.evaluation_point_groups.first_name || '');
groupName = isGroupIdEmpty ? '' : (apiRule.evaluation_point_groups.second_name || `${apiRule.evaluation_point_groups_id}`);
groupId = isGroupIdEmpty ? '' : apiRule.evaluation_point_groups_id?.toString() || '';
} else {
// 没有任何分组信息
const isGroupIdEmpty = !apiRule.evaluation_point_groups_id;
groupId = isGroupIdEmpty ? '' : apiRule.evaluation_point_groups_id?.toString() || '';
}
// 🔑 清洗评查点编码:移除最后一个 '--' 及其后面的字符
// 例如:'code-mis--mz' --> 'code-mis', 'code-mbs--alsi--gz' --> 'code-mbs--alsi'
let cleanedCode = apiRule.code || '';
const lastDoubleHyphenIndex = cleanedCode.lastIndexOf('--');
if (lastDoubleHyphenIndex !== -1) {
cleanedCode = cleanedCode.substring(0, lastDoubleHyphenIndex);
}
// 规则组名称只在有分组ID时才显示
const groupName = isGroupIdEmpty ? '' : (apiRule.evaluation_point_groups?.second_name || `${apiRule.evaluation_point_groups_id}`);
return {
id: apiRule.id ? apiRule.id.toString() : '', // 添加空值检查
code: apiRule.code || '',
id: apiRule.id ? apiRule.id.toString() : '',
code: cleanedCode,
name: apiRule.name || '',
ruleType: ruleType,
groupId: isGroupIdEmpty ? '' : apiRule.evaluation_point_groups_id?.toString() || '',
groupId: groupId,
groupName: groupName,
priority: priorityMap[apiRule.risk] || 'medium',
description: apiRule.description || '',
@@ -156,40 +182,63 @@ function mapApiRuleToFrontendModel(apiRule: ApiRule): Rule {
export async function getRulesList(params: RulesQueryParams): Promise<{data: RulesListResponse; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 解构并设置默认值
const {
page = 1,
pageSize = 10,
ruleType,
groupId,
isActive,
const {
page = 1,
pageSize = 10,
ruleType,
groupId,
isActive,
keyword,
area,
orderBy = 'created_at',
orderDirection = 'desc',
reviewType,
userRole,
token
} = params;
// 🔑 如果没有传递 userRole,尝试从 localStorage 中获取
let user_role = ''
if (!userRole && typeof window !== 'undefined' && window.localStorage) {
try {
const userInfoStr = localStorage.getItem('user_info');
if (userInfoStr) {
const userInfo = JSON.parse(userInfoStr);
user_role = userInfo.user_role || userInfo.userRole;
// console.log('📋 [getRulesList] 从 localStorage 获取用户角色:', userRole);
}
} catch (error) {
console.error('❌ [getRulesList] 解析 localStorage 用户信息失败:', error);
}
}
// 构建PostgrestParams参数
const postgrestParams: PostgrestParams = {
// 修改select语句,不使用嵌入查询语法
// 🆕 使用 PostgREST 双连接查询(直接连接父子分组)
// child_group: 通过 evaluation_point_groups_id 获取子分组(所属规则组)
// parent_group: 通过 evaluation_point_groups_pid 直接获取父分组(评查点类型)
// ⚠️ 重要:使用 .replace() 移除换行符,PostgREST 的 select 参数不支持多行字符串
select: `
id,
code,
name,
area,
evaluation_point_groups_id,
evaluation_point_groups_pid,
risk,
description,
is_enabled,
created_at,
updated_at
`,
updated_at,
child_group:evaluation_point_groups!fk_evaluation_points_group(id,name),
parent_group:evaluation_point_groups!fk_evaluation_points_parent_group(id,name)
`.replace(/\s+/g, ' ').trim(),
// 设置分页
limit: pageSize,
offset: (page - 1) * pageSize,
// 设置排序
order: `${orderBy}.${orderDirection}`,
// 构建过滤条件
filter: {},
@@ -204,62 +253,30 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul
if (groupId) {
postgrestParams.filter!['evaluation_point_groups_id'] = `eq.${groupId}`;
}
if (isActive !== undefined) {
postgrestParams.filter!['is_enabled'] = `eq.${isActive}`;
}
// 根据reviewType添加过滤条件
if (reviewType) {
try {
// 先获取所有评查点组数据,用于找到对应的pid
const groupsAllResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number; pid: number; m_type: number}>}>('evaluation_point_groups', {
select: 'id,pid,m_type',
token
});
let groups: Array<{id: number; pid: number; m_type: number}> = [];
if (!groupsAllResponse.error) {
if (groupsAllResponse.data && 'code' in groupsAllResponse.data && groupsAllResponse.data.data) {
groups = groupsAllResponse.data.data;
} else if (Array.isArray(groupsAllResponse.data)) {
groups = groupsAllResponse.data;
}
}
// 根据reviewType过滤pid
let pidList: number[] = [];
if (reviewType === 'contract') {
// 合同类型,找到所有pid=3的评查点组 2025/10/29: 修改为通过m_type = 0 去查找评查点组
// const contractGroups = groups.filter(g => g.pid === 3).map(g => g.id);
const contractGroups = groups.filter(g => g.m_type === 0).map(g => g.id);
pidList = contractGroups;
} else if (reviewType === 'record') {
// 卷宗类型,找到所有pid=1或pid=2的评查点组 2025/10/29: 修改为通过m_type = 1 去查找评查点组
// const recordGroups = groups.filter(g => g.pid === 1 || g.pid === 2).map(g => g.id);
const recordGroups = groups.filter(g => g.m_type === 1).map(g => g.id);
pidList = recordGroups;
}
// 如果有过滤的组id,则添加到查询条件中
if (pidList.length > 0) {
postgrestParams.filter!['evaluation_point_groups_id'] = `in.(${pidList.join(',')})`;
}
} catch (error) {
console.error('获取评查点组数据出错:', error);
}
// 🔑 添加地区过滤
if (user_role == 'provincial_admin') {
postgrestParams.filter!['area'] = `eq.省级`;
}else{
postgrestParams.filter!['area'] = `eq.${area}`;
}
// 如果指定了评查点类型ID,需要先查询该类型下的所有规则组ID
if (ruleType) {
try {
// 先获取该类型下的所有规则组
// 🔑 检查是否为多个类型(逗号分隔)
const isMultipleTypes = ruleType.includes(',');
// 先获取该类型(或多个类型)下的所有规则组
const groupsParams: PostgrestParams = {
select: 'id',
filter: {
'pid': `eq.${ruleType}`
// 如果是多个类型,使用 in.(type1,type2),否则使用 eq.type
'pid': isMultipleTypes ? `in.(${ruleType})` : `eq.${ruleType}`
},
token
};
@@ -311,7 +328,7 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul
if (response.error) {
return { error: response.error, status: response.status };
}
// 处理不同的API响应格式
let apiRules: ApiRule[] = [];
let totalCount = 0;
@@ -352,87 +369,18 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul
totalCount = apiRules.length;
}
// 打印第一个数据项来查看结构(仅用于调试)
// if (apiRules.length > 0) {
// console.log('评查点数据示例:', JSON.stringify(apiRules[0], null, 2));
// }
// 🆕 使用嵌套查询后,不再需要手动查询分组信息
// PostgREST 的嵌套 select 已经自动关联了分组数据
console.log('📋 [getRulesList] 使用 PostgREST 嵌套查询获取评查点数据');
// 收集所有规则分组ID(实际上是二级分组) (多进行一步查找表的操作)
const groupIds = [...new Set(apiRules.map(rule => rule.evaluation_point_groups_id))];
// 如果有分组ID,查询分组信息 - 更新类型定义
const groupsMap: Record<string, {first_name: string; second_name: string}> = {};
if (groupIds.length > 0) {
try {
// 过滤null和空值
const validGroupIds = groupIds.filter(id => id != null && id.toString().trim() !== '');
if (validGroupIds.length > 0) {
// 使用Promise.all并行查询所有分组信息 - 使用正确的函数名
const groupPromises = validGroupIds.map(id =>
postgrestGet<{code: number; msg: string; data: {id: number; pid: number; name: string; first_name: string; second_name: string}[]}>(
`rpc/get_evaluation_point_group_with_pid?input_id=${id}`,
{ token }
)
);
const groupResponses = await Promise.all(groupPromises);
// console.log("groupResponsesdddddddddddddddddd",groupResponses);
// 处理响应,填充groupsMap
groupResponses.forEach((response) => {
if (!response.error && response.data) {
// 处理不同API响应格式
if (Array.isArray(response.data.data) && response.data.data.length > 0) {
const groupid = response.data.data[1]?.id || "";
groupsMap[groupid] = {
first_name: response.data.data[0]?.name || "",
second_name: response.data.data[1]?.name || ""
};
}else if(Array.isArray(response.data) && response.data.length > 0){
const groupid = response.data[1]?.id || "";
groupsMap[groupid] = {
first_name: response.data[0]?.name || "",
second_name: response.data[1]?.name || ""
};
}
}
});
}
} catch (error) {
console.error('获取分组数据失败:', error);
// 失败不阻止主流程,使用默认分组名
}
}
// 将API返回的数据映射到前端模型
const mappedRules = apiRules.map(apiRule => {
const rule = mapApiRuleToFrontendModel(apiRule);
// 应用本地过滤 - 只在API不支持的情况下使用
const filteredRules = [...apiRules];
// 不再需要本地过滤,因为已经在API层面添加了评查点类型过滤
// 如果进行了本地过滤,则需要调整总记录数
// if (ruleType) {
// totalCount = filteredRules.length;
// }
// 将API返回的数据映射到前端模型,并附加分组名称
// console.log("groupsMap",groupsMap);
const mappedRules = filteredRules.map(apiRule => {
// 创建修改版的apiRule,添加从分组映射获取的名称
const enhancedApiRule = {
...apiRule,
// 从映射中获取分组名称,如果不存在则使用默认值
evaluation_point_groups: {
first_name: groupsMap[apiRule.evaluation_point_groups_id?.toString() || '']?.first_name || `${apiRule.evaluation_point_groups_id}`,
second_name: groupsMap[apiRule.evaluation_point_groups_id?.toString() || '']?.second_name || `${apiRule.evaluation_point_groups_id}`
}
};
const rule = mapApiRuleToFrontendModel(enhancedApiRule);
// 格式化日期字段
rule.createdAt = formatDate(rule.createdAt);
rule.updatedAt = formatDate(rule.updatedAt);
return rule;
});
@@ -848,72 +796,134 @@ export interface RuleGroup {
/**
* 获取评查点类型列表
* @param reviewType 评查类型,contract表示合同,record表示卷宗
* @param documentTypeIds 文档类型 ID 列表
* @param token JWT token (可选)
* @returns 评查点类型列表
*/
export async function getRuleTypes(reviewType?: string, token?: string): Promise<{data: RuleType[]; error?: never} | {data?: never; error: string; status?: number}> {
export async function getRuleTypes(documentTypeIds?: number[], token?: string): Promise<{data: RuleType[]; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 构建PostgrestParams参数
const postgrestParams: PostgrestParams = {
// 如果没有传入 documentTypeIds,返回空数组
if (!documentTypeIds || documentTypeIds.length === 0) {
console.warn('getRuleTypes: 未提供 documentTypeIds');
return { data: [] };
}
// 1️⃣ 根据 documentTypeIds 查询 document_types 表
const typeIdsStr = documentTypeIds.join(',');
const documentTypesParams: PostgrestParams = {
select: 'id, name, evaluation_point_groups_ids',
filter: {
'id': `in.(${typeIdsStr})`
},
token
};
const documentTypesResponse = await postgrestGet<{
code: number;
msg: string;
data: Array<{
id: number;
name: string;
evaluation_point_groups_ids: number[];
}>
}>('document_types', documentTypesParams);
if (documentTypesResponse.error) {
return { error: documentTypesResponse.error, status: documentTypesResponse.status };
}
// 提取 document_types 数据
let documentTypesData: Array<{
id: number;
name: string;
evaluation_point_groups_ids: number[];
}> = [];
if (documentTypesResponse.data && 'code' in documentTypesResponse.data && documentTypesResponse.data.data) {
if (Array.isArray(documentTypesResponse.data.data)) {
documentTypesData = documentTypesResponse.data.data;
}
} else if (Array.isArray(documentTypesResponse.data)) {
documentTypesData = documentTypesResponse.data as Array<{
id: number;
name: string;
evaluation_point_groups_ids: number[];
}>;
}
if (documentTypesData.length === 0) {
console.warn('getRuleTypes: 未找到对应的文档类型数据');
return { data: [] };
}
// 2️⃣ 提取并组合所有的 evaluation_point_groups_ids
const allGroupIds = new Set<number>();
documentTypesData.forEach(docType => {
if (Array.isArray(docType.evaluation_point_groups_ids)) {
docType.evaluation_point_groups_ids.forEach(id => allGroupIds.add(id));
}
});
if (allGroupIds.size === 0) {
console.warn('getRuleTypes: 未找到评查点组 ID');
return { data: [] };
}
// console.log('📋 [getRuleTypes] 提取的评查点组 IDs:', Array.from(allGroupIds));
// 3️⃣ 根据组合后的 ID 查询 evaluation_point_groups 表
const groupIdsStr = Array.from(allGroupIds).join(',');
const groupsParams: PostgrestParams = {
select: `
id,
pid,
code,
name,
description,
is_enabled,
m_type
is_enabled
`,
// 查询父ID为0的类型(顶级类型)
filter: {
'pid': 'eq.0'
'id': `in.(${groupIdsStr})`,
'pid': 'eq.0'
},
token
};
// 根据 reviewType 添加过滤条件
if (reviewType === 'contract') {
// 如果是合同类型,只加载id=3的评查点类型 2025/10/29: 修改为通过m_type = 0 去查找评查点组
postgrestParams.filter!['m_type'] = 'eq.0';
} else if (reviewType === 'record') {
// 如果是卷宗类型,只加载id=1和id=2的评查点类型 2025/10/29: 修改为通过m_type = 1 去查找评查点组
postgrestParams.filter!['m_type'] = 'eq.1';
}
// 发送请求获取评查点类型列表
const response = await postgrestGet<{code: number; msg: string; data: Array<{
id: number;
pid: number;
code: string;
name: string;
description: string;
is_enabled: boolean;
}>;
}>('evaluation_point_groups', postgrestParams);
const response = await postgrestGet<{
code: number;
msg: string;
data: Array<{
id: number;
pid: number;
code: string;
name: string;
description: string;
is_enabled: boolean;
}>
}>('evaluation_point_groups', groupsParams);
// 检查是否有错误响应
if (response.error) {
return { error: response.error, status: response.status };
}
if(response.data && 'code' in response.data && response.data.data){
if(Array.isArray(response.data.data) && response.data.data.length > 0){
// 将API返回的数据映射到前端模型
const ruleTypes = response.data.data.map(item => ({
id: item.id.toString(),
pid: item.pid.toString(),
code: item.code,
name: item.name,
description: item.description,
isEnabled: item.is_enabled
}));
return { data: ruleTypes };
}else{
return { error: '9000接口返回数据格式不正确', status: 500 };
// 处理响应数据
if (response.data && 'code' in response.data && response.data.data) {
if (Array.isArray(response.data.data)) {
const ruleTypes = response.data.data.map(item => ({
id: item.id.toString(),
pid: item.pid.toString(),
code: item.code,
name: item.name,
description: item.description,
isEnabled: item.is_enabled
}));
// console.log('📋 [getRuleTypes] 返回评查点类型:', ruleTypes);
return { data: ruleTypes };
} else {
return { data: [] };
}
}else if(Array.isArray(response.data) && response.data.length > 0){
// console.log("评查点类型列表",response.data);
} else if (Array.isArray(response.data)) {
const ruleTypes = response.data.map(item => ({
id: item.id.toString(),
pid: item.pid.toString(),
@@ -922,13 +932,14 @@ export async function getRuleTypes(reviewType?: string, token?: string): Promise
description: item.description,
isEnabled: item.is_enabled
}));
// console.log('📋 [getRuleTypes] 返回评查点类型:', ruleTypes);
return { data: ruleTypes };
}else{
return { error: '3000接口返回数据格式不正确', status: 500 };
} else {
return { data: [] };
}
} catch (error) {
console.error('获取评查点类型出错:', error);
return {
return {
error: error instanceof Error ? error.message : '获取评查点类型失败',
status: 500
};
+70 -154
View File
@@ -12,9 +12,9 @@ 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 &&
if (typeof responseData === 'object' && responseData !== null &&
'code' in responseData &&
'data' in responseData &&
(responseData as { data: unknown }).data) {
return (responseData as { data: T }).data;
}
@@ -23,6 +23,34 @@ function extractApiData<T>(responseData: unknown): T | null {
return responseData as T;
}
/**
* 从 sessionStorage 获取文档类型 ID 列表(客户端专用)
* @returns 文档类型 ID 数组,如果不存在则返回 null
*/
function getDocumentTypeIdsFromSession(): number[] | null {
if (typeof window === 'undefined') {
return null; // 服务端环境返回 null
}
try {
const typeIdsStr = sessionStorage.getItem('documentTypeIds');
if (!typeIdsStr) {
return null;
}
const typeIds = JSON.parse(typeIdsStr);
if (Array.isArray(typeIds) && typeIds.every(id => typeof id === 'number')) {
return typeIds;
}
console.warn('⚠️ [getDocumentTypeIds] documentTypeIds 格式不正确:', typeIds);
return null;
} catch (error) {
console.error('❌ [getDocumentTypeIds] 解析 documentTypeIds 失败:', error);
return null;
}
}
// 文档状态枚举
export enum DocumentStatus {
waiting = 'waiting',
@@ -226,6 +254,7 @@ export async function uploadContractTemplate(
* @param mergeMode 合并模式:'overwrite'(覆盖原文档)或 'new'(新建文档记录)
* @param isReprocess 是否触发重新处理
* @param remark 备注
* @param token JWT token(可选)
* @returns 上传结果
*/
export async function appendContractAttachments(
@@ -233,7 +262,8 @@ export async function appendContractAttachments(
files: File[],
mergeMode: 'overwrite' | 'new' = 'overwrite',
isReprocess: boolean = true,
remark?: string
remark?: string,
token?: string
): Promise<{data: FileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> {
try {
console.log('【合同附件追加】开始追加附件:', { documentId, fileCount: files.length, mergeMode });
@@ -256,20 +286,18 @@ export async function appendContractAttachments(
// 构建请求URL
const uploadUrl = `${UPLOAD_URL}/contracts/${documentId}/append_attachments`;
console.log('【合同附件追加】准备发送请求到服务器:', uploadUrl);
// 设置请求头
const headers: HeadersInit = {
'Accept': 'application/json'
};
// 从 localStorage 获取 token
if (typeof window !== 'undefined') {
const token = localStorage.getItem('access_token');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// 使用传入的 token 或从 localStorage 获取
const authToken = token || (typeof window !== 'undefined' ? localStorage.getItem('access_token') : null);
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
// 发送请求
const response = await fetch(uploadUrl, {
method: 'POST',
@@ -440,10 +468,15 @@ export async function uploadDocumentToServer(
/**
* 获取当天的文档列表
* @param userInfo 用户信息(必需)
* @param reviewType 审核类型(可选)
* @param token JWT token
* @param documentTypeIds 文档类型 ID 列表(可选)
* @returns 文档列表
*/
export async function getTodayDocuments(userInfo?: { user_id?: number; [key: string]: unknown }, reviewType?: string, token?: string): Promise<{data: Document[]; error?: never} | {data?: never; error: string; status?: number}> {
export async function getTodayDocuments(
userInfo?: { user_id?: number; [key: string]: unknown },
token?: string,
documentTypeIds?: number[]
): Promise<{data: Document[]; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 检查用户信息是否存在
if (!userInfo?.user_id) {
@@ -455,129 +488,11 @@ export async function getTodayDocuments(userInfo?: { user_id?: number; [key: str
const today = dayjs().startOf('day').format('YYYY-MM-DD');
// console.log('查询当天文档,日期范围:', today);
// 如果是合同类型,需要合并查询documents表和contract_structure_comparison表
if (reviewType === 'contract') {
try {
// 查询documents表中的合同数据
const documentsParams: PostgrestParams = {
select: `
id,
name,
type_id,
file_size,
status,
created_at,
document_number,
path,
storage_type,
is_test_document,
evaluation_level,
ocr_result,
extracted_results,
sumary,
remark,
audit_status
`,
order: 'created_at.desc',
filter: {
'created_at': `gte.${today}`,
'type_id': 'eq.1',
'user_id': `eq.${userInfo.user_id}`
}
};
// 🔑 优先使用传入的 documentTypeIds,否则从 sessionStorage 读取(客户端)
const typeIds = documentTypeIds || getDocumentTypeIdsFromSession();
// console.log('📋 [getTodayDocuments] 文档类型 IDs:', typeIds, '来源:', documentTypeIds ? 'URL参数' : 'sessionStorage');
// 查询contract_structure_comparison表中的数据
// const comparisonParams: PostgrestParams = {
// select: `
// id,
// template_contract_name,
// file_size,
// status,
// created_at,
// document_id,
// template_contract_path,
// ocr_results,
// comparison_results
// `,
// order: 'created_at.desc',
// filter: {
// 'created_at': `gte.${today}`
// }
// };
// 并行查询两个表
// const [documentsResponse, comparisonResponse] = await Promise.all([
// postgrestGet<Document[]>('documents', documentsParams),
// postgrestGet<ContractStructureComparison[]>('contract_structure_comparison', comparisonParams)
// ]);
const documentsResponse = await postgrestGet<Document[]>('documents', { ...documentsParams, token });
// console.log('documents表响应:', documentsResponse);
// console.log('contract_structure_comparison表响应:', comparisonResponse);
// if (documentsResponse.error && comparisonResponse.error) {
// console.error('两个表查询都失败:', documentsResponse.error, comparisonResponse.error);
// return { error: documentsResponse.error || comparisonResponse.error, status: documentsResponse.status || comparisonResponse.status };
// }
if (documentsResponse.error) {
console.error('documents表查询失败:', documentsResponse.error);
return { error: documentsResponse.error, status: documentsResponse.status };
}
// 提取documents表数据
let documentsData: Document[] = [];
if (!documentsResponse.error && documentsResponse.data) {
const extractedDocuments = extractApiData<Document[]>(documentsResponse.data);
if (extractedDocuments) {
documentsData = extractedDocuments;
}
}
// 提取contract_structure_comparison表数据并转换为Document格式
// let comparisonData: Document[] = [];
// if (!comparisonResponse.error && comparisonResponse.data) {
// const extractedComparison = extractApiData<ContractStructureComparison[]>(comparisonResponse.data);
// if (extractedComparison) {
// // 将ContractStructureComparison转换为Document格式
// console.log('extractedComparison:', extractedComparison);
// comparisonData = extractedComparison.map(item => ({
// id: item.id,
// name: item.template_contract_name || `合同结构比较记录_${item.id}`,
// type_id: 1, // 合同结构比较默认为合同类型
// file_size: item.file_size || 0,
// status: item.status,
// created_at: item.created_at,
// document_id: item.document_id,
// template_contract_path: item.template_contract_path,
// ocr_results: item.ocr_results,
// comparison_results: item.comparison_results
// }));
// }
// }
// 合并两个数据源
// const allData = [...documentsData, ...comparisonData];
const allData = [...documentsData];
// 按created_at降序排序
allData.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
// console.log('合并后的数据:', allData);
return { data: allData };
} catch (contractError) {
console.error('合同类型查询失败:', contractError);
return {
error: contractError instanceof Error ? contractError.message : '合同类型查询失败',
status: 500
};
}
}
// 非合同类型的原有逻辑
const params: PostgrestParams = {
select: `
id,
@@ -604,14 +519,16 @@ export async function getTodayDocuments(userInfo?: { user_id?: number; [key: str
}
};
// 根据reviewType添加过滤条件
if (reviewType === 'record') {
// 如果是卷宗类型,只显示type_id=2或type_id=3的文档
// 🔑 根据 documentTypeIds 添加过滤条件
if (typeIds && typeIds.length > 0) {
// 使用动态的文档类型 ID 列表
const typeIdsStr = typeIds.join(',');
if (params.filter) {
params.filter['type_id'] = 'in.(2,3,155)';
params.filter['type_id'] = `in.(${typeIdsStr})`;
} else {
params.filter = { 'type_id': 'in.(2,3,155)' };
params.filter = { 'type_id': `in.(${typeIdsStr})` };
}
console.log('📋 [getTodayDocuments] 使用文档类型 IDs 查询:', typeIdsStr);
}
// console.log('发送请求参数:', params);
@@ -643,33 +560,32 @@ export async function getTodayDocuments(userInfo?: { user_id?: number; [key: str
/**
* 获取文档类型列表
* @param reviewType 审核类型(可选)
* @param token JWT token (可选)
* @returns 文档类型列表
*/
export async function getDocumentTypes(reviewType?: string, token?: string): Promise<{data: DocumentType[]; error?: never} | {data?: never; error: string; status?: number}> {
export async function getDocumentTypes(token?: string): Promise<{data: DocumentType[]; error?: never} | {data?: never; error: string; status?: number}> {
try {
const params: PostgrestParams = {
select: 'id, name',
filter: {} // 初始化为空对象
};
// 根据reviewType添加过滤条件
if (reviewType === 'contract') {
// 如果是合同类型,只显示id=1的文档类型
// 🔑 从 sessionStorage 获取文档类型 ID 列表(动态方式)
const documentTypeIds = getDocumentTypeIdsFromSession();
// console.log('📋 [getDocumentTypes] 文档类型 IDs:', documentTypeIds);
// 根据 documentTypeIds 添加过滤条件
if (documentTypeIds && documentTypeIds.length > 0) {
// 使用动态的文档类型 ID 列表
const typeIdsStr = documentTypeIds.join(',');
if (params.filter) {
params.filter['id'] = 'eq.1';
params.filter['id'] = `in.(${typeIdsStr})`;
} else {
params.filter = { 'id': 'eq.1' };
}
} else if (reviewType === 'record') {
// 如果是卷宗类型,只显示id=2或id=3的文档类型
if (params.filter) {
params.filter['id'] = 'in.(2,3,155)';
} else {
params.filter = { 'id': 'in.(2,3,155)' };
params.filter = { 'id': `in.(${typeIdsStr})` };
}
console.log('📋 [getDocumentTypes] 使用动态类型 IDs 查询:', typeIdsStr);
}
// 如果没有 documentTypeIds,返回所有文档类型(不添加过滤条件)
const response = await postgrestGet<DocumentType[]>('document_types', { ...params, token });
+122
View File
@@ -485,3 +485,125 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
}
}
/**
* 入口模块类型定义
*/
export interface EntryModule {
id: number;
name: string;
description: string | null;
path: string | null;
areas: string[];
created_at: string;
updated_at: string;
document_types?: Array<{
id: number;
name: string;
code: string | null;
}>;
}
/**
* 获取用户可访问的入口模块
* @param userArea 用户所属地区
* @param token JWT token
* @returns 入口模块列表
*/
export async function getEntryModules(userRole: string | null | undefined, userArea: string | null | undefined, token?: string): Promise<EntryModule[]> {
try {
if (!userRole || !userArea) {
console.warn('⚠️ [getEntryModules] 用户角色或地区为空,返回空模块列表');
return [];
}
// console.log('🔍 [getEntryModules] 查询地区:', userArea);
// 查询 entry_modules 表,筛选 areas 数组中包含用户地区的模块
// 使用 PostgreSQL JSONB 操作符 @> 检查数组是否包含值
const params: PostgrestParams = {
select: 'id,name,description,path,areas,created_at,updated_at',
filter: {
// areas 数组中包含用户的 area
// areas: `cs.["${userArea}"]` // cs = contains (PostgreSQL @> 操作符)
}
};
if (userRole != 'provincial_admin'){
params.filter = {
areas: `cs.["${userArea}"]`
}
}
const modulesResponse = await postgrestGet('entry_modules', { ...params, token });
if (modulesResponse.error) {
console.error('❌ [getEntryModules] 查询入口模块失败:', modulesResponse.error);
return [];
}
const modules = extractApiData<EntryModule[]>(modulesResponse.data);
if (!modules || modules.length === 0) {
console.warn('⚠️ [getEntryModules] 未找到匹配的入口模块');
return [];
}
console.log(`✅ [getEntryModules] 找到 ${modules.length} 个入口模块`);
// 为每个模块查询关联的 document_types
const modulesWithTypes = await Promise.all(
modules.map(async (module) => {
try {
const typesParams: PostgrestParams = {
select: 'id,name,code',
filter: {
entry_module_id: `eq.${module.id}`
}
};
const typesResponse = await postgrestGet('document_types', { ...typesParams, token });
if (typesResponse.error) {
console.error(`❌ [getEntryModules] 查询模块 ${module.id} 的文档类型失败:`, typesResponse.error);
return { ...module, document_types: [] };
}
const documentTypes = extractApiData<Array<{ id: number; name: string; code: string | null }>>(typesResponse.data);
return {
...module,
document_types: documentTypes || []
};
} catch (error) {
console.error(`❌ [getEntryModules] 处理模块 ${module.id} 时出错:`, error);
return { ...module, document_types: [] };
}
})
);
console.log('✅ [getEntryModules] 入口模块数据(含文档类型):', JSON.stringify(modulesWithTypes));
// 默认会多加一个 智慧法务大模型 入口 默认所有人都可以用,看到
modulesWithTypes.push({
"id": 0,
"name": "智慧法务大模型",
"description": "智慧法务大模型",
"path": "entryModule/assistant",
"areas": [],
"created_at": "2025-11-18T21:33:33.857417+08:00",
"updated_at": "2025-11-18T22:28:51.819722+08:00",
"document_types": [
{
"id": 0,
"name": "空",
"code": "空"
}
]
})
return modulesWithTypes;
} catch (error) {
console.error('❌ [getEntryModules] 获取入口模块失败:', error instanceof Error ? error.message : String(error));
return [];
}
}
+80 -50
View File
@@ -29,7 +29,7 @@ import { OAUTH_CONFIG, API_BASE_URL } from "~/config/api-config";
* @property {'common'} common - 普通用户,有基本的系统访问权限
* @property {'developer'} developer - 开发者/管理员,有完整的系统管理权限
*/
export type UserRole = 'common' | 'admin' | 'deptLeader' | 'groupLeader' | string;
export type UserRole = string;
/**
* 用户信息接口,对应 sso_users 表结构
@@ -144,22 +144,22 @@ export async function getSession(request: Request) {
* @param expiresIn OAuth token过期时间(秒)
* @returns JWT字符串
*/
async function generateFrontendJWT(userInfo: UserInfo, savedUserData: SsoUser, userRole: UserRole, expiresIn: number): Promise<string> {
const jwtUserInfo: UserInfoForJWT = {
sub: userInfo.sub,
user_id: savedUserData.id!,
username: savedUserData.username,
nick_name: savedUserData.nick_name,
email: savedUserData.email,
phone_number: savedUserData.phone_number,
ou_id: savedUserData.ou_id,
ou_name: savedUserData.ou_name,
is_leader: savedUserData.is_leader,
user_role: userRole
};
// async function generateFrontendJWT(userInfo: UserInfo, savedUserData: SsoUser, userRole: UserRole, expiresIn: number): Promise<string> {
// const jwtUserInfo: UserInfoForJWT = {
// sub: userInfo.sub,
// user_id: savedUserData.id!,
// username: savedUserData.username,
// nick_name: savedUserData.nick_name,
// email: savedUserData.email,
// phone_number: savedUserData.phone_number,
// ou_id: savedUserData.ou_id,
// ou_name: savedUserData.ou_name,
// is_leader: savedUserData.is_leader,
// user_role: userRole
// };
return JWTUtils.generateJWT(jwtUserInfo, expiresIn);
}
// return JWTUtils.generateJWT(jwtUserInfo, expiresIn);
// }
/**
* 创建包含JWT的用户信息对象
@@ -169,38 +169,44 @@ async function generateFrontendJWT(userInfo: UserInfo, savedUserData: SsoUser, u
* @param frontendJWT 前端JWT
* @returns 完整的用户信息对象
*/
function createUserInfoWithJWT(userInfo: UserInfo, savedUserData: SsoUser, userRole: UserRole, frontendJWT: string) {
return {
// 保持与callback.tsx中enhancedUserInfo相同的数据结构
sub: userInfo.sub,
username: savedUserData.username,
nick_name: savedUserData.nick_name,
phone_number: savedUserData.phone_number,
email: savedUserData.email,
ou_id: savedUserData.ou_id,
ou_name: savedUserData.ou_name,
status: savedUserData.status,
is_leader: savedUserData.is_leader,
// 增强字段,与OAuth登录保持一致
user_id: savedUserData.id,
user_role: userRole,
frontend_jwt: frontendJWT
};
}
// function createUserInfoWithJWT(userInfo: UserInfo, savedUserData: SsoUser, userRole: UserRole, frontendJWT: string) {
// return {
// // 保持与callback.tsx中enhancedUserInfo相同的数据结构
// sub: userInfo.sub,
// username: savedUserData.username,
// nick_name: savedUserData.nick_name,
// phone_number: savedUserData.phone_number,
// email: savedUserData.email,
// ou_id: savedUserData.ou_id,
// ou_name: savedUserData.ou_name,
// status: savedUserData.status,
// is_leader: savedUserData.is_leader,
// // 增强字段,与OAuth登录保持一致
// user_id: savedUserData.id,
// user_role: userRole,
// frontend_jwt: frontendJWT
// };
// }
export async function getUserSession(request: Request) {
const session = await getSession(request);
const isAuthenticated = session.get("isAuthenticated") === true;
const userRole = session.get("userRole") as UserRole;
let accessToken = session.get("accessToken");
const accessToken = session.get("accessToken");
const refreshToken = session.get("refreshToken");
let tokenIssuedAt = session.get("tokenIssuedAt");
const tokenIssuedAt = session.get("tokenIssuedAt");
let tokenExpiresIn = session.get("tokenExpiresIn");
const userInfo = session.get("userInfo");
let frontendJWT = session.get("frontendJWT");
const frontendJWT = session.get("frontendJWT");
// 🔑 检查是否是公共路径(不需要认证的路径)
const url = new URL(request.url);
const pathname = url.pathname;
const publicPaths = ['/login', '/favicon.ico', '/callback'];
const isPublicPath = publicPaths.some(path => pathname.startsWith(path));
let refreshedSession = null;
let shouldRegenerateJWT = false;
// let shouldRegenerateJWT = false;
// 🔑 新的统一过期检查逻辑
// 不区分 admin 和 OAuth 用户,所有用户都使用同样的过期检查
@@ -251,20 +257,44 @@ export async function getUserSession(request: Request) {
}
}
// 🚨 如果 session 无效(包括 token 过期),自动重定向到登录页
if (!finalIsAuthenticated && isAuthenticated) {
console.error("❌ [getUserSession] Session 已失效,清除 session 并重定向到登录页");
// 🚨 统一的认证检查和重定向逻辑
if (!finalIsAuthenticated) {
// 如果是公共路径,不重定向,直接返回未认证状态
if (isPublicPath) {
// console.log("️ [getUserSession] 公共路径,允许未认证访问:", pathname);
return {
isAuthenticated: false,
userRole,
accessToken,
refreshToken,
userInfo,
refreshedSession,
frontendJWT
};
}
// 销毁服务端 session
const { redirect } = await import("@remix-run/node");
const destroyedSession = await sessionStorage.destroySession(session);
// 非公共路径且未认证,重定向到登录页
if (isAuthenticated) {
// Session 存在但已失效(token 过期或数据不完整)
console.error("❌ [getUserSession] Session 已失效,清除 session 并重定向到登录页");
const { redirect } = await import("@remix-run/node");
const destroyedSession = await sessionStorage.destroySession(session);
// 重定向到登录页,添加 expired=true 参数标识是因为过期重定向
throw redirect("/login?expired=true", {
headers: {
"Set-Cookie": destroyedSession
}
});
// 重定向到登录页,添加 expired=true 参数
throw redirect("/login?expired=true", {
headers: {
"Set-Cookie": destroyedSession
}
});
} else {
// Session 不存在(首次访问或已登出)
console.warn("⚠️ [getUserSession] 未登录,重定向到登录页");
const { redirect } = await import("@remix-run/node");
// 保存当前路径,登录后可以跳转回来
const redirectTo = pathname !== '/login' ? pathname : '/';
throw redirect(`/login?redirect=${encodeURIComponent(redirectTo)}`);
}
}
return {
+1
View File
@@ -45,6 +45,7 @@ export interface LoginResponse {
is_leader: boolean;
user_role: string;
sub: string;
area?: string; // 🔑 用户所属地区
};
};
error?: string;
+9 -4
View File
@@ -96,24 +96,29 @@ function mergeAuthHeaders(
explicitToken?: string
): Record<string, string> {
const headers = { ...existingHeaders };
// 如果已经有 Authorization 头部(不区分大小写),不覆盖
const hasAuth = Object.keys(headers).some(
key => key.toLowerCase() === 'authorization'
);
if (hasAuth) {
console.log('🔑 [mergeAuthHeaders] 已存在 Authorization 头,不覆盖');
return headers;
}
// 优先使用显式传入的 token,否则尝试从客户端 localStorage 获取
const token = explicitToken || (typeof window !== 'undefined' ? localStorage.getItem('access_token') : undefined);
// 如果有有效的 token(显式传入或从客户端获取),添加到 Authorization 头部
if (token && token !== 'undefined') {
// console.log('🔑 [mergeAuthHeaders] 添加 Authorization 头,token 来源:', explicitToken ? 'explicitToken' : 'localStorage');
// console.log('🔑 [mergeAuthHeaders] Token 前10位:', token.substring(0, 10));
headers['Authorization'] = `Bearer ${token}`;
} else {
console.warn('⚠️ [mergeAuthHeaders] 没有可用的 tokenexplicitToken:', explicitToken, 'window:', typeof window);
}
return headers;
}