import { postgrestGet, type PostgrestParams } from '../postgrest-client'; import dayjs from 'dayjs'; import { UPLOAD_URL, API_BASE_URL } from '../../config/api-config'; import axios from 'axios'; import { CONTRACT_TYPES, DEFAULT_CONTRACT_TYPE, type ContractType } from '~/constants/contractTypes'; /** * 检查文档名称是否重复 * @param name 文档名称 * @param typeId 文档类型ID * @returns 重复检查结果 */ export async function checkDocumentDuplicate( name: string, typeId: number ): Promise<{ is_duplicate: boolean; count: number }> { try { // 获取 token let token: string | null = null; if (typeof window !== 'undefined') { token = localStorage.getItem('access_token'); } const headers: Record = { 'Accept': 'application/json' }; if (token) { headers['Authorization'] = `Bearer ${token}`; } const response = await axios.get( `${API_BASE_URL}/api/v2/documents/check-duplicate`, { params: { name, type_id: typeId }, headers } ); // 解析响应数据 const data = response.data; if (data && typeof data === 'object') { // 处理标准响应格式 { code, msg, data } if ('data' in data && data.data) { return { is_duplicate: data.data.is_duplicate ?? false, count: data.data.count ?? 0 }; } // 直接返回数据格式 return { is_duplicate: data.is_duplicate ?? false, count: data.count ?? 0 }; } return { is_duplicate: false, count: 0 }; } catch (error) { console.error('【文档重名检查】检查失败:', error); // 检查失败时默认允许上传 return { is_duplicate: false, count: 0 }; } } // import { API_BASE_URL } from '../client'; /** * 从不同格式的 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; } /** * 从 sessionStorage 获取文档类型 ID 列表(客户端专用) * @returns 文档类型 ID 数组,如果不存在则返回 null */ function getDocumentTypeIdsFromSession(): number[] | null { if (typeof window === 'undefined') { return null; // 服务端环境返回 null } try { const typeIdsStr = sessionStorage.getItem('documentTypeIds'); if (!typeIdsStr) { return null; } const typeIds = JSON.parse(typeIdsStr); if (Array.isArray(typeIds) && typeIds.every(id => typeof id === 'number')) { return typeIds; } console.warn('⚠️ [getDocumentTypeIds] documentTypeIds 格式不正确:', typeIds); return null; } catch (error) { console.error('❌ [getDocumentTypeIds] 解析 documentTypeIds 失败:', error); return null; } } function getSelectedModuleIdFromSession(): number | null { if (typeof window === 'undefined') { return null; } try { const moduleId = sessionStorage.getItem('selectedModuleId'); if (!moduleId) { return null; } const parsed = Number(moduleId); return Number.isFinite(parsed) && parsed > 0 ? parsed : null; } catch (error) { console.error('❌ [getSelectedModuleId] 解析 selectedModuleId 失败:', error); return null; } } // 文档状态枚举 export enum DocumentStatus { waiting = 'waiting', WAITING = "Waiting", QUEUED = "Queued", // 排队中 CUTTING = "Cutting", EXTRACTIONING = "Extractioning", EVALUATIONING = "Evaluationing", FAILED = "Failed", PROCESSED = "Processed" } // 文档类型接口 export interface DocumentType { id: number; name: string; code?: string; entryModuleId?: number; entryModuleName?: string | null; isEnabled?: boolean; ruleSetIds?: number[]; childDocumentTypeIds?: number[]; } export interface DocumentSubtypeGroup { id: number; name: string; code: string; documentTypeId: number; documentTypeName?: string | null; rootGroupId?: number | null; rootGroupName?: string | null; entryModuleName?: string | null; entryModuleId?: number | null; isDefault?: boolean; displayName?: string; displayHint?: string; } export interface UploadErrorDetails { title: string; summary: string; detailLines: string[]; actionLines: string[]; rawMessage: string; category: 'rule_binding' | 'rule_version' | 'entry_module' | 'permission' | 'file' | 'unknown'; } // 提取结果接口 interface ExtractedResult { [key: string]: unknown; } // 摘要接口 interface Summary { [key: string]: unknown; } // 文档接口 export interface Document { id: number; name: string; type_id: number; file_size: number; status: DocumentStatus; created_at: string; document_number?: string; path?: string; storage_type?: string; is_test_document?: boolean; evaluation_level?: string; ocr_result?: Record; extracted_results?: ExtractedResult; sumary?: Summary; remark?: string; audit_status?: number; } // 合同结构比较表接口 export interface ContractStructureComparison { id: number; template_contract_name: string; file_size: number; status: DocumentStatus; created_at: string; document_id?: number; template_contract_path?: string; ocr_results?: Record; comparison_results?: Record; } // 文件上传响应接口(兼容旧前端) export interface UploadResult { success: boolean; documentId: number; fileId: number; fileName: string; fileSize: number; typeId: number; groupId?: number | null; region: string; processingStatus: string; duplicateUpload: boolean; error?: string; } export interface UploadProgressInfo { loaded: number; total: number; percent: number; } // 旧接口上传响应(uploadContractTemplate / appendContractAttachments 仍在使用) interface LegacyUploadResponse { success: boolean; result?: { id: number; file_name: string; file_size: number; [key: string]: unknown }; error: string | null; } // 新后端上传响应 interface NewUploadResponse { documentId: number; internalDocumentNo: number; versionGroupKey: string; versionNo: number; previousVersionId: number | null; rootVersionId: number; duplicateUpload: boolean; fileId: number; typeId: number; typeCode: string; groupId?: number | null; region: string; fileName: string; ossUrl: string; speed: string; processingStatus: string; autoRunTriggered: boolean; } type UploadErrorPayload = Record | null | undefined; function readPayloadValue(payload: UploadErrorPayload, keys: string[]): unknown { if (!payload || typeof payload !== 'object') { return undefined; } for (const key of keys) { if (key in payload && payload[key] !== undefined && payload[key] !== null && payload[key] !== '') { return payload[key]; } } const nestedData = payload.data; if (nestedData && typeof nestedData === 'object') { const nestedRecord = nestedData as Record; for (const key of keys) { if (key in nestedRecord && nestedRecord[key] !== undefined && nestedRecord[key] !== null && nestedRecord[key] !== '') { return nestedRecord[key]; } } } return undefined; } function stringifyIdList(values?: number[]): string | null { if (!values || values.length === 0) { return null; } return values.join('、'); } export function buildUploadErrorDetails( rawMessage: string, options?: { documentType?: DocumentType | null; status?: number; payload?: UploadErrorPayload; } ): UploadErrorDetails { const documentType = options?.documentType ?? null; const status = options?.status; const payload = options?.payload; const message = (rawMessage || '上传失败').trim(); const normalizedMessage = message.toLowerCase(); const payloadRuleSetName = readPayloadValue(payload, ['ruleSetName', 'ruleName', 'ruleSet', 'rule_type']); const payloadRuleSetId = readPayloadValue(payload, ['ruleSetId', 'rule_set_id']); const payloadEntryModuleName = readPayloadValue(payload, ['entryModuleName', 'moduleName', 'entryModule']); const payloadEntryModuleId = readPayloadValue(payload, ['entryModuleId', 'entry_module_id']); const ruleSetDisplayName = payloadRuleSetName ? String(payloadRuleSetName) : documentType?.ruleSetIds?.length === 1 ? `规则集 ID ${documentType.ruleSetIds[0]}` : null; const entryModuleDisplayName = payloadEntryModuleName ? String(payloadEntryModuleName) : payloadEntryModuleId ? `入口模块 ID ${payloadEntryModuleId}` : documentType?.entryModuleId ? `入口模块 ID ${documentType.entryModuleId}` : null; const documentTypeDisplayName = documentType ? `${documentType.name}${documentType.code ? `(${documentType.code})` : ''}` : '当前文档类型'; if (status === 401 || normalizedMessage.includes('unauthorized') || message.includes('未登录') || message.includes('token')) { return { title: '登录状态已失效', summary: '当前登录状态已失效,系统无法继续上传文件。', detailLines: [ '请先重新登录,再重新选择文件上传。', `后端返回:${message}`, ], actionLines: ['刷新页面并重新登录后重试。'], rawMessage: message, category: 'permission', }; } if (status === 403 || message.includes('无权限') || message.includes('权限') || normalizedMessage.includes('forbidden')) { return { title: '当前账号没有上传权限', summary: '当前账号没有执行该上传操作的权限,文件尚未进入审核流程。', detailLines: [ `文档类型:${documentTypeDisplayName}`, `后端返回:${message}`, ], actionLines: ['请联系管理员检查当前账号的上传权限或入口模块授权。'], rawMessage: message, category: 'permission', }; } if ( message.includes('未绑定可用规则版本') || message.includes('未绑定规则版本') || message.includes('没有可用规则版本') || message.includes('未找到可用规则版本') ) { const detailLines = [ `文档类型:${documentTypeDisplayName}`, documentType?.ruleSetIds?.length ? `已绑定规则集:${stringifyIdList(documentType.ruleSetIds)}` : '当前文档类型还没有绑定任何规则集。', entryModuleDisplayName ? `归属入口:${entryModuleDisplayName}` : '当前文档类型还没有配置入口模块,上传入口配置也需要一起检查。', ]; if (ruleSetDisplayName || payloadRuleSetId) { detailLines.push(`疑似异常规则集:${ruleSetDisplayName || `规则集 ID ${payloadRuleSetId}`}`); } else if (documentType?.ruleSetIds && documentType.ruleSetIds.length > 1) { detailLines.push('该文档类型绑定了多个规则集,需要逐个确认可用规则数是否正常。'); } else { detailLines.push('后端没有返回具体规则集名称,建议优先检查该文档类型绑定的规则集是否存在可用规则。'); } detailLines.push(`后端返回:${message}`); return { title: '审核规则未配置完整', summary: `${documentTypeDisplayName} 目前没有可用的审核规则,所以系统无法接收本次上传。`, detailLines, actionLines: [ '到“系统设置 / 文档类型管理”检查该文档类型是否绑定了正确的规则集。', '到“规则管理”确认对应规则集的可用规则数是否正常。', '如果首页入口也异常,请同时到“系统设置 / 入口模块管理”检查入口模块绑定。', ], rawMessage: message, category: 'rule_binding', }; } if (message.includes('规则集不存在') || message.includes('规则版本不存在') || message.includes('未发布')) { const detailLines = [ `文档类型:${documentTypeDisplayName}`, ruleSetDisplayName ? `相关规则集:${ruleSetDisplayName}` : '后端未返回明确的规则集名称。', `后端返回:${message}`, ]; return { title: '规则集不可用', summary: '当前上传入口关联的规则集不可用,文件无法开始审核。', detailLines, actionLines: [ '到“规则管理”检查对应规则集是否存在、可用规则数是否正常。', '如文档类型绑定了错误的规则集,请到“系统设置 / 文档类型管理”修正绑定关系。', ], rawMessage: message, category: 'rule_version', }; } if (message.includes('入口模块')) { return { title: '入口模块配置异常', summary: '当前文档类型关联的入口模块配置异常,导致上传链路无法正常工作。', detailLines: [ `文档类型:${documentTypeDisplayName}`, entryModuleDisplayName ? `相关入口模块:${entryModuleDisplayName}` : '后端未返回明确的入口模块名称或编号。', `后端返回:${message}`, ], actionLines: [ '到“系统设置 / 入口模块管理”检查目标入口模块是否存在、是否启用、跳转路径是否正确。', '到“系统设置 / 文档类型管理”确认该文档类型绑定到了正确的入口模块。', ], rawMessage: message, category: 'entry_module', }; } if (status === 413 || message.includes('文件过大') || message.includes('too large')) { return { title: '文件过大,上传被拒绝', summary: '当前文件大小超出系统允许范围,服务器未接收该文件。', detailLines: [`后端返回:${message}`], actionLines: ['请压缩文件体积或拆分后重新上传。'], rawMessage: message, category: 'file', }; } if (message.includes('文件类型') || message.includes('格式') || status === 415) { return { title: '文件格式不支持', summary: '当前文件格式不符合上传要求,服务器拒绝处理。', detailLines: [`后端返回:${message}`], actionLines: ['请确认上传的是 PDF 或 Word 文件,并检查文件是否损坏。'], rawMessage: message, category: 'file', }; } return { title: '上传失败', summary: '文件未能成功上传到服务器,请根据下方信息检查原因。', detailLines: [ `文档类型:${documentTypeDisplayName}`, `后端返回:${message}`, ], actionLines: [ '如果是规则或入口配置问题,请先检查文档类型、规则集、入口模块三处配置。', '如暂时无法定位,可将下方后端原始提示发给后端同学继续排查。', ], rawMessage: message, category: 'unknown', }; } /** * 将文件转换为二进制数据 */ export async function uploadFileToBinary(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (reader.result instanceof ArrayBuffer) { resolve(reader.result); } else { reject(new Error('文件读取失败')); } }; reader.onerror = () => reject(new Error('文件读取失败')); reader.readAsArrayBuffer(file); }); } /** * 上传文件到文档审核系统 * @param binaryData 文件的二进制数据 * @param fileName 文件名 * @param fileType 文件类型 * @param typeId 文档类型ID * @param priority 优先级 * @param documentNumber 文档编号(可选) * @param remark 备注信息(可选) * @param isTestDocument 是否为测试文档 * @param documentId 关联的文档ID(用于合同附件上传) * @param isReupload 是否为重新上传 * @param jwtToken JWT token * @returns 上传结果 */ /** * 上传合同模板(用于与合同文档结构对比) * @param file 模板文件 * @param documentId 源合同文档ID * @param comparisonId 已有对比记录ID(可选) * @param jwtToken JWT token * @returns 上传结果 */ export async function uploadContractTemplate( file: File, documentId: number, comparisonId?: number, jwtToken?: string ): Promise<{data: LegacyUploadResponse; error?: never} | {data?: never; error: string; status?: number}> { try { console.log('【合同模板上传】开始上传模板:', { fileName: file.name, documentId, comparisonId }); // 创建FormData对象 const formData = new FormData(); // 添加文件 formData.append('file', file); // 添加上传信息 const uploadInfo = { document_id: documentId, ...(comparisonId && { comparison_id: comparisonId }) }; formData.append('upload_info', JSON.stringify(uploadInfo)); // 构建请求URL const uploadUrl = `${UPLOAD_URL}/upload_contract_template`; console.log('【合同模板上传】准备发送请求到服务器:', uploadUrl); // 设置请求头 const headers: HeadersInit = { 'Accept': 'application/json' }; // 从 localStorage 获取 token if (typeof window !== 'undefined') { const token = localStorage.getItem('access_token'); if (token) { headers['Authorization'] = `Bearer ${token}`; } } // 发送请求 const response = await axios.post(uploadUrl, formData, { headers }); console.log('【合同模板上传】服务器响应状态:', response.status); const result = response.data; console.log('【合同模板上传】服务器返回结果:', result); if (result.success) { return { data: result.result }; } else { return { error: result.error || '合同模板上传失败' }; } } catch (error) { console.error('【合同模板上传】上传过程中发生错误:', error); return { error: error instanceof Error ? error.message : '合同模板上传过程中发生未知错误' }; } } /** * 合同文档追加附件并合并 * @param documentId 合同文档ID * @param files 附件文件列表 * @param mergeMode 合并模式:'overwrite'(覆盖原文档)或 'new'(新建文档记录) * @param isReprocess 是否触发重新处理 * @param remark 备注 * @param token JWT token(可选) * @returns 上传结果 */ export async function appendContractAttachments( documentId: number, files: File[], mergeMode: 'overwrite' | 'new' = 'overwrite', isReprocess: boolean = true, remark?: string, token?: string ): Promise<{data: LegacyUploadResponse; error?: never} | {data?: never; error: string; status?: number}> { try { console.log('【合同附件追加】开始追加附件:', { documentId, fileCount: files.length, mergeMode }); // 创建FormData对象 const formData = new FormData(); // 添加多个文件 files.forEach(file => { formData.append('files', file); }); // 新链路仅保留附件追加;mergeMode / remark 在后端暂不消费,但继续保留函数签名兼容旧页面调用。 void mergeMode; void remark; // 构建请求URL const uploadUrl = `${API_BASE_URL}/api/documents/${documentId}/attachments`; console.log('【合同附件追加】准备发送请求到服务器:', uploadUrl); // 设置请求头 const headers: HeadersInit = { 'Accept': 'application/json' }; // 使用传入的 token 或从 localStorage 获取 const authToken = token || (typeof window !== 'undefined' ? localStorage.getItem('access_token') : null); if (authToken) { headers['Authorization'] = `Bearer ${authToken}`; } // 发送请求 const response = await axios.post(uploadUrl, formData, { headers }); console.log('【合同附件追加】服务器响应状态:', response.status); const result = response.data; console.log('【合同附件追加】服务器返回结果:', result); if (result?.data) { if (isReprocess) { await axios.post( `${API_BASE_URL}/api/audit/run`, { documentId, force: true, speed: 'normal', }, { headers } ); } return { data: { success: true, result: result.data, error: null, } }; } return { error: result?.message || result?.msg || '附件追加失败' }; } catch (error) { console.error('【合同附件追加】上传过程中发生错误:', error); return { error: error instanceof Error ? error.message : '附件追加过程中发生未知错误' }; } } export async function uploadDocumentToServer( binaryData: ArrayBuffer, fileName: string, fileType: string, typeId: number, groupId?: number | null, region: string = "default", createdBy?: number, attachments?: File[], autoRun: boolean = true, speed: string = "normal", jwtToken?: string, onProgress?: (progress: UploadProgressInfo) => void, ): Promise<{ data: UploadResult } | { error: string; status?: number; payload?: UploadErrorPayload }> { try { const formData = new FormData(); const blob = new Blob([binaryData], { type: fileType }); formData.append("file", blob, fileName); formData.append("typeId", String(typeId)); if (groupId) { formData.append("groupId", String(groupId)); } (attachments || []).forEach((attachment) => { formData.append("attachments", attachment); }); formData.append("region", region); formData.append("fileRole", "primary"); if (createdBy !== undefined) { formData.append("createdBy", String(createdBy)); } formData.append("autoRun", String(autoRun)); formData.append("speed", speed); const headers: Record = { "X-File-Name": encodeURIComponent(fileName), }; if (jwtToken) { headers["Authorization"] = `Bearer ${jwtToken}`; } const response = await axios.post(`${API_BASE_URL}/api/upload`, formData, { headers, onUploadProgress: (event) => { const fileBlob = formData.get("file"); const fallbackTotal = fileBlob instanceof Blob ? fileBlob.size : binaryData.byteLength; const total = Number(event.total || fallbackTotal); const loaded = Number(event.loaded || 0); if (!total || !onProgress) { return; } onProgress({ loaded, total, percent: Math.min(100, Math.max(0, Number(((loaded / total) * 100).toFixed(2)))), }); }, }); const body = response.data; // Result envelope const uploadData: NewUploadResponse | undefined = body?.data; if (!uploadData || !uploadData.documentId) { return { error: body?.message || body?.msg || "上传响应解析失败", status: response.status, payload: body }; } return { data: { success: true, documentId: uploadData.documentId, fileId: uploadData.fileId, fileName: uploadData.fileName, fileSize: binaryData.byteLength, typeId: uploadData.typeId, groupId: uploadData.groupId, region: uploadData.region, processingStatus: uploadData.processingStatus, duplicateUpload: uploadData.duplicateUpload, }, }; } catch (axiosError) { console.error("上传文档失败:", axiosError); if (axios.isAxiosError(axiosError)) { const serverMessage = (axiosError.response?.data as any)?.message || (axiosError.response?.data as any)?.msg || (axiosError.response?.data as any)?.error; return { error: serverMessage || `上传失败 (HTTP ${axiosError.response?.status || "unknown"})`, status: axiosError.response?.status, payload: axiosError.response?.data, }; } return { error: axiosError instanceof Error ? axiosError.message : "上传失败" }; } } /** * 获取当天的文档列表 * @param userInfo 用户信息(必需) * @param token JWT token * @param documentTypeIds 文档类型 ID 列表(可选) * @returns 文档列表 */ export async function getTodayDocuments( userInfo?: { user_id?: number; [key: string]: unknown }, token?: string, documentTypeIds?: number[] ): Promise<{data: Document[]; error?: never} | {data?: never; error: string; status?: number}> { try { if (!userInfo?.user_id) { return { error: "没有找到用户信息,请刷新重试", status: 401 }; } const today = dayjs().startOf("day").format("YYYY-MM-DD"); const params: Record = { page: 1, pageSize: 50, userId: userInfo.user_id, dateFrom: today, }; const selectedModuleId = getSelectedModuleIdFromSession(); if (selectedModuleId) { params.entry_module_id = selectedModuleId; } else if (documentTypeIds && documentTypeIds.length > 0) { params.type_ids = documentTypeIds.join(","); } const headers: Record = {}; if (token) { headers["Authorization"] = `Bearer ${token}`; } const response = await axios.get(`${API_BASE_URL}/api/documents/list`, { params, headers }); const body = response.data; const items = body?.data?.documents || []; const documents: Document[] = items.map((doc: any) => ({ id: doc.documentId, name: doc.fileName || doc.normalizedName || "未命名", type_id: doc.typeId || 0, file_size: doc.fileSize || 0, status: doc.processingStatus || "waiting", created_at: doc.updatedAt || "", document_number: String(doc.internalDocumentNo || ""), path: doc.ossUrl || "", audit_status: 0, })); return { data: documents }; } catch (error) { console.error("获取当天文档列表失败:", error); return { error: error instanceof Error ? error.message : "获取当天文档列表失败", status: 500, }; } } /** * 获取文档类型列表 * @param token JWT token (可选) * @returns 文档类型列表 */ export async function getDocumentTypes(token?: string): Promise<{data: DocumentType[]; error?: never} | {data?: never; error: string; status?: number}> { try { const documentTypeIds = getDocumentTypeIdsFromSession(); const selectedModuleId = getSelectedModuleIdFromSession(); const params: Record = {}; if (selectedModuleId) { params.entry_module_id = String(selectedModuleId); } const headers: Record = {}; if (token) { headers["Authorization"] = `Bearer ${token}`; } const [response, groupRoots] = await Promise.all([ axios.get(`${API_BASE_URL}/api/v3/document-type-roots`, { params, headers }), fetchAllEvaluationPointGroupRoots(token), ]); const body = response.data; if (body?.data && Array.isArray(body.data)) { let types: DocumentType[] = body.data.map((item: { id: number; name: string; code?: string; entryModuleId?: number | null; entryModuleName?: string | null; isEnabled?: boolean; ruleSetIds?: number[]; }) => { const matchedRoot = groupRoots.find((root: any) => Number(root?.id || 0) === Number(item.id)); const childDocumentTypeIds = Array.isArray(matchedRoot?.children) ? Array.from( new Set( matchedRoot.children .map((child: any) => Number(child?.document_type_id || 0)) .filter((childId: number) => childId > 0), ), ) : []; return { id: item.id, name: item.name, code: item.code, entryModuleId: item.entryModuleId ?? null, entryModuleName: item.entryModuleName ?? null, isEnabled: item.isEnabled, ruleSetIds: item.ruleSetIds, childDocumentTypeIds, }; }); if (!selectedModuleId && documentTypeIds && documentTypeIds.length > 0) { types = types.filter((item) => documentTypeIds.includes(item.id) || (item.childDocumentTypeIds || []).some((childId) => documentTypeIds.includes(childId)), ); } return { data: types }; } return { error: body?.message || "获取文档类型失败", status: response.status }; } catch (error) { console.error("获取文档类型列表失败:", error); return { error: error instanceof Error ? error.message : "获取文档类型列表失败", status: 500, }; } } function mapSubtypeChild(child: any, root?: any): DocumentSubtypeGroup { const rawName = typeof child.name === "string" ? child.name.trim() : ""; const rawCode = typeof child.code === "string" ? child.code.trim() : ""; const isDefault = rawName === "通用" || rawCode.endsWith(".default"); const entryModuleId = child.entry_module_id ?? root?.entry_module_id ?? null; const entryModuleName = child.entry_module_name ?? root?.entry_module_name ?? null; const rootGroupName = root?.name ?? null; return { id: child.id, name: child.name, code: child.code, documentTypeId: child.document_type_id, documentTypeName: child.document_type_name, rootGroupId: root?.id ?? null, rootGroupName, entryModuleId: typeof entryModuleId === "number" ? entryModuleId : null, entryModuleName, isDefault, displayName: isDefault ? `默认子类型(${rawName || "通用"})` : rawName || child.name, displayHint: [rootGroupName, child.document_type_name, entryModuleName, rawCode].filter(Boolean).join(" · "), }; } function dedupeSubtypeGroups(groups: DocumentSubtypeGroup[]): DocumentSubtypeGroup[] { const groupMap = new Map(); groups.forEach((group) => { const existing = groupMap.get(group.id); if (!existing) { groupMap.set(group.id, group); return; } const currentScore = (group.rootGroupName ? 2 : 0) + (group.entryModuleId ? 1 : 0); const existingScore = (existing.rootGroupName ? 2 : 0) + (existing.entryModuleId ? 1 : 0); if (currentScore > existingScore) { groupMap.set(group.id, group); } }); return Array.from(groupMap.values()); } async function fetchAllEvaluationPointGroupRoots(token?: string): Promise { const headers: Record = {}; if (token) { headers["Authorization"] = `Bearer ${token}`; } const allResponse = await axios.get(`${API_BASE_URL}/api/v3/evaluation-point-groups/all`, { params: { include_disabled: false, with_rule_count: false, }, headers, }); return extractApiData(allResponse.data) || []; } function collectSubtypeGroupsFromRoots( roots: any[], rootOrDocumentTypeId: number, entryModuleId?: number | null, ): DocumentSubtypeGroup[] { const scopedRoots = roots.filter((root: any) => { if (entryModuleId && Number(root?.entry_module_id || 0) !== Number(entryModuleId)) { return false; } return true; }); const matchedRoot = scopedRoots.find((root: any) => Number(root?.id || 0) === Number(rootOrDocumentTypeId)); if (matchedRoot && Array.isArray(matchedRoot.children)) { return dedupeSubtypeGroups(matchedRoot.children.map((child: any) => mapSubtypeChild(child, matchedRoot))); } return dedupeSubtypeGroups( scopedRoots.flatMap((root: any) => { if (!Array.isArray(root?.children)) return []; return root.children .filter((child: any) => Number(child?.document_type_id || 0) === Number(rootOrDocumentTypeId)) .map((child: any) => mapSubtypeChild(child, root)); }), ); } export async function getDocumentSubtypeGroupsMap( documentTypeIds: number[], token?: string, ): Promise<{ data: Record; error?: never } | { data?: never; error: string; status?: number }> { try { if (!documentTypeIds.length) { return { data: {} }; } const roots = await fetchAllEvaluationPointGroupRoots(token); const ids = Array.from(new Set(documentTypeIds.map((id) => Number(id)).filter(Boolean))); const grouped = Object.fromEntries( ids.map((documentTypeId) => [documentTypeId, collectSubtypeGroupsFromRoots(roots, documentTypeId)]), ); return { data: grouped }; } catch (error) { console.error("批量获取子类型分组失败:", error); return { error: error instanceof Error ? error.message : "批量获取子类型分组失败", status: 500, }; } } export async function getDocumentSubtypeGroups( documentTypeId: number, token?: string, entryModuleId?: number | null, ): Promise<{ data: DocumentSubtypeGroup[]; error?: never } | { data?: never; error: string; status?: number }> { try { const headers: Record = {}; if (token) { headers["Authorization"] = `Bearer ${token}`; } const allRoots = await fetchAllEvaluationPointGroupRoots(token); const matchedFromTree = collectSubtypeGroupsFromRoots(allRoots, documentTypeId, entryModuleId); if (matchedFromTree.length > 0) { return { data: matchedFromTree }; } const response = await axios.get(`${API_BASE_URL}/api/v3/evaluation-point-groups/by-document-types`, { params: { document_type_ids: String(documentTypeId), include_disabled: false, with_rule_count: false, }, headers, }); const roots = extractApiData(response.data) || []; const filteredRoots = entryModuleId ? roots.filter((root: any) => Number(root?.entry_module_id || 0) === Number(entryModuleId)) : roots; const fallbackRoots = filteredRoots.length > 0 ? filteredRoots : roots; const groups = dedupeSubtypeGroups( fallbackRoots.flatMap((root: any) => Array.isArray(root?.children) ? root.children .filter((child: any) => Number(child?.document_type_id || 0) === Number(documentTypeId)) .map((child: any) => mapSubtypeChild(child, root)) : [], ), ); return { data: groups }; } catch (error) { console.error("获取子类型分组失败:", error); return { error: error instanceof Error ? error.message : "获取子类型分组失败", status: 500, }; } } /** * 获取指定文档的状态 * @param documentIds 文档ID列表 * @param attachmentIds 合同附件ID列表(可选) * @param token JWT token (可选) * @returns 文档状态列表 */ export async function getDocumentsStatus( documentIds: number[], attachmentIds?: number[], token?: string ): Promise<{data: Document[]; error?: never} | {data?: never; error: string; status?: number}> { try { if ((!documentIds || documentIds.length === 0) && (!attachmentIds || attachmentIds.length === 0)) { return { data: [] }; } const headers: Record = {}; if (token) { headers["Authorization"] = `Bearer ${token}`; } const normalizedIds = Array.from(new Set(documentIds.filter(id => Number.isFinite(id) && id > 0))); const allData: Document[] = []; if (normalizedIds.length > 0) { const response = await axios.get(`${API_BASE_URL}/api/documents/status`, { params: { ids: normalizedIds.join(",") }, headers, }); const statusItems = extractApiData>(response.data) || []; statusItems.forEach(item => { allData.push({ id: item.documentId, name: `文档_${item.documentId}`, type_id: 0, file_size: 0, status: (item.processingStatus as DocumentStatus) || DocumentStatus.waiting, created_at: item.updatedAt || "", }); }); } if (attachmentIds && attachmentIds.length > 0) { const attachmentParams: PostgrestParams = { select: 'id, status', filter: { 'id': `in.(${attachmentIds.join(',')})` } }; const attachmentResponse = await postgrestGet('/api/postgrest/proxy/contract_structure_comparison', { ...attachmentParams, token }); if (attachmentResponse.error) { return { error: attachmentResponse.error, status: attachmentResponse.status }; } const extractedAttachments = extractApiData(attachmentResponse.data) || []; extractedAttachments.forEach(item => { allData.push({ id: item.id, name: item.template_contract_name || `合同结构比较记录_${item.id}`, type_id: 1, file_size: item.file_size || 0, status: item.status, created_at: item.created_at }); }); } return { data: allData }; } catch (error) { console.error('获取文档状态失败:', error); return { error: error instanceof Error ? error.message : '获取文档状态失败', status: 500 }; } }