Files
leaudit-platform-frontend/docs/evaluation/API对接实施计划.md
T
TanWenyan d3b9403d64 feat(evaluation): 模块1.1 - 增强评查点分组查询接口
## 主要改进

### 1. 增强 getRuleGroups 函数
-  添加完整的分页参数支持 (page, pageSize)
-  添加筛选参数 (name, code, is_enabled, pid)
-  添加排序参数 (orderBy, order)
-  返回总数 (totalCount)
-  支持一级分组和二级分组查询

### 2. 优化 getChildGroups 函数
-  内部使用改进后的 getRuleGroups 函数
-  自动添加评查点数量统计
-  改进类型安全性

### 3. 优化 getRuleGroup 函数
-  确保评查点数量统计准确
-  改进错误处理
-  优化类型守卫逻辑

### 4. 类型定义改进
-  新增 RuleGroupQueryParams 接口
-  ApiRuleGroup.pid 类型支持 null
-  修复所有 TypeScript 类型错误

### 5. 创建对接计划文档
-  详细的 API 对接实施计划
-  分模块逐步实施策略
-  验收标准和风险评估

## 相关文件
- app/api/evaluation_points/rule-groups.ts
- docs/evaluation/API对接实施计划.md

## 验收清单
- [x] TypeScript 类型检查通过
- [x] 支持分页、筛选、排序
- [x] 返回评查点数量统计
- [x] 向后兼容现有代码

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 12:06:48 +08:00

33 KiB
Raw Blame History

评查点系统 API v3 对接实施计划

制定日期: 2025-11-25 版本: v1.0 目标: 逐步对接评查点分组和评查点管理的 API v3 接口,确保前端与后端完全兼容


📋 目录

  1. 项目概况
  2. 模块 1:评查点分组管理
  3. 模块 2:评查点管理
  4. 验收标准
  5. 风险与注意事项

📊 项目概况

涉及文件

文档:

  • docs/evaluation/evaluation_point_groups.md - 分组 API 文档
  • docs/evaluation/evaluation_points.md - 评查点 API 文档

API 客户端:

  • app/api/evaluation_points/rule-groups.ts
  • app/api/evaluation_points/rules.ts

路由组件:

  • app/routes/rule-groups._index.tsx
  • app/routes/rule-groups.new.tsx
  • app/routes/rules.list.tsx
  • app/routes/rules.new.tsx

技术栈

  • 后端: PostgreSQL + PostgREST
  • 前端: Remix + React + TypeScript
  • API 协议: RESTful API (PostgREST 规范)

模块 1:评查点分组管理

📌 当前状态分析

已实现功能:

  • 获取分组列表(含子分组)
  • 获取单个分组详情
  • 创建分组
  • 更新分组
  • 删除分组

缺失功能:

  • 批量启用/禁用分组
  • 批量删除分组
  • 统计信息接口

阶段 1.1:查询接口对接 ⏱️ 1-2 天

任务清单

1. 更新 getRuleGroups 函数

文件: app/api/evaluation_points/rule-groups.ts

当前实现:

export async function getRuleGroups(token?: string): Promise<ApiResponse<RuleGroup[]>> {
  const response = await postgrestGet('evaluation_point_groups', {
    filter: { 'pid': 'is.null' },
    select: '*',
    order: { created_at: 'desc' },
    token
  });
  // ...
}

需要改进:

  • 已使用 PostgREST 接口
  • ⚠️ 缺少分页参数支持
  • ⚠️ 缺少筛选条件支持(名称、编码、状态)

改进方案:

export interface RuleGroupQueryParams {
  // 分页
  page?: number;
  pageSize?: number;

  // 筛选
  name?: string;       // 模糊搜索
  code?: string;       // 模糊搜索
  is_enabled?: boolean;
  pid?: string | null; // 父级IDnull表示一级分组

  // 排序
  orderBy?: 'created_at' | 'updated_at' | 'name' | 'code';
  order?: 'asc' | 'desc';

  token?: string;
}

export async function getRuleGroups(
  params?: RuleGroupQueryParams
): Promise<ApiResponse<RuleGroup[]>> {
  const {
    page = 1,
    pageSize = 50,
    name,
    code,
    is_enabled,
    pid,
    orderBy = 'created_at',
    order = 'desc',
    token
  } = params || {};

  const filter: Record<string, string> = {};

  // 构建筛选条件
  if (name) filter['name'] = `ilike.*${name}*`;
  if (code) filter['code'] = `ilike.*${code}*`;
  if (is_enabled !== undefined) filter['is_enabled'] = `eq.${is_enabled}`;
  if (pid === null) {
    filter['pid'] = 'is.null';
  } else if (pid) {
    filter['pid'] = `eq.${pid}`;
  }

  const response = await postgrestGet('evaluation_point_groups', {
    filter,
    select: '*',
    order: { [orderBy]: order },
    range: { from: (page - 1) * pageSize, to: page * pageSize - 1 },
    token
  });

  return response;
}

验收标准:

  • 支持分页(page, pageSize
  • 支持名称模糊搜索
  • 支持编码模糊搜索
  • 支持状态筛选
  • 支持获取一级分组(pid=null)或二级分组(pid=具体ID
  • 返回总数(通过 PostgREST 的 Prefer: count=exact header

2. 更新 getChildGroups 函数

当前实现:

export async function getChildGroups(parentId: string, token?: string): Promise<ApiResponse<RuleGroup[]>> {
  const response = await postgrestGet('evaluation_point_groups', {
    filter: { 'pid': `eq.${parentId}` },
    select: '*',
    order: { created_at: 'desc' },
    token
  });
  // ...
}

需要改进:

  • 已使用 PostgREST 接口
  • ⚠️ 建议合并到 getRuleGroups 函数(通过 pid 参数区分)

改进方案:

// 删除独立的 getChildGroups 函数,统一使用 getRuleGroups

// 使用示例:
// 获取一级分组
const level1Groups = await getRuleGroups({ pid: null, token });

// 获取指定父级的子分组
const childGroups = await getRuleGroups({ pid: parentId, token });

验收标准:

  • getChildGroups 函数已废弃
  • 路由组件已更新为使用 getRuleGroups({ pid: parentId })

3. 更新 getRuleGroup 函数(获取单个分组详情)

当前实现:

export async function getRuleGroup(id: string, token?: string): Promise<ApiResponse<RuleGroup>> {
  const response = await postgrestGet('evaluation_point_groups', {
    filter: { 'id': `eq.${id}` },
    select: '*',
    token
  });
  // ...
}

需要改进:

  • 已使用 PostgREST 接口
  • ⚠️ 缺少统计信息(评查点数量)

改进方案:

export async function getRuleGroup(id: string, token?: string): Promise<ApiResponse<RuleGroup>> {
  // 方案 1:使用 PostgREST 的关联查询
  const response = await postgrestGet('evaluation_point_groups', {
    filter: { 'id': `eq.${id}` },
    select: '*, evaluation_points(count)',
    token
  });

  // 方案 2:分两次查询
  // 1. 获取分组信息
  // 2. 查询该分组下的评查点数量

  return response;
}

验收标准:

  • 返回分组详细信息
  • 包含该分组下的评查点数量统计

阶段 1.2:创建/更新接口对接 ⏱️ 1 天

任务清单

1. 更新 createRuleGroup 函数

当前实现:

export async function createRuleGroup(
  data: RuleGroupCreateUpdateDto,
  token?: string
): Promise<ApiResponse<RuleGroup>> {
  const response = await postgrestPost('evaluation_point_groups', data, token);
  // ...
}

验证清单:

  • 必填字段验证(name, code
  • 编码唯一性验证
  • 父级ID验证(如果是二级分组)
  • 返回新创建的分组完整信息

2. 更新 updateRuleGroup 函数

当前实现:

export async function updateRuleGroup(
  id: string,
  data: RuleGroupCreateUpdateDto,
  token?: string
): Promise<ApiResponse<RuleGroup>> {
  const response = await postgrestPut(
    'evaluation_point_groups',
    data,
    { id },
    token
  );
  // ...
}

验证清单:

  • ID 有效性验证
  • 不允许修改 pid(防止分组层级混乱)
  • 编码唯一性验证(排除自身)
  • 返回更新后的分组完整信息

阶段 1.3:删除接口对接 ⏱️ 0.5 天

任务清单

1. 更新 deleteRuleGroup 函数

当前实现:

export async function deleteRuleGroup(id: string, token?: string): Promise<ApiResponse<void>> {
  const response = await postgrestDelete('evaluation_point_groups', { id }, token);
  // ...
}

需要改进:

  • ⚠️ 缺少级联删除提示
  • ⚠️ 需要检查是否有关联的评查点

改进方案:

export async function deleteRuleGroup(id: string, token?: string): Promise<ApiResponse<void>> {
  // 1. 检查是否有子分组
  const childGroups = await getRuleGroups({ pid: id, token });
  if (childGroups.data && childGroups.data.length > 0) {
    return {
      success: false,
      error: '该分组下存在子分组,请先删除子分组',
      status: 400
    };
  }

  // 2. 检查是否有关联的评查点
  const points = await postgrestGet('evaluation_points', {
    filter: { 'evaluation_point_groups_id': `eq.${id}` },
    select: 'count',
    token
  });

  if (points.data && points.data.length > 0) {
    return {
      success: false,
      error: '该分组下存在评查点,请先删除或移动评查点',
      status: 400
    };
  }

  // 3. 执行删除
  const response = await postgrestDelete('evaluation_point_groups', { id }, token);
  return response;
}

验收标准:

  • 删除前检查子分组
  • 删除前检查关联评查点
  • 提供清晰的错误提示
  • 删除成功后返回成功状态

阶段 1.4:批量操作接口对接 ⏱️ 1 天

任务清单

1. 新增 batchUpdateRuleGroupStatus 函数

export interface BatchUpdateStatusDto {
  ids: string[];
  is_enabled: boolean;
}

export async function batchUpdateRuleGroupStatus(
  data: BatchUpdateStatusDto,
  token?: string
): Promise<ApiResponse<{ updated_count: number }>> {
  const { ids, is_enabled } = data;

  const response = await postgrestPatch(
    'evaluation_point_groups',
    { is_enabled },
    { id: `in.(${ids.join(',')})` },
    token
  );

  return {
    success: true,
    data: {
      updated_count: response.data?.length || 0
    }
  };
}

验收标准:

  • 支持批量启用/禁用
  • 返回更新数量
  • 处理部分失败的情况

2. 新增 batchDeleteRuleGroups 函数

export async function batchDeleteRuleGroups(
  ids: string[],
  token?: string
): Promise<ApiResponse<{ deleted_count: number; failed_ids: string[] }>> {
  const failedIds: string[] = [];
  let deletedCount = 0;

  for (const id of ids) {
    const result = await deleteRuleGroup(id, token);
    if (result.success) {
      deletedCount++;
    } else {
      failedIds.push(id);
    }
  }

  return {
    success: failedIds.length === 0,
    data: {
      deleted_count: deletedCount,
      failed_ids: failedIds
    }
  };
}

验收标准:

  • 支持批量删除
  • 返回删除成功数量
  • 返回删除失败的 ID 列表
  • 提供详细的失败原因

阶段 1.5:前端组件更新 ⏱️ 2 天

任务清单

1. 更新 rule-groups._index.tsx

需要改进的功能:

a) 分页功能

// 当前:无分页
// 改进:添加分页组件

const [pagination, setPagination] = useState({ page: 1, pageSize: 50 });

// 在 loader 中使用分页参数
const response = await getRuleGroups({
  page: pagination.page,
  pageSize: pagination.pageSize,
  ...filters,
  token: frontendJWT
});

b) 筛选功能优化

// 当前:客户端筛选
// 改进:服务端筛选

const handleFilterChange = (filters: RuleGroupQueryParams) => {
  const newParams = new URLSearchParams();
  if (filters.name) newParams.set('name', filters.name);
  if (filters.code) newParams.set('code', filters.code);
  if (filters.is_enabled !== undefined) {
    newParams.set('is_enabled', filters.is_enabled.toString());
  }
  setSearchParams(newParams);
};

c) 批量操作功能

// 新增批量选择状态
const [selectedIds, setSelectedIds] = useState<string[]>([]);

// 批量启用/禁用
const handleBatchEnable = async (enable: boolean) => {
  const result = await batchUpdateRuleGroupStatus({
    ids: selectedIds,
    is_enabled: enable
  }, frontendJWT);

  if (result.success) {
    toastService.success(`已${enable ? '启用' : '禁用'} ${result.data.updated_count} 个分组`);
    // 刷新列表
  }
};

// 批量删除
const handleBatchDelete = async () => {
  messageService.show({
    title: "确认批量删除",
    message: `确认删除选中的 ${selectedIds.length} 个分组吗?`,
    type: "warning",
    onConfirm: async () => {
      const result = await batchDeleteRuleGroups(selectedIds, frontendJWT);
      toastService.success(`成功删除 ${result.data.deleted_count} 个分组`);
      if (result.data.failed_ids.length > 0) {
        toastService.warning(`有 ${result.data.failed_ids.length} 个分组删除失败`);
      }
      // 刷新列表
    }
  });
};

验收标准:

  • 表格支持多选(Checkbox
  • 顶部批量操作按钮区域
  • 批量启用/禁用功能正常
  • 批量删除功能正常
  • 分页功能正常
  • 服务端筛选功能正常

2. 更新 rule-groups.new.tsx

需要改进的功能:

a) 表单验证增强

// 添加异步验证:检查编码唯一性
const validateCodeUnique = async (code: string, currentId?: string) => {
  const response = await getRuleGroups({ code, token: frontendJWT });
  if (response.data && response.data.length > 0) {
    // 编辑模式下排除自身
    if (currentId && response.data[0].id === currentId) {
      return true;
    }
    return false;
  }
  return true;
};

b) 父级分组选择优化

// 当前:手动过滤
// 改进:使用 API 筛选

// 获取一级分组(用于二级分组的父级选择)
const parentGroupsResponse = await getRuleGroups({
  pid: null,
  is_enabled: true,
  token: frontendJWT
});

验收标准:

  • 编码唯一性实时验证
  • 父级分组列表仅显示一级分组
  • 父级分组列表仅显示启用状态的分组
  • 保存成功后正确跳转

模块 2:评查点管理

📌 当前状态分析

已实现功能:

  • 获取评查点列表
  • 获取单个评查点详情
  • 创建评查点
  • 更新评查点
  • 删除评查点
  • 复制评查点

缺失功能:

  • 批量启用/禁用评查点
  • 批量删除评查点
  • 统计信息接口
  • 评查点使用记录查询

阶段 2.1:查询接口对接 ⏱️ 2 天

任务清单

1. 更新 getRulesList 函数

文件: app/api/evaluation_points/rules.ts

当前实现:

export async function getRulesList(params: {
  ruleType?: string;
  groupId?: string;
  isActive?: boolean;
  keyword?: string;
  area?: string;
  page?: number;
  pageSize?: number;
  token?: string;
}): Promise<ApiResponse<{ rules: ApiRule[]; totalCount: number }>> {
  // ...
}

需要改进:

  • 已支持分页
  • 已支持筛选
  • ⚠️ 缺少排序参数
  • ⚠️ 返回格式需要优化

改进方案:

export interface RuleQueryParams {
  // 分页
  page?: number;
  pageSize?: number;

  // 筛选
  keyword?: string;                 // 名称或编码模糊搜索
  evaluation_point_groups_pid?: string;  // 评查点类型(一级分组)
  evaluation_point_groups_id?: string;   // 所属规则组(二级分组)
  risk?: 'low' | 'medium' | 'high';      // 风险等级
  is_enabled?: boolean;                  // 启用状态
  area?: string;                         // 地区过滤

  // 排序
  orderBy?: 'created_at' | 'updated_at' | 'name' | 'code' | 'usage_count';
  order?: 'asc' | 'desc';

  token?: string;
}

export async function getRulesList(
  params?: RuleQueryParams
): Promise<ApiResponse<{ rules: EvaluationPoint[]; totalCount: number }>> {
  const {
    page = 1,
    pageSize = 10,
    keyword,
    evaluation_point_groups_pid,
    evaluation_point_groups_id,
    risk,
    is_enabled,
    area,
    orderBy = 'created_at',
    order = 'desc',
    token
  } = params || {};

  const filter: Record<string, string> = {};

  // 构建筛选条件
  if (keyword) {
    // PostgREST 不支持 OR 条件,需要使用 or 语法
    filter['or'] = `(name.ilike.*${keyword}*,code.ilike.*${keyword}*)`;
  }
  if (evaluation_point_groups_pid) {
    filter['evaluation_point_groups_pid'] = `eq.${evaluation_point_groups_pid}`;
  }
  if (evaluation_point_groups_id) {
    filter['evaluation_point_groups_id'] = `eq.${evaluation_point_groups_id}`;
  }
  if (risk) filter['risk'] = `eq.${risk}`;
  if (is_enabled !== undefined) filter['is_enabled'] = `eq.${is_enabled}`;

  // 地区过滤(需要通过 code 字段的后缀实现)
  if (area) {
    filter['code'] = `like.*--${area}`;
  }

  const response = await postgrestGet('evaluation_points', {
    filter,
    select: '*,evaluation_point_groups:evaluation_point_groups_id(*)',
    order: { [orderBy]: order },
    range: { from: (page - 1) * pageSize, to: page * pageSize - 1 },
    count: 'exact',
    token
  });

  if (response.data) {
    return {
      success: true,
      data: {
        rules: response.data,
        totalCount: response.count || 0
      }
    };
  }

  return response;
}

验收标准:

  • 支持关键词搜索(名称或编码)
  • 支持按评查点类型筛选
  • 支持按规则组筛选
  • 支持按风险等级筛选
  • 支持按启用状态筛选
  • 支持按地区筛选
  • 支持排序(创建时间、更新时间、使用次数等)
  • 返回总数(用于分页)
  • 关联查询规则组信息

2. 新增 getRuleStatistics 函数(统计信息)

export interface RuleStatistics {
  total_count: number;
  enabled_count: number;
  disabled_count: number;
  by_risk: {
    low: number;
    medium: number;
    high: number;
  };
  by_group: Array<{
    group_id: string;
    group_name: string;
    count: number;
  }>;
}

export async function getRuleStatistics(
  token?: string
): Promise<ApiResponse<RuleStatistics>> {
  // 使用 PostgREST 的聚合查询
  const response = await postgrestGet('evaluation_points', {
    select: `
      count,
      is_enabled,
      risk,
      evaluation_point_groups_id
    `,
    token
  });

  // 处理响应数据,计算统计信息
  // ...

  return {
    success: true,
    data: statistics
  };
}

验收标准:

  • 返回总评查点数
  • 返回启用/禁用数量
  • 返回按风险等级分组的数量
  • 返回按规则组分组的数量

阶段 2.2:创建/更新接口对接 ⏱️ 2 天

任务清单

1. 更新 createRule 函数

当前实现:

// 当前通过 postgrestPost 直接创建
const response = await postgrestPost('evaluation_points', finalData, frontendJWT);

需要改进:

  • ⚠️ 缺少数据验证
  • ⚠️ 缺少 JSONB 字段格式验证

改进方案:

export interface CreateRuleDto {
  name: string;
  code: string;
  risk: 'low' | 'medium' | 'high';
  is_enabled?: boolean;
  description?: string;
  references_laws?: {
    name: string;
    articles: string[];
    content: string;
  };
  evaluation_point_groups_pid: string;
  evaluation_point_groups_id: string;
  extraction_config: {
    llm?: {
      fields: string[];
      prompt_setting: {
        type: string;
        template: string;
      };
    };
    vlm?: {
      fields: Array<string | { name: string; type: string }>;
      prompt_setting: {
        type: string;
        template: string;
      };
    };
    regex?: {
      fields: Array<{ field: string; pattern: string }>;
    };
  };
  evaluation_config: {
    logicType: 'and' | 'or' | 'custom';
    customLogic?: string;
    rules: Array<{
      id: string;
      type: string;
      config: Record<string, unknown>;
    }>;
  };
  pass_message?: string;
  fail_message?: string;
  suggestion_message?: string;
  suggestion_message_type?: 'info' | 'warning' | 'error';
  post_action?: string;
  action_config?: string;
  score?: number;
}

export async function createRule(
  data: CreateRuleDto,
  token?: string
): Promise<ApiResponse<EvaluationPoint>> {
  // 1. 验证必填字段
  if (!data.name || !data.code) {
    return {
      success: false,
      error: '名称和编码不能为空',
      status: 400
    };
  }

  // 2. 验证编码唯一性
  const existingRule = await getRulesList({
    keyword: data.code,
    token
  });
  if (existingRule.data && existingRule.data.rules.length > 0) {
    return {
      success: false,
      error: '评查点编码已存在',
      status: 400
    };
  }

  // 3. 验证 JSONB 字段格式
  try {
    JSON.parse(JSON.stringify(data.extraction_config));
    JSON.parse(JSON.stringify(data.evaluation_config));
  } catch (error) {
    return {
      success: false,
      error: 'extraction_config 或 evaluation_config 格式无效',
      status: 400
    };
  }

  // 4. 执行创建
  const response = await postgrestPost('evaluation_points', data, token);
  return response;
}

验收标准:

  • 必填字段验证
  • 编码唯一性验证
  • JSONB 字段格式验证
  • 返回新创建的评查点完整信息

2. 更新 updateRule 函数

改进方案:

export async function updateRule(
  id: string,
  data: Partial<CreateRuleDto>,
  token?: string
): Promise<ApiResponse<EvaluationPoint>> {
  // 1. 验证 ID 有效性
  const existing = await postgrestGet('evaluation_points', {
    filter: { id: `eq.${id}` },
    token
  });

  if (!existing.data || existing.data.length === 0) {
    return {
      success: false,
      error: '评查点不存在',
      status: 404
    };
  }

  // 2. 验证编码唯一性(排除自身)
  if (data.code) {
    const duplicate = await getRulesList({
      keyword: data.code,
      token
    });
    if (duplicate.data && duplicate.data.rules.length > 0) {
      const isDuplicate = duplicate.data.rules.some(r => r.id !== id);
      if (isDuplicate) {
        return {
          success: false,
          error: '评查点编码已存在',
          status: 400
        };
      }
    }
  }

  // 3. 验证 JSONB 字段格式
  if (data.extraction_config || data.evaluation_config) {
    try {
      if (data.extraction_config) {
        JSON.parse(JSON.stringify(data.extraction_config));
      }
      if (data.evaluation_config) {
        JSON.parse(JSON.stringify(data.evaluation_config));
      }
    } catch (error) {
      return {
        success: false,
        error: 'extraction_config 或 evaluation_config 格式无效',
        status: 400
      };
    }
  }

  // 4. 执行更新
  const response = await postgrestPut(
    'evaluation_points',
    data,
    { id },
    token
  );

  return response;
}

验收标准:

  • ID 有效性验证
  • 编码唯一性验证(排除自身)
  • JSONB 字段格式验证
  • 返回更新后的评查点完整信息
  • 支持部分字段更新

阶段 2.3:复制功能对接 ⏱️ 0.5 天

任务清单

1. 验证 copyRule 函数

当前实现:

// 在 rules.new.tsx 中处理
// 通过 URL 参数 mode=copy 触发复制模式

验证清单:

  • 复制时移除 id, created_at, updated_at, usage_count
  • 清洗评查点编码(移除地区后缀)
  • 提示用户修改编码和名称
  • 保存时验证编码唯一性

阶段 2.4:删除接口对接 ⏱️ 0.5 天

任务清单

1. 更新 deleteRule 函数

当前实现:

export async function deleteRule(id: string, token?: string): Promise<ApiResponse<void>> {
  const response = await postgrestDelete('evaluation_points', { id }, token);
  return response;
}

需要改进:

  • ⚠️ 缺少关联检查(评查结果)

改进方案:

export async function deleteRule(id: string, token?: string): Promise<ApiResponse<void>> {
  // 1. 检查是否有关联的评查结果
  const results = await postgrestGet('evaluation_results', {
    filter: { 'evaluation_point_id': `eq.${id}` },
    select: 'count',
    token
  });

  if (results.data && results.data.length > 0) {
    return {
      success: false,
      error: '该评查点已被使用,无法删除。如需停用,请使用禁用功能。',
      status: 400
    };
  }

  // 2. 执行删除
  const response = await postgrestDelete('evaluation_points', { id }, token);
  return response;
}

验收标准:

  • 删除前检查关联评查结果
  • 提供清晰的错误提示
  • 建议使用禁用功能代替删除

阶段 2.5:批量操作接口对接 ⏱️ 1 天

任务清单

1. 新增 batchUpdateRuleStatus 函数

export async function batchUpdateRuleStatus(
  data: { ids: string[]; is_enabled: boolean },
  token?: string
): Promise<ApiResponse<{ updated_count: number }>> {
  const { ids, is_enabled } = data;

  const response = await postgrestPatch(
    'evaluation_points',
    { is_enabled },
    { id: `in.(${ids.join(',')})` },
    token
  );

  return {
    success: true,
    data: {
      updated_count: response.data?.length || 0
    }
  };
}

验收标准:

  • 支持批量启用/禁用
  • 返回更新数量

2. 新增 batchDeleteRules 函数

export async function batchDeleteRules(
  ids: string[],
  token?: string
): Promise<ApiResponse<{ deleted_count: number; failed_ids: string[] }>> {
  const failedIds: string[] = [];
  let deletedCount = 0;

  for (const id of ids) {
    const result = await deleteRule(id, token);
    if (result.success) {
      deletedCount++;
    } else {
      failedIds.push(id);
    }
  }

  return {
    success: failedIds.length === 0,
    data: {
      deleted_count: deletedCount,
      failed_ids: failedIds
    }
  };
}

验收标准:

  • 支持批量删除
  • 返回删除成功数量
  • 返回删除失败的 ID 列表

阶段 2.6:前端组件更新 ⏱️ 3 天

任务清单

1. 更新 rules.list.tsx

需要改进的功能:

a) 批量选择功能

const [selectedIds, setSelectedIds] = useState<string[]>([]);

const columns = [
  {
    title: <Checkbox checked={allSelected} onChange={handleSelectAll} />,
    key: "selection",
    width: "50px",
    render: (_: unknown, record: Rule) => (
      <Checkbox
        checked={selectedIds.includes(record.id)}
        onChange={() => handleSelectRow(record.id)}
      />
    )
  },
  // ... 其他列
];

b) 批量操作按钮

<div className="batch-actions">
  <Button
    type="default"
    disabled={selectedIds.length === 0}
    onClick={() => handleBatchEnable(true)}
  >
    批量启用
  </Button>
  <Button
    type="default"
    disabled={selectedIds.length === 0}
    onClick={() => handleBatchEnable(false)}
  >
    批量禁用
  </Button>
  <Button
    type="danger"
    disabled={selectedIds.length === 0}
    onClick={handleBatchDelete}
  >
    批量删除
  </Button>
</div>

c) 统计信息展示

const { data: statistics } = await getRuleStatistics(frontendJWT);

// 在页面顶部展示统计卡片
<div className="statistics-cards">
  <StatCard title="总评查点" value={statistics.total_count} />
  <StatCard title="已启用" value={statistics.enabled_count} color="success" />
  <StatCard title="已禁用" value={statistics.disabled_count} color="warning" />
  <StatCard title="高风险" value={statistics.by_risk.high} color="error" />
</div>

验收标准:

  • 表格支持多选
  • 批量操作按钮显示/禁用状态正确
  • 批量启用/禁用功能正常
  • 批量删除功能正常
  • 统计信息展示正常

2. 更新 rules.new.tsx

需要改进的功能:

a) 异步验证优化

// 编码唯一性验证(防抖)
const validateCodeUnique = useMemo(
  () => debounce(async (code: string, currentId?: string) => {
    const response = await getRulesList({ keyword: code, token: frontendJWT });
    if (response.data && response.data.rules.length > 0) {
      const isDuplicate = response.data.rules.some(r => r.id !== currentId);
      if (isDuplicate) {
        setFormErrors(prev => ({
          ...prev,
          code: '评查点编码已存在'
        }));
      }
    }
  }, 500),
  [frontendJWT]
);

b) 保存前验证增强

const handleSave = async () => {
  // 1. 基本字段验证
  if (!formData.name?.trim()) {
    toastService.warning("评查点名称不能为空");
    return;
  }

  // 2. 编码唯一性验证
  const codeValid = await validateCodeUnique(formData.code, formData.id);
  if (!codeValid) {
    toastService.warning("评查点编码已存在");
    return;
  }

  // 3. JSONB 字段格式验证
  try {
    JSON.parse(JSON.stringify(formData.extraction_config));
    JSON.parse(JSON.stringify(formData.evaluation_config));
  } catch (error) {
    toastService.error("配置格式无效");
    return;
  }

  // 4. 评查规则完整性验证
  // (已有实现,保持不变)

  // 5. 执行保存
  const result = isEditMode
    ? await updateRule(formData.id!, formData, frontendJWT)
    : await createRule(formData, frontendJWT);

  if (result.success) {
    toastService.success("保存成功");
    navigate(`/rules/new?id=${result.data.id}`);
  } else {
    toastService.error(result.error || "保存失败");
  }
};

验收标准:

  • 编码唯一性异步验证(带防抖)
  • JSONB 字段格式验证
  • 保存前完整性验证
  • 错误提示清晰明确
  • 保存成功后正确跳转

验收标准

功能验收

模块 1:评查点分组管理

  • 查询功能

    • 获取一级分组列表
    • 获取二级分组列表
    • 按名称筛选
    • 按编码筛选
    • 按状态筛选
    • 分页功能正常
  • 创建功能

    • 创建一级分组
    • 创建二级分组
    • 必填字段验证
    • 编码唯一性验证
  • 更新功能

    • 更新分组信息
    • 不允许修改父级ID
    • 编码唯一性验证(排除自身)
  • 删除功能

    • 删除前检查子分组
    • 删除前检查关联评查点
    • 级联删除提示
  • 批量操作

    • 批量启用/禁用
    • 批量删除
    • 错误处理

模块 2:评查点管理

  • 查询功能

    • 获取评查点列表
    • 按关键词搜索
    • 按评查点类型筛选
    • 按规则组筛选
    • 按风险等级筛选
    • 按状态筛选
    • 按地区筛选
    • 排序功能
    • 分页功能
    • 统计信息展示
  • 创建功能

    • 创建评查点
    • 必填字段验证
    • 编码唯一性验证
    • JSONB 字段格式验证
    • 评查规则完整性验证
  • 更新功能

    • 更新评查点
    • 编码唯一性验证(排除自身)
    • JSONB 字段格式验证
    • 部分字段更新
  • 复制功能

    • 复制评查点
    • 移除不应复制的字段
    • 清洗编码
    • 提示用户修改
  • 删除功能

    • 删除前检查关联评查结果
    • 建议使用禁用功能
  • 批量操作

    • 批量启用/禁用
    • 批量删除
    • 错误处理

性能验收

  • 列表页加载时间 < 2 秒
  • 筛选响应时间 < 1 秒
  • 保存响应时间 < 2 秒
  • 批量操作响应时间 < 5 秒

代码质量验收

  • TypeScript 类型定义完整
  • 无 ESLint 错误
  • 无 TypeScript 类型错误
  • 代码注释清晰
  • 遵循项目编码规范

风险与注意事项

数据迁移风险

问题: 现有数据格式可能与 API v3 不完全兼容

解决方案:

  1. 在开发环境进行充分测试
  2. 编写数据迁移脚本
  3. 备份生产数据
  4. 分批次迁移

性能风险

问题: 大数据量情况下查询性能下降

解决方案:

  1. 添加数据库索引
  2. 优化 PostgREST 查询
  3. 实现分页和懒加载
  4. 使用缓存机制

兼容性风险

问题: 旧版本 API 调用可能失败

解决方案:

  1. 保留旧版本 API(向后兼容)
  2. 使用 API 版本控制
  3. 提供迁移指南
  4. 逐步废弃旧接口

JSONB 字段风险

问题: JSONB 字段格式复杂,容易出错

解决方案:

  1. 严格的数据验证
  2. 使用 JSON Schema 验证
  3. 提供默认值和模板
  4. 详细的错误提示

实施时间表

阶段 模块 工作量 起止日期
1.1 评查点分组 - 查询接口对接 1-2 天 Day 1-2
1.2 评查点分组 - 创建/更新接口对接 1 天 Day 3
1.3 评查点分组 - 删除接口对接 0.5 天 Day 4 上午
1.4 评查点分组 - 批量操作接口对接 1 天 Day 4 下午 - Day 5
1.5 评查点分组 - 前端组件更新 2 天 Day 6-7
2.1 评查点 - 查询接口对接 2 天 Day 8-9
2.2 评查点 - 创建/更新接口对接 2 天 Day 10-11
2.3 评查点 - 复制功能对接 0.5 天 Day 12 上午
2.4 评查点 - 删除接口对接 0.5 天 Day 12 下午
2.5 评查点 - 批量操作接口对接 1 天 Day 13
2.6 评查点 - 前端组件更新 3 天 Day 14-16
测试 集成测试 + 修复 Bug 2 天 Day 17-18
部署 生产环境部署 1 天 Day 19

总计: 约 19 个工作日(约 4 周)


附录

PostgREST 查询语法参考

// 1. 筛选条件
filter: {
  'name': 'ilike.*关键词*',        // 模糊搜索(不区分大小写)
  'is_enabled': 'eq.true',         // 等于
  'risk': 'in.(low,medium)',      // 包含
  'created_at': 'gte.2024-01-01', // 大于等于
  'pid': 'is.null'                // 为空
}

// 2. OR 条件
filter: {
  'or': '(name.ilike.*关键词*,code.ilike.*关键词*)'
}

// 3. 关联查询
select: '*, evaluation_point_groups:evaluation_point_groups_id(*)'

// 4. 排序
order: { 'created_at': 'desc' }

// 5. 分页
range: { from: 0, to: 9 }  // 前 10 条

// 6. 计数
count: 'exact'

类型定义参考

// 评查点分组
export interface RuleGroup {
  id: string;
  name: string;
  code: string;
  pid: string | null;
  description?: string;
  is_enabled: boolean;
  created_at: string;
  updated_at: string;
  children?: RuleGroup[];
  ruleCount?: number;
}

// 评查点
export interface EvaluationPoint {
  id?: string;
  name: string;
  code: string;
  risk: 'low' | 'medium' | 'high';
  is_enabled: boolean;
  description?: string;
  references_laws?: {
    name: string;
    articles: string[];
    content: string;
  };
  evaluation_point_groups_pid: string;
  evaluation_point_groups_id: string;
  extraction_config: ExtractionConfig;
  evaluation_config: EvaluationConfig;
  pass_message?: string;
  fail_message?: string;
  suggestion_message?: string;
  suggestion_message_type?: 'info' | 'warning' | 'error';
  post_action?: string;
  action_config?: string;
  score?: number;
  usage_count?: number;
  created_at?: string;
  updated_at?: string;
}

文档结束