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
+15 -1
View File
@@ -40,6 +40,9 @@ export interface RuleSetOption {
ruleType: string;
ruleName: string;
status: string;
currentVersionId?: number | null;
fallbackVersionId?: number | null;
hasUsableVersion?: boolean;
}
export interface EntryModuleOption {
@@ -224,11 +227,19 @@ export async function getEntryModules(
): Promise<{ data?: EntryModuleOption[]; error?: string }> {
try {
const response = await axios.get(`${API_BASE_URL}/api/v3/entry-modules`, {
params: { page: 1, page_size: 200 },
headers: authHeaders(token),
});
const items = extractData<any[]>(response) || [];
const payload = extractData<any>(response);
const items = Array.isArray(payload)
? payload
: Array.isArray(payload?.items)
? payload.items
: [];
return { data: items.map((m: any) => ({ id: m.id, name: m.name })) };
} catch (error) {
console.error("获取入口模块失败:", error);
return { error: error instanceof Error ? error.message : "获取入口模块失败" };
}
}
@@ -250,6 +261,9 @@ export async function getRuleSets(
ruleType: r.ruleType,
ruleName: r.ruleName,
status: r.status,
currentVersionId: r.currentVersionId ?? null,
fallbackVersionId: r.fallbackVersionId ?? null,
hasUsableVersion: !!r.hasUsableVersion,
})),
};
} catch (error) {
+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: {
+297 -46
View File
@@ -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
};
}
}
}
+12 -2
View File
@@ -4,6 +4,12 @@
import axios from 'axios';
import { API_BASE_URL } from '../config/api-config';
interface ApiResult<T> {
code: number;
message: string;
data: T | null;
}
/**
* 队列状态响应接口
*/
@@ -43,12 +49,16 @@ export async function getQueueStatus(): Promise<{ data?: QueueStatus; error?: st
headers['Authorization'] = `Bearer ${token}`;
}
const response = await axios.get<QueueStatus>(
const response = await axios.get<ApiResult<QueueStatus>>(
`${API_BASE_URL}/api/v2/system/queue/status`,
{ headers }
);
return { data: response.data };
if (response.data?.data) {
return { data: response.data.data };
}
return { error: response.data?.message || '队列状态响应格式错误' };
} catch (error) {
// 队列接口暂未迁移,404 时返回空状态不报错
if (axios.isAxiosError(error) && error.response?.status === 404) {
+50 -2
View File
@@ -516,7 +516,8 @@ export async function getRoleRoutesWithPermissions(roleId: number): Promise<{
*/
export async function saveRoleApiPermissions(
roleId: number,
permissionIds: number[]
permissionIds: number[],
scopePermissionIds: number[] = []
): Promise<{ success: boolean; message: string }> {
try {
// 构建权限配置
@@ -530,7 +531,8 @@ export async function saveRoleApiPermissions(
const response = await post<any>('/api/v3/rbac/role-permissions', {
role_id: roleId,
permissions,
replace: true // 替换模式:先删除现有权限,再插入新权限
replace: true, // 替换模式:仅替换当前页面涉及的权限范围
replace_scope_permission_ids: scopePermissionIds
});
if (response.error) {
@@ -555,6 +557,52 @@ export async function saveRoleApiPermissions(
}
}
/**
* 原子保存角色的菜单权限与 API 权限
* @param roleId 角色ID
* @param routeIds 路由ID数组
* @param permissionIds 权限ID数组
* @param scopePermissionIds 本页允许被替换清理的权限范围
*/
export async function saveRoleAccess(
roleId: number,
routeIds: number[],
permissionIds: number[],
scopePermissionIds: number[] = []
): Promise<{ success: boolean; message: string; code?: number }> {
try {
const response = await post<any>(`/api/v3/rbac/roles/${roleId}/access`, {
route_ids: routeIds,
permission_ids: permissionIds,
route_permission: 'RW',
replace_scope_permission_ids: scopePermissionIds
});
if (response.error) {
throw new Error(response.error);
}
if (response.data?.code && response.data.code !== 200) {
return {
success: false,
message: response.data.message || '角色权限保存失败',
code: response.data.code
};
}
return {
success: true,
message: response.data?.message || '角色权限保存成功'
};
} catch (error) {
console.error('❌ [saveRoleAccess] 保存角色联合权限失败:', error);
return {
success: false,
message: error instanceof Error ? error.message : '角色权限保存失败'
};
}
}
/**
* 更新角色的路由权限 - v3.2更新
* @param roleId 角色ID