feat(evaluation): 模块1.2 - 增强评查点分组创建/更新接口验证
## 主要改进 ### 1. 增强 createRuleGroup 函数 - ✅ 名称长度验证(1-100字符) - ✅ 编码格式验证(只允许字母、数字、连字符、下划线) - ✅ 编码长度验证(1-50字符) - ✅ 编码唯一性验证(查询数据库确保不重复) - ✅ 父级ID存在性验证(二级分组必须有有效的父级) - ✅ 三级分组防护(不允许在二级分组下创建子分组) - ✅ 数据库约束错误友好提示 ### 2. 增强 updateRuleGroup 函数 - ✅ ID有效性验证(检查分组是否存在) - ✅ 名称长度验证(1-100字符) - ✅ 编码格式验证(只允许字母、数字、连字符、下划线) - ✅ 编码长度验证(1-50字符) - ✅ 编码唯一性验证(排除自身) - ✅ **禁止修改pid**(防止分组层级混乱) - ✅ 数据库约束错误友好提示 - ✅ 提供清晰的错误消息 ### 3. 类型安全性改进 - ✅ 修复所有 TypeScript 类型错误 - ✅ 添加类型守卫防止 undefined 访问 - ✅ 确保所有返回值类型正确 ## 验证规则 ### 分组名称 - 必填,不能为空 - 长度:1-100字符 - 自动去除首尾空格 ### 分组编码 - 必填,不能为空 - 长度:1-50字符 - 格式:只允许字母、数字、连字符(-)、下划线(_) - 必须全局唯一 - 自动去除首尾空格 ### 父级ID - 一级分组:pid = null 或 '0' - 二级分组:pid = 有效的父级分组ID - 不允许三级分组 - **更新时不允许修改pid** ## 相关文件 - app/api/evaluation_points/rule-groups.ts ## 验收清单 - [x] TypeScript 类型检查通过 - [x] 完整的字段验证 - [x] 编码唯一性验证 - [x] 父级ID验证 - [x] 禁止修改pid - [x] 友好的错误提示 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -474,75 +474,126 @@ export async function getRuleGroup(id: string, token?: string): Promise<{data: R
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建评查点分组
|
||||
* 创建评查点分组(增强版 - 包含完整验证)
|
||||
* @param groupData 分组数据
|
||||
* @param token JWT token (可选)
|
||||
* @returns 创建的分组
|
||||
*/
|
||||
export async function createRuleGroup(groupData: RuleGroupCreateUpdateDto, token?: string): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> {
|
||||
try {
|
||||
// ========== 1. 基本字段验证 ==========
|
||||
|
||||
// 验证必填字段
|
||||
if (!groupData.name || !groupData.code) {
|
||||
return { error: '分组名称和编码不能为空', status: 400 };
|
||||
}
|
||||
|
||||
// 🆕 确保 pid 是合法值(NULL表示顶级分组)
|
||||
// 验证名称长度
|
||||
const trimmedName = groupData.name.trim();
|
||||
if (trimmedName.length === 0) {
|
||||
return { error: '分组名称不能为空', status: 400 };
|
||||
}
|
||||
if (trimmedName.length > 100) {
|
||||
return { error: '分组名称不能超过100个字符', status: 400 };
|
||||
}
|
||||
|
||||
// 验证编码格式(只允许字母、数字、连字符、下划线)
|
||||
const trimmedCode = groupData.code.trim();
|
||||
if (trimmedCode.length === 0) {
|
||||
return { error: '分组编码不能为空', status: 400 };
|
||||
}
|
||||
if (!/^[a-zA-Z0-9-_]+$/.test(trimmedCode)) {
|
||||
return { error: '分组编码只能包含字母、数字、连字符和下划线', status: 400 };
|
||||
}
|
||||
if (trimmedCode.length > 50) {
|
||||
return { error: '分组编码不能超过50个字符', status: 400 };
|
||||
}
|
||||
|
||||
// ========== 2. 编码唯一性验证 ==========
|
||||
|
||||
const existingGroupsResponse = await getRuleGroups({
|
||||
code: trimmedCode,
|
||||
pageSize: 1,
|
||||
token
|
||||
});
|
||||
|
||||
if (existingGroupsResponse.error) {
|
||||
return {
|
||||
error: `编码唯一性检查失败: ${existingGroupsResponse.error}`,
|
||||
status: existingGroupsResponse.status || 500
|
||||
};
|
||||
}
|
||||
|
||||
if (existingGroupsResponse.data && existingGroupsResponse.data.length > 0) {
|
||||
return { error: '分组编码已存在,请使用其他编码', status: 409 };
|
||||
}
|
||||
|
||||
// ========== 3. 父级ID验证 ==========
|
||||
|
||||
let pidValue: number | null;
|
||||
try {
|
||||
if (!groupData.pid || groupData.pid === '0') {
|
||||
pidValue = null; // 顶级分组
|
||||
// 一级分组(顶级分组)
|
||||
pidValue = null;
|
||||
} else {
|
||||
// 二级分组 - 验证父级ID
|
||||
pidValue = Number(groupData.pid);
|
||||
if (isNaN(pidValue)) {
|
||||
return { error: '父分组ID必须是有效的数字', status: 400 };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('父分组ID转换失败:', error);
|
||||
return { error: '父分组ID格式错误', status: 400 };
|
||||
|
||||
// 验证父级分组是否存在
|
||||
const parentGroupResponse = await getRuleGroup(groupData.pid, token);
|
||||
if (parentGroupResponse.error || !parentGroupResponse.data) {
|
||||
return { error: '父分组不存在或无法访问', status: 404 };
|
||||
}
|
||||
|
||||
// 构建API请求数据 - 确保字段类型符合数据库要求
|
||||
// 验证父级分组本身不是二级分组(不允许三级分组)
|
||||
const parentGroup = parentGroupResponse.data;
|
||||
if (parentGroup.pid && parentGroup.pid !== '0') {
|
||||
return { error: '不允许创建三级分组,父级分组必须是一级分组', status: 400 };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 4. 构建并发送请求 ==========
|
||||
|
||||
const apiGroup: ApiRuleGroup = {
|
||||
pid: pidValue,
|
||||
name: groupData.name.trim(),
|
||||
code: groupData.code.trim(),
|
||||
description: groupData.description || '',
|
||||
is_enabled: groupData.is_enabled
|
||||
name: trimmedName,
|
||||
code: trimmedCode,
|
||||
description: groupData.description?.trim() || '',
|
||||
is_enabled: groupData.is_enabled !== undefined ? groupData.is_enabled : true
|
||||
};
|
||||
|
||||
// console.log('创建评查点分组请求数据:', JSON.stringify(apiGroup, null, 2));
|
||||
|
||||
// 直接发送到 PostgreSQL 表
|
||||
const response = await postgrestPost<ApiResponse<ApiRuleGroup> | ApiRuleGroup, ApiRuleGroup>(
|
||||
'evaluation_point_groups', // 表名
|
||||
'evaluation_point_groups',
|
||||
apiGroup,
|
||||
token
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
console.error('创建评查点分组API返回错误:', response.error, '状态码:', response.status);
|
||||
// 处理数据库约束错误
|
||||
if (response.error.includes('evaluation_point_groups_code_key')) {
|
||||
return { error: '分组编码已存在(数据库约束)', status: 409 };
|
||||
}
|
||||
return { error: response.error, status: response.status };
|
||||
}
|
||||
|
||||
// console.log('创建评查点分组响应数据:', JSON.stringify(response.data, null, 2));
|
||||
// ========== 5. 处理响应数据 ==========
|
||||
|
||||
// 处理响应数据 - 适配不同的API响应格式
|
||||
const apiResponse = extractApiData<ApiRuleGroup>(response.data);
|
||||
|
||||
if (!apiResponse) {
|
||||
console.error('创建分组成功但返回数据格式异常:', response.data);
|
||||
return { error: '创建分组失败,返回数据格式错误', status: 500 };
|
||||
if (!apiResponse || !apiResponse.id) {
|
||||
return { error: '创建成功但未返回分组ID', status: 500 };
|
||||
}
|
||||
|
||||
// 构建返回对象
|
||||
const createdGroup: RuleGroup = {
|
||||
id: apiResponse.id?.toString() || '',
|
||||
pid: apiResponse.pid?.toString() || '', // 🆕 NULL 转换为空字符串(表示顶级分组)
|
||||
name: apiResponse.name || '',
|
||||
code: apiResponse.code?.toString() || '', // 处理可能的数字类型
|
||||
id: apiResponse.id.toString(),
|
||||
pid: apiResponse.pid !== null ? apiResponse.pid.toString() : '0',
|
||||
name: apiResponse.name,
|
||||
code: apiResponse.code || trimmedCode,
|
||||
description: apiResponse.description,
|
||||
is_enabled: apiResponse.is_enabled !== undefined ? apiResponse.is_enabled : true,
|
||||
is_enabled: apiResponse.is_enabled,
|
||||
createdAt: apiResponse.created_at ? formatDate(apiResponse.created_at) : undefined
|
||||
};
|
||||
|
||||
@@ -557,7 +608,7 @@ export async function createRuleGroup(groupData: RuleGroupCreateUpdateDto, token
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新评查点分组
|
||||
* 更新评查点分组(增强版 - 包含完整验证,不允许修改 pid)
|
||||
* @param id 分组ID
|
||||
* @param data 更新的分组数据
|
||||
* @param token JWT token (可选)
|
||||
@@ -565,57 +616,125 @@ export async function createRuleGroup(groupData: RuleGroupCreateUpdateDto, token
|
||||
*/
|
||||
export async function updateRuleGroup(id: string, data: RuleGroupCreateUpdateDto, token?: string): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> {
|
||||
try {
|
||||
// ========== 1. ID有效性验证 ==========
|
||||
|
||||
if (!id) {
|
||||
return { error: '分组ID不能为空', status: 400 };
|
||||
}
|
||||
|
||||
// 验证分组是否存在
|
||||
const existingGroupResponse = await getRuleGroup(id, token);
|
||||
if (existingGroupResponse.error || !existingGroupResponse.data) {
|
||||
return { error: '分组不存在或无法访问', status: 404 };
|
||||
}
|
||||
|
||||
const existingGroup = existingGroupResponse.data;
|
||||
|
||||
// ========== 2. 基本字段验证 ==========
|
||||
|
||||
// 验证必填字段
|
||||
if (!data.name || !data.code) {
|
||||
return { error: '分组名称和编码不能为空', status: 400 };
|
||||
}
|
||||
|
||||
// 构建符合数据库结构的对象
|
||||
// 验证名称长度
|
||||
const trimmedName = data.name.trim();
|
||||
if (trimmedName.length === 0) {
|
||||
return { error: '分组名称不能为空', status: 400 };
|
||||
}
|
||||
if (trimmedName.length > 100) {
|
||||
return { error: '分组名称不能超过100个字符', status: 400 };
|
||||
}
|
||||
|
||||
// 验证编码格式(只允许字母、数字、连字符、下划线)
|
||||
const trimmedCode = data.code.trim();
|
||||
if (trimmedCode.length === 0) {
|
||||
return { error: '分组编码不能为空', status: 400 };
|
||||
}
|
||||
if (!/^[a-zA-Z0-9-_]+$/.test(trimmedCode)) {
|
||||
return { error: '分组编码只能包含字母、数字、连字符和下划线', status: 400 };
|
||||
}
|
||||
if (trimmedCode.length > 50) {
|
||||
return { error: '分组编码不能超过50个字符', status: 400 };
|
||||
}
|
||||
|
||||
// ========== 3. 编码唯一性验证(排除自身) ==========
|
||||
|
||||
const duplicateCheckResponse = await getRuleGroups({
|
||||
code: trimmedCode,
|
||||
pageSize: 10,
|
||||
token
|
||||
});
|
||||
|
||||
if (duplicateCheckResponse.error) {
|
||||
return {
|
||||
error: `编码唯一性检查失败: ${duplicateCheckResponse.error}`,
|
||||
status: duplicateCheckResponse.status || 500
|
||||
};
|
||||
}
|
||||
|
||||
// 检查是否有其他分组使用了相同的编码
|
||||
if (duplicateCheckResponse.data && duplicateCheckResponse.data.length > 0) {
|
||||
const isDuplicate = duplicateCheckResponse.data.some(group => group.id !== id);
|
||||
if (isDuplicate) {
|
||||
return { error: '分组编码已被其他分组使用,请使用其他编码', status: 409 };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 4. 不允许修改 pid(防止分组层级混乱) ==========
|
||||
|
||||
if (data.pid !== undefined) {
|
||||
const existingPid = existingGroup.pid === '0' || !existingGroup.pid ? null : existingGroup.pid;
|
||||
const newPid = !data.pid || data.pid === '0' ? null : data.pid;
|
||||
|
||||
if (existingPid !== newPid) {
|
||||
return {
|
||||
error: '不允许修改分组的父级ID,这会导致分组层级混乱。如需调整层级,请删除后重新创建。',
|
||||
status: 400
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 5. 构建并发送请求 ==========
|
||||
|
||||
const apiGroup: Partial<ApiRuleGroup> = {
|
||||
name: data.name.trim(),
|
||||
code: data.code.trim(),
|
||||
description: data.description || '',
|
||||
is_enabled: data.is_enabled
|
||||
name: trimmedName,
|
||||
code: trimmedCode,
|
||||
description: data.description?.trim() || '',
|
||||
is_enabled: data.is_enabled !== undefined ? data.is_enabled : true
|
||||
};
|
||||
|
||||
// 🆕 如果需要更新父分组,添加 pid(NULL表示顶级分组)
|
||||
if (data.pid !== undefined) {
|
||||
let pidValue: number | null;
|
||||
if (!data.pid || data.pid === '0') {
|
||||
pidValue = null; // 顶级分组
|
||||
} else {
|
||||
pidValue = Number(data.pid);
|
||||
if (isNaN(pidValue)) {
|
||||
return { error: '父分组ID必须是有效的数字', status: 400 };
|
||||
}
|
||||
}
|
||||
apiGroup.pid = pidValue;
|
||||
}
|
||||
// 注意:不包含 pid 字段,防止误修改
|
||||
|
||||
// 使用新的filters参数
|
||||
const response = await postgrestPut<ApiResponse<RuleGroup> | RuleGroup, Partial<ApiRuleGroup>>(
|
||||
'evaluation_point_groups',
|
||||
apiGroup, // 使用转换后的对象
|
||||
apiGroup,
|
||||
{ id },
|
||||
token
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
// 处理数据库约束错误
|
||||
if (response.error.includes('evaluation_point_groups_code_key')) {
|
||||
return { error: '分组编码已存在(数据库约束)', status: 409 };
|
||||
}
|
||||
return { error: response.error, status: response.status };
|
||||
}
|
||||
|
||||
// 使用辅助函数提取数据
|
||||
// ========== 6. 处理响应数据 ==========
|
||||
|
||||
const extractedData = extractApiData<RuleGroup>(response.data);
|
||||
|
||||
if (!extractedData) {
|
||||
return { error: '更新成功但未返回数据' };
|
||||
return { error: '更新成功但未返回数据', status: 500 };
|
||||
}
|
||||
|
||||
return { data: extractedData };
|
||||
} catch (error) {
|
||||
console.error('更新评查点分组失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '更新评查点分组失败'
|
||||
error: error instanceof Error ? error.message : '更新评查点分组失败',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user