feat(evaluation): 将评查点 CRUD 操作从 PostgREST 迁移到 FastAPI

完成评查点管理的完整 API 迁移:
-  createEvaluationPoint: POST /api/v3/evaluation-points
-  updateEvaluationPoint: PUT /api/v3/evaluation-points/{id}
-  getEvaluationPoint: GET /api/v3/evaluation-points/{id}
-  deleteRule: DELETE /api/v3/evaluation-points/{id}
-  getRulesList: GET /api/v3/evaluation-points (带查询参数)

主要变更:
1. 将所有 postgrest* 函数调用替换为 apiRequest (axios-client)
2. 从 PostgREST 查询参数格式迁移到 REST API 路径格式
3. 简化响应处理逻辑 (FastAPI 返回清晰的 JSON)
4. 修复类型定义 (ApiRule 接口字段可选化)
5. 移除复杂的 PostgREST 嵌套查询逻辑

技术细节:
- 删除函数返回类型改为 {success: boolean, message: string}
- getRulesList 从 226 行简化到 122 行
- 所有接口统一使用 JWT Bearer 认证
- 响应格式:{data: T[], total: number, page: number, page_size: number}

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-25 17:39:02 +08:00
parent a0611a3a13
commit 4b9d9868c2
+144 -375
View File
@@ -1,4 +1,5 @@
import { postgrestGet, postgrestPost, postgrestPut, postgrestDelete, type PostgrestParams } from '../postgrest-client';
import { apiRequest } from '../axios-client';
import { formatDate } from '../../utils';
/**
@@ -57,6 +58,7 @@ export interface ApiRule {
name: string;
area?: string; // 地区
evaluation_point_groups_id: number | null;
evaluation_point_groups_pid?: number | null; // 一级分组ID (评查点类型)
risk: string;
description: string;
is_enabled: boolean;
@@ -73,8 +75,9 @@ export interface ApiRule {
id: number;
name: string;
} | null;
references_laws: Record<string, unknown>;
extraction_config: {
// 以下字段仅在详情接口中返回,列表接口可能不包含
references_laws?: Record<string, unknown>;
extraction_config?: {
type: string;
fields: string[];
prompt_setting?: {
@@ -82,7 +85,7 @@ export interface ApiRule {
template: string;
};
};
evaluation_config: {
evaluation_config?: {
rules: Array<{
id: string;
type: string;
@@ -90,12 +93,12 @@ export interface ApiRule {
}>;
logicType: string;
};
pass_message: string;
fail_message: string;
suggestion_message: string;
suggestion_message_type: string;
post_action: string;
action_config: string;
pass_message?: string;
fail_message?: string;
suggestion_message?: string;
suggestion_message_type?: string;
post_action?: string;
action_config?: string;
created_at: string;
updated_at: string;
}
@@ -182,7 +185,6 @@ function mapApiRuleToFrontendModel(apiRule: ApiRule): Rule {
*/
export async function getRulesList(params: RulesQueryParams): Promise<{data: RulesListResponse; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 解构并设置默认值
const {
page = 1,
pageSize = 10,
@@ -192,196 +194,111 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul
isActive,
keyword,
area,
orderBy = 'created_at',
orderDirection = 'desc',
userRole,
token
} = params;
// 🔑 如果没有传递 userRole,尝试从 localStorage 中获取
let user_role = ''
if (!userRole && typeof window !== 'undefined' && window.localStorage) {
let user_role = userRole || '';
if (!user_role && typeof window !== 'undefined' && window.localStorage) {
try {
const userInfoStr = localStorage.getItem('user_info');
if (userInfoStr) {
const userInfo = JSON.parse(userInfoStr);
user_role = userInfo.user_role || userInfo.userRole;
// console.log('📋 [getRulesList] 从 localStorage 获取用户角色:', userRole);
user_role = userInfo.user_role || userInfo.userRole || '';
}
} catch (error) {
console.error('❌ [getRulesList] 解析 localStorage 用户信息失败:', error);
}
}
// 构建PostgrestParams参数
const postgrestParams: PostgrestParams = {
// 🆕 使用 PostgREST 双连接查询(直接连接父子分组)
// child_group: 通过 evaluation_point_groups_id 获取子分组(所属规则组)
// parent_group: 通过 evaluation_point_groups_pid 直接获取父分组(评查点类型)
// ⚠️ 重要:使用 .replace() 移除换行符,PostgREST 的 select 参数不支持多行字符串
select: `
id,
code,
name,
area,
evaluation_point_groups_id,
evaluation_point_groups_pid,
risk,
description,
is_enabled,
created_at,
updated_at,
child_group:evaluation_point_groups!fk_evaluation_points_group(id,name),
parent_group:evaluation_point_groups!fk_evaluation_points_parent_group(id,name)
`.replace(/\s+/g, ' ').trim(),
// 设置分页
limit: pageSize,
offset: (page - 1) * pageSize,
// 设置排序
order: `${orderBy}.${orderDirection}`,
// 🆕 调用后端 FastAPI 接口: GET /api/v3/evaluation-points
// 构建查询参数
const queryParams = new URLSearchParams();
queryParams.append('page', page.toString());
queryParams.append('page_size', pageSize.toString());
// 构建过滤条件
filter: {},
// 添加一级分组ID(评查点类型)
if (ruleType) {
queryParams.append('evaluation_point_groups_pid', ruleType);
}
// 添加额外头部,用于获取总记录数
headers: {
'Prefer': 'count=exact'
},
token
};
// 添加精确匹配过滤:规则组ID
// 添加二级分组ID(规则组)
if (groupId) {
postgrestParams.filter!['evaluation_point_groups_id'] = `eq.${groupId}`;
queryParams.append('evaluation_point_groups_id', groupId);
}
// 添加风险等级筛选
if (risk) {
postgrestParams.filter!['risk'] = `eq.${risk}`;
queryParams.append('risk', risk);
}
// 添加启用状态筛选
if (isActive !== undefined) {
postgrestParams.filter!['is_enabled'] = `eq.${isActive}`;
queryParams.append('is_enabled', isActive.toString());
}
// 🔑 添加地区过滤
if (user_role == 'provincial_admin') {
postgrestParams.filter!['area'] = `eq.省级`;
}else{
postgrestParams.filter!['area'] = `eq.${area}`;
if (user_role === 'provincial_admin') {
queryParams.append('area', '省级');
} else if (area) {
queryParams.append('area', area);
}
// 如果指定了评查点类型ID,需要先查询该类型下的所有规则组ID
if (ruleType) {
try {
// 🔑 检查是否为多个类型(逗号分隔)
const isMultipleTypes = ruleType.includes(',');
// 先获取该类型(或多个类型)下的所有规则组
const groupsParams: PostgrestParams = {
select: 'id',
filter: {
// 如果是多个类型,使用 in.(type1,type2),否则使用 eq.type
'pid': isMultipleTypes ? `in.(${ruleType})` : `eq.${ruleType}`
},
token
};
const groupsResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number}>}>('evaluation_point_groups', groupsParams);
if (groupsResponse.error) {
console.error('获取规则组列表失败:', groupsResponse.error);
} else {
let groupIds: number[] = [];
// 处理不同API响应格式
if (groupsResponse.data && 'code' in groupsResponse.data && groupsResponse.data.data) {
if (Array.isArray(groupsResponse.data.data) && groupsResponse.data.data.length > 0) {
groupIds = groupsResponse.data.data.map(group => group.id);
}
} else if (Array.isArray(groupsResponse.data) && groupsResponse.data.length > 0) {
groupIds = groupsResponse.data.map(group => group.id);
}
// 使用in过滤条件,如果找到规则组
if (groupIds.length > 0) {
postgrestParams.filter!['evaluation_point_groups_id'] = `in.(${groupIds.join(',')})`;
}
if (groupId) {
postgrestParams.filter!['evaluation_point_groups_id'] = `eq.${groupId}`;
}
}
} catch (error) {
console.error('获取规则组ID出错:', error);
// 错误不中断流程,继续使用其他筛选条件
}
}
// 添加模糊搜索
// 添加关键词搜索(后端会同时搜索 name 和 code)
if (keyword) {
// 使用PostgREST的or条件查询
// 同时搜索name和code字段
postgrestParams.or = [
{ name: `ilike.*${keyword}*` },
{ code: `ilike.*${keyword}*` }
];
queryParams.append('name', keyword);
queryParams.append('code', keyword);
}
// 使用postgrestGet发送请求
const response = await postgrestGet<{code: number; msg: string; data: ApiRule[]}>('evaluation_points', postgrestParams);
// 检查是否有错误响应
// 调用 FastAPI 接口
const response = await apiRequest<{
data: EvaluationPointData[];
total: number;
page: number;
page_size: number;
}>(
`/api/v3/evaluation-points?${queryParams.toString()}`,
{
method: 'GET',
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
}
);
if (response.error) {
return { error: response.error, status: response.status };
}
// 处理不同的API响应格式
let apiRules: ApiRule[] = [];
let totalCount = 0;
// 9000端口格式 {code: number, msg: string, data: ApiRule[]}
if (response.data && 'code' in response.data && response.data.data) {
if (Array.isArray(response.data.data)) {
apiRules = response.data.data;
} else {
return { error: '接口返回数据格式不正确', status: 500 };
}
}
// 3000端口格式 ApiRule[]
else if (Array.isArray(response.data)) {
apiRules = response.data;
}
// 不支持的格式
else {
if (!response.data || !Array.isArray(response.data.data)) {
return { error: '接口返回数据格式不正确', status: 500 };
}
// 尝试从响应中获取总数
let rangeHeader = '';
// 安全地检查头信息是否存在
if (response && 'headers' in response && response.headers && typeof response.headers === 'object') {
rangeHeader = (response.headers as Record<string, string>)['content-range'] || '';
}
if (rangeHeader) {
// 例如 Content-Range: 0-9/42 表示总共有 42 条记录
const total = rangeHeader.split('/')[1];
if (total !== '*') { // '*' 表示未知总数
totalCount = parseInt(total, 10);
}
} else {
// 如果没有响应头,则使用当前返回的数据长度作为默认值
totalCount = apiRules.length;
}
// 🆕 使用嵌套查询后,不再需要手动查询分组信息
// PostgREST 的嵌套 select 已经自动关联了分组数据
console.log('📋 [getRulesList] 使用 PostgREST 嵌套查询获取评查点数据');
// 将API返回的数据映射到前端模型
const mappedRules = apiRules.map(apiRule => {
console.log('✅ [getRulesList] 成功获取评查点列表,共', response.data.total, '条');
// 🆕 将 FastAPI 返回的 EvaluationPointData 映射到前端的 Rule 模型
// 注意:FastAPI 已经包含了分组信息,不需要再额外查询
const mappedRules: Rule[] = response.data.data.map((point: EvaluationPointData) => {
// 将 EvaluationPointData 转换为 ApiRule 格式(临时兼容旧的映射函数)
const apiRule: ApiRule = {
id: point.id!,
code: point.code,
name: point.name,
area: point.area,
evaluation_point_groups_id: point.evaluation_point_groups_id,
evaluation_point_groups_pid: point.evaluation_point_groups_pid,
risk: point.risk,
description: point.description || '',
is_enabled: point.is_enabled,
created_at: point.created_at!,
updated_at: point.updated_at!,
// FastAPI 返回的数据已包含分组信息(如果后端实现了关联查询)
// 如果没有,这里可以设置为 null,前端会显示 ID
child_group: null,
parent_group: null
};
const rule = mapApiRuleToFrontendModel(apiRule);
// 格式化日期字段
@@ -390,17 +307,16 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul
return rule;
});
// 返回结果
return {
data: {
rules: mappedRules,
totalCount
totalCount: response.data.total
}
};
} catch (error) {
console.error('获取评查点列表出错:', error);
return {
console.error('获取评查点列表出错:', error);
return {
error: error instanceof Error ? error.message : '获取评查点列表失败',
status: 500
};
@@ -769,145 +685,33 @@ export async function updateRule(id: string, ruleData: Partial<Omit<Rule, 'id' |
* @param token JWT token (可选)
* @returns 删除结果
*/
export async function deleteRule(id: string, token?: string): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> {
export async function deleteRule(id: string, token?: string): Promise<{data: {success: boolean; message: string}; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 1. 验证评查点是否存在
const existingRuleResponse = await getRule(id, token);
if (existingRuleResponse.error || !existingRuleResponse.data) {
return { error: '评查点不存在', status: 404 };
}
// 2. 检查是否有关联的评查结果
// 注意:这里假设存在 evaluation_results 表,如果不存在可以跳过此检查
try {
const resultsParams: PostgrestParams = {
select: 'id',
filter: {
'evaluation_point_id': `eq.${id}`
},
limit: 1, // 只需要知道是否存在,不需要全部
token
};
const resultsResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number}>}>('evaluation_results', resultsParams);
let hasResults = false;
if (resultsResponse.data && 'code' in resultsResponse.data && resultsResponse.data.data) {
hasResults = Array.isArray(resultsResponse.data.data) && resultsResponse.data.data.length > 0;
} else if (Array.isArray(resultsResponse.data)) {
hasResults = resultsResponse.data.length > 0;
// 调用后端 FastAPI 接口: DELETE /api/v3/evaluation-points/{id}
// 后端会处理所有验证逻辑(检查是否存在、是否有关联数据等)
const response = await apiRequest<{success: boolean; message: string}>(
`/api/v3/evaluation-points/${id}`,
{
method: 'DELETE',
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
}
);
if (hasResults) {
return {
error: '该评查点已被使用,无法删除。如需停用,请使用禁用功能。',
status: 400
};
}
} catch (error) {
// 如果 evaluation_results 表不存在,忽略此错误并继续删除
console.warn('检查评查结果关联时出错(表可能不存在):', error);
}
// 3. 使用 PostgREST 语法,通过查询参数指定要删除的行
const postgrestParams: PostgrestParams = {
filter: {
'id': `eq.${id}`
},
headers: {
'Prefer': 'return=representation' // 请求返回被删除的记录
},
token
};
// 使用postgrestDelete删除评查点
const response = await postgrestDelete('evaluation_points', postgrestParams);
// console.log('删除请求响应:', JSON.stringify(response, null, 2));
// 检查是否有错误响应
if (response.error) {
console.error('删除评查点API返回错误:', response.error);
return { error: response.error, status: response.status };
}
// 确保响应数据存在
if (!response.data) {
console.error('API响应缺少数据字段');
return { error: 'API返回数据为空', status: 500 };
}
// 创建一个模拟的成功删除结果
const createMockSuccessRule = (): Rule => {
return {
id: id,
code: '',
name: '',
ruleType: '',
groupId: '',
groupName: '',
priority: 'medium',
description: '',
isActive: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
};
// 处理9000端口响应格式
if (typeof response.data === 'object' && response.data !== null && 'code' in response.data) {
const apiResponse = response.data as {code: number; msg: string; data?: ApiRule | ApiRule[]};
// 检查响应的code - 如果code为0则表示操作成功
if (apiResponse.code === 0) {
// 如果data不存在或是空数组,返回模拟数据
if (!apiResponse.data || (Array.isArray(apiResponse.data) && apiResponse.data.length === 0)) {
return { data: createMockSuccessRule() };
}
// 处理存在的data
let apiRule: ApiRule;
if (Array.isArray(apiResponse.data)) {
apiRule = apiResponse.data[0];
} else {
apiRule = apiResponse.data;
}
// 将API返回的数据映射到前端模型
const rule = mapApiRuleToFrontendModel(apiRule);
return { data: rule };
}
// 如果code不为0,则返回错误信息
return {
error: apiResponse.msg || '删除失败,服务器返回错误',
status: 500
};
}
// 处理3000端口响应格式 (直接返回数据)
else {
// 处理数组响应
if (Array.isArray(response.data)) {
if (response.data.length === 0) {
// 空数组表示成功但没有返回数据
return { data: createMockSuccessRule() };
} else {
// 返回数组中的第一个元素
const apiRule = response.data[0] as ApiRule;
const rule = mapApiRuleToFrontendModel(apiRule);
return { data: rule };
}
}
// 处理单一对象响应
else {
const apiRule = response.data as ApiRule;
const rule = mapApiRuleToFrontendModel(apiRule);
return { data: rule };
}
if (response.data && response.data.success) {
console.log('✅ deleteRule 成功:', response.data.message);
return { data: response.data };
}
return { error: '删除评查点失败', status: 500 };
} catch (error) {
console.error('删除评查点出错:', error);
return {
console.error('删除评查点出错:', error);
return {
error: error instanceof Error ? error.message : '删除评查点失败',
status: 500
};
@@ -1306,7 +1110,7 @@ export function convertApiRuleToFormData(apiRule: ApiRule): FormattedEvaluationP
risk: apiRule.risk,
is_enabled: apiRule.is_enabled,
description: apiRule.description,
references_laws: apiRule.references_laws,
references_laws: apiRule.references_laws || null,
evaluation_point_groups_pid: apiRule.evaluation_point_groups?.first_name ? null : null,
evaluation_point_groups_id: apiRule.evaluation_point_groups_id,
extraction_config: extractFields(),
@@ -2029,39 +1833,30 @@ export async function createEvaluationPoint(
): Promise<{data: EvaluationPointData; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 调用后端 FastAPI 接口: POST /api/v3/evaluation-points
const response = await postgrestPost<EvaluationPointData>('evaluation_points', evaluationPointData, token);
const response = await apiRequest<EvaluationPointData>(
'/api/v3/evaluation-points',
{
method: 'POST',
body: JSON.stringify(evaluationPointData),
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
}
);
if (response.error) {
return { error: response.error, status: response.status };
}
// 调试日志:查看实际返回的数据格式
console.log('createEvaluationPoint 响应数据:', {
data: response.data,
isArray: Array.isArray(response.data),
type: typeof response.data,
length: Array.isArray(response.data) ? response.data.length : 'N/A'
});
if (response.data) {
// 如果是数组且有数据,返回第一个元素
if (Array.isArray(response.data) && response.data.length > 0) {
return { data: response.data[0] };
}
// 如果是数组但为空,PostgREST 可能没有配置正确的 Prefer 头部
else if (Array.isArray(response.data) && response.data.length === 0) {
console.error('PostgREST 返回空数组,无法获取新创建的评查点数据');
return { error: '创建成功但无法获取创建的数据,请刷新页面', status: 500 };
}
// 如果不是数组,直接返回
else if (!Array.isArray(response.data)) {
return { data: response.data };
}
console.log('✅ createEvaluationPoint 成功:', response.data);
return { data: response.data };
}
return { error: '创建评查点失败:返回数据格式不正确', status: 500 };
} catch (error) {
console.error('创建完整评查点出错:', error);
console.error('创建评查点出错:', error);
return {
error: error instanceof Error ? error.message : '创建评查点失败',
status: 500
@@ -2083,49 +1878,30 @@ export async function updateEvaluationPoint(
): Promise<{data: EvaluationPointData; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 调用后端 FastAPI 接口: PUT /api/v3/evaluation-points/{id}
const response = await postgrestPut<EvaluationPointData, Partial<Omit<EvaluationPointData, 'created_at' | 'updated_at'>>>(
'evaluation_points',
evaluationPointData,
{ id: parseInt(id) },
token
const response = await apiRequest<EvaluationPointData>(
`/api/v3/evaluation-points/${id}`,
{
method: 'PUT',
body: JSON.stringify(evaluationPointData),
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
}
);
if (response.error) {
return { error: response.error, status: response.status };
}
// 调试日志:查看实际返回的数据格式
console.log('updateEvaluationPoint 响应数据:', {
data: response.data,
isArray: Array.isArray(response.data),
type: typeof response.data,
length: Array.isArray(response.data) ? response.data.length : 'N/A'
});
if (response.data) {
// 如果是数组且有数据,返回第一个元素
if (Array.isArray(response.data) && response.data.length > 0) {
return { data: response.data[0] };
}
// 如果是数组但为空,说明可能是 PostgREST 配置问题,但更新应该成功了
else if (Array.isArray(response.data) && response.data.length === 0) {
console.warn('PostgREST 返回空数组,可能需要检查 Prefer 头部设置');
// 尝试重新获取数据
const refetchResponse = await getEvaluationPoint(id, token);
if (refetchResponse.data) {
return { data: refetchResponse.data };
}
return { error: '更新成功但无法获取更新后的数据', status: 500 };
}
// 如果不是数组,直接返回
else if (!Array.isArray(response.data)) {
return { data: response.data };
}
console.log('✅ updateEvaluationPoint 成功:', response.data);
return { data: response.data };
}
return { error: '更新评查点失败:返回数据格式不正确', status: 500 };
} catch (error) {
console.error('更新完整评查点出错:', error);
console.error('更新评查点出错:', error);
return {
error: error instanceof Error ? error.message : '更新评查点失败',
status: 500
@@ -2144,36 +1920,29 @@ export async function getEvaluationPoint(
token?: string
): Promise<{data: EvaluationPointData; error?: never} | {data?: never; error: string; status?: number}> {
try {
const postgrestParams: PostgrestParams = {
filter: { 'id': `eq.${id}` },
select: '*',
token
};
const response = await postgrestGet<EvaluationPointData | EvaluationPointData[]>('evaluation_points', postgrestParams);
// 调用后端 FastAPI 接口: GET /api/v3/evaluation-points/{id}
const response = await apiRequest<EvaluationPointData>(
`/api/v3/evaluation-points/${id}`,
{
method: 'GET',
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
}
);
if (response.error) {
return { error: response.error, status: response.status };
}
// 处理响应数据
let evaluationPoint: EvaluationPointData | null = null;
if (response.data) {
if (Array.isArray(response.data)) {
evaluationPoint = response.data.length > 0 ? response.data[0] : null;
} else {
evaluationPoint = response.data;
}
}
if (!evaluationPoint) {
if (!response.data) {
return { error: '评查点不存在', status: 404 };
}
return { data: evaluationPoint };
console.log('✅ getEvaluationPoint 成功:', response.data);
return { data: response.data };
} catch (error) {
console.error('获取完整评查点出错:', error);
console.error('获取评查点出错:', error);
return {
error: error instanceof Error ? error.message : '获取评查点失败',
status: 500