fix: 1. 重新对齐交叉评查的接口。

2. 确认评查结果的接口对接。 3. 新增评查点适配省级创建的响应数据和其他用户创建的单条响应数据。  4. 文档列表的文档类型通过通用的查询接口查询文档类型。优化加载状态的时机。
This commit is contained in:
2025-12-11 11:16:50 +08:00
parent ba517d7b9c
commit d8bba607fc
18 changed files with 3435 additions and 1086 deletions
+64 -30
View File
@@ -136,7 +136,7 @@ export async function submitCrossCheckingOpinion(
evaluation_result_id: opinionData.reviewPointResultId evaluation_result_id: opinionData.reviewPointResultId
}; };
const response = await axios.post(`${API_BASE_URL}/admin/cross_review/proposals`, requestData, { const response = await axios.post(`${API_BASE_URL}/api/v2/cross_review/proposals`, requestData, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
@@ -196,8 +196,8 @@ export async function getCrossCheckingOpinions(
// 如果没传userId,默认用1 // 如果没传userId,默认用1
const realUserId = userId ?? 1; const realUserId = userId ?? 1;
// 实际后端API调用,拼接API_BASE_URL // 实际后端API调用,拼接API_BASE_URL
const response = await axios.post(`${API_BASE_URL}/admin/cross_review/proposals/document`, { const response = await axios.post(`${API_BASE_URL}/api/v2/cross_review/proposals/document`, {
user_id: realUserId, // user_id: realUserId,
document_id: documentId, // 如果后端需要document_id可以加上 document_id: documentId, // 如果后端需要document_id可以加上
page, page,
page_size: pageSize page_size: pageSize
@@ -209,7 +209,7 @@ export async function getCrossCheckingOpinions(
}); });
const data = response.data; const data = response.data;
console.log('最原始的返回data', data); // console.log('最原始的返回data', data);
// 处理新的数据结构,支持分页 // 处理新的数据结构,支持分页
const responseData = data.data || data; const responseData = data.data || data;
const pagination = data.pagination; const pagination = data.pagination;
@@ -318,24 +318,24 @@ export async function performOpinionAction(
switch (actionData.action) { switch (actionData.action) {
case 'agree': case 'agree':
message = '已赞同该意见'; message = '已赞同该意见';
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`; endpoint = `${API_BASE_URL}/api/v2/cross_review/proposals/${actionData.opinionId}/votes`;
requestBody = { vote_type: 'agree', voter_id: userInfo?.user_id }; requestBody = { vote_type: 'agree', voter_id: userInfo?.user_id };
break; break;
case 'disagree': case 'disagree':
message = '已反对该意见'; message = '已反对该意见';
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`; endpoint = `${API_BASE_URL}/api/v2/cross_review/proposals/${actionData.opinionId}/votes`;
requestBody = { vote_type: 'disagree', voter_id: userInfo?.user_id }; requestBody = { vote_type: 'disagree', voter_id: userInfo?.user_id };
break; break;
case 'withdraw_vote': case 'withdraw_vote':
message = '已撤销投票'; message = '已撤销投票';
// 撤销投票的接口,根据实际API调整 // 撤销投票的接口,根据实际API调整
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`; endpoint = `${API_BASE_URL}/api/v2/cross_review/proposals/${actionData.opinionId}/votes`;
requestBody = { vote_type: 'cancel', voter_id: userInfo?.user_id }; requestBody = { vote_type: 'cancel', voter_id: userInfo?.user_id };
break; break;
case 'withdraw_opinion': case 'withdraw_opinion':
message = '已撤销意见'; message = '已撤销意见';
// 撤销意见的接口,根据实际API调整 // 撤销意见的接口,根据实际API调整
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}`; endpoint = `${API_BASE_URL}/api/v2/cross_review/proposals/${actionData.opinionId}`;
requestBody = {}; requestBody = {};
break; break;
default: default:
@@ -389,43 +389,77 @@ export async function performOpinionAction(
/** /**
* 完成评查 * 完成评查(确认文档审核完成)
* @param taskId 任务ID
* @param documentId 文档ID * @param documentId 文档ID
* @param frontendJWT JWT token
* @returns 完成评查结果 * @returns 完成评查结果
*
* 🔥 接口文档: auth_doc/交叉评查接口文档(1).md 接口10
* 📍 API地址: POST /admin/v2/cross_review/tasks/{task_id}/documents/{document_id}/complete
*/ */
export async function confirmReviewResults( export async function confirmReviewResults(
taskId: string | number,
documentId: string | number, documentId: string | number,
frontendJWT?: string frontendJWT?: string
): Promise<{data?: unknown, error?: string, status?: number}> { ): Promise<{data?: unknown, error?: string, status?: number}> {
try { try {
// 通过postgrest的post请求去documents表中进行查找id等于documentId的数据,更新documents表的audit_status为1 if (!taskId) {
const response = await postgrestPut(`/api/postgrest/proxy/documents`, { return { error: '任务ID不能为空', status: 400 };
audit_status: 1 }
}, { if (!documentId) {
id: documentId return { error: '文档ID不能为空', status: 400 };
}, frontendJWT); }
if(response.error) {
// 调用后端API确认文档审核完成
// 接口: POST /admin/v2/cross_review/tasks/{task_id}/documents/{document_id}/complete
const response = await axios.post(
`${API_BASE_URL}/admin/v2/cross_review/tasks/${taskId}/documents/${documentId}/complete`,
{}, // 无需请求体
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${frontendJWT}`
}
}
);
const data = response.data;
// 检查响应是否成功
if (data?.success || data?.code === 0) {
return { return {
error: response.error, data: {
status: response.status task_id: data.task_id || taskId,
}; document_id: data.document_id || documentId,
} message: data.message || '文档评查已完成'
const extractedData = extractApiData<unknown>(response.data); }
if(!extractedData) {
return {
error: '更新文档状态失败',
status: 500
}; };
} }
// 数据为空或格式不正确
console.error('❌ [confirmReviewResults] API响应数据异常:', data);
return { return {
data: extractedData error: data?.message || '确认文档审核失败',
status: 500
}; };
} catch (error) { } catch (error) {
console.error('完成评查失败:', error); console.error('完成评查失败:', error);
// 正确处理 axios 错误响应
let errorMessage = '完成评查失败';
if (axios.isAxiosError(error) && error.response?.data) {
// 从 axios 错误响应中提取 detail 或 msg 字段
errorMessage = error.response.data.detail || error.response.data.msg || errorMessage;
} else if (error instanceof Error) {
errorMessage = error.message || errorMessage;
}
return { return {
error: error instanceof Error ? error.message : '完成评查失败', error: errorMessage,
status: 500 status: axios.isAxiosError(error) ? error.response?.status || 500 : 500
}; };
} }
} }
@@ -444,7 +478,7 @@ export async function checkProposalVotes(
document_id: documentId document_id: documentId
}; };
const response = await axios.post(`${API_BASE_URL}/admin/cross_review/proposals/document/check_pending_votes`, requestData, { const response = await axios.post(`${API_BASE_URL}/api/v2/cross_review/proposals/document/check_pending_votes`, requestData, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
+2 -2
View File
@@ -403,7 +403,7 @@ export async function getUserTaskDocuments(page: number = 1, pageSize: number =
try { try {
// 拼接绝对路径,去除多余斜杠 // 拼接绝对路径,去除多余斜杠
const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL; const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
const url = `${base}/admin/cross_review/tasks/user_tasks`; const url = `${base}/admin/v2/cross_review/tasks/user_tasks`;
const response = await axios.post(url, { const response = await axios.post(url, {
page: page, page: page,
@@ -445,7 +445,7 @@ export async function getTaskDocuments(taskId: number, page: number = 1, pageSiz
try { try {
// 拼接绝对路径,去除多余斜杠 // 拼接绝对路径,去除多余斜杠
const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL; const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
const url = `${base}/admin/cross_review/tasks/${taskId}/documents`; const url = `${base}/admin/v2/cross_review/tasks/${taskId}/documents`;
// console.log('最终请求URL:', url); // console.log('最终请求URL:', url);
const response = await axios.post(url, { const response = await axios.post(url, {
+6 -1
View File
@@ -66,6 +66,7 @@ export interface DocumentTypeSearchParams {
name?: string; name?: string;
group_id?: number; // 按评查点分组ID筛选 group_id?: number; // 按评查点分组ID筛选
entry_module_id?: number; // 按入口模块ID筛选 entry_module_id?: number; // 按入口模块ID筛选
ids?: number[]; // 按ID列表筛选
page?: number; page?: number;
pageSize?: number; pageSize?: number;
} }
@@ -109,7 +110,7 @@ export async function getDocumentTypes(
const pageSize = searchParams.pageSize || 10; const pageSize = searchParams.pageSize || 10;
// 构建查询参数 // 构建查询参数
const params: Record<string, string | number> = { const params: Record<string, string | number | number[]> = {
page, page,
page_size: pageSize, page_size: pageSize,
}; };
@@ -126,6 +127,10 @@ export async function getDocumentTypes(
params.entry_module_id = searchParams.entry_module_id; params.entry_module_id = searchParams.entry_module_id;
} }
if (searchParams.ids) {
params.ids = searchParams.ids;
}
const response = await apiRequest<ApiResponse<ListResponseData>>( const response = await apiRequest<ApiResponse<ListResponseData>>(
'/api/v3/document-types', '/api/v3/document-types',
{ {
+217 -146
View File
@@ -1,5 +1,5 @@
import { postgrestGet, type PostgrestParams, postgrestPut, postgrestPost } from "../postgrest-client"; import { postgrestGet, type PostgrestParams, postgrestPut, postgrestPost } from "../postgrest-client";
import {getDocumentWithNoUserId} from "~/api/files/documents"; // import {getDocumentWithNoUserId} from "~/api/files/documents";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { getUserSession } from "~/api/login/auth.server"; import { getUserSession } from "~/api/login/auth.server";
import { apiRequest } from "../axios-client"; import { apiRequest } from "../axios-client";
@@ -124,13 +124,13 @@ interface ScoringProposal {
document_id: string | number; document_id: string | number;
} }
/** /** ============== (废弃,已经采用api接口的方式进行查询)
* 获取当前评查文件的所有评查点结果 * 获取当前评查文件的所有评查点结果
* @param fileId 评查文件ID * @param fileId 评查文件ID
* @param request Remix请求对象,用于获取用户会话 * @param request Remix请求对象,用于获取用户会话
* @returns 评查点结果列表和统计数据 * @returns 评查点结果列表和统计数据
*/ */
export async function getReviewPoints(fileId: string, request: Request) { export async function getReviewPoints(fileId: string, request: Request) {
// 获取用户会话信息 // 获取用户会话信息
const { userInfo, frontendJWT } = await getUserSession(request); const { userInfo, frontendJWT } = await getUserSession(request);
@@ -744,17 +744,27 @@ export async function getReviewPoints(fileId: string, request: Request) {
* 更新评查结果 * 更新评查结果
* @param resultId 评查结果ID * @param resultId 评查结果ID
* @param editAuditStatusId 审核状态ID * @param editAuditStatusId 审核状态ID
* @param result 评查结果 (true/false) * @param result 评查结果 (true/false/review)
* @param message 评查意见 * @param message 评查意见
* @param request Remix请求对象,用于获取用户会话 * @param request Remix请求对象,用于获取用户会话
* @param documentId 文档ID(可选,用于创建新审核状态)
* @param evaluationPointId 评查点ID(可选,用于创建新审核状态)
* @returns 更新后的评查结果 * @returns 更新后的评查结果
*
* 🔥 接口文档: auth_doc/评查审核接口对接文档.md
* 📍 使用接口:
* - 3.1 更新评查结果: PATCH /admin/v2/evaluations/results/{result_id}
* - 3.2 创建审核状态: POST /admin/v2/evaluations/audit-status
* - 3.3 更新审核状态: PATCH /admin/v2/evaluations/audit-status/{audit_status_id}
*/ */
export async function updateReviewResult( export async function updateReviewResult(
resultId: string, resultId: string,
editAuditStatusId: string | number, editAuditStatusId: string | number,
result: string, result: string,
message: string, message: string,
request: Request request: Request,
documentId?: string | number,
evaluationPointId?: string | number
): Promise<{ ): Promise<{
data?: unknown; data?: unknown;
error?: string; error?: string;
@@ -763,122 +773,184 @@ export async function updateReviewResult(
try { try {
// 获取用户会话信息 // 获取用户会话信息
const { userInfo, frontendJWT } = await getUserSession(request); const { userInfo, frontendJWT } = await getUserSession(request);
if (!userInfo?.user_id) { if (!userInfo?.user_id) {
console.error("用户身份验证失败"); console.error("用户身份验证失败");
return { error: '用户身份验证失败', status: 401 }; return { error: '用户身份验证失败', status: 401 };
} }
const userId = userInfo.user_id;
if (!resultId) { if (!resultId) {
return { error: '评查结果ID不能为空', status: 400 }; return { error: '评查结果ID不能为空', status: 400 };
} }
// 首先获取当前评查结果数据
const currentResultResponse = await postgrestGet('/api/postgrest/proxy/evaluation_results', {
select: '*',
filter: { id: `eq.${resultId}` },
token: frontendJWT
});
console.log('/api/postgrest/proxy/evaluation_results',currentResultResponse.error)
if (currentResultResponse.error) {
return { error: currentResultResponse.error, status: currentResultResponse.status };
}
const currentResultData = extractApiData<EvaluationResult[]>(currentResultResponse.data);
if (!currentResultData || !Array.isArray(currentResultData) || currentResultData.length === 0) {
return { error: '未找到评查结果数据', status: 404 };
}
const currentResult = currentResultData[0];
const currentEvaluatedResults = currentResult.evaluated_results || {};
// 判断是否是重新审核操作 // 判断是否是重新审核操作
const isReview = result === 'review'; const isReview = result === 'review';
// console.log('isReview-------', result);
// ============================================
// 构建要更新的数据,保留原有字段 // 步骤1: 调用3.1接口更新评查结果(如果不是重新审核操作)
const updatedEvaluatedResults = { // ============================================
...currentEvaluatedResults, if (!isReview) {
// 如果是重新审核操作,不更新result和message // 构建请求数据
...(isReview ? {} : { result: result === 'true' ? true : false, message }), const updateResultData: {
}; result?: 'true' | 'false';
message?: string;
const updatedData = { } = {
evaluated_results: updatedEvaluatedResults result: result === 'true' ? 'true' : 'false',
}; message: message
};
// 调用 API 更新评查点结果数据
const resultResponse = await postgrestPut<unknown, typeof updatedData>( // 调用 3.1 接口: PATCH /admin/v2/evaluations/results/{result_id}
'/api/postgrest/proxy/evaluation_results', const resultResponse = await apiRequest<{
updatedData, success: boolean;
{ id: resultId }, message: string;
frontendJWT data: {
); result_id: number;
updated_fields: string[];
if (resultResponse.error) { };
return { error: resultResponse.error, status: resultResponse.status }; }>(
`/admin/v2/evaluations/results/${resultId}`,
{
method: 'PATCH',
headers: {
'Authorization': `Bearer ${frontendJWT}`,
'Content-Type': 'application/json'
},
data: updateResultData
}
);
if (resultResponse.error) {
console.error('❌ [updateReviewResult] 3.1接口调用失败:', resultResponse.error);
return { error: resultResponse.error, status: resultResponse.status || 500 };
}
if (!resultResponse.data?.success) {
console.error('❌ [updateReviewResult] 3.1接口响应异常:', resultResponse.data);
return { error: resultResponse.data?.message || '更新评查结果失败', status: 500 };
}
} }
// 处理audit_status表的更新或新增 // ============================================
// 步骤2: 处理审核状态(创建或更新)
// ============================================
// 确定edit_audit_status的值: // 确定edit_audit_status的值:
// 如果是重新审核操作,值为0;否则值为1 // 如果是重新审核操作,值为0;否则值为1
const editAuditStatusValue = isReview ? 0 : 1; const editAuditStatusValue = isReview ? 0 : 1;
// console.log('editAuditStatusValue-------', editAuditStatusValue);
// console.log('editAuditStatusId-------', editAuditStatusId);
if (editAuditStatusId && editAuditStatusId !== '') { if (editAuditStatusId && editAuditStatusId !== '') {
// 更新现有审核状态记录 // ============================================
const auditStatusResponse = await postgrestPut( // 使用3.3接口更新现有审核状态记录
'/api/postgrest/proxy/audit_status', // PATCH /admin/v2/evaluations/audit-status/{audit_status_id}
{ // ============================================
edit_audit_status: editAuditStatusValue, const updateAuditData: {
// 重新审核时不更新message edit_audit_status: number;
...(isReview ? {} : { message }) message?: string;
}, } = {
{ edit_audit_status: editAuditStatusValue
id: editAuditStatusId, };
user_id: userId // 添加用户ID条件,确保只能更新自己的记录
}, // 重新审核时不更新message
frontendJWT if (!isReview) {
updateAuditData.message = message;
}
const auditStatusResponse = await apiRequest<{
success: boolean;
message: string;
data: {
audit_status_id: number;
updated_fields: string[];
};
}>(
`/admin/v2/evaluations/audit-status/${editAuditStatusId}`,
{
method: 'PATCH',
headers: {
'Authorization': `Bearer ${frontendJWT}`,
'Content-Type': 'application/json'
},
data: updateAuditData
}
); );
if (auditStatusResponse.error) { if (auditStatusResponse.error) {
console.error('❌ [updateReviewResult] 3.3接口调用失败:', auditStatusResponse.error);
return { error: auditStatusResponse.error, status: auditStatusResponse.status || 500 }; return { error: auditStatusResponse.error, status: auditStatusResponse.status || 500 };
} }
} else {
// 如果没有editAuditStatusId,则创建新记录 if (!auditStatusResponse.data?.success) {
// 首先获取文档ID和评查点ID console.error('❌ [updateReviewResult] 3.3接口响应异常:', auditStatusResponse.data);
const documentId = currentResult.document_id; return { error: auditStatusResponse.data?.message || '更新审核状态失败', status: 500 };
const evaluationPointId = currentResult.evaluation_point_id;
// 创建新的审核状态记录
const newAuditStatus = {
document_id: documentId,
evaluation_point_id: evaluationPointId,
evaluation_result_id: resultId,
edit_audit_status: editAuditStatusValue,
message: isReview ? '' : message,
user_id: userId // 添加用户ID
};
// 使用postgrestPost创建新记录
const postResponse = await postgrestPost('/api/postgrest/proxy/audit_status', newAuditStatus, frontendJWT);
if (postResponse.error) {
return { error: postResponse.error, status: postResponse.status || 500 };
} }
return { data: auditStatusResponse.data };
} else {
// ============================================
// 使用3.2接口创建新审核状态记录
// POST /admin/v2/evaluations/audit-status
// ============================================
// 如果没有传入documentId和evaluationPointId,需要先获取
if (!documentId || !evaluationPointId) {
// 从评查结果中获取document_id和evaluation_point_id
const currentResultResponse = await postgrestGet('/api/postgrest/proxy/evaluation_results', {
select: 'document_id,evaluation_point_id',
filter: { id: `eq.${resultId}` },
token: frontendJWT
});
if (currentResultResponse.error) {
return { error: currentResultResponse.error, status: currentResultResponse.status };
}
const currentResultData = extractApiData<EvaluationResult[]>(currentResultResponse.data);
if (!currentResultData || !Array.isArray(currentResultData) || currentResultData.length === 0) {
return { error: '未找到评查结果数据', status: 404 };
}
documentId = currentResultData[0].document_id;
evaluationPointId = currentResultData[0].evaluation_point_id;
}
// 创建新的审核状态记录
const newAuditStatusData = {
document_id: Number(documentId),
evaluation_point_id: Number(evaluationPointId),
evaluation_result_id: Number(resultId),
edit_audit_status: editAuditStatusValue,
message: isReview ? '' : message
};
const createAuditResponse = await apiRequest<{
id: number;
user_id: number;
document_id: number;
evaluation_point_id: number;
evaluation_result_id: number;
edit_audit_status: number;
message: string;
created_at: string;
updated_at: string;
}>(
`/admin/v2/evaluations/audit-status`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${frontendJWT}`,
'Content-Type': 'application/json'
},
data: newAuditStatusData
}
);
if (createAuditResponse.error) {
console.error('❌ [updateReviewResult] 3.2接口调用失败:', createAuditResponse.error);
return { error: createAuditResponse.error, status: createAuditResponse.status || 500 };
}
return { data: createAuditResponse.data };
} }
const extractedData = extractApiData<unknown>(resultResponse.data);
if (!extractedData) {
return { error: '更新评查结果失败', status: 500 };
}
return { data: extractedData };
} catch (error) { } catch (error) {
console.error('更新评查结果失败:', error); console.error('更新评查结果失败:', error);
return { return {
@@ -893,6 +965,9 @@ export async function updateReviewResult(
* @param documentId 文档ID * @param documentId 文档ID
* @param request Remix请求对象,用于获取用户会话 * @param request Remix请求对象,用于获取用户会话
* @returns 更新结果 * @returns 更新结果
*
* 🔥 接口文档: auth_doc/评查审核接口对接文档.md 3.4
* 📍 API地址: PATCH /admin/v2/evaluation/documents/{document_id}/confirm
*/ */
export async function confirmReviewResults(documentId: string, request: Request): Promise<{ export async function confirmReviewResults(documentId: string, request: Request): Promise<{
data?: { auditStatus: number; }; data?: { auditStatus: number; };
@@ -902,57 +977,53 @@ export async function confirmReviewResults(documentId: string, request: Request)
try { try {
// 获取用户会话信息 // 获取用户会话信息
const { userInfo, frontendJWT } = await getUserSession(request); const { userInfo, frontendJWT } = await getUserSession(request);
if (!userInfo?.user_id) { if (!userInfo?.user_id) {
console.error("用户身份验证失败"); console.error("用户身份验证失败");
return { error: '用户身份验证失败', status: 401 }; return { error: '用户身份验证失败', status: 401 };
} }
const userId = userInfo.user_id;
if (!documentId) { if (!documentId) {
return { error: '文档ID不能为空', status: 400 }; return { error: '文档ID不能为空', status: 400 };
} }
// 获取该文档的所有评查点结果 // 调用后端API确认文档审核完成
// const reviewPointsResponse = await getReviewPoints(documentId); // 接口: PATCH /admin/v2/evaluations/documents/{document_id}/confirm
const response = await apiRequest<{
// if ('error' in reviewPointsResponse && reviewPointsResponse.error) { success: boolean;
// return { error: reviewPointsResponse.error, status: reviewPointsResponse.status }; message: string;
// } data: {
document_id: number;
// if (!('data' in reviewPointsResponse) || !reviewPointsResponse.data || !Array.isArray(reviewPointsResponse.data)) { audit_status: number;
// return { error: '获取评查点数据失败', status: 500 }; };
// } }>(
`/admin/v2/evaluations/documents/${documentId}/confirm`,
// // 计算总分数 {
// const totalScore = reviewPointsResponse.stats?.score || 0; method: 'PATCH',
headers: {
// // 根据总分确定审核状态 'Authorization': `Bearer ${frontendJWT}`,
// // <80分:不通过(-1),>=80分:通过(1) 'Content-Type': 'application/json'
// const auditStatus = totalScore < 80 ? -1 : 1; },
data: {
// 更新文档的审核状态 audit_status: 1
const updateDocumentParams = { }
audit_status: 1 }
};
// 调用API更新文档审核状态
const response = await postgrestPut<{ id: string }, typeof updateDocumentParams>(
'/api/postgrest/proxy/documents',
updateDocumentParams,
{
id: documentId,
user_id: userId // 添加用户ID条件,确保只能更新自己的文档
},
frontendJWT
); );
// 处理错误响应
if (response.error) { if (response.error) {
return { error: response.error, status: response.status }; console.error('❌ [confirmReviewResults] API调用失败:', response.error);
return { error: response.error, status: response.status || 500 };
} }
return { data: { auditStatus: 1} }; // 成功响应
if (response.data?.success) {
return { data: { auditStatus: response.data.data?.audit_status || 1 } };
}
// 数据为空或格式不正确
console.error('❌ [confirmReviewResults] API响应数据异常:', response.data);
return { error: response.data?.message || '确认文档审核失败', status: 500 };
} catch (error) { } catch (error) {
console.error('确认评查结果失败:', error); console.error('确认评查结果失败:', error);
return { return {
+1 -754
View File
@@ -434,254 +434,7 @@ export async function getRule(id: string, token?: string): Promise<{data: Rule;
} }
} }
/**
* 创建新评查点
* @param ruleData 评查点数据
* @param token JWT token (可选)
* @returns 创建的评查点
*/
export async function createRule(ruleData: Omit<Rule, 'id' | 'createdAt' | 'updatedAt'>, token?: string): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 1. 验证必填字段
if (!ruleData.name || !ruleData.code) {
return { error: '评查点名称和编码不能为空', status: 400 };
}
// 2. 验证名称长度(1-100字符)
const trimmedName = ruleData.name.trim();
if (trimmedName.length === 0 || trimmedName.length > 100) {
return { error: '评查点名称长度必须在1-100个字符之间', status: 400 };
}
// 3. 验证编码格式(仅允许字母、数字、连字符和下划线)
const trimmedCode = ruleData.code.trim();
if (!/^[a-zA-Z0-9-_]+$/.test(trimmedCode)) {
return { error: '评查点编码只能包含字母、数字、连字符和下划线', status: 400 };
}
// 4. 验证编码唯一性
const existingRulesResponse = await getRulesList({
keyword: trimmedCode,
pageSize: 10,
token
});
if (existingRulesResponse.data && existingRulesResponse.data.rules.length > 0) {
// 精确匹配检查(因为keyword是模糊搜索)
const exactMatch = existingRulesResponse.data.rules.some(r => r.code === trimmedCode);
if (exactMatch) {
return { error: '评查点编码已存在,请使用其他编码', status: 409 };
}
}
// 5. 验证分组ID有效性
if (!ruleData.groupId) {
return { error: '必须选择所属规则组', status: 400 };
}
// 检查分组是否存在
const groupResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number; name: string; pid: number}> }>('/api/postgrest/proxy/evaluation_point_groups', {
filter: { 'id': `eq.${ruleData.groupId}` },
select: 'id,name,pid',
token
});
let groupExists = false;
if (groupResponse.data && 'code' in groupResponse.data && groupResponse.data.data) {
groupExists = Array.isArray(groupResponse.data.data) && groupResponse.data.data.length > 0;
} else if (Array.isArray(groupResponse.data)) {
groupExists = groupResponse.data.length > 0;
}
if (!groupExists) {
return { error: '所选规则组不存在', status: 404 };
}
// 将前端模型转换为API接受的格式
const apiRuleData = {
code: trimmedCode,
name: trimmedName,
evaluation_point_groups_id: parseInt(ruleData.groupId),
risk: ruleData.priority === 'high' ? '高' : ruleData.priority === 'medium' ? '中' : '低',
description: ruleData.description || '',
is_enabled: ruleData.isActive !== undefined ? ruleData.isActive : true,
// 以下是默认值,实际应用中需要根据业务逻辑设置
references_laws: {},
extraction_config: {
type: "OCR+LLM",
fields: []
},
evaluation_config: {
rules: [],
logicType: "and"
},
pass_message: "",
fail_message: "",
suggestion_message: "",
suggestion_message_type: "warning",
post_action: "none",
action_config: ""
};
// 使用postgrestPost创建评查点
const response = await postgrestPost<{code: number; msg: string; data: ApiRule}, typeof apiRuleData>('/api/postgrest/proxy/evaluation_points', apiRuleData, token);
// 检查是否有错误响应
if (response.error) {
return { error: response.error, status: response.status };
}
// 确保响应数据存在且符合预期格式
if (!response.data || !response.data.data) {
return { error: '接口返回数据格式不正确', status: 500 };
}
// 将API返回的数据映射到前端模型
const rule = mapApiRuleToFrontendModel(response.data.data);
return { data: rule };
} catch (error) {
console.error('创建评查点出错:', error);
return {
error: error instanceof Error ? error.message : '创建评查点失败',
status: 500
};
}
}
/**
* 更新评查点
* @param id 评查点ID
* @param ruleData 评查点数据
* @param token JWT token (可选)
* @returns 更新后的评查点
*/
export async function updateRule(id: string, ruleData: Partial<Omit<Rule, 'id' | 'createdAt' | 'updatedAt'>>, token?: string): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 1. 验证评查点ID有效性
const existingRuleResponse = await getRule(id, token);
if (existingRuleResponse.error || !existingRuleResponse.data) {
return { error: '评查点不存在', status: 404 };
}
// 2. 验证名称长度(如果提供)
if (ruleData.name !== undefined) {
const trimmedName = ruleData.name.trim();
if (trimmedName.length === 0 || trimmedName.length > 100) {
return { error: '评查点名称长度必须在1-100个字符之间', status: 400 };
}
}
// 3. 验证编码格式和唯一性(如果提供)
if (ruleData.code !== undefined) {
const trimmedCode = ruleData.code.trim();
// 验证编码格式
if (!/^[a-zA-Z0-9-_]+$/.test(trimmedCode)) {
return { error: '评查点编码只能包含字母、数字、连字符和下划线', status: 400 };
}
// 验证编码唯一性(排除自身)
const existingRulesResponse = await getRulesList({
keyword: trimmedCode,
pageSize: 10,
token
});
if (existingRulesResponse.data && existingRulesResponse.data.rules.length > 0) {
// 精确匹配检查,排除当前评查点自身
const exactMatch = existingRulesResponse.data.rules.some(r => r.code === trimmedCode && r.id !== id);
if (exactMatch) {
return { error: '评查点编码已被其他评查点使用', status: 409 };
}
}
}
// 4. 验证分组ID有效性(如果提供)
if (ruleData.groupId !== undefined) {
const groupResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number; name: string; pid: number}> }>('/api/postgrest/proxy/evaluation_point_groups', {
filter: { 'id': `eq.${ruleData.groupId}` },
select: 'id,name,pid',
token
});
let groupExists = false;
if (groupResponse.data && 'code' in groupResponse.data && groupResponse.data.data) {
groupExists = Array.isArray(groupResponse.data.data) && groupResponse.data.data.length > 0;
} else if (Array.isArray(groupResponse.data)) {
groupExists = groupResponse.data.length > 0;
}
if (!groupExists) {
return { error: '所选规则组不存在', status: 404 };
}
}
// 构建API接受的更新数据
const apiRuleData: Record<string, unknown> = {};
if (ruleData.code !== undefined) {
apiRuleData.code = ruleData.code.trim();
}
if (ruleData.name !== undefined) {
apiRuleData.name = ruleData.name.trim();
}
if (ruleData.groupId !== undefined) {
apiRuleData.evaluation_point_groups_id = parseInt(ruleData.groupId);
}
if (ruleData.priority !== undefined) {
apiRuleData.risk = ruleData.priority === 'high' ? '高' : ruleData.priority === 'medium' ? '中' : '低';
}
if (ruleData.description !== undefined) {
apiRuleData.description = ruleData.description;
}
if (ruleData.isActive !== undefined) {
apiRuleData.is_enabled = ruleData.isActive;
}
// 使用postgrestPut更新评查点 - 使用正确的PostgREST格式
const response = await postgrestPut<{code: number; msg: string; data: ApiRule} | ApiRule[], typeof apiRuleData>('/api/postgrest/proxy/evaluation_points', apiRuleData, { id: parseInt(id) }, token);
// 检查是否有错误响应
if (response.error) {
return { error: response.error, status: response.status };
}
// 处理响应数据(PostgREST可能返回数组或包装对象)
let updatedRule: ApiRule | null = null;
if (response.data) {
// 如果是数组格式(PostgREST标准响应)
if (Array.isArray(response.data)) {
updatedRule = response.data.length > 0 ? response.data[0] : null;
}
// 如果是包装对象格式
else if ('data' in response.data && response.data.data) {
updatedRule = response.data.data as ApiRule;
}
}
if (!updatedRule) {
return { error: '更新成功但无法获取更新后的数据', status: 500 };
}
// 将API返回的数据映射到前端模型
const rule = mapApiRuleToFrontendModel(updatedRule);
return { data: rule };
} catch (error) {
console.error('更新评查点出错:', error);
return {
error: error instanceof Error ? error.message : '更新评查点失败',
status: 500
};
}
}
/** /**
* 删除评查点 * 删除评查点
@@ -722,47 +475,7 @@ export async function deleteRule(id: string, token?: string): Promise<{data: {su
} }
} }
/**
* 复制评查点
* @param id 评查点ID
* @param token JWT token (可选)
* @returns 新创建的评查点
*/
export async function duplicateRule(id: string, token?: string): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 1. 获取原评查点详情
const ruleResponse = await getRule(id, token);
if (ruleResponse.error || !ruleResponse.data) {
return { error: ruleResponse.error || '获取评查点详情失败', status: 500 };
}
// 2. 准备新评查点数据
const rule = ruleResponse.data;
// 创建新评查点对象
const newRuleData = {
code: `${rule.code}-COPY`,
name: `${rule.name} (复制)`,
ruleType: rule.ruleType,
groupId: rule.groupId,
groupName: rule.groupName,
priority: rule.priority,
description: rule.description,
isActive: rule.isActive
};
// 3. 创建新评查点
return createRule(newRuleData, token);
} catch (error) {
console.error('复制评查点出错:', error);
return {
error: error instanceof Error ? error.message : '复制评查点失败',
status: 500
};
}
}
/** /**
* 评查点类型 * 评查点类型
@@ -1143,137 +856,6 @@ export function convertApiRuleToFormData(apiRule: ApiRule): FormattedEvaluationP
return formattedData; return formattedData;
} }
/**
* 获取单个评查点数据
* @param id 评查点ID
* @returns 评查点数据
*/
/**
* 获取格式化的评查点数据(用于列表视图)
* @param id 评查点ID
* @returns 格式化的评查点数据
*/
export async function getFormattedEvaluationPoint(id: number): Promise<{
data?: FormattedEvaluationPoint;
error?: string;
status?: number;
}> {
try {
// console.log(`获取评查点数据,ID: ${id}`);
// 使用 postgrestGet 替代直接调用 fetch
const postgrestParams: PostgrestParams = {
select: `*`,
filter: {
'id': `eq.${id}`
}
};
const response = await postgrestGet<{code: number; msg: string; data: ApiRule[]} | ApiRule[]>('/api/postgrest/proxy/evaluation_points', postgrestParams);
if (response.error) {
return {
error: response.error,
status: response.status
};
}
// 使用 extractApiData 统一处理响应数据
const extractedData = extractApiData<ApiRule[]>(response.data);
if (extractedData && Array.isArray(extractedData) && extractedData.length > 0) {
// 转换数据为前端格式
const formattedData = convertApiRuleToFormData(extractedData[0]);
return { data: formattedData };
} else {
return {
error: '获取数据失败: 返回数据为空',
status: 404
};
}
} catch (error) {
console.error('获取评查点数据失败:', error);
return {
error: error instanceof Error ? error.message : '获取评查点数据失败',
status: 500
};
}
}
/**
* 获取评查点组数据
* @returns 评查点组列表
*/
export async function getEvaluationPointGroups(): Promise<{
data?: Array<{
id: number;
pid: number;
code: string;
name: string;
description: string;
is_enabled: boolean;
created_at: string;
updated_at: string;
}>;
error?: string;
status?: number;
}> {
try {
// console.log("获取评查点组数据");
// 使用 postgrestGet 替代直接调用 fetch
const postgrestParams: PostgrestParams = {
select: `*`
};
// 定义评查点组类型
type EvaluationPointGroupType = {
id: number;
pid: number;
code: string;
name: string;
description: string;
is_enabled: boolean;
created_at?: string;
updated_at?: string;
};
const response = await postgrestGet<{code: number; msg: string; data: EvaluationPointGroupType[]} | EvaluationPointGroupType[]>('/api/postgrest/proxy/evaluation_point_groups', postgrestParams);
if (response.error) {
return {
error: response.error,
status: response.status
};
}
// 使用 extractApiData 统一处理响应数据
const extractedData = extractApiData<EvaluationPointGroupType[]>(response.data);
if (extractedData) {
// 确保每个组都有 created_at 和 updated_at 字段
const currentTime = new Date().toISOString();
const formattedGroups = extractedData.map(group => ({
...group,
created_at: group.created_at || currentTime,
updated_at: group.updated_at || currentTime
}));
return { data: formattedGroups };
} else {
return {
error: '获取评查点组数据失败: 返回数据为空',
status: 404
};
}
} catch (error) {
console.error('获取评查点组数据失败:', error);
return {
error: error instanceof Error ? error.message : '获取评查点组数据失败',
status: 500
};
}
}
// 用于评查点输入的接口 // 用于评查点输入的接口
interface EvaluationPointInput { interface EvaluationPointInput {
@@ -1323,211 +905,6 @@ interface EvaluationPointInput {
score?: number; score?: number;
} }
/**
* 保存评查点数据
* @param evaluationPoint 评查点数据
* @param isEditMode 是否为编辑模式
* @returns 保存结果
*/
export async function saveEvaluationPoint(evaluationPoint: EvaluationPointInput, isEditMode: boolean): Promise<{
data?: FormattedEvaluationPoint[];
error?: string;
status?: number;
}> {
try {
// console.log(`${isEditMode ? '更新' : '创建'}评查点数据`);
// 创建一个符合数据库模式的数据副本
const cleanedData = {
id: evaluationPoint.id,
name: evaluationPoint.name?.trim(),
code: evaluationPoint.code?.trim(),
risk: evaluationPoint.risk || 'low',
is_enabled: evaluationPoint.is_enabled !== undefined ? evaluationPoint.is_enabled : true,
description: evaluationPoint.description || '',
references_laws: evaluationPoint.references_laws || null,
evaluation_point_groups_pid: evaluationPoint.evaluation_point_groups_pid || null,
evaluation_point_groups_id: evaluationPoint.evaluation_point_groups_id || null,
extraction_config: {
llm: {
fields: Array.isArray(evaluationPoint.extraction_config?.llm?.fields) ?
[...evaluationPoint.extraction_config.llm.fields] : [],
prompt_setting: {
type: evaluationPoint.extraction_config?.llm?.prompt_setting?.type || 'system',
template: evaluationPoint.extraction_config?.llm?.prompt_setting?.template || ''
}
},
vlm: {
fields: Array.isArray(evaluationPoint.extraction_config?.vlm?.fields) ?
[...evaluationPoint.extraction_config.vlm.fields] : [],
prompt_setting: {
type: evaluationPoint.extraction_config?.vlm?.prompt_setting?.type || 'system',
template: evaluationPoint.extraction_config?.vlm?.prompt_setting?.template || ''
}
},
regex: {
fields: Array.isArray(evaluationPoint.extraction_config?.regex?.fields) ?
[...evaluationPoint.extraction_config.regex.fields] : []
}
},
evaluation_config: {
logicType: evaluationPoint.evaluation_config?.logicType || 'and',
customLogic: evaluationPoint.evaluation_config?.customLogic || '',
rules: Array.isArray(evaluationPoint.evaluation_config?.rules) ?
evaluationPoint.evaluation_config.rules.map((rule) => ({
id: rule.id || '1',
type: rule.type || '',
config: rule.config || {}
})) : []
},
pass_message: evaluationPoint.pass_message || '文档检查通过,符合规范要求。',
fail_message: evaluationPoint.fail_message || '文档存在以下问题,请修改后重新提交。',
suggestion_message: evaluationPoint.suggestion_message || '',
suggestion_message_type: evaluationPoint.suggestion_message_type || 'warning',
post_action: evaluationPoint.post_action || 'none',
action_config: evaluationPoint.action_config || '',
score: evaluationPoint.score !== undefined ? Number(evaluationPoint.score) : 0
};
// 如果是新建模式,则删除id字段
if (!isEditMode) {
delete cleanedData.id;
}
// 确保配置对象中的规则配置被正确处理
if (cleanedData.evaluation_config && Array.isArray(cleanedData.evaluation_config.rules)) {
cleanedData.evaluation_config.rules = cleanedData.evaluation_config.rules
.filter(rule => rule && rule.type) // 确保规则有类型
.map(rule => {
// 根据规则类型确保config中有必要的字段
const config = { ...rule.config };
// 移除辅助字段(同rules.new.tsx中的逻辑)
switch (rule.type) {
case 'exists':
if (!Array.isArray(config.fields)) config.fields = [];
if (!config.logic) config.logic = 'and';
delete config.availableFields;
delete config.selectedFields;
delete config.existsLogic;
break;
case 'consistency':
if (!Array.isArray(config.pairs)) config.pairs = [];
if (!config.logic) config.logic = 'and';
delete config.availableFields;
delete config.logicRelation;
delete config.initialSourceField;
delete config.initialTargetField;
delete config.initialCompareMethod;
break;
case 'format':
if (!config.field) config.field = '';
if (!config.formatType) config.formatType = 'date';
if (!config.parameters) config.parameters = '';
delete config.availableFields;
delete config.checkField;
delete config.formatParams;
break;
case 'logic':
if (!Array.isArray(config.conditions)) config.conditions = [];
if (!config.logic) config.logic = 'and';
delete config.availableFields;
delete config.logicRelation;
delete config.initialField;
delete config.initialOperator;
delete config.initialValue;
break;
case 'regex':
if (!config.field) config.field = '';
if (!config.pattern) config.pattern = '';
if (!config.matchType) config.matchType = 'match';
delete config.availableFields;
delete config.checkField;
delete config.regexPattern;
break;
case 'ai':
if (!config.model) config.model = 'qwen14b';
if (typeof config.temperature !== 'number') config.temperature = 0.1;
if (!config.prompt) config.prompt = '';
delete config.availableFields;
break;
case 'code':
if (!config.language) config.language = 'javascript';
if (!config.code) config.code = '';
delete config.availableFields;
break;
}
return {
id: rule.id,
type: rule.type,
config
};
});
}
// console.log("准备发送到API的数据大小:", JSON.stringify(cleanedData).length, "字节");
// 使用 postgrest-client 替代直接 fetch 调用
let response;
if (isEditMode) {
// 更新操作
response = await postgrestPut<{code: number; msg: string; data: ApiRule} | ApiRule, typeof cleanedData>(
`/api/postgrest/proxy/evaluation_points`,
cleanedData,
{id: cleanedData.id!}
);
} else {
// 创建操作
response = await postgrestPost<{code: number; msg: string; data: ApiRule} | ApiRule, typeof cleanedData>(
'/api/postgrest/proxy/evaluation_points',
cleanedData
);
}
// 处理错误响应
if (response.error) {
return {
error: response.error,
status: response.status
};
}
// 使用 extractApiData 统一处理响应数据
const extractedData = extractApiData<ApiRule | ApiRule[]>(response.data);
if (extractedData) {
// 转换数据格式后返回
if (Array.isArray(extractedData)) {
return {
data: extractedData.map(rule => convertApiRuleToFormData(rule))
};
} else {
return {
data: [convertApiRuleToFormData(extractedData)]
};
}
} else {
return {
error: `${isEditMode ? '更新' : '创建'}评查点数据失败: 返回数据为空`,
status: 404
};
}
} catch (error) {
console.error("保存评查点失败:", error);
return {
error: error instanceof Error ? error.message : '保存评查点失败',
status: 500
};
}
}
/** /**
* 评查点统计信息 * 评查点统计信息
@@ -1548,129 +925,6 @@ export interface RuleStatistics {
}>; }>;
} }
/**
* 获取评查点统计信息
* @param token JWT token (可选)
* @returns 评查点统计数据
*/
export async function getRuleStatistics(token?: string): Promise<{data: RuleStatistics; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 1. 获取所有评查点基本数据(不需要分页)
const postgrestParams: PostgrestParams = {
select: 'id,is_enabled,risk,evaluation_point_groups_id',
token
};
const response = await postgrestGet<{code: number; msg: string; data: Array<{
id: number;
is_enabled: boolean;
risk: string;
evaluation_point_groups_id: number | null;
}>}>('/api/postgrest/proxy/evaluation_points', postgrestParams);
// 检查是否有错误响应
if (response.error) {
return { error: response.error, status: response.status };
}
// 提取数据
let evaluationPoints: Array<{
id: number;
is_enabled: boolean;
risk: string;
evaluation_point_groups_id: number | null;
}> = [];
if (response.data && 'code' in response.data && response.data.data) {
if (Array.isArray(response.data.data)) {
evaluationPoints = response.data.data;
}
} else if (Array.isArray(response.data)) {
evaluationPoints = response.data;
}
// 2. 计算基础统计
const totalCount = evaluationPoints.length;
const enabledCount = evaluationPoints.filter(p => p.is_enabled).length;
const disabledCount = totalCount - enabledCount;
// 3. 按风险等级统计
const byRisk = {
low: evaluationPoints.filter(p => p.risk === '低').length,
medium: evaluationPoints.filter(p => p.risk === '中').length,
high: evaluationPoints.filter(p => p.risk === '高').length
};
// 4. 按规则组统计
const groupCountMap = new Map<number, number>();
evaluationPoints.forEach(point => {
if (point.evaluation_point_groups_id !== null) {
const currentCount = groupCountMap.get(point.evaluation_point_groups_id) || 0;
groupCountMap.set(point.evaluation_point_groups_id, currentCount + 1);
}
});
// 5. 获取规则组名称
const groupIds = Array.from(groupCountMap.keys());
const byGroup: Array<{
group_id: number;
group_name: string;
count: number;
}> = [];
if (groupIds.length > 0) {
// 批量查询规则组信息
const groupsParams: PostgrestParams = {
select: 'id,name',
filter: {
'id': `in.(${groupIds.join(',')})`
},
token
};
const groupsResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number; name: string}>}>('/api/postgrest/proxy/evaluation_point_groups', groupsParams);
let groups: Array<{id: number; name: string}> = [];
if (groupsResponse.data && 'code' in groupsResponse.data && groupsResponse.data.data) {
if (Array.isArray(groupsResponse.data.data)) {
groups = groupsResponse.data.data;
}
} else if (Array.isArray(groupsResponse.data)) {
groups = groupsResponse.data;
}
// 组合统计数据
groups.forEach(group => {
byGroup.push({
group_id: group.id,
group_name: group.name,
count: groupCountMap.get(group.id) || 0
});
});
// 按数量降序排序
byGroup.sort((a, b) => b.count - a.count);
}
// 返回统计结果
const statistics: RuleStatistics = {
total_count: totalCount,
enabled_count: enabledCount,
disabled_count: disabledCount,
by_risk: byRisk,
by_group: byGroup
};
return { data: statistics };
} catch (error) {
console.error('获取评查点统计信息失败:', error);
return {
error: error instanceof Error ? error.message : '获取评查点统计信息失败',
status: 500
};
}
}
/** /**
* 批量更新评查点启用状态 * 批量更新评查点启用状态
@@ -1696,13 +950,6 @@ export async function batchUpdateRuleStatus(
// 逐个验证并更新 // 逐个验证并更新
for (const id of ids) { for (const id of ids) {
try { try {
// 验证评查点是否存在
const existingRule = await getRule(id, token);
if (existingRule.error || !existingRule.data) {
failedIds.push(id);
errors.push({ id, error: '评查点不存在' });
continue;
}
// 执行更新 // 执行更新
const updateResult = await updateRule(id, { isActive: is_enabled }, token); const updateResult = await updateRule(id, { isActive: is_enabled }, token);
+76 -31
View File
@@ -2,6 +2,7 @@ import { postgrestGet, postgrestDelete, postgrestPut, postgrestPost } from '../p
import { getDocumentTypes } from '../document-types/document-types'; import { getDocumentTypes } from '../document-types/document-types';
import { formatDate } from '../../utils'; import { formatDate } from '../../utils';
import { API_BASE_URL } from '~/config/api-config'; import { API_BASE_URL } from '~/config/api-config';
import type { DocumentType } from './files-upload';
/** /**
* 从不同格式的 API 响应中提取数据 * 从不同格式的 API 响应中提取数据
@@ -76,10 +77,10 @@ export interface DocumentUI {
pageCount?: number; pageCount?: number;
ocrResult?: unknown; ocrResult?: unknown;
// 结果统计字段 // 结果统计字段
pass_count: number | null; // 通过数量 pass_count?: number | null; // 通过数量
warning_count: number | null; // 警告数量 warning_count?: number | null; // 警告数量
error_count: number | null; // 错误数量 error_count?: number | null; // 错误数量
manual_count: number | null; // 人工审核数量 manual_count?: number | null; // 人工审核数量
// 消息详情字段 // 消息详情字段
warning_messages?: string[]; // 警告消息列表 warning_messages?: string[]; // 警告消息列表
error_messages?: string[]; // 错误消息列表 error_messages?: string[]; // 错误消息列表
@@ -336,54 +337,98 @@ export async function getDocument(id: string, userId: string, frontendJWT?: stri
* @param id 文档ID * @param id 文档ID
* @returns 文档详情 * @returns 文档详情
*/ */
export async function getDocumentWithNoUserId(id: string, frontendJWT?: string): Promise<{ // export async function getDocumentWithNoUserId(id: string, frontendJWT?: string): Promise<{
data?: DocumentUI; // data?: DocumentUI;
// error?: string;
// status?: number;
// }> {
// try {
// if (!id) {
// return { error: '文档ID不能为空', status: 400 };
// }
// // console.log("get单个文档id", id)
// const response = await postgrestGet<Document[]>(
// '/api/postgrest/proxy/documents',
// {
// filter: {
// 'id': `eq.${id}`,
// },
// limit: 1,
// token: frontendJWT
// }
// );
// if (response.error) {
// return { error: response.error, status: response.status };
// }
// // console.log("respose", response)
// const extractedData = extractApiData<Document[]>(response.data);
// if (!extractedData || extractedData.length === 0) {
// return { error: '文档不存在', status: 404 };
// }
// // console.log('extractedData', extractedData);
// const documentUI = await convertToUIDocument(extractedData[0], frontendJWT);
// return { data: documentUI };
// } catch (error) {
// console.error('获取文档详情失败:', error);
// return {
// error: error instanceof Error ? error.message : '获取文档详情失败',
// status: 500
// };
// }
// }
/**
* 获取文档类型列表(按IDs过滤版本)
* @param ids 文档类型ID数组(必填)
* @param frontendJWT JWT token(可选)
* @returns 文档类型列表
*/
export async function getDocumentTypesByIds(ids: number[], frontendJWT?: string): Promise<{
data?: { types: DocumentType[], total: number };
error?: string; error?: string;
status?: number; status?: number;
}> { }> {
try { try {
if (!id) { if (!ids || ids.length === 0) {
return { error: '文档ID不能为空', status: 400 }; return { data: { types: [], total: 0 } };
} }
// console.log("get单个文档id", id) const response = await postgrestGet<DocumentType[]>(
'/api/postgrest/proxy/document_types',
const response = await postgrestGet<Document[]>(
'/api/postgrest/proxy/documents',
{ {
filter: { filter: {
'id': `eq.${id}`, 'id': `in.(${ids.join(',')})`
}, },
limit: 1,
token: frontendJWT token: frontendJWT
} });
);
if (response.error) { if (response.error) {
return { error: response.error, status: response.status }; return { error: response.error, status: response.status };
} }
// console.log("respose", response) const extractedData = extractApiData<DocumentType[]>(response.data);
const extractedData = extractApiData<Document[]>(response.data); if (!extractedData) {
if (!extractedData || extractedData.length === 0) { return { error: '获取文档类型列表失败', status: 500 };
return { error: '文档不存在', status: 404 };
} }
// console.log('extractedData', extractedData); return { data: { types: extractedData, total: extractedData.length } };
const documentUI = await convertToUIDocument(extractedData[0], frontendJWT);
return { data: documentUI };
} catch (error) { } catch (error) {
console.error('获取文档详情失败:', error); console.error('获取文档类型列表失败:', error);
return { return {
error: error instanceof Error ? error.message : '获取文档详情失败', error: error instanceof Error ? error.message : '获取文档类型列表失败',
status: 500 status: 500
}; };
} }
} }
/** /**
* 更新文档信息 * 更新文档信息
* @param id 文档ID * @param id 文档ID
+3 -2
View File
@@ -58,7 +58,7 @@ export async function getOrganizationTree(includeUsers: boolean = true, jwtToken
if (jwtToken) { if (jwtToken) {
// 如果提供了JWT Token,则使用axios并携带Authorization头 // 如果提供了JWT Token,则使用axios并携带Authorization头
const url = `${API_BASE_URL}/admin/users/organizations?include_users=${includeUsers}`; const url = `${API_BASE_URL}/api/v2/users/organizations?include_users=${includeUsers}`;
const response = await axios.get(url, { const response = await axios.get(url, {
headers: { headers: {
'Authorization': `Bearer ${jwtToken}`, 'Authorization': `Bearer ${jwtToken}`,
@@ -67,7 +67,8 @@ export async function getOrganizationTree(includeUsers: boolean = true, jwtToken
}); });
responseData = response.data; responseData = response.data;
} else { }
else {
// 否则,使用原有的get方法 // 否则,使用原有的get方法
const response = await get<OrganizationResponse>( const response = await get<OrganizationResponse>(
`/admin/users/organizations?include_users=${includeUsers}` `/admin/users/organizations?include_users=${includeUsers}`
+2 -2
View File
@@ -229,7 +229,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
if (routesResult.success && routesResult.data) { if (routesResult.success && routesResult.data) {
// 从菜单数据中提取所有允许的路径 // 从菜单数据中提取所有允许的路径
allowedPaths = extractAllPaths(routesResult.data); allowedPaths = extractAllPaths(routesResult.data);
console.log("🔑 [Root Loader] 用户允许的路由:", allowedPaths); // console.log("🔑 [Root Loader] 用户允许的路由:", allowedPaths);
// ✅ 保存权限映射表 // ✅ 保存权限映射表
if (routesResult.permissionMap) { if (routesResult.permissionMap) {
@@ -238,7 +238,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
} }
// 检查当前路径是否在允许列表中 // 检查当前路径是否在允许列表中
console.log("🔑 [Root Loader] 检查当前路径是否在允许列表中...", pathname, allowedPaths); // console.log("🔑 [Root Loader] 检查当前路径是否在允许列表中...", pathname, allowedPaths);
const isAllowedPath = isPathAllowed(pathname, allowedPaths); const isAllowedPath = isPathAllowed(pathname, allowedPaths);
if (!isAllowedPath) { if (!isAllowedPath) {
+28 -5
View File
@@ -91,8 +91,15 @@ export async function loader({ request }: LoaderFunctionArgs) {
if (!tasksResponse.success) { if (!tasksResponse.success) {
console.error('获取任务列表失败:', tasksResponse.error); console.error('获取任务列表失败:', tasksResponse.error);
return Response.json({ return Response.json({
error: tasksResponse.error || '获取任务列表失败', tasks: [],
status: 500 totalCount: 0,
currentPage: params.page,
pageSize: params.pageSize,
totalPages: 0,
stats: { totalTasks: 0, pendingTasks: 0, inProgressTasks: 0, completedTasks: 0 },
frontendJWT,
documentTypes: [],
error: tasksResponse.error || '获取任务列表失败'
}, { status: 500 }); }, { status: 500 });
} }
@@ -119,8 +126,15 @@ export async function loader({ request }: LoaderFunctionArgs) {
} catch (error) { } catch (error) {
console.error('加载交叉评查任务列表失败:', error); console.error('加载交叉评查任务列表失败:', error);
return Response.json({ return Response.json({
error: error || '加载任务列表失败', tasks: [],
status: 500 totalCount: 0,
currentPage: params.page,
pageSize: params.pageSize,
totalPages: 0,
stats: { totalTasks: 0, pendingTasks: 0, inProgressTasks: 0, completedTasks: 0 },
frontendJWT: undefined,
documentTypes: [],
error: error instanceof Error ? error.message : '加载任务列表失败'
}, { status: 500 }); }, { status: 500 });
} }
} }
@@ -219,7 +233,16 @@ const docTypeConfig = {
export default function CrossCheckingIndex() { export default function CrossCheckingIndex() {
const loaderData = useLoaderData<typeof loader>(); const loaderData = useLoaderData<typeof loader>();
const { tasks, totalCount, currentPage, pageSize, stats, frontendJWT, documentTypes, documentTypesError } = loaderData; const {
tasks = [],
totalCount = 0,
currentPage = 1,
pageSize = 10,
stats = { totalTasks: 0, pendingTasks: 0, inProgressTasks: 0, completedTasks: 0 },
frontendJWT,
documentTypes = [],
documentTypesError
} = loaderData || {};
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const dateFrom = searchParams.get('dateFrom') || ''; const dateFrom = searchParams.get('dateFrom') || '';
const dateTo = searchParams.get('dateTo') || ''; const dateTo = searchParams.get('dateTo') || '';
+1 -1
View File
@@ -642,7 +642,7 @@ export default function CrossCheckingResult() {
console.log('[完成评查] 用户点击确认,开始更新状态'); console.log('[完成评查] 用户点击确认,开始更新状态');
setIsLoading(true); setIsLoading(true);
try { try {
const res = await confirmReviewResults(document.id, jwtToken); const res = await confirmReviewResults(taskId, document.id, jwtToken);
if (res.error) { if (res.error) {
toastService.error(res.error); toastService.error(res.error);
+30 -29
View File
@@ -11,10 +11,9 @@ import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from "~/comp
import { TableRowSkeleton, LoadingIndicator, NumberSkeleton } from '~/components/ui/SkeletonScreen'; import { TableRowSkeleton, LoadingIndicator, NumberSkeleton } from '~/components/ui/SkeletonScreen';
import documentsIndexStyles from "~/styles/pages/documents_index.css?url"; import documentsIndexStyles from "~/styles/pages/documents_index.css?url";
import documentVersionStyles from "~/styles/components/document-version.css?url"; import documentVersionStyles from "~/styles/components/document-version.css?url";
import { deleteDocument, getDocumentsListFromAPI, type DocumentUI, type DocumentVersionUI } from "~/api/files/documents"; import { getDocumentTypesByIds, deleteDocument, getDocumentsListFromAPI, type DocumentUI, type DocumentVersionUI } from "~/api/files/documents";
// import { IssuesDiff } from "~/components/ui/IssuesDiff"; // import { IssuesDiff } from "~/components/ui/IssuesDiff";
import { ResultStats } from "~/components/ui/ResultStats"; import { ResultStats } from "~/components/ui/ResultStats";
import { getDocumentTypes } from "~/api/document-types/document-types";
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files"; import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
import { appendContractAttachments, uploadContractTemplate } from "~/api/files/files-upload"; import { appendContractAttachments, uploadContractTemplate } from "~/api/files/files-upload";
import { toastService } from "~/components/ui/Toast"; import { toastService } from "~/components/ui/Toast";
@@ -50,12 +49,12 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10); const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10);
// 获取文档类型列表(服务端需要显式传递 token,客户端依赖 axios 拦截器) // 获取文档类型列表(服务端需要显式传递 token,客户端依赖 axios 拦截器)
const typesResponse = await getDocumentTypes({ pageSize: 500 }, frontendJWT); // const typesResponse = await getDocumentTypes(frontendJWT);
const documentTypes = typesResponse.data?.types || []; // const documentTypes = typesResponse.data?.types || [];
const documentTypeOptions = documentTypes.map(type => ({ // const documentTypeOptions = documentTypes.map(type => ({
value: type.id, // value: type.id,
label: type.name // label: type.name
})); // }));
// 初始返回空数据,将在客户端根据 sessionStorage 中的 documentTypeIds 加载实际数据 // 初始返回空数据,将在客户端根据 sessionStorage 中的 documentTypeIds 加载实际数据
return Response.json({ return Response.json({
@@ -63,7 +62,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
total: 0, total: 0,
page, page,
pageSize, pageSize,
documentTypeOptions, documentTypeOptions: [],
userInfo, // 传递用户信息到客户端 userInfo, // 传递用户信息到客户端
frontendJWT, // 传递 JWT 到客户端 frontendJWT, // 传递 JWT 到客户端
initialLoad: true // 标记这是初始加载 initialLoad: true // 标记这是初始加载
@@ -190,6 +189,8 @@ export default function DocumentsIndex() {
// 添加页面加载状态管理 // 添加页面加载状态管理
const [isLoadingData, setIsLoadingData] = useState(true); const [isLoadingData, setIsLoadingData] = useState(true);
// 是否已完成初始化(区分"还没开始加载"和"加载完成但没有数据")
const [hasInitialized, setHasInitialized] = useState(false);
const [documents, setDocuments] = useState<DocumentUI[]>([]); const [documents, setDocuments] = useState<DocumentUI[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [filteredDocumentTypeOptions, setFilteredDocumentTypeOptions] = useState(loaderData.documentTypeOptions); const [filteredDocumentTypeOptions, setFilteredDocumentTypeOptions] = useState(loaderData.documentTypeOptions);
@@ -314,10 +315,7 @@ export default function DocumentsIndex() {
setTotal(result.data.total); setTotal(result.data.total);
// 获取经过过滤的文档类型列表 // 获取经过过滤的文档类型列表
const filteredTypesResponse = await getDocumentTypes({ const filteredTypesResponse = await getDocumentTypesByIds(typeIds, jwtToken);
pageSize: 500,
documentTypeIds: typeIds
}, jwtToken);
const filteredDocumentTypes = filteredTypesResponse.data?.types || []; const filteredDocumentTypes = filteredTypesResponse.data?.types || [];
const filteredOptions = filteredDocumentTypes.map(type => ({ const filteredOptions = filteredDocumentTypes.map(type => ({
value: type.id, value: type.id,
@@ -330,6 +328,7 @@ export default function DocumentsIndex() {
toastService.error('获取文档列表失败: ' + (error instanceof Error ? error.message : '未知错误')); toastService.error('获取文档列表失败: ' + (error instanceof Error ? error.message : '未知错误'));
} finally { } finally {
setIsLoadingData(false); setIsLoadingData(false);
setHasInitialized(true); // 标记初始化完成
loadingBarService.hide(); loadingBarService.hide();
} }
}, [search, documentNumber, documentType, auditStatus, fileStatus, dateFrom, dateTo, currentPage, pageSize, loaderData.frontendJWT]); }, [search, documentNumber, documentType, auditStatus, fileStatus, dateFrom, dateTo, currentPage, pageSize, loaderData.frontendJWT]);
@@ -344,14 +343,22 @@ export default function DocumentsIndex() {
console.log('📋 [useEffect] 从 sessionStorage 获取文档类型IDs:', typeIds); console.log('📋 [useEffect] 从 sessionStorage 获取文档类型IDs:', typeIds);
setDocumentTypeIds(typeIds); setDocumentTypeIds(typeIds);
// 加载数据 // 加载数据(fetchData 中会自动获取并设置过滤后的文档类型选项)
fetchData(typeIds); fetchData(typeIds);
} else { } else {
console.warn('⚠️ [useEffect] sessionStorage 中没有 documentTypeIds'); console.warn('⚠️ [useEffect] sessionStorage 中没有 documentTypeIds');
// 没有 documentTypeIds 时,标记初始化完成但无数据
setIsLoadingData(false);
setHasInitialized(true);
loadingBarService.hide();
} }
} }
} catch (error) { } catch (error) {
console.error('❌ [useEffect] 获取 sessionStorage 中的 documentTypeIds 失败:', error); console.error('❌ [useEffect] 获取 sessionStorage 中的 documentTypeIds 失败:', error);
// 出错时也标记初始化完成
setIsLoadingData(false);
setHasInitialized(true);
loadingBarService.hide();
} }
}, [fetchData]); }, [fetchData]);
@@ -394,23 +401,16 @@ export default function DocumentsIndex() {
} }
}, [documents, expandedRows]); }, [documents, expandedRows]);
// 使用并更新缓存数据 // 更新缓存数据并处理 loader 错误
useEffect(() => { useEffect(() => {
// 如果有缓存数据,先显示缓存,再在后台加载新数据 // 设置缓存数据(用于后续可能的优化)
if (dataCache.current) {
setIsLoadingData(false);
} else {
// 显示加载状态 - 确保显示加载条
loadingBarService.show();
setIsLoadingData(true);
}
// 设置缓存数据
dataCache.current = loaderData; dataCache.current = loaderData;
// 处理loader错误 // 处理loader错误
if (loaderData.error) { if (loaderData.error) {
toastService.error(loaderData.error); toastService.error(loaderData.error);
setIsLoadingData(false);
setHasInitialized(true);
} }
}, [loaderData]); }, [loaderData]);
@@ -1490,7 +1490,7 @@ export default function DocumentsIndex() {
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<div className="flex items-center"> <div className="flex items-center">
<h2 className="text-xl font-medium"></h2> <h2 className="text-xl font-medium"></h2>
{isLoadingData ? ( {!hasInitialized || isLoadingData ? (
<div className="ml-4"> <div className="ml-4">
<NumberSkeleton /> <NumberSkeleton />
</div> </div>
@@ -1622,11 +1622,12 @@ export default function DocumentsIndex() {
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
{isLoadingData && documents.length === 0 ? ( {/* 未初始化完成时显示骨架屏,初始化完成后根据数据显示内容或"暂无数据" */}
{!hasInitialized || (isLoadingData && documents.length === 0) ? (
<TableRowSkeleton count={5} /> <TableRowSkeleton count={5} />
) : documents.length === 0 ? ( ) : documents.length === 0 ? (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
{isLoadingData ? "加载中..." : "暂无数据"}
</div> </div>
) : ( ) : (
<table className="w-full border-collapse"> <table className="w-full border-collapse">
+34 -2
View File
@@ -853,11 +853,43 @@ export default function RuleNew() {
setIsLoading(false); setIsLoading(false);
} else if (response.data) { } else if (response.data) {
// 获取新创建或更新的评查点ID // 获取新创建或更新的评查点ID
const savedPointId = response.data.id; let savedPointId: number | undefined;
let successMessage = '';
if (isEditMode) {
// 编辑模式:直接从 response.data.id 获取
savedPointId = response.data.id;
successMessage = '评查点更新成功!';
} else {
// 创建模式:从 items 数组中找到 code 不包含 '--' 后缀的基础评查点
const responseData = response.data as {
success?: boolean;
total_created?: number;
message?: string;
items?: Array<{ id: number; code: string; [key: string]: unknown }>;
};
if (responseData.items && Array.isArray(responseData.items) && responseData.items.length > 0) {
// 查找 code 不包含 '--' 的评查点(基础评查点)
const baseItem = responseData.items.find(item => !item.code.includes('--'));
if (baseItem) {
savedPointId = baseItem.id;
} else {
// 如果所有 code 都包含 '--',取第一个
savedPointId = responseData.items[0].id;
}
// 使用后端返回的消息,或生成默认消息
successMessage = responseData.message || `评查点创建成功! 共创建 ${responseData.total_created || responseData.items.length} 个地区的评查点`;
} else if (response.data.id) {
// 兼容旧格式:直接返回单个评查点
savedPointId = response.data.id;
successMessage = '评查点创建成功!';
}
}
if (savedPointId) { if (savedPointId) {
// 显示成功消息 // 显示成功消息
toastService.success(`评查点${isEditMode ? '更新' : '创建'}成功!`); toastService.success(successMessage);
// 保存成功后跳转到编辑页面并重新加载数据 // 保存成功后跳转到编辑页面并重新加载数据
navigate(`/rules/new?id=${savedPointId}`, { replace: true }); navigate(`/rules/new?id=${savedPointId}`, { replace: true });
@@ -0,0 +1,326 @@
# 交叉评查接口对接状态报告
> 本文档对比 `auth_doc/交叉评查接口文档(1).md` 中定义的10个接口与前端代码实际调用的接口情况。
>
> 生成时间:2025-12-11
---
## 一、接口总览对比
### 文档定义的10个接口
| 序号 | 方法 | 文档路径 | 接口名称 |
|------|------|----------|----------|
| 1 | `POST` | `/api/v2/cross_review/proposals` | 发起评分提案 |
| 2 | `POST` | `/api/v2/cross_review/proposals/{proposal_id}/votes` | 对提案投票 |
| 3 | `DELETE` | `/api/v2/cross_review/proposals/{proposal_id}` | 撤销评分提案 |
| 4 | `POST` | `/api/v2/cross_review/proposals/details` | 获取提案列表及详情 |
| 5 | `POST` | `/api/v2/cross_review/proposals/document` | 获取指定文档的提案列表 |
| 6 | `POST` | `/api/v2/cross_review/proposals/document/check_pending_votes` | 检查未投票用户 |
| 7 | `POST` | `/api/v2/cross_review/tasks/user_tasks` | 获取用户参与的任务列表 |
| 8 | `GET` | `/api/v2/cross_review/tasks/{task_id}/progress` | 获取评查任务进度 |
| 9 | `POST` | `/api/v2/cross_review/tasks/{task_id}/documents` | 获取任务下文档列表 |
| 10 | `POST` | `/api/v2/cross_review/tasks/{task_id}/documents/{document_id}/complete` | 确认完成文档评查 |
---
## 二、前端代码调用的接口清单
### 涉及文件
1. `app/api/cross-checking/cross-file-result.ts` - 提案/意见相关操作
2. `app/api/cross-checking/cross-files.ts` - 任务列表相关操作
3. `app/api/cross-checking/cross-files-upload.ts` - 文件上传相关操作
---
## 三、逐一对比分析
### ✅ 接口 1:发起评分提案
| 项目 | 文档定义 | 前端实现 | 状态 |
|------|----------|----------|------|
| **方法** | `POST` | `POST` | ✅ 一致 |
| **路径** | `/api/v2/cross_review/proposals` | `/admin/cross_review/proposals` | ⚠️ **差异** |
| **文件** | - | `cross-file-result.ts:139` | - |
| **函数** | - | `submitCrossCheckingOpinion()` | - |
**差异说明**
- 前端使用 `/admin/cross_review/proposals`,缺少 `/v2` 版本号
- 文档推荐使用 `/api/v2/cross_review/proposals`
**请求参数对比**
| 参数 | 文档要求 | 前端发送 | 状态 |
|------|----------|----------|------|
| `document_id` | int, 必填 | ✅ 发送 | ✅ |
| `evaluation_point_id` | int, 必填 | ✅ 发送 | ✅ |
| `proposed_score` | float, 必填 | ✅ 发送 | ✅ |
| `reason` | string, 必填 | ✅ 发送 | ✅ |
| `proposer_id` | int, 必填 | ✅ 发送 | ✅ |
| `evaluation_result_id` | int, 必填 | ✅ 发送 | ✅ |
---
### ✅ 接口 2:对提案投票
| 项目 | 文档定义 | 前端实现 | 状态 |
|------|----------|----------|------|
| **方法** | `POST` | `POST` | ✅ 一致 |
| **路径** | `/api/v2/cross_review/proposals/{proposal_id}/votes` | `/admin/cross_review/proposals/{opinionId}/votes` | ⚠️ **差异** |
| **文件** | - | `cross-file-result.ts:321-333` | - |
| **函数** | - | `performOpinionAction()` (agree/disagree/withdraw_vote) | - |
**差异说明**
- 前端使用 `/admin/cross_review/proposals/{id}/votes`,缺少 `/v2` 版本号
**请求参数对比**
| 参数 | 文档要求 | 前端发送 | 状态 |
|------|----------|----------|------|
| `vote_type` | string (agree/disagree/cancel), 必填 | ✅ 发送 | ✅ |
| `voter_id` | int, 必填 | ✅ 发送 | ✅ |
---
### ✅ 接口 3:撤销评分提案
| 项目 | 文档定义 | 前端实现 | 状态 |
|------|----------|----------|------|
| **方法** | `DELETE` | `DELETE` | ✅ 一致 |
| **路径** | `/api/v2/cross_review/proposals/{proposal_id}` | `/admin/cross_review/proposals/{opinionId}` | ⚠️ **差异** |
| **文件** | - | `cross-file-result.ts:338` | - |
| **函数** | - | `performOpinionAction()` (withdraw_opinion) | - |
**差异说明**
- 前端使用 `/admin/cross_review/proposals/{id}`,缺少 `/v2` 版本号
---
### ❌ 接口 4:获取提案列表及详情
| 项目 | 文档定义 | 前端实现 | 状态 |
|------|----------|----------|------|
| **方法** | `POST` | - | ❌ **未实现** |
| **路径** | `/api/v2/cross_review/proposals/details` | - | - |
| **说明** | 获取当前用户需要处理的所有待投票提案列表 | - | - |
**备注**:此接口用于获取用户需要投票的待处理提案,前端目前未调用此接口。
---
### ✅ 接口 5:获取指定文档的提案列表
| 项目 | 文档定义 | 前端实现 | 状态 |
|------|----------|----------|------|
| **方法** | `POST` | `POST` | ✅ 一致 |
| **路径** | `/api/v2/cross_review/proposals/document` | `/admin/cross_review/proposals/document` | ⚠️ **差异** |
| **文件** | - | `cross-file-result.ts:199` | - |
| **函数** | - | `getCrossCheckingOpinions()` | - |
**差异说明**
- 前端使用 `/admin/cross_review/proposals/document`,缺少 `/v2` 版本号
**请求参数对比**
| 参数 | 文档要求 | 前端发送 | 状态 |
|------|----------|----------|------|
| `document_id` | int, 必填 | ✅ 发送 | ✅ |
| `page` | int, 选填, 默认1 | ✅ 发送 | ✅ |
| `page_size` | int, 选填, 默认10 | ✅ 发送 | ✅ |
| `user_id` | - (文档未要求) | ⚠️ 发送 | ⚠️ 多余参数 |
---
### ✅ 接口 6:检查未投票用户
| 项目 | 文档定义 | 前端实现 | 状态 |
|------|----------|----------|------|
| **方法** | `POST` | `POST` | ✅ 一致 |
| **路径** | `/api/v2/cross_review/proposals/document/check_pending_votes` | `/admin/cross_review/proposals/document/check_pending_votes` | ⚠️ **差异** |
| **文件** | - | `cross-file-result.ts:474` | - |
| **函数** | - | `checkProposalVotes()` | - |
**差异说明**
- 前端使用 `/admin/cross_review/proposals/document/check_pending_votes`,缺少 `/v2` 版本号
**请求参数对比**
| 参数 | 文档要求 | 前端发送 | 状态 |
|------|----------|----------|------|
| `document_id` | int, 必填 | ✅ 发送 | ✅ |
---
### ✅ 接口 7:获取用户参与的任务列表
| 项目 | 文档定义 | 前端实现 | 状态 |
|------|----------|----------|------|
| **方法** | `POST` | `POST` | ✅ 一致 |
| **路径** | `/api/v2/cross_review/tasks/user_tasks` | `/admin/v2/cross_review/tasks/user_tasks` | ✅ **正确** |
| **文件** | - | `cross-files.ts:406` | - |
| **函数** | - | `getUserTaskDocuments()` | - |
**说明**:此接口已正确使用 `/v2` 版本号路径。
**请求参数对比**
| 参数 | 文档要求 | 前端发送 | 状态 |
|------|----------|----------|------|
| `page` | int, 选填, 默认1 | ✅ 发送 | ✅ |
| `page_size` | int, 选填, 默认10 | ✅ 发送 | ✅ |
---
### ❌ 接口 8:获取评查任务进度
| 项目 | 文档定义 | 前端实现 | 状态 |
|------|----------|----------|------|
| **方法** | `GET` | - | ❌ **未实现** |
| **路径** | `/api/v2/cross_review/tasks/{task_id}/progress` | - | - |
| **说明** | 根据任务ID获取评查进度详情 | - | - |
**备注**:前端目前通过 `getUserTaskDocuments` 接口返回的 `progress` 字段获取进度,未单独调用此接口。
---
### ✅ 接口 9:获取任务下文档列表
| 项目 | 文档定义 | 前端实现 | 状态 |
|------|----------|----------|------|
| **方法** | `POST` | `POST` | ✅ 一致 |
| **路径** | `/api/v2/cross_review/tasks/{task_id}/documents` | `/admin/v2/cross_review/tasks/{taskId}/documents` | ✅ **正确** |
| **文件** | - | `cross-files.ts:448` | - |
| **函数** | - | `getTaskDocuments()` | - |
**说明**:此接口已正确使用 `/v2` 版本号路径。
**请求参数对比**
| 参数 | 文档要求 | 前端发送 | 状态 |
|------|----------|----------|------|
| `page` | int, 选填, 默认1 | ✅ 发送 | ✅ |
| `page_size` | int, 选填, 默认10 | ✅ 发送 | ✅ |
| `file_type_ids` | array[int], 选填 | ❌ 未发送 | ⚠️ 未使用 |
| `date_from` | string, 选填 | ❌ 未发送 | ⚠️ 未使用 |
| `date_to` | string, 选填 | ❌ 未发送 | ⚠️ 未使用 |
| `keyword` | string, 选填 | ❌ 未发送 | ⚠️ 未使用 |
| `order` | string, 选填 | ❌ 未发送 | ⚠️ 未使用 |
---
### ✅ 接口 10:确认完成文档评查
| 项目 | 文档定义 | 前端实现 | 状态 |
|------|----------|----------|------|
| **方法** | `POST` | `POST` | ✅ 一致 |
| **路径** | `/api/v2/cross_review/tasks/{task_id}/documents/{document_id}/complete` | `/admin/v2/cross_review/tasks/{task_id}/documents/{document_id}/complete` | ✅ **正确** |
| **文件** | - | `cross-file-result.ts:417` | - |
| **函数** | - | `confirmReviewResults(taskId, documentId, jwtToken)` | - |
**说明**:此接口已正确使用 `/v2` 版本号路径,并包含 `task_id``document_id` 参数。
**已于 2025-12-11 修复完成。**
---
## 四、其他前端调用但文档未定义的接口
### 1. 创建交叉评查任务 (upload相关)
| 项目 | 前端实现 |
|------|----------|
| **方法** | `POST` |
| **路径** | `/admin/cross_review/tasks/assign` |
| **文件** | `cross-files-upload.ts:316` |
| **函数** | `createCrossReviewTask()` |
**说明**:此接口用于创建交叉评查任务并分配用户,在本文档10个接口中未定义。
### 2. 上传并分配文件
| 项目 | 前端实现 |
|------|----------|
| **方法** | `POST` |
| **路径** | `${UPLOAD_URL}/cross_review/documents/upload_and_assign` |
| **文件** | `cross-files-upload.ts:233` |
| **函数** | `batchUploadAndAssignCrossCheckingFiles()` |
**说明**:此接口用于批量上传文件并分配交叉评查任务,在本文档10个接口中未定义。
### 3. PostgREST 直接查询
| 项目 | 前端实现 |
|------|----------|
| **路径** | `/api/postgrest/proxy/cross_examination_tasks` |
| **文件** | `cross-file-result.ts:93` |
| **函数** | `findIsProposer()` |
**说明**:直接通过 PostgREST 查询数据库表,未走统一的 API 接口。
### 4. PostgREST 直接查询文档类型
| 项目 | 前端实现 |
|------|----------|
| **路径** | `/api/postgrest/proxy/document_types` |
| **文件** | `cross-files.ts:529` |
| **函数** | `getCrossCheckingDocumentTypes()` |
**说明**:直接通过 PostgREST 查询数据库表获取文档类型。
---
## 五、总结
### 对接状态统计
| 状态 | 数量 | 百分比 |
|------|------|--------|
| ✅ 已正确对接 | 3 | 30% |
| ⚠️ 路径差异(缺少v2 | 5 | 50% |
| ❌ 未实现 | 2 | 20% |
### 需要修复的问题
#### 🟡 中优先级(建议修复)
1. **统一使用 `/api/v2` 或 `/admin/v2` 前缀**
- 接口 1、2、3、5、6 使用了旧路径 `/admin/cross_review/...`
- 建议统一改为 `/admin/v2/cross_review/...``/api/v2/cross_review/...`
#### 🟢 低优先级(可选实现)
3. **接口4:获取提案列表及详情**
- 当前未实现,如需要在其他页面展示待投票提案列表可实现
4. **接口8:获取评查任务进度**
- 当前通过任务列表接口获取进度,如需单独获取可实现
5. **接口9:增加筛选参数支持**
- `getTaskDocuments()` 未支持 `file_type_ids``date_from``date_to``keyword``order` 等筛选参数
---
## 六、修复建议代码示例
### 统一路径前缀
建议在 `api-config.ts` 中定义:
```typescript
// api-config.ts
export const CROSS_REVIEW_API_PREFIX = '/admin/v2/cross_review';
```
然后在各接口中使用:
```typescript
// cross-file-result.ts
import { API_BASE_URL, CROSS_REVIEW_API_PREFIX } from '../../config/api-config';
// 使用示例
const response = await axios.post(
`${API_BASE_URL}${CROSS_REVIEW_API_PREFIX}/proposals`,
requestData,
{ headers: { ... } }
);
```
---
*报告生成完毕*
File diff suppressed because it is too large Load Diff
+741
View File
@@ -0,0 +1,741 @@
# 评查审核接口对接文档
> 版本:v1.0
> 更新时间:2024-12-10
> 模块:评查审核(Evaluation Audit
---
## 一、接口总览
| 序号 | 接口名称 | 方法 | 路径 | 功能说明 |
| ---- | ------------ | --------- | ----------------------------------------------- | ---------------------------- |
| 1 | 更新评查结果 | `PATCH` | `/admin/v2/evaluation/results/{result_id}` | 修改评查结果的通过状态和说明 |
| 2 | 创建审核状态 | `POST` | `/admin/v2/evaluation/audit-status` | 记录人工审核操作 |
| 3 | 更新审核状态 | `PATCH` | `/admin/v2/evaluation/audit-status/{id}` | 更新已有的审核状态记录 |
| 4 | 确认文档审核 | `PATCH` | `/admin/v2/evaluation/documents/{id}/confirm` | 确认文档整体审核完成 |
---
## 二、通用说明
### 2.1 请求头
所有接口都需要携带 JWT Token:
```http
Authorization: Bearer <your_jwt_token>
Content-Type: application/json
```
### 2.2 基础路径
```
开发环境: http://localhost:8000
生产环境: https://your-domain.com
```
### 2.3 通用响应格式
**成功响应**
```json
{
"success": true,
"message": "操作成功",
"data": { ... }
}
```
**错误响应**
```json
{
"detail": "错误信息"
}
```
### 2.4 HTTP 状态码
| 状态码 | 说明 |
| ------ | -------------- |
| 200 | 请求成功 |
| 400 | 请求参数错误 |
| 403 | 无权限访问 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
---
## 三、接口详细说明
---
### 3.1 更新评查结果
**功能说明**:人工审核时,修改某个评查点的评查结果(通过/不通过)和说明信息。
#### 请求信息
| 项目 | 说明 |
| -------- | --------------------------------------------- |
| 请求路径 | `/admin/v2/evaluations/results/{result_id}` |
| 请求方法 | `PATCH` |
| 权限要求 | 用户必须对该文档有访问权限 |
#### 路径参数
| 参数名 | 类型 | 必填 | 说明 |
| --------- | ------- | ---- | ----------------------------------------- |
| result_id | integer | 是 | 评查结果IDevaluation_results 表的主键) |
#### 请求体参数
| 参数名 | 类型 | 必填 | 说明 |
| ----------- | ------ | ---- | ----------------------------------------- |
| result | string | 否 | 评查结果,可选值:`"pass"` / `"fail"` |
| message | string | 否 | 评查结果说明(人工审核备注) |
| final_score | float | 否 | 最终分数(人工修正后的分数) |
#### 请求示例
```http
PATCH /admin/v2/evaluation/results/123
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"result": "pass",
"message": "",
"final_score": 100.0
}
```
#### 响应示例
**成功响应** (200)
```json
{
"success": true,
"message": "评查结果更新成功",
"data": {
"result_id": 123,
"updated_fields": ["evaluated_results", "final_score"]
}
}
```
**错误响应** (404)
```json
{
"detail": "评查结果 123 不存在"
}
```
**错误响应** (403)
```json
{
"detail": "无权修改此评查结果"
}
```
#### 业务逻辑
```
1. 接收请求,验证 JWT Token
2. 根据 result_id 查询 evaluation_results 表
3. 获取该评查结果关联的 document_id
4. 验证当前用户是否有权限访问该文档:
- 是文档上传者
- 或是交叉评查任务的参与者
5. 更新 evaluation_results 表:
- evaluated_results.result = 请求的 result
- evaluated_results.message = 请求的 message
- final_score = 请求的 final_score
6. 返回更新结果
```
#### 数据库变更
更新 `evaluation_results` 表:
- `evaluated_results` (JSONB): 更新其中的 `result``message` 字段
- `final_score` (float): 更新最终分数
---
### 3.2 创建审核状态
**功能说明**:记录用户对某个评查点的人工审核操作,用于追踪审核历史。
#### 请求信息
| 项目 | 说明 |
| -------- | -------------------------------------- |
| 请求路径 | `/admin/v2/evaluations/audit-status` |
| 请求方法 | `POST` |
| 权限要求 | 用户必须对该文档有访问权限 |
#### 请求体参数
| 参数名 | 类型 | 必填 | 说明 |
| -------------------- | ------- | ---- | ------------------------------------ |
| document_id | integer | 是 | 文档ID |
| evaluation_point_id | integer | 是 | 评查点ID |
| evaluation_result_id | integer | 是 | 评查结果ID |
| edit_audit_status | integer | 是 | 审核状态:`0`=未审核,`1`=已审核 |
| message | string | 否 | 操作记录文本(最大255字符) |
#### 请求示例
```http
POST /admin/v2/evaluation/audit-status
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"document_id": 456,
"evaluation_point_id": 789,
"evaluation_result_id": 123,
"edit_audit_status": 1,
"message": ""
}
```
#### 响应示例
**成功响应** (200)
```json
{
"id": 1,
"user_id": 10,
"document_id": 456,
"evaluation_point_id": 789,
"evaluation_result_id": 123,
"edit_audit_status": 1,
"message": "已完成人工审核,确认通过",
"created_at": "2024-12-10T10:30:00+08:00",
"updated_at": "2024-12-10T10:30:00+08:00"
}
```
**错误响应** (403)
```json
{
"detail": "无权操作此文档的审核状态"
}
```
#### 业务逻辑
```
1. 接收请求,验证 JWT Token
2. 验证当前用户是否有权限访问 document_id 对应的文档
3. 在 audit_status 表中插入新记录:
- user_id = 当前登录用户ID(自动填充)
- document_id = 请求的 document_id
- evaluation_point_id = 请求的 evaluation_point_id
- evaluation_result_id = 请求的 evaluation_result_id
- edit_audit_status = 请求的 edit_audit_status
- message = 请求的 message
4. 返回创建的记录
```
#### 数据库变更
`audit_status` 表中插入新记录
#### 前端使用场景
当用户点击"通过"或"不通过"按钮时:
1. 先调用 **3.1 更新评查结果** 更新结果
2. 再调用 **3.2 创建审核状态** 记录操作
---
### 3.3 更新审核状态
**功能说明**:更新已有的审核状态记录,用于"重新审核"等场景。
#### 请求信息
| 项目 | 说明 |
| -------- | -------------------------------------------------------- |
| 请求路径 | `/admin/v2/evaluations/audit-status/{audit_status_id}` |
| 请求方法 | `PATCH` |
| 权限要求 | 用户必须是该记录的创建者,或对关联文档有访问权限 |
#### 路径参数
| 参数名 | 类型 | 必填 | 说明 |
| --------------- | ------- | ---- | ----------------------------------- |
| audit_status_id | integer | 是 | 审核状态IDaudit_status 表的主键) |
#### 请求体参数
| 参数名 | 类型 | 必填 | 说明 |
| ----------------- | ------- | ---- | ------------------------------------ |
| edit_audit_status | integer | 否 | 审核状态:`0`=未审核,`1`=已审核 |
| message | string | 否 | 操作记录文本(最大255字符) |
#### 请求示例
```http
PATCH /admin/v2/evaluation/audit-status/1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"edit_audit_status": 1,
"message": ""
}
```
#### 响应示例
**成功响应** (200)
```json
{
"success": true,
"message": "审核状态更新成功",
"data": {
"audit_status_id": 1,
"updated_fields": ["edit_audit_status", "message"]
}
}
```
**错误响应** (404)
```json
{
"detail": "审核状态记录 1 不存在"
}
```
#### 业务逻辑
```
1. 接收请求,验证 JWT Token
2. 根据 audit_status_id 查询 audit_status 表
3. 验证权限:
- 如果是自己创建的记录,允许更新
- 如果不是自己的记录,检查是否有关联文档的访问权限
4. 更新 audit_status 表对应记录
5. 返回更新结果
```
#### 数据库变更
更新 `audit_status` 表:
- `edit_audit_status`: 审核状态
- `message`: 操作记录
- `updated_at`: 自动更新(数据库触发器)
---
### 3.4 确认文档审核完成
**功能说明**:确认整个文档的审核完成,更新文档的审核状态字段。
#### 请求信息
| 项目 | 说明 |
| -------- | --------------------------------------------------------- |
| 请求路径 | `/admin/v2/evaluations/documents/{document_id}/confirm` |
| 请求方法 | `PATCH` |
| 权限要求 | 用户必须对该文档有访问权限 |
#### 路径参数
| 参数名 | 类型 | 必填 | 说明 |
| ----------- | ------- | ---- | ------ |
| document_id | integer | 是 | 文档ID |
#### 请求体参数
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| ------------ | ------- | ---- | ------ | ------------------------ |
| audit_status | integer | 否 | 1 | 审核状态:`1`=审核完成 |
#### 请求示例
```http
PATCH /admin/v2/evaluation/documents/456/confirm
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"audit_status": 1
}
```
或者使用默认值(不传请求体):
```http
PATCH /admin/v2/evaluation/documents/456/confirm
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{}
```
#### 响应示例
**成功响应** (200)
```json
{
"success": true,
"message": "文档审核确认成功",
"data": {
"document_id": 456,
"audit_status": 1
}
}
```
**错误响应** (403)
```json
{
"detail": "无权确认此文档的审核状态"
}
```
#### 业务逻辑
```
1. 接收请求,验证 JWT Token
2. 验证当前用户是否有权限访问 document_id 对应的文档
3. 更新 documents 表的 audit_status 字段
4. 返回更新结果
```
#### 数据库变更
更新 `documents` 表:
- `audit_status`: 设置为 1(审核完成)
---
## 四、完整业务流程
### 4.1 人工审核单个评查点
```mermaid
sequenceDiagram
participant 前端
participant 后端
participant 数据库
前端->>后端: PATCH /results/{id} (修改评查结果)
后端->>数据库: 验证权限
后端->>数据库: 更新 evaluation_results
后端-->>前端: 返回成功
前端->>后端: POST /audit-status (记录审核操作)
后端->>数据库: 插入 audit_status
后端-->>前端: 返回创建的记录
```
### 4.2 确认文档整体审核完成
```mermaid
sequenceDiagram
participant 前端
participant 后端
participant 数据库
Note over 前端: 用户完成所有评查点审核后
前端->>后端: PATCH /documents/{id}/confirm
后端->>数据库: 验证权限
后端->>数据库: 更新 documents.audit_status = 1
后端-->>前端: 返回成功
```
### 4.3 前端调用示例(TypeScript/Axios
```typescript
import axios from 'axios';
const API_BASE = '/admin/v2/evaluation';
// 获取 JWT Token
const getToken = () => localStorage.getItem('token');
// 创建 axios 实例
const api = axios.create({
headers: {
'Content-Type': 'application/json',
},
});
// 添加请求拦截器
api.interceptors.request.use((config) => {
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
/**
* 更新评查结果
* @param resultId 评查结果ID
* @param data 更新数据
*/
export async function updateEvaluationResult(
resultId: number,
data: {
result?: 'pass' | 'fail';
message?: string;
final_score?: number;
}
) {
const response = await api.patch(`${API_BASE}/results/${resultId}`, data);
return response.data;
}
/**
* 创建审核状态记录
* @param data 审核状态数据
*/
export async function createAuditStatus(data: {
document_id: number;
evaluation_point_id: number;
evaluation_result_id: number;
edit_audit_status: 0 | 1;
message?: string;
}) {
const response = await api.post(`${API_BASE}/audit-status`, data);
return response.data;
}
/**
* 更新审核状态记录
* @param auditStatusId 审核状态ID
* @param data 更新数据
*/
export async function updateAuditStatus(
auditStatusId: number,
data: {
edit_audit_status?: 0 | 1;
message?: string;
}
) {
const response = await api.patch(`${API_BASE}/audit-status/${auditStatusId}`, data);
return response.data;
}
/**
* 确认文档审核完成
* @param documentId 文档ID
*/
export async function confirmDocumentReview(documentId: number) {
const response = await api.patch(`${API_BASE}/documents/${documentId}/confirm`, {
audit_status: 1,
});
return response.data;
}
// ============================================
// 业务场景示例
// ============================================
/**
* 场景1:用户点击"通过"按钮
*/
async function handlePassClick(
documentId: number,
evaluationPointId: number,
evaluationResultId: number
) {
try {
// 1. 更新评查结果为"通过"
await updateEvaluationResult(evaluationResultId, {
result: 'pass',
message: '人工审核通过',
final_score: 100,
});
// 2. 记录审核操作
await createAuditStatus({
document_id: documentId,
evaluation_point_id: evaluationPointId,
evaluation_result_id: evaluationResultId,
edit_audit_status: 1,
message: '用户确认通过',
});
console.log('审核成功');
} catch (error) {
console.error('审核失败:', error);
}
}
/**
* 场景2:用户点击"不通过"按钮
*/
async function handleFailClick(
documentId: number,
evaluationPointId: number,
evaluationResultId: number,
reason: string
) {
try {
// 1. 更新评查结果为"不通过"
await updateEvaluationResult(evaluationResultId, {
result: 'fail',
message: reason,
final_score: 0,
});
// 2. 记录审核操作
await createAuditStatus({
document_id: documentId,
evaluation_point_id: evaluationPointId,
evaluation_result_id: evaluationResultId,
edit_audit_status: 1,
message: `用户确认不通过: ${reason}`,
});
console.log('审核成功');
} catch (error) {
console.error('审核失败:', error);
}
}
/**
* 场景3:用户完成所有评查点审核,点击"确认完成"
*/
async function handleConfirmAllClick(documentId: number) {
try {
await confirmDocumentReview(documentId);
console.log('文档审核确认成功');
} catch (error) {
console.error('确认失败:', error);
}
}
```
---
## 五、数据库表结构参考
### 5.1 evaluation_results 表
| 字段名 | 类型 | 说明 |
| --------------------------- | --------------- | -------------------------------------------- |
| id | integer | 主键 |
| document_id | integer | 文档ID |
| evaluation_point_id | integer | 评查点ID |
| status | varchar(20) | 状态 |
| extracted_results | jsonb | 抽取结果 |
| rules_results | jsonb | 规则判断结果 |
| **evaluated_results** | **jsonb** | **评查结果(包含 result 和 message** |
| **final_score** | **float** | **最终分数** |
| machine_score | float | 机器评查分数 |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
**evaluated_results 字段结构**
```json
{
"result": "pass", // 评查结果:pass/fail
"message": "说明信息" // 评查结果说明
}
```
### 5.2 audit_status 表
| 字段名 | 类型 | 说明 |
| --------------------------- | ---------------------- | ---------------------------------- |
| id | integer | 主键 |
| user_id | integer | 操作用户ID |
| document_id | integer | 文档ID |
| evaluation_point_id | integer | 评查点ID |
| evaluation_result_id | integer | 评查结果ID |
| **edit_audit_status** | **integer** | **审核状态(0=未审核,1=已审核)** |
| **message** | **varchar(255)** | **操作记录文本** |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
### 5.3 documents 表(相关字段)
| 字段名 | 类型 | 说明 |
| ---------------------- | ----------------- | ---------------------------------- |
| id | integer | 主键 |
| **audit_status** | **integer** | **审核状态(0=待审核,1=已完成)** |
| ... | ... | 其他字段省略 |
---
## 六、错误处理
### 6.1 常见错误码
| HTTP 状态码 | detail 示例 | 原因 | 解决方案 |
| ----------- | ----------------------- | ------------------ | ---------------------------------------- |
| 403 | "无权修改此评查结果" | 用户无权访问该文档 | 检查用户是否是文档上传者或交叉评查参与者 |
| 404 | "评查结果 123 不存在" | 评查结果ID不存在 | 检查传入的 result_id 是否正确 |
| 404 | "审核状态记录 1 不存在" | 审核状态ID不存在 | 检查传入的 audit_status_id 是否正确 |
| 422 | "field required" | 缺少必填字段 | 检查请求体是否包含所有必填字段 |
| 500 | "更新评查结果失败: ..." | 服务器内部错误 | 查看服务器日志排查问题 |
### 6.2 前端错误处理示例
```typescript
try {
await updateEvaluationResult(resultId, data);
} catch (error) {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
const detail = error.response?.data?.detail;
switch (status) {
case 403:
alert('您没有权限执行此操作');
break;
case 404:
alert('记录不存在,请刷新页面后重试');
break;
case 422:
alert('请求参数错误,请检查输入');
break;
default:
alert(`操作失败: ${detail || '未知错误'}`);
}
}
}
```
---
## 七、注意事项
1. **权限验证**:所有接口都会验证用户对文档的访问权限,前端无需额外处理权限逻辑
2. **字段更新**:PATCH 接口只更新传入的字段,未传入的字段保持不变
3. **操作顺序**:建议先调用"更新评查结果",再调用"创建审核状态"
4. **审核状态值**
- `edit_audit_status = 0`:未审核(按钮显示"通过/不通过"
- `edit_audit_status = 1`:已审核(按钮显示"重新审核")
5. **文档 audit_status**
- `audit_status = 0`:待人工确认
- `audit_status = 1`:审核完成
---
## 八、联系方式
如有问题,请联系后端开发人员。
@@ -0,0 +1,742 @@
# 通用权限前端对接文档
## 概述
系统中存在**通用权限**,即同一个权限被多个页面共享使用。例如「查看审计状态」权限同时被「文档评查结果详情」和「交叉评查-评查结果」两个页面使用。
本文档详细说明前端如何查询、展示和管理这些通用权限。
---
## ⚠️ 重要:必须修改的查询方式
### 问题
当前前端查询某个路由下的权限可能使用:
```javascript
// ❌ 错误方式:只能查到独立权限,查不到通用权限
const permissions = await fetch(`/api/postgrest/proxy/permissions?route_id=eq.${routeId}`);
```
这种查询方式**无法获取通用权限**,因为通用权限的 `route_id``NULL`
### 解决方案
**必须修改为以下查询方式**
```javascript
// ✅ 正确方式:同时查询独立权限 + 通用权限
const permissions = await fetch(
`/api/postgrest/proxy/permissions?or=(route_id.eq.${routeId},related_routes.cs.{${routeId}})`
);
```
### 查询参数说明
| 参数 | 说明 |
|------|------|
| `route_id.eq.${routeId}` | 查询独立权限(route_id = 指定值) |
| `related_routes.cs.{${routeId}}` | 查询通用权限(related_routes 数组包含指定值) |
| `or=(...)` | 两个条件取并集 |
| `cs` | PostgREST 的 contains 操作符,用于数组包含查询 |
### 修改前后对比
以交叉评查页面 (route_id=37) 为例:
| 查询方式 | 返回权限数量 | 说明 |
|---------|-------------|------|
| `?route_id=eq.37` | 6个 | ❌ 只有独立权限 |
| `?or=(route_id.eq.37,related_routes.cs.{37})` | 11个 | ✅ 独立权限 + 通用权限 |
### 交叉评查页面应显示的完整权限列表
| 类型 | 权限名称 | permission_key |
|------|---------|----------------|
| **通用** | 查看审计状态 | evaluation:audit_status:view |
| **通用** | 更新审计状态 | evaluation:audit_status:update |
| **通用** | 创建审核状态 | evaluation:audit_status:create |
| **通用** | 更新评查结果 | evaluation:result:update |
| **通用** | 确认文档审核完成 | evaluation:document:confirm |
| 独立 | 查看评分提案 | cross_review:proposal:read |
| 独立 | 创建评分提案 | cross_review:proposal:create |
| 独立 | 对提案投票 | cross_review:proposal:vote |
| 独立 | 删除评分提案 | cross_review:proposal:delete |
| 独立 | 标记文档完成 | cross_review:document:complete |
| 独立 | 查看任务进度 | cross_review:progress:view |
---
## 数据库结构
### permissions 表关键字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | INTEGER | 权限ID(主键) |
| `permission_key` | VARCHAR(100) | 权限唯一标识,如 `evaluation:audit_status:view` |
| `display_name` | VARCHAR(200) | 权限显示名称 |
| `route_id` | INTEGER | 关联的路由ID(独立权限使用) |
| `related_routes` | INTEGER[] | **关联的多个路由ID(通用权限使用)** |
### 权限类型区分
| 类型 | route_id | related_routes | 说明 |
|------|----------|----------------|------|
| **独立权限** | 有值(如58) | NULL | 只属于单个页面 |
| **通用权限** | NULL | 有值(如{58,37} | 被多个页面共享 |
---
## 当前通用权限数据
```sql
SELECT id, permission_key, route_id, related_routes, display_name
FROM permissions
WHERE related_routes IS NOT NULL;
```
| id | permission_key | route_id | related_routes | display_name |
|----|----------------|----------|----------------|--------------|
| 88 | evaluation:audit_status:view | NULL | {58, 37} | 查看审计状态 |
| 89 | evaluation:audit_status:update | NULL | {58, 37} | 更新审计状态 |
| 136 | evaluation:result:update | NULL | {58, 37} | 更新评查结果 |
| 137 | evaluation:audit_status:create | NULL | {58, 37} | 创建审核状态 |
| 138 | evaluation:document:confirm | NULL | {58, 37} | 确认文档审核完成 |
### 关联的路由
| route_id | route_name | route_title | route_path |
|----------|------------|-------------|------------|
| 58 | Reviews | 文档评查结果详情 | /reviews |
| 37 | CrossCheckingResult | 评查结果 | /cross-checking/result |
---
## 查询某个页面的所有权限
### SQL 查询语句
```sql
-- 查询 route_id=58 (文档评查结果详情) 的所有权限
SELECT
id,
permission_key,
display_name,
route_id,
related_routes,
CASE
WHEN related_routes IS NOT NULL THEN true
ELSE false
END AS is_shared
FROM permissions
WHERE route_id = 58 OR 58 = ANY(related_routes)
ORDER BY is_shared, permission_key;
```
### PostgREST 查询方式
```http
GET /api/postgrest/proxy/permissions?or=(route_id.eq.58,related_routes.cs.{58})&select=id,permission_key,display_name,route_id,related_routes
```
**参数说明**
- `route_id.eq.58` - 独立权限(route_id = 58
- `related_routes.cs.{58}` - 通用权限(related_routes 包含 58
- `cs` = containsPostgreSQL 数组包含操作符
### 返回示例
```json
[
// 独立权限
{
"id": 82,
"permission_key": "evaluation_point:result:view",
"display_name": "查看评查结果",
"route_id": 58,
"related_routes": null
},
{
"id": 83,
"permission_key": "evaluation_point:result:update",
"display_name": "更新评查结果",
"route_id": 58,
"related_routes": null
},
{
"id": 48,
"permission_key": "review_point:detail:read",
"display_name": "查看评查详情",
"route_id": 58,
"related_routes": null
},
// 通用权限
{
"id": 88,
"permission_key": "evaluation:audit_status:view",
"display_name": "查看审计状态",
"route_id": null,
"related_routes": [58, 37]
},
{
"id": 89,
"permission_key": "evaluation:audit_status:update",
"display_name": "更新审计状态",
"route_id": null,
"related_routes": [58, 37]
},
{
"id": 136,
"permission_key": "evaluation:result:update",
"display_name": "更新评查结果",
"route_id": null,
"related_routes": [58, 37]
},
{
"id": 137,
"permission_key": "evaluation:audit_status:create",
"display_name": "创建审核状态",
"route_id": null,
"related_routes": [58, 37]
},
{
"id": 138,
"permission_key": "evaluation:document:confirm",
"display_name": "确认文档审核完成",
"route_id": null,
"related_routes": [58, 37]
}
]
```
---
## 前端展示逻辑
### 页面权限树结构
```
文件管理 (/documents)
├── 文档列表 (/documents/list)
├── 文档评查结果详情 (/reviews) ← route_id=58
│ ├── [独立] GET 查看评查结果
│ ├── [独立] PATCH 更新评查结果
│ ├── [独立] GET 查看评查详情
│ ├── [通用] GET 查看审计状态 ← is_shared=true
│ ├── [通用] PATCH 更新审计状态 ← is_shared=true
│ ├── [通用] POST 创建审核状态 ← is_shared=true
│ ├── [通用] PATCH 更新评查结果 ← is_shared=true
│ └── [通用] PATCH 确认文档审核完成 ← is_shared=true
交叉评查 (/cross-checking)
├── 上传评查文档 (/cross-checking/upload)
├── 评查结果 (/cross-checking/result) ← route_id=37
│ ├── [独立] POST 查看评分提案
│ ├── [独立] POST 创建评分提案
│ ├── [独立] POST 对提案投票
│ ├── [独立] DELETE 删除评分提案
│ ├── [独立] POST 标记文档完成
│ ├── [独立] GET 查看任务进度
│ ├── [通用] GET 查看审计状态 ← is_shared=true (同上)
│ ├── [通用] PATCH 更新审计状态 ← is_shared=true (同上)
│ ├── [通用] POST 创建审核状态 ← is_shared=true (同上)
│ ├── [通用] PATCH 更新评查结果 ← is_shared=true (同上)
│ └── [通用] PATCH 确认文档审核完成 ← is_shared=true (同上)
```
### 识别通用权限
```javascript
// 判断是否为通用权限
function isSharedPermission(permission) {
return permission.related_routes !== null &&
Array.isArray(permission.related_routes) &&
permission.related_routes.length > 1;
}
// 获取通用权限关联的所有路由ID
function getRelatedRouteIds(permission) {
if (isSharedPermission(permission)) {
return permission.related_routes;
}
return permission.route_id ? [permission.route_id] : [];
}
```
### UI 展示建议
```jsx
// 权限项组件
function PermissionItem({ permission, checked, onChange }) {
const isShared = isSharedPermission(permission);
return (
<div className={`permission-item ${isShared ? 'shared' : ''}`}>
<Checkbox
checked={checked}
onChange={onChange}
/>
<span className="permission-name">
{isShared && <Tag color="blue">通用</Tag>}
{permission.display_name}
</span>
{isShared && (
<Tooltip title={`此权限同时适用于: ${permission.related_routes.join(', ')}`}>
<Icon type="info-circle" />
</Tooltip>
)}
</div>
);
}
```
---
## 同步勾选逻辑
### 核心逻辑
当用户勾选/取消一个**通用权限**时,需要同步更新所有关联路由下该权限的勾选状态。
### 实现代码
```javascript
// 权限状态管理
class PermissionManager {
constructor() {
// 存储所有权限数据
this.permissions = [];
// 存储已勾选的权限ID集合
this.checkedPermissionIds = new Set();
// 通用权限ID列表(用于快速查找)
this.sharedPermissionIds = new Set();
}
// 加载权限数据
async loadPermissions() {
// 查询所有权限
const response = await fetch('/api/postgrest/proxy/permissions?select=*');
this.permissions = await response.json();
// 识别通用权限
this.sharedPermissionIds = new Set(
this.permissions
.filter(p => p.related_routes !== null)
.map(p => p.id)
);
}
// 获取某个路由下的所有权限
getPermissionsByRouteId(routeId) {
return this.permissions.filter(p =>
p.route_id === routeId ||
(p.related_routes && p.related_routes.includes(routeId))
);
}
// 勾选权限
checkPermission(permissionId) {
this.checkedPermissionIds.add(permissionId);
// 通用权限会自动在所有关联路由下显示为勾选状态
// 因为我们是用 permission_id 来管理勾选状态,而不是 route_id + permission_id
}
// 取消勾选权限
uncheckPermission(permissionId) {
this.checkedPermissionIds.delete(permissionId);
}
// 判断权限是否被勾选
isPermissionChecked(permissionId) {
return this.checkedPermissionIds.has(permissionId);
}
// 判断是否为通用权限
isSharedPermission(permissionId) {
return this.sharedPermissionIds.has(permissionId);
}
// 获取通用权限关联的路由名称
getRelatedRouteNames(permission) {
if (!permission.related_routes) return [];
// 需要有路由名称映射
return permission.related_routes.map(routeId => {
// 从 sys_routes 表获取路由名称
return this.routeMap[routeId]?.route_title || `路由${routeId}`;
});
}
}
```
### React/Vue 组件示例
```jsx
// React 示例
function PermissionTree({ routeId, permissionManager }) {
const [checkedIds, setCheckedIds] = useState(new Set());
// 获取当前路由下的所有权限
const permissions = permissionManager.getPermissionsByRouteId(routeId);
// 处理勾选变化
const handleCheckChange = (permissionId, checked) => {
const newCheckedIds = new Set(checkedIds);
if (checked) {
newCheckedIds.add(permissionId);
} else {
newCheckedIds.delete(permissionId);
}
setCheckedIds(newCheckedIds);
// 如果是通用权限,通知其他关联路由的组件更新UI
if (permissionManager.isSharedPermission(permissionId)) {
// 触发全局状态更新或事件
eventBus.emit('sharedPermissionChanged', { permissionId, checked });
}
};
// 监听通用权限变化事件
useEffect(() => {
const handleSharedChange = ({ permissionId, checked }) => {
// 检查这个通用权限是否属于当前路由
const permission = permissions.find(p => p.id === permissionId);
if (permission) {
// 更新本地勾选状态
const newCheckedIds = new Set(checkedIds);
if (checked) {
newCheckedIds.add(permissionId);
} else {
newCheckedIds.delete(permissionId);
}
setCheckedIds(newCheckedIds);
}
};
eventBus.on('sharedPermissionChanged', handleSharedChange);
return () => eventBus.off('sharedPermissionChanged', handleSharedChange);
}, [permissions, checkedIds]);
return (
<div className="permission-tree">
{permissions.map(permission => (
<PermissionItem
key={permission.id}
permission={permission}
checked={checkedIds.has(permission.id)}
onChange={(checked) => handleCheckChange(permission.id, checked)}
/>
))}
</div>
);
}
```
---
## 保存角色权限
### 请求格式
```http
POST /api/v3/rbac/role-permissions
Content-Type: application/json
{
"role_id": 2,
"permissions": [
{"permission_id": 82, "grant_type": "GRANT", "data_scope": "ALL"},
{"permission_id": 83, "grant_type": "GRANT", "data_scope": "DEPT"},
{"permission_id": 88, "grant_type": "GRANT", "data_scope": "ALL"}, //
{"permission_id": 89, "grant_type": "GRANT", "data_scope": "ALL"}, //
// ...
],
"replace": true
}
```
**注意**
- 通用权限只需要提交一次(使用 permission_id
- 不需要为每个关联路由分别提交
- 后端权限判断基于 `permission_key`,与 route_id 无关
---
## 完整工作流程
### 1. 加载权限配置页面
```javascript
async function loadPermissionConfigPage(roleId) {
// 1. 获取所有路由树
const routes = await fetch('/api/postgrest/proxy/sys_routes?deleted_at=is.null&order=sort_order');
// 2. 获取所有权限
const permissions = await fetch('/api/postgrest/proxy/permissions?select=*');
// 3. 获取角色已有的权限
const rolePermissions = await fetch(`/api/v3/rbac/role-permissions?role_id=${roleId}`);
// 4. 构建权限树(每个路由下挂载对应的权限)
const permissionTree = buildPermissionTree(routes, permissions);
// 5. 标记已勾选的权限
markCheckedPermissions(permissionTree, rolePermissions.data.permissions);
return permissionTree;
}
function buildPermissionTree(routes, permissions) {
return routes.map(route => ({
...route,
permissions: permissions.filter(p =>
p.route_id === route.id ||
(p.related_routes && p.related_routes.includes(route.id))
).map(p => ({
...p,
isShared: p.related_routes !== null
}))
}));
}
```
### 2. 用户交互
```javascript
// 勾选权限
function onPermissionCheck(permissionId, checked) {
// 更新本地状态
updateLocalState(permissionId, checked);
// 如果是通用权限,同步更新所有关联路由的UI
const permission = findPermission(permissionId);
if (permission.isShared) {
syncSharedPermissionUI(permission, checked);
}
}
// 同步通用权限UI
function syncSharedPermissionUI(permission, checked) {
permission.related_routes.forEach(routeId => {
// 更新对应路由节点下该权限的勾选状态
updateRoutePermissionUI(routeId, permission.id, checked);
});
}
```
### 3. 保存配置
```javascript
async function saveRolePermissions(roleId) {
// 收集所有勾选的权限ID(通用权限只收集一次)
const checkedPermissionIds = collectCheckedPermissionIds();
// 构建请求数据
const requestData = {
role_id: roleId,
permissions: checkedPermissionIds.map(id => ({
permission_id: id,
grant_type: 'GRANT',
data_scope: getDataScope(id) // 根据业务需求设置
})),
replace: true
};
// 提交到后端
await fetch('/api/v3/rbac/role-permissions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
});
}
```
---
## 样式建议
```css
/* 通用权限样式 */
.permission-item.shared {
background-color: #f0f7ff;
border-left: 3px solid #1890ff;
}
.permission-item.shared .permission-name {
color: #1890ff;
}
/* 通用标签 */
.shared-tag {
display: inline-block;
padding: 0 6px;
font-size: 12px;
background-color: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 2px;
color: #1890ff;
margin-right: 8px;
}
/* 同步勾选提示 */
.sync-indicator {
font-size: 12px;
color: #999;
margin-left: 8px;
}
```
---
## API 接口汇总
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/postgrest/proxy/permissions` | GET | 查询权限列表 |
| `/api/postgrest/proxy/sys_routes` | GET | 查询路由列表 |
| `/api/v3/rbac/role-permissions` | GET | 查询角色已有权限 |
| `/api/v3/rbac/role-permissions` | POST | 保存角色权限配置 |
### 查询示例
```bash
# 查询所有权限
curl '/api/postgrest/proxy/permissions?select=id,permission_key,display_name,route_id,related_routes'
# 查询 route_id=58 的权限(包含通用权限)
curl '/api/postgrest/proxy/permissions?or=(route_id.eq.58,related_routes.cs.{58})'
# 查询所有通用权限
curl '/api/postgrest/proxy/permissions?related_routes=not.is.null'
```
---
## 注意事项
1. **通用权限只存储一份**:在 permissions 表中,通用权限只有一条记录,通过 `related_routes` 字段关联多个路由
2. **权限判断不依赖 route_id**:后端权限检查只看 `permission_key`,与 route_id 无关
3. **前端需要做 UI 同步**:当勾选/取消通用权限时,需要同步更新所有关联路由下该权限的显示状态
4. **保存时只提交一次**:通用权限在保存角色权限时只需要提交一次 permission_id
5. **建议添加视觉标识**:使用颜色、图标或标签区分通用权限和独立权限,提升用户体验
---
## 方案深入分析
### 数据结构说明
```
role_permissions 表结构:
┌─────────────────┬───────────────┬──────────────┬────────────┐
│ role_id │ permission_id │ grant_type │ data_scope │
├─────────────────┼───────────────┼──────────────┼────────────┤
│ 1 (市级管理员) │ 88 │ GRANT │ ALL │
│ 2 (普通员工) │ 88 │ GRANT │ DEPT │
└─────────────────┴───────────────┴──────────────┴────────────┘
关键点:role_permissions 只关联 permission_id,不关联 route_id
```
### 各场景分析
| 场景 | 状态 | 说明 |
|------|------|------|
| 后端权限判断 | ✅ 无影响 | 基于 permission_key,不涉及 route_id |
| 权限保存 | ✅ 无影响 | 只保存 permission_id,通用权限只保存一次 |
| 数据范围 (data_scope) | ⚠️ 注意 | 同一权限只有一个数据范围,不按页面区分 |
| 按页面区分权限 | ❌ 不支持 | 同一权限无法在 A 页面有、B 页面无 |
| 前端 UI 同步 | ⚠️ 需实现 | 前端需要实现同步勾选逻辑 |
### 场景详解
#### 1. 后端权限判断 - 无影响
```python
# 后端权限检查逻辑(permission_checker.py
SELECT p.permission_key
FROM role_permissions rp
JOIN permissions p ON rp.permission_id = p.id
WHERE rp.role_id = {user_role_id} AND rp.grant_type = 'GRANT'
```
只查 `permission_key`,完全不涉及 `route_id`,所以通用权限方案不影响权限判断。
#### 2. 权限保存 - 无影响
```javascript
// 保存时只提交 permission_id
{
"role_id": 2,
"permissions": [
{"permission_id": 88, "grant_type": "GRANT"}, // 通用权限只保存一次
{"permission_id": 89, "grant_type": "GRANT"}
]
}
```
通用权限只有一条记录,保存一次即可,两个页面自动都有。
#### 3. 数据范围 (data_scope) - 需注意
**潜在问题**:如果同一个通用权限在不同页面需要不同的数据范围?
| 页面 | 权限 | 期望的 data_scope |
|------|------|-------------------|
| /reviews | 查看审计状态 | DEPT(只看本部门) |
| /cross-checking/result | 查看审计状态 | ALL(看所有) |
**当前设计**:一个权限只有一个 `data_scope`,无法按页面区分。
**实际业务**:通用权限操作的是同一个数据表(audit_status),数据范围应该一致,所以这个问题在当前业务场景下不存在。
#### 4. 按页面区分权限 - 不支持
**场景**:用户想要在 `/reviews` 有权限,但在 `/cross-checking/result` 没有权限。
**当前方案不支持**,因为是同一个 `permission_id`
**但是**:从业务逻辑来看,这种需求不合理:
- 两个页面访问的是**同一个 API**
- 如果用户能在 A 页面调用 API,直接调用 API 也能成功
- 按页面区分只是**前端显示**问题,不是**后端权限**问题
### 适用场景
**方案适用于**
- 同一个 API 被多个页面共享
- 不需要按页面区分权限/数据范围
- 权限判断逻辑一致
**方案不适用于**
- 需要按页面单独控制权限
- 同一 API 在不同页面需要不同的数据范围
### 如果需要按页面区分权限
如果将来有其他共享 API 需要按页面区分权限/数据范围,则需要**创建两个独立权限**:
```sql
-- 方案:为每个页面创建独立权限
INSERT INTO permissions (permission_key, display_name, route_id) VALUES
('evaluation:audit_status:view:reviews', '查看审计状态(评查详情)', 58),
('evaluation:audit_status:view:crosschecking', '查看审计状态(交叉评查)', 37);
```
后端权限检查也需要相应修改,根据请求来源判断使用哪个 permission_key。
---
## 更新记录
| 日期 | 版本 | 说明 |
|------|------|------|
| 2025-12-10 | 1.0 | 初始版本,支持通用权限展示和同步勾选 |
| 2025-12-10 | 1.1 | 添加必须修改的查询方式说明、方案深入分析 |
+78 -78
View File
@@ -1,11 +1,11 @@
// ecosystem.config.cjs - CommonJS 版本 // ecosystem.config.cjs - CommonJS 版本
// 多客户端部署配置:支持3个不同地区客户端通过不同端口访问 // 多客户端部署配置:支持3个不同地区客户端通过不同端口访问
// testing 环境配置
module.exports = { module.exports = {
apps: [ apps: [
// 主服务 - 生产环境 (端口: 51703) // 主服务梅州 - 生产环境 (端口: 51703)
{ {
name: 'docreview-main', name: 'docreview-meizhou-client',
script: 'node', script: 'node',
args: [ args: [
'-r', 'dotenv/config', '-r', 'dotenv/config',
@@ -22,36 +22,36 @@ module.exports = {
env: { env: {
NODE_ENV: 'testing', NODE_ENV: 'testing',
PORT: 5183, PORT: 5183,
CLIENT_ID: 'main', CLIENT_ID: 'meizhou',
API_PORT_CONFIG: '5183', API_PORT_CONFIG: '5183',
// 添加这些环境变量确保客户端能获取到 // 添加这些环境变量确保客户端能获取到
NEXT_PUBLIC_NODE_ENV: 'testing', NEXT_PUBLIC_NODE_ENV: 'testing',
NEXT_PUBLIC_PORT: '5183', NEXT_PUBLIC_PORT: '5183',
NEXT_PUBLIC_CLIENT_ID: 'main', NEXT_PUBLIC_CLIENT_ID: 'meizhou',
NEXT_PUBLIC_API_PORT_CONFIG: '5183', NEXT_PUBLIC_API_PORT_CONFIG: '5183',
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
}, },
env_testing: { env_testing: {
NODE_ENV: 'testing', NODE_ENV: 'testing',
PORT: 5183, PORT: 5183,
CLIENT_ID: 'main', CLIENT_ID: 'meizhou',
API_PORT_CONFIG: '5183', API_PORT_CONFIG: '5183',
// 添加这些环境变量确保客户端能获取到 // 添加这些环境变量确保客户端能获取到
NEXT_PUBLIC_NODE_ENV: 'testing', NEXT_PUBLIC_NODE_ENV: 'testing',
NEXT_PUBLIC_PORT: '5183', NEXT_PUBLIC_PORT: '5183',
NEXT_PUBLIC_CLIENT_ID: 'main', NEXT_PUBLIC_CLIENT_ID: 'meizhou',
NEXT_PUBLIC_API_PORT_CONFIG: '5183', NEXT_PUBLIC_API_PORT_CONFIG: '5183',
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
}, },
error_file: './logs/main-err.log', error_file: './logs/meizhou-err.log',
out_file: './logs/main-out.log', out_file: './logs/meizhou-out.log',
log_file: './logs/main-combined.log', log_file: './logs/meizhou-combined.log',
time: true time: true
}, },
// 客户端潮州 - 反向代理服务 (端口: 51704) // 客户端云浮 - 反向代理服务 (端口: 51704)
{ {
name: 'docreview-client-chaozhou', name: 'docreview-yunfu-client',
script: 'node', script: 'node',
args: [ args: [
'-r', 'dotenv/config', '-r', 'dotenv/config',
@@ -64,12 +64,12 @@ module.exports = {
env: { env: {
NODE_ENV: 'testing', NODE_ENV: 'testing',
PORT: 51704, PORT: 51704,
CLIENT_ID: 'chaozhou', CLIENT_ID: 'yunfu',
API_PORT_CONFIG: '51704', API_PORT_CONFIG: '51704',
// 添加这些环境变量确保客户端能获取到 // 添加这些环境变量确保客户端能获取到
NEXT_PUBLIC_NODE_ENV: 'testing', NEXT_PUBLIC_NODE_ENV: 'testing',
NEXT_PUBLIC_PORT: '51704', NEXT_PUBLIC_PORT: '51704',
NEXT_PUBLIC_CLIENT_ID: 'chaozhou', NEXT_PUBLIC_CLIENT_ID: 'yunfu',
NEXT_PUBLIC_API_PORT_CONFIG: '51704', NEXT_PUBLIC_API_PORT_CONFIG: '51704',
// 🔒 OAuth 敏感配置 // 🔒 OAuth 敏感配置
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
@@ -77,24 +77,24 @@ module.exports = {
env_testing: { env_testing: {
NODE_ENV: 'testing', NODE_ENV: 'testing',
PORT: 51704, PORT: 51704,
CLIENT_ID: 'chaozhou', CLIENT_ID: 'yunfu',
API_PORT_CONFIG: '51704', API_PORT_CONFIG: '51704',
// 添加这些环境变量确保客户端能获取到 // 添加这些环境变量确保客户端能获取到
NEXT_PUBLIC_NODE_ENV: 'testing', NEXT_PUBLIC_NODE_ENV: 'testing',
NEXT_PUBLIC_PORT: '51704', NEXT_PUBLIC_PORT: '51704',
NEXT_PUBLIC_CLIENT_ID: 'chaozhou', NEXT_PUBLIC_CLIENT_ID: 'yunfu',
NEXT_PUBLIC_API_PORT_CONFIG: '51704', NEXT_PUBLIC_API_PORT_CONFIG: '51704',
// 🔒 OAuth 敏感配置 // 🔒 OAuth 敏感配置
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
}, },
error_file: './logs/chaozhou-err.log', error_file: './logs/yunfu-err.log',
out_file: './logs/chaozhou-out.log', out_file: './logs/yunfu-out.log',
log_file: './logs/chaozhou-combined.log', log_file: './logs/yunfu-combined.log',
time: true time: true
}, },
// 客户端揭阳 - 独立服务 (端口: 51705) // 客户端揭阳 - 独立服务 (端口: 51705)
{ {
name: 'docreview-client-jieyang', name: 'docreview-jieyang-client',
script: 'node', script: 'node',
args: [ args: [
'-r', 'dotenv/config', '-r', 'dotenv/config',
@@ -135,9 +135,9 @@ module.exports = {
log_file: './logs/jieyang-combined.log', log_file: './logs/jieyang-combined.log',
time: true time: true
}, },
// 客户端云浮 - 独立服务 (端口: 51706) // 客户端潮州 - 独立服务 (端口: 51706)
{ {
name: 'docreview-client-yunfu', name: 'docreview-chaozhou-client',
script: 'node', script: 'node',
args: [ args: [
'-r', 'dotenv/config', '-r', 'dotenv/config',
@@ -150,12 +150,12 @@ module.exports = {
env: { env: {
NODE_ENV: 'testing', NODE_ENV: 'testing',
PORT: 51706, PORT: 51706,
CLIENT_ID: 'yunfu', CLIENT_ID: 'chaozhou',
API_PORT_CONFIG: '51706', API_PORT_CONFIG: '51706',
// 添加这些环境变量确保客户端能获取到 // 添加这些环境变量确保客户端能获取到
NEXT_PUBLIC_NODE_ENV: 'testing', NEXT_PUBLIC_NODE_ENV: 'testing',
NEXT_PUBLIC_PORT: '51706', NEXT_PUBLIC_PORT: '51706',
NEXT_PUBLIC_CLIENT_ID: 'yunfu', NEXT_PUBLIC_CLIENT_ID: 'chaozhou',
NEXT_PUBLIC_API_PORT_CONFIG: '51706', NEXT_PUBLIC_API_PORT_CONFIG: '51706',
// 🔒 OAuth 敏感配置 // 🔒 OAuth 敏感配置
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
@@ -163,24 +163,24 @@ module.exports = {
env_testing: { env_testing: {
NODE_ENV: 'testing', NODE_ENV: 'testing',
PORT: 51706, PORT: 51706,
CLIENT_ID: 'yunfu', CLIENT_ID: 'chaozhou',
API_PORT_CONFIG: '51706', API_PORT_CONFIG: '51706',
// 添加这些环境变量确保客户端能获取到 // 添加这些环境变量确保客户端能获取到
NEXT_PUBLIC_NODE_ENV: 'testing', NEXT_PUBLIC_NODE_ENV: 'testing',
NEXT_PUBLIC_PORT: '51706', NEXT_PUBLIC_PORT: '51706',
NEXT_PUBLIC_CLIENT_ID: 'yunfu', NEXT_PUBLIC_CLIENT_ID: 'chaozhou',
NEXT_PUBLIC_API_PORT_CONFIG: '51706', NEXT_PUBLIC_API_PORT_CONFIG: '51706',
// 🔒 OAuth 敏感配置 // 🔒 OAuth 敏感配置
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
}, },
error_file: './logs/yunfu-err.log', error_file: './logs/chaozhou-err.log',
out_file: './logs/yunfu-out.log', out_file: './logs/chaozhou-out.log',
log_file: './logs/yunfu-combined.log', log_file: './logs/chaozhou-combined.log',
time: true time: true
}, },
// 客户端梅州 - 独立服务 (端口: 51707) // 客户端省局 - 独立服务 (端口: 51707)
{ {
name: 'docreview-client-meizhou', name: 'docreview-province-client',
script: 'node', script: 'node',
args: [ args: [
'-r', 'dotenv/config', '-r', 'dotenv/config',
@@ -193,12 +193,12 @@ module.exports = {
env: { env: {
NODE_ENV: 'testing', NODE_ENV: 'testing',
PORT: 51707, PORT: 51707,
CLIENT_ID: 'meizhou', CLIENT_ID: 'province',
API_PORT_CONFIG: '51707', API_PORT_CONFIG: '51707',
// 添加这些环境变量确保客户端能获取到 // 添加这些环境变量确保客户端能获取到
NEXT_PUBLIC_NODE_ENV: 'testing', NEXT_PUBLIC_NODE_ENV: 'testing',
NEXT_PUBLIC_PORT: '51707', NEXT_PUBLIC_PORT: '51707',
NEXT_PUBLIC_CLIENT_ID: 'meizhou', NEXT_PUBLIC_CLIENT_ID: 'province',
NEXT_PUBLIC_API_PORT_CONFIG: '51707', NEXT_PUBLIC_API_PORT_CONFIG: '51707',
// 🔒 OAuth 敏感配置 // 🔒 OAuth 敏感配置
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
@@ -206,63 +206,63 @@ module.exports = {
env_testing: { env_testing: {
NODE_ENV: 'testing', NODE_ENV: 'testing',
PORT: 51707, PORT: 51707,
CLIENT_ID: 'meizhou', CLIENT_ID: 'province',
API_PORT_CONFIG: '51707', API_PORT_CONFIG: '51707',
// 添加这些环境变量确保客户端能获取到 // 添加这些环境变量确保客户端能获取到
NEXT_PUBLIC_NODE_ENV: 'testing', NEXT_PUBLIC_NODE_ENV: 'testing',
NEXT_PUBLIC_PORT: '51707', NEXT_PUBLIC_PORT: '51707',
NEXT_PUBLIC_CLIENT_ID: 'meizhou', NEXT_PUBLIC_CLIENT_ID: 'province',
NEXT_PUBLIC_API_PORT_CONFIG: '51707', NEXT_PUBLIC_API_PORT_CONFIG: '51707',
// 🔒 OAuth 敏感配置 // 🔒 OAuth 敏感配置
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
}, },
error_file: './logs/meizhou-err.log',
out_file: './logs/meizhou-out.log',
log_file: './logs/meizhou-combined.log',
time: true
},
// 客户端省局 - 独立服务 (端口: 51708)
{
name: 'docreview-client-province',
script: 'node',
args: [
'-r', 'dotenv/config',
'./server.js' // 使用自定义服务器(包含 HTTP 日志记录)
],
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'testing',
PORT: 51708,
CLIENT_ID: 'province',
API_PORT_CONFIG: '51708',
// 添加这些环境变量确保客户端能获取到
NEXT_PUBLIC_NODE_ENV: 'testing',
NEXT_PUBLIC_PORT: '51708',
NEXT_PUBLIC_CLIENT_ID: 'province',
NEXT_PUBLIC_API_PORT_CONFIG: '51708',
// 🔒 OAuth 敏感配置
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
},
env_testing: {
NODE_ENV: 'testing',
PORT: 51708,
CLIENT_ID: 'province',
API_PORT_CONFIG: '51708',
// 添加这些环境变量确保客户端能获取到
NEXT_PUBLIC_NODE_ENV: 'testing',
NEXT_PUBLIC_PORT: '51708',
NEXT_PUBLIC_CLIENT_ID: 'province',
NEXT_PUBLIC_API_PORT_CONFIG: '51708',
// 🔒 OAuth 敏感配置
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
},
error_file: './logs/province-err.log', error_file: './logs/province-err.log',
out_file: './logs/province-out.log', out_file: './logs/province-out.log',
log_file: './logs/province-combined.log', log_file: './logs/province-combined.log',
time: true time: true
},
// 客户端 - 独立服务 (端口: 51708)
{
name: 'docreview-test-client',
script: 'node',
args: [
'-r', 'dotenv/config',
'./server.js' // 使用自定义服务器(包含 HTTP 日志记录)
],
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'testing',
PORT: 51708,
CLIENT_ID: 'test',
API_PORT_CONFIG: '51708',
// 添加这些环境变量确保客户端能获取到
NEXT_PUBLIC_NODE_ENV: 'testing',
NEXT_PUBLIC_PORT: '51708',
NEXT_PUBLIC_CLIENT_ID: 'test',
NEXT_PUBLIC_API_PORT_CONFIG: '51708',
// 🔒 OAuth 敏感配置
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
},
env_testing: {
NODE_ENV: 'testing',
PORT: 51708,
CLIENT_ID: 'test',
API_PORT_CONFIG: '51708',
// 添加这些环境变量确保客户端能获取到
NEXT_PUBLIC_NODE_ENV: 'testing',
NEXT_PUBLIC_PORT: '51708',
NEXT_PUBLIC_CLIENT_ID: 'test',
NEXT_PUBLIC_API_PORT_CONFIG: '51708',
// 🔒 OAuth 敏感配置
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
},
error_file: './logs/test-err.log',
out_file: './logs/test-out.log',
log_file: './logs/test-combined.log',
time: true
} }
] ]
}; };
+4 -3
View File
@@ -34,13 +34,12 @@
"axios": "^1.9.0", "axios": "^1.9.0",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"compression": "^1.7.5", "compression": "^1.7.5",
"express": "^4.21.2",
"morgan": "^1.10.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"diff": "^7.0.0", "diff": "^7.0.0",
"docx-preview": "^0.3.5", "docx-preview": "^0.3.5",
"docxtemplater": "^3.67.5", "docxtemplater": "^3.67.5",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"express": "^4.21.2",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"html-docx-js": "^0.3.1", "html-docx-js": "^0.3.1",
"immer": "^10.1.1", "immer": "^10.1.1",
@@ -50,10 +49,12 @@
"katex": "^0.16.22", "katex": "^0.16.22",
"mammoth": "^1.9.0", "mammoth": "^1.9.0",
"monaco-editor": "^0.55.1", "monaco-editor": "^0.55.1",
"morgan": "^1.10.0",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pg": "^8.14.1", "pg": "^8.14.1",
"pizzip": "^3.2.0", "pizzip": "^3.2.0",
"pm2": "^6.0.8", "pm2": "^6.0.14",
"pm2-logrotate": "^3.0.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",