适配交叉评查上传接口

修复N多个BUG
This commit is contained in:
2025-07-24 09:29:55 +08:00
parent 8800e982ab
commit 4934b083e3
6 changed files with 236 additions and 151 deletions
+10 -11
View File
@@ -26,15 +26,14 @@ function extractApiData<T>(responseData: unknown): T | null {
export interface SubmitOpinionRequest { export interface SubmitOpinionRequest {
reviewPointResultId: string | number; reviewPointResultId: string | number;
documentId: string | number; documentId: string | number;
auditPoint: string; evaluationPointId: number; // 必须是数字ID
foundIssue: string;
auditOpinion: string; auditOpinion: string;
deductionScore: number; deductionScore: number;
} }
/** /**
* 提出意见的响应接口 * 提出意见的响应接口
*/ */
export interface SubmitOpinionResponse { export interface SubmitOpinionResponse {
success: boolean; success: boolean;
message: string; message: string;
@@ -117,20 +116,20 @@ export async function findIsProposer(taskId: string | number, userId: number | u
*/ */
export async function submitCrossCheckingOpinion( export async function submitCrossCheckingOpinion(
opinionData: SubmitOpinionRequest, opinionData: SubmitOpinionRequest,
jwtToken?: string jwtToken?: string,
userInfo?: { user_id: number }
): Promise<ApiResponse<SubmitOpinionResponse>> { ): Promise<ApiResponse<SubmitOpinionResponse>> {
try { try {
// 获取JWT token // 获取JWT token
const token = await safeGetJWT(jwtToken); const token = await safeGetJWT(jwtToken);
const requestData = { const requestData = {
proposer_user_id: 1, document_id: opinionData.documentId,
evaluation_result_id: opinionData.reviewPointResultId, evaluation_point_id: Number(opinionData.evaluationPointId), // 强制转数字
// document_id: opinionData.documentId, proposed_score: opinionData.deductionScore,
// audit_point: opinionData.auditPoint, reason: opinionData.auditOpinion,
// found_issue: opinionData.foundIssue, proposer_id: userInfo?.user_id || 1,
proposed_score: opinionData.deductionScore, evaluation_result_id: opinionData.reviewPointResultId
reason: opinionData.auditOpinion
}; };
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals`, { const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals`, {
+49 -49
View File
@@ -65,8 +65,14 @@ export interface CrossCheckingUploadedFile {
/** /**
* 将文件转换为二进制数据 * 将文件转换为二进制数据
*/ */
export async function uploadFileToBinary(file: File): Promise<ArrayBuffer> { export async function uploadFileToBinary(file: File | Blob): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 只保留简单类型检查和调试
if (!(file instanceof File) && !(file instanceof Blob)) {
reject(new Error(`参数必须是File或Blob对象,当前类型: ${typeof file}`));
return;
}
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
if (reader.result instanceof ArrayBuffer) { if (reader.result instanceof ArrayBuffer) {
@@ -206,75 +212,69 @@ export async function uploadCrossCheckingDocument(
} }
/** /**
* 批量上传交叉评查文件 * 批量上传并自动分配交叉评查任务(新接口适配)
* @param files 文件列表 * @param files 文件列表
* @param typeId 文档类型ID * @param typeId 文档类型ID
* @param priority 优先级 * @param priority 优先级
* @param documentNumber 文档编号 * @param documentNumber 文档编号
* @param remark 备注信息 * @param remark 备注
* @param isTestDocument 是否为测试文档 * @param isTestDocument 是否为测试文档
* @returns 上传结果列表 * @param assignUserIds 需要分配的用户ID数组
* @param taskName 任务名称
* @param docType 文档类型(如 XZCF、XZXK
* @param token JWT Token
*/ */
export async function batchUploadCrossCheckingFiles( export async function batchUploadAndAssignCrossCheckingFiles(
files: CrossCheckingUploadedFile[], files: CrossCheckingUploadedFile[],
typeId: number, typeId: number,
priority: string = 'normal', priority: string = 'normal',
documentNumber: string = '', documentNumber: string = '',
remark: string = '', remark: string = '',
isTestDocument: boolean = false isTestDocument: boolean = false,
assignUserIds: number[],
taskName: string,
docType: string,
token: string | null = null
): Promise<{ ): Promise<{
successes: Array<{file: CrossCheckingUploadedFile; result: CrossCheckingFileUploadResponse}>; successes: Array<{file: CrossCheckingUploadedFile; result: Record<string, unknown>}>;
failures: Array<{file: CrossCheckingUploadedFile; error: string}>; failures: Array<{file: CrossCheckingUploadedFile; error: string}>;
}> { }> {
const successes: Array<{file: CrossCheckingUploadedFile; result: CrossCheckingFileUploadResponse}> = []; const successes: Array<{file: CrossCheckingUploadedFile; result: Record<string, unknown>}> = [];
const failures: Array<{file: CrossCheckingUploadedFile; error: string}> = []; const failures: Array<{file: CrossCheckingUploadedFile; error: string}> = [];
const uploadEndpoint = '/cross_review/documents/upload_and_assign';
console.log('【交叉评查批量上传】开始批量上传文件,文件数量:', files.length); const uploadUrl = UPLOAD_URL + uploadEndpoint;
for (const fileInfo of files) { for (const fileInfo of files) {
try { try {
console.log('【交叉评查批量上传】上传文件:', fileInfo.name); const formData = new FormData();
formData.append('file', fileInfo.file, fileInfo.name);
// 转换文件为二进制格式 const uploadInfo = {
const binaryData = await uploadFileToBinary(fileInfo.file); type_id: typeof typeId === 'string' ? parseInt(typeId, 10) : typeId,
evaluation_level: priority,
// 上传文件 document_number: documentNumber || null,
const result = await uploadCrossCheckingDocument( remark: remark || null,
binaryData, is_test_document: isTestDocument,
fileInfo.file.name, task_name: taskName,
fileInfo.file.type, doc_type: typeof docType === 'string' ? docType.toUpperCase() : docType
typeId, };
priority, formData.append('upload_info', JSON.stringify(uploadInfo));
documentNumber, formData.append('assign_user_ids', JSON.stringify(assignUserIds));
remark, const headers: HeadersInit = {};
isTestDocument, if (token) headers['Authorization'] = `Bearer ${token}`;
null, // 交叉评查文件通常没有关联的文档ID const response = await fetch(uploadUrl, {
false method: 'POST',
); headers,
body: formData
if (result.error) { });
console.error('【交叉评查批量上传】文件上传失败:', fileInfo.name, result.error); const result = await response.json();
failures.push({ if (result && result.success) {
file: fileInfo, successes.push({ file: fileInfo, result });
error: result.error } else {
}); failures.push({ file: fileInfo, error: result.error || '未知错误' });
} else if (result.data) {
console.log('【交叉评查批量上传】文件上传成功:', fileInfo.name);
successes.push({
file: fileInfo,
result: result.data
});
} }
} catch (error) { } catch (error) {
console.error('【交叉评查批量上传】处理文件时发生错误:', fileInfo.name, error); failures.push({ file: fileInfo, error: error instanceof Error ? error.message : '上传失败' });
failures.push({
file: fileInfo,
error: error instanceof Error ? error.message : '上传失败'
});
} }
} }
console.log('【交叉评查批量上传】批量上传完成,成功:', successes.length, '失败:', failures.length);
return { successes, failures }; return { successes, failures };
} }
+12 -22
View File
@@ -172,29 +172,19 @@ export function convertToTreeData(organizations: OrganizationNode[]): Array<{
}>; }>;
}> { }> {
return organizations.map(org => { return organizations.map(org => {
const children: Array<{ // 递归处理子组织
label: string; const subOrganizations = org.children && org.children.length > 0 ? convertToTreeData(org.children) : [];
value: string;
isUser?: boolean;
userInfo?: UserInfo;
}> = [];
// 添加该组织下的用户 // 添加该组织下的用户
if (org.users && org.users.length > 0) { const userChildren = (org.users && org.users.length > 0)
children.push(...org.users.map(user => ({ ? org.users.map(user => ({
label: user.nick_name, label: user.nick_name,
value: `user_${user.id}`, value: `user_${user.id}`,
isUser: true, isUser: true,
userInfo: user userInfo: user
}))); }))
} : [];
// 合并子组织和用户
// 递归处理子组织,保持原有的层级结构 const children = [...subOrganizations, ...userChildren];
if (org.children && org.children.length > 0) {
const subOrganizations = convertToTreeData(org.children);
children.push(...subOrganizations);
}
return { return {
label: org.ou_name, label: org.ou_name,
value: org.ou_id, value: org.ou_id,
@@ -31,6 +31,7 @@ import {
type OpinionActionType type OpinionActionType
} from '../../api/cross-checking/cross-file-result'; } from '../../api/cross-checking/cross-file-result';
import { useFetcher, useNavigate } from '@remix-run/react'; import { useFetcher, useNavigate } from '@remix-run/react';
import { API_BASE_URL } from '~/config/api-config';
// import '../../styles/components/TooltipStyles.css'; // import '../../styles/components/TooltipStyles.css';
/** /**
@@ -86,6 +87,7 @@ export interface ReviewPoint {
id: string; id: string;
documentId?: string; documentId?: string;
pointId?: string; pointId?: string;
evaluationPointId?: string | number; // 新增,允许兜底
editAuditStatusId?: string | number; editAuditStatusId?: string | number;
editAuditStatus: number; editAuditStatus: number;
editAuditStatusMessage?: string; // 添加审核意见字段 editAuditStatusMessage?: string; // 添加审核意见字段
@@ -439,6 +441,17 @@ export function ReviewPointsList({
const [evaluationResultIds, setEvaluationResultIds] = useState<number[]>([]); // 评分提案的evaluation_result_id const [evaluationResultIds, setEvaluationResultIds] = useState<number[]>([]); // 评分提案的evaluation_result_id
const fetcher = useFetcher(); const fetcher = useFetcher();
// 归一化 reviewPoints,确保每个点都有 id 字段
const [normalizedReviewPoints, setNormalizedReviewPoints] = useState<ReviewPoint[]>([]);
console.log('normalizedReviewPoints', normalizedReviewPoints);
useEffect(() => {
const norm = reviewPoints.map(point => ({
...point,
id: String(point.id || point.evaluationPointId || point.pointId || '') // 保证 id 为字符串且不为 undefined
}));
setNormalizedReviewPoints(norm);
}, [reviewPoints]);
// 在组件中使用scoringProposals(这里只是简单使用以避免linter警告) // 在组件中使用scoringProposals(这里只是简单使用以避免linter警告)
// 将来可以用于显示相关的评分提案信息 // 将来可以用于显示相关的评分提案信息
useEffect(() => { useEffect(() => {
@@ -690,11 +703,6 @@ export function ReviewPointsList({
return; return;
} }
if (opinionForm.deductionScore <= 0) {
toastService.error('扣分必须大于0');
return;
}
if (opinionForm.deductionScore > 100) { if (opinionForm.deductionScore > 100) {
toastService.error('扣分不能大于100分'); toastService.error('扣分不能大于100分');
return; return;
@@ -705,26 +713,75 @@ export function ReviewPointsList({
return; return;
} }
setIsSubmittingOpinion(true); // 新增:详细打印每个校验条件
console.log('校验前 selectedReviewPoint:', selectedReviewPoint);
console.log('校验前 opinionForm:', opinionForm);
console.log('校验前 userInfo:', userInfo);
console.log('documentId:', selectedReviewPoint.documentId, 'isNaN:', isNaN(Number(selectedReviewPoint.documentId)), 'typeof:', typeof selectedReviewPoint.documentId);
console.log('pointId:', selectedReviewPoint.pointId, 'isNaN:', isNaN(Number(selectedReviewPoint.pointId)), 'typeof:', typeof selectedReviewPoint.pointId);
console.log('deductionScore:', opinionForm.deductionScore, 'typeof:', typeof opinionForm.deductionScore, 'isNaN:', isNaN(Number(opinionForm.deductionScore)));
console.log('auditOpinion:', opinionForm.auditOpinion, 'trim:', String(opinionForm.auditOpinion).trim(), 'typeof:', typeof opinionForm.auditOpinion);
console.log('user_id:', userInfo?.user_id, 'typeof:', typeof userInfo?.user_id);
// 更严谨的校验逻辑
if (
selectedReviewPoint.documentId === undefined ||
selectedReviewPoint.pointId === undefined ||
opinionForm.deductionScore === undefined ||
opinionForm.auditOpinion === undefined ||
userInfo?.user_id === undefined ||
isNaN(Number(selectedReviewPoint.documentId)) ||
isNaN(Number(selectedReviewPoint.pointId)) ||
isNaN(Number(opinionForm.deductionScore)) ||
!String(opinionForm.auditOpinion).trim()
) {
toastService.error('请完整填写所有必填项');
setIsSubmittingOpinion(false);
return;
}
// 打印所有关键数据
console.log('selectedReviewPoint:', selectedReviewPoint);
console.log('opinionForm:', opinionForm);
console.log('userInfo:', userInfo);
// 组装后端要求的字段名和内容
const data: Record<string, any> = {
document_id: Number(selectedReviewPoint.documentId),
evaluation_point_id: Number(selectedReviewPoint.pointId),
proposed_score: Number(opinionForm.deductionScore),
reason: opinionForm.auditOpinion,
proposer_id: userInfo.user_id,
problem_message: opinionForm.foundIssue,
evaluation_result_id: Number(selectedReviewPoint.id),
};
if (selectedReviewPoint.evaluationPointId) {
data.evaluation_result_id = Number(selectedReviewPoint.evaluationPointId);
}
// 打印最终请求体
console.log('最终请求体:', data);
// 用原生 fetch + application/json 提交
try { try {
// 使用 fetcher 调用路由的 action const response = await fetch(`${API_BASE_URL.replace(/\/$/, '')}/admin/cross_review/proposals`, {
const formData = new FormData(); method: 'POST',
formData.append("intent", "submitCrossCheckingOpinion"); headers: {
formData.append("reviewPointResultId", selectedReviewPoint.id); 'Content-Type': 'application/json',
formData.append("documentId", selectedReviewPoint.documentId || ''); 'Authorization': `Bearer ${userInfo.frontend_jwt}`,
formData.append("auditPoint", opinionForm.auditPoint); },
formData.append("foundIssue", opinionForm.foundIssue); body: JSON.stringify(data)
formData.append("auditOpinion", opinionForm.auditOpinion); });
formData.append("deductionScore", opinionForm.deductionScore.toString()); const result = await response.json();
if (response.ok) {
fetcher.submit(formData, { method: "POST" }); toastService.success('意见提交成功');
handleCloseOpinionModal();
} else {
toastService.error(result.detail || '提交意见失败');
}
} catch (error) { } catch (error) {
console.error('提交意见失败:', error); console.error('提交意见失败:', error);
toastService.error('提交意见失败,请稍后重试'); toastService.error('提交意见失败,请稍后重试');
setIsSubmittingOpinion(false);
} }
setIsSubmittingOpinion(false);
}; };
/** /**
@@ -2572,10 +2629,14 @@ export function ReviewPointsList({
{ {
title: "调整理由", title: "调整理由",
key: "reason", key: "reason",
width: "25%", width: "15%",
render: (_: unknown, record: CrossCheckingOpinion) => ( render: (_: unknown, record: CrossCheckingOpinion) => {
<div className="text-sm text-left">{record.reason}</div> const reason = record.reason || '';
) const display = reason.length > 20 ? reason.slice(0, 20) + '...' : reason;
return (
<span title={reason}>{display}</span>
);
}
}, },
{ {
title: "调整分数", title: "调整分数",
@@ -2665,6 +2726,31 @@ export function ReviewPointsList({
<div className="text-sm text-left">{record.created_at}</div> <div className="text-sm text-left">{record.created_at}</div>
) )
}, },
{
title: "投票状态",
key: "opinion_status",
width: "10%",
render: (_: unknown, record: CrossCheckingOpinion) => {
let label = '';
let color = '';
switch (record.status) {
case 'approved':
label = '通过';
color = 'text-green-600';
break;
case 'rejected':
label = '不通过';
color = 'text-red-600';
break;
case 'pending':
default:
label = '投票中';
color = 'text-yellow-600';
break;
}
return <span className={`font-bold ${color}`}>{label}</span>;
}
},
{ {
title: "操作", title: "操作",
key: "operation", key: "operation",
+2
View File
@@ -71,6 +71,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
// 获取用户会话信息 // 获取用户会话信息
const { getUserSession } = await import("~/api/login/auth.server"); const { getUserSession } = await import("~/api/login/auth.server");
const { userInfo, frontendJWT } = await getUserSession(request); const { userInfo, frontendJWT } = await getUserSession(request);
console.log('frontendJWT', frontendJWT);
// 获取任务列表和统计数据,传递用户信息和JWT // 获取任务列表和统计数据,传递用户信息和JWT
const [tasksResponse, statsResponse] = await Promise.all([ const [tasksResponse, statsResponse] = await Promise.all([
+54 -46
View File
@@ -14,13 +14,14 @@ import {
type CrossCheckingUploadedFile, type CrossCheckingUploadedFile,
generateFileId, generateFileId,
formatFileSize, formatFileSize,
batchUploadCrossCheckingFiles batchUploadAndAssignCrossCheckingFiles
} from "~/api/cross-checking/cross-files-upload"; } from "~/api/cross-checking/cross-files-upload";
import { import {
getOrganizationTree, getOrganizationTree,
convertToTreeData convertToTreeData
} from "~/api/user"; } from "~/api/user";
import React from "react"; // Added for React.useState import React from "react"; // Added for React.useState
import { API_BASE_URL } from '~/config/api-config';
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
return [ return [
@@ -144,14 +145,15 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
* @param token JWT Token * @param token JWT Token
* @returns 创建结果 * @returns 创建结果
*/ */
async function createCrossReviewTask(taskData: { export async function createCrossReviewTask(taskData: {
documentIds: number[]; documentIds: number[];
userIds: number[]; userIds: number[];
assignerId: number; assignerId: number;
taskName: string; taskName: string;
docType: string;
}, token: string) { }, token: string) {
try { try {
const response = await fetch('/admin/crossreview/tasks/assign', { const response = await fetch(`${API_BASE_URL}/admin/cross_review/tasks/assign`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -161,7 +163,8 @@ async function createCrossReviewTask(taskData: {
document_ids: taskData.documentIds, document_ids: taskData.documentIds,
user_ids: taskData.userIds, user_ids: taskData.userIds,
assigner_id: taskData.assignerId, assigner_id: taskData.assignerId,
task_name: taskData.taskName task_name: taskData.taskName,
doc_type: taskData.docType
}) })
}); });
@@ -207,7 +210,7 @@ export default function CrossCheckingUpload() {
type: '市局交叉评查', type: '市局交叉评查',
}); });
// 步骤2状态 // 步骤2状态
const [groupChecked, setGroupChecked] = useState<string[]>([]); const [groupChecked, setGroupChecked] = useState<string[]>(userInfo?.user_id ? [`user_${userInfo.user_id}`] : []);
const [userSelectionState, setUserSelectionState] = useState<UserSelectionState>({ const [userSelectionState, setUserSelectionState] = useState<UserSelectionState>({
treeData: DEFAULT_TREE, treeData: DEFAULT_TREE,
loading: false, loading: false,
@@ -419,30 +422,9 @@ export default function CrossCheckingUpload() {
setIsUploading(true); setIsUploading(true);
try { try {
// 第一步:上传文件 // 第一步:上传文件并自动分配任务(新接口)
console.log("开始批量上传文件:", filesToUpload.length, "个,案卷类型:", caseType); console.log("开始批量上传文件并分配任务:", filesToUpload.length, "个,案卷类型:", caseType);
const uploadResult = await batchUploadCrossCheckingFiles(
filesToUpload.map(f => f.file),
caseType,
priority,
isTestDocument,
frontendJWT
);
const { successes, failures } = uploadResult;
if (failures.length > 0) {
toastService.error(`文件上传失败:${failures[0].error}`);
return;
}
// 第二步:创建交叉评查任务
console.log("文件上传成功,开始创建任务");
// 提取文档ID
const documentIds = successes.map(success => success.result.result?.id).filter(id => id !== undefined) as number[];
// 提取用户ID(从选中的组织架构中获取用户) // 提取用户ID(从选中的组织架构中获取用户)
const userIds = groupChecked.filter(id => { const userIds = groupChecked.filter(id => {
// 检查是否为用户ID(通常用户ID以特定前缀开头或有特定格式) // 检查是否为用户ID(通常用户ID以特定前缀开头或有特定格式)
@@ -455,18 +437,31 @@ export default function CrossCheckingUpload() {
} }
// 创建任务数据 // 创建任务数据
const taskData = { const docTypeMap = {
documentIds, [CaseType.ADMINISTRATIVE_PENALTY]: 'XZCF',
userIds, [CaseType.ADMINISTRATIVE_PERMIT]: 'XZXK'
assignerId: userInfo?.user_id || 1, // 使用当前用户ID作为分配者
taskName: taskInfo.name
}; };
console.log("创建任务数据:", taskData); const uploadResult = await batchUploadAndAssignCrossCheckingFiles(
filesToUpload,
// 调用创建任务接口 CASE_TYPE_TO_TYPE_ID[caseType],
await createCrossReviewTask(taskData, frontendJWT); priority,
documentNumber,
remark,
isTestDocument,
userIds,
taskInfo.name,
docTypeMap[caseType] || 'XZCF',
frontendJWT
);
const { successes, failures } = uploadResult;
if (failures.length > 0) {
toastService.error(`文件上传或任务分配失败:${failures[0].error}`);
return;
}
// 任务创建成功 // 任务创建成功
toastService.success("交叉评查任务创建成功!"); toastService.success("交叉评查任务创建成功!");
messageService.success( messageService.success(
@@ -630,14 +625,28 @@ export default function CrossCheckingUpload() {
} }
} }
}; };
loadOrganizationData(); loadOrganizationData();
}, [currentStep]); // 只依赖 currentStep,避免无限循环 }, [currentStep]); // 只依赖 currentStep,避免无限循环
// 在 CrossCheckingUpload 组件内添加工具函数
function findUserNameById(tree: TreeNode[], userId: string): string | null {
for (const node of tree) {
if (node.value === userId && (node as { isUser?: boolean }).isUser) {
return node.label;
}
if (node.children) {
const found = findUserNameById(node.children, userId);
if (found) return found;
}
}
return null;
}
return ( return (
<div className="min-h-screen bg-gray-50 py-8"> <div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-6xl mx-auto px-4"> <div className="max-w-6xl mx-auto px-4">
{/* 步骤指示器 */ {/* 步骤指示器 */}
<div className="steps-indicator"> <div className="steps-indicator">
{STEPS.map((step) => ( {STEPS.map((step) => (
<div <div
@@ -649,7 +658,6 @@ export default function CrossCheckingUpload() {
</div> </div>
))} ))}
</div> </div>
}
{/* 步骤1:创建任务 */} {/* 步骤1:创建任务 */}
{currentStep === 1 && ( {currentStep === 1 && (
@@ -741,16 +749,16 @@ export default function CrossCheckingUpload() {
{groupChecked.length > 0 ? ( {groupChecked.length > 0 ? (
<div className="space-y-2 max-h-64 overflow-y-auto"> <div className="space-y-2 max-h-64 overflow-y-auto">
{groupChecked.map((member, index) => { {groupChecked.map((member, index) => {
// 处理用户选择值,支持新的API格式 let displayName: string = member;
let displayName = member;
let displayOrg = ''; let displayOrg = '';
if (member.startsWith('user_')) { if (member.startsWith('user_')) {
// 用户选择,格式为 user_123 // 查找真实用户名
displayName = `用户ID: ${member.replace('user_', '')}`; const userName = findUserNameById(userSelectionState.treeData, member) || member.replace('user_', '');
displayName = userName;
displayOrg = '用户'; displayOrg = '用户';
} else { } else {
// 组织选择,格式为 ou_id 或 ou_id-ou_id // 组织
const parts = member.split('-'); const parts = member.split('-');
displayName = parts[parts.length - 1]; displayName = parts[parts.length - 1];
displayOrg = parts.slice(0, -1).join(' - ') || '组织'; displayOrg = parts.slice(0, -1).join(' - ') || '组织';