Files
leaudit-platform-frontend/docs/文档类型管理CRUD详细分析.md
T
2025-12-05 00:09:32 +08:00

1478 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 文档类型管理 CRUD 详细分析
## 📋 概述
**文件位置**`app/api/document-types/document-types.ts`
**功能**:提供文档类型的完整增删改查(CRUD)操作,包括关联的评查点分组、入口模块、提示词模板等管理功能。
**核心数据库表**
- `document_types` - 文档类型表(主表)
- `evaluation_point_groups` - 评查点分组表
- `entry_modules` - 入口模块表
---
## 📊 数据结构定义
### 1. DocumentType(数据库实体)
```typescript
interface DocumentType {
id: number;
name: string; // 文档类型名称
description: string | null; // 描述
evaluation_point_groups_ids: number[]; // 关联的评查点分组ID数组(JSONB)
prompt_config?: { // 提示词配置(JSONB
summary_template?: number; // 总结模板ID
llm_extract_template?: number; // LLM抽取模板ID
vlm_extract_template?: number; // VLM抽取模板ID
evaluation_template?: number; // 评查模板ID
execution_template?: number; // 执行模板ID(与evaluation_template等价)
} | null;
created_at: string;
updated_at: string;
code?: string | null; // 文档类型代码
}
```
### 2. DocumentTypeUI(前端展示)
```typescript
interface DocumentTypeUI {
id: number;
name: string;
description: string;
groups: DocumentTypeGroup[]; // 关联的评查点分组列表
entry_module?: { // 入口模块
id: number;
name: string;
} | null;
llm_extraction_template_id?: number | null;
vlm_extraction_template_id?: number | null;
evaluation_template_id?: number | null;
summary_template_id?: number | null;
created_at: string; // 格式化后的日期
updated_at: string; // 格式化后的日期
code?: string | null;
}
```
### 3. 辅助接口
```typescript
// 文档类型分组
interface DocumentTypeGroup {
id: string;
name: string;
}
// 创建DTO
interface DocumentTypeCreateDTO {
name: string;
description?: string;
group_ids: string[]; // 评查点分组ID数组
entry_module_id?: number | null;
llm_extraction_template_id?: number | null;
vlm_extraction_template_id?: number | null;
evaluation_template_id?: number | null;
summary_template_id?: number | null;
code?: string | null;
}
// 更新DTO
interface DocumentTypeUpdateDTO extends DocumentTypeCreateDTO {
id: number;
}
// 搜索参数
interface DocumentTypeSearchParams {
name?: string; // 名称模糊搜索
ruleType?: string; // 按评查点分组类型筛选
groupId?: string; // 按评查点分组ID筛选
page?: number; // 页码(默认1
pageSize?: number; // 每页数量(默认10
documentTypeIds?: number[]; // 文档类型ID数组
}
```
---
## 🗄️ 数据库表结构
### document_types 表
```sql
CREATE TABLE document_types (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
evaluation_point_groups_ids JSONB, -- 评查点分组ID数组
entry_module_id INTEGER,
prompt_config JSONB, -- 提示词配置
code VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (entry_module_id) REFERENCES entry_modules(id)
);
```
### evaluation_point_groups 表
```sql
CREATE TABLE evaluation_point_groups (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
pid INTEGER DEFAULT 0, -- 父级分组ID0表示根分组)
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
### entry_modules 表
```sql
CREATE TABLE entry_modules (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
---
## 🔧 辅助函数
### 1. extractApiData<T>
**功能**:从不同格式的 API 响应中提取数据
**代码位置**lines 81-94
**支持的响应格式**
1. `{ code: number, msg: string, data: T }` - 标准响应格式
2. 直接返回数据对象
```typescript
function extractApiData<T>(responseData: unknown): T | null {
// 格式1: 标准响应格式
if (responseData有code和data字段) {
return responseData.data;
}
// 格式2: 直接是数据
return responseData as T;
}
```
### 2. convertToUIDocumentType
**功能**:将数据库实体转换为前端UI格式
**代码位置**lines 504-552
**转换逻辑**
- 提取 `prompt_config` 中的各个模板ID
- 格式化日期字段(`created_at`, `updated_at`
- 处理 `entry_modules` 关联数据
-`groups` 添加到返回对象
---
## 📖 查询操作(Read
### 1. getAllEvaluationPointGroups
**功能**:获取所有评查点分组
**代码位置**lines 101-142
**数据库表**`evaluation_point_groups`
**查询参数**
```typescript
{
select: 'id, name',
token?: string // JWT token(可选)
}
```
**SQL 等价查询**
```sql
SELECT id, name
FROM evaluation_point_groups;
```
**PostgREST API 请求**
```http
GET /evaluation_point_groups?select=id,name
Authorization: Bearer {token}
```
**返回数据**
```typescript
{
data?: DocumentTypeGroup[]; // [{ id: "1", name: "合同形式要素" }, ...]
error?: string;
status?: number;
}
```
**使用场景**
- 文档类型创建/编辑时选择评查点分组
- 获取所有可用的评查点分组列表
---
### 2. getParentEvaluationPointGroups
**功能**:获取父级评查分组(pid=0 的根分组)
**代码位置**lines 149-194
**数据库表**`evaluation_point_groups`
**查询参数**
```typescript
{
select: 'id, name',
filter: {
'pid': 'eq.0' // 只查询父级分组
},
order: 'id.asc',
token?: string
}
```
**SQL 等价查询**
```sql
SELECT id, name
FROM evaluation_point_groups
WHERE pid = 0
ORDER BY id ASC;
```
**PostgREST API 请求**
```http
GET /evaluation_point_groups?pid=eq.0&select=id,name&order=id.asc
Authorization: Bearer {token}
```
**返回数据**
```typescript
{
data?: DocumentTypeGroup[];
error?: string;
status?: number;
}
```
**使用场景**
- 获取顶级评查点分组
- 构建树形结构的第一层
---
### 3. getEntryModules
**功能**:获取所有入口模块
**代码位置**lines 201-237
**数据库表**`entry_modules`
**查询参数**
```typescript
{
select: 'id, name',
order: 'id.asc',
token?: string
}
```
**SQL 等价查询**
```sql
SELECT id, name
FROM entry_modules
ORDER BY id ASC;
```
**PostgREST API 请求**
```http
GET /entry_modules?select=id,name&order=id.asc
Authorization: Bearer {token}
```
**返回数据**
```typescript
{
data?: Array<{ id: number; name: string }>;
error?: string;
status?: number;
}
```
**数据示例**
```json
{
"data": [
{ "id": 1, "name": "多模态抽取" },
{ "id": 2, "name": "智能评查" }
]
}
```
**使用场景**
- 文档类型创建/编辑时选择入口模块
- 确定文档处理的入口流程
---
### 4. getEvaluationPointGroupsByIds
**功能**:根据ID批量获取评查点分组信息
**代码位置**lines 245-304
**数据库表**`evaluation_point_groups`
**参数**
- `ids: number[] | number` - 分组ID或ID数组
- `token?: string` - JWT token
**查询参数**
```typescript
{
select: 'id, name',
filter: {
'id': `in.(${idsArray.join(',')})` // IN操作符批量查询
},
token?: string
}
```
**SQL 等价查询**
```sql
SELECT id, name
FROM evaluation_point_groups
WHERE id IN (1, 2, 3);
```
**PostgREST API 请求**
```http
GET /evaluation_point_groups?id=in.(1,2,3)&select=id,name
Authorization: Bearer {token}
```
**返回数据**
```typescript
{
data?: DocumentTypeGroup[];
error?: string;
status?: number;
}
```
**使用场景**
- 获取文档类型关联的所有评查点分组详情
- 批量查询避免N+1查询问题
---
### 5. getDocumentTypes(核心查询)
**功能**:获取文档类型列表(支持搜索、分页、筛选)
**代码位置**lines 311-458
**数据库表**
- `document_types`(主表)
- `entry_modules`(关联查询)
- `evaluation_point_groups`(后续批量查询)
**参数**`DocumentTypeSearchParams`
```typescript
{
name?: string; // 名称模糊搜索
ruleType?: string; // 按评查点分组类型筛选
groupId?: string; // 按评查点分组ID筛选
page?: number; // 页码(默认1
pageSize?: number; // 每页数量(默认10
documentTypeIds?: number[]; // 文档类型ID数组过滤
}
```
**查询参数**
```typescript
{
select: `
id,
name,
description,
evaluation_point_groups_ids,
entry_module_id,
entry_modules!fk_document_types_entry_module(id, name), // 关联查询
prompt_config,
created_at,
updated_at,
code
`,
order: 'updated_at.desc',
headers: {
'Prefer': 'count=exact' // 获取总数
},
limit: pageSize,
offset: (page - 1) * pageSize,
filter: {
'name'?: `ilike.%${name}%`, // 名称模糊搜索
'evaluation_point_groups_ids'?: `cs.[${groupId}]`, // 包含指定分组
'id'?: `in.(${documentTypeIds.join(',')})` // ID过滤
},
token?: string
}
```
**SQL 等价查询**(简化版):
```sql
SELECT
dt.id,
dt.name,
dt.description,
dt.evaluation_point_groups_ids,
dt.entry_module_id,
dt.prompt_config,
dt.created_at,
dt.updated_at,
dt.code,
em.id as entry_module_id,
em.name as entry_module_name
FROM document_types dt
LEFT JOIN entry_modules em ON dt.entry_module_id = em.id
WHERE
dt.name ILIKE '%searchName%' -- 可选:名称模糊搜索
AND dt.evaluation_point_groups_ids @> '[5]' -- 可选:包含指定分组ID
AND dt.id IN (1, 2, 3) -- 可选:ID过滤
ORDER BY dt.updated_at DESC
LIMIT 10 OFFSET 0;
-- 同时获取总数
SELECT COUNT(*) FROM document_types WHERE ...;
```
**PostgREST API 请求**
```http
GET /document_types?select=id,name,description,evaluation_point_groups_ids,entry_module_id,entry_modules!fk_document_types_entry_module(id,name),prompt_config,created_at,updated_at,code&order=updated_at.desc&limit=10&offset=0&name=ilike.%%&evaluation_point_groups_ids=cs.[5]
Prefer: count=exact
Authorization: Bearer {token}
```
**查询逻辑流程**
```
1. 查询 document_types 表(主查询)
├─ 关联查询 entry_modulesLEFT JOIN
├─ 应用筛选条件
├─ 分页处理
└─ 获取总数(通过 Content-Range header
2. 收集所有文档类型的 evaluation_point_groups_ids
└─ 去重后批量查询 evaluation_point_groups
3. 构建 Map 映射(O(1)查找)
└─ groupsMap: Map<number, DocumentTypeGroup>
4. 遍历文档类型列表,合并数据
├─ 添加 entry_module 信息
├─ 添加 groups 信息(从 groupsMap 查找)
└─ 转换为 DocumentTypeUI 格式
5. 返回完整数据
├─ types: DocumentTypeUI[]
└─ total: number(总数)
```
**返回数据**
```typescript
{
data?: {
types: DocumentTypeUI[];
total: number;
};
error?: string;
status?: number;
}
```
**数据示例**
```json
{
"data": {
"types": [
{
"id": 1,
"name": "烟草专卖行政处罚案卷",
"description": "烟草专卖行政处罚案卷的智能评查",
"groups": [
{ "id": "5", "name": "程序合法性" },
{ "id": "8", "name": "证据完整性" }
],
"entry_module": {
"id": 1,
"name": "多模态抽取"
},
"llm_extraction_template_id": 10,
"vlm_extraction_template_id": 15,
"evaluation_template_id": 20,
"summary_template_id": 25,
"created_at": "2024-01-15 10:00:00",
"updated_at": "2024-01-20 15:30:00",
"code": "TOBACCO_CASE"
}
],
"total": 25
}
}
```
**性能优化**
- ✅ 使用 PostgREST 的资源嵌入语法关联查询(避免N+1)
- ✅ 批量查询评查点分组(一次查询获取所有分组)
- ✅ 使用 Map 进行快速查找(O(1)时间复杂度)
- ✅ 分页查询减少数据传输量
**使用场景**
- 文档类型管理列表页
- 文档上传时选择文档类型
- 文档类型搜索和筛选
---
### 6. getDocumentType
**功能**:获取单个文档类型的详细信息
**代码位置**lines 560-648
**数据库表**
- `document_types`
- `entry_modules`(关联查询)
- `evaluation_point_groups`(后续查询)
**参数**
- `id: string` - 文档类型ID
- `frontendJWT?: string` - JWT token
**查询参数**
```typescript
{
select: `
id,
name,
description,
evaluation_point_groups_ids,
entry_module_id,
entry_modules!fk_document_types_entry_module(id, name),
prompt_config,
created_at,
updated_at,
code
`,
filter: {
'id': `eq.${id}` // 精确匹配ID
},
token?: string
}
```
**SQL 等价查询**
```sql
SELECT
dt.id,
dt.name,
dt.description,
dt.evaluation_point_groups_ids,
dt.entry_module_id,
dt.prompt_config,
dt.created_at,
dt.updated_at,
dt.code,
em.id as entry_module_id,
em.name as entry_module_name
FROM document_types dt
LEFT JOIN entry_modules em ON dt.entry_module_id = em.id
WHERE dt.id = :id
LIMIT 1;
```
**PostgREST API 请求**
```http
GET /document_types?id=eq.1&select=id,name,description,evaluation_point_groups_ids,entry_module_id,entry_modules!fk_document_types_entry_module(id,name),prompt_config,created_at,updated_at,code
Authorization: Bearer {token}
```
**查询逻辑流程**
```
1. 查询 document_types 表(单条记录)
└─ 关联查询 entry_modules
2. 解析 evaluation_point_groups_ids
├─ 字符串 → JSON.parse()
├─ 数组 → 直接使用
└─ 其他 → 转换为数组
3. 批量查询评查点分组
└─ getEvaluationPointGroupsByIds(groupIds)
4. 合并数据并转换为 DocumentTypeUI
└─ convertToUIDocumentType()
```
**返回数据**
```typescript
{
data?: DocumentTypeUI;
error?: string;
status?: number;
}
```
**数据示例**
```json
{
"data": {
"id": 1,
"name": "烟草专卖行政处罚案卷",
"description": "烟草专卖行政处罚案卷的智能评查",
"groups": [
{ "id": "5", "name": "程序合法性" },
{ "id": "8", "name": "证据完整性" }
],
"entry_module": {
"id": 1,
"name": "多模态抽取"
},
"llm_extraction_template_id": 10,
"vlm_extraction_template_id": 15,
"evaluation_template_id": 20,
"summary_template_id": 25,
"created_at": "2024-01-15 10:00:00",
"updated_at": "2024-01-20 15:30:00",
"code": "TOBACCO_CASE"
}
}
```
**错误处理**
- ❌ ID为空 → 400 错误
- ❌ 未找到记录 → 404 错误
- ❌ 分组ID解析失败 → 记录错误日志,返回空数组
**使用场景**
- 文档类型编辑页面
- 文档类型详情查看
---
## 创建操作(Create
### createDocumentType
**功能**:创建新的文档类型
**代码位置**lines 655-774
**数据库表**
- `document_types`(主表,INSERT
- `evaluation_point_groups`(关联查询)
**参数**`DocumentTypeCreateDTO`
```typescript
{
name: string; // 必填:文档类型名称
description?: string; // 可选:描述
group_ids: string[]; // 必填:评查点分组ID数组
entry_module_id?: number | null; // 可选:入口模块ID
llm_extraction_template_id?: number | null;
vlm_extraction_template_id?: number | null;
evaluation_template_id?: number | null;
summary_template_id?: number | null;
code?: string | null;
}
```
**数据验证**
```typescript
1. name
2. group_ids
3. group_ids[0] ID
4. ID必须是有效数字
```
**数据转换逻辑**
```typescript
// 1. 提取第一个分组ID(目前只支持单选)
const groupId = documentType.group_ids[0];
const groupIds = [parseInt(groupId, 10)];
// 2. 构建 prompt_config 对象
const promptConfig = {
llm_extract_template: documentType.llm_extraction_template_id || null,
vlm_extract_template: documentType.vlm_extraction_template_id || null,
execution_template: documentType.evaluation_template_id || null, // 注意字段名转换
summary_template: documentType.summary_template_id || null
};
// 3. 构建 API 请求数据
const apiDocumentType = {
name: documentType.name.trim(),
description: documentType.description || '',
evaluation_point_groups_ids: groupIds, // 数组格式
entry_module_id: documentType.entry_module_id || null,
prompt_config: promptConfig // JSONB对象
};
```
**SQL 等价操作**
```sql
INSERT INTO document_types (
name,
description,
evaluation_point_groups_ids,
entry_module_id,
prompt_config,
created_at,
updated_at
) VALUES (
'烟草专卖行政处罚案卷',
'烟草专卖行政处罚案卷的智能评查',
'[5]'::jsonb,
1,
'{
"llm_extract_template": 10,
"vlm_extract_template": 15,
"execution_template": 20,
"summary_template": 25
}'::jsonb,
NOW(),
NOW()
)
RETURNING *;
```
**PostgREST API 请求**
```http
POST /document_types
Content-Type: application/json
Authorization: Bearer {token}
{
"name": "",
"description": "",
"evaluation_point_groups_ids": [5],
"entry_module_id": 1,
"prompt_config": {
"llm_extract_template": 10,
"vlm_extract_template": 15,
"execution_template": 20,
"summary_template": 25
}
}
```
**创建流程**
```
1. 验证必填字段
├─ name 不为空
├─ group_ids 至少有一个
└─ 各ID格式有效
2. 转换数据格式
├─ group_ids[0] → groupIds: [number]
└─ 各模板ID → prompt_config: JSONB
3. 发送 POST 请求到 document_types 表
└─ 使用 postgrestPost
4. 提取返回的新记录
└─ extractApiData<DocumentType>
5. 查询关联的评查点分组信息
└─ getEvaluationPointGroupsByIds(groupIds)
6. 合并数据并转换为 DocumentTypeUI
└─ convertToUIDocumentType()
7. 返回创建的文档类型
```
**返回数据**
```typescript
{
data?: DocumentTypeUI; // 新创建的文档类型(包含ID
error?: string;
status?: number;
}
```
**数据示例**
```json
{
"data": {
"id": 26, // 自动生成的ID
"name": "烟草专卖行政处罚案卷",
"description": "烟草专卖行政处罚案卷的智能评查",
"groups": [
{ "id": "5", "name": "程序合法性" }
],
"entry_module": null,
"llm_extraction_template_id": 10,
"vlm_extraction_template_id": 15,
"evaluation_template_id": 20,
"summary_template_id": 25,
"created_at": "2024-01-26 16:45:00",
"updated_at": "2024-01-26 16:45:00",
"code": null
}
}
```
**错误处理**
- ❌ name 为空 → `{ error: '文档类型名称不能为空', status: 400 }`
- ❌ group_ids 为空 → `{ error: '请至少选择一个关联的评查点分组', status: 400 }`
- ❌ group_id 无效 → `{ error: '无效的评查点分组ID', status: 400 }`
- ❌ 模板ID无效 → `{ error: '无效的xxx提示词模板ID', status: 400 }`
- ❌ API返回错误 → 返回 API 错误信息
**使用场景**
- 文档类型管理页面的"新建"操作
- 系统初始化时批量创建文档类型
---
## ✏️ 更新操作(Update
### updateDocumentType
**功能**:更新现有文档类型
**代码位置**lines 782-896
**数据库表**
- `document_types`(主表,UPDATE
- `evaluation_point_groups`(关联查询)
**参数**
- `id: string` - 文档类型ID
- `documentType: DocumentTypeUpdateDTO` - 更新数据
```typescript
{
id: number; // 包含在DTO中,但实际使用函数参数的id
name: string;
description?: string;
group_ids: string[];
entry_module_id?: number | null;
llm_extraction_template_id?: number | null;
vlm_extraction_template_id?: number | null;
evaluation_template_id?: number | null;
summary_template_id?: number | null;
code?: string | null;
}
```
**数据验证**
```typescript
1. id
2. name
3. group_ids
4. ID必须是有效数字
```
**数据转换逻辑**
```typescript
// 1. 将 group_ids 转换为数字数组
const groupIds = documentType.group_ids.map(id => parseInt(id, 10));
// 2. 构建 prompt_config 对象(与创建逻辑相同)
const promptConfig = {
llm_extract_template: documentType.llm_extraction_template_id || null,
vlm_extract_template: documentType.vlm_extraction_template_id || null,
execution_template: documentType.evaluation_template_id || null,
summary_template: documentType.summary_template_id || null
};
// 3. 构建 API 请求数据
const apiDocumentType = {
name: documentType.name.trim(),
description: documentType.description || '',
evaluation_point_groups_ids: groupIds,
entry_module_id: documentType.entry_module_id || null,
prompt_config: promptConfig
};
```
**SQL 等价操作**
```sql
UPDATE document_types
SET
name = '烟草专卖行政处罚案卷(修订版)',
description = '更新的描述',
evaluation_point_groups_ids = '[5, 8]'::jsonb,
entry_module_id = 2,
prompt_config = '{
"llm_extract_template": 11,
"vlm_extract_template": 16,
"execution_template": 21,
"summary_template": 26
}'::jsonb,
updated_at = NOW()
WHERE id = 1
RETURNING *;
```
**PostgREST API 请求**
```http
PATCH /document_types?id=eq.1
Content-Type: application/json
Authorization: Bearer {token}
{
"name": "",
"description": "",
"evaluation_point_groups_ids": [5, 8],
"entry_module_id": 2,
"prompt_config": {
"llm_extract_template": 11,
"vlm_extract_template": 16,
"execution_template": 21,
"summary_template": 26
}
}
```
**更新流程**
```
1. 验证必填字段
├─ id 不为空
├─ name 不为空
└─ group_ids 至少有一个
2. 转换数据格式
├─ group_ids → groupIds: number[]
└─ 各模板ID → prompt_config: JSONB
3. 发送 PUT/PATCH 请求到 document_types 表
└─ 使用 postgrestPutWHERE id=:id
4. 提取返回的更新后记录
└─ extractApiData<DocumentType>
5. 查询关联的评查点分组信息
└─ getEvaluationPointGroupsByIds(groupIds)
6. 合并数据并转换为 DocumentTypeUI
└─ convertToUIDocumentType()
7. 返回更新后的文档类型
```
**返回数据**
```typescript
{
data?: DocumentTypeUI; // 更新后的文档类型
error?: string;
status?: number;
}
```
**数据示例**
```json
{
"data": {
"id": 1,
"name": "烟草专卖行政处罚案卷(修订版)",
"description": "更新的描述",
"groups": [
{ "id": "5", "name": "程序合法性" },
{ "id": "8", "name": "证据完整性" }
],
"entry_module": {
"id": 2,
"name": "智能评查"
},
"llm_extraction_template_id": 11,
"vlm_extraction_template_id": 16,
"evaluation_template_id": 21,
"summary_template_id": 26,
"created_at": "2024-01-15 10:00:00",
"updated_at": "2024-01-26 17:30:00", // 自动更新
"code": "TOBACCO_CASE"
}
}
```
**错误处理**
- ❌ id 为空 → `{ error: '文档类型ID不能为空', status: 400 }`
- ❌ name 为空 → `{ error: '文档类型名称不能为空', status: 400 }`
- ❌ group_ids 为空 → `{ error: '请至少选择一个关联的评查点分组', status: 400 }`
- ❌ 模板ID无效 → `{ error: '无效的xxx提示词模板ID', status: 400 }`
- ❌ API返回错误 → 返回 API 错误信息
- ❌ 记录不存在 → `{ error: '更新文档类型失败: 无法获取更新后的数据', status: 500 }`
**使用场景**
- 文档类型管理页面的"编辑"操作
- 批量更新文档类型配置
---
## ❌ 删除操作(Delete
### deleteDocumentType
**功能**:删除指定的文档类型
**代码位置**lines 466-499
**数据库表**
- `document_types`DELETE操作)
**参数**
- `id: string` - 文档类型ID
- `frontendJWT?: string` - JWT token
**查询参数**
```typescript
{
filter: {
'id': `eq.${id}` // 精确匹配要删除的ID
},
token?: string
}
```
**SQL 等价操作**
```sql
DELETE FROM document_types
WHERE id = :id;
```
**PostgREST API 请求**
```http
DELETE /document_types?id=eq.1
Authorization: Bearer {token}
```
**删除流程**
```
1. 验证 ID 不为空
└─ 如果为空 → 返回 400 错误
2. 发送 DELETE 请求到 document_types 表
└─ 使用 postgrestDeleteWHERE id=:id
3. 检查是否有错误
└─ 如果有错误 → 返回错误信息
4. 返回删除成功标识
└─ { success: true }
```
**返回数据**
```typescript
{
success?: boolean; // true 表示删除成功
error?: string;
status?: number;
}
```
**成功响应示例**
```json
{
"success": true
}
```
**错误处理**
- ❌ ID为空 → `{ error: '文档类型ID不能为空', status: 400 }`
- ❌ 记录不存在 → PostgREST 不会报错,返回成功
- ❌ 外键约束 → `{ error: '无法删除,该文档类型正在被使用', status: 409 }`(数据库级错误)
- ❌ 权限不足 → `{ error: '权限不足', status: 403 }`
**注意事项**
- ⚠️ 删除操作是**物理删除**,不可恢复
- ⚠️ 如果有文档关联到此类型,删除可能失败(外键约束)
- ⚠️ 建议前端弹窗确认后再执行删除
**使用场景**
- 文档类型管理页面的"删除"操作
- 清理无用的文档类型
---
## 🔄 数据流转图
### 创建文档类型数据流
```mermaid
graph TD
A[前端提交表单] --> B[createDocumentType]
B --> C{验证数据}
C -->|失败| D[返回400错误]
C -->|成功| E[转换数据格式]
E --> F[构建prompt_config]
F --> G[POST /document_types]
G --> H{创建成功?}
H -->|否| I[返回错误]
H -->|是| J[提取新记录]
J --> K[查询评查点分组]
K --> L[合并数据]
L --> M[转换为UI格式]
M --> N[返回给前端]
style A fill:#90EE90
style N fill:#FFD700
style D fill:#FF6B6B
style I fill:#FF6B6B
```
### 查询文档类型列表数据流
```mermaid
graph LR
A[getDocumentTypes] --> B[查询 document_types]
B --> C[关联查询 entry_modules]
C --> D[收集所有 group_ids]
D --> E[批量查询 evaluation_point_groups]
E --> F[构建 groupsMap]
F --> G[遍历文档类型]
G --> H[合并关联数据]
H --> I[转换为UI格式]
I --> J[返回列表和总数]
style A fill:#87CEEB
style J fill:#FFD700
```
---
## 📋 关键业务逻辑
### 1. prompt_config 字段处理
**数据库存储格式**JSONB):
```json
{
"llm_extract_template": 10,
"vlm_extract_template": 15,
"execution_template": 20, // 注意:数据库字段名
"summary_template": 25
}
```
**前端使用格式**DocumentTypeUI):
```typescript
{
llm_extraction_template_id: 10,
vlm_extraction_template_id: 15,
evaluation_template_id: 20, // 注意:前端字段名
summary_template_id: 25
}
```
**字段映射关系**
| 数据库字段 | 前端字段 | 说明 |
|-----------|---------|------|
| `llm_extract_template` | `llm_extraction_template_id` | LLM抽取模板 |
| `vlm_extract_template` | `vlm_extraction_template_id` | VLM抽取模板 |
| `execution_template``evaluation_template` | `evaluation_template_id` | 评查模板 |
| `summary_template` | `summary_template_id` | 总结模板 |
**转换逻辑**convertToUIDocumentType):
```typescript
// 优先使用 evaluation_template,如果不存在则使用 execution_template
if (type.prompt_config.evaluation_template !== undefined && type.prompt_config.evaluation_template !== null) {
evaluationTemplateId = type.prompt_config.evaluation_template;
} else if (type.prompt_config.execution_template !== undefined && type.prompt_config.execution_template !== null) {
evaluationTemplateId = type.prompt_config.execution_template;
}
```
### 2. evaluation_point_groups_ids 处理
**数据类型**JSONB数组
**存储格式**`[5, 8, 12]`
**解析逻辑**lines 609-624):
```typescript
if (typeof evaluation_point_groups_ids === 'string') {
// 情况1JSON字符串
groupIds = JSON.parse(evaluation_point_groups_ids);
} else if (Array.isArray(evaluation_point_groups_ids)) {
// 情况2:已经是数组
groupIds = evaluation_point_groups_ids;
} else if (evaluation_point_groups_ids) {
// 情况3:单个值
groupIds = [evaluation_point_groups_ids];
}
```
**当前限制**
- 创建时只使用 `group_ids[0]`(第一个分组)
- 更新时支持多个分组(`groupIds = group_ids.map(...)`
### 3. PostgREST 资源嵌入语法
**关联查询 entry_modules**
```typescript
select: `
entry_modules!fk_document_types_entry_module(id, name)
`
```
**说明**
- `!fk_document_types_entry_module` - 外键约束名称
- 自动执行 LEFT JOIN
- 返回嵌套对象
**等价 SQL**
```sql
LEFT JOIN entry_modules em
ON dt.entry_module_id = em.id
```
### 4. 分页和总数获取
**分页参数**
```typescript
limit: pageSize, // 每页数量
offset: (page - 1) * pageSize // 跳过记录数
```
**获取总数**
```typescript
headers: {
'Prefer': 'count=exact' // 请求返回总数
}
// 从响应头读取总数
const rangeHeader = response.headers['content-range'];
// 格式: "0-9/25" → 总数是25
const total = rangeHeader.split('/')[1];
```
### 5. 批量查询优化
**问题**N+1查询
```typescript
// ❌ 不好的做法
for (const type of documentTypes) {
const groups = await getEvaluationPointGroupsByIds(type.evaluation_point_groups_ids);
}
```
**解决方案**:批量查询
```typescript
// ✅ 好的做法
// 1. 收集所有分组ID
const allGroupIds = new Set<number>();
documentTypes.forEach(type => {
type.evaluation_point_groups_ids.forEach(id => allGroupIds.add(id));
});
// 2. 一次性查询所有分组
const groupsResponse = await getEvaluationPointGroupsByIds(Array.from(allGroupIds));
// 3. 构建Map快速查找
const groupsMap = new Map<number, DocumentTypeGroup>();
groupsResponse.data.forEach(group => {
groupsMap.set(parseInt(group.id, 10), group);
});
// 4. 遍历时O(1)查找
const typeGroups = ids.map(id => groupsMap.get(id)).filter(Boolean);
```
---
## 🎯 使用示例
### 前端调用示例
#### 1. 获取文档类型列表
```typescript
import { getDocumentTypes } from '~/api/document-types/document-types';
// 在 Remix loader 中
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page') || '1');
const name = url.searchParams.get('name') || '';
const { userInfo, frontendJWT } = await getUserSession(request);
const result = await getDocumentTypes({
name,
page,
pageSize: 10
}, frontendJWT);
if (result.error) {
return json({ error: result.error }, { status: result.status || 500 });
}
return json(result.data);
}
// 前端组件使用
const { types, total } = useLoaderData<typeof loader>();
```
#### 2. 创建文档类型
```typescript
import { createDocumentType } from '~/api/document-types/document-types';
// 在 Remix action 中
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const { frontendJWT } = await getUserSession(request);
const documentType: DocumentTypeCreateDTO = {
name: formData.get('name') as string,
description: formData.get('description') as string,
group_ids: formData.getAll('group_ids') as string[],
entry_module_id: parseInt(formData.get('entry_module_id') as string) || null,
llm_extraction_template_id: parseInt(formData.get('llm_template') as string) || null,
// ...
};
const result = await createDocumentType(documentType, frontendJWT);
if (result.error) {
return json({ error: result.error }, { status: result.status || 500 });
}
return redirect('/document-types');
}
```
#### 3. 更新文档类型
```typescript
import { updateDocumentType } from '~/api/document-types/document-types';
export async function action({ params, request }: ActionFunctionArgs) {
const id = params.id!;
const formData = await request.formData();
const { frontendJWT } = await getUserSession(request);
const documentType: DocumentTypeUpdateDTO = {
id: parseInt(id),
name: formData.get('name') as string,
// ...
};
const result = await updateDocumentType(id, documentType, frontendJWT);
if (result.error) {
return json({ error: result.error }, { status: result.status || 500 });
}
return json(result.data);
}
```
#### 4. 删除文档类型
```typescript
import { deleteDocumentType } from '~/api/document-types/document-types';
export async function action({ params, request }: ActionFunctionArgs) {
const id = params.id!;
const { frontendJWT } = await getUserSession(request);
const result = await deleteDocumentType(id, frontendJWT);
if (result.error) {
return json({ error: result.error }, { status: result.status || 500 });
}
return redirect('/document-types');
}
```
---
## 🔍 数据库索引建议
为了优化查询性能,建议创建以下索引:
```sql
-- document_types 表
CREATE INDEX idx_document_types_name ON document_types(name);
CREATE INDEX idx_document_types_updated_at ON document_types(updated_at DESC);
CREATE INDEX idx_document_types_entry_module_id ON document_types(entry_module_id);
-- 使用 GIN 索引优化 JSONB 查询
CREATE INDEX idx_document_types_evaluation_groups
ON document_types USING GIN (evaluation_point_groups_ids);
-- evaluation_point_groups 表
CREATE INDEX idx_evaluation_point_groups_pid ON evaluation_point_groups(pid);
CREATE INDEX idx_evaluation_point_groups_name ON evaluation_point_groups(name);
-- entry_modules 表
CREATE INDEX idx_entry_modules_name ON entry_modules(name);
```
---
## 📝 总结
### 核心功能
1. **完整的CRUD操作**:创建、读取、更新、删除文档类型
2. **关联查询**:自动关联评查点分组和入口模块
3. **搜索和筛选**:支持名称模糊搜索、分组筛选
4. **分页查询**:支持大数据量的分页展示
5. **批量查询优化**:避免N+1查询问题
### 数据库表
| 表名 | 操作类型 | 说明 |
|-----|---------|------|
| `document_types` | CRUD | 主表,存储文档类型信息 |
| `evaluation_point_groups` | R | 评查点分组表,用于关联查询 |
| `entry_modules` | R | 入口模块表,用于关联查询 |
### 关键字段
| 字段名 | 类型 | 说明 |
|-------|------|------|
| `evaluation_point_groups_ids` | JSONB数组 | 关联的评查点分组ID |
| `prompt_config` | JSONB对象 | 提示词模板配置 |
| `entry_module_id` | INTEGER | 入口模块外键 |
### 性能优化
- ✅ PostgREST 资源嵌入语法(避免多次查询)
- ✅ 批量查询评查点分组(避免N+1查询)
- ✅ Map 映射快速查找(O(1)时间复杂度)
- ✅ 分页查询(减少数据传输)
- ✅ GIN 索引优化 JSONB 查询
### 使用场景
- 文档类型管理页面(列表、创建、编辑、删除)
- 文档上传时选择文档类型
- 配置文档处理流程(入口模块、提示词模板)
- 评查点分组管理
---
**最后更新**2025-11-26
**文档版本**v1.0
**代码文件**`app/api/document-types/document-types.ts`