feat(evaluation): 模块1.3 - 增强评查点分组删除接口安全性

## 主要改进

### 1. 删除策略优化(从级联删除改为阻止删除)
-  **安全优先**:采用阻止删除策略而非级联删除
-  删除前检查子分组,如有则拒绝删除
-  删除前检查关联评查点,如有则拒绝删除
-  只有空分组才能被删除

### 2. 详细的删除检查
-  ID有效性验证
-  分组存在性验证
-  子分组检查(仅一级分组)
-  评查点关联检查(所有分组)
-  返回详细的检查结果(childCount, pointCount)

### 3. 友好的错误提示
-  明确提示存在多少个子分组
-  明确提示存在多少个评查点
-  建议用户先清理关联数据
-  区分不同类型的删除失败原因

### 4. 标记废弃函数
-  deleteChildGroup 标记为 @deprecated
-  deleteEvaluationPointsByGroupId 标记为 @deprecated
-  保留代码以备将来批量删除功能使用

## 删除策略对比

### 旧策略(级联删除)- 高风险
 删除一级分组时自动删除所有子分组
 自动删除所有关联的评查点
 用户可能误删大量数据
 无法恢复

### 新策略(阻止删除)- 安全
 拒绝删除有子分组的一级分组
 拒绝删除有评查点的分组
 用户必须手动清理关联数据
 防止误删除
 提供清晰的错误提示

## 返回值增强

```typescript
{
  success: boolean;
  error?: string;
  details?: {
    hasChildren: boolean;  // 是否有子分组
    hasPoints: boolean;    // 是否有评查点
    childCount?: number;   // 子分组数量
    pointCount?: number;   // 评查点数量
  }
}
```

## 相关文件
- app/api/evaluation_points/rule-groups.ts

## 验收清单
- [x] TypeScript 类型检查通过
- [x] 删除前完整的关联检查
- [x] 阻止删除有依赖的分组
- [x] 详细的错误提示
- [x] 返回详细的检查结果

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-25 12:13:57 +08:00
parent e148fca429
commit 89b1d2e5f5
+104 -30
View File
@@ -740,50 +740,114 @@ export async function updateRuleGroup(id: string, data: RuleGroupCreateUpdateDto
}
/**
* 删除评查点分组
* 删除评查点分组(增强版 - 安全的阻止删除策略)
*
* 删除策略:
* - 如果分组下有子分组,拒绝删除,提示用户先删除子分组
* - 如果分组下有评查点,拒绝删除,提示用户先删除或移动评查点
* - 只有空分组才能被删除
*
* @param id 分组ID
* @param token JWT token (可选)
* @returns 删除结果
*/
export async function deleteRuleGroup(id: string, token?: string): Promise<{success: boolean; error?: string}> {
export async function deleteRuleGroup(id: string, token?: string): Promise<{success: boolean; error?: string; details?: { hasChildren: boolean; hasPoints: boolean; childCount?: number; pointCount?: number }}> {
try {
// 1. 首先获取分组信息,判断是一级还是二级分组
// ========== 1. ID验证 ==========
if (!id) {
return { success: false, error: '分组ID不能为空' };
}
// 验证分组是否存在
const groupResponse = await getRuleGroup(id, token);
if (groupResponse.error) {
return { success: false, error: groupResponse.error };
if (groupResponse.error || !groupResponse.data) {
return { success: false, error: '分组不存在或无法访问' };
}
const group = groupResponse.data;
if (!group) {
return { success: false, error: '未找到指定分组' };
}
// 2. 如果是一级分组(级分组pid为NULL或'0'),需要先删除所有子分组
if (!group.pid || group.pid === '0' || group.pid === null) {
// 获取所有子分组
// ========== 2. 检查是否有子分组(级分组专用) ==========
let hasChildren = false;
let childCount = 0;
// 如果是一级分组,检查是否有子分组
if (!group.pid || group.pid === '0') {
const childGroupsResponse = await getChildGroups(id, token);
if (childGroupsResponse.error) {
return { success: false, error: childGroupsResponse.error };
return {
success: false,
error: `检查子分组时出错: ${childGroupsResponse.error}`
};
}
const childGroups = childGroupsResponse.data || [];
// 遍历删除每个子分组
for (const childGroup of childGroups) {
const deleteChildResult = await deleteChildGroup(childGroup.id, token);
if (!deleteChildResult.success) {
return deleteChildResult;
}
childCount = childGroups.length;
hasChildren = childCount > 0;
if (hasChildren) {
return {
success: false,
error: `该分组下存在 ${childCount} 个子分组,请先删除所有子分组后再删除此分组。`,
details: {
hasChildren: true,
hasPoints: false,
childCount
}
};
}
}
// 3. 删除分组下的所有评查点
const deletePointsResult = await deleteEvaluationPointsByGroupId(id, token);
if (!deletePointsResult.success) {
return deletePointsResult;
// ========== 3. 检查是否有关联的评查点 ==========
const pointsParams: PostgrestParams = {
select: 'id',
filter: {
'evaluation_point_groups_id': `eq.${id}`
},
limit: 1, // 只需要知道是否存在,不需要获取所有数据
token
};
const pointsResponse = await postgrestGet<ApiResponse<Array<{id: number}>>>(
'evaluation_points',
pointsParams
);
let hasPoints = false;
let pointCount = group.ruleCount || 0;
if (pointsResponse.error) {
return {
success: false,
error: `检查关联评查点时出错: ${pointsResponse.error}`
};
}
// 4. 最后删除分组本身
if (pointsResponse.data) {
if ('code' in pointsResponse.data && pointsResponse.data.data) {
hasPoints = Array.isArray(pointsResponse.data.data) && pointsResponse.data.data.length > 0;
} else if (Array.isArray(pointsResponse.data)) {
hasPoints = pointsResponse.data.length > 0;
}
}
if (hasPoints) {
return {
success: false,
error: `该分组下存在 ${pointCount} 个评查点,请先删除或移动所有评查点后再删除此分组。`,
details: {
hasChildren: false,
hasPoints: true,
pointCount
}
};
}
// ========== 4. 执行删除操作 ==========
const response = await postgrestDelete<ApiResponse<{id: number}>>('evaluation_point_groups', {
filter: {
'id': `eq.${id}`
@@ -792,21 +856,29 @@ export async function deleteRuleGroup(id: string, token?: string): Promise<{succ
});
if (response.error) {
return { success: false, error: response.error };
return { success: false, error: `删除失败: ${response.error}` };
}
return { success: true };
return {
success: true,
details: {
hasChildren: false,
hasPoints: false
}
};
} catch (error) {
console.error('删除评查点分组失败:', error);
return {
success: false,
return {
success: false,
error: error instanceof Error ? error.message : '删除评查点分组失败'
};
}
}
/**
* 删除子分组及其相关数据
* 删除子分组及其相关数据(级联删除)
*
* @deprecated 当前采用阻止删除策略,此函数暂不使用
* @param id 子分组ID
* @param token JWT token (可选)
* @returns 删除结果
@@ -842,7 +914,9 @@ async function deleteChildGroup(id: string, token?: string): Promise<{success: b
}
/**
* 删除指定分组下的所有评查点
* 删除指定分组下的所有评查点(级联删除)
*
* @deprecated 当前采用阻止删除策略,此函数暂不使用
* @param groupId 分组ID
* @param token JWT token (可选)
* @returns 删除结果