Merge branch 'PingChuan' into shiy-login

# Conflicts:
#	app/config/api-config.ts
fix: 1. 修复无法加载数据的问题:没有从入口页中进来会缺少数据。
2. 加强后端接口关于token的校验错误和权限校验错误的管理。

feat: 1. 对接后端的数据看板的接口。
2. 将系统设置单独抽出来作为管理员的固定一个入口。
This commit is contained in:
2025-11-22 15:57:22 +08:00
27 changed files with 1972 additions and 643 deletions
+76 -15
View File
@@ -493,11 +493,11 @@ 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}, JWT前20字符: ${jwt?.substring(0, 20)}`);
if (!jwt) {
console.error('❌ [User Routes] JWT token 未提供');
toastService.error("认证信息缺失,请重新登录");
// 不显示 toast,让 root loader 处理重定向
return { success: false, error: "JWT token 未提供", shouldRedirectToHome: true };
}
@@ -519,15 +519,34 @@ export async function getUserRoutesByRole(roleKey: string, jwt?: string, include
// 检查响应是否成功
if (response.error) {
console.error('❌ [User Routes] API 请求失败:', response.error);
toastService.error(response.error);
return { success: false, error: response.error, shouldRedirectToHome: true };
// 🔑 如果是令牌过期错误,标记需要重定向到登录页
const isTokenExpired = response.error.includes('令牌已过期') ||
response.error.includes('令牌') ||
response.error.includes('token') ||
response.error.includes('expired') ||
response.error.includes('认证') ||
response.error.includes('401');
console.log('🔍 [User Routes] 错误检测:', {
error: response.error,
isTokenExpired,
willRedirect: isTokenExpired
});
// 只在客户端显示toast(服务端调用时跳过)
if (!isTokenExpired && typeof window !== 'undefined') {
toastService.error(response.error);
}
return { success: false, error: response.error, shouldRedirectToHome: isTokenExpired };
}
// 检查响应数据
if (!response.data) {
console.error('❌ [User Routes] 后端未返回数据');
toastService.error("获取路由数据失败");
return { success: false, error: "后端未返回数据", shouldRedirectToHome: true };
if (typeof window !== 'undefined') {
toastService.error("获取路由数据失败");
}
return { success: false, error: "后端未返回数据", shouldRedirectToHome: false };
}
const backendResponse = response.data;
@@ -535,23 +554,45 @@ export async function getUserRoutesByRole(roleKey: string, jwt?: string, include
// 检查业务状态码(后端使用 code: 0 表示成功)
if (backendResponse.code !== 0 && backendResponse.code !== 200) {
console.error(`❌ [User Routes] 后端返回错误: ${backendResponse.msg}`);
toastService.error(backendResponse.msg || "获取路由权限失败");
return { success: false, error: backendResponse.msg || "获取路由权限失败", shouldRedirectToHome: true };
// 🔑 如果是令牌过期错误,标记需要重定向到登录页
const isTokenExpired = backendResponse.msg?.includes('令牌已过期') ||
backendResponse.msg?.includes('令牌') ||
backendResponse.msg?.includes('token') ||
backendResponse.msg?.includes('expired') ||
backendResponse.msg?.includes('认证') ||
backendResponse.msg?.includes('401');
console.log('🔍 [User Routes] 业务错误检测:', {
msg: backendResponse.msg,
code: backendResponse.code,
isTokenExpired,
willRedirect: isTokenExpired
});
// 只在客户端显示toast
if (!isTokenExpired && typeof window !== 'undefined') {
toastService.error(backendResponse.msg || "获取路由权限失败");
}
return { success: false, error: backendResponse.msg || "获取路由权限失败", shouldRedirectToHome: isTokenExpired };
}
// 检查数据完整性
if (!backendResponse.data || !Array.isArray(backendResponse.data.routes)) {
console.error('❌ [User Routes] 后端未返回路由数据');
toastService.error("未获取到路由权限,请联系管理员配置");
return { success: false, error: "后端未返回路由数据", shouldRedirectToHome: true };
if (typeof window !== 'undefined') {
toastService.error("未获取到路由权限,请联系管理员配置");
}
return { success: false, error: "后端未返回路由数据", shouldRedirectToHome: false };
}
const routes = backendResponse.data.routes;
if (routes.length === 0) {
console.log(`⚠️ [User Routes] 用户没有分配任何路由权限`);
toastService.error("您的角色没有分配任何路由权限,请联系管理员配置");
return { success: false, error: "用户没有分配任何路由权限", shouldRedirectToHome: true };
if (typeof window !== 'undefined') {
toastService.error("您的角色没有分配任何路由权限,请联系管理员配置");
}
return { success: false, error: "用户没有分配任何路由权限", shouldRedirectToHome: false };
}
// console.log('🔍 [User Routes] 后端返回的原始路由数据:', JSON.stringify(routes, null, 2));
@@ -568,11 +609,31 @@ export async function getUserRoutesByRole(roleKey: string, jwt?: string, include
} catch (error) {
console.error("❌ [User Routes] 获取用户路由时发生错误:", error);
toastService.error("获取用户路由时发生错误,请稍后再试");
const errorMessage = error instanceof Error ? error.message : String(error);
// 🔑 如果是认证相关错误,标记需要重定向到登录页
const isAuthError = errorMessage.includes('令牌') ||
errorMessage.includes('token') ||
errorMessage.includes('expired') ||
errorMessage.includes('认证') ||
errorMessage.includes('401') ||
errorMessage.includes('403');
console.log('🔍 [User Routes] 异常错误检测:', {
errorMessage,
isAuthError,
willRedirect: isAuthError
});
// 只在客户端显示toast
if (!isAuthError && typeof window !== 'undefined') {
toastService.error("获取用户路由时发生错误,请稍后再试");
}
return {
success: false,
error: `获取用户路由失败: ${error instanceof Error ? error.message : String(error)}`,
shouldRedirectToHome: true
error: `获取用户路由失败: ${errorMessage}`,
shouldRedirectToHome: isAuthError
};
}
}
+28 -2
View File
@@ -443,9 +443,35 @@ export async function apiRequest<T>(
// 检查API返回的状态码
const data = response.data;
if (data && typeof data === 'object' && 'code' in data && data.code !== 0) {
console.error(`API请求失败: ${data.message || data.msg || '未知错误'} - ${url}`);
const errorMessage = data.message || data.msg || '未知错误';
console.error(`API请求失败: ${errorMessage} - ${url}`);
// 🔑 检测令牌过期错误
const isTokenExpired = errorMessage.includes('令牌已过期') ||
errorMessage.includes('令牌') ||
errorMessage.includes('token') ||
errorMessage.includes('expired') ||
errorMessage.includes('认证') ||
errorMessage.includes('未授权');
if (isTokenExpired) {
console.error('🔑 [API Client] 检测到令牌过期,准备清除会话并重定向...');
// 只在客户端执行重定向
if (typeof window !== 'undefined') {
console.error('🔑 [API Client] 客户端环境,清除 localStorage 并重定向到登录页');
// 清除所有认证相关数据
localStorage.removeItem('access_token');
localStorage.removeItem('user_info');
sessionStorage.clear();
// 重定向到登录页
window.location.href = '/login?expired=true';
}
}
return {
error: data.message || data.msg || '请求失败',
error: errorMessage,
status: response.status,
headers: responseHeaders
};
+121 -385
View File
@@ -1,6 +1,6 @@
import { postgrestGet, postgrestPost, type PostgrestParams } from "../postgrest-client";
import { postgrestGet, type PostgrestParams } from "../postgrest-client";
import { apiRequest } from "../axios-client";
import dayjs from 'dayjs';
// import dayjs from 'dayjs';
/**
* 从不同格式的 API 响应中提取数据
@@ -78,397 +78,107 @@ interface HomeStatistics {
}
/**
* 通过传入的 reviewType 参数构建类型过滤条件
* @param reviewType 文档类型
* @returns 过滤条件字符串
* 后端统计接口响应类型(蛇形命名)
*/
function buildTypeFilter(reviewType: string | null): string {
let typeFilter = '';
if (reviewType === 'contract') {
typeFilter = 'type_id.eq.1';
} else if (reviewType === 'record') {
typeFilter = '(type_id.eq.2,type_id.eq.3)';
}
return typeFilter;
interface BackendStatisticsResponse {
today_pending_files: number;
monthly_reviewed_files: number;
monthly_review_growth: {
value: number;
is_up: boolean;
};
monthly_pass_rate: number;
pass_rate_growth: {
value: number;
is_up: boolean;
};
issues_detected: number;
issues_growth: {
value: number;
is_up: boolean;
};
}
/**
* 获取主页数据
* @param reviewType 从客户端传入的 reviewType 值
* @param userId 用户ID
* @param reviewType 从客户端传入的 reviewType 值(已废弃,现在从sessionStorage读取)
* @param userId 用户ID(已废弃,后端通过JWT自动识别)
* @param token JWT token
* @returns 主页数据
*/
export async function getHomeData(reviewType?: string | null,userId?: string | number, token?: string): Promise<HomeStatistics> {
export async function getHomeData(reviewType?: string | null, userId?: string | number, token?: string): Promise<HomeStatistics> {
try {
// 获取当前日期和时间相关值
const startOfToday = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss');
const startOfThisMonth = dayjs().startOf('month').format('YYYY-MM-DD HH:mm:ss');
const endOfThisMonth = dayjs().endOf('month').format('YYYY-MM-DD HH:mm:ss');
const startOfLastMonth = dayjs().subtract(1, 'month').startOf('month').format('YYYY-MM-DD HH:mm:ss');
const endOfLastMonth = dayjs().subtract(1, 'month').endOf('month').format('YYYY-MM-DD HH:mm:ss');
// console.log('传入的 reviewType', reviewType);
// console.log('传入的 userId', userId);
// 基于 reviewType 构建类型过滤条件
const typeFilter = buildTypeFilter(reviewType || null);
// console.log('构建的 typeFilter', typeFilter);
// 通用API响应处理函数
const handleApiResponse = async <T>(
apiCall: Promise<{
data?: unknown;
headers?: Record<string, string>;
error?: string;
status?: number
}>,
errorMessage: string,
defaultValue: T
): Promise<T> => {
try {
const response = await apiCall;
if (response.error) {
console.error(`${errorMessage}: ${response.error}`);
return defaultValue;
// 🔑 从 sessionStorage 获取文档类型IDs
let typeIds: string | null = null;
if (typeof window !== 'undefined') {
const storedTypeIds = sessionStorage.getItem('documentTypeIds');
if (storedTypeIds) {
try {
const typeIdsArray = JSON.parse(storedTypeIds) as number[];
if (Array.isArray(typeIdsArray) && typeIdsArray.length > 0) {
typeIds = typeIdsArray.join(',');
console.log('📊 [getHomeData] 从 sessionStorage 获取文档类型:', typeIds);
}
} catch (error) {
console.error('❌ [getHomeData] 解析 documentTypeIds 失败:', error);
}
const data = extractApiData<T>(response.data);
if (!data) {
console.warn(`${errorMessage}: 无法提取有效数据`);
return defaultValue;
}
return data;
} catch (error) {
console.error(`${errorMessage}: ${error instanceof Error ? error.message : '未知错误'}`);
return defaultValue;
}
};
// 1. 今日待审核文件 - 获取今天的待审核文件数量 (audit_status = 0 或 2)
const todayPendingParams: PostgrestParams = {
select: 'count',
filter: {
or: `(audit_status.eq.0,audit_status.eq.2,audit_status.is.null)`,
created_at: `gte.${startOfToday}`,
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
// 添加类型过滤条件
if (typeFilter) {
if (typeFilter.startsWith('(')) {
// 确保 filter 已初始化
if (!todayPendingParams.filter) {
todayPendingParams.filter = {};
}
todayPendingParams.filter.or = typeFilter + ',' + todayPendingParams.filter.or;
} else {
const [field, op, value] = typeFilter.split('.');
if (!todayPendingParams.filter) {
todayPendingParams.filter = {};
}
todayPendingParams.filter[field] = `${op}.${value}`;
}
}
const todayPendingCount = await handleApiResponse<{ count: number }[]>(
postgrestGet('documents', { ...todayPendingParams, token }),
'获取今日待审核文件数量失败',
[]
// 🔑 构建请求参数
const params: Record<string, string> = {
time_range: '30days' // 默认近30天
};
// 如果有文档类型,添加到参数
if (typeIds) {
params.type_ids = typeIds;
}
console.log('📊 [getHomeData] 请求参数:', params);
// 🔑 调用后端统计接口
const response = await apiRequest<BackendStatisticsResponse>(
'/admin/statistics/home-data',
{
method: 'GET',
headers: token ? {
'Authorization': `Bearer ${token}`
} : undefined
},
params // 查询参数
);
const todayPendingFiles = todayPendingCount[0]?.count || 0;
// 2. 本月已审核文件 - 获取本月已审核文件数量 (audit_status != 0 且 != 2)
const thisMonthReviewedParams: PostgrestParams = {
select: 'count',
filter: {
and: `(audit_status.neq.0,audit_status.neq.2)`,
upload_time: `gte.${startOfThisMonth}`,
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
// 添加类型过滤条件
if (typeFilter) {
if (typeFilter.startsWith('(')) {
thisMonthReviewedParams.or = typeFilter;
} else {
const [field, op, value] = typeFilter.split('.');
if (!thisMonthReviewedParams.filter) {
thisMonthReviewedParams.filter = {};
}
thisMonthReviewedParams.filter[field] = `${op}.${value}`;
}
if (response.error) {
console.error('❌ [getHomeData] 获取统计数据失败:', response.error);
throw new Error(response.error);
}
const thisMonthReviewedCount = await handleApiResponse<{ count: number }[]>(
postgrestGet('documents', { ...thisMonthReviewedParams, token }),
'获取本月已审核文件数量失败',
[]
);
// 本月已审核文件数量
const monthlyReviewedFiles = thisMonthReviewedCount[0]?.count || 0;
// 上月已审核文件
const lastMonthReviewedParams: PostgrestParams = {
select: 'count',
filter: {
// or: `(audit_status.eq.1,audit_status.eq.-1)`,
and: `(upload_time.gte.${startOfLastMonth},upload_time.lte.${endOfLastMonth},audit_status.neq.0,audit_status.neq.2)`,
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
// 添加类型过滤条件
if (typeFilter) {
if (typeFilter.startsWith('(')) {
// 确保 filter 已初始化
if (!lastMonthReviewedParams.filter) {
lastMonthReviewedParams.filter = {};
}
lastMonthReviewedParams.filter.or = typeFilter;
} else {
const [field, op, value] = typeFilter.split('.');
if (!lastMonthReviewedParams.filter) {
lastMonthReviewedParams.filter = {};
}
lastMonthReviewedParams.filter[field] = `${op}.${value}`;
}
const backendData = response.data;
if (!backendData) {
console.error('❌ [getHomeData] 后端未返回数据');
throw new Error('后端未返回统计数据');
}
const lastMonthReviewedCount = await handleApiResponse<{ count: number }[]>(
postgrestGet('documents', { ...lastMonthReviewedParams, token }),
'获取上月已审核文件数量失败',
[]
);
// 上月已审核文件数量
const lastMonthReviewed = lastMonthReviewedCount[0]?.count || 0;
// console.log('上月已审核文件查询参数', lastMonthReviewedParams);
// console.log('上月已审核文件数量', lastMonthReviewed);
// 计算同比增长
let reviewGrowthValue = 0;
let reviewGrowthIsUp = true;
if (lastMonthReviewed > 0) {
const growthRate = ((monthlyReviewedFiles - lastMonthReviewed) / lastMonthReviewed) * 100;
reviewGrowthValue = Math.abs(parseFloat(growthRate.toFixed(1)));
reviewGrowthIsUp = growthRate >= 0;
} else if (lastMonthReviewed == 0 && monthlyReviewedFiles > 0) {
reviewGrowthValue = 100;
reviewGrowthIsUp = true;
}
console.log('✅ [getHomeData] 获取统计数据成功:', backendData);
// 3. 审核通过率 - 本月审核通过率
const thisMonthTotalParams: PostgrestParams = {
select: 'count',
filter: {
audit_status: `eq.1`,
created_at: `gte.${startOfThisMonth}`,
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
// 添加类型过滤条件
if (typeFilter) {
if (typeFilter.startsWith('(')) {
thisMonthTotalParams.or = typeFilter;
} else {
const [field, op, value] = typeFilter.split('.');
if (!thisMonthTotalParams.filter) {
thisMonthTotalParams.filter = {};
}
thisMonthTotalParams.filter[field] = `${op}.${value}`;
}
}
const thisMonthTotalCount = await handleApiResponse<{ count: number }[]>(
postgrestGet('documents', { ...thisMonthTotalParams, token }),
'获取本月审核通过数量失败',
[]
);
// console.log('本月审核通过数量查询参数', thisMonthTotalParams);
// 本月审核通过数量
const thisMonthPassTotal = thisMonthTotalCount[0]?.count || 0;
// console.log('本月审核通过数量', thisMonthPassTotal);
// console.log('本月已审核文件数量', monthlyReviewedFiles);
// 本月审核通过率
const monthlyPassRate = (thisMonthPassTotal > 0 && monthlyReviewedFiles > 0)
? parseFloat(((thisMonthPassTotal / monthlyReviewedFiles) * 100).toFixed(1))
: 0;
// 上月审核通过率
const lastMonthTotalParams: PostgrestParams = {
select: 'count',
filter: {
audit_status: `eq.1`,
and: `(upload_time.gte.${startOfLastMonth},upload_time.lte.${endOfLastMonth})`,
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
// 添加类型过滤条件
if (typeFilter) {
if (typeFilter.startsWith('(')) {
lastMonthTotalParams.or = typeFilter;
} else {
const [field, op, value] = typeFilter.split('.');
if (!lastMonthTotalParams.filter) {
lastMonthTotalParams.filter = {};
}
lastMonthTotalParams.filter[field] = `${op}.${value}`;
}
}
const lastMonthTotalCount = await handleApiResponse<{ count: number }[]>(
postgrestGet('documents', { ...lastMonthTotalParams, token }),
'获取上月审核通过数量失败',
[]
);
// 上月审核通过数量
const lastMonthTotal = lastMonthTotalCount[0]?.count || 0;
// 上月审核通过率
const lastMonthPassRate = (lastMonthTotal > 0 && lastMonthReviewed > 0)
? parseFloat(((lastMonthTotal / lastMonthReviewed) * 100).toFixed(1))
: 0;
// console.log('上个月-------', lastMonthPassRate);
// 计算通过率同比增长
let passRateGrowthValue = 0;
let passRateGrowthIsUp = true;
if (lastMonthPassRate > 0) {
const passRateGrowth = ((monthlyPassRate - lastMonthPassRate) / lastMonthPassRate) * 100;
passRateGrowthValue = Math.abs(parseFloat(passRateGrowth.toFixed(1)));
passRateGrowthIsUp = passRateGrowth >= 0;
} else if (lastMonthPassRate == 0 && monthlyPassRate > 0) {
passRateGrowthValue = 100;
passRateGrowthIsUp = true;
}
// console.log('上月通过率-------', lastMonthPassRate);
// console.log('本月通过率-------', monthlyPassRate);
// 4. 检查出的问题总数(从评估结果表中统计)
// 使用新的数据库函数 count_evaluation_results_by_type 获取指定类型文档的问题数量
let thisMonthIssuesCount = 0;
let lastMonthIssuesCount = 0;
// 根据 reviewType 设置要查询的文档类型
if (reviewType === 'contract') {
// 合同类型 - 直接查询类型 1
const typeToQuery = [1];
// 调用数据库函数获取本月指定类型的问题数量
const thisMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
postgrestPost('rpc/count_evaluation_results_by_type', {
start_time: startOfThisMonth,
end_time: endOfThisMonth,
type_val: typeToQuery,
userid: parseInt(userId as string)
}, token),
'获取合同本月问题数据失败',
[]
);
// 本月问题数量
thisMonthIssuesCount = thisMonthIssuesResponse[0]?.count || 0;
// 调用数据库函数获取上月指定类型的问题数量
const lastMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
postgrestPost('rpc/count_evaluation_results_by_type', {
start_time: startOfLastMonth,
end_time: endOfLastMonth,
type_val: typeToQuery,
userid: parseInt(userId as string)
}, token),
'获取上月问题数据失败',
[]
);
// 上月问题数量
lastMonthIssuesCount = lastMonthIssuesResponse[0]?.count || 0;
} else if (reviewType === 'record') {
// 记录类型 - 需要查询类型 2 和类型 3,并合并结果
const typeToQuery = [2,3];
const thisMonthType2Response = await handleApiResponse<{ count: number }[]>(
postgrestPost('rpc/count_evaluation_results_by_type', {
start_time: startOfThisMonth,
end_time: endOfThisMonth,
type_val: typeToQuery,
userid: parseInt(userId as string)
}, token),
'获取本月许可卷宗类型2问题数据失败',
[]
);
// 本月两种类型的问题数量
const thisMonthType2Count = thisMonthType2Response[0]?.count || 0;
thisMonthIssuesCount = thisMonthType2Count
// 上月两种类型的问题数量
const lastMonthType2Response = await handleApiResponse<{ count: number }[]>(
postgrestPost('rpc/count_evaluation_results_by_type', {
start_time: startOfLastMonth,
end_time: endOfLastMonth,
type_val: typeToQuery,
userid: parseInt(userId as string)
}, token),
'获取上月许可卷宗类型2问题数据失败',
[]
);
// 上月两种类型的问题数量
const lastMonthType2Count = lastMonthType2Response[0]?.count || 0;
lastMonthIssuesCount = lastMonthType2Count
}
// 计算问题数量同比增长
let issuesGrowthValue = 0;
let issuesGrowthIsUp = true;
if (lastMonthIssuesCount > 0) {
const issuesGrowth = ((thisMonthIssuesCount - lastMonthIssuesCount) / lastMonthIssuesCount) * 100;
issuesGrowthValue = Math.abs(parseFloat(issuesGrowth.toFixed(1)));
issuesGrowthIsUp = issuesGrowth >= 0;
}else if(lastMonthIssuesCount == 0 && thisMonthIssuesCount > 0){
issuesGrowthValue = 100;
issuesGrowthIsUp = true;
}
// 返回统计结果
// 🔑 将后端响应(蛇形命名)转换为前端格式(驼峰命名)
return {
todayPendingFiles,
monthlyReviewedFiles,
todayPendingFiles: backendData.today_pending_files,
monthlyReviewedFiles: backendData.monthly_reviewed_files,
monthlyReviewGrowth: {
value: reviewGrowthValue,
isUp: reviewGrowthIsUp
value: backendData.monthly_review_growth.value,
isUp: backendData.monthly_review_growth.is_up
},
monthlyPassRate,
monthlyPassRate: backendData.monthly_pass_rate,
passRateGrowth: {
value: passRateGrowthValue,
isUp: passRateGrowthIsUp
value: backendData.pass_rate_growth.value,
isUp: backendData.pass_rate_growth.is_up
},
issuesDetected: thisMonthIssuesCount,
issuesDetected: backendData.issues_detected,
issuesGrowth: {
value: issuesGrowthValue,
isUp: issuesGrowthIsUp
value: backendData.issues_growth.value,
isUp: backendData.issues_growth.is_up
}
};
} catch (error) {
@@ -486,6 +196,15 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
}
}
/**
* 地区配置类型定义
*/
export interface AreaConfig {
area: string; // 地区名称
enabled: boolean; // 是否启用
sort_order: number; // 排序顺序
}
/**
* 入口模块类型定义
*/
@@ -494,7 +213,7 @@ export interface EntryModule {
name: string;
description: string | null;
path: string | null;
areas: string[];
areas: AreaConfig[]; // 修改为对象数组
created_at: string;
updated_at: string;
document_types?: Array<{
@@ -519,20 +238,11 @@ export async function getEntryModules(userRole: string | null | undefined, userA
// console.log('🔍 [getEntryModules] 查询地区:', userArea);
// 查询 entry_modules 表,筛选 areas 数组中包含用户地区的模块
// 使用 PostgreSQL JSONB 操作符 @> 检查数组是否包含值
// 查询 entry_modules 表,获取所有模块(在客户端进行过滤)
const params: PostgrestParams = {
select: 'id,name,description,path,areas,created_at,updated_at',
filter: {
// areas 数组中包含用户的 area
// areas: `cs.["${userArea}"]` // cs = contains (PostgreSQL @> 操作符)
}
filter: {}
};
if (userRole != 'provincial_admin'){
params.filter = {
areas: `cs.["${userArea}"]`
}
}
const modulesResponse = await postgrestGet('entry_modules', { ...params, token });
@@ -541,13 +251,38 @@ export async function getEntryModules(userRole: string | null | undefined, userA
return [];
}
const modules = extractApiData<EntryModule[]>(modulesResponse.data);
if (!modules || modules.length === 0) {
console.warn('⚠️ [getEntryModules] 未找到匹配的入口模块');
const allModules = extractApiData<EntryModule[]>(modulesResponse.data);
if (!allModules || allModules.length === 0) {
console.warn('⚠️ [getEntryModules] 未找到任何入口模块');
return [];
}
console.log(`✅ [getEntryModules] 找到 ${modules.length} 个入口模块`);
// 🔑 在客户端过滤:只保留包含用户地区且已启用的模块
const modules = allModules.filter(module => {
// 省级管理员可以看到所有模块
if (userRole === 'provincial_admin') {
return true;
}
// 检查 areas 数组中是否存在匹配的地区配置
if (!module.areas || !Array.isArray(module.areas)) {
return false;
}
// 查找用户地区的配置
const areaConfig = module.areas.find(config =>
config.area === userArea && config.enabled === true
);
return !!areaConfig; // 找到且启用才返回 true
});
if (modules.length === 0) {
console.warn('⚠️ [getEntryModules] 未找到已启用的入口模块');
return [];
}
console.log(`✅ [getEntryModules] 找到 ${modules.length} 个已启用的入口模块`);
// 为每个模块查询关联的 document_types
const modulesWithTypes = await Promise.all(
@@ -580,7 +315,7 @@ export async function getEntryModules(userRole: string | null | undefined, userA
})
);
console.log('✅ [getEntryModules] 入口模块数据(含文档类型):', JSON.stringify(modulesWithTypes));
// console.log('✅ [getEntryModules] 入口模块数据(含文档类型):', JSON.stringify(modulesWithTypes));
// 默认会多加一个 智慧法务大模型 入口 默认所有人都可以用,看到
modulesWithTypes.push({
@@ -598,7 +333,8 @@ export async function getEntryModules(userRole: string | null | undefined, userA
"code": "空"
}
]
})
}
)
return modulesWithTypes;
+29 -132
View File
@@ -455,15 +455,27 @@ export async function logout(request: Request) {
const accessToken = session.get("accessToken");
const appId = OAUTH_CONFIG.appId || 'idaasoauth2';
// 如果存在访问令牌,调用IDaaS单点登出
console.log("🚪 [Logout] 开始登出流程...");
console.log("🔑 [Logout] accessToken 存在:", !!accessToken);
console.log("📱 [Logout] appId:", appId);
// 如果存在访问令牌,调用IDaaS单点登出(仅 OAuth 登录用户)
if (accessToken && appId) {
console.log("🌐 [Logout] OAuth 用户,准备调用 IDaaS 单点登出...");
try {
await callIDaaSLogout(accessToken, appId);
console.log("IDaaS单点登出成功");
console.log("✅ [Logout] IDaaS单点登出成功");
} catch (error) {
console.error("IDaaS单点登出失败:", error);
console.error("❌ [Logout] IDaaS单点登出失败:");
console.error(" 错误详情:", error);
if (error instanceof Error) {
console.error(" 错误消息:", error.message);
console.error(" 错误堆栈:", error.stack);
}
// 即使IDaaS登出失败,也继续清除本地会话
}
} else {
console.log("️ [Logout] 管理员登录用户,无需调用 IDaaS 登出");
}
return new Response(null, {
@@ -487,6 +499,11 @@ async function callIDaaSLogout(accessToken: string, appId: string): Promise<void
const redirectUri = OAUTH_CONFIG.redirectUri || 'http://10.79.97.17/';
const logoutUrl = `${serverUrl}/public/sp/slo/${appId}`;
console.log("📡 [callIDaaSLogout] 准备发送登出请求:");
console.log(" 登出URL:", logoutUrl);
console.log(" 重定向URL:", redirectUri);
console.log(" accessToken:", accessToken ? `${accessToken.substring(0, 20)}...` : 'null');
const formData = new URLSearchParams();
formData.append('access_token', accessToken);
formData.append('redirect_url', encodeURIComponent(redirectUri));
@@ -498,13 +515,19 @@ async function callIDaaSLogout(accessToken: string, appId: string): Promise<void
},
});
console.log("IDaaS单点登出请求成功");
console.log("✅ [callIDaaSLogout] IDaaS单点登出请求成功");
console.log(" 响应状态:", response.status);
console.log(" 响应数据:", response.data);
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("调用IDaaS登出接口失败:", error.response?.status, error.response?.statusText);
console.error("❌ [callIDaaSLogout] 调用IDaaS登出接口失败:");
console.error(" HTTP状态:", error.response?.status);
console.error(" 状态文本:", error.response?.statusText);
console.error(" 响应数据:", error.response?.data);
console.error(" 请求配置:", error.config?.url, error.config?.method);
throw new Error(`IDaaS登出失败: ${error.response?.status} ${error.response?.statusText}`);
}
console.error("调用IDaaS登出接口失败:", error);
console.error("❌ [callIDaaSLogout] 调用IDaaS登出接口失败(非HTTP错误):", error);
throw error;
}
}
@@ -751,129 +774,3 @@ export async function getUserBySub(sub: string) {
};
}
}
/**
* 账号密码登录接口
*
* @param username - 用户名
* @param password - 密码
* @param redirectTo - 登录成功后重定向的URL
* @returns HTTP重定向响应或错误响应
*/
export async function simpleRootLogin(
username: string,
password: string,
redirectTo: string
) {
try {
// 输入验证
if (!username?.trim() || !password?.trim()) {
return new Response(JSON.stringify({
success: false,
error: "用户名和密码不能为空"
}), {
status: 400,
headers: { "Content-Type": "application/json" }
});
}
// 调用登录接口
const loginResponse = await axios.post(`${API_BASE_URL}/password_login`, {
sub: username.trim(),
password: password.trim()
}, {
headers: {
'Content-Type': 'application/json',
}
});
const loginResult = loginResponse.data;
console.log('登录接口返回', loginResult);
// 检查重试次数
const retryCount = loginResult.retryCount || loginResult.retry_count || 0;
console.log('登录重试次数:', retryCount);
if (loginResult.code === 0 && loginResult.data) {
// 登录成功,构建用户信息
const userData = loginResult.data;
// console.log('管理员登录userData', userData);
const userRole = userData.role; // 默认角色
// 生成模拟的OAuth token信息
const mockTokenExpiresIn = 7200; // 2小时
const mockAccessToken = `mock_access_token_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const mockRefreshToken = `mock_refresh_token_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 生成前端JWT
const jwtUserInfo: UserInfoForJWT = {
sub: userData.sub,
user_id: userData.user_id,
username: userData.username,
nick_name: userData.nick_name,
email: userData.email,
phone_number: userData.phone_number,
ou_id: userData.ou_id,
ou_name: userData.ou_name,
is_leader: userData.is_leader,
user_role: userRole
};
const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, mockTokenExpiresIn);
// 构建增强的用户信息对象
const enhancedUserInfo = {
...userData,
user_id: userData.user_id,
user_role: userRole,
frontend_jwt: frontendJWT
};
// 使用统一的session创建函数
return createUserSession({
isAuthenticated: true,
userRole: userRole,
redirectTo,
accessToken: mockAccessToken,
refreshToken: mockRefreshToken,
tokenExpiresIn: mockTokenExpiresIn,
userInfo: enhancedUserInfo,
frontendJWT
});
} else {
// 登录失败,检查账户是否被锁定
let errorMsg = loginResult.msg || "登录失败,请检查用户名和密码";
let isLocked = false;
// 检查是否因重试次数过多被锁定
if (retryCount >= 5) {
errorMsg = "账户已被锁定,密码错误次数过多,请联系管理员";
isLocked = true;
} else if (retryCount > 0) {
// 显示剩余尝试次数
const remainingAttempts = 5 - retryCount;
errorMsg = `${loginResult.msg || "用户名或密码错误"},还有 ${remainingAttempts} 次尝试机会`;
}
return new Response(JSON.stringify({
success: false,
error: errorMsg,
retryCount: retryCount,
isLocked: isLocked,
remainingAttempts: isLocked ? 0 : (5 - retryCount)
}), {
status: isLocked ? 403 : 401, // 403 表示禁止访问(账户被锁)
headers: { "Content-Type": "application/json" }
});
}
} catch (error) {
console.error("登录请求失败:", error);
return new Response(JSON.stringify({
success: false,
error: "登录请求失败,请稍后重试"
}), {
status: 500,
headers: { "Content-Type": "application/json" }
});
}
}
+73 -8
View File
@@ -276,13 +276,29 @@ export async function postgrestGet<T>(endpoint: string, params?: PostgrestParams
},
queryParams
);
if (response.error) {
// 🔑 检测令牌过期错误
const isTokenExpired = response.error.includes('令牌已过期') ||
response.error.includes('令牌') ||
response.error.includes('token') ||
response.error.includes('expired') ||
response.error.includes('认证') ||
response.error.includes('未授权');
if (isTokenExpired && typeof window !== 'undefined') {
console.error('🔑 [PostgREST Client - GET] 检测到令牌过期,清除会话并重定向到登录页');
localStorage.removeItem('access_token');
localStorage.removeItem('user_info');
sessionStorage.clear();
window.location.href = '/login?expired=true';
}
throw new Error(response.error);
}
// 返回数据和响应头
return {
return {
data: response.data as T,
headers: response.headers
};
@@ -421,6 +437,23 @@ export async function postgrestPost<T, D = Record<string, unknown>>(endpoint: st
if (response.error) {
console.error(`POST请求失败: ${response.error}`);
// 🔑 检测令牌过期错误
const isTokenExpired = response.error.includes('令牌已过期') ||
response.error.includes('令牌') ||
response.error.includes('token') ||
response.error.includes('expired') ||
response.error.includes('认证') ||
response.error.includes('未授权');
if (isTokenExpired && typeof window !== 'undefined') {
console.error('🔑 [PostgREST Client] 检测到令牌过期,清除会话并重定向到登录页');
localStorage.removeItem('access_token');
localStorage.removeItem('user_info');
sessionStorage.clear();
window.location.href = '/login?expired=true';
}
throw new Error(response.error);
}
@@ -548,15 +581,31 @@ export async function postgrestPut<T, D extends object>(
},
queryParams
);
if (response.error) {
// 🔑 检测令牌过期错误
const isTokenExpired = response.error.includes('令牌已过期') ||
response.error.includes('令牌') ||
response.error.includes('token') ||
response.error.includes('expired') ||
response.error.includes('认证') ||
response.error.includes('未授权');
if (isTokenExpired && typeof window !== 'undefined') {
console.error('🔑 [PostgREST Client - PATCH] 检测到令牌过期,清除会话并重定向到登录页');
localStorage.removeItem('access_token');
localStorage.removeItem('user_info');
sessionStorage.clear();
window.location.href = '/login?expired=true';
}
throw new Error(response.error);
}
if (!response.data) {
throw new Error('更新成功但未返回数据');
}
return { data: response.data };
} catch (error) {
const apiError = handleApiError(error);
@@ -595,11 +644,27 @@ export async function postgrestDelete<T>(endpoint: string, params?: PostgrestPar
},
queryParams
);
if (response.error) {
// 🔑 检测令牌过期错误
const isTokenExpired = response.error.includes('令牌已过期') ||
response.error.includes('令牌') ||
response.error.includes('token') ||
response.error.includes('expired') ||
response.error.includes('认证') ||
response.error.includes('未授权');
if (isTokenExpired && typeof window !== 'undefined') {
console.error('🔑 [PostgREST Client - DELETE] 检测到令牌过期,清除会话并重定向到登录页');
localStorage.removeItem('access_token');
localStorage.removeItem('user_info');
sessionStorage.clear();
window.location.href = '/login?expired=true';
}
throw new Error(response.error);
}
return { data: response.data as T };
} catch (error) {
const apiError = handleApiError(error);