## 🗄️ PostgreSQL数据对接规范 ### 概述 本系统使用PostgreSQL作为主数据库,通过PostgREST提供RESTful API接口。所有数据操作统一通过封装的`postgrest-client.ts`模块进行,确保数据访问的一致性和可维护性。 ### 技术架构 ``` 前端组件 (Remix Routes) ↓ API层 (app/api/[module]/[api].ts) ↓ PostgREST客户端 (postgrest-client.ts) ↓ PostgreSQL数据库 ``` ### 基础封装方法 #### 1. 导入PostgREST客户端 ```typescript import { postgrestGet, postgrestPost, postgrestPut, postgrestDelete, type PostgrestParams } from "../postgrest-client"; ``` #### 2. 查询操作 (GET) ```typescript // 基础查询 const params: PostgrestParams = { select: '*', filter: { 'id': `eq.${id}`, 'status': `eq.active` }, order: 'created_at.desc', limit: 20, offset: 0 }; const response = await postgrestGet('table_name', params); // 复杂查询示例 const complexParams: PostgrestParams = { select: 'id,name,status,created_at,user_id,users(name)', // 关联查询 filter: { 'created_at': `gte.2023-01-01`, 'status': `in.(active,pending)` }, or: 'name.ilike.*keyword*,description.ilike.*keyword*', // OR条件 order: 'created_at.desc,name.asc', limit: 50 }; const response = await postgrestGet('documents', complexParams); ``` #### 3. 创建操作 (POST) ```typescript // 单条记录创建 const newRecord = { name: '文档名称', document_number: 'DOC001', type_id: 1, user_id: 123, status: 'pending' }; const response = await postgrestPost('documents', newRecord); // 批量创建 const batchRecords = [ { name: '文档1', type_id: 1 }, { name: '文档2', type_id: 2 } ]; const response = await postgrestPost('documents', batchRecords); ``` #### 4. 更新操作 (PUT/PATCH) ```typescript // 根据ID更新 const updateData = { status: 'completed', audit_status: 1, updated_at: new Date().toISOString() }; const response = await postgrestPut( 'documents', updateData, { id: documentId } ); // 根据条件批量更新 const response = await postgrestPut( 'evaluation_results', { status: 'reviewed' }, { document_id: docId, status: 'pending' } ); ``` #### 5. 删除操作 (DELETE) ```typescript // 根据ID删除 const response = await postgrestDelete('documents', { filter: { 'id': `eq.${id}` } }); // 条件删除 const response = await postgrestDelete('temp_files', { filter: { 'created_at': `lt.2023-01-01` } }); ``` ### API层实现规范 #### 1. 文件结构 ``` app/api/ ├── [module]/ # 功能模块 │ ├── [feature].ts # 具体功能API │ └── types.ts # 类型定义 (可选) ├── postgrest-client.ts # PostgREST客户端 ├── axios-client.ts # HTTP客户端 └── error-handler.ts # 错误处理 ``` #### 2. API文件模板 ```typescript // app/api/evaluation_points/reviews.ts import { postgrestGet, type PostgrestParams, postgrestPut, postgrestPost } from "../postgrest-client"; import { getDocument } from "~/api/files/documents"; import { formatDate } from "~/utils"; /** * 数据提取工具函数 * 统一处理不同格式的API响应 */ function extractApiData(responseData: unknown): T | null { if (!responseData) return null; // 格式1: { code: number, msg: string, data: T } if (typeof responseData === 'object' && responseData !== null && 'code' in responseData && 'data' in responseData && (responseData as { data: unknown }).data) { return (responseData as { data: T }).data; } // 格式2: 直接是数据对象 return responseData as T; } // 类型定义 interface EvaluationResult { id: string | number; document_id: string | number; evaluation_point_id: string | number; evaluated_results?: { result?: boolean; message?: string; data?: string; [key: string]: unknown; }; [key: string]: unknown; } interface EvaluationPoint { id: string | number; evaluation_point_groups_id: string | number; suggestion_message_type?: string; suggestion_message?: string; score?: number; updated_at?: string; [key: string]: unknown; } // 前端使用的结果类型 interface ReviewPointResult { id: string | number; title: string; groupName: string; status: string; content: string; suggestion: string; result?: boolean; score: number; } /** * 获取评查点数据 * @param fileId 文件ID * @returns 评查点结果列表和统计数据 */ export async function getReviewPoints(fileId: string) { try { // 步骤1: 获取文档基础数据 const documentData = await getDocument(fileId); if (documentData.error) { return { error: documentData.error, status: documentData.status || 500 }; } // 步骤2: 查询评查结果 const evaluationResultsParams: PostgrestParams = { select: '*', filter: { 'document_id': `eq.${fileId}` } }; const evaluationResultsResponse = await postgrestGet('evaluation_results', evaluationResultsParams); if (evaluationResultsResponse.error) { return { error: evaluationResultsResponse.error, status: evaluationResultsResponse.status }; } const evaluationResultsData = extractApiData(evaluationResultsResponse.data) || []; if (evaluationResultsData.length <= 0) { return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 }, error: '获取评查结果数据失败' }; } // 步骤3: 获取评查点详情 const evaluationPointIds = evaluationResultsData.map(item => item.evaluation_point_id).filter(Boolean); const evaluationPointsParams: PostgrestParams = { select: '*', filter: { 'id': `in.(${evaluationPointIds.join(',')})` } }; const evaluationPointsResponse = await postgrestGet('evaluation_points', evaluationPointsParams); if (evaluationPointsResponse.error) { return { error: evaluationPointsResponse.error, status: evaluationPointsResponse.status }; } const evaluationPointsData = extractApiData(evaluationPointsResponse.data) || []; // 步骤4: 数据处理和转换 const resultData: ReviewPointResult[] = evaluationResultsData.map(result => { const point = evaluationPointsData.find(p => p.id === result.evaluation_point_id); return { id: result.id, title: result.evaluated_results?.message || '', groupName: point?.group_name || '', status: point?.suggestion_message_type || '', content: result.evaluated_results?.data || '', suggestion: point?.suggestion_message || '', result: result.evaluated_results?.result, score: point?.score || 0 }; }); // 步骤5: 统计数据计算 const stats = { total: resultData.length, success: resultData.filter(item => item.result === true).length, warning: resultData.filter(item => item.status === 'warning').length, error: resultData.filter(item => item.status === 'error').length, score: resultData.reduce((sum, item) => sum + item.score, 0) }; return { data: resultData, stats, document: documentData.data }; } catch (error) { console.error('获取评查数据失败:', error); return { error: error instanceof Error ? error.message : '获取评查数据失败', status: 500 }; } } /** * 更新评查结果 * @param resultId 评查结果ID * @param editAuditStatusId 审核状态ID * @param result 评查结果 * @param message 评查意见 */ export async function updateReviewResult( resultId: string, editAuditStatusId: string | number, result: string, message: string ): Promise<{ data?: unknown; error?: string; status?: number; }> { try { if (!resultId) { return { error: '评查结果ID不能为空', status: 400 }; } // 获取当前数据 const currentResultResponse = await postgrestGet('evaluation_results', { select: '*', filter: { id: `eq.${resultId}` } }); if (currentResultResponse.error) { return { error: currentResultResponse.error, status: currentResultResponse.status }; } const currentResultData = extractApiData(currentResultResponse.data); if (!currentResultData || currentResultData.length === 0) { return { error: '未找到评查结果数据', status: 404 }; } const currentResult = currentResultData[0]; const currentEvaluatedResults = currentResult.evaluated_results || {}; // 构建更新数据 const isReview = result === 'review'; const updatedEvaluatedResults = { ...currentEvaluatedResults, ...(isReview ? { message } : { result: result === 'true', message }), }; // 更新评查结果 const resultResponse = await postgrestPut( 'evaluation_results', { evaluated_results: updatedEvaluatedResults }, { id: resultId } ); if (resultResponse.error) { return { error: resultResponse.error, status: resultResponse.status }; } // 处理审核状态 const editAuditStatusValue = isReview ? 0 : 1; if (editAuditStatusId && editAuditStatusId !== '') { // 更新现有记录 const auditStatusResponse = await postgrestPut( 'audit_status', { edit_audit_status: editAuditStatusValue }, { id: editAuditStatusId } ); if (auditStatusResponse.error) { return { error: auditStatusResponse.error, status: auditStatusResponse.status }; } } else { // 创建新记录 const newAuditStatus = { document_id: currentResult.document_id, evaluation_point_id: currentResult.evaluation_point_id, evaluation_result_id: resultId, edit_audit_status: editAuditStatusValue }; const postResponse = await postgrestPost('audit_status', newAuditStatus); if (postResponse.error) { return { error: postResponse.error, status: postResponse.status }; } } return { data: extractApiData(resultResponse.data) }; } catch (error) { console.error('更新评查结果失败:', error); return { error: error instanceof Error ? error.message : '更新评查结果失败', status: 500 }; } } ``` #### 3. 错误处理规范 ```typescript // 统一的错误处理格式 interface ApiResponse { data?: T; error?: string; status?: number; } // 在API函数中的错误处理 export async function apiFunction(): Promise> { try { const response = await postgrestGet('table_name', params); if (response.error) { return { error: response.error, status: response.status }; } const data = extractApiData(response.data); if (!data) { return { error: '数据格式错误', status: 500 }; } return { data }; } catch (error) { console.error('API调用失败:', error); return { error: error instanceof Error ? error.message : '未知错误', status: 500 }; } } ``` ### 前端路由集成规范 #### 1. Loader函数中调用API ```typescript // app/routes/reviews.tsx import { getReviewPoints, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews"; export async function loader({ request }: LoaderFunctionArgs) { try { const url = new URL(request.url); const id = url.searchParams.get('id') || undefined; const previousRoute = url.searchParams.get('previousRoute') || ''; if (!id) { return Response.json({ result: false, message: '文件ID不能为空' }); } // 获取评查点数据 const reviewData = await getReviewPoints(id); if ('error' in reviewData && reviewData.error) { console.error("获取评查点数据错误:", reviewData.error); return Response.json({ result: false, message: reviewData.error }); } // 确保数据格式正确 if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) { return Response.json({ previousRoute: previousRoute, document: reviewData.document, reviewPoints: reviewData.data, reviewInfo: reviewData.reviewInfo, statistics: reviewData.stats }); } else { console.error("返回的评查数据格式不正确", JSON.stringify(reviewData, null, 2)); return Response.json({ result: false, message: '返回的评查数据格式不正确' }); } } catch (error) { console.error('获取评查数据失败:', error); return Response.json({ result: false, message: '获取评查数据失败' }); } } ``` #### 2. 组件中处理API响应 ```typescript // 在React组件中使用 export default function ReviewDetails() { const loaderData = useLoaderData(); const { document, reviewPoints, statistics, reviewInfo } = loaderData; const [isLoading, setIsLoading] = useState(false); // 处理loader错误 useEffect(() => { if (Object.keys(loaderData).find(key => key === 'result') && !loaderData.result) { messageService.show({ title: '错误', message: loaderData.message, type: 'error', confirmText: '确定', onConfirm: () => { navigate(-1); } }); } }, [loaderData, navigate]); // 处理状态更新 const handleReviewPointStatusChange = async ( reviewPointResultId: string, editAuditStatusId: string | number, newStatus: string, message: string ) => { try { const response = await updateReviewResult( reviewPointResultId, editAuditStatusId, newStatus, message ); if (response.error) { console.error('更新评查结果失败:', response.error); toastService.error(`更新评查结果失败: ${response.error}`); return; } // 更新本地状态 setReviewData(prevData => { // 更新逻辑... }); toastService.success('评查点状态已更新'); } catch (error) { console.error('更新评查结果出错:', error); toastService.error('更新评查结果失败,请稍后重试'); } }; } ``` ### 数据类型定义规范 #### 1. 数据库实体类型 ```typescript // 数据库表对应的接口 interface Document { id: number; user_id: number | null; type_id: number; name: string; document_number: string; path: string; storage_type: string; file_size: number; upload_time: string; is_test_document: boolean; evaluation_level: string; status: 'pass' | 'warning' | 'waiting' | 'processing' | 'fail'; file_status: 'Waiting' | 'Cutting' | 'Extractioning' | 'Evaluationing' | 'Processed'; audit_status: number; // -1: 不通过, 0: 待审核, 1: 通过, 2: 警告, 3: 审核中 ocr_result?: Record; extracted_results?: unknown; summary?: unknown; remark?: string; created_at: string; updated_at: string; } ``` #### 2. 前端UI类型 ```typescript // 前端组件使用的接口 interface ReviewFileUI { id: string; status: string; path: string; fileName: string; fileCode: string; fileType: string; fileTypeId: number; fileSize: number; uploadTime: string; reviewStatus: string; reviewStatusCode: number; issueCount: number; score?: number; auditStatus: number | null; issues: Array<{ severity: 'info' | 'warning' | 'error' | 'critical'; message: string; }>; createdBy: string; passCount: number; warningCount: number; failCount: number; manualCount: number; } ``` #### 3. API参数类型 ```typescript // 搜索参数类型 interface DocumentSearchParams { keyword?: string; status?: string; fileType?: string; dateRange?: [string, string]; page?: number; pageSize?: number; sortBy?: string; sortOrder?: 'asc' | 'desc'; } ``` ### PostgREST客户端功能 #### 1. 查询参数转换 ```typescript // 转换通用参数为PostgREST格式 export function transformParams(params: PostgrestParams): QueryParams { const result: QueryParams = {}; // 处理select参数 if (params.select) { result.select = params.select; } // 处理过滤条件 if (params.filter) { Object.entries(params.filter).forEach(([key, value]) => { if (value !== undefined) { result[key] = value as string | number | boolean; } }); } // 处理排序 if (params.order) { result.order = params.order; } // 处理分页 if (params.limit !== undefined) { result.limit = params.limit; } if (params.offset !== undefined) { result.offset = params.offset; } // 处理OR条件 if (params.or) { if (typeof params.or === 'string') { result.or = params.or; } else if (Array.isArray(params.or)) { const orConditions = params.or.map(condition => { const [field, operator] = Object.entries(condition)[0]; return `${field}.${operator}`; }); result.or = `(${orConditions.join(',')})`; } } return result; } ``` #### 2. 开发环境日志 ```typescript function logPostgrestQuery(endpoint: string, params?: QueryParams, method: string = 'GET'): void { if (process.env.NODE_ENV !== 'production') { const baseUrl = 'http://nas.7bm.co:3000'; const normalizedEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; // console.log('\n📦 PostgREST 查询日志 ======================start============='); // console.log(`📦 HTTP 方法: ${method}`); // console.log(`📦 API 端点: ${decodeUrlForDisplay(`${baseUrl}/${normalizedEndpoint}`)}`); // if (params && Object.keys(params).length > 0) { // console.log('📦 查询参数:'); // Object.entries(params).forEach(([key, value]) => { // if (value !== undefined) { // console.log(` - ${key}: ${JSON.stringify(value)}`); // } // }); // } // console.log('PostgREST 查询日志=============================end============\n'); } } ``` #### 3. 数据预处理 ```typescript function preprocessData(data: Record): Record { const processed: Record = {}; for (const [key, value] of Object.entries(data)) { // 处理null值 if (value === null) { processed[key] = null; continue; } // 处理布尔值字符串 if (typeof value === 'string' && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) { processed[key] = value.toLowerCase() === 'true'; } // 处理ID字段 else if ((key === 'id' || key.endsWith('_id') || key === 'pid') && value !== undefined) { try { const numValue = Number(value); if (!isNaN(numValue)) { processed[key] = numValue; } else { processed[key] = value; } } catch { processed[key] = value; } } // 其他值保持不变 else { processed[key] = value; } } return processed; } ``` ### 开发最佳实践 #### 1. API命名规范 - 获取数据: `get[EntityName]s()` 或 `get[EntityName]()` - 创建数据: `create[EntityName]()` - 更新数据: `update[EntityName]()` - 删除数据: `delete[EntityName]()` #### 2. 错误处理策略 - 所有API函数必须返回统一的响应格式 - 在API层处理数据库错误,前端只处理业务逻辑错误 - 使用TypeScript严格类型检查避免运行时错误 #### 3. 性能优化 - 使用`select`参数只获取需要的字段 - 合理使用`limit`和`offset`进行分页 - 避免N+1查询,使用关联查询获取相关数据 #### 4. 调试技巧 - 开发环境自动打印查询日志 - 使用浏览器网络面板检查实际请求 - 通过PostgREST文档验证查询语法 ### 数据对接检查清单 #### 新功能开发前 - [ ] 确认数据库表结构和关系 - [ ] 定义TypeScript接口类型 - [ ] 规划API函数命名和参数 - [ ] 设计错误处理策略 #### API实现阶段 - [ ] 导入必要的postgrest方法 - [ ] 实现extractApiData数据提取函数 - [ ] 定义所有相关的类型接口 - [ ] 编写查询参数构建逻辑 - [ ] 实现数据转换和处理逻辑 - [ ] 添加完整的错误处理 #### 前端集成阶段 - [ ] 在loader函数中调用API - [ ] 处理loading和error状态 - [ ] 实现UI状态更新逻辑 - [ ] 添加用户反馈(toast/message) - [ ] 测试各种边界情况 #### 上线前检查 - [ ] API函数测试通过 - [ ] 错误处理验证完成 - [ ] 性能测试满足要求 - [ ] 日志输出正常 - [ ] 代码注释完整 --- *此数据对接规范基于实际项目经验总结,将根据项目发展持续更新和完善。*