35 KiB
文档类型管理 CRUD 详细分析
📋 概述
文件位置:app/api/document-types/document-types.ts
功能:提供文档类型的完整增删改查(CRUD)操作,包括关联的评查点分组、入口模块、提示词模板等管理功能。
核心数据库表:
document_types- 文档类型表(主表)evaluation_point_groups- 评查点分组表entry_modules- 入口模块表
📊 数据结构定义
1. DocumentType(数据库实体)
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(前端展示)
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. 辅助接口
// 文档类型分组
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 表
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 表
CREATE TABLE evaluation_point_groups (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
pid INTEGER DEFAULT 0, -- 父级分组ID(0表示根分组)
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
entry_modules 表
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
功能:从不同格式的 API 响应中提取数据
代码位置:lines 81-94
支持的响应格式:
{ code: number, msg: string, data: T }- 标准响应格式- 直接返回数据对象
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
查询参数:
{
select: 'id, name',
token?: string // JWT token(可选)
}
SQL 等价查询:
SELECT id, name
FROM evaluation_point_groups;
PostgREST API 请求:
GET /evaluation_point_groups?select=id,name
Authorization: Bearer {token}
返回数据:
{
data?: DocumentTypeGroup[]; // [{ id: "1", name: "合同形式要素" }, ...]
error?: string;
status?: number;
}
使用场景:
- 文档类型创建/编辑时选择评查点分组
- 获取所有可用的评查点分组列表
2. getParentEvaluationPointGroups
功能:获取父级评查分组(pid=0 的根分组)
代码位置:lines 149-194
数据库表:evaluation_point_groups
查询参数:
{
select: 'id, name',
filter: {
'pid': 'eq.0' // 只查询父级分组
},
order: 'id.asc',
token?: string
}
SQL 等价查询:
SELECT id, name
FROM evaluation_point_groups
WHERE pid = 0
ORDER BY id ASC;
PostgREST API 请求:
GET /evaluation_point_groups?pid=eq.0&select=id,name&order=id.asc
Authorization: Bearer {token}
返回数据:
{
data?: DocumentTypeGroup[];
error?: string;
status?: number;
}
使用场景:
- 获取顶级评查点分组
- 构建树形结构的第一层
3. getEntryModules
功能:获取所有入口模块
代码位置:lines 201-237
数据库表:entry_modules
查询参数:
{
select: 'id, name',
order: 'id.asc',
token?: string
}
SQL 等价查询:
SELECT id, name
FROM entry_modules
ORDER BY id ASC;
PostgREST API 请求:
GET /entry_modules?select=id,name&order=id.asc
Authorization: Bearer {token}
返回数据:
{
data?: Array<{ id: number; name: string }>;
error?: string;
status?: number;
}
数据示例:
{
"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
查询参数:
{
select: 'id, name',
filter: {
'id': `in.(${idsArray.join(',')})` // IN操作符批量查询
},
token?: string
}
SQL 等价查询:
SELECT id, name
FROM evaluation_point_groups
WHERE id IN (1, 2, 3);
PostgREST API 请求:
GET /evaluation_point_groups?id=in.(1,2,3)&select=id,name
Authorization: Bearer {token}
返回数据:
{
data?: DocumentTypeGroup[];
error?: string;
status?: number;
}
使用场景:
- 获取文档类型关联的所有评查点分组详情
- 批量查询避免N+1查询问题
5. getDocumentTypes(核心查询)
功能:获取文档类型列表(支持搜索、分页、筛选)
代码位置:lines 311-458
数据库表:
document_types(主表)entry_modules(关联查询)evaluation_point_groups(后续批量查询)
参数:DocumentTypeSearchParams
{
name?: string; // 名称模糊搜索
ruleType?: string; // 按评查点分组类型筛选
groupId?: string; // 按评查点分组ID筛选
page?: number; // 页码(默认1)
pageSize?: number; // 每页数量(默认10)
documentTypeIds?: number[]; // 文档类型ID数组过滤
}
查询参数:
{
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 等价查询(简化版):
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 请求:
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_modules(LEFT 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(总数)
返回数据:
{
data?: {
types: DocumentTypeUI[];
total: number;
};
error?: string;
status?: number;
}
数据示例:
{
"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_typesentry_modules(关联查询)evaluation_point_groups(后续查询)
参数:
id: string- 文档类型IDfrontendJWT?: string- JWT token
查询参数:
{
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 等价查询:
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 请求:
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()
返回数据:
{
data?: DocumentTypeUI;
error?: string;
status?: number;
}
数据示例:
{
"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
{
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;
}
数据验证:
1. name 不能为空
2. group_ids 至少选择一个
3. group_ids[0] 必须是有效的数字ID
4. 各模板ID必须是有效数字(如果提供)
数据转换逻辑:
// 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 等价操作:
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 请求:
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. 返回创建的文档类型
返回数据:
{
data?: DocumentTypeUI; // 新创建的文档类型(包含ID)
error?: string;
status?: number;
}
数据示例:
{
"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- 文档类型IDdocumentType: DocumentTypeUpdateDTO- 更新数据
{
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;
}
数据验证:
1. id 不能为空
2. name 不能为空
3. group_ids 至少选择一个
4. 各模板ID必须是有效数字(如果提供)
数据转换逻辑:
// 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 等价操作:
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 请求:
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 表
└─ 使用 postgrestPut(WHERE id=:id)
4. 提取返回的更新后记录
└─ extractApiData<DocumentType>
5. 查询关联的评查点分组信息
└─ getEvaluationPointGroupsByIds(groupIds)
6. 合并数据并转换为 DocumentTypeUI
└─ convertToUIDocumentType()
7. 返回更新后的文档类型
返回数据:
{
data?: DocumentTypeUI; // 更新后的文档类型
error?: string;
status?: number;
}
数据示例:
{
"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- 文档类型IDfrontendJWT?: string- JWT token
查询参数:
{
filter: {
'id': `eq.${id}` // 精确匹配要删除的ID
},
token?: string
}
SQL 等价操作:
DELETE FROM document_types
WHERE id = :id;
PostgREST API 请求:
DELETE /document_types?id=eq.1
Authorization: Bearer {token}
删除流程:
1. 验证 ID 不为空
└─ 如果为空 → 返回 400 错误
2. 发送 DELETE 请求到 document_types 表
└─ 使用 postgrestDelete(WHERE id=:id)
3. 检查是否有错误
└─ 如果有错误 → 返回错误信息
4. 返回删除成功标识
└─ { success: true }
返回数据:
{
success?: boolean; // true 表示删除成功
error?: string;
status?: number;
}
成功响应示例:
{
"success": true
}
错误处理:
- ❌ ID为空 →
{ error: '文档类型ID不能为空', status: 400 } - ❌ 记录不存在 → PostgREST 不会报错,返回成功
- ❌ 外键约束 →
{ error: '无法删除,该文档类型正在被使用', status: 409 }(数据库级错误) - ❌ 权限不足 →
{ error: '权限不足', status: 403 }
注意事项:
- ⚠️ 删除操作是物理删除,不可恢复
- ⚠️ 如果有文档关联到此类型,删除可能失败(外键约束)
- ⚠️ 建议前端弹窗确认后再执行删除
使用场景:
- 文档类型管理页面的"删除"操作
- 清理无用的文档类型
🔄 数据流转图
创建文档类型数据流
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
查询文档类型列表数据流
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):
{
"llm_extract_template": 10,
"vlm_extract_template": 15,
"execution_template": 20, // 注意:数据库字段名
"summary_template": 25
}
前端使用格式(DocumentTypeUI):
{
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):
// 优先使用 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):
if (typeof evaluation_point_groups_ids === 'string') {
// 情况1:JSON字符串
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:
select: `
entry_modules!fk_document_types_entry_module(id, name)
`
说明:
!fk_document_types_entry_module- 外键约束名称- 自动执行 LEFT JOIN
- 返回嵌套对象
等价 SQL:
LEFT JOIN entry_modules em
ON dt.entry_module_id = em.id
4. 分页和总数获取
分页参数:
limit: pageSize, // 每页数量
offset: (page - 1) * pageSize // 跳过记录数
获取总数:
headers: {
'Prefer': 'count=exact' // 请求返回总数
}
// 从响应头读取总数
const rangeHeader = response.headers['content-range'];
// 格式: "0-9/25" → 总数是25
const total = rangeHeader.split('/')[1];
5. 批量查询优化
问题:N+1查询
// ❌ 不好的做法
for (const type of documentTypes) {
const groups = await getEvaluationPointGroupsByIds(type.evaluation_point_groups_ids);
}
解决方案:批量查询
// ✅ 好的做法
// 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. 获取文档类型列表
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. 创建文档类型
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. 更新文档类型
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. 删除文档类型
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');
}
🔍 数据库索引建议
为了优化查询性能,建议创建以下索引:
-- 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);
📝 总结
核心功能
- 完整的CRUD操作:创建、读取、更新、删除文档类型
- 关联查询:自动关联评查点分组和入口模块
- 搜索和筛选:支持名称模糊搜索、分组筛选
- 分页查询:支持大数据量的分页展示
- 批量查询优化:避免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