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

1488 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 评查点系统 API v3 对接实施计划
> **制定日期**: 2025-11-25
> **版本**: v1.0
> **目标**: 逐步对接评查点分组和评查点管理的 API v3 接口,确保前端与后端完全兼容
---
## 📋 目录
1. [项目概况](#项目概况)
2. [模块 1:评查点分组管理](#模块-1评查点分组管理)
3. [模块 2:评查点管理](#模块-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`
**当前实现**:
```typescript
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 接口
- ⚠️ 缺少分页参数支持
- ⚠️ 缺少筛选条件支持(名称、编码、状态)
**改进方案**:
```typescript
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` 函数**
**当前实现**:
```typescript
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` 参数区分)
**改进方案**:
```typescript
// 删除独立的 getChildGroups 函数,统一使用 getRuleGroups
// 使用示例:
// 获取一级分组
const level1Groups = await getRuleGroups({ pid: null, token });
// 获取指定父级的子分组
const childGroups = await getRuleGroups({ pid: parentId, token });
```
**验收标准**:
- [ ] `getChildGroups` 函数已废弃
- [ ] 路由组件已更新为使用 `getRuleGroups({ pid: parentId })`
---
**3. 更新 `getRuleGroup` 函数(获取单个分组详情)**
**当前实现**:
```typescript
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 接口
- ⚠️ 缺少统计信息(评查点数量)
**改进方案**:
```typescript
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` 函数**
**当前实现**:
```typescript
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` 函数**
**当前实现**:
```typescript
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` 函数**
**当前实现**:
```typescript
export async function deleteRuleGroup(id: string, token?: string): Promise<ApiResponse<void>> {
const response = await postgrestDelete('evaluation_point_groups', { id }, token);
// ...
}
```
**需要改进**:
- ⚠️ 缺少级联删除提示
- ⚠️ 需要检查是否有关联的评查点
**改进方案**:
```typescript
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` 函数**
```typescript
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` 函数**
```typescript
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) **分页功能**
```typescript
// 当前:无分页
// 改进:添加分页组件
const [pagination, setPagination] = useState({ page: 1, pageSize: 50 });
// 在 loader 中使用分页参数
const response = await getRuleGroups({
page: pagination.page,
pageSize: pagination.pageSize,
...filters,
token: frontendJWT
});
```
b) **筛选功能优化**
```typescript
// 当前:客户端筛选
// 改进:服务端筛选
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) **批量操作功能**
```typescript
// 新增批量选择状态
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) **表单验证增强**
```typescript
// 添加异步验证:检查编码唯一性
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) **父级分组选择优化**
```typescript
// 当前:手动过滤
// 改进:使用 API 筛选
// 获取一级分组(用于二级分组的父级选择)
const parentGroupsResponse = await getRuleGroups({
pid: null,
is_enabled: true,
token: frontendJWT
});
```
**验收标准**:
- [ ] 编码唯一性实时验证
- [ ] 父级分组列表仅显示一级分组
- [ ] 父级分组列表仅显示启用状态的分组
- [ ] 保存成功后正确跳转
---
## 模块 2:评查点管理
### 📌 当前状态分析
**已实现功能**:
- ✅ 获取评查点列表
- ✅ 获取单个评查点详情
- ✅ 创建评查点
- ✅ 更新评查点
- ✅ 删除评查点
- ✅ 复制评查点
**缺失功能**:
- ❌ 批量启用/禁用评查点
- ❌ 批量删除评查点
- ❌ 统计信息接口
- ❌ 评查点使用记录查询
---
### 阶段 2.1:查询接口对接 ⏱️ 2 天
#### 任务清单
**1. 更新 `getRulesList` 函数**
**文件**: `app/api/evaluation_points/rules.ts`
**当前实现**:
```typescript
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 }>> {
// ...
}
```
**需要改进**:
- ✅ 已支持分页
- ✅ 已支持筛选
- ⚠️ 缺少排序参数
- ⚠️ 返回格式需要优化
**改进方案**:
```typescript
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` 函数(统计信息)**
```typescript
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` 函数**
**当前实现**:
```typescript
// 当前通过 postgrestPost 直接创建
const response = await postgrestPost('evaluation_points', finalData, frontendJWT);
```
**需要改进**:
- ⚠️ 缺少数据验证
- ⚠️ 缺少 JSONB 字段格式验证
**改进方案**:
```typescript
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` 函数**
**改进方案**:
```typescript
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` 函数**
**当前实现**:
```typescript
// 在 rules.new.tsx 中处理
// 通过 URL 参数 mode=copy 触发复制模式
```
**验证清单**:
- [ ] 复制时移除 `id`, `created_at`, `updated_at`, `usage_count`
- [ ] 清洗评查点编码(移除地区后缀)
- [ ] 提示用户修改编码和名称
- [ ] 保存时验证编码唯一性
---
### 阶段 2.4:删除接口对接 ⏱️ 0.5 天
#### 任务清单
**1. 更新 `deleteRule` 函数**
**当前实现**:
```typescript
export async function deleteRule(id: string, token?: string): Promise<ApiResponse<void>> {
const response = await postgrestDelete('evaluation_points', { id }, token);
return response;
}
```
**需要改进**:
- ⚠️ 缺少关联检查(评查结果)
**改进方案**:
```typescript
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` 函数**
```typescript
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` 函数**
```typescript
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) **批量选择功能**
```typescript
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) **批量操作按钮**
```tsx
<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) **统计信息展示**
```typescript
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) **异步验证优化**
```typescript
// 编码唯一性验证(防抖)
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) **保存前验证增强**
```typescript
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 查询语法参考
```typescript
// 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'
```
### 类型定义参考
```typescript
// 评查点分组
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;
}
```
---
**文档结束**