From add399e126ee4732afbf74ab206a09ae10314573 Mon Sep 17 00:00:00 2001 From: wren <“porlong@qq.com”> Date: Thu, 7 May 2026 18:15:40 +0800 Subject: [PATCH] feat: migrate cross checking ui to v3 flow --- app/api/cross-checking/cross-file-result.ts | 229 ++++++++-------- app/api/cross-checking/cross-files-upload.ts | 54 ++-- app/api/cross-checking/cross-files.ts | 256 +++++++++++++++--- app/components/reviews/ReviewTabs.tsx | 43 ++- app/routes/cross-checking.result.tsx | 262 +++++++++++-------- app/routes/cross-checking.upload.tsx | 96 +++---- docs/cross-checking-v3-migration-status.md | 179 +++++++++++++ 7 files changed, 786 insertions(+), 333 deletions(-) create mode 100644 docs/cross-checking-v3-migration-status.md diff --git a/app/api/cross-checking/cross-file-result.ts b/app/api/cross-checking/cross-file-result.ts index a21e2e3..338670d 100644 --- a/app/api/cross-checking/cross-file-result.ts +++ b/app/api/cross-checking/cross-file-result.ts @@ -2,6 +2,19 @@ import { postgrestGet, postgrestPut } from "../postgrest-client"; import axios from 'axios'; import { API_BASE_URL } from '../../config/api-config'; +interface ResultEnvelope { + code?: number; + msg?: string; + data?: T; +} + +function unwrapResultEnvelope(payload: unknown): T { + if (payload && typeof payload === 'object' && 'data' in (payload as ResultEnvelope)) { + return ((payload as ResultEnvelope).data ?? null) as T; + } + return payload as T; +} + /** * 从不同格式的 API 响应中提取数据 * @param responseData API 响应数据 @@ -86,8 +99,7 @@ async function safeGetJWT(jwtToken?: string): Promise { /** * 检查用户是否有权确认完成文档评查 * - * 🔥 接口文档: auth_doc/交叉评查接口文档.md 接口11 - * 📍 API地址: GET /api/v2/cross_review/tasks/{task_id}/can-confirm + * 📍 API地址: GET /api/v3/cross-review/tasks/{task_id}/can-confirm * * @param taskId 任务ID * @param frontendJWT JWT token @@ -100,10 +112,8 @@ export async function findIsProposer(taskId: string | number, userId: number | u return false; } - // 调用新的接口检查用户是否有权确认完成 - // GET /api/v2/cross_review/tasks/{task_id}/can-confirm - const response = await axios.get( - `${API_BASE_URL}/api/v2/cross_review/tasks/${taskId}/can-confirm`, + const response = await axios.get>( + `${API_BASE_URL}/api/v3/cross-review/tasks/${taskId}/can-confirm`, { headers: { 'Content-Type': 'application/json', @@ -112,12 +122,8 @@ export async function findIsProposer(taskId: string | number, userId: number | u } ); - const data = response.data; - // console.log('[findIsProposer] 检查权限响应:', data); - - // 返回 can_confirm 字段,表示是否有权确认完成 - // 有权限的用户:任务创建者(assigner_id) 或 主要负责人(principal_user_ids) - return data?.can_confirm === true; + const data = response.data?.data || response.data; + return data?.canConfirm === true || data?.can_confirm === true; } catch (error) { console.error('[findIsProposer] 检查权限失败:', error); @@ -147,26 +153,29 @@ export async function submitCrossCheckingOpinion( const token = await safeGetJWT(jwtToken); const requestData = { - document_id: opinionData.documentId, - evaluation_point_id: Number(opinionData.evaluationPointId), // 强制转数字 - proposed_score: opinionData.deductionScore, - reason: opinionData.auditOpinion, - proposer_id: userInfo?.user_id, - evaluation_result_id: opinionData.reviewPointResultId + documentId: Number(opinionData.documentId), + evaluationPointId: Number(opinionData.evaluationPointId), + deductionScore: opinionData.deductionScore, + auditOpinion: opinionData.auditOpinion, + reviewPointResultId: Number(opinionData.reviewPointResultId) }; - const response = await axios.post(`${API_BASE_URL}/api/v2/cross_review/proposals`, requestData, { + const response = await axios.post>(`${API_BASE_URL}/api/v3/cross-review/proposals`, requestData, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } }); + const data = unwrapResultEnvelope<{ proposalId?: number; createdAt?: string }>(response.data); return { data: { success: true, message: '意见提交成功', - data: response.data + data: { + id: data?.proposalId, + created_at: data?.createdAt || new Date().toISOString() + } } }; } catch (error) { @@ -213,64 +222,80 @@ export async function getCrossCheckingOpinions( // 如果没传userId,默认用1 const realUserId = userId ?? 1; // 实际后端API调用,拼接API_BASE_URL - const response = await axios.post(`${API_BASE_URL}/api/v2/cross_review/proposals/document`, { - // user_id: realUserId, - document_id: documentId, // 如果后端需要document_id可以加上 - page, - page_size: pageSize - }, { + const response = await axios.get; + agreeVoters?: string[]; + disagreeVoters?: string[]; + pendingVoters?: string[]; + canVote?: boolean; + problemMessage?: string; + proposerId: number; + createdAt: string; + status: string; + }>; + }>>(`${API_BASE_URL}/api/v3/cross-review/documents/${documentId}/proposals`, { + params: { + page, + pageSize + }, headers: { - 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } }); - const data = response.data; - // console.log('最原始的返回data', data); - // 处理新的数据结构,支持分页 - const responseData = data.data || data; - const pagination = data.pagination; + const pageData = unwrapResultEnvelope<{ + total: number; + page: number; + pageSize: number; + items: Array<{ + proposalId: string | number; + evaluationPointName: string; + proposedScore: number; + reason: string; + proposer: string; + votes?: Array<{ voter: string; voteType: string }>; + agreeVoters?: string[]; + disagreeVoters?: string[]; + pendingVoters?: string[]; + canVote?: boolean; + problemMessage?: string; + proposerId: number; + createdAt: string; + status: string; + }>; + }>(response.data); // 定义后端返回的数据项类型 - interface ProposalItem { - proposal_id: string | number; - evaluation_point_name: string; - proposed_score: number; - reason: string; - proposer: string; - votes?: Array<{ voter: string; vote_type: string }>; - agree_voters?: string[]; - disagree_voters?: string[]; - pending_voters?: string[]; - can_vote?: boolean; - problem_message?: string; - proposer_id: number; - created_at: string; - status: string; - } - - // 适配后端返回结构,使用新字段 - const opinions: CrossCheckingOpinion[] = Array.isArray(responseData) ? responseData.map((item: ProposalItem) => ({ - proposal_id: item.proposal_id, - evaluation_point_name: item.evaluation_point_name, - proposed_score: item.proposed_score, + const opinions: CrossCheckingOpinion[] = Array.isArray(pageData?.items) ? pageData.items.map((item) => ({ + proposal_id: item.proposalId, + evaluation_point_name: item.evaluationPointName, + proposed_score: item.proposedScore, reason: item.reason, proposer: item.proposer, - votes: item.votes || [], - agree_voters: item.agree_voters || [], - disagree_voters: item.disagree_voters || [], - pending_voters: item.pending_voters || [], - can_vote: item.can_vote ?? false, - problem_message: item.problem_message || '', - proposer_id: item.proposer_id, - created_at: item.created_at, + votes: (item.votes || []).map((vote) => ({ voter: vote.voter, vote_type: vote.voteType })), + agree_voters: item.agreeVoters || [], + disagree_voters: item.disagreeVoters || [], + pending_voters: item.pendingVoters || [], + can_vote: item.canVote ?? false, + problem_message: item.problemMessage || '', + proposer_id: item.proposerId, + created_at: item.createdAt, status: item.status })) : []; return { data: { opinions, - total: pagination?.total || opinions.length + total: pageData?.total || opinions.length } }; } catch (error) { @@ -335,31 +360,29 @@ export async function performOpinionAction( switch (actionData.action) { case 'agree': message = '已赞同该意见'; - endpoint = `${API_BASE_URL}/api/v2/cross_review/proposals/${actionData.opinionId}/votes`; - requestBody = { vote_type: 'agree', voter_id: userInfo?.user_id }; + endpoint = `${API_BASE_URL}/api/v3/cross-review/proposals/${actionData.opinionId}/votes`; + requestBody = { voteType: 'agree' }; break; case 'disagree': message = '已反对该意见'; - endpoint = `${API_BASE_URL}/api/v2/cross_review/proposals/${actionData.opinionId}/votes`; - requestBody = { vote_type: 'disagree', voter_id: userInfo?.user_id }; + endpoint = `${API_BASE_URL}/api/v3/cross-review/proposals/${actionData.opinionId}/votes`; + requestBody = { voteType: 'disagree' }; break; case 'withdraw_vote': message = '已撤销投票'; - // 撤销投票的接口,根据实际API调整 - endpoint = `${API_BASE_URL}/api/v2/cross_review/proposals/${actionData.opinionId}/votes`; - requestBody = { vote_type: 'cancel', voter_id: userInfo?.user_id }; + endpoint = `${API_BASE_URL}/api/v3/cross-review/proposals/${actionData.opinionId}/votes`; + requestBody = { voteType: 'cancel' }; break; case 'withdraw_opinion': message = '已撤销意见'; - // 撤销意见的接口,根据实际API调整 - endpoint = `${API_BASE_URL}/api/v2/cross_review/proposals/${actionData.opinionId}`; + endpoint = `${API_BASE_URL}/api/v3/cross-review/proposals/${actionData.opinionId}`; requestBody = {}; break; default: throw new Error('无效的操作类型'); } - const response = actionData.action === 'withdraw_opinion' + await (actionData.action === 'withdraw_opinion' ? await axios.delete(endpoint, { headers: { 'Content-Type': 'application/json', @@ -371,11 +394,7 @@ export async function performOpinionAction( 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } - }); - - const data = response.data; - - console.log('返回的意见列表数据',data); + })); return { data: { @@ -412,8 +431,7 @@ export async function performOpinionAction( * @param frontendJWT JWT token * @returns 完成评查结果 * - * 🔥 接口文档: auth_doc/交叉评查接口文档(1).md 接口10 - * 📍 API地址: POST /admin/v2/cross_review/tasks/{task_id}/documents/{document_id}/complete + * 📍 API地址: POST /api/v3/cross-review/tasks/{task_id}/documents/{document_id}/complete */ export async function confirmReviewResults( taskId: string | number, @@ -428,10 +446,13 @@ export async function confirmReviewResults( return { error: '文档ID不能为空', status: 400 }; } - // 调用后端API确认文档审核完成 - // 接口: POST /admin/v2/cross_review/tasks/{task_id}/documents/{document_id}/complete - const response = await axios.post( - `${API_BASE_URL}/admin/v2/cross_review/tasks/${taskId}/documents/${documentId}/complete`, + const response = await axios.post>( + `${API_BASE_URL}/api/v3/cross-review/tasks/${taskId}/documents/${documentId}/complete`, {}, // 无需请求体 { headers: { @@ -441,15 +462,16 @@ export async function confirmReviewResults( } ); - const data = response.data; + const data = response.data?.data || response.data; - // 检查响应是否成功 - if (data?.success || data?.code === 0) { + if (data) { return { data: { - task_id: data.task_id || taskId, - document_id: data.document_id || documentId, - message: data.message || '文档评查已完成' + task_id: data.taskId || taskId, + document_id: data.documentId || documentId, + task_status: data.taskStatus, + audit_status: data.auditStatus, + message: '文档评查已完成' } }; } @@ -457,7 +479,7 @@ export async function confirmReviewResults( // 数据为空或格式不正确 console.error('❌ [confirmReviewResults] API响应数据异常:', data); return { - error: data?.message || '确认文档审核失败', + error: '确认文档审核失败', status: 500 }; @@ -491,26 +513,30 @@ export async function checkProposalVotes( // 获取JWT token const token = await safeGetJWT(jwtToken); - const requestData = { - document_id: documentId - }; - - const response = await axios.post(`${API_BASE_URL}/api/v2/cross_review/proposals/document/check_pending_votes`, requestData, { + const response = await axios.get; + }>>(`${API_BASE_URL}/api/v3/cross-review/documents/${documentId}/pending-votes`, { headers: { - 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } }); - const data = response.data; - - console.log("检查投票数据",data); + const data = unwrapResultEnvelope<{ + hasPendingVotes?: boolean; + pendingProposals?: Array<{ evaluationPointName: string; pendingVotersNum: number }>; + }>(response.data); return { data: { success: true, message: '检查成功', - data: data + data: { + pending_proposals: (data?.pendingProposals || []).map((item) => ({ + evaluation_point_name: item.evaluationPointName, + pending_voters_num: item.pendingVotersNum + })) + } } }; } catch (error) { @@ -533,4 +559,3 @@ export async function checkProposalVotes( }; } } - diff --git a/app/api/cross-checking/cross-files-upload.ts b/app/api/cross-checking/cross-files-upload.ts index 1ef7341..c76c664 100644 --- a/app/api/cross-checking/cross-files-upload.ts +++ b/app/api/cross-checking/cross-files-upload.ts @@ -199,19 +199,13 @@ export async function uploadCrossCheckingDocument( } /** - * 批量上传并自动分配交叉评查任务(新接口适配) - * @param files 文件列表 - * @param typeId 文档类型ID - * @param priority 优先级 - * @param documentNumber 文档编号 - * @param remark 备注 - * @param isTestDocument 是否为测试文档 - * @param assignUserIds 需要分配的用户ID数组 - * @param taskName 任务名称 - * @param docType 文档类型(如 XZCF、XZXK) - * @param taskType 任务类型(如 市局间交叉评查、区局间交叉评查) - * @param token JWT Token - * @param principalUserIds 负责人ID数组(包含主要负责人和额外负责人) + * 旧的一体化“上传并分配任务”接口适配。 + * + * 现在创建交叉评查任务已改为: + * 1. 先上传文档 + * 2. 再调用 `createCrossReviewTask()` 创建 v3 任务 + * + * 这里先保留兼容实现,避免影响可能的旧调用方。 */ export async function batchUploadAndAssignCrossCheckingFiles( files: CrossCheckingUploadedFile[], @@ -286,16 +280,15 @@ export async function batchUploadAndAssignCrossCheckingFiles( } /** - * 创建交叉评查任务 - * @param taskData 任务数据 - * @param token JWT Token - * @returns 创建结果 + * 创建交叉评查任务(v3)。 + * 先由前端完成文档上传,再将上传成功后的 documentIds 挂到任务上。 */ export async function createCrossReviewTask(taskData: { documentIds: number[]; userIds: number[]; - assignerId: number; + principalUserIds?: number[]; taskName: string; + docTypeId?: number; docType: string; taskType?: string; }, token: string | null = null): Promise<{ @@ -305,12 +298,13 @@ export async function createCrossReviewTask(taskData: { }> { try { const requestBody = { - document_ids: taskData.documentIds, - user_ids: taskData.userIds, - assigner_id: taskData.assignerId, - task_name: taskData.taskName, - doc_type: taskData.docType, - task_type: taskData.taskType || '市局间交叉评查' + documentIds: taskData.documentIds, + memberUserIds: taskData.userIds, + principalUserIds: taskData.principalUserIds || [], + taskName: taskData.taskName, + docTypeId: taskData.docTypeId, + docTypeCode: taskData.docType, + taskType: taskData.taskType || 'CITY' }; // console.log('[创建任务] 请求数据:', requestBody); @@ -323,7 +317,7 @@ export async function createCrossReviewTask(taskData: { } const response = await axios.post( - `${API_BASE_URL}/admin/cross_review/tasks/assign`, + `${API_BASE_URL}/api/v3/cross-review/tasks`, requestBody, { headers } ); @@ -376,7 +370,7 @@ export function formatFileSize(bytes: number): string { /** * 向已有任务上传新文档 * - * POST /api/v2/cross_review/tasks/{task_id}/upload_documents + * POST /api/v3/cross-review/tasks/{task_id}/documents/upload * * @param params 上传参数 * @returns 上传结果 @@ -396,10 +390,9 @@ export async function uploadDocumentToTask(params: { console.log('[上传文档到任务] 开始上传:', { taskId, fileName: file.name }); const formData = new FormData(); - // 添加文件(使用 files 字段名) - formData.append('files', file, file.name); + formData.append('file', file, file.name); - const uploadEndpoint = `/api/v2/cross_review/tasks/${taskId}/upload_documents`; + const uploadEndpoint = `/api/v3/cross-review/tasks/${taskId}/documents/upload`; const uploadUrl = API_BASE_URL + uploadEndpoint; const headers: Record = {}; @@ -410,10 +403,9 @@ export async function uploadDocumentToTask(params: { const response = await axios.post(uploadUrl, formData, { headers }); const result = response.data; - // 新接口响应格式: { code: 0, success: true, message: "...", data: {...} } if (result && (result.success || result.code === 0)) { console.log('[上传文档到任务] 上传成功:', result.message); - return { success: true, data: result.data }; + return { success: true, data: result.data || result }; } else { console.error('[上传文档到任务] 上传失败:', result.detail || result.message); return { success: false, error: result.detail || result.message || '上传失败' }; diff --git a/app/api/cross-checking/cross-files.ts b/app/api/cross-checking/cross-files.ts index df45688..ad04cc5 100644 --- a/app/api/cross-checking/cross-files.ts +++ b/app/api/cross-checking/cross-files.ts @@ -76,7 +76,7 @@ export interface UserTaskApiResponse { items: UserTaskInfo[]; } -// 任务文档接口类型定义(旧版,保留兼容) +// 任务文档接口类型定义(任务详情页兼容结构) export interface TaskDocument { document_id: number; file_name: string; @@ -289,6 +289,156 @@ export interface ApiResponse { status?: number; } +interface ResultEnvelope { + code?: number; + msg?: string; + data?: T; +} + +interface V3UserTaskItem { + taskId: number; + taskName: string; + taskType: string; + docTypeId?: number | null; + docTypeCode?: string | null; + status: string; + progress?: number; + totalDocuments?: number; + completedDocuments?: number; + createdAt?: string; +} + +interface V3UserTaskPage { + total: number; + page: number; + pageSize: number; + items: V3UserTaskItem[]; +} + +interface V3TaskDocumentItem { + documentId: number; + name: string; + documentNumber?: string | null; + typeId?: number | null; + processingStatus?: string | null; + versionNo?: number; + isLatestVersion?: boolean; + auditStatus?: number; + createdAt?: string; +} + +interface V3TaskDocumentPage { + taskId: number; + total: number; + page: number; + pageSize: number; + items: V3TaskDocumentItem[]; +} + +function unwrapResultEnvelope(payload: unknown): T { + if (payload && typeof payload === 'object' && 'data' in (payload as ResultEnvelope)) { + return ((payload as ResultEnvelope).data ?? null) as T; + } + return payload as T; +} + +function mapProcessingStatus(status?: string | null): CrossReviewDocumentWithVersion['status'] { + switch (status) { + case 'processed': + case 'Processed': + return 'Processed'; + case 'failed': + case 'Failed': + return 'Failed'; + case 'cutting': + case 'Cutting': + return 'Cutting'; + case 'extracting': + case 'Extractioning': + return 'Extractioning'; + case 'evaluating': + case 'Evaluationing': + return 'Evaluationing'; + case 'waiting': + case 'Waiting': + default: + return 'Waiting'; + } +} + +function mapV3TaskToUserTaskInfo(item: V3UserTaskItem): UserTaskInfo { + return { + task_id: item.taskId, + task_name: item.taskName, + task_status: item.status, + doc_type: item.docTypeCode || undefined, + task_created_at: item.createdAt, + task_type: item.taskType, + progress: item.progress, + total_documents: item.totalDocuments + }; +} + +function mapV3DocumentToTaskDocument(item: V3TaskDocumentItem): TaskDocument { + return { + document_id: item.documentId, + file_name: item.name || '', + status: mapProcessingStatus(item.processingStatus), + path: '', + file_code: item.documentNumber || '', + file_type_name: item.typeId ? `类型${item.typeId}` : '', + file_type_id: item.typeId || 0, + file_size: 0, + upload_time: item.createdAt || '', + created_at: item.createdAt || '', + evaluations_status: 0, + audit_status: item.auditStatus || 0, + created_by_user_id: 0, + issues: [], + final_score: 0, + score_summary: '', + score_percent: null, + pass_count: 0, + warning_count: 0, + fail_count: 0, + manual_count: 0 + }; +} + +function mapV3DocumentToVersionedDocument(item: V3TaskDocumentItem): CrossReviewDocumentWithVersion { + const typeName = item.typeId ? `类型${item.typeId}` : '未知类型'; + return { + id: item.documentId, + name: item.name || '', + path: '', + version_number: item.versionNo || 1, + created_at: item.createdAt || '', + status: mapProcessingStatus(item.processingStatus), + file_size: 0, + document_number: item.documentNumber || null, + type_id: item.typeId || 0, + type_name: typeName, + upload_time: item.createdAt || '', + audit_status: (item.auditStatus || 0) as 0 | 1, + total_evaluation_points: 0, + pass_count: 0, + warning_count: 0, + error_count: 0, + manual_count: 0, + issue_count: 0, + warning_messages: [], + error_messages: [], + issue_messages: [], + manual_messages: [], + final_score: 0, + full_score: 100, + score_summary: '', + score_percent: 0, + total_versions: 1, + history_versions: [] + }; +} + // 任务列表查询参数 export interface TaskListParams { page?: number; @@ -321,10 +471,7 @@ export interface TaskListResponse { */ export async function getCrossCheckingTasks(params: TaskListParams = {}, userInfo?: { user_id?: number; [key: string]: unknown }, jwtToken?: string): Promise> { try { - // console.log('开始调用getCrossCheckingTasks,参数:', params); - - // 调用用户任务API,获取当前用户参与的任务 - const userTasksResponse = await getUserTaskDocuments(params.page || 1, params.pageSize || 10, jwtToken); + const userTasksResponse = await getUserTaskDocuments(params, jwtToken); // console.log('getUserTaskDocuments响应:', JSON.stringify(userTasksResponse,null,2)); @@ -347,7 +494,7 @@ export async function getCrossCheckingTasks(params: TaskListParams = {}, userInf startDate: userTask.task_created_at ? new Date(userTask.task_created_at).toISOString().split('T')[0] : new Date().toISOString().split('T')[0], taskType: userTask.task_type, // 保持默认任务类型 docType: userTask.doc_type || '未知类型', // 使用API返回的文档类型 - evaluationRegion: userTask.evaluation_region || [], // 保持默认评查地区 + evaluationRegion: userTask.evaluation_region || [], progress: userTask.progress || 0, // 使用API返回的进度 status: userTask.task_status || 'pending', // 使用API返回的任务状态 score: userTask.task_status === 'completed' ? 85 : 0, // 默认分数 @@ -391,7 +538,7 @@ export async function getCrossCheckingTasks(params: TaskListParams = {}, userInf const lowerKeyword = keyword.toLowerCase(); filteredTasks = filteredTasks.filter(task => task.taskName.toLowerCase().includes(lowerKeyword) || - task.evaluationRegion.toLowerCase().includes(lowerKeyword) + task.evaluationRegion.join(',').toLowerCase().includes(lowerKeyword) ); } @@ -525,7 +672,7 @@ export async function getCrossCheckingStats(userInfo?: { user_id?: number; [key: console.log('开始调用getCrossCheckingStats'); // 获取用户任务数据来计算统计(默认获取第一页数据进行统计) - const userTasksResponse = await getUserTaskDocuments(1, 100, jwtToken); // 获取前100个任务用于统计 + const userTasksResponse = await getUserTaskDocuments({ page: 1, pageSize: 100 }, jwtToken); if (!userTasksResponse.success || !userTasksResponse.data) { console.error('获取用户任务失败:', userTasksResponse.error); @@ -568,25 +715,49 @@ export async function getCrossCheckingStats(userInfo?: { user_id?: number; [key: * @param jwtToken JWT token * @returns 用户任务列表 */ -export async function getUserTaskDocuments(page: number = 1, pageSize: number = 10, jwtToken?: string): Promise> { +export async function getUserTaskDocuments(params: TaskListParams = {}, jwtToken?: string): Promise> { try { - // 拼接绝对路径,去除多余斜杠 const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL; - const url = `${base}/admin/v2/cross_review/tasks/user_tasks`; + const url = `${base}/api/v3/cross-review/tasks/query`; - const response = await axios.post(url, { - page: page, - page_size: pageSize - }, { + const page = params.page || 1; + const pageSize = params.pageSize || 10; + const requestBody: Record = { + page, + pageSize + }; + + if (params.keyword?.trim()) { + requestBody.keyword = params.keyword.trim(); + } + if (params.status && params.status !== 'all') { + requestBody.status = params.status; + } + if (params.taskType && params.taskType !== 'all') { + requestBody.taskType = params.taskType; + } + if (params.docType && params.docType !== 'all') { + requestBody.docTypeCode = params.docType; + } + + const response = await axios.post>(url, requestBody, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${jwtToken || ''}` } }); + const pageData = unwrapResultEnvelope(response.data); + const items = Array.isArray(pageData?.items) ? pageData.items.map(mapV3TaskToUserTaskInfo) : []; + return { success: true, - data: response.data + data: { + total: pageData?.total || 0, + page: pageData?.page || page, + page_size: pageData?.pageSize || pageSize, + items + } }; } catch (error) { if (axios.isAxiosError(error)) { @@ -603,7 +774,7 @@ export async function getUserTaskDocuments(page: number = 1, pageSize: number = } /** - * 获取指定任务的文档列表(旧版接口,保留兼容) + * 获取指定任务的文档列表(兼容任务详情使用) * @param taskId 任务ID * @param page 页码 * @param pageSize 每页大小 @@ -612,24 +783,30 @@ export async function getUserTaskDocuments(page: number = 1, pageSize: number = */ export async function getTaskDocuments(taskId: number, page: number = 1, pageSize: number = 10, jwtToken?: string): Promise> { try { - // 拼接绝对路径,去除多余斜杠 const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL; - const url = `${base}/admin/v2/cross_review/tasks/${taskId}/documents`; - // console.log('最终请求URL:', url); + const url = `${base}/api/v3/cross-review/tasks/${taskId}/documents`; - const response = await axios.post(url, { - page: page, - page_size: pageSize - }, { + const response = await axios.get>(url, { + params: { + page, + pageSize + }, headers: { - 'Content-Type': 'application/json', 'Authorization': `Bearer ${jwtToken || ''}` } }); + const pageData = unwrapResultEnvelope(response.data); + const items = Array.isArray(pageData?.items) ? pageData.items.map(mapV3DocumentToTaskDocument) : []; + return { success: true, - data: response.data + data: { + total: pageData?.total || 0, + page: pageData?.page || page, + page_size: pageData?.pageSize || pageSize, + items + } }; } catch (error) { if (axios.isAxiosError(error)) { @@ -646,9 +823,9 @@ export async function getTaskDocuments(taskId: number, page: number = 1, pageSiz } /** - * 获取任务下文档列表(支持版本归纳)- 新版接口 + * 获取任务下文档列表(支持版本归纳) * - * POST /api/v2/cross_review/tasks/{task_id}/documents + * GET /api/v3/cross-review/tasks/{task_id}/documents * * 同一任务内同名且同类型的文档会被归纳为版本组,最新上传的为当前版本,其余为历史版本。 * @@ -661,18 +838,16 @@ export async function getTaskDocumentsWithVersions( const { taskId, page = 1, pageSize = 10, keyword, jwtToken } = params; try { - // 拼接绝对路径,去除多余斜杠 const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL; - const url = `${base}/api/v2/cross_review/tasks/${taskId}/documents`; + const url = `${base}/api/v3/cross-review/tasks/${taskId}/documents`; - // 构建请求体 const queryParams: { page: number; - page_size: number; + pageSize: number; keyword?: string; } = { page, - page_size: pageSize + pageSize }; // 只有当 keyword 有值时才添加 @@ -680,16 +855,25 @@ export async function getTaskDocumentsWithVersions( queryParams.keyword = keyword.trim(); } - const response = await axios.get(url, { + const response = await axios.get>(url, { params: queryParams, headers: { 'Authorization': `Bearer ${jwtToken || ''}` } }); + const pageData = unwrapResultEnvelope(response.data); + const documents = Array.isArray(pageData?.items) ? pageData.items.map(mapV3DocumentToVersionedDocument) : []; + return { success: true, - data: response.data + data: { + total: pageData?.total || 0, + page: pageData?.page || page, + page_size: pageData?.pageSize || pageSize, + total_pages: Math.ceil((pageData?.total || 0) / (pageData?.pageSize || pageSize || 1)), + documents + } }; } catch (error) { if (axios.isAxiosError(error)) { @@ -1030,4 +1214,4 @@ export async function uploadCrossReviewDocumentTemplate( error: error instanceof Error ? error.message : '上传模板失败' }; } -} \ No newline at end of file +} diff --git a/app/components/reviews/ReviewTabs.tsx b/app/components/reviews/ReviewTabs.tsx index 5c71dea..d8f33dc 100644 --- a/app/components/reviews/ReviewTabs.tsx +++ b/app/components/reviews/ReviewTabs.tsx @@ -25,15 +25,18 @@ interface ReviewTabsProps { auditStatus?: number; type?: string; comparisonId?: number; + backTo?: string; }; onConfirmResults: () => void; onExportReport?: () => void; jwtToken?: string | null; + showConfirmButton?: boolean; + showCompareTab?: boolean; /** 下载前保存文档的回调,返回 true 表示保存成功可以继续下载 */ onSaveBeforeDownload?: () => Promise; } -export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfirmResults, onExportReport, jwtToken, onSaveBeforeDownload }: ReviewTabsProps) { +export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfirmResults, onExportReport, jwtToken, showConfirmButton = true, showCompareTab = true, onSaveBeforeDownload }: ReviewTabsProps) { const [isNavigating, setIsNavigating] = useState(false); const [isReuploadModalOpen, setIsReuploadModalOpen] = useState(false); const [selectedTemplateFiles, setSelectedTemplateFiles] = useState([]); @@ -54,11 +57,15 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi // 根据来源页面返回 const previousRoute = fileInfo.previousRoute || 'documents'; - const returnTo = previousRoute === 'documents' - ? "/documents/list" - : previousRoute === 'filesUpload' - ? "/files/upload" - : "/rules-files"; + const returnTo = fileInfo.backTo || ( + previousRoute === 'documents' + ? "/documents/list" + : previousRoute === 'filesUpload' + ? "/files/upload" + : previousRoute === 'crossChecking' + ? "/cross-checking" + : "/rules-files" + ); navigate(returnTo); setTimeout(() => { revalidator.revalidate(); @@ -266,7 +273,7 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi > AI智能分析 */} - {fileInfo.type?.toString().includes('1') && ( + {showCompareTab && fileInfo.type?.toString().includes('1') && ( )} - + {showConfirmButton && ( + + )} + {showConfirmButton && ( + + )} diff --git a/app/routes/cross-checking.result.tsx b/app/routes/cross-checking.result.tsx index 52bcf97..75eb221 100644 --- a/app/routes/cross-checking.result.tsx +++ b/app/routes/cross-checking.result.tsx @@ -25,24 +25,26 @@ import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node"; import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { useNavigate, useLoaderData } from "@remix-run/react"; -import crossCheckingStyles from "~/styles/cross-checking-result.css?url"; -import { getReviewPoints, updateReviewResult, getReviewPoints_fromApi} from "~/api/evaluation_points/reviews"; +import reviewsStyles from "~/styles/reviews.css?url"; +import { updateReviewResult, getReviewPoints_fromApi, getUnifiedEvaluationResults } from "~/api/evaluation_points/reviews"; +import { postgrestGet } from "~/api/postgrest-client"; import { toastService } from "~/components/ui/Toast"; import { confirmReviewResults, checkProposalVotes, findIsProposer } from "~/api/cross-checking/cross-file-result"; import { usePermission } from "~/hooks/usePermission"; -// 导入交叉评查详情页面组件 +// 复用新版评查详情页外壳,保留交叉评查提案面板 import { - FileInfo, + ReviewTabs, FilePreview, - ReviewPointsList -} from "~/components/cross-checking"; + FileDetails, +} from "~/components/reviews"; +import { ReviewPointsList, type CharPosition } from "~/components/cross-checking"; // 导入文档对比组件 import { ComparePreview } from "~/components/reviews/previewComponents/ComparePreview"; -// 从ReviewPointsList组件中导入ReviewPoint类型和CharPosition类型 -import { type ReviewPoint, type CharPosition } from '~/components/cross-checking'; +// 从ReviewPointsList组件中导入ReviewPoint类型 +import { type ReviewPoint } from '~/components/cross-checking'; import { messageService } from "~/components/ui/MessageModal"; import { loadingBarService } from "~/components/ui/LoadingBar"; import { Breadcrumb } from "~/components/layout/Breadcrumb"; @@ -177,7 +179,7 @@ export const meta: MetaFunction = () => { }; export function links() { - return [{ rel: "stylesheet", href: crossCheckingStyles }]; + return [{ rel: "stylesheet", href: reviewsStyles }]; } export const handle = { @@ -229,15 +231,102 @@ export async function loader({ request }: LoaderFunctionArgs) { // console.log(`✅ [Loader] 用户 ${userInfo.user_id} (${accessCheck.userRole}) 访问文档 ${id} - 权限验证通过`); + async function patchPointCodes(points: Array>, jwt: string) { + try { + const pointIds = points.map((point) => point.pointId).filter(Boolean); + if (pointIds.length === 0) return; + + const response = await postgrestGet('/api/postgrest/proxy/evaluation_points', { + select: 'id,code', + filter: { id: `in.(${[...new Set(pointIds)].join(',')})` }, + token: jwt, + }); + + const raw = response.data; + const pointList = Array.isArray(raw) ? raw : (raw?.data && Array.isArray(raw.data) ? raw.data : []); + const codeMap: Record = {}; + + pointList.forEach((point: any) => { + if (point.code) { + codeMap[String(point.id)] = point.code; + } + }); + + points.forEach((point) => { + point.pointCode = codeMap[String(point.pointId)] || point.pointCode || ''; + }); + } catch (error) { + console.error('[CrossChecking Loader] patchPointCodes error:', error); + } + } + + const isProposer = await findIsProposer(taskId, userInfo?.user_id, frontendJWT); + const unifiedData = await getUnifiedEvaluationResults(id, request); + + if (!('error' in unifiedData) && unifiedData.flow_type === 'graphrag') { + const reviewData = await getReviewPoints_fromApi(id, request); + const existingPoints = ('data' in reviewData && !('error' in reviewData)) ? reviewData.data : []; + const notApplicablePoints = (unifiedData.results || []) + .filter((result: any) => result.result_type === 'not_applicable') + .map((result: any) => ({ + id: `na-${result.evaluation_point_id}`, + documentId: id, + pointId: result.evaluation_point_id, + editAuditStatusId: '', + editAuditStatus: '', + editAuditStatusMessage: '', + title: '该评查点未涉及', + pointName: result.name || '', + pointCode: result.code || '', + groupName: '', + status: 'notApplicable', + content: {}, + contentPage: {}, + suggestion: result.ai_suggestion || '该评查点未涉及', + result: null, + score: result.score || 0, + finalScore: null, + machineScore: 0, + postAction: '', + })); + const allReviewPoints = [...existingPoints, ...notApplicablePoints]; + await patchPointCodes(allReviewPoints as Array>, frontendJWT); + + return Response.json({ + previousRoute, + document: ('document' in reviewData && !('error' in reviewData)) ? reviewData.document : null, + reviewPoints: allReviewPoints, + reviewInfo: { + reviewTime: unifiedData.evaluated_at, + reviewModel: 'GraphRAG', + ruleGroup: '', + result: '', + issueCount: unifiedData.summary?.total_points || 0 + }, + statistics: { + total: unifiedData.summary?.total_points || 0, + success: unifiedData.summary?.passed_count || 0, + warning: unifiedData.summary?.failed_count || 0, + error: 0, + score: unifiedData.summary?.total_score || 0 + }, + comparison_document: ('comparison_document' in reviewData && !('error' in reviewData)) ? reviewData.comparison_document : null, + scoring_proposals: ('scoring_proposals' in reviewData && !('error' in reviewData)) ? (reviewData.scoring_proposals || []) : [], + userInfo, + jwtToken: frontendJWT, + isProposer, + taskId, + taskName, + flowType: 'graphrag' + }); + } + // 对接接口,新的获取评查点结果的方法 - const reviewData = await getReviewPoints_fromApi(id, request) + const reviewData = await getReviewPoints_fromApi(id, request); // 获取评查点数据,传递request对象 旧获取评查点结果的方法 // const reviewData = await getReviewPoints(id, request); - // 获取当前登录用户是否是负责人 - const isProposer = await findIsProposer(taskId, userInfo?.user_id, frontendJWT); - // console.log("documentData-------",JSON.stringify(documentData.data,null,2)); // console.log("reviewData-------",JSON.stringify('data' in reviewData ? reviewData.data : '',null,2)); // console.log("reviewData-------",JSON.stringify(reviewData,null,2)); @@ -249,6 +338,7 @@ export async function loader({ request }: LoaderFunctionArgs) { // 确保reviewData有效且具有预期的属性 if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) { + await patchPointCodes(reviewData.data as Array>, frontendJWT); // console.log("reviewData-------",JSON.stringify(reviewData.data)); return Response.json({ previousRoute: previousRoute, @@ -262,7 +352,8 @@ export async function loader({ request }: LoaderFunctionArgs) { jwtToken: frontendJWT, // 传递JWT token isProposer: isProposer, taskId: taskId, // 传递任务ID - taskName: taskName // 传递任务名称 + taskName: taskName, // 传递任务名称 + flowType: 'legacy' }); } else { console.error("返回的评查数据格式不正确",JSON.stringify(reviewData,null,2)); @@ -338,12 +429,7 @@ export default function CrossCheckingResult() { const { document, reviewPoints, statistics, reviewInfo, comparison_document, scoring_proposals, jwtToken, userInfo, isProposer, taskId, taskName } = loaderData; const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态 - // 视图切换状态:'review' = 评查结果视图, 'compare' = 结构比对视图 - const [viewMode, setViewMode] = useState<'review' | 'compare'>('review'); - - - // 判断是否有模板可以进行结构比对 - const hasTemplateForCompare = Boolean(comparison_document?.template_contract_path?.trim()); + const [activeTab, setActiveTab] = useState('preview'); // 权限控制 const { hasPermission } = usePermission(); @@ -377,6 +463,12 @@ export default function CrossCheckingResult() { setLocalScoringProposals(scoring_proposals || []); }, [scoring_proposals]); + useEffect(() => { + if (!comparison_document?.template_contract_path?.trim() && activeTab === 'filecompare') { + setActiveTab('preview'); + } + }, [activeTab, comparison_document?.template_contract_path]); + // 处理意见提交成功的回调 const handleOpinionSubmitted = useCallback((newProposal: ScoringProposal) => { setLocalScoringProposals(prev => [...prev, newProposal]); @@ -697,7 +789,7 @@ export default function CrossCheckingResult() { isProcessingRef.current = false; toastService.error(`确认评查结果失败: ${error instanceof Error ? error.message : '未知错误'}`); } - }, [document, jwtToken, navigate]); + }, [document, jwtToken, navigate, taskId, taskName]); // 构建自定义面包屑项 - 使用 useMemo 缓存 const breadcrumbItems = useMemo(() => { @@ -716,7 +808,7 @@ export default function CrossCheckingResult() { }, [document?.id, loaderData.previousRoute]); return ( -
+
{isLoading ? (
@@ -753,90 +845,35 @@ export default function CrossCheckingResult() { )}
- - {/* 返回按钮 */} - - - {/* 结构比对/查看评查结果按钮 - 仅当文档类型包含"合同"且有模板时显示 */} - {hasTemplateForCompare && ( - - )} - - {/* 完成评查按钮 - 需要 isProposer 且拥有 cross_review:document:complete 权限 */} - {isProposer && canCompleteDocument && ( - - )}
- {/* 文件信息和操作按钮 */} - {/* */} - - {/* 根据视图模式切换内容 */} - {viewMode === 'review' ? ( - /* 评查结果视图 */ -
+ onConfirmResults={() => { + void handleConfirmResults(); + }} + jwtToken={jwtToken} + showConfirmButton={Boolean(isProposer && canCompleteDocument)} + showCompareTab={Boolean(comparison_document?.template_contract_path?.trim())} + > + {activeTab === 'preview' && ( +
{/* 左侧:文件预览 */} -
+
@@ -868,10 +906,11 @@ export default function CrossCheckingResult() { canVoteProposal={canVoteProposal} />
-
- ) : ( - /* 结构比对视图 */ -
+ )} + + {activeTab === 'filecompare' && ( +
-
- )} +
+ )} + + {activeTab === 'fileinfo' && ( + + )} + )}
diff --git a/app/routes/cross-checking.upload.tsx b/app/routes/cross-checking.upload.tsx index 212d297..e717838 100644 --- a/app/routes/cross-checking.upload.tsx +++ b/app/routes/cross-checking.upload.tsx @@ -14,7 +14,7 @@ import { type CrossCheckingUploadedFile, generateFileId, formatFileSize, - batchUploadAndAssignCrossCheckingFiles, + uploadCrossCheckingDocument, createCrossReviewTask } from "~/api/cross-checking/cross-files-upload"; import { @@ -302,8 +302,7 @@ export default function CrossCheckingUpload() { return; } - // 第一步:上传文件并自动分配任务(新接口) - // console.log("开始批量上传文件并分配任务:", filesToUpload.length, "个,案卷类型:", selectedDocType.name); + // 第一步:先上传文档到平台,再用 v3 接口创建交叉评查任务 // 提取用户ID(从选中的组织架构中获取用户) const userIds = groupChecked.filter(id => { @@ -316,23 +315,6 @@ export default function CrossCheckingUpload() { return; } - // const requireParam = { - // filesToUpload: filesToUpload, - // selectedDocTypeId: selectedDocTypeId, - // priority: priority, - // documentNumber: documentNumber, - // remark: remark, - // isTestDocument: isTestDocument, - // userIds: userIds, - // taskInfo_name: taskInfo.name, - // selectedDocType_name: selectedDocType.code, - // taskInfo_type: taskInfo.type, - // frontendJWT - // } - - // // console.log("requireParam", requireParam) - // return; - // 构建负责人ID数组:当前用户(主要负责人)+ 额外负责人 const principalUserIds: number[] = []; // 添加当前用户作为主要负责人 @@ -347,37 +329,63 @@ export default function CrossCheckingUpload() { } }); - // 使用文档类型名称作为 doc_type - const uploadResult = await batchUploadAndAssignCrossCheckingFiles( - filesToUpload, - selectedDocTypeId, // 使用选中的文档类型ID - priority, - documentNumber, - remark, - isTestDocument, + const uploadSuccesses: Array<{ file: CrossCheckingUploadedFile; documentId: number }> = []; + const uploadFailures: Array<{ file: CrossCheckingUploadedFile; error: string }> = []; + + for (const fileInfo of filesToUpload) { + const binaryData = await fileInfo.file.arrayBuffer(); + const uploadResponse = await uploadCrossCheckingDocument( + binaryData, + fileInfo.name, + fileInfo.type, + selectedDocTypeId, + priority, + documentNumber, + remark, + isTestDocument, + null, + false, + frontendJWT + ); + + if (uploadResponse.error || !uploadResponse.data?.result?.id) { + uploadFailures.push({ + file: fileInfo, + error: uploadResponse.error || '上传后未返回文档ID' + }); + continue; + } + + uploadSuccesses.push({ + file: fileInfo, + documentId: uploadResponse.data.result.id + }); + } + + if (uploadFailures.length > 0) { + toastService.error(`文件上传失败:${uploadFailures[0].error}`); + return; + } + + const createTaskResult = await createCrossReviewTask({ + documentIds: uploadSuccesses.map(item => item.documentId), userIds, - taskInfo.name, - selectedDocType.code, // 使用文档类型code - taskInfo.type, // 使用任务类型(市局间交叉评查 或 区局间交叉评查) - frontendJWT, - principalUserIds, // 负责人ID数组 - attributeType // 合同类型 - ); + principalUserIds, + taskName: taskInfo.name, + docTypeId: selectedDocTypeId, + docType: selectedDocType.code, + taskType: taskInfo.type + }, frontendJWT); - - // return; - - const { successes, failures } = uploadResult; - - if (failures.length > 0) { - toastService.error(`文件上传或任务分配失败:${failures[0].error}`); + if (!createTaskResult.success) { + toastService.error(createTaskResult.error || '创建交叉评查任务失败'); return; } // 任务创建成功 toastService.success("交叉评查任务创建成功!"); messageService.success( - `任务创建完成!\n任务名称:${taskInfo.name}\n上传文件:${successes.length} 个\n评查人员:${userIds.length} 人`, + `任务创建完成!\n任务名称:${taskInfo.name}\n上传文件:${uploadSuccesses.length} 个\n评查人员:${userIds.length} 人`, { title: '任务创建成功', confirmText: '确定', @@ -1183,4 +1191,4 @@ export default function CrossCheckingUpload() { ); -} \ No newline at end of file +} diff --git a/docs/cross-checking-v3-migration-status.md b/docs/cross-checking-v3-migration-status.md new file mode 100644 index 0000000..0c812e1 --- /dev/null +++ b/docs/cross-checking-v3-migration-status.md @@ -0,0 +1,179 @@ +# 交叉评查 v3 迁移收口说明 + +## 当前主链路状态 + +目前交叉评查的主流程已经切到新版本链路: + +1. 创建任务页先上传文档,再调用 `POST /api/v3/cross-review/tasks` 创建任务 +2. 任务列表走 `POST /api/v3/cross-review/tasks/query` +3. 任务文档列表走 `GET /api/v3/cross-review/tasks/{taskId}/documents` +4. 交叉评查详情页使用新版评查详情页外壳 +5. 提案、投票、待投票检查走 `v3 cross-review` 接口 +6. 完成评查走 `POST /api/v3/cross-review/tasks/{taskId}/documents/{documentId}/complete` + +## 本次已完成的前端收口 + +### 1. 创建任务 + +文件: + +- `app/routes/cross-checking.upload.tsx` +- `app/api/cross-checking/cross-files-upload.ts` + +现状: + +- 不再把“上传文件 + 自动分配任务”作为主链路 +- 改为先上传文档,再使用上传成功返回的 `documentId` 创建任务 +- 创建任务请求体已经对齐后端 `v3` DTO: + - `taskName` + - `taskType` + - `docTypeId` + - `docTypeCode` + - `memberUserIds` + - `principalUserIds` + - `documentIds` + +### 2. 任务页 + +文件: + +- `app/api/cross-checking/cross-files.ts` +- `app/routes/cross-checking._index.tsx` + +现状: + +- 任务列表、统计、文档列表均已接新接口 +- 返回任务页后仍保留 `openModal + taskId + taskName` 的 reopen 机制 + +### 3. 详情页 + +文件: + +- `app/routes/cross-checking.result.tsx` +- `app/components/reviews/ReviewTabs.tsx` + +现状: + +- 页面外壳切到新版评查详情页 +- 优先复用新版预览、tab、文件信息组件 +- 交叉评查特有的提案面板仍保留,作为业务差异层 + +### 4. 提案与投票 + +文件: + +- `app/api/cross-checking/cross-file-result.ts` + +现状: + +- 已切到以下新接口: + - `POST /api/v3/cross-review/proposals` + - `POST /api/v3/cross-review/proposals/{proposalId}/votes` + - `DELETE /api/v3/cross-review/proposals/{proposalId}` + - `GET /api/v3/cross-review/documents/{documentId}/proposals` + - `GET /api/v3/cross-review/documents/{documentId}/pending-votes` + +## 边支能力迁移状态 + +结合最新后端实现,交叉评查的边支能力里,“向已有任务补传文档”也已经切到新版本链路。 + +### 1. 向已有任务补传文档 + +文件: + +- `app/api/cross-checking/cross-files-upload.ts` + +接口: + +- `POST /api/v3/cross-review/tasks/{taskId}/documents/upload` + +调用位置: + +- `app/components/cross-checking/DocumentListModal.tsx` + +当前实现: + +- 前端改为直接上传 `file` +- 后端不再走旧 `cross_review` 边支上传逻辑 +- 改为复用 `leaudit` 文档上传与抽取链路,再把新文档挂到交叉评查任务下 +- 上传成功后任务状态会回到 `in_progress` + +说明: + +- 这条能力已经不再依赖旧版 `v2 cross_review upload_documents` +- 文档抽取、后续评查点生成与新版主链路保持一致 + +## 仍保留旧接口的支链路 + +以下能力当前仍是旧接口,尚未迁到 `v3`: + +### 1. 任务文档追加附件 + +文件: + +- `app/api/cross-checking/cross-files.ts` + +接口: + +- `POST /api/v2/cross_review/tasks/{task_id}/documents/{document_id}/append_attachments` + +调用位置: + +- `app/components/cross-checking/DocumentListModal.tsx` + +说明: + +- 该能力主要服务合同场景的版本追加 +- 当前仍可继续保留旧接口,等后端 `v3` 能力明确后再迁 + +### 2. 合同模板上传 + +文件: + +- `app/api/cross-checking/cross-files.ts` + +接口: + +- 复用上传服务 `/upload_contract_template` + +调用位置: + +- `app/components/cross-checking/DocumentListModal.tsx` + +说明: + +- 该能力不属于 `cross-review` 专属接口,暂不要求迁到 `v3 cross-review` + +## 建议的后续迁移优先级 + +### P1 + +- 追加附件:`appendTaskDocumentAttachments()` + +原因: + +- 业务价值高,但更多是合同场景增强功能 +- 可以在主链路稳定后迁 + +### P2 + +- 历史兼容 helper 清理 +- 旧注释、旧命名继续清理 + +原因: + +- 不影响业务,但能减少后续误接老接口的概率 + +## 当前结论 + +可以认为交叉评查的核心业务链路已经完成 `v3` 迁移: + +- 创建任务 +- 任务列表 +- 文档列表 +- 详情展示 +- 提案投票 +- 完成评查 +- 已有任务补传文档 + +剩余工作主要集中在“追加附件”这条旧边支能力,以及后续兼容代码清理。