# 文档上传与列表 — 接口分析 & 接入方案

> 所有信息已逐文件验证。源代码位置均已标注。
>
> 说明：这份文档已经吸收旧的 `新系统版_documents_list接口.md` 与 `文档上传与评查接口.md`，后续以本文件作为文档上传、列表、详情、更新、删除、评查触发与状态查询的统一说明。

---

## 一、后端接口（已验证）

### 1.1 POST /api/upload — 文档上传

**源码**: `fastapi_modules/fastapi_leaudit/controllers/documentController.py:20-45`  
**实现**: `fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py:44-246`

```
URL:    POST /api/upload
格式:   multipart/form-data (扁平字段)
```

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `file` | UploadFile | **是** | — | 文件二进制 |
| `typeId` | int | typeId/typeCode 二选一 | None | 文档类型 ID |
| `typeCode` | str | typeId/typeCode 二选一 | None | 文档类型编码，如 `contract.construction` |
| `region` | str | 否 | `"default"` | 地区标识 |
| `fileRole` | str | 否 | `"primary"` | 文件角色：primary/attachment/template |
| `createdBy` | int | 否 | None | 前端可传，但后端当前以 JWT 中的 `user_id` 为准 |
| `autoRun` | bool | 否 | `false` | 是否上传后自动触发评查 |
| `speed` | str | 否 | `"normal"` | urgent / normal |

**内部流程**:
1. 校验文件名/内容非空 → 解析文档类型（查 `leaudit_document_types`）
2. SHA-256 去重（仅 primary 文件）：同名+同类型+同区域+同 hash → 复用已有记录，`duplicateUpload=true`
3. 非重复：生成 `internalDocumentNo`，匹配前一版本，递增 `versionNo`，写 `leaudit_documents`
4. 存 MinIO OSS，路径 `bdocs/{Region}/{TypeCode}/{Year}/...`
5. 写 `leaudit_document_files`
6. 若 `autoRun=true` → 调 `AuditService.Run()` 触发评查

**响应** `Result[DocumentUploadVO]` (`domian/vo/documentVo.py:8-27`):

```json
{
  "code": 200,
  "message": "ok",
  "data": {
    "documentId": 1,
    "internalDocumentNo": 1712345678000000001,
    "versionGroupKey": "abc123...",
    "versionNo": 2,
    "previousVersionId": 1,
    "rootVersionId": 1,
    "duplicateUpload": false,
    "fileId": 5,
    "typeId": 1,
    "typeCode": "contract.construction",
    "region": "meizhou",
    "fileName": "建设工程合同.pdf",
    "ossUrl": "http://minio/bdocs/...",
    "speed": "normal",
    "processingStatus": "waiting",
    "autoRunTriggered": true,
    "run": { "runId": 10, "status": "pending", ... }
  }
}
```

---

### 1.2 GET /api/documents/list — 文档列表

**源码**: `controllers/documentController.py:47-67`  
**实现**: `documentServiceImpl.py:248-443`

```
URL:    GET /api/documents/list
格式:   query params
```

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `page` | int | 否 (1) | 页码 |
| `pageSize` | int | 否 (20, max 100) | 每页条数 |
| `keyword` | str | 否 | 搜索文件名或归一化名 |
| `typeCode` | str | 否 | 文档类型编码过滤 |
| `region` | str | 否 | 地区过滤 |
| `processingStatus` | str | 否 | waiting/processing/completed/failed |
| `resultStatus` | str | 否 | pass/fail/partial/review/error |

**内部流程**:
1. `leaudit_documents d` JOIN `leaudit_document_files f` ON `f.document_id = d.id`
2. 条件: `d.is_latest_version=true`, `d.deleted_at IS NULL`, `f.is_active=true`, `f.file_role='primary'`
3. LEFT JOIN `leaudit_document_types dt`, `leaudit_audit_runs ar`
4. 子查询统计 `total_versions`（按 `version_group_key` 分组）
5. 批量加载历史版本（`is_latest_version=false` 的同组文档）

**响应** `Result[DocumentListPageVO]` (`domian/vo/documentVo.py:77-84`):

```json
{
  "code": 200, "message": "ok",
  "data": {
    "total": 100, "page": 1, "pageSize": 20, "totalPages": 5,
    "documents": [
      {
        "documentId": 1, "internalDocumentNo": 1712345678000000001,
        "versionGroupKey": "abc123", "versionNo": 2,
        "typeId": 1, "typeCode": "contract.construction",
        "region": "meizhou", "normalizedName": "建设工程合同",
        "fileId": 5, "fileName": "建设工程合同.pdf",
        "fileExt": ".pdf", "mimeType": "application/pdf",
        "fileSize": 1048576, "ossUrl": "http://minio/...",
        "processingStatus": "completed",
        "runStatus": "completed", "resultStatus": "pass",
        "totalScore": 85, "passedCount": 10,
        "failedCount": 2, "skippedCount": 0,
        "hasHistory": true, "totalVersions": 2,
        "historyVersions": [
          { "documentId": 1, "versionNo": 1, "fileName": "...", ... }
        ]
      }
    ]
  }
}
```

---

### 1.3 文档详情 / 更新 / 删除（现已实现）

#### GET /api/documents/{id}

- 用途：获取单个文档详情
- 返回：`Result[DocumentDetailVO]`
- 关键字段：
  - 继承列表接口的版本链、文件、run、统计字段
  - 新增 `documentNumber`、`remark`、`isTestDocument`、`auditStatus`、`pageCount`
- 历史版本：`historyVersions` 返回同一 `version_group_key` 下除当前文档外的其它未删除版本

#### PUT /api/documents/{id}

- 用途：更新文档元数据
- 请求体：`DocumentUpdateDTO`

```json
{
  "documentNumber": "粤烟合同〔2026〕001号",
  "remark": "地市管理员修订备注",
  "isTestDocument": false,
  "auditStatus": 1
}
```

- 字段说明：
  - `documentNumber`：若 `leaudit_documents.document_number` 列存在则写入
  - `remark`：若 `leaudit_documents.remark` 列存在则写入
  - `isTestDocument`：若 `leaudit_documents.is_test_document` 列存在则写入
  - `auditStatus`：若 `leaudit_documents.audit_status` 列存在则写入；若当前环境未加该列，则接口忽略该字段但不报错

#### DELETE /api/documents/{id}

- 用途：软删除文档
- 行为：
  - `leaudit_documents.deleted_at = now()`
  - 当前文档的 active file 置为 `is_active = false`
  - 若删除的是当前最新版本，则自动把同版本链中剩余的最高版本提升为 `is_latest_version = true`

#### 数据隔离规则（已在后端强制执行）

| 角色 | 范围 |
|------|------|
| `super_admin` / `provincial_admin` | 全量文档 |
| `admin` | **仅本地市 `region = user.area`** |
| `common` | **仅自己上传的文档 `created_by = current_user_id`** |

额外约束：
- `admin` 若在列表接口传入其他地市 `region`，后端直接返回空结果
- `common` 若试图访问、修改、删除他人文档，后端返回“文档不存在或无权访问”

#### 尚未完成

| 操作 | 当前 | 需要 |
|------|------|------|
| 附件追加 | ❌ | `POST /api/documents/{id}/attachments` — fileRole=attachment |

---

### 1.4 自动评查 / 手动评查 / 状态结果查询

#### POST /api/upload

- 上传成功且 `autoRun=true` 时，后端会直接调用 `AuditService.Run()` 创建 run
- 队列档位当前只保留：
  - `urgent`
  - `normal`

#### POST /api/audit/run

- 用途：对已存在文档手动触发评查
- 典型参数：
  - `documentId`
  - `speed=normal|urgent`
- 当前收口：
  - 若同一文档已有活动 run，后端优先复用，不重复创建

#### GET /api/audit/run/{runId}

- 用途：查询评查任务状态
- 关键状态：
  - `queued`
  - `running`
  - `completed`
  - `failed`
- 关键阶段：
  - `dispatch`
  - `prepare`
  - `ocr / extraction / evaluation / rescue / persist`

#### GET /api/audit/result/{runId}

- 用途：查询评查结果
- 返回来源：
  - `leaudit_rule_results`
  - `leaudit_field_results`
  - `leaudit_run_metrics`
  - `leaudit_run_errors`

#### 当前评查链路口径

1. 上传文档
2. 建立 `leaudit_documents / leaudit_document_files`
3. 创建 `leaudit_audit_runs`
4. 按 `speed` 投递 `leaudit.urgent` 或 `leaudit.normal`
5. worker 按 `runId` 查库
6. bridge 下载文档与规则 YAML 到本地临时文件
7. `NativeRunner` 构建原生 `AuditCtx`
8. `AuditService.audit(ctx)` 执行
9. `StorageAdapter` 结果写回 `leaudit_*`

---

## 二、前端调用现状（已验证）

### 2.1 上传 — `uploadDocumentToServer()`

**源码**: `new_doc_review/app/api/files/files-upload.ts:374-443`

```
URL:    ${UPLOAD_URL}/upload    (例: http://172.16.0.59:8096/upload)
格式:   FormData { file: Blob, upload_info: JSON字符串 }
```

前端实际发送的 FormData:

```
file:           Blob (文件二进制)
upload_info:    '{"type_id":1,"evaluation_level":"normal","document_number":null,"remark":null,"is_test_document":false,"document_id":null,"is_reupload":false,"attribute_type":null}'
```

**❌ 与后端完全不兼容。** 后端期望扁平字段 `typeId`/`typeCode`/`region`/`fileRole`/`autoRun`/`speed`，不接受嵌套的 `upload_info` JSON。

### 2.2 附件追加 — `appendContractAttachments()`

**源码**: `files-upload.ts:320-336`

```
URL:    ${UPLOAD_URL}/contracts/{documentId}/append_attachments
格式:   FormData { attachments: File[] }
```

后端没有对应接口，需要补。

### 2.3 合同模板上传 — `uploadContractTemplate()`

**源码**: `files-upload.ts:244-259`

```
URL:    ${UPLOAD_URL}/upload_contract_template
格式:   FormData { file: Blob }
```

后端没有对应接口。

### 2.4 列表 — `getDocumentsListFromAPI()`

**源码**: `new_doc_review/app/api/files/documents.ts:627-784`

```
URL:    ${API_BASE_URL}/api/documents/list   ✅ 正确
```

✅ 列表已接入新后端，但存在两个问题：
1. **字段映射层太厚**：`LeauditListItem → DocumentUI` 转换 50+ 行，且把 `fileExt` 映射为 `fileType`、`ossUrl` 映射为 `path` 等
2. **筛选项不全**：`documentNumber`、`auditStatus`、`dateFrom/dateTo` 在后端列表接口未支持，前端写了 `void` 占位

### 2.5 删除 — `deleteDocument()`

**源码**: `documents.ts:540-560`

```
URL:    /api/postgrest/proxy/documents?id=eq.{id}&user_id=eq.{userId}
方法:   DELETE
```

❌ 走 PostgREST 直删，未接入新后端。

### 2.6 编辑 — `updateDocument()`

**源码**: `documents.ts:485-510`

```
URL:    /api/postgrest/proxy/documents?id=eq.{id}
方法:   PATCH
```

❌ 走 PostgREST，未接入新后端。

---

## 三、接入方案

### 第 1 步：上传接口对齐 ⭐ 最高优先级

**前端改造** `uploadDocumentToServer()`：

```
改前:  FormData { file, upload_info: JSON.stringify({ type_id, evaluation_level, ... }) }
       POST ${UPLOAD_URL}/upload

改后:  FormData { file, typeId, typeCode, region, fileRole, createdBy, autoRun, speed }
       POST ${API_BASE_URL}/api/upload
```

字段映射：

| 前端旧参数 | 新后端参数 | 说明 |
|-----------|-----------|------|
| `upload_info.type_id` | `typeId` | 直接传 |
| — | `typeCode` | 补充，从 document types 查 |
| — | `region` | 从 sessionStorage / user info 取当前用户地区 |
| — | `fileRole` | `"primary"` |
| — | `createdBy` | 从 JWT payload 取 user_id |
| `evaluation_level` | `speed` | urgent/normal 映射 |
| — | `autoRun` | 默认 true（参考前端现有流程：上传后自动 treatment） |

附件和模板上传合并方案：
- 主文件：`POST /api/upload` + `fileRole=primary`
- 附件：`POST /api/upload` + `fileRole=attachment`（或批量复用主上传）
- 模板：`POST /api/upload` + `fileRole=template`

影响文件：
- `new_doc_review/app/api/files/files-upload.ts` — `uploadDocumentToServer()` 重写
- `new_doc_review/app/routes/files.upload.tsx` — `startUpload()` 适配新参数

### 第 2 步：列表字段精简

**前端字段直接对齐**，逐步去掉 `mapLeauditDocToAuditStatus`、`mapProcessingStatusToFileStatus` 等映射函数。`DocumentUI` 接口增加新字段，旧字段保留兼容。

影响文件：
- `new_doc_review/app/api/files/documents.ts` — 简化 `LeauditListItem → DocumentUI` 转换

### 第 3 步：补后端 CRUD

后端新增：

```
DELETE  /api/documents/{id}       → 软删除 (deleted_at = now())
PUT     /api/documents/{id}       → 更新元数据 (备注、测试标记)
GET     /api/documents/{id}       → 文档详情（含所有版本）
POST    /api/documents/{id}/attachments  → 追加附件
```

影响文件：
- `fastapi_modules/fastapi_leaudit/controllers/documentController.py`
- `fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py`

### 第 4 步：前端切割 PostgREST

`deleteDocument`、`updateDocument` 从前端 PostgREST 调用改为走新后端 `/api/documents/...`。

### 第 5 步：清理旧依赖

确认无其他 PostgREST 调用后，移除旧 RPC 函数。

---

## 四、涉及文件清单

| 层 | 文件 | 改动类型 |
|----|------|---------|
| 后端 | `documentController.py` | 新增 3 个端点 |
| 后端 | `documentServiceImpl.py` | 新增 Delete/Update/GetById/AppendAttachments |
| 后端 | `documentVo.py` | 可能新增 VO |
| 前端 | `files-upload.ts` | 重写 uploadDocumentToServer |
| 前端 | `files.upload.tsx` | 适配新上传参数 |
| 前端 | `documents.ts` | 简化列表映射 + 切割删除/编辑到新后端 |
| 前端 | `documents.list.tsx` | 小改适配新字段 |
| 前端 | `documents.edit.tsx` | 切割到新后端 PUT |
