feat(evaluation): 模块2.2 - 增强评查点创建/更新接口验证逻辑

功能变更:
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 <noreply@anthropic.com>
This commit is contained in:
2025-11-25 12:24:32 +08:00
parent aaa4046c41
commit 371846c5ad
+138 -24
View File
@@ -509,14 +509,69 @@ export async function getRule(id: string, token?: string): Promise<{data: Rule;
*/
export async function createRule(ruleData: Omit<Rule, 'id' | 'createdAt' | 'updatedAt'>, 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<Rule, 'id' | 'createdAt' | 'upda
post_action: "none",
action_config: ""
};
// 使用postgrestPost创建评查点
const response = await postgrestPost<{code: number; msg: string; data: ApiRule}, typeof apiRuleData>('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<Rule, 'id' | 'createdAt' | 'upda
*/
export async function updateRule(id: string, ruleData: Partial<Omit<Rule, 'id' | 'createdAt' | 'updatedAt'>>, 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<string, unknown> = {};
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
};