import axios from 'axios'; import { postgrestGet, postgrestDelete } from '../postgrest-client'; import { getDocumentTypes } from '../document-types/document-types'; import { formatDate } from '../../utils'; import { API_BASE_URL } from '~/config/api-config'; import type { DocumentType } from '../document-types/document-types'; /** * 从不同格式的 API 响应中提取数据 * @param responseData API 响应数据 * @returns 提取后的数据或 null */ function extractApiData(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; } /** * 数据库文档结构 */ export interface Document { id: number; user_id: number | null; type_id: number; name: string; document_number: string; path: string; storage_type: string; file_size: number; upload_time: string; is_test_document: boolean; evaluation_level: string; status: 'pass' | 'warning' | 'waiting' | 'processing' | 'fail'; file_status: 'Waiting' | 'Cutting' | 'Extractioning' | 'Evaluationing' | 'Processed'; audit_status: number; // -1: 不通过, 0: 待审核, 1: 通过, 2: 警告, 3: 审核中 ocr_result?: { __meta?: { page_count?: number; } }; extracted_results?: unknown; summary?: unknown; remark?: string; created_at: string; updated_at: string; } /** * 前端UI文档结构 */ export interface DocumentUI { id: number; name: string; documentNumber: string; type: string; typeName: string; groupId?: number | null; groupName?: string | null; size: number; auditStatus: number; // -1: 不通过, 0: 待审核, 1: 通过, 2: 警告, 3: 审核中 fileStatus: string; // Waiting, Cutting, Extractioning, Failed, Evaluationing, Processed issues: number | null; uploadTime: string; fileType: string; path: string; isTest: boolean; remark?: string; updatedAt?: string; pageCount?: number; ocrResult?: unknown; // 结果统计字段 pass_count?: number | null; // 通过数量 warning_count?: number | null; // 警告数量 error_count?: number | null; // 错误数量 manual_count?: number | null; // 人工审核数量 // 消息详情字段 warning_messages?: string[]; // 警告消息列表 error_messages?: string[]; // 错误消息列表 manual_messages?: string[]; // 人工审核消息列表 // 版本管理相关字段 historyCount?: number; // 历史版本数量(不含当前版本) previousIssues?: number | null; // 上一个版本的问题数量 previous_pass_count?: number | null; // 上一版本通过数量 previous_warning_count?: number | null; // 上一版本警告数量 previous_error_count?: number | null; // 上一版本错误数量 previous_manual_count?: number | null; // 上一版本人工数量 isExpanded?: boolean; // 是否展开历史版本(前端状态) historyVersions?: DocumentVersionUI[]; // 历史版本列表 } /** * 文档历史版本结构 */ export interface DocumentVersionUI { id: number; name: string; documentNumber: string; type: string; typeName: string; groupId?: number | null; groupName?: string | null; size: number; auditStatus: number; fileStatus: string; issues: number | null; issuesDiff?: number; // 与上一个版本的问题数量差异(绝对值) issuesDiffType?: 'increase' | 'decrease' | 'same'; // 差异类型 uploadTime: string; fileType: string; path: string; isTest: boolean; updatedAt?: string; pageCount?: number; ocrResult?: unknown; versionNumber?: number; // 版本号(v2, v3, v4...) // 结果统计字段 pass_count: number | null; // 通过数量 warning_count: number | null; // 警告数量 error_count: number | null; // 错误数量 manual_count: number | null; // 人工审核数量 previous_pass_count?: number | null; // 上一版本通过数量 previous_warning_count?: number | null; // 上一版本警告数量 previous_error_count?: number | null; // 上一版本错误数量 previous_manual_count?: number | null; // 上一版本人工数量 } interface LeauditHistoryVersion { documentId: number; fileId?: number | null; versionNo: number; fileName?: string | null; fileExt?: string | null; processingStatus?: string | null; runStatus?: string | null; resultStatus?: string | null; updatedAt?: string | null; } interface LeauditListItem { documentId: number; internalDocumentNo: number; versionGroupKey: string; versionNo: number; rootVersionId: number; previousVersionId?: number | null; typeId?: number | null; typeCode?: string | null; typeName?: string | null; groupId?: number | null; groupName?: string | null; region: string; normalizedName?: string | null; fileId?: number | null; fileName?: string | null; fileExt?: string | null; mimeType?: string | null; fileSize?: number | null; ossUrl?: string | null; processingStatus?: string | null; currentRunId?: number | null; runStatus?: string | null; resultStatus?: string | null; totalScore?: number | null; passedCount?: number | null; failedCount?: number | null; skippedCount?: number | null; documentNumber?: string | null; auditStatus?: number | null; isTestDocument?: boolean | null; updatedAt?: string | null; hasHistory?: boolean; totalVersions?: number; historyVersions?: LeauditHistoryVersion[]; } interface LeauditListPage { total: number; page: number; pageSize: number; totalPages: number; documents: LeauditListItem[]; } interface LeauditDocumentDetail extends LeauditListItem { documentNumber?: string | null; remark?: string | null; isTestDocument?: boolean | null; auditStatus?: number | null; pageCount?: number | null; } interface DocumentMetadataUpdateDTO { documentNumber?: string; auditStatus?: number; isTestDocument?: boolean; remark?: string; } /** * 获取文件扩展名 * @param filename 文件名 * @returns 文件扩展名 */ function getFileExtension(filename: string): string { const parts = filename.split('.'); return parts.length > 1 ? parts.pop()?.toLowerCase() || '' : ''; } function mapProcessingStatusToFileStatus(status?: string | null): string { const normalized = (status || '').toLowerCase(); if (normalized === 'completed') return 'Processed'; if (normalized === 'failed') return 'Failed'; if (normalized === 'running' || normalized === 'queued' || normalized === 'dispatch') return 'Evaluationing'; if (normalized === 'waiting' || normalized === 'pending') return 'Waiting'; return 'Waiting'; } function mapLeauditDocToAuditStatus(doc: { processingStatus?: string | null; runStatus?: string | null; passedCount?: number | null; failedCount?: number | null; }): number { const processingStatus = (doc.processingStatus || '').toLowerCase(); const runStatus = (doc.runStatus || '').toLowerCase(); if (runStatus === 'queued' || runStatus === 'running' || processingStatus === 'running') { return 2; } if (processingStatus === 'waiting' || processingStatus === 'pending') { return 0; } if (processingStatus === 'failed') { return -1; } if ((doc.failedCount || 0) > 0) { return -1; } if ((doc.passedCount || 0) > 0) { return 1; } return 0; } function typeNameFromCode(typeCode?: string | null): string { if (!typeCode) return '未知类型'; const typeMap: Record = { 'contract.sale': '购销合同', 'contract.purchase': '采购合同', 'contract.lease': '租赁合同', 'contract.service': '服务合同', }; if (typeMap[typeCode]) return typeMap[typeCode]; return typeCode; } function buildDocumentNumber(doc: LeauditListItem | LeauditHistoryVersion): string { if ('versionNo' in doc && doc.versionNo) { return `v${doc.versionNo}`; } return ''; } function isUnsupportedNewDocumentCrud(error: unknown): boolean { return axios.isAxiosError(error) && [404, 405, 501].includes(error.response?.status || 0); } function getErrorMessage(error: unknown, fallback: string): string { if (axios.isAxiosError(error)) { return error.response?.data?.message || error.response?.data?.detail || error.message || fallback; } return error instanceof Error ? error.message : fallback; } function mapHistoryVersionToUI(history: LeauditHistoryVersion, source: LeauditListItem): DocumentVersionUI { return { id: history.documentId, name: history.fileName || source.fileName || source.normalizedName || '未命名文档', documentNumber: buildDocumentNumber(history), type: source.typeId?.toString() || '', typeName: source.typeName || typeNameFromCode(source.typeCode), groupId: source.groupId ?? null, groupName: source.groupName ?? null, size: 0, auditStatus: mapLeauditDocToAuditStatus({ processingStatus: history.processingStatus, runStatus: history.runStatus, passedCount: null, failedCount: null, }), fileStatus: mapProcessingStatusToFileStatus(history.processingStatus), issues: null, uploadTime: formatDate(history.updatedAt || ''), fileType: history.fileExt || getFileExtension(history.fileName || source.fileName || ''), path: '', isTest: false, updatedAt: formatDate(history.updatedAt || ''), pageCount: 0, ocrResult: undefined, versionNumber: history.versionNo, pass_count: null, warning_count: 0, error_count: null, manual_count: null, previous_pass_count: null, previous_warning_count: null, previous_error_count: null, previous_manual_count: null }; } function mapLeauditDocumentToUI(doc: LeauditListItem | LeauditDocumentDetail): DocumentUI { const historyVersions = (doc.historyVersions || []).map((history) => mapHistoryVersionToUI(history, doc)); return { id: doc.documentId, name: doc.fileName || doc.normalizedName || '未命名文档', documentNumber: ('documentNumber' in doc && doc.documentNumber) ? doc.documentNumber : buildDocumentNumber(doc), type: doc.typeId?.toString() || '', typeName: doc.typeName || typeNameFromCode(doc.typeCode), groupId: doc.groupId ?? null, groupName: doc.groupName ?? null, size: doc.fileSize || 0, auditStatus: ('auditStatus' in doc && doc.auditStatus !== null && doc.auditStatus !== undefined) ? doc.auditStatus : mapLeauditDocToAuditStatus(doc), fileStatus: mapProcessingStatusToFileStatus(doc.processingStatus), issues: doc.failedCount ?? null, uploadTime: formatDate(doc.updatedAt || ''), fileType: doc.fileExt || getFileExtension(doc.fileName || ''), path: doc.ossUrl || '', isTest: Boolean(('isTestDocument' in doc && doc.isTestDocument) || false), remark: 'remark' in doc ? (doc.remark || '') : '', updatedAt: formatDate(doc.updatedAt || ''), pageCount: ('pageCount' in doc ? (doc.pageCount || 0) : 0), ocrResult: undefined, pass_count: doc.passedCount ?? null, warning_count: 0, error_count: doc.failedCount ?? null, manual_count: doc.skippedCount ?? null, warning_messages: [], error_messages: [], manual_messages: [], historyCount: Math.max(0, (doc.totalVersions || 1) - 1), previousIssues: historyVersions[0]?.issues ?? null, previous_pass_count: historyVersions[0]?.pass_count ?? null, previous_warning_count: historyVersions[0]?.warning_count ?? null, previous_error_count: historyVersions[0]?.error_count ?? null, previous_manual_count: historyVersions[0]?.manual_count ?? null, historyVersions: historyVersions.length > 0 ? historyVersions : undefined }; } /** * 获取评查结果 * @param id 评查结果ID * @returns 评查结果 */ async function getEvaluationResults(id: number, frontendJWT?: string) { const response = await postgrestGet<[]>('/api/postgrest/proxy/evaluation_results', { filter: { 'document_id': `eq.${id}` }, token: frontendJWT }); if (response.error) { return { error: response.error, status: response.status }; } const evaluationResult = extractApiData<[]>(response.data); return evaluationResult; } /** * 将API文档转换为UI文档 */ async function convertToUIDocument(doc: Document, frontendJWT?: string): Promise { // 获取文档类型信息 const typeResponse = await getDocumentTypes(undefined, frontendJWT); const documentTypes = typeResponse.data?.types || []; const docType = documentTypes.find(type => type.id.toString() === doc.type_id.toString()); const evaluationResult = await getEvaluationResults(doc.id, frontendJWT); let issues = 0; interface EvaluationResultItem { evaluated_results?: { result?: string; [key: string]: unknown; }; [key: string]: unknown; } if (evaluationResult && Array.isArray(evaluationResult)) { evaluationResult.forEach((result: EvaluationResultItem) => { if(result && result.evaluated_results && !result.evaluated_results.result){ issues++; } }); } return { id: doc.id, name: doc.name, documentNumber: doc.document_number, type: doc.type_id.toString(), typeName: docType?.name || '未知类型', size: doc.file_size, auditStatus: doc.audit_status || 0, fileStatus: doc.status || '', // 默认为'' issues: issues, // 使用计算得到的issues uploadTime: formatDate(doc.updated_at), fileType: getFileExtension(doc.name), path: doc.path, isTest: doc.is_test_document, remark: doc.remark, updatedAt: formatDate(doc.updated_at), pageCount: doc.ocr_result?.__meta?.page_count || 0, ocrResult: doc.ocr_result }; } /** * 后端SQL函数返回的文档结构 */ interface DocumentFromSQL { id: number; name: string; document_number: string; type_id: number; type_name: string; file_size: number; audit_status: number; status: string; false_count: number; updated_at: string; path: string; is_test_document: boolean; ocr_result: { __meta?: { page_count?: number; } }; } /** * 删除文档 * @param id 文档ID * @param userId 用户ID * @param token JWT token (可选) * @returns 删除结果 */ export async function deleteDocument(id: string, userId: string, token?: string): Promise<{ success?: boolean; error?: string; status?: number; }> { try { if (!id) { return { error: '文档ID不能为空', status: 400 }; } if (!userId) { return { error: '用户身份验证失败', status: 401 }; } try { // 新后端接口应基于 JWT 在服务端做数据隔离: // - provincial_admin: 全量 // - admin: 本地市 // - common: 自己上传的文档 await axios.delete(`${API_BASE_URL}/api/documents/${id}`, { headers: token ? { 'Authorization': `Bearer ${token}` } : undefined }); return { success: true }; } catch (error) { if (!isUnsupportedNewDocumentCrud(error)) { return { error: getErrorMessage(error, '删除文档失败'), status: axios.isAxiosError(error) ? error.response?.status : 500 }; } } const response = await postgrestDelete('/api/postgrest/proxy/documents', { filter: { 'id': `eq.${id}`, 'user_id': `eq.${userId}` // 旧链路仅允许删除自己的文档 }, token }); if (response.error) { return { error: response.error, status: response.status }; } return { success: true }; } catch (error) { console.error('删除文档失败:', error); return { error: error instanceof Error ? error.message : '删除文档失败', status: 500 }; } } /** * 获取单个文档详情 * @param id 文档ID * @returns 文档详情 */ export async function getDocument(id: string, userId: string, frontendJWT?: string): Promise<{ data?: DocumentUI; error?: string; status?: number; }> { try { if (!id) { return { error: '文档ID不能为空', status: 400 }; } if (!userId) { return { error: '用户身份验证失败', status: 401 }; } try { const response = await axios.get(`${API_BASE_URL}/api/documents/${id}`, { headers: frontendJWT ? { 'Authorization': `Bearer ${frontendJWT}` } : undefined }); const detail = extractApiData(response.data); if (!detail) { return { error: '文档不存在', status: 404 }; } return { data: mapLeauditDocumentToUI(detail) }; } catch (error) { if (!isUnsupportedNewDocumentCrud(error)) { return { error: getErrorMessage(error, '获取文档详情失败'), status: axios.isAxiosError(error) ? error.response?.status : 500 }; } } const response = await postgrestGet('/api/postgrest/proxy/documents', { filter: { 'id': `eq.${id}`, 'user_id': `eq.${userId}` }, limit: 1, token: frontendJWT }); if (response.error) { return { error: response.error, status: response.status }; } const extractedData = extractApiData(response.data); if (!extractedData || extractedData.length === 0) { return { error: '文档不存在', status: 404 }; } const documentUI = await convertToUIDocument(extractedData[0], frontendJWT); return { data: documentUI }; } catch (error) { console.error('获取文档详情失败:', error); return { error: error instanceof Error ? error.message : '获取文档详情失败', status: 500 }; } } /** * 获取单个文档详情 * @param id 文档ID * @returns 文档详情 */ // export async function getDocumentWithNoUserId(id: string, frontendJWT?: string): Promise<{ // data?: DocumentUI; // error?: string; // status?: number; // }> { // try { // if (!id) { // return { error: '文档ID不能为空', status: 400 }; // } // // console.log("get单个文档id", id) // const response = await postgrestGet( // '/api/postgrest/proxy/documents', // { // filter: { // 'id': `eq.${id}`, // }, // limit: 1, // token: frontendJWT // } // ); // if (response.error) { // return { error: response.error, status: response.status }; // } // // console.log("respose", response) // const extractedData = extractApiData(response.data); // if (!extractedData || extractedData.length === 0) { // return { error: '文档不存在', status: 404 }; // } // // console.log('extractedData', extractedData); // const documentUI = await convertToUIDocument(extractedData[0], frontendJWT); // return { data: documentUI }; // } catch (error) { // console.error('获取文档详情失败:', error); // return { // error: error instanceof Error ? error.message : '获取文档详情失败', // status: 500 // }; // } // } /** * 获取文档类型列表(按IDs过滤版本) * @param ids 文档类型ID数组(必填) * @param frontendJWT JWT token(可选) * @returns 文档类型列表 */ export async function getDocumentTypesByIds(ids: number[], frontendJWT?: string): Promise<{ data?: { types: DocumentType[], total: number }; error?: string; status?: number; }> { try { if (!ids || ids.length === 0) { return { data: { types: [], total: 0 } }; } const response = await getDocumentTypes({ ids, page: 1, pageSize: Math.max(ids.length, 10) }, frontendJWT); if (response.error) { return { error: response.error, status: response.status }; } const extractedData = response.data?.types; if (!extractedData) { return { error: '获取文档类型列表失败', status: 500 }; } return { data: { types: extractedData, total: extractedData.length } }; } catch (error) { console.error('获取文档类型列表失败:', error); return { error: error instanceof Error ? error.message : '获取文档类型列表失败', status: 500 }; } } /** * 更新文档信息 * * 使用 PATCH 方法调用 /api/postgrest/proxy/documents 接口 * 后端会自动注入 user_id 过滤条件,确保用户只能更新自己的文档 * * @param id 文档ID * @param document 部分文档数据(可更新字段:document_number, audit_status, is_test_document, remark) * @param userId 用户ID(用于权限验证) * @param frontendJWT JWT Token(可选,如不传则使用 localStorage 中的 access_token) * @returns 更新结果 * * @see auth_doc/document_update_api.md 接口文档 */ export async function updateDocument(id: string, document: Partial & { remark?: string }, userId: string, frontendJWT?: string): Promise<{ data?: DocumentUI; error?: string; status?: number; }> { try { if (!id) { return { error: '文档ID不能为空', status: 400 }; } if (!userId) { return { error: '用户身份验证失败', status: 401 }; } const apiDocument: DocumentMetadataUpdateDTO = {}; if (document.documentNumber !== undefined) { apiDocument.documentNumber = document.documentNumber; } if (document.auditStatus !== undefined) { apiDocument.auditStatus = document.auditStatus; } if (document.isTest !== undefined) { apiDocument.isTestDocument = document.isTest; } if (document.remark !== undefined) { apiDocument.remark = document.remark; } try { await axios.put(`${API_BASE_URL}/api/documents/${id}`, apiDocument, { headers: { 'Content-Type': 'application/json', ...(frontendJWT ? { 'Authorization': `Bearer ${frontendJWT}` } : {}) } }); } catch (error) { if (!isUnsupportedNewDocumentCrud(error)) { console.error('❌ [updateDocument] 更新文档API错误:', error); return { error: getErrorMessage(error, '更新文档信息失败'), status: axios.isAxiosError(error) ? error.response?.status : 500 }; } // 旧链路回退:仅允许修改自己的文档;新链路上线后应由后端基于地区/角色做数据隔离。 const { apiRequest } = await import('../axios-client'); const response = await apiRequest( `/api/postgrest/proxy/documents?id=eq.${id}`, { method: 'PATCH', data: { ...(document.documentNumber !== undefined ? { document_number: document.documentNumber } : {}), ...(document.auditStatus !== undefined ? { audit_status: document.auditStatus } : {}), ...(document.isTest !== undefined ? { is_test_document: document.isTest } : {}), ...(document.remark !== undefined ? { remark: document.remark } : {}), }, headers: { 'Content-Type': 'application/json', ...(frontendJWT ? { 'Authorization': `Bearer ${frontendJWT}` } : {}) } } ); if (response.error) { console.error('❌ [updateDocument] 更新文档API错误:', response.error); return { error: response.error, status: response.status }; } const responseData = response.data; if (!responseData || (Array.isArray(responseData) && responseData.length === 0)) { return { error: '文档不存在或无权访问', status: 404 }; } } // 获取更新后的完整文档数据(包含关联的文档类型信息) const updatedResponse = await getDocument(id, userId, frontendJWT); return updatedResponse; } catch (error) { console.error('❌ [updateDocument] 更新文档信息失败:', error); return { error: error instanceof Error ? error.message : '更新文档信息失败', status: 500 }; } } /** * 获取文档列表(使用新的后端API) * @param searchParams 搜索参数 * @returns 文档列表和总数 */ export async function getDocumentsListFromAPI(searchParams: { page?: number; pageSize?: number; name?: string; documentNumber?: string; documentTypeIds?: number[]; // 文档类型ID数组 entryModuleId?: number; auditStatus?: string; fileStatus?: string; dateFrom?: string; dateTo?: string; token: string; // JWT token (必填) }): Promise<{ data?: { documents: DocumentUI[], total: number, page: number, totalPages: number }; error?: string; status?: number; }> { try { const { page = 1, pageSize = 10, name, documentNumber, documentTypeIds, entryModuleId, auditStatus, fileStatus, dateFrom, dateTo, token } = searchParams; const params: Record = { page, pageSize }; // 新接口已落地的筛选项 if (name) params.keyword = name; if (fileStatus) { const normalizedFileStatus = fileStatus.toLowerCase(); if (normalizedFileStatus === 'processed') { params.processingStatus = 'completed'; } else if (normalizedFileStatus === 'failed') { params.processingStatus = 'failed'; } else { params.processingStatus = 'running'; } } if (documentTypeIds && documentTypeIds.length > 0) { params.type_ids = documentTypeIds.join(','); } if (entryModuleId && entryModuleId > 0) { params.entry_module_id = entryModuleId; } if (documentNumber) params.documentNumber = documentNumber; if (auditStatus !== undefined && auditStatus !== "") { params.auditStatus = Number(auditStatus); } if (dateFrom) params.dateFrom = dateFrom; if (dateTo) params.dateTo = dateTo; const response = await axios.get(`${API_BASE_URL}/api/documents/list`, { params, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); const pageData = extractApiData(response.data); if (!pageData) { return { error: '获取文档列表失败', status: response.status }; } const backendDocuments = pageData.documents || []; const totalCount = pageData.total || 0; const totalPages = pageData.totalPages || Math.ceil(totalCount / pageSize) || 0; const convertedDocuments: DocumentUI[] = backendDocuments.map((doc) => mapLeauditDocumentToUI(doc)); return { data: { documents: convertedDocuments, total: totalCount, page: pageData.page || page, totalPages } }; } catch (error) { console.error('❌ [getDocumentsListFromAPI] 获取文档列表失败:', error); // 处理axios错误 if (error && typeof error === 'object' && 'response' in error) { const axiosError = error as { response?: { data?: any; status?: number; statusText?: string } }; return { error: axiosError.response?.data?.message || axiosError.response?.statusText || '获取文档列表失败', status: axiosError.response?.status || 500 }; } return { error: error instanceof Error ? error.message : '获取文档列表失败', status: 500 }; } }