Compare commits

..

3 Commits

Author SHA1 Message Date
wren 7fdb7386ee fix: reconnect cross checking upload flow 2026-05-07 19:26:37 +08:00
wren ba113a0e24 fix: restore cross checking org tree 2026-05-07 19:14:14 +08:00
wren 050aa679be fix: stabilize rule editor yaml roundtrip 2026-05-07 18:58:55 +08:00
5 changed files with 371 additions and 125 deletions
+37 -48
View File
@@ -116,39 +116,16 @@ export async function uploadCrossCheckingDocument(
token: string | null = null
): Promise<{data: CrossCheckingFileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> {
try {
console.log('【交叉评查上传】开始上传文档:', { fileName, fileSize: binaryData.byteLength, typeId });
// 创建FormData对象
const formData = new FormData();
// 将二进制数据转换为Blob并添加到FormData
const blob = new Blob([binaryData], { type: fileType });
formData.append('file', blob, fileName);
console.log('【交叉评查上传】Blob已创建,文件大小:', blob.size);
formData.append('typeId', String(typeId));
formData.append('region', 'default');
formData.append('fileRole', 'primary');
formData.append('autoRun', 'true');
formData.append('speed', priority === 'urgent' ? 'urgent' : 'normal');
// 将信息添加到一个JSON对象中
const uploadInfo = {
type_id: typeId,
evaluation_level: priority,
document_number: documentNumber || null,
remark: remark || null,
is_test_document: isTestDocument,
document_id: documentId || null,
is_reupload: isReupload
};
// 添加JSON字符串到FormData
formData.append('upload_info', JSON.stringify(uploadInfo));
console.log('【交叉评查上传】FormData准备完成:', JSON.stringify(uploadInfo));
// 根据是否有documentId决定使用哪个接口
const uploadEndpoint = '/batch_upload';
const uploadUrl = UPLOAD_URL + uploadEndpoint;
console.log('【交叉评查上传】准备发送请求到服务器:', uploadUrl);
// 发送请求
try {
console.log('【交叉评查上传】开始axios请求...');
const headers: Record<string, string> = {
'X-File-Name': encodeURIComponent(fileName),
};
@@ -157,30 +134,43 @@ export async function uploadCrossCheckingDocument(
headers['Authorization'] = `Bearer ${token}`;
}
const response = await axios.post(uploadUrl, formData, {
const response = await axios.post(`${API_BASE_URL}/api/upload`, formData, {
headers
});
console.log('【交叉评查上传】收到服务器响应:', { status: response.status, statusText: response.statusText });
console.log('【交叉评查上传】JSON响应解析成功:', response.data);
const extractedData = extractApiData<CrossCheckingFileUploadResponse>(response.data);
console.log('【交叉评查上传】提取的数据:', extractedData);
if (!extractedData) {
console.error('【交叉评查上传】无法提取数据');
return { error: '处理上传响应失败', status: 500 };
const uploadData = response.data?.data;
if (!uploadData?.documentId) {
return { error: response.data?.message || response.data?.msg || '处理上传响应失败', status: response.status };
}
console.log('【交叉评查上传】上传成功,返回数据');
return { data: extractedData as CrossCheckingFileUploadResponse };
} catch (axiosError) {
console.error('【交叉评查上传】axios请求失败:', axiosError);
if (axios.isAxiosError(axiosError)) {
const errorText = axiosError.response?.data || axiosError.message;
return {
error: `上传失败: ${axiosError.response?.status || 500} ${axiosError.response?.statusText || ''} - ${errorText}`,
data: {
success: true,
result: {
id: Number(uploadData.documentId),
file_name: uploadData.fileName || fileName,
file_size: binaryData.byteLength,
file_url: uploadData.storagePath || '',
type_id: Number(uploadData.typeId || typeId),
type_description: uploadData.typeCode || '',
document_number: documentNumber || null,
storage_type: 'oss',
is_test_document: isTestDocument,
remark: remark || null,
background_processing: true,
evaluation_level: priority,
},
error: null,
}
};
} catch (axiosError) {
if (axios.isAxiosError(axiosError)) {
const errorText =
axiosError.response?.data?.message ||
axiosError.response?.data?.msg ||
axiosError.response?.data?.detail ||
axiosError.message;
return {
error: `上传失败: ${errorText}`,
status: axiosError.response?.status || 500
};
}
@@ -190,7 +180,6 @@ export async function uploadCrossCheckingDocument(
};
}
} catch (error) {
console.error('【交叉评查上传】上传过程中发生错误:', error);
return {
error: error instanceof Error ? error.message : '上传失败',
status: 500
+17 -26
View File
@@ -946,33 +946,24 @@ export async function updateDocumentAuditStatus(id: string, auditStatus: number,
*/
export async function getCrossCheckingDocumentTypes(jwtToken?: string): Promise<ApiResponse<DocumentType[]>> {
try {
// console.log('[getCrossCheckingDocumentTypes] 开始获取交叉评查文档类型');
const response = await postgrestGet<DocumentType>('/api/postgrest/proxy/document_types',{
select: 'id,name,code,evaluation_point_groups_ids',
filter: {
evaluation_point_groups_ids: 'not.is.null'
},
token: jwtToken
const response = await axios.get<{ data?: Array<{
id: number;
name: string;
code: string;
isEnabled?: boolean;
ruleSetIds?: number[];
}> }>(`${API_BASE_URL}/api/document-types`, {
headers: jwtToken ? { Authorization: `Bearer ${jwtToken}` } : undefined,
});
if (response.error) {
console.error('[getCrossCheckingDocumentTypes] 获取失败:', response.error);
return {
success: false,
error: response.error
};
}
// 进一步过滤,确保 evaluation_point_groups_ids 是非空数组
const dataArray = Array.isArray(response.data) ? response.data : [];
const filteredData = dataArray.filter(
(item: DocumentType) => item.evaluation_point_groups_ids &&
Array.isArray(item.evaluation_point_groups_ids) &&
item.evaluation_point_groups_ids.length > 0
);
// console.log('[getCrossCheckingDocumentTypes] 获取成功,共', filteredData.length, '个文档类型');
const rawItems = Array.isArray(response.data?.data) ? response.data.data : [];
const filteredData = rawItems
.filter((item) => item && item.isEnabled !== false)
.map((item) => ({
id: item.id,
name: item.name,
code: item.code,
}))
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
return {
success: true,
+253 -29
View File
@@ -62,6 +62,207 @@ export interface ApiResponse<T> {
status?: number;
}
type RbacUserRole = {
role_id?: number;
role_key?: string;
role_name?: string;
};
type RbacUserItem = {
id: number;
username: string;
nick_name: string;
area: string;
ou_id: string | null;
ou_name: string | null;
is_leader: boolean;
status: number;
tenant_name: string | null;
dep_name: string | null;
dep_short_name?: string | null;
email?: string | null;
phone_number?: string | null;
roles?: RbacUserRole[];
};
type RbacUsersPayload = {
total: number;
page: number;
page_size: number;
items: RbacUserItem[];
};
let rbacUsersAvailable: boolean | null = null;
function normalizeUser(user: RbacUserItem): UserInfo {
return {
id: user.id,
username: user.username,
nick_name: user.nick_name,
area: user.area,
ou_id: user.ou_id || '',
ou_name: user.ou_name || '',
is_leader: Boolean(user.is_leader),
status: Number(user.status ?? 0),
tenant_name: user.tenant_name ?? null,
dep_name: user.dep_name ?? null,
dep_short_name: user.dep_short_name ?? null,
email: user.email ?? undefined,
phone_number: user.phone_number ?? undefined,
organization_path: {
tenant_name: user.tenant_name || '未分组租户',
dep_name: user.dep_name || '未分组部门',
dep_short_name: user.dep_short_name || user.dep_name || '未分组部门',
ou_name: user.ou_name || '未分组组织',
},
};
}
async function fetchRbacUsers(
jwtToken?: string,
search?: string,
): Promise<ApiResponse<RbacUsersPayload>> {
try {
const params = new URLSearchParams();
params.set('page', '1');
params.set('pageSize', '5000');
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (jwtToken) {
headers.Authorization = `Bearer ${jwtToken}`;
}
const response = await axios.get<{ code?: number; message?: string; data?: RbacUsersPayload }>(
`${API_BASE_URL}/api/v3/rbac/users?${params.toString()}`,
{ headers },
);
if (!response.data?.data) {
return {
success: false,
error: response.data?.message || '用户列表返回为空',
};
}
const keyword = search?.trim().toLowerCase();
const items = keyword
? response.data.data.items.filter((item) => {
const haystacks = [
item.username,
item.nick_name,
item.area,
item.tenant_name,
item.dep_name,
item.ou_name,
];
return haystacks.some((value) => value?.toLowerCase().includes(keyword));
})
: response.data.data.items;
return {
success: true,
data: {
...response.data.data,
total: items.length,
items,
},
};
} catch (error) {
let errorMessage = '获取用户列表失败';
if (axios.isAxiosError(error)) {
errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message;
} else if (error instanceof Error) {
errorMessage = error.message;
}
return {
success: false,
error: errorMessage,
};
}
}
function buildOrganizationTreeFromUsers(
users: UserInfo[],
includeUsers: boolean,
rootUuid?: string,
): OrganizationResponse {
const tenantMap = new Map<string, OrganizationNode>();
const ensureChild = (
parent: OrganizationNode,
key: string,
create: () => OrganizationNode,
): OrganizationNode => {
const existing = parent.children.find((item) => item.ou_id === key);
if (existing) return existing;
const next = create();
parent.children.push(next);
return next;
};
users.forEach((user) => {
const tenantName = user.tenant_name || user.area || '未分组租户';
const depName = user.dep_name || '未分组部门';
const ouId = user.ou_id || `ou_${depName}`;
const ouName = user.ou_name || depName || '未分组组织';
const tenantKey = `tenant:${tenantName}`;
let tenantNode = tenantMap.get(tenantKey);
if (!tenantNode) {
tenantNode = {
ou_id: tenantKey,
ou_name: tenantName,
parent_ou_id: null,
level: 1,
children: [],
users: [],
};
tenantMap.set(tenantKey, tenantNode);
}
const depKey = `dep:${tenantName}:${depName}`;
const depNode = ensureChild(tenantNode, depKey, () => ({
ou_id: depKey,
ou_name: depName,
parent_ou_id: tenantNode!.ou_id,
level: 2,
children: [],
users: [],
}));
const orgKey = ouId.startsWith('ou_') ? `org:${tenantName}:${depName}:${ouName}` : ouId;
const orgNode = ensureChild(depNode, orgKey, () => ({
ou_id: orgKey,
ou_name: ouName,
parent_ou_id: depNode.ou_id,
level: 3,
children: [],
users: [],
}));
if (includeUsers) {
orgNode.users.push(user);
}
});
const organizations = Array.from(tenantMap.values());
const pickSubtree = (nodes: OrganizationNode[], target: string): OrganizationNode[] => {
for (const node of nodes) {
if (node.ou_id === target) return [node];
const nested = pickSubtree(node.children || [], target);
if (nested.length > 0) return nested;
}
return [];
};
return {
organizations: rootUuid ? pickSubtree(organizations, rootUuid) : organizations,
total_organizations: organizations.length,
total_users: users.length,
};
}
/**
* 获取组织架构树(新版接口,支持按需加载)
* @param includeUsers 是否包含用户信息
@@ -74,17 +275,6 @@ export async function getOrganizationTree(
jwtToken?: string,
rootUuid?: string
): Promise<ApiResponse<OrganizationResponse>> {
try {
// console.log('[getOrganizationTree] 开始调用获取组织架构API:', { includeUsers, rootUuid });
const params: string[] = [];
params.push(`include_users=${includeUsers}`);
if (rootUuid) {
params.push(`root_uuid=${rootUuid}`);
}
const url = `${API_BASE_URL}/api/v2/users/organizations/tree?${params.join('&')}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
@@ -92,18 +282,46 @@ export async function getOrganizationTree(
headers['Authorization'] = `Bearer ${jwtToken}`;
}
const response = await axios.get<OrganizationResponse>(url, { headers });
const responseData = response.data;
// console.log('[getOrganizationTree] API响应:', responseData);
try {
// 新平台直接基于 RBAC 用户列表构造组织树,避免依赖已下线的旧 v2 接口。
const fallbackUsers = await fetchRbacUsers(jwtToken);
if (!fallbackUsers.success || !fallbackUsers.data) {
throw new Error(fallbackUsers.error || '获取组织架构失败');
}
rbacUsersAvailable = true;
const normalizedUsers = fallbackUsers.data.items.map(normalizeUser);
return {
success: true,
data: responseData
data: buildOrganizationTreeFromUsers(normalizedUsers, includeUsers, rootUuid),
};
} catch (error) {
if (rbacUsersAvailable === false) {
console.error('[getOrganizationTree] 获取组织架构失败:', error);
return {
success: false,
error: error instanceof Error ? error.message : '获取组织架构失败',
};
}
rbacUsersAvailable = false;
}
try {
const params: string[] = [];
params.push(`include_users=${includeUsers}`);
if (rootUuid) {
params.push(`root_uuid=${rootUuid}`);
}
const url = `${API_BASE_URL}/api/v2/users/organizations/tree?${params.join('&')}`;
const response = await axios.get<OrganizationResponse>(url, { headers });
return {
success: true,
data: response.data
};
} catch (error) {
console.error('[getOrganizationTree] 获取组织架构失败:', error);
let errorMessage = '获取组织架构失败';
if (axios.isAxiosError(error)) {
if (error.response?.data?.detail) {
@@ -139,16 +357,13 @@ export async function searchUsers(
users: UserInfo[];
total: number;
}>> {
try {
// console.log('[searchUsers] 搜索用户:', { search, page, pageSize });
const fallbackUsers = await fetchRbacUsers(jwtToken, search);
if (!fallbackUsers.success || !fallbackUsers.data) {
const params: string[] = [];
if (search) params.push(`search=${encodeURIComponent(search)}`);
params.push(`page=${page}`);
params.push(`page_size=${pageSize}`);
const url = `${API_BASE_URL}/api/v2/users?${params.join('&')}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
@@ -156,19 +371,16 @@ export async function searchUsers(
headers['Authorization'] = `Bearer ${jwtToken}`;
}
try {
const url = `${API_BASE_URL}/api/v2/users?${params.join('&')}`;
const response = await axios.get<{ users: UserInfo[]; total: number }>(url, { headers });
const responseData = response.data;
// console.log('[searchUsers] 搜索结果:', { total: responseData.total, count: responseData.users?.length });
return {
success: true,
data: responseData
data: response.data
};
} catch (error) {
console.error('[searchUsers] 搜索用户失败:', error);
let errorMessage = '搜索用户失败';
let errorMessage = fallbackUsers.error || '搜索用户失败';
if (axios.isAxiosError(error)) {
if (error.response?.data?.detail) {
errorMessage = error.response.data.detail;
@@ -186,6 +398,18 @@ export async function searchUsers(
}
}
const normalized = fallbackUsers.data.items.map(normalizeUser);
const start = (page - 1) * pageSize;
const paged = normalized.slice(start, start + pageSize);
return {
success: true,
data: {
users: paged,
total: normalized.length,
}
};
}
/**
* 获取用户列表
* @param params 查询参数
+44 -2
View File
@@ -330,7 +330,7 @@ function normalizeBooleanText(value: string | boolean | undefined): boolean {
}
function rewriteExtractNodes(fields: ExtractFieldSummary[]): Array<Record<string, unknown>> {
const topLevelFields = fields.filter((field) => !field.name.includes('[*].'));
const topLevelFields = fields.filter((field) => field.group !== '派生字段' && !field.name.includes('[*].'));
return Array.from(groupBy(topLevelFields, (field) => field.group || '未分组').entries()).map(([group, items]) => ({
group,
fields: items.map((field) => ({
@@ -343,6 +343,46 @@ function rewriteExtractNodes(fields: ExtractFieldSummary[]): Array<Record<string
}));
}
function rewriteDerivedFieldNodes(
fields: ExtractFieldSummary[],
existingNodes: unknown,
): Array<Record<string, unknown>> {
const derivedFields = fields.filter((field) => field.group === '派生字段');
if (derivedFields.length === 0) {
return [];
}
const existingMap = new Map<string, Record<string, unknown>>();
if (Array.isArray(existingNodes)) {
existingNodes.forEach((node) => {
if (!node || typeof node !== 'object') return;
const record = deepClone(node as Record<string, unknown>);
const name = String(record.name || '').trim();
if (name) {
existingMap.set(name, record);
}
});
}
return derivedFields.map((field) => {
const existing = existingMap.get(field.name) || {};
const nextNode: Record<string, unknown> = {
...existing,
name: field.name,
type: field.type || String(existing.type || 'computed'),
};
const nextCompute = field.description && field.description !== '由其他字段计算得出'
? field.description
: String(existing.compute || '').trim();
if (nextCompute) {
nextNode.compute = nextCompute;
} else {
delete nextNode.compute;
}
return nextNode;
});
}
function rewriteSubDocumentNodes(subDocuments: SubDocumentSummary[]): Array<Record<string, unknown>> {
return subDocuments.map((document) => ({
id: document.id,
@@ -474,7 +514,6 @@ function rewriteRuleNode(baseRule: Record<string, unknown> | undefined, rule: Ru
}
delete nextRule.rules;
delete nextRule.logic;
const existingStages = Array.isArray(nextRule.stages) ? (nextRule.stages as Array<Record<string, unknown>>) : [];
const stages = existingStages.length > 0 ? deepClone(existingStages) : (buildMinimalRuleNode(rule).stages as Array<Record<string, unknown>>);
@@ -494,6 +533,8 @@ function rewriteRuleNode(baseRule: Record<string, unknown> | undefined, rule: Ru
}
nextRule.stages = stages;
if (rule.logic?.trim()) nextRule.logic = rule.logic.trim();
else if (typeof nextRule.logic === 'string' && !String(nextRule.logic).trim()) delete nextRule.logic;
return nextRule;
}
@@ -512,6 +553,7 @@ export function serializeEditableRuleConfig(config: EditableRuleConfig): string
if (Array.isArray(config.metadata.inheritsFrom) && config.metadata.inheritsFrom.length > 0) metadata.inherits_from = [...config.metadata.inheritsFrom];
root.metadata = metadata;
root.extract = rewriteExtractNodes(config.fields);
root.derived_fields = rewriteDerivedFieldNodes(config.fields, root.derived_fields);
root.sub_documents = rewriteSubDocumentNodes(config.subDocuments);
root.visual_elements = rewriteVisualElementNodes(config.visualElements);
+3 -3
View File
@@ -415,9 +415,9 @@ function parseRules(source: string): RuleSummary[] {
group,
risk: stripYamlValue(ruleBlock.match(/^\s+risk:\s*(.+)$/m)?.[1] || 'medium'),
score: stripYamlValue(ruleBlock.match(/^\s+score:\s*(.+)$/m)?.[1] || '-'),
type: stripYamlValue(ruleBlock.match(/^\s+type:\s*(.+)$/m)?.[1] || 'deterministic'),
type: stripYamlValue(ruleBlock.match(/^\s{4}type:\s*(.+)$/m)?.[1] || 'deterministic'),
checkTypes,
logic: stripYamlValue(ruleBlock.match(/^\s+logic:\s*(.+)$/m)?.[1] || ''),
logic: stripYamlValue(ruleBlock.match(/^\s{4}logic:\s*(.+)$/m)?.[1] || ''),
subRules,
subRuleIds: readList(ruleBlock, 'rules'),
scope: scope.slice(0, 8),
@@ -425,7 +425,7 @@ function parseRules(source: string): RuleSummary[] {
stageCount: subRules.length,
appliesIn: readFlexibleList(ruleBlock, 'applies_in'),
prompt: prompts.join('\n\n'),
description: stripYamlValue(ruleBlock.match(/^\s+desc:\s*(.+)$/m)?.[1] || '')
description: stripYamlValue(ruleBlock.match(/^\s{4}desc:\s*(.+)$/m)?.[1] || '')
};
});
});