42 KiB
getReviewPoints 方法详细分析
📋 方法概述
文件位置:app/api/evaluation_points/reviews.ts:132-740
方法签名:
export async function getReviewPoints(fileId: string, request: Request)
功能:获取指定文档的所有评查点结果、统计数据、评查信息以及相关联的文档数据。
返回数据:
{
data: ReviewPointResult[], // 评查点结果列表
stats: StatsData, // 统计数据
reviewInfo: ReviewInfo, // 评查信息
document: Document, // 文档数据
comparison_document: ComparisonDoc, // 合同结构比对数据
scoring_proposals: ScoringProposal[] // 评分提案(交叉评查)
}
🔄 执行流程图
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
const { userInfo, frontendJWT } = await getUserSession(request);
验证:
- 检查
userInfo.user_id是否存在 - 如果不存在,返回 401 错误
2️⃣ 查询文档数据(lines 143-148)
表名:documents
调用方法:
const documentData = await getDocumentWithNoUserId(fileId, frontendJWT);
查询参数:
fileId- 文档ID
SQL 等价查询:
SELECT *
FROM documents
WHERE id = :fileId
LIMIT 1;
PostgREST API 请求:
GET /documents?id=eq.{fileId}&select=*
Authorization: Bearer {frontendJWT}
返回数据结构:
{
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[] // 该表单出现的页码列表
}
}
},
// ... 其他文档字段
}
}
数据示例:
{
"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)
- 显示文档名称和路径
错误处理:
if (documentData.error) {
console.error("获取文档数据错误:", documentData.error);
return Response.json({ error: documentData.error }, { status: documentData.status || 500 });
}
3️⃣ 查询合同结构比对数据(lines 151-191)
表名:contract_structure_comparison
查询参数:
{
select: '*',
filter: {
'document_id': `eq.${fileId}`
},
order: 'id.desc',
limit: 1,
token: frontendJWT
}
查询逻辑:
- 根据
document_id查询 - 按
id降序排序,取最新的一条记录 - 限制返回 1 条
SQL 等价查询:
SELECT *
FROM contract_structure_comparison
WHERE document_id = :fileId
ORDER BY id DESC
LIMIT 1;
PostgREST API 请求:
GET /contract_structure_comparison?document_id=eq.{fileId}&order=id.desc&limit=1&select=*
Authorization: Bearer {frontendJWT}
返回数据结构:
{
id: string | number,
document_id: string | number,
template_contract_path: string, // 模板文件路径
comparison_results: string | object, // 比对结果(JSON字符串或对象)
created_at: string,
updated_at: string,
// ... 其他字段
}
数据示例:
{
"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)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)if (!contractStructureComparisonData || contractStructureComparisonData.length === 0) { comparisonDocument = { template_contract_path: '' }; }
使用场景:
- 获取合同模板比对结果
- 提供模板文件路径用于对比显示
- 显示字段匹配状态和相似度
4️⃣ 查询评查结果(lines 196-215)
表名:evaluation_results ⭐ 核心表
查询参数:
{
select: '*',
filter: {
'document_id': `eq.${fileId}`
},
token: frontendJWT
}
查询逻辑:
- 根据
document_id查询该文档的所有评查结果 - 这是整个方法的核心数据源,后续查询都基于此
SQL 等价查询:
SELECT *
FROM evaluation_results
WHERE document_id = :fileId;
PostgREST API 请求:
GET /evaluation_results?document_id=eq.{fileId}&select=*
Authorization: Bearer {frontendJWT}
返回数据结构:
[
{
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,
// ... 其他字段
}
]
数据示例:
[
{
"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)
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
查询参数:
{
select: '*',
filter: {
'id': `in.(${evaluationPointIds.join(',')})`
},
token: frontendJWT
}
查询逻辑:
- 从评查结果中提取所有
evaluation_point_id(line 218) - 使用
IN操作符批量查询评查点详情
返回数据结构:
[
{
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
查询参数:
{
select: '*',
filter: {
'id': `in.(${groupIds.join(',')})`
},
token: frontendJWT
}
查询逻辑:
- 从评查点中提取所有
evaluation_point_groups_id(line 245) - 使用
IN操作符批量查询评查点组
返回数据结构:
[
{
id: string | number,
name: string, // 评查点组名称
// ... 其他字段
}
]
使用场景:
- 获取评查点所属的分组名称(如"合同形式要素"、"合同实质要素"等)
7️⃣ 查询人工审核状态(lines 272-308)
表名:audit_status
查询参数:
{
select: '*',
filter: {
'document_id': `eq.${fileId}`,
'evaluation_point_id': `in.(${manualReviewPointsIds.join(',')})`
},
token: frontendJWT
}
查询逻辑:
- 筛选出
post_action === 'manual'的评查点(line 273) - 只查询需要人工审核的评查点的审核状态
返回数据结构:
[
{
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
查询参数:
{
select: '*',
filter: {
'document_id': `eq.${fileId}`,
'deleted_at': `is.null`
},
token: frontendJWT
}
查询逻辑:
- 根据
document_id查询 - 排除已删除的提案(
deleted_at IS NULL)
返回数据结构:
[
{
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)
// 评查点映射
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)
const resultData: ReviewPointResult[] = evaluationResultsData.map(result => {
// ...
});
c) 提取评查内容和页码(lines 361-403)
步骤:
- 从
evaluated_results中提取message和data(lines 361-368) - 解析
data对象中的页码信息(lines 375-403)
页码提取逻辑:
// 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)
返回的字段:
{
// 基本信息
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)
统计维度:
const stats: StatsData = {
total: 0, // 总评查点数量
success: 0, // 通过数量
warning: 0, // 警告数量
error: 0, // 错误数量
score: 0 // 总分数
};
计算逻辑:
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)
let latestUpdatedAt = '';
evaluationResultsData.forEach(result => {
if (result.updated_at && (!latestUpdatedAt || result.updated_at > latestUpdatedAt)) {
latestUpdatedAt = result.updated_at.toString();
}
});
b) 提取不重复的规则组名称(line 724)
const uniqueGroups = Array.from(new Set(resultData.map(item => item.groupName))).filter(Boolean);
c) 计算问题数量(line 727)
const issueCount = stats.warning + stats.error;
d) 构建评查信息对象(lines 730-736)
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 // 问题数量
};
📊 数据流转图
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 |
🚨 错误处理
错误返回格式
{
error: string,
status: number
}
错误场景
| 错误场景 | 返回内容 | 状态码 |
|---|---|---|
| 用户未认证 | { error: '用户身份验证失败' } |
401 |
| 文档不存在 | { error: documentData.error } |
500 |
| 合同比对数据错误 | { error: contractStructureComparisonResponse.error } |
500 |
| 评查结果为空 | { data: [], stats: {...}, error: '获取评查结果数据失败' } |
- |
| 评查点ID为空 | { data: [], stats: {...}, error: '获取评查点ID失败' } |
- |
💡 核心业务逻辑
1. 评查状态判断逻辑
// 评查结果 (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. 人工审核默认值设置
// 对于 post_action === 'manual' 的评查点
// 如果没有 audit_status 记录,设置默认值
{
id: '',
status: 0, // 0 表示待审核
message: ''
}
4. 总体评查结果判断
const issueCount = stats.warning + stats.error;
const result = issueCount > 0 ? 'warning' : 'success';
规则:
- 有任何 warning 或 error →
'warning' - 全部通过 →
'success'
📈 性能优化点
批量查询策略
-
收集ID后批量查询:
// 不好的做法:循环内单独查询 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(',')})` } }); -
使用 Map 进行快速查找:
// O(1) 时间复杂度查找 const pointsMap = new Map(); const point = pointsMap.get(evaluation_point_id);
查询顺序优化
1. 先查评查结果 → 获取所有相关ID
2. 批量查询关联数据 → 减少数据库往返次数
3. 内存中构建数据 → Map 映射快速关联
🔍 调试建议
关键日志位置
-
文档附件数据:line 170
console.log('文档附件的数据', JSON.stringify(contractStructureComparisonData, null, 2)); -
评查信息:line 737
console.log("reviewInfo-------", JSON.stringify(reviewInfo, null, 2)); -
已注释的调试日志:
- 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
- line 161:
建议添加日志的位置
// 1. 查询结果数量
console.log(`📊 评查结果数量: ${evaluationResultsData.length}`);
// 2. 各类评查点统计
console.log(`📊 统计数据:`, stats);
// 3. 人工审核评查点数量
console.log(`👤 需人工审核: ${manualReviewPointsIds.length} 个`);
// 4. 评分提案数量
console.log(`📝 评分提案: ${scoringProposalsData.length} 个`);
📝 使用示例
调用示例
// 在 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);
}
前端使用示例
// 在 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>
))}
🎓 总结
核心功能
- 多表联查:关联查询 8 张表的数据
- 数据聚合:将分散的数据聚合为前端友好的格式
- 统计计算:实时计算评查统计数据
- 智能映射:通过 Map 实现高效数据关联
- 页码提取:智能提取和匹配页码信息
适用场景
- ✅ 文档评查结果展示页面
- ✅ 评查点详情查看
- ✅ 人工审核功能
- ✅ 交叉评查功能
- ✅ 评查统计分析
性能特点
- 批量查询:减少数据库往返次数
- 内存映射:O(1) 时间复杂度查找
- 并行处理:多个无依赖查询可并行执行
扩展性
- ✅ 支持新增评查点类型
- ✅ 支持自定义统计维度
- ✅ 支持多种评查模式(自动/人工/交叉)
📦 完整返回数据结构详解
返回数据类型定义
interface GetReviewPointsResult {
data: ReviewPointResult[]; // 评查点结果列表
stats: StatsData; // 统计数据
reviewInfo: ReviewInfo; // 评查信息
document: Document; // 文档数据
comparison_document: ComparisonDoc; // 合同结构比对数据
scoring_proposals: ScoringProposal[]; // 评分提案(交叉评查)
}
完整返回数据示例
{
"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 |
🔍 查询执行时序图
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 表(核心表)
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 表
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 表
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 新增内容:完整返回数据结构、数据依赖关系、查询时序图、数据库表结构参考