Files
leaudit-platform-frontend/docs/getReviewPoints方法详细分析.md
2025-12-05 00:09:32 +08:00

1520 lines
42 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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表<br/>人工审核状态]
H --> I[查询cross_scoring_proposals表<br/>评分提案]
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` 是字符串,尝试解析为 JSONlines 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 评查点设置默认值 0lines 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<string | number, EvaluationPoint>();
evaluationPointsData.forEach(point => {
pointsMap.set(point.id, point);
});
// 评查点组映射
const groupsMap = new Map<string | number, EvaluationPointGroup>();
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<typeof loader>();
// 显示统计信息
<div>
<p>总计: {stats.total}</p>
<p>通过: {stats.success}</p>
<p>警告: {stats.warning}</p>
<p>错误: {stats.error}</p>
<p>总分: {stats.score}</p>
</div>
// 显示评查信息
<div>
<p>评查时间: {reviewInfo.reviewTime}</p>
<p>评查模型: {reviewInfo.reviewModel}</p>
<p>规则组: {reviewInfo.ruleGroup}</p>
<p>总体结果: {reviewInfo.result}</p>
<p>问题数量: {reviewInfo.issueCount}</p>
</div>
// 遍历评查点
{data.map(point => (
<div key={point.id}>
<h3>{point.title}</h3>
<p>状态: {point.status}</p>
<p>建议: {point.suggestion}</p>
{/* 更多字段... */}
</div>
))}
```
---
## 🎓 总结
### 核心功能
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 映射<br/>pointsMap, groupsMap, editAuditStatusMap
Note over getReviewPoints: 遍历 evaluation_results<br/>合并数据,提取页码
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
**新增内容**:完整返回数据结构、数据依赖关系、查询时序图、数据库表结构参考