feat: stabilize document type and upload flows

This commit is contained in:
wren
2026-04-30 17:44:05 +08:00
parent 81c5e98b53
commit 3fb7e9f5d0
18 changed files with 2122 additions and 491 deletions
+205 -131
View File
@@ -1,4 +1,5 @@
import { postgrestGet, postgrestDelete, postgrestPut, postgrestPost } from '../postgrest-client';
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';
@@ -181,6 +182,21 @@ interface LeauditListPage {
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 文件名
@@ -246,6 +262,92 @@ function buildDocumentNumber(doc: LeauditListItem | LeauditHistoryVersion): stri
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: typeNameFromCode(source.typeCode),
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: typeNameFromCode(doc.typeCode),
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
@@ -360,21 +462,36 @@ export async function deleteDocument(id: string, userId: string, token?: string)
return { error: '用户身份验证失败', status: 401 };
}
const response = await postgrestDelete(
'/api/postgrest/proxy/documents',
{
filter: {
'id': `eq.${id}`,
'user_id': `eq.${userId}` // 确保只能删除自己的文档
},
token
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);
@@ -404,22 +521,39 @@ export async function getDocument(id: string, userId: string, frontendJWT?: stri
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
try {
const response = await axios.get(`${API_BASE_URL}/api/documents/${id}`, {
headers: frontendJWT ? { 'Authorization': `Bearer ${frontendJWT}` } : undefined
});
const detail = extractApiData<LeauditDocumentDetail>(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<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 };
@@ -555,54 +689,68 @@ export async function updateDocument(id: string, document: Partial<DocumentUI> &
return { error: '用户身份验证失败', status: 401 };
}
// 准备API数据 - 将UI数据转换为API格式
// 根据文档,可更新字段:document_number, audit_status, is_test_document, remark
const apiDocument: Partial<Document> = {};
const apiDocument: DocumentMetadataUpdateDTO = {};
if (document.documentNumber !== undefined) {
apiDocument.document_number = document.documentNumber;
apiDocument.documentNumber = document.documentNumber;
}
if (document.auditStatus !== undefined) {
apiDocument.audit_status = document.auditStatus;
apiDocument.auditStatus = document.auditStatus;
}
if (document.isTest !== undefined) {
apiDocument.is_test_document = document.isTest;
apiDocument.isTestDocument = 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,
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
};
}
);
if (response.error) {
console.error('❌ [updateDocument] 更新文档API错误:', response.error);
return { error: response.error, status: response.status };
}
// 旧链路回退:仅允许修改自己的文档;新链路上线后应由后端基于地区/角色做数据隔离。
const { apiRequest } = await import('../axios-client');
const response = await apiRequest<Document[]>(
`/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}` } : {})
}
}
);
// 检查返回数据
// 成功时返回更新后的文档数组,空数组表示文档不存在或无权访问
const responseData = response.data;
if (!responseData || (Array.isArray(responseData) && responseData.length === 0)) {
return { error: '文档不存在或无权访问', status: 404 };
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 };
}
}
// 获取更新后的完整文档数据(包含关联的文档类型信息)
@@ -672,26 +820,18 @@ export async function getDocumentsListFromAPI(searchParams: {
}
}
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;
}
if (documentTypeIds && documentTypeIds.length > 0) {
params.type_ids = documentTypeIds.join(',');
}
// 下面几个旧筛选项在新系统版列表接口里暂未一一对齐:
// 下面几个旧筛选项暂未完全对齐:
// - documentNumber
// - auditStatus
// - dateFrom/dateTo
// - 多个 documentTypeIds 的组合筛选
// 先保留参数签名,后续再单独接新后端的类型/状态体系。
void documentNumber;
void auditStatus;
void dateFrom;
void dateTo;
if (dateFrom) params.dateFrom = dateFrom;
if (dateTo) params.dateTo = dateTo;
const axios = await import('axios').then(m => m.default);
const response = await axios.get(`${API_BASE_URL}/api/documents/list`, {
params,
headers: {
@@ -709,73 +849,7 @@ export async function getDocumentsListFromAPI(searchParams: {
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
};
});
const convertedDocuments: DocumentUI[] = backendDocuments.map((doc) => mapLeauditDocumentToUI(doc));
return {
data: {