From 371846c5ade6913de0f18a2428d59c0d953eb008 Mon Sep 17 00:00:00 2001 From: Wenyan Date: Tue, 25 Nov 2025 12:24:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(evaluation):=20=E6=A8=A1=E5=9D=972.2=20-?= =?UTF-8?q?=20=E5=A2=9E=E5=BC=BA=E8=AF=84=E6=9F=A5=E7=82=B9=E5=88=9B?= =?UTF-8?q?=E5=BB=BA/=E6=9B=B4=E6=96=B0=E6=8E=A5=E5=8F=A3=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 功能变更: 1. 增强 createRule 函数 - 添加必填字段验证(name, code) - 验证名称长度(1-100字符) - 验证编码格式(仅允许字母、数字、连字符和下划线) - 验证编码唯一性(防止重复) - 验证分组ID有效性(检查分组是否存在) - 自动trim名称和编码空格 - 返回详细的错误信息和HTTP状态码 2. 增强 updateRule 函数 - 验证评查点ID有效性(检查评查点是否存在) - 验证名称长度(如果提供) - 验证编码格式(如果提供) - 验证编码唯一性(排除自身,防止与其他评查点冲突) - 验证分组ID有效性(如果提供) - 自动trim名称和编码空格 - 支持部分字段更新 - 返回详细的错误信息和HTTP状态码 技术实现: - 复用 getRulesList 进行编码唯一性检查 - 复用 getRule 进行ID有效性检查 - 使用 PostgREST 查询验证分组存在性 - 精确匹配防止关键词模糊搜索误判 - 统一错误处理和状态码返回 验收标准: ✅ 必填字段验证 ✅ 名称长度验证(1-100字符) ✅ 编码格式验证(^[a-zA-Z0-9-_]+$) ✅ 编码唯一性验证 ✅ 分组ID有效性验证 ✅ 更新时ID存在性验证 ✅ 更新时编码唯一性验证(排除自身) ✅ 支持部分字段更新 ✅ 返回清晰的错误提示 符合实施计划: - 阶段 2.2:评查点创建/更新接口对接 ✅ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/api/evaluation_points/rules.ts | 162 ++++++++++++++++++++++++----- 1 file changed, 138 insertions(+), 24 deletions(-) diff --git a/app/api/evaluation_points/rules.ts b/app/api/evaluation_points/rules.ts index 73d01a8..5db0c44 100644 --- a/app/api/evaluation_points/rules.ts +++ b/app/api/evaluation_points/rules.ts @@ -509,14 +509,69 @@ export async function getRule(id: string, token?: string): Promise<{data: Rule; */ export async function createRule(ruleData: Omit, token?: string): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> { try { + // 1. 验证必填字段 + if (!ruleData.name || !ruleData.code) { + return { error: '评查点名称和编码不能为空', status: 400 }; + } + + // 2. 验证名称长度(1-100字符) + const trimmedName = ruleData.name.trim(); + if (trimmedName.length === 0 || trimmedName.length > 100) { + return { error: '评查点名称长度必须在1-100个字符之间', status: 400 }; + } + + // 3. 验证编码格式(仅允许字母、数字、连字符和下划线) + const trimmedCode = ruleData.code.trim(); + if (!/^[a-zA-Z0-9-_]+$/.test(trimmedCode)) { + return { error: '评查点编码只能包含字母、数字、连字符和下划线', status: 400 }; + } + + // 4. 验证编码唯一性 + const existingRulesResponse = await getRulesList({ + keyword: trimmedCode, + pageSize: 10, + token + }); + + if (existingRulesResponse.data && existingRulesResponse.data.rules.length > 0) { + // 精确匹配检查(因为keyword是模糊搜索) + const exactMatch = existingRulesResponse.data.rules.some(r => r.code === trimmedCode); + if (exactMatch) { + return { error: '评查点编码已存在,请使用其他编码', status: 409 }; + } + } + + // 5. 验证分组ID有效性 + if (!ruleData.groupId) { + return { error: '必须选择所属规则组', status: 400 }; + } + + // 检查分组是否存在 + const groupResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number; name: string; pid: number}> }>('evaluation_point_groups', { + filter: { 'id': `eq.${ruleData.groupId}` }, + select: 'id,name,pid', + token + }); + + let groupExists = false; + if (groupResponse.data && 'code' in groupResponse.data && groupResponse.data.data) { + groupExists = Array.isArray(groupResponse.data.data) && groupResponse.data.data.length > 0; + } else if (Array.isArray(groupResponse.data)) { + groupExists = groupResponse.data.length > 0; + } + + if (!groupExists) { + return { error: '所选规则组不存在', status: 404 }; + } + // 将前端模型转换为API接受的格式 const apiRuleData = { - code: ruleData.code, - name: ruleData.name, + code: trimmedCode, + name: trimmedName, evaluation_point_groups_id: parseInt(ruleData.groupId), risk: ruleData.priority === 'high' ? '高' : ruleData.priority === 'medium' ? '中' : '低', - description: ruleData.description, - is_enabled: ruleData.isActive, + description: ruleData.description || '', + is_enabled: ruleData.isActive !== undefined ? ruleData.isActive : true, // 以下是默认值,实际应用中需要根据业务逻辑设置 references_laws: {}, extraction_config: { @@ -534,27 +589,27 @@ export async function createRule(ruleData: Omit('evaluation_points', apiRuleData, token); - + // 检查是否有错误响应 if (response.error) { return { error: response.error, status: response.status }; } - + // 确保响应数据存在且符合预期格式 if (!response.data || !response.data.data) { return { error: '接口返回数据格式不正确', status: 500 }; } - + // 将API返回的数据映射到前端模型 const rule = mapApiRuleToFrontendModel(response.data.data); - + return { data: rule }; } catch (error) { console.error('创建评查点出错:', error); - return { + return { error: error instanceof Error ? error.message : '创建评查点失败', status: 500 }; @@ -570,53 +625,112 @@ export async function createRule(ruleData: Omit>, token?: string): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> { try { + // 1. 验证评查点ID有效性 + const existingRuleResponse = await getRule(id, token); + if (existingRuleResponse.error || !existingRuleResponse.data) { + return { error: '评查点不存在', status: 404 }; + } + + // 2. 验证名称长度(如果提供) + if (ruleData.name !== undefined) { + const trimmedName = ruleData.name.trim(); + if (trimmedName.length === 0 || trimmedName.length > 100) { + return { error: '评查点名称长度必须在1-100个字符之间', status: 400 }; + } + } + + // 3. 验证编码格式和唯一性(如果提供) + if (ruleData.code !== undefined) { + const trimmedCode = ruleData.code.trim(); + + // 验证编码格式 + if (!/^[a-zA-Z0-9-_]+$/.test(trimmedCode)) { + return { error: '评查点编码只能包含字母、数字、连字符和下划线', status: 400 }; + } + + // 验证编码唯一性(排除自身) + const existingRulesResponse = await getRulesList({ + keyword: trimmedCode, + pageSize: 10, + token + }); + + if (existingRulesResponse.data && existingRulesResponse.data.rules.length > 0) { + // 精确匹配检查,排除当前评查点自身 + const exactMatch = existingRulesResponse.data.rules.some(r => r.code === trimmedCode && r.id !== id); + if (exactMatch) { + return { error: '评查点编码已被其他评查点使用', status: 409 }; + } + } + } + + // 4. 验证分组ID有效性(如果提供) + if (ruleData.groupId !== undefined) { + const groupResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number; name: string; pid: number}> }>('evaluation_point_groups', { + filter: { 'id': `eq.${ruleData.groupId}` }, + select: 'id,name,pid', + token + }); + + let groupExists = false; + if (groupResponse.data && 'code' in groupResponse.data && groupResponse.data.data) { + groupExists = Array.isArray(groupResponse.data.data) && groupResponse.data.data.length > 0; + } else if (Array.isArray(groupResponse.data)) { + groupExists = groupResponse.data.length > 0; + } + + if (!groupExists) { + return { error: '所选规则组不存在', status: 404 }; + } + } + // 构建API接受的更新数据 const apiRuleData: Record = {}; - + if (ruleData.code !== undefined) { - apiRuleData.code = ruleData.code; + apiRuleData.code = ruleData.code.trim(); } - + if (ruleData.name !== undefined) { - apiRuleData.name = ruleData.name; + apiRuleData.name = ruleData.name.trim(); } - + if (ruleData.groupId !== undefined) { apiRuleData.evaluation_point_groups_id = parseInt(ruleData.groupId); } - + if (ruleData.priority !== undefined) { apiRuleData.risk = ruleData.priority === 'high' ? '高' : ruleData.priority === 'medium' ? '中' : '低'; } - + if (ruleData.description !== undefined) { apiRuleData.description = ruleData.description; } - + if (ruleData.isActive !== undefined) { apiRuleData.is_enabled = ruleData.isActive; } - + // 使用postgrestPut更新评查点 const response = await postgrestPut<{code: number; msg: string; data: ApiRule}, typeof apiRuleData>(`evaluation_points/${id}`, apiRuleData, undefined, token); - + // 检查是否有错误响应 if (response.error) { return { error: response.error, status: response.status }; } - + // 确保响应数据存在且符合预期格式 if (!response.data || !response.data.data) { return { error: '接口返回数据格式不正确', status: 500 }; } - + // 将API返回的数据映射到前端模型 const rule = mapApiRuleToFrontendModel(response.data.data); - + return { data: rule }; } catch (error) { console.error('更新评查点出错:', error); - return { + return { error: error instanceof Error ? error.message : '更新评查点失败', status: 500 };