feat: stabilize document type and upload flows
This commit is contained in:
+297
-46
@@ -110,6 +110,25 @@ function getDocumentTypeIdsFromSession(): number[] | 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',
|
||||
@@ -132,6 +151,15 @@ export interface DocumentType {
|
||||
ruleSetIds?: number[];
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -216,6 +244,223 @@ interface NewUploadResponse {
|
||||
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',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文件转换为二进制数据
|
||||
*/
|
||||
@@ -406,7 +651,7 @@ export async function uploadDocumentToServer(
|
||||
autoRun: boolean = true,
|
||||
speed: string = "normal",
|
||||
jwtToken?: string,
|
||||
): Promise<{ data: UploadResult } | { error: string; status?: number }> {
|
||||
): Promise<{ data: UploadResult } | { error: string; status?: number; payload?: UploadErrorPayload }> {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
const blob = new Blob([binaryData], { type: fileType });
|
||||
@@ -433,7 +678,7 @@ export async function uploadDocumentToServer(
|
||||
// Result<DocumentUploadVO> envelope
|
||||
const uploadData: NewUploadResponse | undefined = body?.data;
|
||||
if (!uploadData || !uploadData.documentId) {
|
||||
return { error: body?.message || "上传响应解析失败", status: response.status };
|
||||
return { error: body?.message || body?.msg || "上传响应解析失败", status: response.status, payload: body };
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -454,10 +699,12 @@ export async function uploadDocumentToServer(
|
||||
if (axios.isAxiosError(axiosError)) {
|
||||
const serverMessage =
|
||||
(axiosError.response?.data as any)?.message ||
|
||||
(axiosError.response?.data as any)?.msg;
|
||||
(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 : "上传失败" };
|
||||
@@ -489,8 +736,11 @@ export async function getTodayDocuments(
|
||||
dateFrom: today,
|
||||
};
|
||||
|
||||
if (documentTypeIds && documentTypeIds.length > 0) {
|
||||
params.typeCode = ""; // 后续可按 typeId→typeCode 映射
|
||||
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> = {};
|
||||
@@ -532,8 +782,11 @@ export async function getTodayDocuments(
|
||||
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 (documentTypeIds && documentTypeIds.length > 0) {
|
||||
if (selectedModuleId) {
|
||||
params.entry_module_id = String(selectedModuleId);
|
||||
} else if (documentTypeIds && documentTypeIds.length > 0) {
|
||||
params.ids = documentTypeIds.join(",");
|
||||
}
|
||||
|
||||
@@ -582,23 +835,37 @@ export async function getDocumentsStatus(
|
||||
if ((!documentIds || documentIds.length === 0) && (!attachmentIds || attachmentIds.length === 0)) {
|
||||
return { data: [] };
|
||||
}
|
||||
|
||||
// 查询主文档状态
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let documentsResponse: any = { data: [], error: undefined, status: undefined };
|
||||
if (documentIds && documentIds.length > 0) {
|
||||
const documentsParams: PostgrestParams = {
|
||||
select: 'id, status',
|
||||
filter: {
|
||||
'id': `in.(${documentIds.join(',')})`
|
||||
}
|
||||
};
|
||||
documentsResponse = await postgrestGet<Document[]>('/api/postgrest/proxy/documents', { ...documentsParams, token });
|
||||
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 || "",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 查询合同附件状态
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let attachmentResponse: any = { data: [], error: undefined, status: undefined };
|
||||
if (attachmentIds && attachmentIds.length > 0) {
|
||||
const attachmentParams: PostgrestParams = {
|
||||
select: 'id, status',
|
||||
@@ -606,38 +873,22 @@ export async function getDocumentsStatus(
|
||||
'id': `in.(${attachmentIds.join(',')})`
|
||||
}
|
||||
};
|
||||
attachmentResponse = await postgrestGet<ContractStructureComparison[]>('/api/postgrest/proxy/contract_structure_comparison', { ...attachmentParams, token });
|
||||
}
|
||||
|
||||
if (documentsResponse.error && attachmentResponse.error) {
|
||||
return { error: documentsResponse.error || attachmentResponse.error, status: documentsResponse.status || attachmentResponse.status };
|
||||
}
|
||||
|
||||
let allData: Document[] = [];
|
||||
|
||||
// 处理主文档数据
|
||||
if (!documentsResponse.error && documentsResponse.data) {
|
||||
const extractedDocuments = extractApiData<Document[]>(documentsResponse.data);
|
||||
if (extractedDocuments) {
|
||||
allData = [...allData, ...extractedDocuments];
|
||||
const attachmentResponse = await postgrestGet<ContractStructureComparison[]>('/api/postgrest/proxy/contract_structure_comparison', { ...attachmentParams, token });
|
||||
if (attachmentResponse.error) {
|
||||
return { error: attachmentResponse.error, status: attachmentResponse.status };
|
||||
}
|
||||
}
|
||||
|
||||
// 处理合同附件数据
|
||||
if (!attachmentResponse.error && attachmentResponse.data) {
|
||||
const extractedAttachments = extractApiData<ContractStructureComparison[]>(attachmentResponse.data);
|
||||
if (extractedAttachments) {
|
||||
// 将ContractStructureComparison转换为Document格式
|
||||
const convertedAttachments: Document[] = extractedAttachments.map(item => ({
|
||||
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
|
||||
}));
|
||||
allData = [...allData, ...convertedAttachments];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return { data: allData };
|
||||
@@ -648,4 +899,4 @@ export async function getDocumentsStatus(
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user