Files
leaudit-platform-frontend/app/api/evaluation_points/rule-groups.ts
T
TanWenyan e7646d17a6 fix(evaluation-groups): 修复一级分组显示错误和 React key 警告
## 修复内容

### 1. 修复一级分组过滤问题
- **问题**: getEvaluationPointGroups 函数忽略了 pid 参数,导致返回所有分组(包括二级分组)
- **修复**: 添加 pid 参数处理逻辑,支持传递 "null" 字符串来查询一级分组
- **文件**: app/api/evaluation_points/rule-groups.ts:1186-1198

### 2. 修复 React key 重复警告
- **问题**: 父分组和子分组可能有相同的 ID,导致 "Encountered two children with the same key" 警告
- **修复**: 将 rowKey 从简单的 "id" 改为根据 isParent 生成唯一 key
- **文件**: app/routes/rule-groups._index.tsx:817

### 3. 新增后端 API 规范文档
- **文件**: docs/evaluation/evaluation_point_groups_backend_api_spec.md
- **内容**:
  - 完整的 9 个 FastAPI v3 接口规范
  - Python Pydantic 模型定义
  - TypeScript 接口定义
  - pid 参数处理说明(字符串 "null" 转换为 None)
  - 10 个完整测试用例
  - 数据库表结构建议

## 技术细节

**pid 参数处理**:
```typescript
// 前端发送
GET /api/v3/evaluation-point-groups?pid=null&page=1

// 后端需要识别字符串 "null" 并转换为 None/NULL
if (pid == "null") {
  query = query.filter(EvaluationPointGroup.pid.is_(None))
}
```

**唯一 key 生成**:
```typescript
rowKey={(record) => record.isParent ? `parent-${record.id}` : `child-${record.id}`}
```

🔗 相关文档: docs/evaluation/evaluation_point_groups_backend_api_spec.md
2025-11-26 10:05:39 +08:00

1644 lines
49 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { postgrestGet, postgrestPost, postgrestPut, postgrestDelete, type PostgrestParams } from '../postgrest-client';
import { apiRequest } from '../axios-client';
import { formatDate } from '../../utils';
/**
* 评查点分组接口
*/
export interface RuleGroup {
id: string;
pid: string;
name: string;
code?: string; // 添加分组编码字段
is_enabled: boolean;
ruleCount?: number; // 评查点数量
children?: RuleGroup[]; // 子分组
createdAt?: string; // 添加创建时间字段
description?: string; // 描述
}
// API请求模型
export interface ApiRuleGroup {
id?: number;
pid: number | null; // 允许 null,表示一级分组
name: string;
code?: string;
description?: string;
is_enabled: boolean;
created_at?: string;
updated_at?: string;
}
// 创建或更新分组请求参数
export interface RuleGroupCreateUpdateDto {
name: string;
code: string;
pid: string | null; // 父分组ID,如果是一级分组则为null或'0'
description?: string;
is_enabled: boolean;
}
// 用于替换代码中的 any 类型
interface ApiResponse<T> {
code: number;
msg: string;
data: T;
}
/**
* 从不同格式的 API 响应中提取数据
* @param responseData API 响应数据
* @returns 提取后的数据或 null
*/
function extractApiData<T>(responseData: unknown): T | null {
if (!responseData) return null;
// 格式1: { code: number, msg: string, data: T }
if (typeof responseData === 'object' && responseData !== null &&
'code' in responseData &&
'data' in responseData &&
(responseData as { data: unknown }).data) {
return (responseData as { data: T }).data;
}
// 格式2: 直接是数据对象
return responseData as T;
}
/**
* 评查点分组查询参数
*/
export interface RuleGroupQueryParams {
// 分页参数
page?: number;
pageSize?: number;
// 筛选参数
name?: string; // 名称模糊搜索
code?: string; // 编码模糊搜索
is_enabled?: boolean; // 启用状态
pid?: string | null; // 父级ID (null表示一级分组, 具体ID表示查询该父级的子分组)
// 排序参数
orderBy?: 'created_at' | 'updated_at' | 'name' | 'code';
order?: 'asc' | 'desc';
token?: string;
}
/**
* 获取评查点分组列表(支持分页、筛选、排序)
* @deprecated 使用 getEvaluationPointGroups 代替(FastAPI v3
* @param params 查询参数
* @returns 评查点分组列表和总数
*/
export async function getRuleGroups_legacy(
params?: RuleGroupQueryParams
): Promise<{data: RuleGroup[]; totalCount?: number; error?: never} | {data?: never; error: string; status?: number}> {
try {
const {
page = 1,
pageSize = 50,
name,
code,
is_enabled,
pid = '0', // 默认获取一级分组
orderBy = 'created_at',
order = 'desc',
token
} = params || {};
// 构建筛选条件
const filter: Record<string, string> = {};
// 父级ID筛选 (pid=null或'0'表示一级分组)
if (pid === null || pid === '0') {
filter['pid'] = 'eq.0';
} else if (pid) {
filter['pid'] = `eq.${pid}`;
}
// 名称模糊搜索
if (name) {
filter['name'] = `ilike.*${name}*`;
}
// 编码模糊搜索
if (code) {
filter['code'] = `ilike.*${code}*`;
}
// 状态筛选
if (is_enabled !== undefined) {
filter['is_enabled'] = `eq.${is_enabled}`;
}
const postgrestParams: PostgrestParams = {
select: `
id,
pid,
name,
code,
description,
is_enabled,
created_at
`,
filter,
order: `${orderBy}.${order}`, // PostgREST order format: field.direction
limit: pageSize,
offset: (page - 1) * pageSize,
token
};
const response = await postgrestGet<{code: number; msg: string; data: Array<{
id: number;
pid: number;
name: string;
code?: string;
description?: string;
is_enabled: boolean;
created_at?: string;
}>}>('evaluation_point_groups', postgrestParams);
if (response.error) {
return { error: response.error, status: response.status };
}
// 处理响应数据
let groups: RuleGroup[] = [];
if (response.data && 'code' in response.data && response.data.data) {
groups = response.data.data.map(group => ({
id: group.id.toString(),
pid: group.pid.toString(),
name: group.name,
code: group.code,
description: group.description,
is_enabled: group.is_enabled,
createdAt: group.created_at ? formatDate(group.created_at) : undefined
}));
} else if (Array.isArray(response.data)) {
groups = response.data.map(group => ({
id: group.id.toString(),
pid: group.pid.toString(),
name: group.name,
code: group.code,
description: group.description,
is_enabled: group.is_enabled,
createdAt: group.created_at ? formatDate(group.created_at) : undefined
}));
}
// 注意:由于当前 PostgREST 客户端不支持 count 参数,totalCount 返回当前页的记录数
// 后续可优化为单独查询获取准确的总数
return {
data: groups,
totalCount: groups.length
};
} catch (error) {
console.error('获取评查点分组列表失败:', error);
return {
error: error instanceof Error ? error.message : '获取评查点分组列表失败',
status: 500
};
}
}
/**
* 获取指定分组的子分组(包含评查点数量统计)
* 🆕 已更新为使用 FastAPI v3 接口
* @param parentId 父分组ID
* @param token JWT token (可选)
* @returns 子分组列表
*/
export async function getChildGroups(parentId: string, token?: string): Promise<{data: RuleGroup[]; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 🆕 使用 FastAPI v3 的 getEvaluationPointGroupChildren 获取子分组
const response = await getEvaluationPointGroupChildren(
parentId,
{
pageSize: 1000, // 设置较大的页面大小以获取所有子分组
},
token
);
if (response.error) {
return { error: response.error, status: response.status };
}
// 🆕 FastAPI v3 接口已经返回了 rule_count(已转换为 ruleCount),无需手动统计
return { data: response.data || [] };
} catch (error) {
console.error('获取子分组列表出错:', error);
return {
error: error instanceof Error ? error.message : '获取子分组列表失败',
status: 500
};
}
}
/**
* 获取所有评查点分组(包括一级和二级)
* @deprecated 使用 getAllEvaluationPointGroups 代替(FastAPI v3
* @param token JWT token (可选)
* @returns 完整的评查点分组列表
*/
export async function getAllRuleGroups_legacy(token?: string): Promise<{data: RuleGroup[]; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 1. 获取所有分组
const allGroupsParams: PostgrestParams = {
select: `
id,
pid,
name,
is_enabled
`,
token
};
const allGroupsResponse = await postgrestGet<{code: number; msg: string; data: Array<{
id: number;
pid: number;
name: string;
is_enabled: boolean;
}>}>('evaluation_point_groups', allGroupsParams);
if (allGroupsResponse.error) {
return { error: allGroupsResponse.error, status: allGroupsResponse.status };
}
// 2. 处理响应数据
let allGroups: RuleGroup[] = [];
if (allGroupsResponse.data && 'code' in allGroupsResponse.data && allGroupsResponse.data.data) {
allGroups = allGroupsResponse.data.data.map(group => ({
id: group.id.toString(),
pid: group.pid.toString(),
name: group.name,
is_enabled: group.is_enabled,
children: []
}));
} else if (Array.isArray(allGroupsResponse.data)) {
allGroups = allGroupsResponse.data.map(group => ({
id: group.id.toString(),
pid: group.pid.toString(),
name: group.name,
is_enabled: group.is_enabled,
children: []
}));
}
// 3. 构建树形结构(pid为NULL表示顶级分组)
const parentGroups = allGroups.filter(group => !group.pid || group.pid === '0' || group.pid === null);
// 4. 为每个父分组添加子分组
for (const parent of parentGroups) {
parent.children = allGroups.filter(group => group.pid === parent.id);
// 5. 获取每个子分组的评查点数量
for (const child of parent.children) {
const ruleCountParams: PostgrestParams = {
select: 'id',
filter: {
'evaluation_point_groups_id': `eq.${child.id}`
},
token
};
const ruleCountResponse = await postgrestGet<ApiResponse<Array<{id: number}>>>('evaluation_points', ruleCountParams);
child.ruleCount = ruleCountResponse.data && 'code' in ruleCountResponse.data
? (ruleCountResponse.data.data && Array.isArray(ruleCountResponse.data.data) ? ruleCountResponse.data.data.length : 0)
: (Array.isArray(ruleCountResponse.data) ? (ruleCountResponse.data as unknown[]).length : 0)
}
}
return { data: parentGroups };
} catch (error) {
console.error('获取所有评查点分组出错:', error);
return {
error: error instanceof Error ? error.message : '获取所有评查点分组失败',
status: 500
};
}
}
/**
* 获取单个评查点分组详情(包含评查点数量统计)
* @deprecated 使用 getEvaluationPointGroup 代替(FastAPI v3
* @param id 分组ID
* @param token JWT token (可选)
* @returns 分组详情
*/
export async function getRuleGroup_legacy(id: string, token?: string): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> {
try {
if (!id) {
return { error: '分组ID不能为空', status: 400 };
}
const params: PostgrestParams = {
select: `
id,
pid,
name,
code,
description,
is_enabled,
created_at
`,
filter: {
'id': `eq.${id}`
},
token
};
const response = await postgrestGet<{code: number; msg: string; data: Array<{
id: number;
pid: number;
name: string;
code?: string;
description?: string;
is_enabled: boolean;
created_at?: string;
}>}>('evaluation_point_groups', params);
if (response.error) {
return { error: response.error, status: response.status };
}
let group: RuleGroup | null = null;
if (response.data && 'code' in response.data && response.data.data && response.data.data.length > 0) {
const apiGroup = response.data.data[0];
group = {
id: apiGroup.id.toString(),
pid: apiGroup.pid.toString(),
name: apiGroup.name,
code: apiGroup.code,
description: apiGroup.description,
is_enabled: apiGroup.is_enabled,
createdAt: apiGroup.created_at ? formatDate(apiGroup.created_at) : undefined
};
} else if (Array.isArray(response.data) && response.data.length > 0) {
const apiGroup = response.data[0];
group = {
id: apiGroup.id.toString(),
pid: apiGroup.pid.toString(),
name: apiGroup.name,
code: apiGroup.code,
description: apiGroup.description,
is_enabled: apiGroup.is_enabled,
createdAt: apiGroup.created_at ? formatDate(apiGroup.created_at) : undefined
};
}
if (!group) {
return { error: '未找到指定分组', status: 404 };
}
// 获取该分组下的评查点数量(一级分组和二级分组都统计)
const ruleCountParams: PostgrestParams = {
select: 'id',
filter: {
'evaluation_point_groups_id': `eq.${group.id}`
},
token
};
const ruleCountResponse = await postgrestGet<ApiResponse<Array<{id: number}>>>(
'evaluation_points',
ruleCountParams
);
// 计算评查点数量
let ruleCount = 0;
if (ruleCountResponse.error) {
// 查询失败,使用默认值 0
ruleCount = 0;
} else if (ruleCountResponse.data) {
// 处理包装格式的响应
if ('code' in ruleCountResponse.data && 'data' in ruleCountResponse.data) {
const wrappedData = ruleCountResponse.data as {code: number; data: Array<{id: number}>};
ruleCount = Array.isArray(wrappedData.data) ? wrappedData.data.length : 0;
}
// 处理直接数组格式的响应
else if (Array.isArray(ruleCountResponse.data)) {
ruleCount = (ruleCountResponse.data as Array<{id: number}>).length;
}
}
group.ruleCount = ruleCount;
return { data: group };
} catch (error) {
console.error('获取评查点分组详情失败:', error);
return {
error: error instanceof Error ? error.message : '获取评查点分组详情失败',
status: 500
};
}
}
/**
* 创建评查点分组(增强版 - 包含完整验证)
* @deprecated 使用 createEvaluationPointGroup 代替(FastAPI v3
* @param groupData 分组数据
* @param token JWT token (可选)
* @returns 创建的分组
*/
export async function createRuleGroup_legacy(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 };
}
// 验证名称长度
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;
if (!groupData.pid || groupData.pid === '0') {
// 一级分组(顶级分组)
pidValue = null;
} else {
// 二级分组 - 验证父级ID
pidValue = Number(groupData.pid);
if (isNaN(pidValue)) {
return { error: '父分组ID必须是有效的数字', status: 400 };
}
// 验证父级分组是否存在
const parentGroupResponse = await getRuleGroup(groupData.pid, token);
if (parentGroupResponse.error || !parentGroupResponse.data) {
return { error: '父分组不存在或无法访问', status: 404 };
}
// 验证父级分组本身不是二级分组(不允许三级分组)
const parentGroup = parentGroupResponse.data;
if (parentGroup.pid && parentGroup.pid !== '0') {
return { error: '不允许创建三级分组,父级分组必须是一级分组', status: 400 };
}
}
// ========== 4. 构建并发送请求 ==========
const apiGroup: ApiRuleGroup = {
pid: pidValue,
name: trimmedName,
code: trimmedCode,
description: groupData.description?.trim() || '',
is_enabled: groupData.is_enabled !== undefined ? groupData.is_enabled : true
};
const response = await postgrestPost<ApiResponse<ApiRuleGroup> | ApiRuleGroup, ApiRuleGroup>(
'evaluation_point_groups',
apiGroup,
token
);
if (response.error) {
// 处理数据库约束错误
if (response.error.includes('evaluation_point_groups_code_key')) {
return { error: '分组编码已存在(数据库约束)', status: 409 };
}
return { error: response.error, status: response.status };
}
// ========== 5. 处理响应数据 ==========
const apiResponse = extractApiData<ApiRuleGroup>(response.data);
if (!apiResponse || !apiResponse.id) {
return { error: '创建成功但未返回分组ID', status: 500 };
}
// 构建返回对象
const createdGroup: RuleGroup = {
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,
createdAt: apiResponse.created_at ? formatDate(apiResponse.created_at) : undefined
};
return { data: createdGroup };
} catch (error) {
console.error('创建评查点分组失败:', error);
return {
error: error instanceof Error ? error.message : '创建评查点分组失败',
status: 500
};
}
}
/**
* 更新评查点分组(增强版 - 包含完整验证,不允许修改 pid)
* @deprecated 使用 updateEvaluationPointGroup 代替(FastAPI v3
* @param id 分组ID
* @param data 更新的分组数据
* @param token JWT token (可选)
* @returns 更新后的分组
*/
export async function updateRuleGroup_legacy(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: trimmedName,
code: trimmedCode,
description: data.description?.trim() || '',
is_enabled: data.is_enabled !== undefined ? data.is_enabled : true
};
// 注意:不包含 pid 字段,防止误修改
const response = await postgrestPut<ApiResponse<RuleGroup> | RuleGroup, Partial<ApiRuleGroup>>(
'evaluation_point_groups',
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: '更新成功但未返回数据', status: 500 };
}
return { data: extractedData };
} catch (error) {
console.error('更新评查点分组失败:', error);
return {
error: error instanceof Error ? error.message : '更新评查点分组失败',
status: 500
};
}
}
/**
* 删除评查点分组(增强版 - 安全的阻止删除策略)
* @deprecated 使用 deleteEvaluationPointGroup 代替(FastAPI v3
*
* 删除策略:
* - 如果分组下有子分组,拒绝删除,提示用户先删除子分组
* - 如果分组下有评查点,拒绝删除,提示用户先删除或移动评查点
* - 只有空分组才能被删除
*
* @param id 分组ID
* @param token JWT token (可选)
* @returns 删除结果
*/
export async function deleteRuleGroup_legacy(id: string, token?: string): Promise<{success: boolean; error?: string; details?: { hasChildren: boolean; hasPoints: boolean; childCount?: number; pointCount?: number }}> {
try {
// ========== 1. ID验证 ==========
if (!id) {
return { success: false, error: '分组ID不能为空' };
}
// 验证分组是否存在
const groupResponse = await getRuleGroup(id, token);
if (groupResponse.error || !groupResponse.data) {
return { success: false, error: '分组不存在或无法访问' };
}
const group = groupResponse.data;
// ========== 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}`
};
}
const childGroups = childGroupsResponse.data || [];
childCount = childGroups.length;
hasChildren = childCount > 0;
if (hasChildren) {
return {
success: false,
error: `该分组下存在 ${childCount} 个子分组,请先删除所有子分组后再删除此分组。`,
details: {
hasChildren: true,
hasPoints: false,
childCount
}
};
}
}
// ========== 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}`
};
}
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}`
},
token
});
if (response.error) {
return { success: false, error: `删除失败: ${response.error}` };
}
return {
success: true,
details: {
hasChildren: false,
hasPoints: false
}
};
} catch (error) {
console.error('删除评查点分组失败:', error);
return {
success: false,
error: error instanceof Error ? error.message : '删除评查点分组失败'
};
}
}
/**
* 删除子分组及其相关数据(级联删除)
*
* @deprecated 当前采用阻止删除策略,此函数暂不使用
* @param id 子分组ID
* @param token JWT token (可选)
* @returns 删除结果
*/
async function deleteChildGroup(id: string, token?: string): Promise<{success: boolean; error?: string}> {
try {
// 1. 删除子分组下的所有评查点
const deletePointsResult = await deleteEvaluationPointsByGroupId(id, token);
if (!deletePointsResult.success) {
return deletePointsResult;
}
// 2. 删除子分组本身
const response = await postgrestDelete<ApiResponse<{id: number}>>('evaluation_point_groups', {
filter: {
'id': `eq.${id}`
},
token
});
if (response.error) {
return { success: false, error: response.error };
}
return { success: true };
} catch (error) {
console.error('删除子分组失败:', error);
return {
success: false,
error: error instanceof Error ? error.message : '删除子分组失败'
};
}
}
/**
* 删除指定分组下的所有评查点(级联删除)
*
* @deprecated 当前采用阻止删除策略,此函数暂不使用
* @param groupId 分组ID
* @param token JWT token (可选)
* @returns 删除结果
*/
async function deleteEvaluationPointsByGroupId(groupId: string, token?: string): Promise<{success: boolean; error?: string}> {
try {
const response = await postgrestDelete<ApiResponse<{id: number}>>('evaluation_points', {
filter: {
'evaluation_point_groups_id': `eq.${groupId}`
},
token
});
if (response.error) {
return { success: false, error: response.error };
}
return { success: true };
} catch (error) {
console.error('删除评查点失败:', error);
return {
success: false,
error: error instanceof Error ? error.message : '删除评查点失败'
};
}
}
// ==================== 批量操作接口 ====================
/**
* 批量更新分组状态(启用/禁用)
* @deprecated 使用 batchUpdateEvaluationPointGroupStatus 代替(FastAPI v3
* @param ids 分组ID列表
* @param is_enabled 目标状态
* @param token JWT token (可选)
* @returns 更新结果
*/
export async function batchUpdateRuleGroupStatus_legacy(
ids: string[],
is_enabled: boolean,
token?: string
): Promise<{
success: boolean;
updated_count: number;
failed_ids: string[];
errors?: Array<{ id: string; error: string }>;
}> {
try {
// ========== 1. 参数验证 ==========
if (!Array.isArray(ids) || ids.length === 0) {
return {
success: false,
updated_count: 0,
failed_ids: [],
errors: [{ id: 'validation', error: 'ID列表不能为空' }]
};
}
// 验证每个ID的有效性
const invalidIds = ids.filter(id => !id || id.trim() === '');
if (invalidIds.length > 0) {
return {
success: false,
updated_count: 0,
failed_ids: ids,
errors: [{ id: 'validation', error: '存在无效的分组ID' }]
};
}
// ========== 2. 逐个更新(确保每个分组都能被正确处理) ==========
const failedIds: string[] = [];
const errors: Array<{ id: string; error: string }> = [];
let updatedCount = 0;
for (const id of ids) {
try {
// 验证分组是否存在
const groupResponse = await getRuleGroup(id, token);
if (groupResponse.error || !groupResponse.data) {
failedIds.push(id);
errors.push({ id, error: '分组不存在或无法访问' });
continue;
}
// 执行更新
const updateResponse = await postgrestPut<ApiResponse<RuleGroup> | RuleGroup, Partial<ApiRuleGroup>>(
'evaluation_point_groups',
{ is_enabled },
{ id },
token
);
if (updateResponse.error) {
failedIds.push(id);
errors.push({ id, error: updateResponse.error });
} else {
updatedCount++;
}
} catch (error) {
failedIds.push(id);
errors.push({
id,
error: error instanceof Error ? error.message : '更新失败'
});
}
}
// ========== 3. 返回结果 ==========
return {
success: failedIds.length === 0,
updated_count: updatedCount,
failed_ids: failedIds,
errors: errors.length > 0 ? errors : undefined
};
} catch (error) {
console.error('批量更新分组状态失败:', error);
return {
success: false,
updated_count: 0,
failed_ids: ids,
errors: [{
id: 'batch',
error: error instanceof Error ? error.message : '批量更新失败'
}]
};
}
}
/**
* 批量删除分组(安全的阻止删除策略)
* @deprecated 使用 batchDeleteEvaluationPointGroups 代替(FastAPI v3
* @param ids 分组ID列表
* @param token JWT token (可选)
* @returns 删除结果
*/
export async function batchDeleteRuleGroups_legacy(
ids: string[],
token?: string
): Promise<{
success: boolean;
deleted_count: number;
failed_ids: string[];
errors?: Array<{ id: string; error: string; details?: { hasChildren?: boolean; hasPoints?: boolean } }>;
}> {
try {
// ========== 1. 参数验证 ==========
if (!Array.isArray(ids) || ids.length === 0) {
return {
success: false,
deleted_count: 0,
failed_ids: [],
errors: [{ id: 'validation', error: 'ID列表不能为空' }]
};
}
// 验证每个ID的有效性
const invalidIds = ids.filter(id => !id || id.trim() === '');
if (invalidIds.length > 0) {
return {
success: false,
deleted_count: 0,
failed_ids: ids,
errors: [{ id: 'validation', error: '存在无效的分组ID' }]
};
}
// ========== 2. 逐个删除(使用安全的阻止删除策略) ==========
const failedIds: string[] = [];
const errors: Array<{ id: string; error: string; details?: { hasChildren?: boolean; hasPoints?: boolean } }> = [];
let deletedCount = 0;
for (const id of ids) {
try {
const deleteResult = await deleteRuleGroup(id, token);
if (!deleteResult.success) {
failedIds.push(id);
errors.push({
id,
error: deleteResult.error || '删除失败',
details: deleteResult.details ? {
hasChildren: deleteResult.details.hasChildren,
hasPoints: deleteResult.details.hasPoints
} : undefined
});
} else {
deletedCount++;
}
} catch (error) {
failedIds.push(id);
errors.push({
id,
error: error instanceof Error ? error.message : '删除失败'
});
}
}
// ========== 3. 返回结果 ==========
return {
success: failedIds.length === 0,
deleted_count: deletedCount,
failed_ids: failedIds,
errors: errors.length > 0 ? errors : undefined
};
} catch (error) {
console.error('批量删除分组失败:', error);
return {
success: false,
deleted_count: 0,
failed_ids: ids,
errors: [{
id: 'batch',
error: error instanceof Error ? error.message : '批量删除失败'
}]
};
}
}
// ========================================
// FastAPI v3 接口函数(新版)
// ========================================
/**
* FastAPI v3 响应数据格式
*/
interface EvaluationPointGroupResponse {
id: number;
pid: number | null;
name: string;
code: string;
description: string | null;
is_enabled: boolean;
created_at: string;
updated_at: string;
rule_count?: number | null;
children?: EvaluationPointGroupResponse[] | null;
}
interface EvaluationPointGroupListResponse {
data: EvaluationPointGroupResponse[];
total: number;
page: number;
page_size: number;
}
/**
* 转换 API 响应数据为前端格式
*/
function convertApiGroupToRuleGroup(apiGroup: EvaluationPointGroupResponse): RuleGroup {
return {
id: String(apiGroup.id),
pid: apiGroup.pid !== null ? String(apiGroup.pid) : '0',
name: apiGroup.name,
code: apiGroup.code,
description: apiGroup.description || undefined,
is_enabled: apiGroup.is_enabled,
createdAt: apiGroup.created_at,
ruleCount: apiGroup.rule_count !== null && apiGroup.rule_count !== undefined ? apiGroup.rule_count : undefined,
children: apiGroup.children ? apiGroup.children.map(convertApiGroupToRuleGroup) : undefined
};
}
/**
* 1. 获取一级分组列表(FastAPI v3)
* @param params 查询参数
* @param token JWT token
* @returns 一级分组列表
*/
export async function getEvaluationPointGroups(
params?: RuleGroupQueryParams,
token?: string
): Promise<{data: RuleGroup[]; totalCount?: number; error?: never} | {data?: never; error: string; status?: number}> {
try {
const {
page = 1,
pageSize = 20,
name,
code,
is_enabled,
pid,
orderBy = 'created_at',
order = 'desc'
} = params || {};
// 构建查询参数
const queryParams = new URLSearchParams();
queryParams.append('page', String(page));
queryParams.append('page_size', String(pageSize));
if (name) queryParams.append('name', name);
if (code) queryParams.append('code', code);
if (is_enabled !== undefined) queryParams.append('is_enabled', String(is_enabled));
// 🔑 添加 pid 参数过滤
// pid=null 或 pid='0' 表示只查询一级分组,后端需要识别字符串 "null"
// 如果 pid 未定义,则不传该参数(默认查询所有分组)
if (pid !== undefined) {
if (pid === null || pid === '0') {
// 方案1:传递字符串 "null",后端需要识别并转换为 None/NULL
queryParams.append('pid', 'null');
// 方案2:不传参数,后端默认查询一级分组(需要后端支持)
// 不添加 pid 参数
} else {
queryParams.append('pid', String(pid));
}
}
const url = `/api/v3/evaluation-point-groups?${queryParams.toString()}`;
const response = await apiRequest<EvaluationPointGroupListResponse>(url, {
method: 'GET',
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
});
if (response.error) {
return { error: response.error, status: response.status };
}
if (response.data) {
const ruleGroups = response.data.data.map(convertApiGroupToRuleGroup);
return {
data: ruleGroups,
totalCount: response.data.total
};
}
return { error: '获取一级分组列表失败:返回数据格式不正确', status: 500 };
} catch (error) {
console.error('❌ 获取一级分组列表出错:', error);
return {
error: error instanceof Error ? error.message : '获取一级分组列表失败',
status: 500
};
}
}
/**
* 2. 获取所有分组(树形结构)(FastAPI v3)
* @param includeDisabled 是否包含禁用的分组
* @param withRuleCount 是否返回评查点数量
* @param token JWT token
* @returns 树形分组结构
*/
export async function getAllEvaluationPointGroups(
includeDisabled: boolean = false,
withRuleCount: boolean = true,
token?: string
): Promise<{data: RuleGroup[]; error?: never} | {data?: never; error: string; status?: number}> {
try {
const queryParams = new URLSearchParams();
queryParams.append('include_disabled', String(includeDisabled));
queryParams.append('with_rule_count', String(withRuleCount));
const url = `/api/v3/evaluation-point-groups/all?${queryParams.toString()}`;
const response = await apiRequest<EvaluationPointGroupListResponse>(url, {
method: 'GET',
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
});
if (response.error) {
return { error: response.error, status: response.status };
}
if (response.data) {
const ruleGroups = response.data.data.map(convertApiGroupToRuleGroup);
return { data: ruleGroups };
}
return { error: '获取分组树形结构失败:返回数据格式不正确', status: 500 };
} catch (error) {
console.error('❌ 获取分组树形结构出错:', error);
return {
error: error instanceof Error ? error.message : '获取分组树形结构失败',
status: 500
};
}
}
/**
* 3. 获取单个分组详情(FastAPI v3)
* @param id 分组ID
* @param withRuleCount 是否返回评查点数量
* @param token JWT token
* @returns 分组详情
*/
export async function getEvaluationPointGroup(
id: string,
withRuleCount: boolean = true,
token?: string
): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> {
try {
const queryParams = new URLSearchParams();
queryParams.append('with_rule_count', String(withRuleCount));
const url = `/api/v3/evaluation-point-groups/${id}?${queryParams.toString()}`;
const response = await apiRequest<EvaluationPointGroupResponse>(url, {
method: 'GET',
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
});
if (response.error) {
return { error: response.error, status: response.status };
}
if (response.data) {
const ruleGroup = convertApiGroupToRuleGroup(response.data);
return { data: ruleGroup };
}
return { error: '获取分组详情失败:返回数据格式不正确', status: 500 };
} catch (error) {
console.error('❌ 获取分组详情出错:', error);
return {
error: error instanceof Error ? error.message : '获取分组详情失败',
status: 500
};
}
}
/**
* 4. 获取子分组列表(FastAPI v3
* @param parentId 父分组ID
* @param params 查询参数
* @param token JWT token
* @returns 子分组列表
*/
export async function getEvaluationPointGroupChildren(
parentId: string,
params?: { page?: number; pageSize?: number; is_enabled?: boolean },
token?: string
): Promise<{data: RuleGroup[]; totalCount?: number; error?: never} | {data?: never; error: string; status?: number}> {
try {
const {
page = 1,
pageSize = 20,
is_enabled
} = params || {};
const queryParams = new URLSearchParams();
queryParams.append('page', String(page));
queryParams.append('page_size', String(pageSize));
if (is_enabled !== undefined) queryParams.append('is_enabled', String(is_enabled));
const url = `/api/v3/evaluation-point-groups/${parentId}/children?${queryParams.toString()}`;
const response = await apiRequest<EvaluationPointGroupListResponse>(url, {
method: 'GET',
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
});
if (response.error) {
return { error: response.error, status: response.status };
}
if (response.data) {
const ruleGroups = response.data.data.map(convertApiGroupToRuleGroup);
return {
data: ruleGroups,
totalCount: response.data.total
};
}
return { error: '获取子分组列表失败:返回数据格式不正确', status: 500 };
} catch (error) {
console.error('❌ 获取子分组列表出错:', error);
return {
error: error instanceof Error ? error.message : '获取子分组列表失败',
status: 500
};
}
}
/**
* 5. 创建评查点分组(FastAPI v3
* @param groupData 分组数据
* @param token JWT token
* @returns 创建后的分组数据
*/
export async function createEvaluationPointGroup(
groupData: RuleGroupCreateUpdateDto,
token?: string
): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 转换为 API 格式
const apiGroup = {
name: groupData.name,
code: groupData.code,
pid: groupData.pid === null || groupData.pid === '0' ? null : Number(groupData.pid),
description: groupData.description || null,
is_enabled: groupData.is_enabled !== undefined ? groupData.is_enabled : true
};
const response = await apiRequest<EvaluationPointGroupResponse>(
'/api/v3/evaluation-point-groups',
{
method: 'POST',
body: JSON.stringify(apiGroup),
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
}
);
if (response.error) {
return { error: response.error, status: response.status };
}
if (response.data) {
const ruleGroup = convertApiGroupToRuleGroup(response.data);
console.log('✅ createEvaluationPointGroup 成功:', ruleGroup);
return { data: ruleGroup };
}
return { error: '创建分组失败:返回数据格式不正确', status: 500 };
} catch (error) {
console.error('❌ 创建分组出错:', error);
return {
error: error instanceof Error ? error.message : '创建分组失败',
status: 500
};
}
}
/**
* 6. 更新评查点分组(FastAPI v3
* @param id 分组ID
* @param groupData 更新的分组数据(部分更新)
* @param token JWT token
* @returns 更新后的分组数据
*/
export async function updateEvaluationPointGroup(
id: string,
groupData: Partial<RuleGroupCreateUpdateDto>,
token?: string
): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 转换为 API 格式
const apiGroup: Partial<{
name: string;
code: string;
pid: number | null;
description: string | null;
is_enabled: boolean;
}> = {};
if (groupData.name !== undefined) apiGroup.name = groupData.name;
if (groupData.code !== undefined) apiGroup.code = groupData.code;
if (groupData.pid !== undefined) {
apiGroup.pid = groupData.pid === null || groupData.pid === '0' ? null : Number(groupData.pid);
}
if (groupData.description !== undefined) apiGroup.description = groupData.description || null;
if (groupData.is_enabled !== undefined) apiGroup.is_enabled = groupData.is_enabled;
const response = await apiRequest<EvaluationPointGroupResponse>(
`/api/v3/evaluation-point-groups/${id}`,
{
method: 'PUT',
body: JSON.stringify(apiGroup),
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
}
);
if (response.error) {
return { error: response.error, status: response.status };
}
if (response.data) {
const ruleGroup = convertApiGroupToRuleGroup(response.data);
console.log('✅ updateEvaluationPointGroup 成功:', ruleGroup);
return { data: ruleGroup };
}
return { error: '更新分组失败:返回数据格式不正确', status: 500 };
} catch (error) {
console.error('❌ 更新分组出错:', error);
return {
error: error instanceof Error ? error.message : '更新分组失败',
status: 500
};
}
}
/**
* 7. 删除评查点分组(FastAPI v3
* @param id 分组ID
* @param token JWT token
* @returns 删除结果
*/
export async function deleteEvaluationPointGroup(
id: string,
token?: string
): Promise<{success: boolean; message?: string; deleted_groups?: number; deleted_points?: number; error?: never} | {success: false; error: string; status?: number}> {
try {
const response = await apiRequest<{
success: boolean;
message: string;
deleted_groups: number;
deleted_points: number;
}>(
`/api/v3/evaluation-point-groups/${id}`,
{
method: 'DELETE',
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
}
);
if (response.error) {
return { success: false, error: response.error, status: response.status };
}
if (response.data) {
console.log('✅ deleteEvaluationPointGroup 成功:', response.data);
return {
success: response.data.success,
message: response.data.message,
deleted_groups: response.data.deleted_groups,
deleted_points: response.data.deleted_points
};
}
return { success: false, error: '删除分组失败:返回数据格式不正确', status: 500 };
} catch (error) {
console.error('❌ 删除分组出错:', error);
return {
success: false,
error: error instanceof Error ? error.message : '删除分组失败',
status: 500
};
}
}
/**
* 8. 批量更新分组启用状态(FastAPI v3)
* @param ids 分组ID列表
* @param is_enabled 启用状态
* @param token JWT token
* @returns 批量更新结果
*/
export async function batchUpdateEvaluationPointGroupStatus(
ids: string[],
is_enabled: boolean,
token?: string
): Promise<{success: boolean; updated_count?: number; message?: string; error?: never} | {success: false; error: string; status?: number}> {
try {
const requestBody = {
ids: ids.map(id => Number(id)),
is_enabled
};
const response = await apiRequest<{
success: boolean;
updated_count: number;
message: string;
}>(
'/api/v3/evaluation-point-groups/batch/status',
{
method: 'PATCH',
body: JSON.stringify(requestBody),
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
}
);
if (response.error) {
return { success: false, error: response.error, status: response.status };
}
if (response.data) {
console.log('✅ batchUpdateEvaluationPointGroupStatus 成功:', response.data);
return {
success: response.data.success,
updated_count: response.data.updated_count,
message: response.data.message
};
}
return { success: false, error: '批量更新状态失败:返回数据格式不正确', status: 500 };
} catch (error) {
console.error('❌ 批量更新状态出错:', error);
return {
success: false,
error: error instanceof Error ? error.message : '批量更新状态失败',
status: 500
};
}
}
/**
* 9. 批量删除分组(FastAPI v3
* @param ids 分组ID列表
* @param token JWT token
* @returns 批量删除结果
*/
export async function batchDeleteEvaluationPointGroups(
ids: string[],
token?: string
): Promise<{success: boolean; deleted_groups?: number; deleted_points?: number; message?: string; error?: never} | {success: false; error: string; status?: number}> {
try {
const requestBody = {
ids: ids.map(id => Number(id))
};
const response = await apiRequest<{
success: boolean;
deleted_groups: number;
deleted_points: number;
message: string;
}>(
'/api/v3/evaluation-point-groups/batch',
{
method: 'DELETE',
body: JSON.stringify(requestBody),
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
}
);
if (response.error) {
return { success: false, error: response.error, status: response.status };
}
if (response.data) {
console.log('✅ batchDeleteEvaluationPointGroups 成功:', response.data);
return {
success: response.data.success,
deleted_groups: response.data.deleted_groups,
deleted_points: response.data.deleted_points,
message: response.data.message
};
}
return { success: false, error: '批量删除分组失败:返回数据格式不正确', status: 500 };
} catch (error) {
console.error('❌ 批量删除分组出错:', error);
return {
success: false,
error: error instanceof Error ? error.message : '批量删除分组失败',
status: 500
};
}
}