From 89b1d2e5f511ac6974f85d95412fbb54af5a2be4 Mon Sep 17 00:00:00 2001 From: Wenyan Date: Tue, 25 Nov 2025 12:13:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(evaluation):=20=E6=A8=A1=E5=9D=971.3=20-?= =?UTF-8?q?=20=E5=A2=9E=E5=BC=BA=E8=AF=84=E6=9F=A5=E7=82=B9=E5=88=86?= =?UTF-8?q?=E7=BB=84=E5=88=A0=E9=99=A4=E6=8E=A5=E5=8F=A3=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 主要改进 ### 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 --- app/api/evaluation_points/rule-groups.ts | 134 ++++++++++++++++++----- 1 file changed, 104 insertions(+), 30 deletions(-) diff --git a/app/api/evaluation_points/rule-groups.ts b/app/api/evaluation_points/rule-groups.ts index 5f8a9e3..ee4d4aa 100644 --- a/app/api/evaluation_points/rule-groups.ts +++ b/app/api/evaluation_points/rule-groups.ts @@ -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>>( + '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>('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 删除结果