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

806 lines
24 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, postgrestDelete, postgrestPut, postgrestPost } 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 './files-upload';
/**
* 从不同格式的 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;
}
/**
* 数据库文档结构
*/
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;
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;
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;
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;
updatedAt?: string | null;
hasHistory?: boolean;
totalVersions?: number;
historyVersions?: LeauditHistoryVersion[];
}
interface LeauditListPage {
total: number;
page: number;
pageSize: number;
totalPages: number;
documents: LeauditListItem[];
}
/**
* 获取文件扩展名
* @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<string, string> = {
'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 '';
}
/**
* 获取评查结果
* @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<DocumentUI> {
// 获取文档类型信息
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 };
}
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 };
}
const response = await postgrestGet<Document[]>(
'/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<Document[]>(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<Document[]>(
// '/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<Document[]>(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<DocumentUI> & { 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 };
}
// 准备API数据 - 将UI数据转换为API格式
// 根据文档,可更新字段:document_number, audit_status, is_test_document, remark
const apiDocument: Partial<Document> = {};
if (document.documentNumber !== undefined) {
apiDocument.document_number = document.documentNumber;
}
if (document.auditStatus !== undefined) {
apiDocument.audit_status = document.auditStatus;
}
if (document.isTest !== undefined) {
apiDocument.is_test_document = document.isTest;
}
if (document.remark !== undefined) {
apiDocument.remark = document.remark;
}
// console.log('📤 [updateDocument] 更新文档API数据:', apiDocument);
// 使用 axios-client 的 apiRequest 方法(支持自定义 headers
// 接口路径: /api/postgrest/proxy/documents?id=eq.{id}
// 后端会自动注入 user_id 过滤条件(根据JWT中的用户信息)
const { apiRequest } = await import('../axios-client');
const response = await apiRequest<Document[]>(
`/api/postgrest/proxy/documents?id=eq.${id}`,
{
method: 'PATCH',
data: apiDocument,
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数组
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,
auditStatus,
fileStatus,
dateFrom,
dateTo,
token
} = searchParams;
const params: Record<string, any> = {
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 === 1) {
const typeResponse = await getDocumentTypes({ ids: documentTypeIds, page: 1, pageSize: 10 }, token);
const matchedType = typeResponse.data?.types?.[0];
if (matchedType?.code) {
params.typeCode = matchedType.code;
}
}
// 下面几个旧筛选项在新系统版列表接口里暂未一一对齐:
// - documentNumber
// - auditStatus
// - dateFrom/dateTo
// - 多个 documentTypeIds 的组合筛选
// 先保留参数签名,后续再单独接新后端的类型/状态体系。
void documentNumber;
void auditStatus;
void dateFrom;
void dateTo;
const axios = await import('axios').then(m => m.default);
const response = await axios.get(`${API_BASE_URL}/api/documents/list`, {
params,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const pageData = extractApiData<LeauditListPage>(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) => {
const historyVersions: DocumentVersionUI[] = (doc.historyVersions || []).map((hv) => ({
id: hv.documentId,
name: hv.fileName || doc.fileName || doc.normalizedName || '未命名文档',
documentNumber: buildDocumentNumber(hv),
type: doc.typeId?.toString() || '',
typeName: typeNameFromCode(doc.typeCode),
size: 0,
auditStatus: mapLeauditDocToAuditStatus({
processingStatus: hv.processingStatus,
runStatus: hv.runStatus,
passedCount: null,
failedCount: null,
}),
fileStatus: mapProcessingStatusToFileStatus(hv.processingStatus),
issues: null,
uploadTime: formatDate(hv.updatedAt || ''),
fileType: hv.fileExt || getFileExtension(hv.fileName || doc.fileName || ''),
path: '',
isTest: false,
updatedAt: formatDate(hv.updatedAt || ''),
pageCount: 0,
ocrResult: undefined,
versionNumber: hv.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
}));
return {
id: doc.documentId,
name: doc.fileName || doc.normalizedName || '未命名文档',
documentNumber: buildDocumentNumber(doc),
type: doc.typeId?.toString() || '',
typeName: typeNameFromCode(doc.typeCode),
size: doc.fileSize || 0,
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: false,
updatedAt: formatDate(doc.updatedAt || ''),
pageCount: 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
};
});
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
};
}
}