626 lines
16 KiB
Markdown
626 lines
16 KiB
Markdown
# getReviewPoints 接口对接文档
|
||
|
||
> 面向前端开发的 API 对接指南
|
||
|
||
## 📋 概述
|
||
|
||
本接口用于替代前端直接调用 PostgREST 的 `getReviewPoints` 方法,**数据格式与原实现完全一致**,前端只需更改调用地址即可完成迁移。
|
||
|
||
---
|
||
|
||
## 🔗 接口信息
|
||
|
||
| 项目 | 内容 |
|
||
|-----|------|
|
||
| **接口地址** | `GET /api/v3/review-points/{document_id}` |
|
||
| **请求方式** | GET |
|
||
| **认证方式** | JWT Token(Authorization: Bearer xxx) |
|
||
| **Content-Type** | application/json |
|
||
|
||
---
|
||
|
||
## 📥 请求参数
|
||
|
||
### 路径参数
|
||
|
||
| 参数名 | 类型 | 必填 | 说明 |
|
||
|-------|------|-----|------|
|
||
| `document_id` | number | ✅ | 文档ID,必须大于0 |
|
||
|
||
### 请求头
|
||
|
||
| Header | 必填 | 说明 |
|
||
|--------|-----|------|
|
||
| `Authorization` | ✅ | Bearer Token,格式:`Bearer {JWT_TOKEN}` |
|
||
|
||
### 请求示例
|
||
|
||
```http
|
||
GET /api/v3/review-points/123 HTTP/1.1
|
||
Host: your-api-domain.com
|
||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||
```
|
||
|
||
---
|
||
|
||
## 📤 响应数据
|
||
|
||
### 成功响应 (HTTP 200)
|
||
|
||
```typescript
|
||
interface GetReviewPointsResponse {
|
||
data: ReviewPointResult[]; // 评查点结果列表
|
||
stats: StatsData; // 统计数据
|
||
reviewInfo: ReviewInfo; // 评查信息
|
||
document: DocumentData; // 文档数据
|
||
comparison_document: ComparisonDoc; // 合同结构比对数据
|
||
scoring_proposals: ScoringProposal[]; // 评分提案
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 数据结构详解
|
||
|
||
### 1. ReviewPointResult(评查点结果)
|
||
|
||
每个评查点结果包含以下字段:
|
||
|
||
```typescript
|
||
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(统计数据)
|
||
|
||
```typescript
|
||
interface StatsData {
|
||
total: number; // 总评查点数量
|
||
success: number; // 通过数量
|
||
warning: number; // 警告数量
|
||
error: number; // 错误数量
|
||
score: number; // 总分数
|
||
}
|
||
```
|
||
|
||
### 3. ReviewInfo(评查信息)
|
||
|
||
```typescript
|
||
interface ReviewInfo {
|
||
reviewTime: string; // 评查时间(YYYY-MM-DD HH:mm:ss)
|
||
reviewModel: string; // 评查模型(固定 "DeepSeek")
|
||
ruleGroup: string; // 规则组名称(用顿号分隔)
|
||
result: string; // 总体结果:success/warning
|
||
issueCount: number; // 问题数量
|
||
}
|
||
```
|
||
|
||
### 4. DocumentData(文档数据)
|
||
|
||
```typescript
|
||
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(合同结构比对数据)
|
||
|
||
```typescript
|
||
interface ComparisonDoc {
|
||
id?: string | number;
|
||
document_id?: string | number;
|
||
template_contract_path: string; // 模板文件路径
|
||
comparison_results?: object; // 比对结果
|
||
}
|
||
```
|
||
|
||
**注意**:当无比对数据时,返回 `{ template_contract_path: "" }`
|
||
|
||
### 6. ScoringProposal(评分提案)
|
||
|
||
```typescript
|
||
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;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📝 完整响应示例
|
||
|
||
```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 }
|
||
]
|
||
}
|
||
},
|
||
"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
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## ❌ 错误响应
|
||
|
||
### 错误响应格式
|
||
|
||
```typescript
|
||
interface ErrorResponse {
|
||
error: string; // 错误消息
|
||
status: number; // HTTP状态码
|
||
}
|
||
```
|
||
|
||
### 错误码说明
|
||
|
||
| HTTP状态码 | error 消息 | 说明 |
|
||
|-----------|-----------|------|
|
||
| 401 | 用户身份验证失败 | Token无效或过期 |
|
||
| 403 | 无权访问此文档 | 用户无权限查看该文档 |
|
||
| 404 | 文档不存在 | document_id对应的文档不存在 |
|
||
| 500 | 获取评查点结果失败: xxx | 服务器内部错误 |
|
||
|
||
### 错误响应示例
|
||
|
||
```json
|
||
{
|
||
"error": "无权访问此文档",
|
||
"status": 403
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔄 前端迁移指南
|
||
|
||
### 迁移前(直接调用 PostgREST)
|
||
|
||
```typescript
|
||
// 原来需要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', {...});
|
||
|
||
// 然后在前端组装数据...
|
||
```
|
||
|
||
### 迁移后(调用后端接口)
|
||
|
||
```typescript
|
||
// 现在只需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 示例
|
||
|
||
```typescript
|
||
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 示例
|
||
|
||
```typescript
|
||
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 示例
|
||
|
||
```typescript
|
||
// 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**: 返回空数据结构:
|
||
```json
|
||
{
|
||
"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
|
||
**维护者**:后端开发团队
|