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

42 KiB
Raw Blame History

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 是字符串,尝试解析为 JSONlines 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_idline 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_idline 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 评查点设置默认值 0lines 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

步骤

  1. evaluated_results 中提取 messagedatalines 361-368
  2. 解析 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.lengthline 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'

📈 性能优化点

批量查询策略

  1. 收集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(',')})` }
    });
    
  2. 使用 Map 进行快速查找

    // O(1) 时间复杂度查找
    const pointsMap = new Map();
    const point = pointsMap.get(evaluation_point_id);
    

查询顺序优化

1. 先查评查结果 → 获取所有相关ID
2. 批量查询关联数据 → 减少数据库往返次数
3. 内存中构建数据 → Map 映射快速关联

🔍 调试建议

关键日志位置

  1. 文档附件数据line 170

    console.log('文档附件的数据', JSON.stringify(contractStructureComparisonData, null, 2));
    
  2. 评查信息line 737

    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

建议添加日志的位置

// 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>
))}

🎓 总结

核心功能

  1. 多表联查:关联查询 8 张表的数据
  2. 数据聚合:将分散的数据聚合为前端友好的格式
  3. 统计计算:实时计算评查统计数据
  4. 智能映射:通过 Map 实现高效数据关联
  5. 页码提取:智能提取和匹配页码信息

适用场景

  • 文档评查结果展示页面
  • 评查点详情查看
  • 人工审核功能
  • 交叉评查功能
  • 评查统计分析

性能特点

  • 批量查询:减少数据库往返次数
  • 内存映射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 新增内容:完整返回数据结构、数据依赖关系、查询时序图、数据库表结构参考