Files
leaudit-platform-frontend/app/api/files/files-upload.ts
T

1160 lines
37 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { postgrestGet, 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<string, string> = {
'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<T>(responseData: unknown): T | null {
if (!responseData) return null;
// 格式1: { code: number, msg: string, data: T }
if (typeof responseData === 'object' && responseData !== null &&
'code' in responseData &&
'data' in responseData &&
(responseData as { data: unknown }).data) {
return (responseData as { data: T }).data;
}
// 格式2: 直接是数据对象
return responseData as T;
}
/**
* 从 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<string, string>;
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<string, unknown>;
comparison_results?: Record<string, unknown>;
}
// 文件上传响应接口(兼容旧前端)
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<string, unknown> | 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<string, unknown>;
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<ArrayBuffer> {
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<string, string> = {
"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<DocumentUploadVO> 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<string, string | number> = {
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<string, string> = {};
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<string, string> = {};
if (selectedModuleId) {
params.entry_module_id = String(selectedModuleId);
}
const headers: Record<string, string> = {};
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<number, DocumentSubtypeGroup>();
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<any[]> {
const headers: Record<string, string> = {};
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<any[]>(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<number, DocumentSubtypeGroup[]>; 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<string, string> = {};
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<any[]>(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<string, string> = {};
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<Array<{
documentId: number;
processingStatus?: string | null;
updatedAt?: string | null;
}>>(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<ContractStructureComparison[]>('/api/postgrest/proxy/contract_structure_comparison', { ...attachmentParams, token });
if (attachmentResponse.error) {
return { error: attachmentResponse.error, status: attachmentResponse.status };
}
const extractedAttachments = extractApiData<ContractStructureComparison[]>(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
};
}
}