Files
leaudit-platform-frontend/app/api/home/home.ts
T
LiangShiyong dab0835605 feat: 1.修改提示词模板的不用角色的操作权限。
2. 对接数据看板的数据。
3. 添加入口模块管理的页面。
2025-11-21 17:16:07 +08:00

793 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { postgrestGet, postgrestPost, type PostgrestParams } from "../postgrest-client";
import { apiRequest } from "../axios-client";
import dayjs from 'dayjs';
/**
* 从不同格式的 API 响应中提取数据
* @param responseData API 响应数据
* @returns 提取后的数据或 null
*/
function extractApiData<T>(responseData: unknown): T | null {
if (!responseData) {
console.warn('API响应数据为空');
return null;
}
try {
// 检查是否有错误信息
if (typeof responseData === 'object' && responseData !== null) {
// 错误检查: 检查错误码,一般成功的错误码是0或200
if ('code' in responseData) {
const code = (responseData as { code: number }).code;
// 如果有错误码且不是成功状态
if (code !== 0 && code !== 200) {
const errorMsg = 'msg' in responseData
? (responseData as { msg: string }).msg
: '未知错误';
console.error(`API响应错误: [${code}] ${errorMsg}`);
return null;
}
}
// 错误检查: 检查是否包含错误消息但没有数据
if ('error' in responseData && (responseData as { error: unknown }).error) {
const error = (responseData as { error: unknown }).error;
console.error(`API响应包含错误: ${typeof error === 'string' ? error : JSON.stringify(error)}`);
return null;
}
// 格式1: { code: number, msg: string, data: T }
if ('data' in responseData) {
const data = (responseData as { data: unknown }).data;
if (!data) {
console.warn('API响应中的data字段为空');
return null;
}
return data as T;
}
}
// 格式2: 直接是数据对象
return responseData as T;
} catch (error) {
console.error('处理API响应数据时出错:', error);
return null;
}
}
/**
* 首页数据统计响应类型
*/
interface HomeStatistics {
todayPendingFiles: number;
monthlyReviewedFiles: number;
monthlyReviewGrowth: {
value: number;
isUp: boolean;
};
monthlyPassRate: number;
passRateGrowth: {
value: number;
isUp: boolean;
};
issuesDetected: number;
issuesGrowth: {
value: number;
isUp: boolean;
};
}
/**
* 通过传入的 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;
}
/**
* 获取主页数据
* @param reviewType 从客户端传入的 reviewType 值
* @param userId 用户ID
* @param token JWT token
* @returns 主页数据
*/
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;
}
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 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}`;
}
}
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 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;
}
// 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,
monthlyReviewGrowth: {
value: reviewGrowthValue,
isUp: reviewGrowthIsUp
},
monthlyPassRate,
passRateGrowth: {
value: passRateGrowthValue,
isUp: passRateGrowthIsUp
},
issuesDetected: thisMonthIssuesCount,
issuesGrowth: {
value: issuesGrowthValue,
isUp: issuesGrowthIsUp
}
};
} catch (error) {
console.error('获取首页数据失败:', error instanceof Error ? error.message : String(error));
// 返回默认值以防止页面崩溃
return {
todayPendingFiles: 0,
monthlyReviewedFiles: 0,
monthlyReviewGrowth: { value: 0, isUp: true },
monthlyPassRate: 0,
passRateGrowth: { value: 0, isUp: true },
issuesDetected: 0,
issuesGrowth: { value: 0, isUp: true }
};
}
}
/**
* 入口模块类型定义
*/
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 [];
}
}
/**
* 高频错误评查点数据类型
*/
export interface TopErrorPoint {
rank: number;
evaluation_point_id: number;
point_name: string;
error_user_count: number;
}
export interface TopErrorPointsResponse {
available: boolean; // 标记该模块是否可用(是否有权限访问)
total: number;
items: TopErrorPoint[];
}
/**
* 获取高频错误评查点 Top N
* @param limit 返回 Top N 条记录,默认 10
* @param startDate 开始时间(格式:YYYY-MM-DD
* @param endDate 结束时间(格式:YYYY-MM-DD
* @param typeId 文档类型ID数组
* @param token JWT token
* @returns 高频错误评查点列表
*/
export async function getTopErrorPoints(
limit: number = 10,
startDate?: string,
endDate?: string,
typeId?: number[],
token?: string
): Promise<TopErrorPointsResponse> {
try {
console.log('🔍 [getTopErrorPoints] 请求参数:', { limit, startDate, endDate, typeId, hasToken: !!token });
// 构建查询参数
const params: Record<string, string | number | number[]> = {
limit: limit
};
if (startDate) {
params.start_date = startDate;
}
if (endDate) {
params.end_date = endDate;
}
if (typeId && typeId.length > 0) {
// 直接传递数组,axios 会自动处理序列化
params.type_id = typeId;
}
// 构建请求配置
const requestOptions: { method: string; headers?: Record<string, string> } = {
method: 'GET'
};
// 只有在显式传入 token 时才添加 Authorization header
// 否则让 axios 拦截器自动处理(从 localStorage 获取)
if (token) {
requestOptions.headers = {
'Authorization': `Bearer ${token}`
};
}
// 调用 API
const response = await apiRequest<TopErrorPointsResponse>(
'/admin/statistics/top-error-points',
requestOptions,
params
);
if (response.error) {
console.error('❌ [getTopErrorPoints] 获取高频错误评查点失败:', response.error);
// 请求失败(如权限不足),标记为不可用
return { available: false, total: 0, items: [] };
}
console.log('✅ [getTopErrorPoints] 成功获取高频错误评查点数据:', response.data);
// 请求成功,标记为可用(即使数据为空)
const data = response.data || { total: 0, items: [] };
return { available: true, ...data };
} catch (error) {
console.error('❌ [getTopErrorPoints] 获取高频错误评查点异常:', error instanceof Error ? error.message : String(error));
// 请求异常,标记为不可用
return { available: false, total: 0, items: [] };
}
}
/**
* 高风险用户数据类型
*/
export interface TopRiskUser {
rank: number;
user_id: number;
user_name: string;
department: string;
total_errors: number;
avg_errors_per_doc: number;
}
export interface TopRiskUsersResponse {
available: boolean; // 标记该模块是否可用(是否有权限访问)
total: number;
items: TopRiskUser[];
}
/**
* 获取高风险用户 Top N
* @param limit 返回 Top N 条记录,默认 5
* @param startDate 开始时间(格式:YYYY-MM-DD
* @param endDate 结束时间(格式:YYYY-MM-DD
* @param typeId 文档类型ID数组
* @param token JWT token
* @returns 高风险用户列表
*/
export async function getTopRiskUsers(
limit: number = 5,
startDate?: string,
endDate?: string,
typeId?: number[],
token?: string
): Promise<TopRiskUsersResponse> {
try {
console.log('🔍 [getTopRiskUsers] 请求参数:', { limit, startDate, endDate, typeId, hasToken: !!token });
// 构建查询参数
const params: Record<string, string | number | number[]> = {
limit: limit
};
if (startDate) {
params.start_date = startDate;
}
if (endDate) {
params.end_date = endDate;
}
if (typeId && typeId.length > 0) {
// 直接传递数组,axios 会自动处理序列化
params.type_id = typeId;
}
// 构建请求配置
const requestOptions: { method: string; headers?: Record<string, string> } = {
method: 'GET'
};
// 只有在显式传入 token 时才添加 Authorization header
// 否则让 axios 拦截器自动处理(从 localStorage 获取)
if (token) {
requestOptions.headers = {
'Authorization': `Bearer ${token}`
};
}
// 调用 API
const response = await apiRequest<TopRiskUsersResponse>(
'/admin/statistics/top-risk-users',
requestOptions,
params
);
if (response.error) {
console.error('❌ [getTopRiskUsers] 获取高风险用户失败:', response.error);
// 请求失败(如权限不足),标记为不可用
return { available: false, total: 0, items: [] };
}
console.log('✅ [getTopRiskUsers] 成功获取高风险用户数据:', response.data);
// 请求成功,标记为可用(即使数据为空)
const data = response.data || { total: 0, items: [] };
return { available: true, ...data };
} catch (error) {
console.error('❌ [getTopRiskUsers] 获取高风险用户异常:', error instanceof Error ? error.message : String(error));
// 请求异常,标记为不可用
return { available: false, total: 0, items: [] };
}
}