1096 lines
42 KiB
TypeScript
1096 lines
42 KiB
TypeScript
import { postgrestGet, type PostgrestParams, postgrestPut, postgrestPost } from "../postgrest-client";
|
||
import { getDocument } from "~/api/files/documents";
|
||
import dayjs from "dayjs";
|
||
import { getUserSession } from "~/api/login/auth.server";
|
||
import { apiRequest } from "../axios-client";
|
||
// import { formatDate } from "~/utils";
|
||
|
||
/**
|
||
* 从不同格式的 API 响应中提取数据
|
||
* @param responseData API 响应数据
|
||
* @returns 提取后的数据或 null
|
||
*/
|
||
function extractApiData<T>(responseData: unknown): T | null {
|
||
if (!responseData) return null;
|
||
|
||
// 格式1: { code: number, msg: string, data: T }
|
||
if (typeof responseData === 'object' && responseData !== null &&
|
||
'code' in responseData &&
|
||
'data' in responseData &&
|
||
(responseData as { data: unknown }).data) {
|
||
return (responseData as { data: T }).data;
|
||
}
|
||
|
||
// 格式2: 直接是数据对象
|
||
return responseData as T;
|
||
}
|
||
|
||
// 定义评查结果类型
|
||
interface EvaluationResult {
|
||
id: string | number;
|
||
document_id: string | number;
|
||
evaluation_point_id: string | number;
|
||
evaluated_results?: {
|
||
result?: boolean;
|
||
message?: string;
|
||
data?: string;
|
||
[key: string]: unknown;
|
||
};
|
||
evaluated_point_results_log?: {
|
||
rules?: unknown[];
|
||
[key: string]: unknown;
|
||
};
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
// 定义评查点类型
|
||
interface EvaluationPoint {
|
||
id: string | number;
|
||
evaluation_point_groups_id: string | number;
|
||
suggestion_message_type?: string;
|
||
suggestion_message?: string;
|
||
score?: number;
|
||
updated_at?: string;
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
// 定义审核状态类型
|
||
interface AuditStatus {
|
||
id: string | number;
|
||
document_id: string | number;
|
||
evaluation_point_id: string | number;
|
||
edit_audit_status: number;
|
||
message: string;
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
// 定义评查点组类型
|
||
interface EvaluationPointGroup {
|
||
id: string | number;
|
||
name?: string;
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
// 定义前端使用的评查点结果类型
|
||
interface ReviewPointResult {
|
||
id: string | number;
|
||
title: string;
|
||
pointCode?: string;
|
||
groupName: string;
|
||
status: string;
|
||
content: string;
|
||
suggestion: string;
|
||
result?: boolean;
|
||
score: number;
|
||
// evaluatedPointResultsLog: Record<string, Array<Record<string, unknown>>>;
|
||
evaluatedPointResultsLog: Record<string, unknown>;
|
||
}
|
||
|
||
// 定义统计数据类型
|
||
interface StatsData {
|
||
total: number;
|
||
success: number;
|
||
warning: number;
|
||
error: number;
|
||
notApplicable?: number;
|
||
score: number;
|
||
}
|
||
|
||
// GraphRAG Scored 评查结果类型
|
||
interface FieldScore {
|
||
field_path: string;
|
||
evaluation_as: string;
|
||
weight: number;
|
||
scored: number;
|
||
max_score: number;
|
||
status: string; // 'filled' | 'placeholder'
|
||
value: string;
|
||
page?: string;
|
||
ai_feedback?: string;
|
||
}
|
||
|
||
interface ScoredEvaluationResult {
|
||
evaluation_point_id: number;
|
||
code: string;
|
||
name: string;
|
||
passed: boolean;
|
||
machine_score: number;
|
||
score: number;
|
||
percentage: number;
|
||
total_score: number;
|
||
total_weight: number;
|
||
pass_threshold: number;
|
||
result_type: 'scored';
|
||
field_results: FieldScore[];
|
||
missing_fields?: string[];
|
||
ai_suggestion?: string;
|
||
}
|
||
|
||
interface EvaluationSummary {
|
||
total_points: number;
|
||
passed_count: number;
|
||
failed_count: number;
|
||
total_score: number;
|
||
total_full_score: number;
|
||
average_percentage: number;
|
||
}
|
||
|
||
interface UnifiedEvaluationResponse {
|
||
document_id: number;
|
||
flow_type: 'graphrag' | 'legacy';
|
||
results: ScoredEvaluationResult[];
|
||
summary: EvaluationSummary;
|
||
evaluated_at: string;
|
||
}
|
||
|
||
// 在文件顶部添加的类型定义,在interface区块前添加
|
||
interface OcrDataResult {
|
||
pages?: number[];
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
interface OcrData {
|
||
ocr_result?: Record<string, OcrDataResult>;
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
interface ContractStructureComparison {
|
||
id: string | number;
|
||
document_id: string | number;
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
// 定义评分提案数据接口
|
||
interface ScoringProposal {
|
||
id: string | number;
|
||
evaluation_result_id: string | number;
|
||
proposer_id: string | number;
|
||
proposed_score: number;
|
||
reason: string;
|
||
status: string;
|
||
created_at: string;
|
||
updated_at: string;
|
||
document_id: string | number;
|
||
}
|
||
|
||
/** ============== (废弃,已经采用api接口的方式进行查询)
|
||
* 获取当前评查文件的所有评查点结果
|
||
* @param fileId 评查文件ID
|
||
* @param request Remix请求对象,用于获取用户会话
|
||
* @returns 评查点结果列表和统计数据
|
||
*/
|
||
export async function getReviewPoints(fileId: string, request: Request) {
|
||
// 获取用户会话信息
|
||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||
|
||
if (!userInfo?.user_id) {
|
||
console.error("用户身份验证失败");
|
||
return { error: '用户身份验证失败', status: 401 };
|
||
}
|
||
|
||
const userId = userInfo.user_id.toString();
|
||
|
||
// 首先获取当前文档详情,优先走新后端接口
|
||
const documentData = await getDocument(fileId, userId, frontendJWT);
|
||
if (documentData.error) {
|
||
console.error("获取文档数据错误:", documentData.error);
|
||
return Response.json({ error: documentData.error }, { status: documentData.status || 500 });
|
||
}
|
||
|
||
// 其次需要查询这个文档关联的文档附件,查询contract_structure_comparison表
|
||
const contractStructureComparisonParams: PostgrestParams = {
|
||
select: '*',
|
||
filter: {
|
||
'document_id': `eq.${fileId}`
|
||
},
|
||
order: 'id.desc',
|
||
limit: 1,
|
||
token: frontendJWT
|
||
};
|
||
const contractStructureComparisonResponse = await postgrestGet('/api/postgrest/proxy/contract_structure_comparison', contractStructureComparisonParams);
|
||
// console.log('contract_structure_comparison', contractStructureComparisonResponse)
|
||
|
||
let contractStructureComparisonData: ContractStructureComparison[] | null = null;
|
||
if (contractStructureComparisonResponse.error) {
|
||
console.warn("获取文档附件数据失败,降级为空附件对比数据:", contractStructureComparisonResponse.error);
|
||
} else {
|
||
contractStructureComparisonData = extractApiData<ContractStructureComparison[]>(contractStructureComparisonResponse.data);
|
||
}
|
||
|
||
// console.log('文档附件的数据', JSON.stringify(contractStructureComparisonData, null, 2));
|
||
|
||
// 解析比对结果
|
||
let comparisonDocument = null;
|
||
if (contractStructureComparisonData && contractStructureComparisonData.length > 0) {
|
||
comparisonDocument = contractStructureComparisonData[0];
|
||
// 测试:将合同封面中的status改为abnormal
|
||
// (comparisonDocument.comparison_results as Record<string, Array<{ status: string; [key: string]: unknown }>>)['合同封面'][0]['status'] = 'abnormal';
|
||
// 如果 comparison_results 是字符串,尝试解析为 JSON
|
||
if (comparisonDocument.comparison_results && typeof comparisonDocument.comparison_results === 'string') {
|
||
try {
|
||
comparisonDocument.comparison_results = JSON.parse(comparisonDocument.comparison_results);
|
||
} catch (e) {
|
||
console.error('解析比对结果失败:', e);
|
||
comparisonDocument.comparison_results = null;
|
||
}
|
||
}
|
||
}else{
|
||
comparisonDocument = {
|
||
template_contract_path: '',
|
||
}
|
||
}
|
||
|
||
|
||
// console.log('documentData-------', documentData);
|
||
// 步骤1:根据fileId查询evaluation_results表
|
||
const evaluationResultsParams: PostgrestParams = {
|
||
select: '*',
|
||
filter: {
|
||
'document_id': `eq.${fileId}`
|
||
},
|
||
token: frontendJWT
|
||
};
|
||
const evaluationResultsResponse = await postgrestGet('/api/postgrest/proxy/evaluation_results', evaluationResultsParams);
|
||
|
||
// console.log('evaluationResultsResponse-------', evaluationResultsResponse,);
|
||
if (evaluationResultsResponse.error) {
|
||
return { error: evaluationResultsResponse.error, status: evaluationResultsResponse.status };
|
||
}
|
||
|
||
// 评查结果数据
|
||
const evaluationResultsData = extractApiData<EvaluationResult[]>(evaluationResultsResponse.data) || [];
|
||
|
||
if (Array.isArray(evaluationResultsData) && evaluationResultsData.length <= 0) {
|
||
return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 },error: '获取评查结果数据失败' };
|
||
}
|
||
|
||
// 收集所有评查点ID,用于查询评查点详情
|
||
const evaluationPointIds = evaluationResultsData.map(item => item.evaluation_point_id).filter(Boolean);
|
||
|
||
if (evaluationPointIds.length === 0) {
|
||
return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 },error: '获取评查点ID失败' };
|
||
}
|
||
|
||
// 步骤2:根据evaluation_point_id查询evaluation_points表
|
||
const evaluationPointsParams: PostgrestParams = {
|
||
select: '*',
|
||
filter: {
|
||
'id': `in.(${evaluationPointIds.join(',')})`
|
||
},
|
||
token: frontendJWT
|
||
};
|
||
const evaluationPointsResponse = await postgrestGet('/api/postgrest/proxy/evaluation_points', evaluationPointsParams);
|
||
|
||
if (evaluationPointsResponse.error) {
|
||
return { error: evaluationPointsResponse.error, status: evaluationPointsResponse.status };
|
||
}
|
||
|
||
const evaluationPointsData = extractApiData<EvaluationPoint[]>(evaluationPointsResponse.data);
|
||
|
||
if (!evaluationPointsData || !Array.isArray(evaluationPointsData)) {
|
||
return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 } };
|
||
}
|
||
|
||
// 收集所有评查点组ID,用于查询评查点组详情
|
||
const groupIds = evaluationPointsData.map(item => item.evaluation_point_groups_id).filter(Boolean);
|
||
|
||
if (groupIds.length === 0) {
|
||
return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 } };
|
||
}
|
||
|
||
// 步骤3:查询评查点组
|
||
const groupsParams: PostgrestParams = {
|
||
select: '*',
|
||
filter: {
|
||
'id': `in.(${groupIds.join(',')})`
|
||
},
|
||
token: frontendJWT
|
||
};
|
||
const groupsResponse = await postgrestGet('/api/postgrest/proxy/evaluation_point_groups', groupsParams);
|
||
|
||
if (groupsResponse.error) {
|
||
return { error: groupsResponse.error, status: groupsResponse.status };
|
||
}
|
||
|
||
const groupsData = extractApiData<EvaluationPointGroup[]>(groupsResponse.data);
|
||
|
||
if (!groupsData || !Array.isArray(groupsData)) {
|
||
return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 } };
|
||
}
|
||
|
||
// 步骤4:从audit_status表中 获取 需人工审核 的那些评查点的数据
|
||
// console.log('evaluationPointsData1112------', evaluationPointsData.find(point => point.post_action === 'manual'));
|
||
const manualReviewPoints = evaluationPointsData.filter(point => point.post_action === 'manual');
|
||
const manualReviewPointsIds = manualReviewPoints.map(point => point.id);
|
||
const manualReviewPointsParams: PostgrestParams = {
|
||
select: '*',
|
||
filter: {
|
||
'document_id': `eq.${fileId}`,
|
||
'evaluation_point_id': `in.(${manualReviewPointsIds.join(',')})`
|
||
},
|
||
token: frontendJWT
|
||
};
|
||
const manualReviewPointsResponse = await postgrestGet('/api/postgrest/proxy/audit_status', manualReviewPointsParams);
|
||
let manualReviewPointsData: AuditStatus[] | null = null;
|
||
if (manualReviewPointsResponse.error) {
|
||
console.warn('获取人工审核状态失败,降级为空审核状态:', manualReviewPointsResponse.error);
|
||
} else {
|
||
manualReviewPointsData = extractApiData<AuditStatus[]>(manualReviewPointsResponse.data);
|
||
}
|
||
|
||
// 构建评查点ID到editAuditStatus的映射
|
||
const editAuditStatusMap = new Map<string | number, {id: string | number, status: number, message: string}>();
|
||
|
||
// 如果有查询结果,则根据evaluation_point_id索引到对应数据
|
||
if (manualReviewPointsData && Array.isArray(manualReviewPointsData)) {
|
||
manualReviewPointsData.forEach(auditStatus => {
|
||
if (auditStatus.evaluation_point_id && auditStatus.edit_audit_status !== undefined) {
|
||
editAuditStatusMap.set(auditStatus.evaluation_point_id, {id: auditStatus.id, status: auditStatus.edit_audit_status, message: auditStatus.message});
|
||
}
|
||
});
|
||
}
|
||
|
||
// 为没有对应audit_status记录的manual类型评查点设置默认值0
|
||
if (manualReviewPointsIds.length > 0) {
|
||
manualReviewPointsIds.forEach(pointId => {
|
||
if (!editAuditStatusMap.has(pointId)) {
|
||
editAuditStatusMap.set(pointId, {id: '', status: 0, message: ''});
|
||
}
|
||
});
|
||
}
|
||
|
||
// console.log('manualReviewPoints-------', manualReviewPoints);
|
||
|
||
|
||
// 创建映射关系以便快速查找
|
||
const pointsMap = new Map<string | number, EvaluationPoint>();
|
||
evaluationPointsData.forEach(point => {
|
||
pointsMap.set(point.id, point);
|
||
});
|
||
|
||
// console.log('pointsMap-------', pointsMap);
|
||
|
||
const groupsMap = new Map<string | number, EvaluationPointGroup>();
|
||
groupsData.forEach(group => {
|
||
groupsMap.set(group.id, group);
|
||
});
|
||
|
||
// console.log('groupsMap-------', groupsMap);
|
||
|
||
|
||
//从cross_scoring_proposals表中获取评分提案数据,用于交叉评查
|
||
const scoringProposalsParams: PostgrestParams = {
|
||
select: '*',
|
||
filter: {
|
||
'document_id': `eq.${fileId}`,
|
||
'deleted_at': `is.null`
|
||
},
|
||
token: frontendJWT
|
||
};
|
||
const scoringProposalsResponse = await postgrestGet('/api/postgrest/proxy/cross_scoring_proposals', scoringProposalsParams);
|
||
|
||
let scoringProposalsData: ScoringProposal[] = [];
|
||
if (scoringProposalsResponse.error) {
|
||
console.warn('获取评分提案失败,降级为空提案数据:', scoringProposalsResponse.error);
|
||
} else {
|
||
scoringProposalsData = extractApiData<ScoringProposal[]>(scoringProposalsResponse.data) || [];
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
// 构建前端所需的数据格式
|
||
const resultData: ReviewPointResult[] = evaluationResultsData.map(result => {
|
||
const point = pointsMap.get(result.evaluation_point_id) || {} as EvaluationPoint;
|
||
const group = groupsMap.get(point.evaluation_point_groups_id || 0) || {} as EvaluationPointGroup;
|
||
const editAuditStatus = editAuditStatusMap.get(result.evaluation_point_id) || {id: '', status: 0, message: ''};
|
||
|
||
// 评查结果内容改成由evaluated_point_results_log中获取
|
||
const evaluatedPointResultsLog = result.evaluated_point_results_log || {};
|
||
// console.log('evaluatedPointResultsLog-------', evaluatedPointResultsLog);
|
||
|
||
|
||
// 从 evaluated_results 中提取数据
|
||
let message = '';
|
||
let data = '';
|
||
|
||
if (result.evaluated_results && typeof result.evaluated_results === 'object') {
|
||
message = result.evaluated_results.message || '';
|
||
data = result.evaluated_results.data || '';
|
||
}
|
||
|
||
// 提取页码数组
|
||
let contentPage: Record<string, string> = {};
|
||
// console.log('result-------', result.evaluated_results?.result);
|
||
// console.log('datacontent-------', data);
|
||
// console.log('documentData-------', documentData);
|
||
if (data && typeof data === 'object') {
|
||
// 4-22 更改数据结构:通过拿到的data数据(每一个key对应一个object),将object中的page提取出来
|
||
try{
|
||
const dataObj = data as Record<string, {page: number | string,value: string}>;
|
||
for (const key in dataObj) {
|
||
if (Object.prototype.hasOwnProperty.call(dataObj, key)) {
|
||
let newPage = dataObj[key].page.toString();
|
||
// 如果newPage里面有文本,则把文本去掉
|
||
if(newPage.match(/\d+/g)){
|
||
newPage = newPage.match(/^\d+/g)?.map(Number).join('') || '';
|
||
}
|
||
contentPage[key] = newPage;
|
||
}
|
||
|
||
// 如果contentPage[key]为空,则需要根据这个key去ocrResult中找到对应的key,然后根据ocrResult中的pages数组,找到对应的页码
|
||
if(!contentPage[key]){
|
||
// 分割key获取数组的第一位
|
||
const keyArray = key.split('-');
|
||
const ocrResult = documentData?.data?.ocrResult as OcrData;
|
||
const pages = ocrResult?.ocr_result?.[keyArray[0]]?.pages;
|
||
contentPage[key] = pages?.[0]?.toString() || '';
|
||
}
|
||
}
|
||
}
|
||
catch (e) {
|
||
console.error('解析评查点data失败:', e);
|
||
contentPage = {};
|
||
}
|
||
}
|
||
|
||
return {
|
||
id: result.id,
|
||
documentId: fileId,
|
||
pointId: point.id,
|
||
editAuditStatusId: editAuditStatus.id,
|
||
editAuditStatus: editAuditStatus.status,
|
||
editAuditStatusMessage: editAuditStatus.message,
|
||
title: message,
|
||
pointName: point.name || '',
|
||
pointCode: String(point.code || ''),
|
||
groupName: group.name || '',
|
||
|
||
status: point.suggestion_message_type || '', //评查点的评查结果状态
|
||
// status: 'error', //评查点的评查结果状态
|
||
|
||
content: data,
|
||
|
||
contentPage: contentPage,
|
||
|
||
suggestion: point.suggestion_message || '',
|
||
// suggestion: '只是给建议的修改内容',
|
||
|
||
result: result.evaluated_results?.result, // 记录评查结果,用于统计
|
||
// score是评查点设置的满分的分数
|
||
score: point.score || 0,
|
||
|
||
// finalScore是评查点对应评查结果最终所得的分数 用户交叉评查时使用,可能为null
|
||
finalScore: result.final_score,
|
||
|
||
// machineScore是评查点对应评查结果机器进行的评分,交叉评查时使用,实际上对应的是评查点设置好的分数
|
||
machineScore: result.machine_score,
|
||
|
||
postAction: point.post_action || '',
|
||
// postAction: 'manual',
|
||
|
||
actionContent: point.action_config || '',
|
||
// actionContent: '用户提前在评查点输入过的修改内容',
|
||
|
||
legalBasis: point.references_laws || {},
|
||
// legalBasis: {
|
||
// name: '中华人民共和国食品安全法',
|
||
// content: '中华人民共和国食品安全法',
|
||
// article: [
|
||
// {
|
||
// name: '中华人民共和国食品安全法',
|
||
// content: '中华人民共和国食品安全法'
|
||
// }
|
||
// ]
|
||
// }
|
||
|
||
// 评查配置: point.evaluation_config
|
||
evaluationConfig: point.evaluation_config || {},
|
||
|
||
// 评查点evaluation_point中的fail_message和pass_message 用于交叉评查的提出意见
|
||
failMessage: point.fail_message || '',
|
||
passMessage: point.pass_message || '',
|
||
|
||
evaluatedPointResultsLog: evaluatedPointResultsLog || {}
|
||
// evaluatedPointResultsLog: {
|
||
// rules:[
|
||
// {
|
||
// "id": "0",
|
||
// "type": "consistency",
|
||
// "res": true,
|
||
// "config": {
|
||
// "logic": "all",
|
||
// "pairs": [
|
||
// {
|
||
// "sourceField": {"证据先行登记保存批准书-负责人意见并签名-时间": {page: 1,value: ''}},
|
||
// "targetField": {"证据先行登记保存批准书-负责人意见并签名-签名": {page: 2,value: '有无判断类型'}},
|
||
// "compareMethod": "exact",
|
||
// "res": true
|
||
// }
|
||
// ]
|
||
// }
|
||
// },
|
||
// {
|
||
// "id": "1",
|
||
// "type": "consistency",
|
||
// "res": false,
|
||
// "config": {
|
||
// "logic": "and",
|
||
// "pairs": [
|
||
// {
|
||
// "sourceField": {"a":{page: 1,value: '张三拉萨看得见佛i啊是觉得离开房间啊善良的是的链接发了上帝就发垃圾袋的时间佛爱上立刻就阿拉山口大家分厘卡即使灯笼裤飞机啊顺利打开解放拉萨酱豆腐立刻阿萨到了经历多空双方叫阿里的肌肤垃圾收到了看见螺丝钉解放了啊撒旦解放垃圾的等级分类教师劳动纠纷爱丽丝的开发教师的肌肤啊撒旦解放考虑进来阿斯兰的看法骄傲'}},
|
||
// "targetField": {"b":{page: 1,value: '张三'}},
|
||
// "compareMethod": "exact",
|
||
// "res": true
|
||
// },
|
||
// {
|
||
// "sourceField": {"b":{page: 1,value: '张三'}},
|
||
// "targetField": {"c":{page: 1,value: '张三拉萨看得见佛i啊是觉得离开房间啊善良的是的链接发了上帝就发垃圾袋的时间佛爱上立刻就阿拉山口大家分厘卡即使灯笼裤飞机啊顺利打开解放拉萨酱豆腐立刻阿萨到了经历多空双方叫阿里的肌肤垃圾收到了看见螺丝钉解放了啊撒旦解放垃圾的等级分类教师劳动纠纷爱丽丝的开发教师的肌肤啊撒旦解放考虑进来阿斯兰的看法骄傲'}},
|
||
// "compareMethod": "exact",
|
||
// "res": false
|
||
// },
|
||
// {
|
||
// "sourceField": {"c":{page: 1,value: '张三'}},
|
||
// "targetField": {"d":{page: 1,value: '张三'}},
|
||
// "compareMethod": "contains",
|
||
// "res": true
|
||
// },
|
||
// {
|
||
// "sourceField": {"d":{page: 1,value: '张三'}},
|
||
// "targetField": {"e":{page: 1,value: '张三'}},
|
||
// "compareMethod": "exact",
|
||
// "res": true
|
||
// },
|
||
// {
|
||
// "sourceField": {"现场笔录-被检查人名称":{page: 1,value: '张三'}},
|
||
// "targetField": {"证据复制(提取)单-营业执照-名称":{page: 1,value: '张三'}},
|
||
// "compareMethod": "exact",
|
||
// "res": true
|
||
// },
|
||
// {
|
||
// "sourceField": {"证据复制(提取)单-营业执照-名称":{page: 1,value: '张三'}},
|
||
// "targetField": {"证据复制(提取)单-营业执照-目录-名称":{page: 1,value: '张三'}},
|
||
// "compareMethod": "exact",
|
||
// "res": true
|
||
// },
|
||
// {
|
||
// "sourceField": {"现场笔录-法定代表人(负责人)":{page: 1,value: '张三'}},
|
||
// "targetField": {"证据复制(提取)单-营业执照-法定代表人":{page: 1,value: '张三'}},
|
||
// "compareMethod": "exact",
|
||
// "res": true
|
||
// },
|
||
// {
|
||
// "sourceField": {"现场笔录-烟草专卖许可证号码":{page: 1,value: '张三'}},
|
||
// "targetField": {"证据复制(提取)单-烟草专卖零售许可证-许可证号":{page: 1,value: '张三'}},
|
||
// "compareMethod": "exact",
|
||
// "res": true
|
||
// },
|
||
// {
|
||
// "sourceField": {"证据复制(提取)单-烟草专卖零售许可证-企业名称":{page: 1,value: '张三'}},
|
||
// "targetField": {"证据复制(提取)单-营业执照-名称":{page: 1,value: '张三拉萨看得见佛i啊是觉得离开房间啊善良的是的链接发了上帝就发垃圾袋的时间佛爱上立刻就阿拉山口大家分厘卡即使灯笼裤飞机啊顺利打开解放拉萨酱豆腐立刻阿萨到了经历多空双方叫阿里的肌肤垃圾收到了看见螺丝钉解放了啊撒旦解放垃圾的等级分类教师劳动纠纷爱丽丝的开发教师的肌肤啊撒旦解放考虑进来阿斯兰的看法骄傲'}},
|
||
// "compareMethod": "exact",
|
||
// "res": false
|
||
// },
|
||
// {
|
||
// "sourceField": {"证据复制(提取)单-烟草专卖零售许可证-负责人姓名":{page: 1,value: '张三'}},
|
||
// "targetField": {"证据复制(提取)单-营业执照-法定代表人":{page: 1,value: '张三'}},
|
||
// "compareMethod": "exact",
|
||
// "res": true
|
||
// },
|
||
// {
|
||
// "sourceField": {"立案报告表-当事人-单位-名称":{page: 1,value: '张三'}},
|
||
// "targetField": {"证据复制(提取)单-营业执照-名称":{page: 1,value: '张三'}},
|
||
// "compareMethod": "exact",
|
||
// "res": true
|
||
// },
|
||
// {
|
||
// "sourceField": {"立案报告表-当事人-单位-法定代表人(负责人)":{page: 1,value: '张三拉萨看得见佛i啊是觉得离开房间啊善良的是的链接发了上帝就发垃圾袋的时间佛爱上立刻就阿拉山口大家分厘卡即使灯笼裤飞机啊顺利打开解放拉萨酱豆腐立刻阿萨到了经历多空双方叫阿里的肌肤垃圾收到了看见螺丝钉解放了啊撒旦解放垃圾的等级分类教师劳动纠纷爱丽丝的开发教师的肌肤啊撒旦解放考虑进来阿斯兰的看法骄傲'}},
|
||
// "targetField": {"证据复制(提取)单-营业执照-法定代表人":{page: 1,value: '张三'}},
|
||
// "compareMethod": "exact",
|
||
// "res": true
|
||
// },
|
||
// {
|
||
// "sourceField": {"立案报告表-当事人-单位-地址":{page: 1,value: '张三'}},
|
||
// "targetField": {"证据复制(提取)单-营业执照-住所":{page: 1,value: '张三'}},
|
||
// "compareMethod": "exact",
|
||
// "res": true
|
||
// }
|
||
// ],
|
||
// "selectedFields": []
|
||
// }
|
||
// },
|
||
// {
|
||
// "id": "2",
|
||
// "type": "exists",
|
||
// "config": {
|
||
// "logic": "all",
|
||
// "res": true,
|
||
// "fields": {
|
||
// "证据先行登记保存批准书-负责人意见并签名-时间": {page: 1,value: ''},
|
||
// "证据先行登记保存批准书-负责人意见并签名-签名": {page: 2,value: '有无判断类型'}
|
||
// },
|
||
// }
|
||
// },
|
||
// {
|
||
// "id": "3",
|
||
// "type": "exists",
|
||
// "config": {
|
||
// "logic": "all",
|
||
// "res": false,
|
||
// "fields": {
|
||
// "证据先行登记-负责人意见并签名-时间": {page: 1,value: ''},
|
||
// "证据先行登记-负责人意见并签名-签名": {page: 2,value: '有无判断类型'}
|
||
// },
|
||
// }
|
||
// },
|
||
// {
|
||
// "id": "4",
|
||
// "type": "logic",
|
||
// "config": {
|
||
// "logic": "all",
|
||
// "res": true,
|
||
// "conditions": [
|
||
// {
|
||
// "field": {
|
||
// "送达回证-送达方式": {
|
||
// "page": 5,
|
||
// "value": "逻辑判断"
|
||
// }
|
||
// },
|
||
// "value": "直接送达",
|
||
// "operator": "eq",
|
||
// "res": true
|
||
// },
|
||
// {
|
||
// "field": {
|
||
// "犯罪证据-犯罪方式": {
|
||
// "page": 6,
|
||
// "value": "逻辑判断"
|
||
// }
|
||
// },
|
||
// "value": "直接送达",
|
||
// "operator": "eq",
|
||
// "res": false
|
||
// }
|
||
// ],
|
||
// "selectedFields": []
|
||
// }
|
||
// },
|
||
// {
|
||
// "id": "5",
|
||
// "type": "regex",
|
||
// "config": {
|
||
// "res": true,
|
||
// "field": {
|
||
// "广东省没收、收缴、追缴财务收据-标题":{
|
||
// page: 1,value: 'asdasdasd'
|
||
// }
|
||
// },
|
||
// "pattern": "^(.*广东省没收.*财务收据.*)",
|
||
// "matchType": "match",
|
||
// "selectedFields": []
|
||
// }
|
||
// },
|
||
// {
|
||
// "id": "6",
|
||
// "type": "format",
|
||
// "config": {
|
||
// "field": {
|
||
// "广东省没收、收缴、追缴财务收据-标题": {
|
||
// "value": "",
|
||
// "page": 8
|
||
// }
|
||
// },
|
||
// "formatType": "date",
|
||
// "parameters": "YYYY-MM-DD",
|
||
// "selectedFields": [],
|
||
// "res": false
|
||
// }
|
||
// },
|
||
// {
|
||
// "id": "7",
|
||
// "type": "ai",
|
||
// "config": {
|
||
// "res": false,
|
||
// "model": "qwen14b",
|
||
// "fields": {
|
||
// "涉案物件核价表-涉案物品价格-品种规格、单价": {
|
||
// "page": 1,
|
||
// "value": "规则和单价你都无法想象"
|
||
// },
|
||
// "涉案物件核价表-涉案物品种": {
|
||
// "page": 1,
|
||
// "value": "什么都有"
|
||
// },
|
||
// "涉案物件核价表-涉案": {
|
||
// "page": 19,
|
||
// "value": ""
|
||
// }
|
||
// },
|
||
// "prompt": "请判断以下{涉案物件核价表-涉案物品价格-品种规格、单价}各品种规格的数量、单价计算的合计金额是否正确,各品种规格合计金额计算总计金额是否正确,仅回答\"符合\"或\"不符合\",并简要说明理由。",
|
||
// "message": "缺少字段: 涉案物件核价表-涉案物品价格-品种规格、单价",
|
||
// "temperature": 0.1,
|
||
// "selectedFields": []
|
||
// }
|
||
// }
|
||
// ]}
|
||
};
|
||
});
|
||
|
||
// 统计数据
|
||
const stats: StatsData = {
|
||
total: evaluationResultsData.length,
|
||
success: 0,
|
||
warning: 0,
|
||
error: 0,
|
||
score: 0
|
||
};
|
||
|
||
// 计算统计数据
|
||
resultData.forEach(item => {
|
||
// 成功数量统计
|
||
if (item.result === true) {
|
||
stats.success += 1;
|
||
} else if (item.result === false) {
|
||
// 警告和错误数量统计
|
||
if (item.status === 'warning') {
|
||
stats.warning += 1;
|
||
} else if (item.status === 'error') {
|
||
stats.error += 1;
|
||
}
|
||
}
|
||
|
||
// 分数统计
|
||
stats.score += item.score || 0;
|
||
});
|
||
|
||
// 构建文件信息-评查信息的数据
|
||
// 找出最新的评查时间
|
||
let latestUpdatedAt = '';
|
||
evaluationResultsData.forEach(result => {
|
||
if (result.updated_at && (!latestUpdatedAt || result.updated_at > latestUpdatedAt)) {
|
||
latestUpdatedAt = result.updated_at.toString();
|
||
}
|
||
});
|
||
|
||
// 提取不重复的规则组名称
|
||
const uniqueGroups = Array.from(new Set(resultData.map(item => item.groupName))).filter(Boolean);
|
||
|
||
// 计算问题数量
|
||
const issueCount = stats.warning + stats.error;
|
||
|
||
// 构建评查信息对象
|
||
const reviewInfo = {
|
||
reviewTime: dayjs.utc(latestUpdatedAt).format('YYYY-MM-DD HH:mm:ss'),
|
||
reviewModel: 'DeepSeek',
|
||
ruleGroup: uniqueGroups.join('、'),
|
||
result: issueCount > 0 ? 'warning' : 'success',
|
||
issueCount: issueCount
|
||
};
|
||
// console.log("reviewInfo-------",JSON.stringify(reviewInfo,null,2));
|
||
// data->reviewPoints stats->statistics reviewInfo->reviewInfo document->document scoring_proposals->scoringProposalsData
|
||
// 构建 pointId -> code 映射(供 route loader 补充 pointCode,绕过 Vite tree-shake)
|
||
const pointCodeMap: Record<string, string> = {};
|
||
evaluationPointsData.forEach(point => {
|
||
if (point.code) pointCodeMap[String(point.id)] = String(point.code);
|
||
});
|
||
|
||
return { data: resultData, stats, reviewInfo, document: documentData.data, comparison_document: comparisonDocument, scoring_proposals: scoringProposalsData, pointCodeMap };
|
||
}
|
||
|
||
/**
|
||
* 更新评查结果
|
||
* @param resultId 评查结果ID
|
||
* @param editAuditStatusId 审核状态ID
|
||
* @param result 评查结果 (true/false/review)
|
||
* @param message 评查意见
|
||
* @param request Remix请求对象,用于获取用户会话
|
||
* @param documentId 文档ID(可选,用于创建新审核状态)
|
||
* @param evaluationPointId 评查点ID(可选,用于创建新审核状态)
|
||
* @returns 更新后的评查结果
|
||
*
|
||
* 🔥 接口文档: auth_doc/评查审核接口对接文档.md
|
||
* 📍 使用接口:
|
||
* - 3.1 更新评查结果: PATCH /admin/v2/evaluations/results/{result_id}
|
||
* - 3.2 创建审核状态: POST /admin/v2/evaluations/audit-status
|
||
* - 3.3 更新审核状态: PATCH /admin/v2/evaluations/audit-status/{audit_status_id}
|
||
*/
|
||
export async function updateReviewResult(
|
||
resultId: string,
|
||
editAuditStatusId: string | number,
|
||
result: string,
|
||
message: string,
|
||
request: Request,
|
||
documentId?: string | number,
|
||
evaluationPointId?: string | number
|
||
): Promise<{
|
||
data?: unknown;
|
||
error?: string;
|
||
status?: number;
|
||
}> {
|
||
try {
|
||
// 获取用户会话信息
|
||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||
|
||
if (!userInfo?.user_id) {
|
||
console.error("用户身份验证失败");
|
||
return { error: '用户身份验证失败', status: 401 };
|
||
}
|
||
|
||
if (!resultId) {
|
||
return { error: '评查结果ID不能为空', status: 400 };
|
||
}
|
||
|
||
const response = await apiRequest<{
|
||
code?: number;
|
||
message?: string;
|
||
data?: {
|
||
reviewPointResultId: number;
|
||
editAuditStatusId: number;
|
||
editAuditStatus: number;
|
||
overrideResult: boolean | null;
|
||
message: string;
|
||
};
|
||
}>(
|
||
`/api/v3/review-points/${resultId}/audit`,
|
||
{
|
||
method: 'PATCH',
|
||
headers: {
|
||
'Authorization': `Bearer ${frontendJWT}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
data: {
|
||
result,
|
||
message
|
||
}
|
||
}
|
||
);
|
||
|
||
if (response.error) {
|
||
console.error('❌ [updateReviewResult] 新接口调用失败:', response.error);
|
||
return { error: response.error, status: response.status || 500 };
|
||
}
|
||
|
||
return { data: response.data };
|
||
} catch (error) {
|
||
console.error('更新评查结果失败:', error);
|
||
return {
|
||
error: error instanceof Error ? error.message : '更新评查结果失败',
|
||
status: 500
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 确认评查结果并更新文档审核状态 只更新文档的审核状态为通过
|
||
* @param documentId 文档ID
|
||
* @param request Remix请求对象,用于获取用户会话
|
||
* @returns 更新结果
|
||
*
|
||
* 🔥 接口文档: auth_doc/评查审核接口对接文档.md 3.4
|
||
* 📍 API地址: PATCH /admin/v2/evaluation/documents/{document_id}/confirm
|
||
*/
|
||
export async function confirmReviewResults(documentId: string, request: Request): Promise<{
|
||
data?: { auditStatus: number; };
|
||
error?: string;
|
||
status?: number;
|
||
}> {
|
||
try {
|
||
// 获取用户会话信息
|
||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||
|
||
if (!userInfo?.user_id) {
|
||
console.error("用户身份验证失败");
|
||
return { error: '用户身份验证失败', status: 401 };
|
||
}
|
||
|
||
if (!documentId) {
|
||
return { error: '文档ID不能为空', status: 400 };
|
||
}
|
||
|
||
const response = await apiRequest<{
|
||
code?: number;
|
||
message?: string;
|
||
data: {
|
||
documentId?: number;
|
||
auditStatus?: number;
|
||
};
|
||
}>(
|
||
`/api/v3/documents/${documentId}/confirm`,
|
||
{
|
||
method: 'PATCH',
|
||
headers: {
|
||
'Authorization': `Bearer ${frontendJWT}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
}
|
||
);
|
||
|
||
// 处理错误响应
|
||
if (response.error) {
|
||
console.error('❌ [confirmReviewResults] API调用失败:', response.error);
|
||
return { error: response.error, status: response.status || 500 };
|
||
}
|
||
|
||
// 成功响应
|
||
if (response.data?.data) {
|
||
return { data: { auditStatus: response.data.data?.auditStatus || 1 } };
|
||
}
|
||
|
||
// 数据为空或格式不正确
|
||
console.error('❌ [confirmReviewResults] API响应数据异常:', response.data);
|
||
return { error: response.data?.message || '确认文档审核失败', status: 500 };
|
||
} catch (error) {
|
||
console.error('确认评查结果失败:', error);
|
||
return {
|
||
error: error instanceof Error ? error.message : '确认评查结果失败',
|
||
status: 500
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 🆕 从后端API获取评查点数据(新版本)
|
||
* @param fileId 评查文件ID
|
||
* @param request Remix请求对象,用于获取用户会话
|
||
* @returns 评查点结果列表和统计数据
|
||
*
|
||
* 🔥 接口文档: auth_doc/review_points_api_frontend.md
|
||
* 📍 API地址: GET /api/v3/review-points/{document_id}
|
||
*
|
||
* ✅ 优势:
|
||
* - 单次API调用替代原7次请求
|
||
* - 后端统一处理权限验证
|
||
* - 数据格式与原方法完全一致
|
||
*/
|
||
export async function getReviewPoints_fromApi(fileId: string, request: Request) {
|
||
try {
|
||
// 获取用户会话信息(用于JWT认证)
|
||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||
|
||
// const test_jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo1LCJ1c2VybmFtZSI6ImFkbWluIiwidXNlcl9yb2xlIjoicHJvdmluY2lhbF9hZG1pbiIsImV4cCI6MTc2NDE2NTI2MCwiaWF0IjoxNzY0MTQzNjYwLCJpc3N1ZWRfdGltZSI6IjIwMjUtMTEtMjYgMTU6NTQ6MjAiLCJhdWQiOiJkb2NyZXZpZXctZnJvbnRlbmQiLCJzdWIiOiIwMDAiLCJuaWNrX25hbWUiOiJhZG1pbiIsIm91X2lkIjoiMDAwIiwib3VfbmFtZSI6InRlc3QiLCJpc19sZWFkZXIiOnRydWUsImFyZWEiOiJcdTY4ODVcdTVkZGUifQ.VRuMvMh98CMeoWDhX1gVczg6DzBNbWwFK9g4kkKqPpc'
|
||
if (!userInfo?.user_id) {
|
||
console.error("用户身份验证失败");
|
||
return { error: '用户身份验证失败', status: 401 };
|
||
}
|
||
|
||
// console.log(`📡 [getReviewPoints_fromApi] 调用后端API获取文档 ${fileId} 的评查点数据`);
|
||
// console.log(`🔑 [getReviewPoints_fromApi] 使用JWT Token (前10位): ${frontendJWT}`);
|
||
|
||
// 🔑 调用后端API(服务端环境需要手动传递Authorization header)
|
||
const response = await apiRequest<{
|
||
data: ReviewPointResult[];
|
||
stats: StatsData;
|
||
reviewInfo: {
|
||
reviewTime: string;
|
||
reviewModel: string;
|
||
ruleGroup: string;
|
||
result: string;
|
||
issueCount: number;
|
||
};
|
||
document: unknown;
|
||
comparison_document: unknown;
|
||
scoring_proposals: ScoringProposal[];
|
||
}>(
|
||
`/api/v3/review-points/${fileId}`,
|
||
{
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${frontendJWT}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
}
|
||
);
|
||
|
||
// 处理错误响应
|
||
if (response.error) {
|
||
console.error('❌ [getReviewPoints_fromApi] API调用失败:', response.error);
|
||
return { error: response.error, status: response.status || 500 };
|
||
}
|
||
|
||
// 成功响应
|
||
if (response.data) {
|
||
// console.log('✅ [getReviewPoints_fromApi] API调用成功,返回数据结构:', JSON.stringify(response.data, null, 2))
|
||
// console.log('✅ [getReviewPoints_fromApi] API调用成功,返回数据结构:', JSON.stringify(response.data))
|
||
// console.log('✅ [getReviewPoints_fromApi] API调用成功,返回数据结构:', JSON.stringify({
|
||
// 评查点数量: response.data.data?.length || 0,
|
||
// 评查点数量: response.data.data
|
||
// 统计信息: response.data.stats,
|
||
// 评查信息: response.data.reviewInfo,
|
||
// 有文档数据: response.data.document,
|
||
// 有比对数据: !!response.data.comparison_document,
|
||
// 评分提案数量: response.data.scoring_proposals?.length || 0
|
||
// }));
|
||
|
||
// 返回数据格式与原方法完全一致
|
||
return {
|
||
data: response.data.data,
|
||
stats: response.data.stats,
|
||
reviewInfo: response.data.reviewInfo,
|
||
document: response.data.document,
|
||
comparison_document: response.data.comparison_document,
|
||
scoring_proposals: response.data.scoring_proposals
|
||
};
|
||
}
|
||
|
||
// 数据为空
|
||
console.error('❌ [getReviewPoints_fromApi] API响应数据为空');
|
||
return { error: 'API响应数据为空', status: 500 };
|
||
|
||
} catch (error) {
|
||
console.error('❌ [getReviewPoints_fromApi] 调用失败:', error);
|
||
return {
|
||
error: error instanceof Error ? error.message : '获取评查点数据失败',
|
||
status: 500
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取统一评查结果(GraphRAG Scored 模式)
|
||
*
|
||
* @param fileId 文档ID
|
||
* @param request Remix请求对象
|
||
* @returns 统一评查结果
|
||
*/
|
||
export async function getUnifiedEvaluationResults(fileId: string, request: Request) {
|
||
try {
|
||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||
if (!userInfo?.user_id) {
|
||
return { error: '用户身份验证失败', status: 401 };
|
||
}
|
||
|
||
const response = await apiRequest<UnifiedEvaluationResponse>(
|
||
`/api/v2/evaluation/results-unified/${fileId}`,
|
||
{
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${frontendJWT}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
}
|
||
);
|
||
|
||
if (response.error) {
|
||
console.error('❌ [getUnifiedEvaluationResults] API调用失败:', response.error);
|
||
return { error: response.error, status: response.status || 500 };
|
||
}
|
||
|
||
if (response.data) {
|
||
return response.data;
|
||
}
|
||
|
||
console.error('❌ [getUnifiedEvaluationResults] API响应数据为空');
|
||
return { error: 'API响应数据为空', status: 500 };
|
||
|
||
} catch (error) {
|
||
console.error('❌ [getUnifiedEvaluationResults] 调用失败:', error);
|
||
return {
|
||
error: error instanceof Error ? error.message : '获取评查结果失败',
|
||
status: 500
|
||
};
|
||
}
|
||
}
|