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
+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;