feat: 1. 完善交叉评查上传创建任务,改成动态加载文档类型。

2. 重新对齐交叉评查的接口。
This commit is contained in:
2025-12-02 10:10:03 +08:00
parent c9e0d5abba
commit 88466b7a8b
21 changed files with 561 additions and 174 deletions
+59 -10
View File
@@ -102,10 +102,10 @@ export async function findIsProposer(taskId: string | number, userId: number | u
return false;
}
const data = extractApiData<{assigner_id: number}[]>(response.data);
// console.log('data', data);
// console.log('data', data, userId);
if (data && data.length > 0) {
return data[0].assigner_id === userId;
return data[0].assigner_id.toString() === userId?.toString();
}
return false;
@@ -124,6 +124,7 @@ export async function submitCrossCheckingOpinion(
): Promise<ApiResponse<SubmitOpinionResponse>> {
try {
// 获取JWT token
console.log('jwtToken', jwtToken)
const token = await safeGetJWT(jwtToken);
const requestData = {
@@ -151,9 +152,21 @@ export async function submitCrossCheckingOpinion(
};
} catch (error) {
console.error('提交交叉评查意见失败:', error);
// 正确处理 axios 错误响应
let errorMessage = '提交意见失败';
if (axios.isAxiosError(error) && error.response?.data) {
// 从 axios 错误响应中提取 msg 字段
errorMessage = error.response.data.msg || errorMessage;
} else if (error instanceof Error) {
// 处理普通 Error 对象
errorMessage = error.message || errorMessage;
}
return {
error: error instanceof Error ? error.message : '提交意见失败',
status: 500
error: errorMessage,
status: axios.isAxiosError(error) ? error.response?.status || 500 : 500
};
}
}
@@ -245,9 +258,21 @@ export async function getCrossCheckingOpinions(
};
} catch (error) {
console.error('获取交叉评查意见失败:', error);
// 正确处理 axios 错误响应
let errorMessage = '获取意见列表失败';
if (axios.isAxiosError(error) && error.response?.data) {
// 从 axios 错误响应中提取 msg 字段
errorMessage = error.response.data.msg || errorMessage;
} else if (error instanceof Error) {
// 处理普通 Error 对象
errorMessage = error.message || errorMessage;
}
return {
error: error instanceof Error ? error.message : '获取意见列表失败',
status: 500
error: errorMessage,
status: axios.isAxiosError(error) ? error.response?.status || 500 : 500
};
}
}
@@ -343,9 +368,21 @@ export async function performOpinionAction(
};
} catch (error) {
console.error('执行意见操作失败:', error);
// 正确处理 axios 错误响应
let errorMessage = '操作失败';
if (axios.isAxiosError(error) && error.response?.data) {
// 从 axios 错误响应中提取 msg 字段
errorMessage = error.response.data.msg || errorMessage;
} else if (error instanceof Error) {
// 处理普通 Error 对象
errorMessage = error.message || errorMessage;
}
return {
error: error instanceof Error ? error.message : '操作失败',
status: 500
error: errorMessage,
status: axios.isAxiosError(error) ? error.response?.status || 500 : 500
};
}
}
@@ -427,9 +464,21 @@ export async function checkProposalVotes(
};
} catch (error) {
console.error('检查失败:', error);
// 正确处理 axios 错误响应
let errorMessage = '检查失败';
if (axios.isAxiosError(error) && error.response?.data) {
// 从 axios 错误响应中提取 msg 字段
errorMessage = error.response.data.msg || errorMessage;
} else if (error instanceof Error) {
// 处理普通 Error 对象
errorMessage = error.message || errorMessage;
}
return {
error: error instanceof Error ? error.message : '检查失败',
status: 500
error: errorMessage,
status: axios.isAxiosError(error) ? error.response?.status || 500 : 500
};
}
}
+80 -2
View File
@@ -1,4 +1,4 @@
import { UPLOAD_URL } from '../../config/api-config';
import { UPLOAD_URL, API_BASE_URL } from '../../config/api-config';
import axios from 'axios';
/**
@@ -208,6 +208,7 @@ export async function uploadCrossCheckingDocument(
* @param assignUserIds 需要分配的用户ID数组
* @param taskName 任务名称
* @param docType 文档类型(如 XZCF、XZXK
* @param taskType 任务类型(如 市局间交叉评查、区局间交叉评查)
* @param token JWT Token
*/
export async function batchUploadAndAssignCrossCheckingFiles(
@@ -220,6 +221,7 @@ export async function batchUploadAndAssignCrossCheckingFiles(
assignUserIds: number[],
taskName: string,
docType: string,
taskType: string = '市局间交叉评查',
token: string | null = null
): Promise<{
successes: Array<{file: CrossCheckingUploadedFile; result: Record<string, unknown>}>;
@@ -229,6 +231,10 @@ export async function batchUploadAndAssignCrossCheckingFiles(
const failures: Array<{file: CrossCheckingUploadedFile; error: string}> = [];
const uploadEndpoint = '/cross_review/documents/upload_and_assign';
const uploadUrl = UPLOAD_URL + uploadEndpoint;
// console.log('[批量上传] 任务类型:', taskType, '文档类型:', docType);
for (const fileInfo of files) {
try {
const formData = new FormData();
@@ -240,15 +246,22 @@ export async function batchUploadAndAssignCrossCheckingFiles(
remark: remark || null,
is_test_document: isTestDocument,
task_name: taskName,
doc_type: typeof docType === 'string' ? docType.toUpperCase() : docType
doc_type: typeof docType === 'string' ? docType.toUpperCase() : docType,
task_type: taskType
};
// console.log('fileInfo', fileInfo)
formData.append('upload_info', JSON.stringify(uploadInfo));
formData.append('assign_user_ids', JSON.stringify(assignUserIds));
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const response = await axios.post(uploadUrl, formData, {
headers
});
const result = response.data;
if (result && result.success) {
successes.push({ file: fileInfo, result });
@@ -262,6 +275,71 @@ export async function batchUploadAndAssignCrossCheckingFiles(
return { successes, failures };
}
/**
* 创建交叉评查任务
* @param taskData 任务数据
* @param token JWT Token
* @returns 创建结果
*/
export async function createCrossReviewTask(taskData: {
documentIds: number[];
userIds: number[];
assignerId: number;
taskName: string;
docType: string;
taskType?: string;
}, token: string | null = null): Promise<{
success: boolean;
data?: unknown;
error?: string;
}> {
try {
const requestBody = {
document_ids: taskData.documentIds,
user_ids: taskData.userIds,
assigner_id: taskData.assignerId,
task_name: taskData.taskName,
doc_type: taskData.docType,
task_type: taskData.taskType || '市局间交叉评查'
};
// console.log('[创建任务] 请求数据:', requestBody);
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await axios.post(
`${API_BASE_URL}/admin/cross_review/tasks/assign`,
requestBody,
{ headers }
);
console.log('[创建任务] 成功:', response.data);
return {
success: true,
data: response.data
};
} catch (error) {
console.error('[创建任务] 失败:', error);
let errorMessage = '创建任务失败';
if (axios.isAxiosError(error) && error.response?.data) {
errorMessage = error.response.data.msg || errorMessage;
} else if (error instanceof Error) {
errorMessage = error.message || errorMessage;
}
return {
success: false,
error: errorMessage
};
}
}
/**
* 生成唯一文件ID
* @returns 唯一ID字符串
+9 -7
View File
@@ -11,8 +11,8 @@ export enum CrossCheckingTaskStatus {
// 交叉评查任务类型枚举
export enum CrossCheckingTaskType {
CITY = 'city',
COUNTY = 'county'
CITY = 'CITY',
DISTRICT = 'DISTRICT'
}
// 案卷类型枚举
@@ -37,7 +37,7 @@ export interface CrossCheckingTask {
startDate: string;
taskType: CrossCheckingTaskType;
docType: string; // 改为直接使用返回的 doc_type 字符串
evaluationRegion: string;
evaluationRegion: string[];
progress: number;
status: string; // 改为直接使用返回的 task_status 字符串
score: number;
@@ -61,6 +61,8 @@ export interface UserTaskInfo {
task_status: string;
doc_type?: string;
task_created_at?: string;
evaluation_region?: string[];
task_type?: string;
progress?: number;
total_documents?: number; // 新增:任务包含的文档总数
}
@@ -150,7 +152,7 @@ export interface TaskListResponse {
*/
export async function getCrossCheckingTasks(params: TaskListParams = {}, userInfo?: { user_id?: number; [key: string]: unknown }, jwtToken?: string): Promise<ApiResponse<TaskListResponse>> {
try {
console.log('开始调用getCrossCheckingTasks,参数:', params);
// console.log('开始调用getCrossCheckingTasks,参数:', params);
// 调用用户任务API,获取当前用户参与的任务
const userTasksResponse = await getUserTaskDocuments(params.page || 1, params.pageSize || 10, jwtToken);
@@ -174,9 +176,9 @@ export async function getCrossCheckingTasks(params: TaskListParams = {}, userInf
sequence: index + 1,
taskName: userTask.task_name || `任务 ${userTask.task_id}`, // 使用API返回的任务名称
startDate: userTask.task_created_at ? new Date(userTask.task_created_at).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
taskType: CrossCheckingTaskType.CITY, // 保持默认任务类型
taskType: userTask.task_type, // 保持默认任务类型
docType: userTask.doc_type || '未知类型', // 使用API返回的文档类型
evaluationRegion: '待定', // 保持默认评查地区
evaluationRegion: userTask.evaluation_region || [], // 保持默认评查地区
progress: userTask.progress || 0, // 使用API返回的进度
status: userTask.task_status || 'pending', // 使用API返回的任务状态
score: userTask.task_status === 'completed' ? 85 : 0, // 默认分数
@@ -302,7 +304,7 @@ export async function getCrossCheckingTaskDetail(
pageSize: number;
}>> {
try {
console.log('开始调用getCrossCheckingTaskDetail,参数:', { taskId, page, pageSize });
// console.log('开始调用getCrossCheckingTaskDetail,参数:', { taskId, page, pageSize });
// 获取任务的文档列表
const taskDocumentsResponse = await getTaskDocuments(taskId, page, pageSize, jwtToken);
@@ -0,0 +1,143 @@
/**
* 验证用户是否有权访问指定文档
*
* 权限验证逻辑:
* 1. 检查文档是否属于指定的任务
* 2. 检查用户是否是该任务的参与者(评审员或发起人)
* 3. 防止用户通过修改 URL 参数访问未授权的文档
*/
import { postgrestGet } from "../postgrest-client";
interface DocumentAccessCheckParams {
documentId: string | number;
taskId: string | number;
userId: number;
jwtToken?: string;
}
interface DocumentAccessCheckResult {
hasAccess: boolean;
reason?: string;
userRole?: 'assigner' | 'assignee' | 'none';
}
/**
* 验证文档访问权限
* @param params 验证参数
* @returns 验证结果
*/
export async function verifyDocumentAccess(
params: DocumentAccessCheckParams
): Promise<DocumentAccessCheckResult> {
const { documentId, taskId, userId, jwtToken } = params;
try {
// 1. 检查文档是否属于该任务(通过 cross_task_document_mapping 表)
const documentMappingResponse = await postgrestGet('cross_task_document_mapping', {
select: 'task_id,document_id',
filter: {
task_id: `eq.${taskId}`,
document_id: `eq.${documentId}`
},
token: jwtToken
});
if (documentMappingResponse.error) {
console.error('❌ [verifyDocumentAccess] 查询文档-任务映射失败:', documentMappingResponse.error);
return {
hasAccess: false,
reason: '查询文档-任务映射失败'
};
}
// 提取数据
const mappingData = Array.isArray(documentMappingResponse.data)
? documentMappingResponse.data
: [];
// 文档不属于该任务
if (mappingData.length === 0) {
console.warn(`⚠️ [verifyDocumentAccess] 文档 ${documentId} 不属于任务 ${taskId}`);
return {
hasAccess: false,
reason: '文档不属于该任务'
};
}
// 2. 检查用户是否是该任务的参与者
const taskResponse = await postgrestGet('cross_examination_tasks', {
select: 'assigner_id,assignee_ids',
filter: {
id: `eq.${taskId}`
},
token: jwtToken
});
if (taskResponse.error) {
console.error('❌ [verifyDocumentAccess] 查询任务信息失败:', taskResponse.error);
return {
hasAccess: false,
reason: '查询任务信息失败'
};
}
const taskData = Array.isArray(taskResponse.data) ? taskResponse.data[0] : null;
if (!taskData) {
console.warn(`⚠️ [verifyDocumentAccess] 任务 ${taskId} 不存在`);
return {
hasAccess: false,
reason: '任务不存在'
};
}
// 3. 验证用户身份
const isAssigner = taskData.assigner_id === userId;
const assigneeIds = Array.isArray(taskData.assignee_ids) ? taskData.assignee_ids : [];
const isAssignee = assigneeIds.includes(userId);
if (isAssigner) {
console.log(`✅ [verifyDocumentAccess] 用户 ${userId} 是任务 ${taskId} 的发起人,允许访问文档 ${documentId}`);
return {
hasAccess: true,
userRole: 'assigner'
};
}
if (isAssignee) {
console.log(`✅ [verifyDocumentAccess] 用户 ${userId} 是任务 ${taskId} 的评审员,允许访问文档 ${documentId}`);
return {
hasAccess: true,
userRole: 'assignee'
};
}
// 用户既不是发起人也不是评审员
console.warn(`⚠️ [verifyDocumentAccess] 用户 ${userId} 无权访问任务 ${taskId} 的文档 ${documentId}`);
return {
hasAccess: false,
reason: '您没有权限访问该文档',
userRole: 'none'
};
} catch (error) {
console.error('❌ [verifyDocumentAccess] 验证失败:', error);
return {
hasAccess: false,
reason: error instanceof Error ? error.message : '权限验证失败'
};
}
}
/**
* 快速验证文档访问权限(仅返回布尔值)
* @param params 验证参数
* @returns 是否有权限访问
*/
export async function canAccessDocument(
params: DocumentAccessCheckParams
): Promise<boolean> {
const result = await verifyDocumentAccess(params);
return result.hasAccess;
}
+3
View File
@@ -43,6 +43,7 @@ export interface DocumentTypeCreateDTO {
name: string;
description?: string;
group_ids: number[]; // 改为 number[] 数组
code: string | null;
entry_module_id?: number | null;
llm_extraction_template_id?: number | null;
vlm_extraction_template_id?: number | null;
@@ -236,6 +237,7 @@ export async function createDocumentType(
name: documentType.name.trim(),
description: documentType.description || '',
entry_module_id: documentType.entry_module_id || null,
code: documentType.code || null,
group_ids: documentType.group_ids,
llm_extraction_template_id: documentType.llm_extraction_template_id || null,
vlm_extraction_template_id: documentType.vlm_extraction_template_id || null,
@@ -305,6 +307,7 @@ export async function updateDocumentType(
name: documentType.name.trim(),
description: documentType.description || '',
entry_module_id: documentType.entry_module_id || null,
code: documentType.code || null,
group_ids: documentType.group_ids,
llm_extraction_template_id: documentType.llm_extraction_template_id || null,
vlm_extraction_template_id: documentType.vlm_extraction_template_id || null,
+1 -1
View File
@@ -1023,7 +1023,7 @@ export async function getReviewPoints_fromApi(fileId: string, request: Request)
// 成功响应
if (response.data) {
// console.log('✅ [getReviewPoints_fromApi] API调用成功,返回数据结构:', JSON.stringify({
// // 评查点数量: response.data.data?.length || 0,
// 评查点数量: response.data.data?.length || 0,
// // 统计信息: response.data.stats,
// // 评查信息: response.data.reviewInfo,
// 有文档数据: response.data.document,