# 文档类型管理 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, -- 父级分组ID(0表示根分组) 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 **功能**:从不同格式的 API 响应中提取数据 **代码位置**:lines 81-94 **支持的响应格式**: 1. `{ code: number, msg: string, data: T }` - 标准响应格式 2. 直接返回数据对象 ```typescript function extractApiData(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_modules(LEFT JOIN) ├─ 应用筛选条件 ├─ 分页处理 └─ 获取总数(通过 Content-Range header) 2. 收集所有文档类型的 evaluation_point_groups_ids └─ 去重后批量查询 evaluation_point_groups 3. 构建 Map 映射(O(1)查找) └─ groupsMap: Map 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 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 表 └─ 使用 postgrestPut(WHERE id=:id) 4. 提取返回的更新后记录 └─ extractApiData 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 表 └─ 使用 postgrestDelete(WHERE 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') { // 情况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**: ```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(); 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(); 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(); ``` #### 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`