# getReviewPoints 方法详细分析 ## 📋 方法概述 **文件位置**:`app/api/evaluation_points/reviews.ts:132-740` **方法签名**: ```typescript export async function getReviewPoints(fileId: string, request: Request) ``` **功能**:获取指定文档的所有评查点结果、统计数据、评查信息以及相关联的文档数据。 **返回数据**: ```typescript { data: ReviewPointResult[], // 评查点结果列表 stats: StatsData, // 统计数据 reviewInfo: ReviewInfo, // 评查信息 document: Document, // 文档数据 comparison_document: ComparisonDoc, // 合同结构比对数据 scoring_proposals: ScoringProposal[] // 评分提案(交叉评查) } ``` --- ## 🔄 执行流程图 ```mermaid graph TD A[开始: getReviewPoints] --> B[获取用户会话信息] B --> C[查询documents表获取文档数据] C --> D[查询contract_structure_comparison表] D --> E[查询evaluation_results表] E --> F[查询evaluation_points表] F --> G[查询evaluation_point_groups表] G --> H[查询audit_status表
人工审核状态] H --> I[查询cross_scoring_proposals表
评分提案] I --> J[构建前端数据格式] J --> K[计算统计数据] K --> L[构建评查信息] L --> M[返回完整数据] style A fill:#90EE90 style M fill:#FFD700 style C fill:#87CEEB style D fill:#87CEEB style E fill:#87CEEB style F fill:#87CEEB style G fill:#87CEEB style H fill:#87CEEB style I fill:#87CEEB ``` --- ## 📊 数据库查询总览 ### 查询汇总表 | 序号 | 表名 | 查询类型 | 查询条件 | 返回记录数 | 查询目的 | |-----|------|---------|---------|-----------|---------| | 1 | `documents` | 单条查询 | `id = fileId` | 1 | 获取文档基本信息和OCR结果 | | 2 | `contract_structure_comparison` | 单条查询 | `document_id = fileId` | 0-1 | 获取合同模板比对结果 | | 3 | `evaluation_results` | 多条查询 | `document_id = fileId` | N | 获取所有评查结果(核心数据) | | 4 | `evaluation_points` | 批量查询 | `id IN (...)` | N | 获取评查点配置详情 | | 5 | `evaluation_point_groups` | 批量查询 | `id IN (...)` | M | 获取评查点分组信息 | | 6 | `audit_status` | 批量查询 | `document_id + evaluation_point_id IN (...)` | 0-K | 获取人工审核状态 | | 7 | `cross_scoring_proposals` | 多条查询 | `document_id = fileId AND deleted_at IS NULL` | 0-P | 获取交叉评查提案 | **查询特点**: - ✅ 采用批量查询策略(IN 操作符),减少数据库往返 - ✅ 查询顺序经过优化,先查主表,再关联查询 - ✅ 使用 JWT Token 进行身份验证 - ✅ 所有查询都通过 PostgREST API 执行 --- ## 🗄️ 数据库查询详解 ### 1️⃣ 用户会话验证(lines 133-141) **目的**:获取用户身份信息和 JWT token ```typescript const { userInfo, frontendJWT } = await getUserSession(request); ``` **验证**: - 检查 `userInfo.user_id` 是否存在 - 如果不存在,返回 401 错误 --- ### 2️⃣ 查询文档数据(lines 143-148) **表名**:`documents` **调用方法**: ```typescript const documentData = await getDocumentWithNoUserId(fileId, frontendJWT); ``` **查询参数**: - `fileId` - 文档ID **SQL 等价查询**: ```sql SELECT * FROM documents WHERE id = :fileId LIMIT 1; ``` **PostgREST API 请求**: ```http GET /documents?id=eq.{fileId}&select=* Authorization: Bearer {frontendJWT} ``` **返回数据结构**: ```typescript { data: { id: string | number, name: string, // 文档名称 path: string, // 文档存储路径 user_id: string | number, // 上传用户ID document_type_id: string | number, // 文档类型ID audit_status: number, // 审核状态:0-待审核,1-已审核,-1-不通过 created_at: string, // 创建时间 updated_at: string, // 更新时间 ocrResult: { ocr_result: { [key: string]: { // 表单名称作为key pages: number[] // 该表单出现的页码列表 } } }, // ... 其他文档字段 } } ``` **数据示例**: ```json { "data": { "id": 123, "name": "烟草专卖案卷-示例.pdf", "path": "/uploads/2024/01/document123.pdf", "user_id": 6, "document_type_id": 1, "audit_status": 0, "ocrResult": { "ocr_result": { "立案报告表": { "pages": [1] }, "现场笔录": { "pages": [2, 3] }, "证据复制(提取)单": { "pages": [4, 5] } } } } } ``` **使用场景**: - 获取文档基本信息 - 获取 OCR 结果用于页码映射(lines 393-396) - 显示文档名称和路径 **错误处理**: ```typescript if (documentData.error) { console.error("获取文档数据错误:", documentData.error); return Response.json({ error: documentData.error }, { status: documentData.status || 500 }); } ``` --- ### 3️⃣ 查询合同结构比对数据(lines 151-191) **表名**:`contract_structure_comparison` **查询参数**: ```typescript { select: '*', filter: { 'document_id': `eq.${fileId}` }, order: 'id.desc', limit: 1, token: frontendJWT } ``` **查询逻辑**: - 根据 `document_id` 查询 - 按 `id` 降序排序,取最新的一条记录 - 限制返回 1 条 **SQL 等价查询**: ```sql SELECT * FROM contract_structure_comparison WHERE document_id = :fileId ORDER BY id DESC LIMIT 1; ``` **PostgREST API 请求**: ```http GET /contract_structure_comparison?document_id=eq.{fileId}&order=id.desc&limit=1&select=* Authorization: Bearer {frontendJWT} ``` **返回数据结构**: ```typescript { id: string | number, document_id: string | number, template_contract_path: string, // 模板文件路径 comparison_results: string | object, // 比对结果(JSON字符串或对象) created_at: string, updated_at: string, // ... 其他字段 } ``` **数据示例**: ```json { "id": 45, "document_id": 123, "template_contract_path": "/templates/standard_contract.pdf", "comparison_results": { "合同封面": [ { "field": "合同名称", "status": "normal", "similarity": 1.0 } ], "合同条款": [ { "field": "甲方信息", "status": "abnormal", "similarity": 0.65 } ] } } ``` **数据处理**: - 如果 `comparison_results` 是字符串,尝试解析为 JSON(lines 179-186) ```typescript if (typeof comparisonDocument.comparison_results === 'string') { try { comparisonDocument.comparison_results = JSON.parse(comparisonDocument.comparison_results); } catch (e) { console.error('解析比对结果失败:', e); comparisonDocument.comparison_results = null; } } ``` - 如果没有记录,设置默认值 `{ template_contract_path: '' }`(lines 187-191) ```typescript if (!contractStructureComparisonData || contractStructureComparisonData.length === 0) { comparisonDocument = { template_contract_path: '' }; } ``` **使用场景**: - 获取合同模板比对结果 - 提供模板文件路径用于对比显示 - 显示字段匹配状态和相似度 --- ### 4️⃣ 查询评查结果(lines 196-215) **表名**:`evaluation_results` ⭐ **核心表** **查询参数**: ```typescript { select: '*', filter: { 'document_id': `eq.${fileId}` }, token: frontendJWT } ``` **查询逻辑**: - 根据 `document_id` 查询该文档的所有评查结果 - 这是整个方法的核心数据源,后续查询都基于此 **SQL 等价查询**: ```sql SELECT * FROM evaluation_results WHERE document_id = :fileId; ``` **PostgREST API 请求**: ```http GET /evaluation_results?document_id=eq.{fileId}&select=* Authorization: Bearer {frontendJWT} ``` **返回数据结构**: ```typescript [ { id: string | number, document_id: string | number, evaluation_point_id: string | number, evaluated_results: { result: boolean, // 评查结果 true/false message: string, // 评查消息 data: object | string // 评查数据 }, evaluated_point_results_log: { rules: unknown[] // 规则执行日志 }, final_score: number, // 最终得分(交叉评查) machine_score: number, // 机器评分 updated_at: string, // 更新时间 created_at: string, // ... 其他字段 } ] ``` **数据示例**: ```json [ { "id": 1001, "document_id": 123, "evaluation_point_id": 5, "evaluated_results": { "result": false, "message": "立案报告表中的当事人信息与营业执照不一致", "data": { "立案报告表-当事人-单位-名称": { "page": 1, "value": "张三烟草店" }, "证据复制(提取)单-营业执照-名称": { "page": 4, "value": "李四烟草店" } } }, "evaluated_point_results_log": { "rules": [ { "id": "0", "type": "consistency", "res": false, "config": { "logic": "all", "pairs": [ { "sourceField": { "立案报告表-当事人-单位-名称": { "page": 1, "value": "张三烟草店" } }, "targetField": { "证据复制(提取)单-营业执照-名称": { "page": 4, "value": "李四烟草店" } }, "compareMethod": "exact", "res": false } ] } } ] }, "final_score": null, "machine_score": 5, "updated_at": "2024-01-15T10:30:00Z" }, { "id": 1002, "document_id": 123, "evaluation_point_id": 8, "evaluated_results": { "result": true, "message": "签名完整性检查通过", "data": { "现场笔录-执法人员签名": { "page": 3, "value": "有" } } }, "evaluated_point_results_log": { "rules": [] }, "final_score": null, "machine_score": 10, "updated_at": "2024-01-15T10:30:00Z" } ] ``` **数据验证**: - 如果没有评查结果,返回空数组和空统计(lines 213-215) ```typescript if (Array.isArray(evaluationResultsData) && evaluationResultsData.length <= 0) { return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 }, error: '获取评查结果数据失败' }; } ``` **关键字段说明**: - `evaluated_results.result` - 评查通过/不通过(true/false) - `evaluated_results.message` - 评查点标题/问题描述 - `evaluated_results.data` - 具体的评查内容(字段值和页码) - 格式:`{ "字段名": { "page": 页码, "value": 字段值 } }` - `evaluated_point_results_log.rules` - 规则执行日志(用于调试和详情展示) **数据使用**: - 从中提取 `evaluation_point_id` 列表 → 查询 evaluation_points - `updated_at` 用于确定最新评查时间 - `evaluated_results.data` 用于页码映射 --- ### 5️⃣ 查询评查点详情(lines 218-242) **表名**:`evaluation_points` **查询参数**: ```typescript { select: '*', filter: { 'id': `in.(${evaluationPointIds.join(',')})` }, token: frontendJWT } ``` **查询逻辑**: - 从评查结果中提取所有 `evaluation_point_id`(line 218) - 使用 `IN` 操作符批量查询评查点详情 **返回数据结构**: ```typescript [ { id: string | number, evaluation_point_groups_id: string | number, name: string, // 评查点名称 suggestion_message_type: string, // 建议消息类型(success/warning/error) suggestion_message: string, // 建议内容 score: number, // 满分分数 post_action: string, // 后续动作(manual/auto) action_config: string | object, // 动作配置 references_laws: object, // 法律依据 evaluation_config: object, // 评查配置 fail_message: string, // 不通过提示 pass_message: string, // 通过提示 updated_at: string, // ... 其他字段 } ] ``` **使用场景**: - 获取评查点配置信息 - 获取评分标准 - 获取建议内容和法律依据 --- ### 6️⃣ 查询评查点组(lines 244-269) **表名**:`evaluation_point_groups` **查询参数**: ```typescript { select: '*', filter: { 'id': `in.(${groupIds.join(',')})` }, token: frontendJWT } ``` **查询逻辑**: - 从评查点中提取所有 `evaluation_point_groups_id`(line 245) - 使用 `IN` 操作符批量查询评查点组 **返回数据结构**: ```typescript [ { id: string | number, name: string, // 评查点组名称 // ... 其他字段 } ] ``` **使用场景**: - 获取评查点所属的分组名称(如"合同形式要素"、"合同实质要素"等) --- ### 7️⃣ 查询人工审核状态(lines 272-308) **表名**:`audit_status` **查询参数**: ```typescript { select: '*', filter: { 'document_id': `eq.${fileId}`, 'evaluation_point_id': `in.(${manualReviewPointsIds.join(',')})` }, token: frontendJWT } ``` **查询逻辑**: - 筛选出 `post_action === 'manual'` 的评查点(line 273) - 只查询需要人工审核的评查点的审核状态 **返回数据结构**: ```typescript [ { id: string | number, document_id: string | number, evaluation_point_id: string | number, edit_audit_status: number, // 审核状态:0-待审核,1-已审核 message: string, // 审核意见 // ... 其他字段 } ] ``` **数据处理**: - 构建 `editAuditStatusMap` 映射表(lines 290-298) - 为没有审核记录的 manual 评查点设置默认值 0(lines 302-308) **使用场景**: - 判断哪些评查点需要人工审核 - 显示审核状态和审核意见 --- ### 8️⃣ 查询评分提案(lines 330-343) **表名**:`cross_scoring_proposals` **查询参数**: ```typescript { select: '*', filter: { 'document_id': `eq.${fileId}`, 'deleted_at': `is.null` }, token: frontendJWT } ``` **查询逻辑**: - 根据 `document_id` 查询 - 排除已删除的提案(`deleted_at IS NULL`) **返回数据结构**: ```typescript [ { id: string | number, evaluation_result_id: string | number, proposer_id: string | number, proposed_score: number, // 建议分数 reason: string, // 提议理由 status: string, // 提案状态 created_at: string, updated_at: string, document_id: string | number, // ... 其他字段 } ] ``` **使用场景**: - 交叉评查功能 - 显示评分提案和投票情况 --- ## 🔧 数据构建与处理 ### 9️⃣ 构建前端数据格式(lines 350-685) **目标**:将多表查询结果合并为前端所需的数据格式 **核心逻辑**: #### a) 创建映射表(lines 314-327) ```typescript // 评查点映射 const pointsMap = new Map(); evaluationPointsData.forEach(point => { pointsMap.set(point.id, point); }); // 评查点组映射 const groupsMap = new Map(); groupsData.forEach(group => { groupsMap.set(group.id, group); }); ``` #### b) 遍历评查结果构建数据(line 351) ```typescript const resultData: ReviewPointResult[] = evaluationResultsData.map(result => { // ... }); ``` #### c) 提取评查内容和页码(lines 361-403) **步骤**: 1. 从 `evaluated_results` 中提取 `message` 和 `data`(lines 361-368) 2. 解析 `data` 对象中的页码信息(lines 375-403) **页码提取逻辑**: ```typescript // data 结构示例: // { // "合同封面-合同名称": { page: 1, value: "采购合同" }, // "合同封面-签订日期": { page: 1, value: "2024-01-01" } // } // 提取页码 for (const key in dataObj) { let newPage = dataObj[key].page.toString(); // 提取页码中的数字部分 if (newPage.match(/\d+/g)) { newPage = newPage.match(/^\d+/g)?.map(Number).join('') || ''; } contentPage[key] = newPage; // 如果页码为空,从 ocrResult 中查找 if (!contentPage[key]) { const keyArray = key.split('-'); const ocrResult = documentData?.data?.ocrResult as OcrData; const pages = ocrResult?.ocr_result?.[keyArray[0]]?.pages; contentPage[key] = pages?.[0]?.toString() || ''; } } ``` #### d) 构建单个评查点结果对象(lines 405-684) **返回的字段**: ```typescript { // 基本信息 id: result.id, // 评查结果ID documentId: fileId, // 文档ID pointId: point.id, // 评查点ID // 审核状态 editAuditStatusId: editAuditStatus.id, // 审核状态ID editAuditStatus: editAuditStatus.status, // 审核状态:0-待审核,1-已审核 editAuditStatusMessage: editAuditStatus.message, // 审核意见 // 显示信息 title: message, // 评查点标题/问题描述 pointName: point.name || '', // 评查点名称 groupName: group.name || '', // 评查点组名称 status: point.suggestion_message_type || '', // 评查状态:success/warning/error // 内容数据 content: data, // 评查内容(原始数据) contentPage: contentPage, // 页码映射 // 建议和配置 suggestion: point.suggestion_message || '', // 建议内容 postAction: point.post_action || '', // 后续动作:manual/auto actionContent: point.action_config || '', // 动作配置 legalBasis: point.references_laws || {}, // 法律依据 evaluationConfig: point.evaluation_config || {}, // 评查配置 // 评分信息 score: point.score || 0, // 满分分数 finalScore: result.final_score, // 最终得分(交叉评查) machineScore: result.machine_score, // 机器评分 result: result.evaluated_results?.result, // 评查结果:true/false // 交叉评查 failMessage: point.fail_message || '', // 不通过提示 passMessage: point.pass_message || '', // 通过提示 // 日志 evaluatedPointResultsLog: evaluatedPointResultsLog || {} // 规则执行日志 } ``` --- ### 🔟 计算统计数据(lines 687-712) **统计维度**: ```typescript const stats: StatsData = { total: 0, // 总评查点数量 success: 0, // 通过数量 warning: 0, // 警告数量 error: 0, // 错误数量 score: 0 // 总分数 }; ``` **计算逻辑**: ```typescript resultData.forEach(item => { // 1. 成功数量统计 if (item.result === true) { stats.success += 1; } // 2. 警告和错误数量统计 else if (item.result === false) { if (item.status === 'warning') { stats.warning += 1; } else if (item.status === 'error') { stats.error += 1; } } // 3. 分数累加 stats.score += item.score || 0; }); ``` **统计说明**: - `total` = `evaluationResultsData.length`(line 689) - `success` = `result === true` 的数量 - `warning` = `result === false && status === 'warning'` 的数量 - `error` = `result === false && status === 'error'` 的数量 - `score` = 所有评查点满分之和 --- ### 1️⃣1️⃣ 构建评查信息(lines 714-736) **目标**:生成评查总览信息 **步骤**: #### a) 找出最新评查时间(lines 716-721) ```typescript let latestUpdatedAt = ''; evaluationResultsData.forEach(result => { if (result.updated_at && (!latestUpdatedAt || result.updated_at > latestUpdatedAt)) { latestUpdatedAt = result.updated_at.toString(); } }); ``` #### b) 提取不重复的规则组名称(line 724) ```typescript const uniqueGroups = Array.from(new Set(resultData.map(item => item.groupName))).filter(Boolean); ``` #### c) 计算问题数量(line 727) ```typescript const issueCount = stats.warning + stats.error; ``` #### d) 构建评查信息对象(lines 730-736) ```typescript const reviewInfo = { reviewTime: dayjs.utc(latestUpdatedAt).format('YYYY-MM-DD HH:mm:ss'), // 评查时间 reviewModel: 'DeepSeek', // 评查模型 ruleGroup: uniqueGroups.join('、'), // 规则组名称(用顿号分隔) result: issueCount > 0 ? 'warning' : 'success', // 总体结果 issueCount: issueCount // 问题数量 }; ``` --- ## 📊 数据流转图 ```mermaid graph LR A[evaluation_results] --> B[提取 evaluation_point_id] B --> C[查询 evaluation_points] C --> D[提取 evaluation_point_groups_id] D --> E[查询 evaluation_point_groups] A --> F[提取 manual 类型的评查点] F --> G[查询 audit_status] A --> H[构建 resultData] C --> H E --> H G --> H H --> I[计算 stats] I --> J[构建 reviewInfo] K[documents] --> H L[contract_structure_comparison] --> M[返回] N[cross_scoring_proposals] --> M H --> M I --> M J --> M K --> M style A fill:#87CEEB style C fill:#87CEEB style E fill:#87CEEB style G fill:#87CEEB style K fill:#87CEEB style L fill:#87CEEB style N fill:#87CEEB style M fill:#FFD700 ``` --- ## 🎯 关键数据关系 ### 表关系图 ``` documents (文档表) ↓ (1:N) evaluation_results (评查结果表) ↓ (N:1) evaluation_points (评查点表) ↓ (N:1) evaluation_point_groups (评查点组表) evaluation_results + evaluation_points (manual 类型) ↓ (1:1) audit_status (人工审核状态表) documents ↓ (1:1) contract_structure_comparison (合同结构比对表) documents + evaluation_results ↓ (1:N) cross_scoring_proposals (评分提案表) ``` ### 字段映射关系 | 源表 | 源字段 | 目标表 | 目标字段 | |-----|-------|-------|---------| | evaluation_results | evaluation_point_id | evaluation_points | id | | evaluation_points | evaluation_point_groups_id | evaluation_point_groups | id | | evaluation_results | document_id + evaluation_point_id | audit_status | document_id + evaluation_point_id | | documents | id | contract_structure_comparison | document_id | | documents | id | cross_scoring_proposals | document_id | --- ## 🚨 错误处理 ### 错误返回格式 ```typescript { error: string, status: number } ``` ### 错误场景 | 错误场景 | 返回内容 | 状态码 | |---------|---------|--------| | 用户未认证 | `{ error: '用户身份验证失败' }` | 401 | | 文档不存在 | `{ error: documentData.error }` | 500 | | 合同比对数据错误 | `{ error: contractStructureComparisonResponse.error }` | 500 | | 评查结果为空 | `{ data: [], stats: {...}, error: '获取评查结果数据失败' }` | - | | 评查点ID为空 | `{ data: [], stats: {...}, error: '获取评查点ID失败' }` | - | --- ## 💡 核心业务逻辑 ### 1. 评查状态判断逻辑 ```typescript // 评查结果 (result) 决定是否通过 if (item.result === true) { // 通过 stats.success += 1; } else if (item.result === false) { // 未通过,根据 status 区分严重程度 if (item.status === 'warning') { stats.warning += 1; // 警告 } else if (item.status === 'error') { stats.error += 1; // 错误 } } ``` ### 2. 页码提取优先级 ``` 1. 优先从 data[key].page 提取 2. 提取数字部分(去除文本) 3. 如果为空,从 ocrResult 中根据 key 前缀查找 4. 仍然为空,设为空字符串 ``` ### 3. 人工审核默认值设置 ```typescript // 对于 post_action === 'manual' 的评查点 // 如果没有 audit_status 记录,设置默认值 { id: '', status: 0, // 0 表示待审核 message: '' } ``` ### 4. 总体评查结果判断 ```typescript const issueCount = stats.warning + stats.error; const result = issueCount > 0 ? 'warning' : 'success'; ``` **规则**: - 有任何 warning 或 error → `'warning'` - 全部通过 → `'success'` --- ## 📈 性能优化点 ### 批量查询策略 1. **收集ID后批量查询**: ```typescript // 不好的做法:循环内单独查询 for (const result of evaluationResultsData) { await postgrestGet('evaluation_points', { filter: { id: result.evaluation_point_id } }); } // 好的做法:收集ID后批量查询(当前实现) const evaluationPointIds = evaluationResultsData.map(item => item.evaluation_point_id); await postgrestGet('evaluation_points', { filter: { 'id': `in.(${evaluationPointIds.join(',')})` } }); ``` 2. **使用 Map 进行快速查找**: ```typescript // O(1) 时间复杂度查找 const pointsMap = new Map(); const point = pointsMap.get(evaluation_point_id); ``` ### 查询顺序优化 ``` 1. 先查评查结果 → 获取所有相关ID 2. 批量查询关联数据 → 减少数据库往返次数 3. 内存中构建数据 → Map 映射快速关联 ``` --- ## 🔍 调试建议 ### 关键日志位置 1. **文档附件数据**:line 170 ```typescript console.log('文档附件的数据', JSON.stringify(contractStructureComparisonData, null, 2)); ``` 2. **评查信息**:line 737 ```typescript console.log("reviewInfo-------", JSON.stringify(reviewInfo, null, 2)); ``` 3. **已注释的调试日志**: - line 161: `contract_structure_comparison` 响应 - line 194: `documentData` - line 205: `evaluationResultsResponse` - line 272: `manualReviewPoints` - line 310: `manualReviewPoints` - line 319: `pointsMap` - line 326: `groupsMap` - line 358: `evaluatedPointResultsLog` - line 372-374: `result`, `datacontent`, `documentData` ### 建议添加日志的位置 ```typescript // 1. 查询结果数量 console.log(`📊 评查结果数量: ${evaluationResultsData.length}`); // 2. 各类评查点统计 console.log(`📊 统计数据:`, stats); // 3. 人工审核评查点数量 console.log(`👤 需人工审核: ${manualReviewPointsIds.length} 个`); // 4. 评分提案数量 console.log(`📝 评分提案: ${scoringProposalsData.length} 个`); ``` --- ## 📝 使用示例 ### 调用示例 ```typescript // 在 Remix loader 中调用 export async function loader({ params, request }: LoaderFunctionArgs) { const fileId = params.fileId; if (!fileId) { return json({ error: '文档ID不能为空' }, { status: 400 }); } const result = await getReviewPoints(fileId, request); if (result.error) { return json({ error: result.error }, { status: result.status || 500 }); } return json(result); } ``` ### 前端使用示例 ```typescript // 在 React 组件中使用 const { data, stats, reviewInfo, document, comparison_document } = useLoaderData(); // 显示统计信息

总计: {stats.total}

通过: {stats.success}

警告: {stats.warning}

错误: {stats.error}

总分: {stats.score}

// 显示评查信息

评查时间: {reviewInfo.reviewTime}

评查模型: {reviewInfo.reviewModel}

规则组: {reviewInfo.ruleGroup}

总体结果: {reviewInfo.result}

问题数量: {reviewInfo.issueCount}

// 遍历评查点 {data.map(point => (

{point.title}

状态: {point.status}

建议: {point.suggestion}

{/* 更多字段... */}
))} ``` --- ## 🎓 总结 ### 核心功能 1. **多表联查**:关联查询 8 张表的数据 2. **数据聚合**:将分散的数据聚合为前端友好的格式 3. **统计计算**:实时计算评查统计数据 4. **智能映射**:通过 Map 实现高效数据关联 5. **页码提取**:智能提取和匹配页码信息 ### 适用场景 - ✅ 文档评查结果展示页面 - ✅ 评查点详情查看 - ✅ 人工审核功能 - ✅ 交叉评查功能 - ✅ 评查统计分析 ### 性能特点 - **批量查询**:减少数据库往返次数 - **内存映射**:O(1) 时间复杂度查找 - **并行处理**:多个无依赖查询可并行执行 ### 扩展性 - ✅ 支持新增评查点类型 - ✅ 支持自定义统计维度 - ✅ 支持多种评查模式(自动/人工/交叉) --- ## 📦 完整返回数据结构详解 ### 返回数据类型定义 ```typescript interface GetReviewPointsResult { data: ReviewPointResult[]; // 评查点结果列表 stats: StatsData; // 统计数据 reviewInfo: ReviewInfo; // 评查信息 document: Document; // 文档数据 comparison_document: ComparisonDoc; // 合同结构比对数据 scoring_proposals: ScoringProposal[]; // 评分提案(交叉评查) } ``` ### 完整返回数据示例 ```json { "data": [ { "id": 1001, "documentId": "123", "pointId": 5, "editAuditStatusId": "", "editAuditStatus": 0, "editAuditStatusMessage": "", "title": "立案报告表中的当事人信息与营业执照不一致", "pointName": "当事人信息一致性检查", "groupName": "合同形式要素", "status": "error", "content": { "立案报告表-当事人-单位-名称": { "page": 1, "value": "张三烟草店" }, "证据复制(提取)单-营业执照-名称": { "page": 4, "value": "李四烟草店" } }, "contentPage": { "立案报告表-当事人-单位-名称": "1", "证据复制(提取)单-营业执照-名称": "4" }, "suggestion": "请核对当事人信息,确保立案报告表与营业执照信息一致", "postAction": "manual", "actionContent": "", "legalBasis": { "name": "烟草专卖法", "articles": ["第三十七条", "第三十八条"] }, "evaluationConfig": { "type": "consistency", "compareMethod": "exact" }, "score": 5, "finalScore": null, "machineScore": 5, "result": false, "failMessage": "当事人信息不一致", "passMessage": "当事人信息一致", "evaluatedPointResultsLog": { "rules": [ { "id": "0", "type": "consistency", "res": false, "config": { "logic": "all", "pairs": [ { "sourceField": { "立案报告表-当事人-单位-名称": { "page": 1, "value": "张三烟草店" } }, "targetField": { "证据复制(提取)单-营业执照-名称": { "page": 4, "value": "李四烟草店" } }, "compareMethod": "exact", "res": false } ] } } ] } }, { "id": 1002, "documentId": "123", "pointId": 8, "editAuditStatusId": "201", "editAuditStatus": 1, "editAuditStatusMessage": "已人工审核确认", "title": "签名完整性检查通过", "pointName": "执法人员签名检查", "groupName": "程序合法性", "status": "success", "content": { "现场笔录-执法人员签名": { "page": 3, "value": "有" } }, "contentPage": { "现场笔录-执法人员签名": "3" }, "suggestion": "", "postAction": "manual", "actionContent": "", "legalBasis": {}, "evaluationConfig": {}, "score": 10, "finalScore": null, "machineScore": 10, "result": true, "failMessage": "", "passMessage": "签名完整", "evaluatedPointResultsLog": { "rules": [] } } ], "stats": { "total": 15, "success": 12, "warning": 2, "error": 1, "score": 100 }, "reviewInfo": { "reviewTime": "2024-01-15 10:30:00", "reviewModel": "DeepSeek", "ruleGroup": "合同形式要素、合同实质要素、程序合法性", "result": "warning", "issueCount": 3 }, "document": { "id": 123, "name": "烟草专卖案卷-示例.pdf", "path": "/uploads/2024/01/document123.pdf", "user_id": 6, "document_type_id": 1, "audit_status": 0, "created_at": "2024-01-15T09:00:00Z", "updated_at": "2024-01-15T10:30:00Z", "ocrResult": { "ocr_result": { "立案报告表": { "pages": [1] }, "现场笔录": { "pages": [2, 3] }, "证据复制(提取)单": { "pages": [4, 5] } } } }, "comparison_document": { "id": 45, "document_id": 123, "template_contract_path": "/templates/standard_contract.pdf", "comparison_results": { "合同封面": [ { "field": "合同名称", "status": "normal", "similarity": 1.0 } ], "合同条款": [ { "field": "甲方信息", "status": "abnormal", "similarity": 0.65 } ] } }, "scoring_proposals": [ { "id": 301, "evaluation_result_id": 1001, "proposer_id": 12, "proposed_score": 3, "reason": "虽然信息不一致,但属于笔误,建议降低扣分", "status": "pending", "created_at": "2024-01-15T11:00:00Z", "updated_at": "2024-01-15T11:00:00Z", "document_id": 123 } ] } ``` ### 返回字段详细说明 #### 1. data 数组(ReviewPointResult[]) 每个评查点结果对象包含以下字段: | 字段名 | 类型 | 说明 | 来源表 | |-------|------|------|--------| | `id` | string\|number | 评查结果ID | evaluation_results.id | | `documentId` | string | 文档ID | 参数 fileId | | `pointId` | string\|number | 评查点ID | evaluation_points.id | | `editAuditStatusId` | string\|number | 审核状态ID | audit_status.id | | `editAuditStatus` | number | 审核状态(0-待审核,1-已审核) | audit_status.edit_audit_status | | `editAuditStatusMessage` | string | 审核意见 | audit_status.message | | `title` | string | 评查点标题/问题描述 | evaluation_results.evaluated_results.message | | `pointName` | string | 评查点名称 | evaluation_points.name | | `groupName` | string | 评查点组名称 | evaluation_point_groups.name | | `status` | string | 评查状态(success/warning/error) | evaluation_points.suggestion_message_type | | `content` | object | 评查内容(原始数据) | evaluation_results.evaluated_results.data | | `contentPage` | object | 页码映射 | 从 content 提取 + ocrResult 补充 | | `suggestion` | string | 建议内容 | evaluation_points.suggestion_message | | `postAction` | string | 后续动作(manual/auto) | evaluation_points.post_action | | `actionContent` | string\|object | 动作配置 | evaluation_points.action_config | | `legalBasis` | object | 法律依据 | evaluation_points.references_laws | | `evaluationConfig` | object | 评查配置 | evaluation_points.evaluation_config | | `score` | number | 满分分数 | evaluation_points.score | | `finalScore` | number\|null | 最终得分(交叉评查) | evaluation_results.final_score | | `machineScore` | number | 机器评分 | evaluation_results.machine_score | | `result` | boolean | 评查结果(true/false) | evaluation_results.evaluated_results.result | | `failMessage` | string | 不通过提示 | evaluation_points.fail_message | | `passMessage` | string | 通过提示 | evaluation_points.pass_message | | `evaluatedPointResultsLog` | object | 规则执行日志 | evaluation_results.evaluated_point_results_log | #### 2. stats 对象(StatsData) | 字段名 | 类型 | 说明 | 计算方式 | |-------|------|------|---------| | `total` | number | 总评查点数量 | evaluationResultsData.length | | `success` | number | 通过数量 | result === true 的数量 | | `warning` | number | 警告数量 | result === false && status === 'warning' | | `error` | number | 错误数量 | result === false && status === 'error' | | `score` | number | 总分数 | 所有评查点 score 之和 | #### 3. reviewInfo 对象(ReviewInfo) | 字段名 | 类型 | 说明 | 计算方式 | |-------|------|------|---------| | `reviewTime` | string | 评查时间 | 评查结果中最新的 updated_at | | `reviewModel` | string | 评查模型 | 固定值 'DeepSeek' | | `ruleGroup` | string | 规则组名称 | 所有不重复的 groupName,用顿号分隔 | | `result` | string | 总体结果(success/warning) | issueCount > 0 ? 'warning' : 'success' | | `issueCount` | number | 问题数量 | stats.warning + stats.error | #### 4. document 对象(Document) 文档基本信息,包含: - 文档元数据(id, name, path等) - OCR 识别结果(ocrResult) #### 5. comparison_document 对象(ComparisonDoc) 合同结构比对数据,包含: - 模板文件路径(template_contract_path) - 比对结果(comparison_results) #### 6. scoring_proposals 数组(ScoringProposal[]) 交叉评查评分提案列表,每个提案包含: - 提案ID、提议人、建议分数、理由 - 提案状态、创建时间等 --- ## 🔄 数据依赖关系详解 ### 查询依赖链 ``` 1. getUserSession (认证) ↓ 2. documents (fileId) → 获取文档数据 ↓ 3. contract_structure_comparison (document_id) → 获取比对数据 ↓ 4. evaluation_results (document_id) → 获取评查结果 ⭐核心 ↓ ├─→ 5. evaluation_points (evaluation_point_id IN [...]) │ ↓ │ └─→ 6. evaluation_point_groups (evaluation_point_groups_id IN [...]) │ ├─→ 7. audit_status (document_id + evaluation_point_id IN [...]) │ ↑ 仅查询 post_action === 'manual' 的评查点 │ └─→ 8. cross_scoring_proposals (document_id) ``` ### 数据合并流程 ``` evaluation_results (N条) + evaluation_points (N条) → pointsMap + evaluation_point_groups (M条) → groupsMap + audit_status (K条) → editAuditStatusMap ↓ 遍历 evaluation_results,通过 Map 快速查找关联数据 ↓ 构建 resultData 数组(N条 ReviewPointResult) ↓ 计算 stats 统计数据 ↓ 构建 reviewInfo 评查信息 ↓ 返回完整数据 ``` ### 关键ID映射关系 | 源ID | 用于查询 | 目标ID | 关系类型 | |------|---------|--------|---------| | `fileId` | documents | `documents.id` | 1:1 | | `fileId` | contract_structure_comparison | `contract_structure_comparison.document_id` | 1:1 | | `fileId` | evaluation_results | `evaluation_results.document_id` | 1:N | | `evaluation_results.evaluation_point_id` | evaluation_points | `evaluation_points.id` | N:1 | | `evaluation_points.evaluation_point_groups_id` | evaluation_point_groups | `evaluation_point_groups.id` | N:1 | | `evaluation_results.evaluation_point_id` (manual) | audit_status | `audit_status.evaluation_point_id` | 1:1 | | `fileId` | cross_scoring_proposals | `cross_scoring_proposals.document_id` | 1:N | --- ## 🔍 查询执行时序图 ```mermaid sequenceDiagram participant Client participant getReviewPoints participant DB Client->>getReviewPoints: 调用(fileId, request) getReviewPoints->>DB: 1. getUserSession (验证) DB-->>getReviewPoints: userInfo + JWT getReviewPoints->>DB: 2. SELECT * FROM documents WHERE id = fileId DB-->>getReviewPoints: document (1条) getReviewPoints->>DB: 3. SELECT * FROM contract_structure_comparison WHERE document_id = fileId DB-->>getReviewPoints: comparison (0-1条) getReviewPoints->>DB: 4. SELECT * FROM evaluation_results WHERE document_id = fileId DB-->>getReviewPoints: evaluation_results (N条) ⭐ Note over getReviewPoints: 提取 evaluation_point_id 列表 getReviewPoints->>DB: 5. SELECT * FROM evaluation_points WHERE id IN (...) DB-->>getReviewPoints: evaluation_points (N条) Note over getReviewPoints: 提取 evaluation_point_groups_id 列表 getReviewPoints->>DB: 6. SELECT * FROM evaluation_point_groups WHERE id IN (...) DB-->>getReviewPoints: evaluation_point_groups (M条) Note over getReviewPoints: 筛选 post_action='manual' 的评查点 getReviewPoints->>DB: 7. SELECT * FROM audit_status WHERE document_id = fileId AND evaluation_point_id IN (...) DB-->>getReviewPoints: audit_status (K条) getReviewPoints->>DB: 8. SELECT * FROM cross_scoring_proposals WHERE document_id = fileId AND deleted_at IS NULL DB-->>getReviewPoints: scoring_proposals (P条) Note over getReviewPoints: 构建 Map 映射
pointsMap, groupsMap, editAuditStatusMap Note over getReviewPoints: 遍历 evaluation_results
合并数据,提取页码 Note over getReviewPoints: 计算统计数据 stats Note over getReviewPoints: 构建评查信息 reviewInfo getReviewPoints-->>Client: 返回完整数据 ``` --- ## 💾 数据库表结构参考 ### evaluation_results 表(核心表) ```sql CREATE TABLE evaluation_results ( id SERIAL PRIMARY KEY, document_id INTEGER NOT NULL, evaluation_point_id INTEGER NOT NULL, evaluated_results JSONB, -- { result, message, data } evaluated_point_results_log JSONB, -- { rules: [...] } final_score NUMERIC(5,2), machine_score NUMERIC(5,2), created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), FOREIGN KEY (document_id) REFERENCES documents(id), FOREIGN KEY (evaluation_point_id) REFERENCES evaluation_points(id) ); ``` ### evaluation_points 表 ```sql CREATE TABLE evaluation_points ( id SERIAL PRIMARY KEY, evaluation_point_groups_id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, suggestion_message_type VARCHAR(50), -- success/warning/error suggestion_message TEXT, score NUMERIC(5,2), post_action VARCHAR(50), -- manual/auto action_config JSONB, references_laws JSONB, evaluation_config JSONB, fail_message TEXT, pass_message TEXT, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), FOREIGN KEY (evaluation_point_groups_id) REFERENCES evaluation_point_groups(id) ); ``` ### audit_status 表 ```sql CREATE TABLE audit_status ( id SERIAL PRIMARY KEY, document_id INTEGER NOT NULL, evaluation_point_id INTEGER NOT NULL, evaluation_result_id INTEGER NOT NULL, edit_audit_status INTEGER DEFAULT 0, -- 0-待审核, 1-已审核 message TEXT, user_id INTEGER, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), FOREIGN KEY (document_id) REFERENCES documents(id), FOREIGN KEY (evaluation_point_id) REFERENCES evaluation_points(id), FOREIGN KEY (evaluation_result_id) REFERENCES evaluation_results(id) ); ``` --- **最后更新**:2025-11-26 **文档版本**:v2.0 **新增内容**:完整返回数据结构、数据依赖关系、查询时序图、数据库表结构参考