Files
leaudit-platform-frontend/auth_doc/review_points_api_frontend.md
2025-12-05 00:09:32 +08:00

16 KiB
Raw Permalink Blame History

getReviewPoints 接口对接文档

面向前端开发的 API 对接指南

📋 概述

本接口用于替代前端直接调用 PostgREST 的 getReviewPoints 方法,数据格式与原实现完全一致,前端只需更改调用地址即可完成迁移。


🔗 接口信息

项目 内容
接口地址 GET /api/v3/review-points/{document_id}
请求方式 GET
认证方式 JWT TokenAuthorization: Bearer xxx
Content-Type application/json

📥 请求参数

路径参数

参数名 类型 必填 说明
document_id number 文档ID,必须大于0

请求头

Header 必填 说明
Authorization Bearer Token,格式:Bearer {JWT_TOKEN}

请求示例

GET /api/v3/review-points/123 HTTP/1.1
Host: your-api-domain.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

📤 响应数据

成功响应 (HTTP 200)

interface GetReviewPointsResponse {
  data: ReviewPointResult[];          // 评查点结果列表
  stats: StatsData;                   // 统计数据
  reviewInfo: ReviewInfo;             // 评查信息
  document: DocumentData;             // 文档数据
  comparison_document: ComparisonDoc; // 合同结构比对数据
  scoring_proposals: ScoringProposal[]; // 评分提案
}

📊 数据结构详解

1. ReviewPointResult(评查点结果)

每个评查点结果包含以下字段:

interface ReviewPointResult {
  // ===== 基本信息 =====
  id: string | number;                // 评查结果ID
  documentId: string;                 // 文档ID
  pointId: string | number;           // 评查点ID

  // ===== 审核状态 =====
  editAuditStatusId: string | number; // 审核状态ID
  editAuditStatus: number;            // 审核状态:0-待审核,1-已审核
  editAuditStatusMessage: string;     // 审核意见

  // ===== 显示信息 =====
  title: string;                      // 评查点标题/问题描述
  pointName: string;                  // 评查点名称
  groupName: string;                  // 评查点组名称
  status: string;                     // 评查状态:success/warning/error

  // ===== 内容数据 =====
  content: {                          // 评查内容(原始数据)
    [key: string]: {
      page: number | string;
      value: string;
    }
  };
  contentPage: {                      // 页码映射
    [key: string]: string;
  };

  // ===== 建议和配置 =====
  suggestion: string;                 // 建议内容
  postAction: string;                 // 后续动作:manual/auto/none
  actionContent: string | object;     // 动作配置
  legalBasis: object;                 // 法律依据
  evaluationConfig: object;           // 评查配置

  // ===== 评分信息 =====
  score: number;                      // 满分分数
  finalScore: number | null;          // 最终得分(交叉评查)
  machineScore: number | null;        // 机器评分
  result: boolean;                    // 评查结果:true-通过/false-不通过

  // ===== 交叉评查 =====
  failMessage: string;                // 不通过提示
  passMessage: string;                // 通过提示

  // ===== 日志 =====
  evaluatedPointResultsLog: {         // 规则执行日志
    rules?: Array<{
      id: string;
      type: string;
      res: boolean;
      config: object;
    }>;
  };
}

2. StatsData(统计数据)

interface StatsData {
  total: number;    // 总评查点数量
  success: number;  // 通过数量
  warning: number;  // 警告数量
  error: number;    // 错误数量
  score: number;    // 总分数
}

3. ReviewInfo(评查信息)

interface ReviewInfo {
  reviewTime: string;    // 评查时间(YYYY-MM-DD HH:mm:ss
  reviewModel: string;   // 评查模型(固定 "DeepSeek"
  ruleGroup: string;     // 规则组名称(用顿号分隔)
  result: string;        // 总体结果:success/warning
  issueCount: number;    // 问题数量
}

4. DocumentData(文档数据)

interface DocumentData {
  id: string | number;
  name: string;                       // 文档名称
  path: string;                       // 文档存储路径
  user_id: string | number;           // 上传用户ID
  document_type_id: string | number;  // 文档类型ID
  audit_status: number;               // 审核状态
  created_at: string;                 // 创建时间
  updated_at: string;                 // 更新时间
  ocrResult: {                        // OCR结果
    ocr_result: {
      [表单名称: string]: {
        pages: number[];
      }
    }
  };
  // ... 其他文档字段
}

5. ComparisonDoc(合同结构比对数据)

interface ComparisonDoc {
  id?: string | number;
  document_id?: string | number;
  template_contract_path: string;     // 模板文件路径
  comparison_results?: object;        // 比对结果
}

注意:当无比对数据时,返回 { template_contract_path: "" }

6. ScoringProposal(评分提案)

interface ScoringProposal {
  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;
}

📝 完整响应示例

{
  "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 }
      ]
    }
  },
  "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
    }
  ]
}

错误响应

错误响应格式

interface ErrorResponse {
  error: string;   // 错误消息
  status: number;  // HTTP状态码
}

错误码说明

HTTP状态码 error 消息 说明
401 用户身份验证失败 Token无效或过期
403 无权访问此文档 用户无权限查看该文档
404 文档不存在 document_id对应的文档不存在
500 获取评查点结果失败: xxx 服务器内部错误

错误响应示例

{
  "error": "无权访问此文档",
  "status": 403
}

🔄 前端迁移指南

迁移前(直接调用 PostgREST

// 原来需要7次API调用
const documentData = await getDocumentWithNoUserId(fileId, frontendJWT);
const contractStructureComparisonData = await postgrestGet('contract_structure_comparison', {...});
const evaluationResultsData = await postgrestGet('evaluation_results', {...});
const evaluationPointsData = await postgrestGet('evaluation_points', {...});
const groupsData = await postgrestGet('evaluation_point_groups', {...});
const auditStatusData = await postgrestGet('audit_status', {...});
const scoringProposalsData = await postgrestGet('cross_scoring_proposals', {...});

// 然后在前端组装数据...

迁移后(调用后端接口)

// 现在只需1次API调用
const response = await fetch(`/api/v3/review-points/${fileId}`, {
  method: 'GET',
  headers: {
    'Authorization': `Bearer ${frontendJWT}`,
    'Content-Type': 'application/json'
  }
});

const result = await response.json();

if (response.ok) {
  // 直接使用,数据格式完全一致
  const { data, stats, reviewInfo, document, comparison_document, scoring_proposals } = result;
} else {
  // 错误处理
  console.error(result.error);
}

Axios 示例

import axios from 'axios';

async function getReviewPoints(fileId: string, token: string) {
  try {
    const response = await axios.get(`/api/v3/review-points/${fileId}`, {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
    return response.data;
  } catch (error) {
    if (axios.isAxiosError(error) && error.response) {
      // 处理错误响应
      const { error: errorMsg, status } = error.response.data;
      throw new Error(errorMsg);
    }
    throw error;
  }
}

React Query 示例

import { useQuery } from '@tanstack/react-query';

function useReviewPoints(fileId: string) {
  return useQuery({
    queryKey: ['reviewPoints', fileId],
    queryFn: async () => {
      const response = await fetch(`/api/v3/review-points/${fileId}`, {
        headers: {
          'Authorization': `Bearer ${getToken()}`,
        }
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.error);
      }

      return response.json();
    },
    enabled: !!fileId,
  });
}

// 使用
function ReviewPointsPage({ fileId }) {
  const { data, isLoading, error } = useReviewPoints(fileId);

  if (isLoading) return <Loading />;
  if (error) return <Error message={error.message} />;

  return (
    <div>
      <Stats data={data.stats} />
      <ReviewInfo data={data.reviewInfo} />
      <ReviewPointsList data={data.data} />
    </div>
  );
}

Remix Loader 示例

// app/routes/review.$fileId.tsx
import { json, LoaderFunctionArgs } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';

export async function loader({ params, request }: LoaderFunctionArgs) {
  const fileId = params.fileId;

  if (!fileId) {
    return json({ error: '文档ID不能为空' }, { status: 400 });
  }

  // 获取JWT Token
  const token = await getJWTFromRequest(request);

  // 调用后端接口
  const response = await fetch(
    `${process.env.API_BASE_URL}/api/v3/review-points/${fileId}`,
    {
      headers: {
        'Authorization': `Bearer ${token}`,
      }
    }
  );

  const result = await response.json();

  if (!response.ok) {
    return json({ error: result.error }, { status: result.status || 500 });
  }

  return json(result);
}

export default function ReviewPage() {
  const { data, stats, reviewInfo, document, comparison_document, scoring_proposals } =
    useLoaderData<typeof loader>();

  // 直接使用,数据格式与原来完全一致
  return (
    <div>
      {/* 统计信息 */}
      <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>
      ))}
    </div>
  );
}

迁移检查清单

  • 更新 API 调用地址为 /api/v3/review-points/{document_id}
  • 确保 Authorization Header 携带有效的 JWT Token
  • 移除前端的多表查询和数据组装逻辑
  • 更新错误处理逻辑以匹配新的错误响应格式
  • 测试各种场景(正常、无评查结果、无权限、文档不存在)

常见问题

Q1: 数据格式会有变化吗?

A: 不会。后端接口返回的数据格式与原前端 getReviewPoints 方法完全一致,包括字段名、数据类型、嵌套结构等。

Q2: 需要修改前端的数据处理逻辑吗?

A: 不需要。只需将原来的多次 PostgREST 调用替换为单次后端 API 调用即可,返回数据可直接使用。

Q3: 性能会有影响吗?

A: 会更好。原来前端需要发起 7 次 HTTP 请求,现在只需 1 次。后端使用批量查询优化,减少数据库往返次数。

Q4: 无评查结果时返回什么?

A: 返回空数据结构:

{
  "data": [],
  "stats": { "total": 0, "success": 0, "warning": 0, "error": 0, "score": 0 },
  "reviewInfo": { ... },
  "document": { ... },
  "comparison_document": { "template_contract_path": "" },
  "scoring_proposals": []
}

Q5: 如何处理 Token 过期?

A: 接口返回 401 状态码时,前端应引导用户重新登录或刷新 Token。


📞 联系方式

如有问题,请联系后端开发人员。


文档版本v1.0 最后更新2025-11-26 维护者:后端开发团队