diff --git a/app/api/contract-template/templates.ts b/app/api/contract-template/templates.ts index aa02704..4affb19 100644 --- a/app/api/contract-template/templates.ts +++ b/app/api/contract-template/templates.ts @@ -82,7 +82,7 @@ export async function getContractCategories(jwt?: string) { token: jwt }; - const response = await postgrestGet('contract_categories', params); + const response = await postgrestGet('/api/postgrest/proxy/contract_categories', params); if (response.error) { return { error: response.error, status: response.status || 500 }; @@ -107,7 +107,7 @@ export async function getContractCategories(jwt?: string) { export async function getContractCategoriesWithCount(jwt?: string) { try { // 获取所有分类 - const categoriesResponse = await postgrestGet('contract_categories', { + const categoriesResponse = await postgrestGet('/api/postgrest/proxy/contract_categories', { select: '*', order: 'sort_order.asc,name.asc', token: jwt @@ -124,7 +124,7 @@ export async function getContractCategoriesWithCount(jwt?: string) { categories.map(async (category) => { try { // 简化方案:获取该分类下的所有模板ID,然后计算数量 - const countResponse = await postgrestGet<{ id: number }[]>('contract_templates', { + const countResponse = await postgrestGet<{ id: number }[]>('/api/postgrest/proxy/contract_templates', { select: 'id', filter: { 'category_id': `eq.${category.id}` }, token: jwt @@ -214,7 +214,7 @@ export async function getContractTemplates(searchParams: TemplateSearchParams = // 先查询匹配的分类ID let matchingCategoryIds: number[] = []; try { - const categoryResponse = await postgrestGet('contract_categories', { + const categoryResponse = await postgrestGet('/api/postgrest/proxy/contract_categories', { select: 'id', filter: { 'name': `ilike.*${cleanKeyword}*` }, token @@ -245,7 +245,7 @@ export async function getContractTemplates(searchParams: TemplateSearchParams = // 如果有分类名称,需要先获取分类ID if (category && !category_id) { - const categoryResponse = await postgrestGet('contract_categories', { + const categoryResponse = await postgrestGet('/api/postgrest/proxy/contract_categories', { select: 'id', filter: { 'name': `eq.${category}` }, token @@ -264,7 +264,7 @@ export async function getContractTemplates(searchParams: TemplateSearchParams = } // 执行查询 - const response = await postgrestGet('contract_templates', params); + const response = await postgrestGet('/api/postgrest/proxy/contract_templates', params); if (response.error) { return { error: response.error, status: response.status || 500 }; @@ -280,7 +280,7 @@ export async function getContractTemplates(searchParams: TemplateSearchParams = token }; - const countResponse = await postgrestGet<{ id: number }[]>('contract_templates', countParams); + const countResponse = await postgrestGet<{ id: number }[]>('/api/postgrest/proxy/contract_templates', countParams); let total = 0; if (!countResponse.error && countResponse.data) { const countData = extractApiData<{ id: number }[]>(countResponse.data) || []; @@ -318,7 +318,7 @@ export async function getContractTemplate(id: string | number, jwt?: string) { token: jwt }; - const response = await postgrestGet('contract_templates', params); + const response = await postgrestGet('/api/postgrest/proxy/contract_templates', params); if (response.error) { return { error: response.error, status: response.status || 500 }; @@ -355,7 +355,7 @@ export async function getFeaturedTemplates(limit: number = 6, jwt?: string) { token: jwt }; - const response = await postgrestGet('contract_templates', params); + const response = await postgrestGet('/api/postgrest/proxy/contract_templates', params); if (response.error) { return { error: response.error, status: response.status || 500 }; diff --git a/app/api/contracts/draft-service.server.ts b/app/api/contracts/draft-service.server.ts index 245d480..6dfb562 100644 --- a/app/api/contracts/draft-service.server.ts +++ b/app/api/contracts/draft-service.server.ts @@ -80,134 +80,3 @@ export async function copyMinioFile( } } -/** - * 创建起草合同记录 - * @param request 创建请求 - * @param userId 用户ID - * @param draftFilePath 可选:草稿文件路径(如果不提供,使用模板路径) - * @returns 创建的记录 - * - * 使用场景: - * 1. 不传 draftFilePath:直接使用模板文件路径,在原模板上编辑 - * 2. 传 draftFilePath:使用复制后的文件路径(由文件复制接口提供) - */ -export async function createDraftContract( - request: CreateDraftRequest, - userId: number, - draftFilePath?: string, - jwt?: string -): Promise { - try { - // 1. 查询模板信息 - const templateResponse = await postgrestGet('contract_templates', { - select: 'id,file_path', - filter: { id: `eq.${request.templateId}` }, - token: jwt - }); - - if (!templateResponse.data || (Array.isArray(templateResponse.data) && templateResponse.data.length === 0)) { - throw new Error('模板不存在'); - } - - const template = Array.isArray(templateResponse.data) ? templateResponse.data[0] : templateResponse.data; - - // 2. 确定使用的文件路径 - // 如果没有提供草稿路径,直接使用模板路径(适合直接编辑模板的场景) - // 如果提供了草稿路径,使用复制后的文件路径 - const finalFilePath = draftFilePath || template.file_path; - - console.log('[Draft Service] 创建草稿:', { - templateId: request.templateId, - templatePath: template.file_path, - draftPath: draftFilePath, - finalPath: finalFilePath, - mode: draftFilePath ? '使用复制文件' : '直接使用模板文件' - }); - - // 3. 创建草稿记录 - const insertResponse = await postgrestPost('drafted_contracts', { - body: { - template_id: request.templateId, - file_path: finalFilePath, - title: request.title, - placeholder_values: {}, - status: 'draft', - created_by: userId - }, - select: '*', - token: jwt - }); - - if (!insertResponse.data) { - throw new Error('创建草稿记录失败'); - } - - const draft = Array.isArray(insertResponse.data) ? insertResponse.data[0] : insertResponse.data; - return draft as DraftedContract; - } catch (error) { - console.error('[Draft Service] 创建草稿失败:', error); - throw error; - } -} - -/** - * 删除草稿记录 - * @param draftId 草稿ID - * @param userId 用户ID - * @param jwt JWT token - */ -export async function deleteDraft( - draftId: number, - userId: number, - jwt?: string -): Promise { - try { - const response = await postgrestDelete('drafted_contracts', { - filter: { - id: `eq.${draftId}`, - created_by: `eq.${userId}` // 确保只能删除自己的草稿 - }, - token: jwt - }); - - console.log('[Draft Service] 草稿已删除:', draftId); - } catch (error) { - console.error('[Draft Service] 删除草稿失败:', error); - throw error; - } -} - - -/** - * 获取草稿详情 - * @param draftId 草稿ID - * @param userId 用户ID - * @returns 草稿记录 - */ -export async function getDraftById( - draftId: number, - userId: number, - jwt?: string -): Promise { - try { - const response = await postgrestGet('drafted_contracts', { - select: '*', - filter: { - id: `eq.${draftId}`, - created_by: `eq.${userId}` - }, - token: jwt - }); - - if (!response.data || (Array.isArray(response.data) && response.data.length === 0)) { - return null; - } - - const draft = Array.isArray(response.data) ? response.data[0] : response.data; - return draft as DraftedContract; - } catch (error) { - console.error('[Draft Service] 获取草稿失败:', error); - return null; - } -} - diff --git a/app/api/cross-checking/cross-file-result.ts b/app/api/cross-checking/cross-file-result.ts index b3cc2fb..2185bd7 100644 --- a/app/api/cross-checking/cross-file-result.ts +++ b/app/api/cross-checking/cross-file-result.ts @@ -90,7 +90,7 @@ async function safeGetJWT(jwtToken?: string): Promise { */ export async function findIsProposer(taskId: string | number, userId: number | undefined, frontendJWT?: string): Promise { // 通过postgrest的get请求去cross_examination_tasks表中进行查找assignee_id是否等于userId - const response = await postgrestGet(`cross_examination_tasks`, { + const response = await postgrestGet(`/api/postgrest/proxy/cross_examination_tasks`, { select: 'assigner_id', filter: { id: `eq.${taskId}` @@ -399,7 +399,7 @@ export async function confirmReviewResults( ): Promise<{data?: unknown, error?: string, status?: number}> { try { // 通过postgrest的post请求去documents表中进行查找id等于documentId的数据,更新documents表的audit_status为1 - const response = await postgrestPut(`documents`, { + const response = await postgrestPut(`/api/postgrest/proxy/documents`, { audit_status: 1 }, { id: documentId diff --git a/app/api/cross-checking/cross-files.ts b/app/api/cross-checking/cross-files.ts index 90f6070..802ad5e 100644 --- a/app/api/cross-checking/cross-files.ts +++ b/app/api/cross-checking/cross-files.ts @@ -494,7 +494,7 @@ export async function updateDocumentAuditStatus(id: string, auditStatus: number, } const response = await postgrestPut>( - 'documents', + '/api/postgrest/proxy/documents', { audit_status: auditStatus }, { id: parseInt(id) @@ -526,7 +526,7 @@ export async function getCrossCheckingDocumentTypes(jwtToken?: string): Promise< try { // console.log('[getCrossCheckingDocumentTypes] 开始获取交叉评查文档类型'); - const response = await postgrestGet('document_types',{ + const response = await postgrestGet('/api/postgrest/proxy/document_types',{ select: 'id,name,code,evaluation_point_groups_ids', filter: { evaluation_point_groups_ids: 'not.is.null' diff --git a/app/api/cross-checking/verify-document-access.ts b/app/api/cross-checking/verify-document-access.ts index 9078979..c6205fc 100644 --- a/app/api/cross-checking/verify-document-access.ts +++ b/app/api/cross-checking/verify-document-access.ts @@ -34,7 +34,7 @@ export async function verifyDocumentAccess( try { // 1. 检查文档是否属于该任务(通过 cross_task_document_mapping 表) - const documentMappingResponse = await postgrestGet('cross_task_document_mapping', { + const documentMappingResponse = await postgrestGet('/api/postgrest/proxy/cross_task_document_mapping', { select: 'task_id,document_id', filter: { task_id: `eq.${taskId}`, @@ -66,7 +66,7 @@ export async function verifyDocumentAccess( } // 2. 检查用户是否是该任务的参与者 - const taskResponse = await postgrestGet('cross_examination_tasks', { + const taskResponse = await postgrestGet('/api/postgrest/proxy/cross_examination_tasks', { select: 'assigner_id,assignee_ids', filter: { id: `eq.${taskId}` diff --git a/app/api/evaluation_points/reviews.ts b/app/api/evaluation_points/reviews.ts index 2929201..7fee200 100644 --- a/app/api/evaluation_points/reviews.ts +++ b/app/api/evaluation_points/reviews.ts @@ -158,7 +158,7 @@ export async function getReviewPoints(fileId: string, request: Request) { limit: 1, token: frontendJWT }; - const contractStructureComparisonResponse = await postgrestGet('contract_structure_comparison', contractStructureComparisonParams); + const contractStructureComparisonResponse = await postgrestGet('/api/postgrest/proxy/contract_structure_comparison', contractStructureComparisonParams); // console.log('contract_structure_comparison', contractStructureComparisonResponse) if (contractStructureComparisonResponse.error) { @@ -201,7 +201,7 @@ export async function getReviewPoints(fileId: string, request: Request) { }, token: frontendJWT }; - const evaluationResultsResponse = await postgrestGet('evaluation_results', evaluationResultsParams); + const evaluationResultsResponse = await postgrestGet('/api/postgrest/proxy/evaluation_results', evaluationResultsParams); // console.log('evaluationResultsResponse-------', evaluationResultsResponse,); if (evaluationResultsResponse.error) { @@ -230,7 +230,7 @@ export async function getReviewPoints(fileId: string, request: Request) { }, token: frontendJWT }; - const evaluationPointsResponse = await postgrestGet('evaluation_points', evaluationPointsParams); + const evaluationPointsResponse = await postgrestGet('/api/postgrest/proxy/evaluation_points', evaluationPointsParams); if (evaluationPointsResponse.error) { return { error: evaluationPointsResponse.error, status: evaluationPointsResponse.status }; @@ -257,7 +257,7 @@ export async function getReviewPoints(fileId: string, request: Request) { }, token: frontendJWT }; - const groupsResponse = await postgrestGet('evaluation_point_groups', groupsParams); + const groupsResponse = await postgrestGet('/api/postgrest/proxy/evaluation_point_groups', groupsParams); if (groupsResponse.error) { return { error: groupsResponse.error, status: groupsResponse.status }; @@ -281,7 +281,7 @@ export async function getReviewPoints(fileId: string, request: Request) { }, token: frontendJWT }; - const manualReviewPointsResponse = await postgrestGet('audit_status', manualReviewPointsParams); + const manualReviewPointsResponse = await postgrestGet('/api/postgrest/proxy/audit_status', manualReviewPointsParams); if (manualReviewPointsResponse.error) { return { error: manualReviewPointsResponse.error, status: manualReviewPointsResponse.status }; } @@ -336,7 +336,7 @@ export async function getReviewPoints(fileId: string, request: Request) { }, token: frontendJWT }; - const scoringProposalsResponse = await postgrestGet('cross_scoring_proposals', scoringProposalsParams); + const scoringProposalsResponse = await postgrestGet('/api/postgrest/proxy/cross_scoring_proposals', scoringProposalsParams); if (scoringProposalsResponse.error) { return { error: scoringProposalsResponse.error, status: scoringProposalsResponse.status }; @@ -776,12 +776,13 @@ export async function updateReviewResult( } // 首先获取当前评查结果数据 - const currentResultResponse = await postgrestGet('evaluation_results', { + 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 }; } @@ -812,7 +813,7 @@ export async function updateReviewResult( // 调用 API 更新评查点结果数据 const resultResponse = await postgrestPut( - 'evaluation_results', + '/api/postgrest/proxy/evaluation_results', updatedData, { id: resultId }, frontendJWT @@ -831,7 +832,7 @@ export async function updateReviewResult( if (editAuditStatusId && editAuditStatusId !== '') { // 更新现有审核状态记录 const auditStatusResponse = await postgrestPut( - 'audit_status', + '/api/postgrest/proxy/audit_status', { edit_audit_status: editAuditStatusValue, // 重新审核时不更新message @@ -864,7 +865,7 @@ export async function updateReviewResult( }; // 使用postgrestPost创建新记录 - const postResponse = await postgrestPost('audit_status', newAuditStatus, frontendJWT); + const postResponse = await postgrestPost('/api/postgrest/proxy/audit_status', newAuditStatus, frontendJWT); if (postResponse.error) { return { error: postResponse.error, status: postResponse.status || 500 }; @@ -938,7 +939,7 @@ export async function confirmReviewResults(documentId: string, request: Request) // 调用API更新文档审核状态 const response = await postgrestPut<{ id: string }, typeof updateDocumentParams>( - 'documents', + '/api/postgrest/proxy/documents', updateDocumentParams, { id: documentId, diff --git a/app/api/evaluation_points/rule-groups.ts b/app/api/evaluation_points/rule-groups.ts index 95d87cd..86e30e8 100644 --- a/app/api/evaluation_points/rule-groups.ts +++ b/app/api/evaluation_points/rule-groups.ts @@ -88,122 +88,6 @@ export interface RuleGroupQueryParams { token?: string; } -/** - * 获取评查点分组列表(支持分页、筛选、排序) - * @deprecated 使用 getEvaluationPointGroups 代替(FastAPI v3) - * @param params 查询参数 - * @returns 评查点分组列表和总数 - */ -export async function getRuleGroups_legacy( - params?: RuleGroupQueryParams -): Promise<{data: RuleGroup[]; totalCount?: number; error?: never} | {data?: never; error: string; status?: number}> { - try { - const { - page = 1, - pageSize = 50, - name, - code, - is_enabled, - pid = '0', // 默认获取一级分组 - orderBy = 'created_at', - order = 'desc', - token - } = params || {}; - - // 构建筛选条件 - const filter: Record = {}; - - // 父级ID筛选 (pid=null或'0'表示一级分组) - if (pid === null || pid === '0') { - filter['pid'] = 'eq.0'; - } else if (pid) { - filter['pid'] = `eq.${pid}`; - } - - // 名称模糊搜索 - if (name) { - filter['name'] = `ilike.*${name}*`; - } - - // 编码模糊搜索 - if (code) { - filter['code'] = `ilike.*${code}*`; - } - - // 状态筛选 - if (is_enabled !== undefined) { - filter['is_enabled'] = `eq.${is_enabled}`; - } - - const postgrestParams: PostgrestParams = { - select: ` - id, - pid, - name, - code, - description, - is_enabled, - created_at - `, - filter, - order: `${orderBy}.${order}`, // PostgREST order format: field.direction - limit: pageSize, - offset: (page - 1) * pageSize, - token - }; - - const response = await postgrestGet<{code: number; msg: string; data: Array<{ - id: number; - pid: number; - name: string; - code?: string; - description?: string; - is_enabled: boolean; - created_at?: string; - }>}>('evaluation_point_groups', postgrestParams); - - if (response.error) { - return { error: response.error, status: response.status }; - } - - // 处理响应数据 - let groups: RuleGroup[] = []; - if (response.data && 'code' in response.data && response.data.data) { - groups = response.data.data.map(group => ({ - id: group.id.toString(), - pid: group.pid.toString(), - name: group.name, - code: group.code, - description: group.description, - is_enabled: group.is_enabled, - createdAt: group.created_at ? formatDate(group.created_at) : undefined - })); - } else if (Array.isArray(response.data)) { - groups = response.data.map(group => ({ - id: group.id.toString(), - pid: group.pid.toString(), - name: group.name, - code: group.code, - description: group.description, - is_enabled: group.is_enabled, - createdAt: group.created_at ? formatDate(group.created_at) : undefined - })); - } - - // 注意:由于当前 PostgREST 客户端不支持 count 参数,totalCount 返回当前页的记录数 - // 后续可优化为单独查询获取准确的总数 - return { - data: groups, - totalCount: groups.length - }; - } catch (error) { - console.error('获取评查点分组列表失败:', error); - return { - error: error instanceof Error ? error.message : '获取评查点分组列表失败', - status: 500 - }; - } -} /** * 获取指定分组的子分组(包含评查点数量统计) @@ -241,204 +125,6 @@ export async function getChildGroups(parentId: string, token?: string): Promise< - -// ==================== 批量操作接口 ==================== - -/** - * 批量更新分组状态(启用/禁用) - * @deprecated 使用 batchUpdateEvaluationPointGroupStatus 代替(FastAPI v3) - * @param ids 分组ID列表 - * @param is_enabled 目标状态 - * @param token JWT token (可选) - * @returns 更新结果 - */ -export async function batchUpdateRuleGroupStatus_legacy( - ids: string[], - is_enabled: boolean, - token?: string -): Promise<{ - success: boolean; - updated_count: number; - failed_ids: string[]; - errors?: Array<{ id: string; error: string }>; -}> { - try { - // ========== 1. 参数验证 ========== - - if (!Array.isArray(ids) || ids.length === 0) { - return { - success: false, - updated_count: 0, - failed_ids: [], - errors: [{ id: 'validation', error: 'ID列表不能为空' }] - }; - } - - // 验证每个ID的有效性 - const invalidIds = ids.filter(id => !id || id.trim() === ''); - if (invalidIds.length > 0) { - return { - success: false, - updated_count: 0, - failed_ids: ids, - errors: [{ id: 'validation', error: '存在无效的分组ID' }] - }; - } - - // ========== 2. 逐个更新(确保每个分组都能被正确处理) ========== - - const failedIds: string[] = []; - const errors: Array<{ id: string; error: string }> = []; - let updatedCount = 0; - - for (const id of ids) { - try { - // 验证分组是否存在 - const groupResponse = await getRuleGroup(id, token); - if (groupResponse.error || !groupResponse.data) { - failedIds.push(id); - errors.push({ id, error: '分组不存在或无法访问' }); - continue; - } - - // 执行更新 - const updateResponse = await postgrestPut | RuleGroup, Partial>( - 'evaluation_point_groups', - { is_enabled }, - { id }, - token - ); - - if (updateResponse.error) { - failedIds.push(id); - errors.push({ id, error: updateResponse.error }); - } else { - updatedCount++; - } - } catch (error) { - failedIds.push(id); - errors.push({ - id, - error: error instanceof Error ? error.message : '更新失败' - }); - } - } - - // ========== 3. 返回结果 ========== - - return { - success: failedIds.length === 0, - updated_count: updatedCount, - failed_ids: failedIds, - errors: errors.length > 0 ? errors : undefined - }; - } catch (error) { - console.error('批量更新分组状态失败:', error); - return { - success: false, - updated_count: 0, - failed_ids: ids, - errors: [{ - id: 'batch', - error: error instanceof Error ? error.message : '批量更新失败' - }] - }; - } -} - -/** - * 批量删除分组(安全的阻止删除策略) - * @deprecated 使用 batchDeleteEvaluationPointGroups 代替(FastAPI v3) - * @param ids 分组ID列表 - * @param token JWT token (可选) - * @returns 删除结果 - */ -export async function batchDeleteRuleGroups_legacy( - ids: string[], - token?: string -): Promise<{ - success: boolean; - deleted_count: number; - failed_ids: string[]; - errors?: Array<{ id: string; error: string; details?: { hasChildren?: boolean; hasPoints?: boolean } }>; -}> { - try { - // ========== 1. 参数验证 ========== - - if (!Array.isArray(ids) || ids.length === 0) { - return { - success: false, - deleted_count: 0, - failed_ids: [], - errors: [{ id: 'validation', error: 'ID列表不能为空' }] - }; - } - - // 验证每个ID的有效性 - const invalidIds = ids.filter(id => !id || id.trim() === ''); - if (invalidIds.length > 0) { - return { - success: false, - deleted_count: 0, - failed_ids: ids, - errors: [{ id: 'validation', error: '存在无效的分组ID' }] - }; - } - - // ========== 2. 逐个删除(使用安全的阻止删除策略) ========== - - const failedIds: string[] = []; - const errors: Array<{ id: string; error: string; details?: { hasChildren?: boolean; hasPoints?: boolean } }> = []; - let deletedCount = 0; - - for (const id of ids) { - try { - const deleteResult = await deleteRuleGroup(id, token); - - if (!deleteResult.success) { - failedIds.push(id); - errors.push({ - id, - error: deleteResult.error || '删除失败', - details: deleteResult.details ? { - hasChildren: deleteResult.details.hasChildren, - hasPoints: deleteResult.details.hasPoints - } : undefined - }); - } else { - deletedCount++; - } - } catch (error) { - failedIds.push(id); - errors.push({ - id, - error: error instanceof Error ? error.message : '删除失败' - }); - } - } - - // ========== 3. 返回结果 ========== - - return { - success: failedIds.length === 0, - deleted_count: deletedCount, - failed_ids: failedIds, - errors: errors.length > 0 ? errors : undefined - }; - } catch (error) { - console.error('批量删除分组失败:', error); - return { - success: false, - deleted_count: 0, - failed_ids: ids, - errors: [{ - id: 'batch', - error: error instanceof Error ? error.message : '批量删除失败' - }] - }; - } -} - // ======================================== // FastAPI v3 接口函数(新版) // ======================================== diff --git a/app/api/evaluation_points/rules-files.ts b/app/api/evaluation_points/rules-files.ts index 05d719b..2109694 100644 --- a/app/api/evaluation_points/rules-files.ts +++ b/app/api/evaluation_points/rules-files.ts @@ -136,7 +136,7 @@ export async function updateDocumentAuditStatus(id: string, auditStatus: number, }); const response = await postgrestPut>( - 'documents', + '/api/postgrest/proxy/documents', { audit_status: auditStatus }, { id: parseInt(id), diff --git a/app/api/evaluation_points/rules.ts b/app/api/evaluation_points/rules.ts index 3c1c33f..c4a78f2 100644 --- a/app/api/evaluation_points/rules.ts +++ b/app/api/evaluation_points/rules.ts @@ -364,7 +364,7 @@ export async function getRule(id: string, token?: string): Promise<{data: Rule; }; // 获取评查点详情 - 使用正确的PostgREST格式 - const response = await postgrestGet<{code: number; msg: string; data: ApiRule} | ApiRule[]>('evaluation_points', postgrestParams); + const response = await postgrestGet<{code: number; msg: string; data: ApiRule} | ApiRule[]>('/api/postgrest/proxy/evaluation_points', postgrestParams); // 检查是否有错误响应 if (response.error) { @@ -401,7 +401,7 @@ export async function getRule(id: string, token?: string): Promise<{data: Rule; }; // 查询评查点分组 - const groupResponse = await postgrestGet<{code: number; msg: string; data: {id: number; name: string}[]}>('evaluation_point_groups', groupParams); + const groupResponse = await postgrestGet<{code: number; msg: string; data: {id: number; name: string}[]}>('/api/postgrest/proxy/evaluation_point_groups', groupParams); if (groupResponse.data?.data && groupResponse.data.data.length > 0) { // 将分组信息添加到评查点数据中 @@ -480,7 +480,7 @@ export async function createRule(ruleData: Omit }>('evaluation_point_groups', { + 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 @@ -524,7 +524,7 @@ export async function createRule(ruleData: Omit('evaluation_points', apiRuleData, token); + const response = await postgrestPost<{code: number; msg: string; data: ApiRule}, typeof apiRuleData>('/api/postgrest/proxy/evaluation_points', apiRuleData, token); // 检查是否有错误响应 if (response.error) { @@ -599,7 +599,7 @@ export async function updateRule(id: string, ruleData: Partial }>('evaluation_point_groups', { + 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 @@ -645,7 +645,7 @@ export async function updateRule(id: string, ruleData: Partial('evaluation_points', apiRuleData, { id: parseInt(id) }, token); + 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) { @@ -816,7 +816,7 @@ export async function getRuleTypes(documentTypeIds?: number[], token?: string): name: string; evaluation_point_groups_ids: number[]; }> - }>('document_types', documentTypesParams); + }>('/api/postgrest/proxy/document_types', documentTypesParams); if (documentTypesResponse.error) { return { error: documentTypesResponse.error, status: documentTypesResponse.status }; @@ -890,7 +890,7 @@ export async function getRuleTypes(documentTypeIds?: number[], token?: string): description: string; is_enabled: boolean; }> - }>('evaluation_point_groups', groupsParams); + }>('/api/postgrest/proxy/evaluation_point_groups', groupsParams); // 检查是否有错误响应 if (response.error) { @@ -975,7 +975,7 @@ export async function getRuleGroupsByType(typeId: string, token?: string): Promi description: string; is_enabled: boolean; }>; - }>('evaluation_point_groups', postgrestParams); + }>('/api/postgrest/proxy/evaluation_point_groups', postgrestParams); // 检查是否有错误响应 if (response.error) { @@ -1169,7 +1169,7 @@ export async function getFormattedEvaluationPoint(id: number): Promise<{ } }; - const response = await postgrestGet<{code: number; msg: string; data: ApiRule[]} | ApiRule[]>('evaluation_points', postgrestParams); + const response = await postgrestGet<{code: number; msg: string; data: ApiRule[]} | ApiRule[]>('/api/postgrest/proxy/evaluation_points', postgrestParams); if (response.error) { return { @@ -1238,7 +1238,7 @@ export async function getEvaluationPointGroups(): Promise<{ updated_at?: string; }; - const response = await postgrestGet<{code: number; msg: string; data: EvaluationPointGroupType[]} | EvaluationPointGroupType[]>('evaluation_point_groups', postgrestParams); + const response = await postgrestGet<{code: number; msg: string; data: EvaluationPointGroupType[]} | EvaluationPointGroupType[]>('/api/postgrest/proxy/evaluation_point_groups', postgrestParams); if (response.error) { return { @@ -1480,14 +1480,14 @@ export async function saveEvaluationPoint(evaluationPoint: EvaluationPointInput, if (isEditMode) { // 更新操作 response = await postgrestPut<{code: number; msg: string; data: ApiRule} | ApiRule, typeof cleanedData>( - `evaluation_points`, + `/api/postgrest/proxy/evaluation_points`, cleanedData, {id: cleanedData.id!} ); } else { // 创建操作 response = await postgrestPost<{code: number; msg: string; data: ApiRule} | ApiRule, typeof cleanedData>( - 'evaluation_points', + '/api/postgrest/proxy/evaluation_points', cleanedData ); } @@ -1566,7 +1566,7 @@ export async function getRuleStatistics(token?: string): Promise<{data: RuleStat is_enabled: boolean; risk: string; evaluation_point_groups_id: number | null; - }>}>('evaluation_points', postgrestParams); + }>}>('/api/postgrest/proxy/evaluation_points', postgrestParams); // 检查是否有错误响应 if (response.error) { @@ -1628,7 +1628,7 @@ export async function getRuleStatistics(token?: string): Promise<{data: RuleStat token }; - const groupsResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number; name: string}>}>('evaluation_point_groups', groupsParams); + 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) { diff --git a/app/api/files/documents.ts b/app/api/files/documents.ts index 8590f5f..ac7e95e 100644 --- a/app/api/files/documents.ts +++ b/app/api/files/documents.ts @@ -145,7 +145,7 @@ function getFileExtension(filename: string): string { * @returns 评查结果 */ async function getEvaluationResults(id: number, frontendJWT?: string) { - const response = await postgrestGet<[]>('evaluation_results', { + const response = await postgrestGet<[]>('/api/postgrest/proxy/evaluation_results', { filter: { 'document_id': `eq.${id}` }, @@ -254,7 +254,7 @@ export async function deleteDocument(id: string, userId: string, token?: string) } const response = await postgrestDelete( - 'documents', + '/api/postgrest/proxy/documents', { filter: { 'id': `eq.${id}`, @@ -298,7 +298,7 @@ export async function getDocument(id: string, userId: string, frontendJWT?: stri } const response = await postgrestGet( - 'documents', + '/api/postgrest/proxy/documents', { filter: { 'id': `eq.${id}`, @@ -349,7 +349,7 @@ export async function getDocumentWithNoUserId(id: string, frontendJWT?: string): // console.log("get单个文档id", id) const response = await postgrestGet( - 'documents', + '/api/postgrest/proxy/documents', { filter: { 'id': `eq.${id}`, @@ -430,7 +430,7 @@ export async function updateDocument(id: string, document: Partial & // console.log('更新文档API数据:', apiDocument); const response = await postgrestPut>( - 'documents', + '/api/postgrest/proxy/documents', apiDocument, { id: parseInt(id), @@ -653,101 +653,3 @@ export async function getDocumentsListFromAPI(searchParams: { }; } } - -/** - * 获取文档历史版本列表 - * @param documentName 文档名称 - * @param userId 用户ID - * @param excludeId 排除的文档ID(当前最新版本的ID) - * @param token JWT token - * @returns 历史版本列表 - */ -export async function getDocumentHistory( - documentName: string, - userId: string, - excludeId: number, - token?: string -): Promise<{ - data?: DocumentVersionUI[]; - error?: string; - status?: number; -}> { - try { - if (!documentName) { - return { error: '文档名称不能为空', status: 400 }; - } - - if (!userId) { - return { error: '用户身份验证失败', status: 401 }; - } - - // 调用 RPC 函数获取历史版本 - const response = await postgrestPost( - 'rpc/documents_get_document_history', - { - p_document_name: documentName, - p_user_id: parseInt(userId, 10), - p_exclude_id: excludeId - }, - token - ); - - if (response.error || !response.data) { - return { error: response.error || '获取历史版本失败', status: response.status || 500 }; - } - - const historyDocs = response.data; - - // 转换为 UI 格式,并计算问题数量差异 - const documents: DocumentVersionUI[] = historyDocs.map((doc: any, index: number) => { - // 计算与下一个版本(更早的版本)的问题数量差异 - let issuesDiff: number | undefined; - let issuesDiffType: 'increase' | 'decrease' | 'same' | undefined; - - if (index < historyDocs.length - 1) { - const olderDoc = historyDocs[index + 1]; - if (doc.false_count != null && olderDoc.false_count != null) { - const diff = doc.false_count - olderDoc.false_count; - issuesDiff = Math.abs(diff); - if (diff > 0) { - issuesDiffType = 'increase'; - } else if (diff < 0) { - issuesDiffType = 'decrease'; - } else { - issuesDiffType = 'same'; - } - } - } - - return { - id: doc.id, - name: doc.name, - documentNumber: doc.document_number, - type: doc.type_id.toString(), - typeName: doc.type_name || '未知类型', - size: doc.file_size, - auditStatus: doc.audit_status ?? 0, - fileStatus: doc.status || '', - issues: doc.false_count ?? null, - issuesDiff, - issuesDiffType, - uploadTime: formatDate(doc.created_at), - fileType: getFileExtension(doc.name), - path: doc.path, - isTest: doc.is_test_document, - updatedAt: formatDate(doc.updated_at), - pageCount: doc.ocr_result?.__meta?.page_count || 0, - ocrResult: doc.ocr_result, - versionNumber: historyDocs.length - index - }; - }); - - return { data: documents }; - } catch (error) { - console.error('获取文档历史版本失败:', error); - return { - error: error instanceof Error ? error.message : '获取文档历史版本失败', - status: 500 - }; - } -} \ No newline at end of file diff --git a/app/api/files/files-upload.ts b/app/api/files/files-upload.ts index b457338..89ffe83 100644 --- a/app/api/files/files-upload.ts +++ b/app/api/files/files-upload.ts @@ -497,7 +497,7 @@ export async function getTodayDocuments( } // console.log('发送请求参数:', params); - const response = await postgrestGet('documents', { ...params, token }); + const response = await postgrestGet('/api/postgrest/proxy/documents', { ...params, token }); // console.log('API 响应:', response); if (response.error) { @@ -552,7 +552,7 @@ export async function getDocumentTypes(token?: string): Promise<{data: DocumentT } // 如果没有 documentTypeIds,返回所有文档类型(不添加过滤条件) - const response = await postgrestGet('document_types', { ...params, token }); + const response = await postgrestGet('/api/postgrest/proxy/ocument_types', { ...params, token }); if (response.error) { return { error: response.error, status: response.status }; @@ -600,7 +600,7 @@ export async function getDocumentsStatus( 'id': `in.(${documentIds.join(',')})` } }; - documentsResponse = await postgrestGet('documents', { ...documentsParams, token }); + documentsResponse = await postgrestGet('/api/postgrest/proxy/documents', { ...documentsParams, token }); } // 查询合同附件状态 @@ -613,7 +613,7 @@ export async function getDocumentsStatus( 'id': `in.(${attachmentIds.join(',')})` } }; - attachmentResponse = await postgrestGet('contract_structure_comparison', { ...attachmentParams, token }); + attachmentResponse = await postgrestGet('/api/postgrest/proxy/contract_structure_comparison', { ...attachmentParams, token }); } if (documentsResponse.error && attachmentResponse.error) { diff --git a/app/api/home/home.ts b/app/api/home/home.ts index e2be60b..cf46220 100644 --- a/app/api/home/home.ts +++ b/app/api/home/home.ts @@ -244,7 +244,7 @@ export async function getEntryModules(userRole: string | null | undefined, userA filter: {} }; - const modulesResponse = await postgrestGet('entry_modules', { ...params, token }); + const modulesResponse = await postgrestGet('/api/postgrest/proxy/entry_modules', { ...params, token }); if (modulesResponse.error) { console.error('❌ [getEntryModules] 查询入口模块失败:', modulesResponse.error); @@ -295,7 +295,7 @@ export async function getEntryModules(userRole: string | null | undefined, userA } }; - const typesResponse = await postgrestGet('document_types', { ...typesParams, token }); + const typesResponse = await postgrestGet('/api/postgrest/proxy/document_types', { ...typesParams, token }); if (typesResponse.error) { console.error(`❌ [getEntryModules] 查询模块 ${module.id} 的文档类型失败:`, typesResponse.error); diff --git a/app/api/login/auth.server.ts b/app/api/login/auth.server.ts index c10563a..604769d 100644 --- a/app/api/login/auth.server.ts +++ b/app/api/login/auth.server.ts @@ -530,247 +530,4 @@ async function callIDaaSLogout(accessToken: string, appId: string): Promise - */ -export async function saveUserInfo( - userInfo: UserInfo, - userRole: UserRole, - tokenExpiresIn: number, - area?: string -): Promise<{success: boolean, data?: SsoUser, tempToken?: string, error?: string}> { - try { - console.log("开始保存用户信息", userInfo); - - // 验证必要字段 - if (!userInfo.sub) { - return { success: false, error: "用户唯一标识 sub 不能为空" }; - } - - // 🔒 安全:在服务端生成临时 JWT,user_id 使用占位符 'login' - // 这样客户端无法看到真实的 user_id - const tempUserInfo: UserInfoForJWT = { - sub: userInfo.sub, - user_id: 'login', // 使用占位符,避免在客户端暴露真实ID - username: 'login', - nick_name: userInfo.nick_name || userInfo.nickname || userInfo.name || "未知用户", - email: userInfo.email, - phone_number: userInfo.phone_number, - ou_id: userInfo.ou_id || "default", - ou_name: userInfo.ou_name || "未知部门", - is_leader: userInfo.is_leader || false, - user_role: userRole - }; - - const tempToken = JWTUtils.generateJWT(tempUserInfo, tokenExpiresIn); - - // 1. 根据 sub 查询是否已存在该用户 - const existingUserResult = await postgrestGet("sso_users", { - filter: { - "sub": `eq.${userInfo.sub}`, - "deleted_at": "is.null" // 只查询未删除的记录 - }, - token: tempToken - }); - - if (existingUserResult.error) { - console.error("查询用户失败:", existingUserResult.error); - return { success: false, error: `查询用户失败: ${existingUserResult.error}` }; - } - - const existingUsers = existingUserResult.data || []; - const existingUser = existingUsers.length > 0 ? existingUsers[0] : null; - - // 准备要保存的用户数据 - // 注意:OAuth返回的字段是nickname,而不是nick_name - const userData: Partial = { - sub: userInfo.sub, - username: userInfo.username || userInfo.name || userInfo.sub, - nick_name: userInfo.nick_name || userInfo.nickname || userInfo.name || "未知用户", - phone_number: userInfo.phone_number || undefined, - email: userInfo.email || undefined, - ou_id: userInfo.ou_id || "default", - ou_name: userInfo.ou_name || "未知部门", - status: userInfo.status !== undefined ? userInfo.status : 0, - is_leader: userInfo.is_leader || false, - }; - - if (existingUser) { - // 2. 用户已存在,执行更新操作 - console.log("用户已存在,执行更新操作", existingUser.id); - - // 只有在现有用户没有 area 或 area 为空时,才更新 area - if (area && !existingUser.area) { - userData.area = area; - console.log("用户原本无地区信息,更新地区为:", area); - } else if (existingUser.area) { - console.log("用户已有地区信息:", existingUser.area, "不更新"); - } - - const updateResult = await postgrestPut>( - "sso_users", - userData, - { id: existingUser.id! }, - tempToken - ); - - if (updateResult.error) { - console.error("更新用户失败:", updateResult.error); - return { success: false, error: `更新用户失败: ${updateResult.error}` }; - } - - console.log("用户信息更新成功"); - return { - success: true, - data: Array.isArray(updateResult.data) ? updateResult.data[0] : updateResult.data as unknown as SsoUser, - tempToken // 返回临时 JWT - }; - } else { - // 3. 用户不存在,执行插入操作,设置地区信息 - console.log("用户不存在,执行插入操作"); - - // 新用户直接设置 area - if (area) { - userData.area = area; - console.log("新用户,设置地区为:", area); - } - - const insertResult = await postgrestPost("sso_users", userData as SsoUser, tempToken); - - if (insertResult.error) { - console.error("插入用户失败:", insertResult.error); - return { success: false, error: `插入用户失败: ${insertResult.error}` }; - } - - console.log("用户信息插入成功"); - - // 4. 给这个用户默认添加一个角色,角色为common - const userData_with_id = Array.isArray(insertResult.data) ? insertResult.data[0] : insertResult.data as unknown as SsoUser; - if (userData_with_id?.id) { - await addDefaultRole(userData_with_id.id, 2, tempToken); - } - - return { - success: true, - data: userData_with_id, - tempToken // 返回临时 JWT - }; - } - } catch (error) { - console.error("保存用户信息时发生错误:", error); - return { - success: false, - error: `保存用户信息失败: ${error instanceof Error ? error.message : String(error)}` - }; - } -} - -/** - * 为用户添加默认角色 - * - * @param userId - 用户ID - * @param roleId - 角色ID,默认为2(common角色) - * @param token - JWT令牌,用于调用postgrest服务 - * @returns 添加结果 - */ -export async function addDefaultRole(userId: string, roleId: number = 2, token?: string) { - try { - console.log(`为用户 ${userId} 添加默认角色 ${roleId}`); - - // 检查用户是否已经有此角色 - const existingRoleResult = await postgrestGet>("user_role", { - filter: { - user_id: `eq.${userId}`, - role_id: `eq.${roleId}` - }, - token - }); - - if (existingRoleResult.error) { - console.error("查询用户角色失败:", existingRoleResult.error); - return { success: false, error: `查询用户角色失败: ${existingRoleResult.error}` }; - } - - const existingRoles = existingRoleResult.data || []; - if (existingRoles.length > 0) { - console.log("用户已经拥有此角色,跳过添加"); - return { success: true, data: existingRoles[0] }; - } - - // 添加角色 - const addRoleResult = await postgrestPost, {user_id: string, role_id: number}>("user_role", { - user_id: userId, - role_id: roleId - }, token); - - if (addRoleResult.error) { - console.error("添加用户角色失败:", addRoleResult.error); - return { success: false, error: `添加用户角色失败: ${addRoleResult.error}` }; - } - - console.log("用户角色添加成功"); - return { - success: true, - data: Array.isArray(addRoleResult.data) ? addRoleResult.data[0] : addRoleResult.data - }; - } catch (error) { - console.error("添加用户角色时发生错误:", error); - return { - success: false, - error: `添加用户角色失败: ${error instanceof Error ? error.message : String(error)}` - }; - } -} - -/** - * 通过用户sub获取用户信息 - * - * @param sub - 用户的唯一标识 - * @returns 用户信息 - */ -export async function getUserBySub(sub: string) { - try { - // console.log(`查询用户: ${sub}`); - - const userResult = await postgrestGet("sso_users", { - filter: { - sub: `eq.${sub}` - } - }); - - if (userResult.error) { - console.error("查询用户失败:", userResult.error); - return { success: false, error: `查询用户失败: ${userResult.error}` }; - } - - const users = userResult.data || []; - const user = users.length > 0 ? users[0] : null; - - if (!user) { - return { success: false, error: "用户不存在" }; - } - - return { success: true, data: user }; - } catch (error) { - console.error("查询用户时发生错误:", error); - return { - success: false, - error: `查询用户失败: ${error instanceof Error ? error.message : String(error)}` - }; - } -} +} \ No newline at end of file diff --git a/app/api/storage/minio-client.ts b/app/api/storage/minio-client.ts new file mode 100644 index 0000000..cdea77c --- /dev/null +++ b/app/api/storage/minio-client.ts @@ -0,0 +1,159 @@ +/** + * MinIO 存储管理 API 客户端 + * 基于 /api/v2/storage 接口 + */ + +import { apiRequest } from '../axios-client'; + +export interface CopyFileRequest { + source_path: string; + destination_path: string; + source_bucket?: string | null; + destination_bucket?: string | null; +} + +export interface CopyFileResponse { + success: boolean; + message: string; + source_path: string; + destination_path: string; +} + +export interface MoveFileRequest { + source_path: string; + destination_path: string; +} + +export interface MoveFileResponse { + success: boolean; + message: string; + source_path: string; + destination_path: string; +} + +export interface DeleteFileRequest { + file_path: string; +} + +export interface DeleteFileResponse { + success: boolean; + message: string; + file_path: string; +} + +/** + * 复制文件 + * @param request 复制文件请求参数 + * @param token JWT token(可选) + * @returns 复制结果 + */ +export async function copyFile(request: CopyFileRequest, token?: string) { + const response = await apiRequest( + '/api/v2/storage/files/copy', + { + method: 'POST', + data: request, + headers: { + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + } + } + ); + + if (response.error) { + throw new Error(response.error); + } + + return response.data; +} + +/** + * 移动/重命名文件 + * @param request 移动文件请求参数 + * @param token JWT token(可选) + * @returns 移动结果 + */ +export async function moveFile(request: MoveFileRequest, token?: string) { + const response = await apiRequest( + '/api/v2/storage/files/move', + { + method: 'POST', + data: request, + headers: { + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + } + } + ); + + if (response.error) { + throw new Error(response.error); + } + + return response.data; +} + +/** + * 删除文件 + * @param request 删除文件请求参数 + * @param token JWT token(可选) + * @returns 删除结果 + */ +export async function deleteFile(request: DeleteFileRequest, token?: string) { + const response = await apiRequest( + '/api/v2/storage/files', + { + method: 'DELETE', + headers: { + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + } + }, + { file_path: request.file_path } + ); + + if (response.error) { + throw new Error(response.error); + } + + return response.data; +} + +/** + * 生成新的文件路径(用于复制模板文件) + * @param originalPath 原始文件路径 + * @param area 地区代码 + * @returns 新文件路径 + */ +export function generateDraftFilePath(originalPath: string, area: string): string { + // 提取文件目录和文件名 + const lastSlashIndex = originalPath.lastIndexOf('/'); + const directory = lastSlashIndex >= 0 ? originalPath.substring(0, lastSlashIndex) : ''; + const fileName = lastSlashIndex >= 0 ? originalPath.substring(lastSlashIndex + 1) : originalPath; + + // 提取文件扩展名 + const lastDotIndex = fileName.lastIndexOf('.'); + const baseName = lastDotIndex >= 0 ? fileName.substring(0, lastDotIndex) : fileName; + const extension = lastDotIndex >= 0 ? fileName.substring(lastDotIndex) : ''; + + // 生成时间戳和 UUID + const timestamp = Date.now(); + const uuid = generateUUID(); + + // 构建新文件名:原文件名_地区_时间戳_uuid.扩展名 + const newFileName = `${baseName}_${area}_${timestamp}_${uuid}${extension}`; + + // 构建完整路径 + const newPath = directory ? `${directory}/${newFileName}` : newFileName; + + return newPath; +} + +/** + * 生成简单的 UUID(不依赖外部库) + * @returns UUID 字符串 + */ +function generateUUID(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} diff --git a/app/components/collabora/lib/Highlightselecttext.ts b/app/components/collabora/lib/Highlightselecttext.ts index b9ccab8..96b0ff9 100644 --- a/app/components/collabora/lib/Highlightselecttext.ts +++ b/app/components/collabora/lib/Highlightselecttext.ts @@ -70,6 +70,7 @@ export async function highlightText( options?: HighlightOptions ): Promise { const color = options?.color ?? 16776960; // 默认黄色 + // const page = options?.page ?? 1; // 默认第1页 const page = options?.page ?? null; // 默认第1页 console.log('[HighlightSelectText] 调用 Python 脚本高亮文本:', { diff --git a/app/components/contracts/PlaceholderForm.tsx b/app/components/contracts/PlaceholderForm.tsx index 662c27b..07eba70 100644 --- a/app/components/contracts/PlaceholderForm.tsx +++ b/app/components/contracts/PlaceholderForm.tsx @@ -5,16 +5,14 @@ import { useState, useEffect } from 'react'; import type { PlaceholderSchema } from '~/types/contract-draft'; +import { messageService } from '~/components/ui/MessageModal'; interface PlaceholderFormProps { schema: PlaceholderSchema | null; values: Record; onChange: (values: Record) => void; - onBatchReplace: () => void; - onExportDocument: () => void; // 改名:导出文档 onComplete: () => void; - isReplacing: boolean; - isDeleting: boolean; // 改名:是否正在删除 + isDeleting: boolean; // 是否正在删除 onSingleReplace?: (key: string, value: string) => void; // 单个替换 onFieldFocus?: (key: string) => void; // 字段聚焦(高亮) } @@ -23,10 +21,7 @@ export function PlaceholderForm({ schema, values, onChange, - onBatchReplace, - onExportDocument, onComplete, - isReplacing, isDeleting, onSingleReplace, onFieldFocus @@ -81,7 +76,19 @@ export function PlaceholderForm({ const handleCompleteClick = () => { const missing = getMissingRequiredFields(); if (missing.length > 0) { - alert(`请填写以下必填字段:\n${missing.join('\n')}`); + messageService.show({ + type: 'warning', + title: '字段校验失败', + message: '请填写以下必填字段:', + children: ( +
    + {missing.map((field, index) => ( +
  • {field}
  • + ))} +
+ ), + confirmText: '确定' + }); return; } onComplete(); @@ -106,11 +113,36 @@ export function PlaceholderForm({
{/* 表单头部 */}
-
-
- +
+
+
+ +
+

填写合同信息

-

填写合同信息

+ + {/* 完成按钮 */} +
@@ -175,53 +207,6 @@ export function PlaceholderForm({ ))}
- - {/* 操作按钮区域(固定在底部) */} -
- - - -
); } diff --git a/app/routes/contract-draft.$draftId.tsx b/app/routes/contract-draft.$draftId.tsx index 3577512..78aa5b5 100644 --- a/app/routes/contract-draft.$draftId.tsx +++ b/app/routes/contract-draft.$draftId.tsx @@ -11,7 +11,9 @@ import { FilePreview, type FilePreviewHandle } from '~/components/reviews/FilePr import { PlaceholderForm } from '~/components/contracts/PlaceholderForm'; import { getDraftById, deleteDraft } from '~/api/contracts/draft-service.server'; import { getUserSession } from '~/api/login/auth.server'; +import { deleteFile } from '~/api/storage/minio-client'; import { toastService } from '~/components/ui/Toast'; +import { messageService } from '~/components/ui/MessageModal'; import { downloadFile } from '~/api/axios-client'; import { extractPlaceholdersFromDocx, generateDefaultSchema } from '~/api/contracts/docx-parser.server'; import path from 'path'; @@ -51,16 +53,28 @@ export async function loader({ params, request }: LoaderFunctionArgs) { const jwt = frontendJWT || undefined; - // 【临时测试】使用测试文档和模拟数据 - // const testDocPath = path.join(process.cwd(), 'public', 'testWork', '买卖合同 (1).docx'); - const testDocPath = 'contract-template/买卖/买卖合同范本.docx'; + // 从 URL 参数获取文件路径、模板 ID 和标题 + const url = new URL(request.url); + const filePath = url.searchParams.get('filePath'); + const templateId = url.searchParams.get('templateId'); + const title = url.searchParams.get('title'); - // 创建临时的草稿对象(用于测试) + if (!filePath) { + throw new Response('文件路径参数缺失', { status: 400 }); + } + + if (!templateId) { + throw new Response('模板ID参数缺失', { status: 400 }); + } + + console.log('[Loader] 起草合同:', { filePath, templateId, title }); + + // 创建草稿对象 const draft: DraftedContract = { id: draftId, - template_id: 1, - file_path: testDocPath, - title: '买卖合同-测试草稿', + template_id: parseInt(templateId), + file_path: filePath, + title: title || '未命名合同', placeholder_values: {}, status: 'draft', created_by: parseInt(userInfo.sub), @@ -72,10 +86,10 @@ export async function loader({ params, request }: LoaderFunctionArgs) { let placeholderSchema: PlaceholderSchema | null = null; try { - console.log('[Loader] 使用测试文档:', testDocPath); + console.log('[Loader] 使用文件:', filePath); // 提取占位符 - const placeholders = await extractPlaceholdersFromDocx(testDocPath); + const placeholders = await extractPlaceholdersFromDocx(filePath); console.log('[Loader] 提取到的占位符:', placeholders); // 生成默认 schema @@ -86,15 +100,15 @@ export async function loader({ params, request }: LoaderFunctionArgs) { placeholderSchema = null; } - // 创建临时的模板对象(用于测试) + // 创建模板对象 const template: ContractTemplate = { - id: 1, - title: '买卖合同模板', - template_code: 'TEST-001', + id: parseInt(templateId), + title: title || '合同模板', + template_code: 'DRAFT-' + templateId, category_id: 1, - file_path: testDocPath, + file_path: filePath, file_format: 'docx', - description: '测试用买卖合同模板', + description: '起草中的合同', is_featured: false, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), @@ -103,12 +117,13 @@ export async function loader({ params, request }: LoaderFunctionArgs) { return Response.json({ draft, - template + template, + returnUrl: `/contract-template/detail/${templateId}` }); } /** - * Action 函数:处理删除草稿 + * Action 函数:处理文件删除 */ export async function action({ request, params }: ActionFunctionArgs) { const draftId = parseInt(params.draftId || '0'); @@ -123,19 +138,26 @@ export async function action({ request, params }: ActionFunctionArgs) { return Response.json({ error: '未登录' }, { status: 401 }); } - const userId = parseInt(userInfo.sub); const jwt = frontendJWT || undefined; try { - // 解析表单数据 const formData = await request.formData(); const actionType = formData.get('_action') as string; - if (actionType === 'delete') { - // 删除草稿记录 - await deleteDraft(draftId, userId, jwt); + if (actionType === 'deleteFile') { + const filePath = formData.get('filePath') as string; - return Response.json({ success: true, message: '草稿已删除' }); + if (!filePath) { + return Response.json({ error: '文件路径缺失' }, { status: 400 }); + } + + // 删除 MinIO 文件,传递 JWT + await deleteFile({ file_path: filePath }, jwt); + + return Response.json({ + success: true, + message: '文件删除成功' + }); } return Response.json({ error: '无效的操作类型' }, { status: 400 }); @@ -149,14 +171,14 @@ export async function action({ request, params }: ActionFunctionArgs) { } export default function ContractDraftPage() { - const { draft, template } = useLoaderData(); + const loaderData = useLoaderData(); + const { draft, template, returnUrl } = loaderData; const navigate = useNavigate(); const fetcher = useFetcher(); const [placeholderValues, setPlaceholderValues] = useState>( draft.placeholder_values || {} ); - const [isReplacing, setIsReplacing] = useState(false); const [highlightValue, setHighlightValue] = useState(undefined); const [aiSuggestionReplace, setAiSuggestionReplace] = useState<{ searchText: string; @@ -165,56 +187,88 @@ export default function ContractDraftPage() { } | undefined>(undefined); const filePreviewRef = useRef(null); + const hasDeletedNormally = useRef(false); // 标记是否已通过正常流程删除 + const currentPathRef = useRef(window.location.pathname); // 保存当前路由路径 // 从 fetcher.state 判断是否正在操作 const isDeleting = fetcher.state !== 'idle'; - // 处理 fetcher 响应(删除草稿) + // 处理 fetcher 响应(文件删除) useEffect(() => { - if (fetcher.data?.success && fetcher.data.message === '草稿已删除') { - // 删除成功,跳转到模板列表 - navigate('/contract-template'); + if (fetcher.data?.success) { + // 标记已通过正常流程删除 + hasDeletedNormally.current = true; + + // 文件删除成功,显示提示并跳转 + // toastService.success('合同已完成'); + + // 延迟跳转,让用户看到成功提示 + setTimeout(() => { + if (returnUrl) { + navigate(returnUrl); + } else { + navigate('/contract-template'); + } + }, 500); } else if (fetcher.data?.error) { toastService.error(fetcher.data.error); } - }, [fetcher.data, navigate]); + }, [fetcher.data, navigate, returnUrl]); - // 监听页面关闭事件 - 自动删除草稿 + // 页面卸载或路由切换时清理文件 useEffect(() => { - const handleBeforeUnload = (e: BeforeUnloadEvent) => { - // 发送删除请求(使用 sendBeacon 确保请求发送) - const formData = new FormData(); - formData.append('_action', 'delete'); + const deleteFileSync = () => { + // 如果已经通过正常流程删除,不再重复删除 + if (hasDeletedNormally.current || !draft.file_path) { + return; + } - navigator.sendBeacon( - `/contract-draft/${draft.id}`, - formData - ); + // console.log('[Cleanup] 尝试删除文件:', draft.file_path); + // console.log('[Cleanup] 使用路径:', currentPathRef.current); + + try { + // 直接使用同步 XMLHttpRequest 确保删除请求真正执行 + // 使用保存的原始路径,而不是当前的 window.location.pathname(可能已切换) + const xhr = new XMLHttpRequest(); + xhr.open('POST', currentPathRef.current, false); // false = 同步请求 + + const formData = new FormData(); + formData.append('_action', 'deleteFile'); + formData.append('filePath', draft.file_path); + + xhr.send(formData); + // console.log('[Cleanup] 同步删除完成,状态:', xhr.status); + + if (xhr.status === 200) { + try { + const response = JSON.parse(xhr.responseText); + // console.log('[Cleanup] ✅ 文件删除成功:', response); + } catch (e) { + // console.log('[Cleanup] ✅ 文件删除成功(状态码200)'); + } + } else { + console.error('[Cleanup] ❌ 文件删除失败,状态码:', xhr.status); + } + } catch (error) { + console.error('[Cleanup] 删除文件失败:', error); + } }; + const handleBeforeUnload = () => { + deleteFileSync(); + }; + + // 监听页面卸载事件 window.addEventListener('beforeunload', handleBeforeUnload); + // 清理事件监听器 return () => { window.removeEventListener('beforeunload', handleBeforeUnload); - }; - }, [draft.id]); - // 组件卸载时删除草稿(处理路由跳转的情况) - useEffect(() => { - return () => { - // 组件卸载时删除草稿记录 - const formData = new FormData(); - formData.append('_action', 'delete'); - - fetch(`/contract-draft/${draft.id}`, { - method: 'POST', - body: formData, - keepalive: true // 确保请求在页面关闭后仍然发送 - }).catch(err => { - console.error('[Draft] 删除草稿失败:', err); - }); + // 组件卸载时(路由切换),执行删除 + deleteFileSync(); }; - }, [draft.id]); + }, [draft.file_path]); // 单个替换占位符 const handleSingleReplace = async (key: string, value: string) => { @@ -231,7 +285,7 @@ export default function ContractDraftPage() { // 短暂延迟后清除参数,以便下次可以重新触发 setTimeout(() => { setAiSuggestionReplace(undefined); - toastService.success(`已替换 ${key}`); + // toastService.success(`已替换 ${key}`); }, 1000); }; @@ -249,52 +303,7 @@ export default function ContractDraftPage() { }, 100); }; - // 批量替换占位符 - const handleBatchReplace = async () => { - setIsReplacing(true); - - try { - // 获取 CollaboraViewer 引用 - const collaboraRef = filePreviewRef.current?.collaboraViewerRef.current; - - if (!collaboraRef?.isReady) { - toastService.warning('文档尚未加载完成,请稍候...'); - setIsReplacing(false); - return; - } - - console.log('[Draft] 开始批量替换占位符:', placeholderValues); - - // 批量替换所有占位符 - let replaceCount = 0; - for (const [key, value] of Object.entries(placeholderValues)) { - if (value) { // 只替换有值的字段 - const placeholder = `{{${key}}}`; - console.log(`[Draft] 替换: ${placeholder} -> ${value}`); - - // 调用 unoCommands.replaceAll 方法 - if (collaboraRef.unoCommands?.replaceAll) { - await collaboraRef.unoCommands.replaceAll(placeholder, value); - replaceCount++; - // 添加延迟避免 Collabora 响应不过来 - await new Promise(resolve => setTimeout(resolve, 100)); - } else { - console.warn('[Draft] unoCommands.replaceAll 方法不可用'); - } - } - } - - console.log(`[Draft] 替换完成,共替换 ${replaceCount} 个占位符`); - toastService.success(`占位符替换完成(${replaceCount}个)`); - } catch (error) { - console.error('[Draft] 替换失败:', error); - toastService.error(`替换失败: ${error instanceof Error ? error.message : '未知错误'}`); - } finally { - setIsReplacing(false); - } - }; - - // 导出文档(下载文件) + // 导出文档(下载当前编辑的文件) const handleExportDocument = async () => { if (!draft.file_path) { toastService.error('文件路径不存在,无法下载'); @@ -304,7 +313,7 @@ export default function ContractDraftPage() { try { toastService.info('正在下载文件...'); - // 使用统一的下载方法 + // 使用 axios-client 的 downloadFile 方法下载文件 const blob = await downloadFile(draft.file_path); // 创建Blob URL @@ -336,30 +345,43 @@ export default function ContractDraftPage() { } }; - // 完成起草(下载文件 + 删除草稿记录) + // 完成起草(下载文件 + 删除 MinIO 文件) const handleComplete = async () => { - // 1. 先下载文件 - await handleExportDocument(); + try { + // 1. 先下载文件 + await handleExportDocument(); - // 2. 延迟后删除草稿记录并跳转 - setTimeout(() => { + // 2. 删除 MinIO 文件 const formData = new FormData(); - formData.append('_action', 'delete'); + formData.append('_action', 'deleteFile'); + formData.append('filePath', draft.file_path); fetcher.submit(formData, { method: 'post' }); - }, 500); + } catch (error) { + console.error('[Complete] 操作失败:', error); + toastService.error('操作失败,请重试'); + } }; - // 返回模板详情页(删除草稿) + // 返回模板详情页 const handleBack = () => { - if (confirm('确定要返回吗?草稿将被删除。')) { - // 删除草稿记录 - const formData = new FormData(); - formData.append('_action', 'delete'); + messageService.show({ + type: 'warning', + title: '确认返回', + message: '确定要返回吗?未保存的更改将丢失。', + confirmText: '确定返回', + cancelText: '取消', + onConfirm: () => { + // 删除 MinIO 文件 + const formData = new FormData(); + formData.append('_action', 'deleteFile'); + formData.append('filePath', draft.file_path); - fetcher.submit(formData, { method: 'post' }); - // 删除成功后会自动跳转(通过 useEffect) - } + fetcher.submit(formData, { method: 'post' }); + + // 注意:文件删除后会在 useEffect 中跳转 + } + }); }; return ( @@ -376,11 +398,11 @@ export default function ContractDraftPage() {
-

{draft.title}

-

- - 基于模板:{template.title} -

+ {/*

{draft.title}

*/} +

+ + 基于模板:{template.title.replace(/-[\d-]+$/, '')} +

@@ -422,10 +444,7 @@ export default function ContractDraftPage() { schema={template.placeholder_schema as any} values={placeholderValues} onChange={setPlaceholderValues} - onBatchReplace={handleBatchReplace} - onExportDocument={handleExportDocument} onComplete={handleComplete} - isReplacing={isReplacing} isDeleting={isDeleting} onSingleReplace={handleSingleReplace} onFieldFocus={handleFieldFocus} diff --git a/app/routes/contract-template.detail.$id.tsx b/app/routes/contract-template.detail.$id.tsx index 591752b..9eb7d8b 100644 --- a/app/routes/contract-template.detail.$id.tsx +++ b/app/routes/contract-template.detail.$id.tsx @@ -1,18 +1,17 @@ import type { MetaFunction, LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'; import { redirect } from '@remix-run/node'; import { useLoaderData, useNavigate, useSubmit } from '@remix-run/react'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { getContractTemplate } from '~/api/contract-template/templates'; import type { ContractTemplate } from '~/api/contract-template/templates'; import styles from '~/styles/pages/contract-template.css?url'; import filePreviewStyles from '~/styles/components/file-preview-isolation.css?url'; import { getUserSession } from '~/api/login/auth.server'; import { createDraftContract } from '~/api/contracts/draft-service.server'; +import { apiRequest, downloadFile } from '~/api/axios-client'; // 导入FilePreview组件 import { FilePreview } from '~/components/reviews'; -// 导入统一的下载方法和提示服务 -import { downloadFile } from '~/api/axios-client'; import { toastService } from '~/components/ui/Toast'; export const links = () => [ @@ -70,7 +69,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { * Action 函数:处理起草合同请求 */ export async function action({ request, params }: ActionFunctionArgs) { - const templateId = parseInt(params.id || '0'); + const templateId = params.id!; if (!templateId) { return Response.json({ error: '模板ID无效' }, { status: 400 }); @@ -86,27 +85,60 @@ export async function action({ request, params }: ActionFunctionArgs) { // 解析表单数据 const formData = await request.formData(); const title = formData.get('title') as string; - const draftFilePath = formData.get('draftFilePath') as string | null; + const originalFilePath = formData.get('originalFilePath') as string; if (!title) { return Response.json({ error: '标题不能为空' }, { status: 400 }); } - // 创建草稿记录(到时候可以换成接口,使用接口来在minio中生成备份文件:备份文件可以用时间戳+uuid来保证唯一性。) - // const draft = await createDraftContract( - // { - // templateId, - // title, - // draftFilePath: draftFilePath || undefined - // }, - // parseInt(userInfo.sub), - // draftFilePath || undefined, - // frontendJWT || undefined - // ); + if (!originalFilePath) { + return Response.json({ error: '文件路径不存在' }, { status: 400 }); + } - // 重定向到草稿编辑页面 - // return redirect(`/contract-draft/${draft.id}`); - return redirect(`/contract-draft/1`); + // 生成新文件路径 + const area = userInfo.area || 'unknown'; + const timestamp = Date.now(); + const uuid = crypto.randomUUID(); + + // 提取文件目录和文件名 + const lastSlashIndex = originalFilePath.lastIndexOf('/'); + const directory = lastSlashIndex >= 0 ? originalFilePath.substring(0, lastSlashIndex) : ''; + const fileName = lastSlashIndex >= 0 ? originalFilePath.substring(lastSlashIndex + 1) : originalFilePath; + + // 提取文件扩展名 + const lastDotIndex = fileName.lastIndexOf('.'); + const baseName = lastDotIndex >= 0 ? fileName.substring(0, lastDotIndex) : fileName; + const extension = lastDotIndex >= 0 ? fileName.substring(lastDotIndex) : ''; + + // 构建新文件名 + const newFileName = `${baseName}_${area}_${timestamp}_${uuid}${extension}`; + const newFilePath = directory ? `${directory}/${newFileName}` : newFileName; + + console.log('[Draft] 复制文件:', { originalFilePath, newFilePath }); + + // 调用 MinIO 复制文件 API(需要传递 JWT) + const jwt = frontendJWT || undefined; + const copyResponse = await apiRequest('/api/v2/storage/files/copy', { + method: 'POST', + data: { + source_path: originalFilePath, + destination_path: newFilePath + }, + headers: { + 'Authorization': jwt ? `Bearer ${jwt}` : '' + } + }); + + if (copyResponse.error) { + console.error('[Draft] 文件复制失败:', copyResponse.error); + return Response.json({ error: `文件复制失败: ${copyResponse.error}` }, { status: 500 }); + } + + console.log('[Draft] 文件复制成功:', copyResponse.data); + + // 重定向到草稿编辑页面,通过 URL 参数传递文件路径和模板 ID + const draftUrl = `/contract-draft/1?filePath=${encodeURIComponent(newFilePath)}&templateId=${templateId}&title=${encodeURIComponent(title)}`; + return redirect(draftUrl); } catch (error) { console.error('[Template Detail] 创建草稿失败:', error); return Response.json( @@ -124,6 +156,12 @@ export default function ContractTemplateDetail() { // 注释掉收藏功能 // const [isFavorited, setIsFavorited] = useState(false); + // 防止页面加载时自动滚动到预览区域(由 Collabora iframe 的 tabIndex 导致) + useEffect(() => { + // 页面加载后立即滚动回顶部 + window.scrollTo({ top: 0, behavior: 'instant' }); + }, []); + const handleBack = () => { navigate(-1); }; @@ -181,21 +219,20 @@ export default function ContractTemplateDetail() { const handleStartDraft = () => { if (isCreatingDraft) return; - // 生成默认标题 - // const defaultTitle = `${template.title}-${new Date().toLocaleDateString('zh-CN').replace(/\//g, '')}`; + if (!template.file_path) { + toastService.error('模板文件路径不存在,无法起草'); + return; + } - // // 提示用户输入标题 - // const title = prompt('请输入合同标题:', defaultTitle); - // if (!title) return; + // 生成默认标题 + const defaultTitle = `${template.title}-${new Date().toLocaleDateString('zh-CN').replace(/\//g, '')}`; setIsCreatingDraft(true); // 使用 Remix 的 submit 提交表单 const formData = new FormData(); - // formData.append('title', title.trim()); - formData.append('title', '买卖合同-拟起草合同'); - // 可选:如果需要复制文件,可以先调用文件复制服务,然后传递 draftFilePath - // formData.append('draftFilePath', draftFilePath); + formData.append('title', defaultTitle); + formData.append('originalFilePath', template.file_path); submit(formData, { method: 'post' }); }; @@ -430,7 +467,7 @@ export default function ContractTemplateDetail() { {/* 合同预览 - 只有当存在pdf_file_path时才显示 */} {fileContent && ( -
+

合同预览

{ try { // console.log("🔍 [fetchEvaluationPointGroups] 开始获取评查点组数据"); - const response = await postgrestGet('evaluation_point_groups', { token: frontendJWT }); + const response = await postgrestGet('/api/postgrest/proxy/evaluation_point_groups', { token: frontendJWT }); // console.log("🔍 [fetchEvaluationPointGroups] API响应:", response); diff --git a/auth_doc/minio-api.md b/auth_doc/minio-api.md new file mode 100644 index 0000000..f8cb811 --- /dev/null +++ b/auth_doc/minio-api.md @@ -0,0 +1,776 @@ +# MinIO 存储管理 API 文档 + +## 概述 + +MinIO 存储管理模块提供对象存储的完整管理能力,包括存储桶管理和文件操作。 + +**基础路径**: `/api/v2/storage` + +**认证要求**: 当前无需认证(后续可按需添加) + +--- + +## 接口列表 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/buckets` | 创建存储桶 | +| DELETE | `/buckets` | 删除存储桶 | +| GET | `/buckets` | 列出所有存储桶 | +| POST | `/files/copy` | 复制文件 | +| POST | `/files/move` | 移动/重命名文件 | +| DELETE | `/files` | 删除单个文件 | +| POST | `/files/batch-delete` | 批量删除文件 | +| GET | `/files/download` | 下载文件 | +| GET | `/files` | 列出目录文件 | +| GET | `/files/metadata` | 获取文件元数据 | +| GET | `/files/presigned-url` | 获取预签名URL | + +--- + +## 存储桶管理 + +### 1. 创建存储桶 + +**POST** `/api/v2/storage/buckets` + +创建新的 MinIO 存储桶。 + +#### 请求体 + +```json +{ + "bucket_name": "my-new-bucket" +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| bucket_name | string | 是 | 存储桶名称(3-63字符,小写字母、数字、连字符) | + +#### 响应示例 + +**成功 (200)** +```json +{ + "success": true, + "message": "存储桶创建成功", + "bucket_name": "my-new-bucket" +} +``` + +**存储桶已存在 (200)** +```json +{ + "success": false, + "message": "存储桶已存在: my-new-bucket", + "bucket_name": "my-new-bucket" +} +``` + +#### cURL 示例 + +```bash +curl -X POST "http://localhost:8000/api/v2/storage/buckets" \ + -H "Content-Type: application/json" \ + -d '{"bucket_name": "my-new-bucket"}' +``` + +--- + +### 2. 删除存储桶 + +**DELETE** `/api/v2/storage/buckets` + +删除 MinIO 存储桶。支持强制删除(会先删除桶内所有文件)。 + +#### 请求体 + +```json +{ + "bucket_name": "my-bucket", + "force": true +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| bucket_name | string | 是 | 存储桶名称 | +| force | boolean | 否 | 强制删除(默认 false,为 true 时会先删除桶内所有文件) | + +#### 响应示例 + +**成功 (200)** +```json +{ + "success": true, + "message": "存储桶删除成功", + "bucket_name": "my-bucket", + "files_deleted": 15 +} +``` + +**存储桶不存在 (200)** +```json +{ + "success": false, + "message": "存储桶不存在: my-bucket", + "bucket_name": "my-bucket", + "files_deleted": 0 +} +``` + +#### cURL 示例 + +```bash +# 普通删除(桶必须为空) +curl -X DELETE "http://localhost:8000/api/v2/storage/buckets" \ + -H "Content-Type: application/json" \ + -d '{"bucket_name": "my-bucket"}' + +# 强制删除(会删除桶内所有文件) +curl -X DELETE "http://localhost:8000/api/v2/storage/buckets" \ + -H "Content-Type: application/json" \ + -d '{"bucket_name": "my-bucket", "force": true}' +``` + +--- + +### 3. 列出存储桶 + +**GET** `/api/v2/storage/buckets` + +列出所有 MinIO 存储桶。 + +#### 响应示例 + +```json +{ + "success": true, + "message": "共 3 个存储桶", + "buckets": [ + { + "name": "docauditai", + "creation_date": "2024-01-15T08:30:00+00:00" + }, + { + "name": "backup", + "creation_date": "2024-02-20T10:15:00+00:00" + }, + { + "name": "temp", + "creation_date": "2024-03-01T14:00:00+00:00" + } + ] +} +``` + +#### cURL 示例 + +```bash +curl -X GET "http://localhost:8000/api/v2/storage/buckets" +``` + +--- + +## 文件操作 + +### 4. 复制文件 + +**POST** `/api/v2/storage/files/copy` + +在 MinIO 内复制文件。支持跨存储桶复制。 + +#### 请求体 + +```json +{ + "source_path": "documents/contract.pdf", + "destination_path": "backup/contract-2024.pdf", + "source_bucket": null, + "destination_bucket": null +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| source_path | string | 是 | 源文件路径 | +| destination_path | string | 是 | 目标文件路径 | +| source_bucket | string | 否 | 源存储桶(默认使用配置的默认桶) | +| destination_bucket | string | 否 | 目标存储桶(默认使用配置的默认桶) | + +#### 响应示例 + +```json +{ + "success": true, + "message": "文件复制成功", + "source_path": "docauditai/documents/contract.pdf", + "destination_path": "docauditai/backup/contract-2024.pdf" +} +``` + +#### cURL 示例 + +```bash +# 同桶内复制 +curl -X POST "http://localhost:8000/api/v2/storage/files/copy" \ + -H "Content-Type: application/json" \ + -d '{ + "source_path": "documents/contract.pdf", + "destination_path": "backup/contract-2024.pdf" + }' + +# 跨桶复制 +curl -X POST "http://localhost:8000/api/v2/storage/files/copy" \ + -H "Content-Type: application/json" \ + -d '{ + "source_path": "documents/contract.pdf", + "destination_path": "contract.pdf", + "source_bucket": "docauditai", + "destination_bucket": "backup" + }' +``` + +--- + +### 5. 移动/重命名文件 + +**POST** `/api/v2/storage/files/move` + +移动文件(复制后删除源文件)。在同一目录内移动即为重命名。 + +#### 请求体 + +```json +{ + "source_path": "documents/old-name.pdf", + "destination_path": "documents/new-name.pdf" +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| source_path | string | 是 | 源文件路径 | +| destination_path | string | 是 | 目标文件路径 | + +#### 响应示例 + +```json +{ + "success": true, + "message": "文件移动成功", + "source_path": "documents/old-name.pdf", + "destination_path": "documents/new-name.pdf" +} +``` + +#### cURL 示例 + +```bash +# 重命名文件 +curl -X POST "http://localhost:8000/api/v2/storage/files/move" \ + -H "Content-Type: application/json" \ + -d '{ + "source_path": "documents/report-v1.pdf", + "destination_path": "documents/report-v2.pdf" + }' + +# 移动到其他目录 +curl -X POST "http://localhost:8000/api/v2/storage/files/move" \ + -H "Content-Type: application/json" \ + -d '{ + "source_path": "temp/upload.pdf", + "destination_path": "documents/final.pdf" + }' +``` + +--- + +### 6. 删除单个文件 + +**DELETE** `/api/v2/storage/files` + +删除 MinIO 中的单个文件。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| file_path | string | 是 | 文件路径 | + +#### 响应示例 + +**成功 (200)** +```json +{ + "success": true, + "message": "文件删除成功", + "file_path": "documents/old-file.pdf" +} +``` + +**文件不存在 (200)** +```json +{ + "success": false, + "message": "文件不存在: documents/old-file.pdf", + "file_path": "documents/old-file.pdf" +} +``` + +#### cURL 示例 + +```bash +curl -X DELETE "http://localhost:8000/api/v2/storage/files?file_path=documents/old-file.pdf" +``` + +--- + +### 7. 批量删除文件 + +**POST** `/api/v2/storage/files/batch-delete` + +批量删除多个文件。 + +#### 请求体 + +```json +{ + "file_paths": [ + "temp/file1.pdf", + "temp/file2.pdf", + "temp/file3.pdf" + ] +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| file_paths | string[] | 是 | 文件路径列表(至少1个) | + +#### 响应示例 + +```json +{ + "success": true, + "message": "删除完成: 成功 3, 失败 0", + "deleted_count": 3, + "failed_count": 0, + "failed_paths": [] +} +``` + +**部分失败 (200)** +```json +{ + "success": false, + "message": "删除完成: 成功 2, 失败 1", + "deleted_count": 2, + "failed_count": 1, + "failed_paths": ["temp/not-exist.pdf"] +} +``` + +#### cURL 示例 + +```bash +curl -X POST "http://localhost:8000/api/v2/storage/files/batch-delete" \ + -H "Content-Type: application/json" \ + -d '{ + "file_paths": [ + "temp/file1.pdf", + "temp/file2.pdf", + "temp/file3.pdf" + ] + }' +``` + +--- + +### 8. 下载文件 + +**GET** `/api/v2/storage/files/download` + +从 MinIO 下载文件。返回文件流。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| file_path | string | 是 | 文件路径 | +| filename | string | 否 | 下载时显示的文件名 | + +#### 响应 + +返回文件二进制流,包含以下响应头: + +``` +Content-Type: application/pdf +Content-Disposition: attachment; filename="contract.pdf" +Content-Length: 1234567 +``` + +#### cURL 示例 + +```bash +# 下载文件 +curl -X GET "http://localhost:8000/api/v2/storage/files/download?file_path=documents/contract.pdf" \ + -o contract.pdf + +# 指定下载文件名 +curl -X GET "http://localhost:8000/api/v2/storage/files/download?file_path=documents/contract.pdf&filename=my-contract.pdf" \ + -o my-contract.pdf +``` + +#### 浏览器直接下载 + +``` +http://localhost:8000/api/v2/storage/files/download?file_path=documents/contract.pdf +``` + +--- + +### 9. 列出目录文件 + +**GET** `/api/v2/storage/files` + +列出指定目录下的文件。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| directory_path | string | 否 | "" | 目录路径 | +| recursive | boolean | 否 | false | 是否递归列出子目录 | +| max_files | int | 否 | 100 | 最大返回数量(1-1000) | +| marker | string | 否 | null | 分页标记 | + +#### 响应示例 + +```json +{ + "success": true, + "message": "共 5 个文件", + "files": [ + "documents/contract-001.pdf", + "documents/contract-002.pdf", + "documents/contract-003.pdf", + "documents/report.docx", + "documents/summary.xlsx" + ], + "total_count": 5, + "next_marker": null +} +``` + +**分页响应 (200)** +```json +{ + "success": true, + "message": "共 100 个文件", + "files": ["..."], + "total_count": 100, + "next_marker": "documents/file-100.pdf" +} +``` + +#### cURL 示例 + +```bash +# 列出根目录 +curl -X GET "http://localhost:8000/api/v2/storage/files" + +# 列出指定目录 +curl -X GET "http://localhost:8000/api/v2/storage/files?directory_path=documents" + +# 递归列出 +curl -X GET "http://localhost:8000/api/v2/storage/files?directory_path=documents&recursive=true" + +# 分页查询 +curl -X GET "http://localhost:8000/api/v2/storage/files?directory_path=documents&max_files=50" + +# 获取下一页 +curl -X GET "http://localhost:8000/api/v2/storage/files?directory_path=documents&max_files=50&marker=documents/file-50.pdf" +``` + +--- + +### 10. 获取文件元数据 + +**GET** `/api/v2/storage/files/metadata` + +获取文件的详细元数据信息。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| file_path | string | 是 | 文件路径 | + +#### 响应示例 + +```json +{ + "success": true, + "message": "获取成功", + "file_info": { + "path": "documents/contract.pdf", + "size": 1234567, + "content_type": "application/pdf", + "created_time": "2024-03-15T10:30:00+00:00", + "modified_time": "2024-03-15T10:30:00+00:00", + "etag": "\"d41d8cd98f00b204e9800998ecf8427e\"", + "url": "http://minio:9000/docauditai/documents/contract.pdf?X-Amz-..." + } +} +``` + +#### cURL 示例 + +```bash +curl -X GET "http://localhost:8000/api/v2/storage/files/metadata?file_path=documents/contract.pdf" +``` + +--- + +### 11. 获取预签名URL + +**GET** `/api/v2/storage/files/presigned-url` + +获取文件的预签名下载URL,可用于临时分享。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| file_path | string | 是 | - | 文件路径 | +| expires | int | 否 | 3600 | 过期时间(秒),范围 60-604800(7天) | + +#### 响应示例 + +```json +{ + "success": true, + "message": "获取成功", + "url": "http://minio:9000/docauditai/documents/contract.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...", + "expires_in": 3600 +} +``` + +#### cURL 示例 + +```bash +# 默认1小时过期 +curl -X GET "http://localhost:8000/api/v2/storage/files/presigned-url?file_path=documents/contract.pdf" + +# 自定义过期时间(1天) +curl -X GET "http://localhost:8000/api/v2/storage/files/presigned-url?file_path=documents/contract.pdf&expires=86400" + +# 最长7天 +curl -X GET "http://localhost:8000/api/v2/storage/files/presigned-url?file_path=documents/contract.pdf&expires=604800" +``` + +--- + +## 前端集成示例 + +### JavaScript/Fetch + +```javascript +const API_BASE = 'http://localhost:8000/api/v2/storage'; + +// 列出存储桶 +async function listBuckets() { + const response = await fetch(`${API_BASE}/buckets`); + return response.json(); +} + +// 创建存储桶 +async function createBucket(bucketName) { + const response = await fetch(`${API_BASE}/buckets`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ bucket_name: bucketName }) + }); + return response.json(); +} + +// 复制文件 +async function copyFile(sourcePath, destPath) { + const response = await fetch(`${API_BASE}/files/copy`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + source_path: sourcePath, + destination_path: destPath + }) + }); + return response.json(); +} + +// 重命名文件 +async function renameFile(oldPath, newPath) { + const response = await fetch(`${API_BASE}/files/move`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + source_path: oldPath, + destination_path: newPath + }) + }); + return response.json(); +} + +// 删除文件 +async function deleteFile(filePath) { + const response = await fetch( + `${API_BASE}/files?file_path=${encodeURIComponent(filePath)}`, + { method: 'DELETE' } + ); + return response.json(); +} + +// 下载文件 +function downloadFile(filePath, filename) { + const url = `${API_BASE}/files/download?file_path=${encodeURIComponent(filePath)}`; + if (filename) { + url += `&filename=${encodeURIComponent(filename)}`; + } + window.open(url, '_blank'); +} + +// 获取文件列表 +async function listFiles(directory = '', recursive = false) { + const params = new URLSearchParams({ + directory_path: directory, + recursive: recursive.toString() + }); + const response = await fetch(`${API_BASE}/files?${params}`); + return response.json(); +} + +// 获取预签名URL +async function getPresignedUrl(filePath, expires = 3600) { + const params = new URLSearchParams({ + file_path: filePath, + expires: expires.toString() + }); + const response = await fetch(`${API_BASE}/files/presigned-url?${params}`); + return response.json(); +} +``` + +### Vue 3 组件示例 + +```vue + + + +``` + +--- + +## 错误处理 + +所有接口返回统一格式: + +```json +{ + "success": false, + "message": "错误描述信息" +} +``` + +### 常见错误 + +| 错误 | 说明 | 解决方案 | +|------|------|----------| +| 存储桶不存在 | 操作的存储桶未找到 | 检查桶名是否正确 | +| 文件不存在 | 操作的文件路径无效 | 检查文件路径 | +| 存储桶非空 | 删除桶时桶内有文件 | 使用 force=true 强制删除 | +| 权限不足 | MinIO 访问凭证问题 | 检查配置的 access_key/secret_key | + +--- + +## 测试 + +使用测试脚本进行完整测试: + +```bash +python scripts/test_minio_api.py --host localhost --port 8000 +```