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

35 KiB
Raw Blame History

文档类型管理 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,                -- 父级分组ID0表示根分组)
  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

支持的响应格式

  1. { code: number, msg: string, data: T } - 标准响应格式
  2. 直接返回数据对象
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_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(总数)

返回数据

{
  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_types
  • entry_modules(关联查询)
  • evaluation_point_groups(后续查询)

参数

  • id: string - 文档类型ID
  • frontendJWT?: 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 - 文档类型ID
  • documentType: 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 表
   └─ 使用 postgrestPutWHERE 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_typesDELETE操作)

参数

  • id: string - 文档类型ID
  • frontendJWT?: 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 表
   └─ 使用 postgrestDeleteWHERE 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_templateevaluation_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') {
  // 情况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

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);

📝 总结

核心功能

  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