Files
leaudit-platform-frontend/docs/postgrest-date.md
T

20 KiB

🗄️ PostgreSQL数据对接规范

概述

本系统使用PostgreSQL作为主数据库,通过PostgREST提供RESTful API接口。所有数据操作统一通过封装的postgrest-client.ts模块进行,确保数据访问的一致性和可维护性。

技术架构

前端组件 (Remix Routes)
    ↓
API层 (app/api/[module]/[api].ts)
    ↓
PostgREST客户端 (postgrest-client.ts)
    ↓
PostgreSQL数据库

基础封装方法

1. 导入PostgREST客户端

import { 
  postgrestGet, 
  postgrestPost, 
  postgrestPut, 
  postgrestDelete,
  type PostgrestParams 
} from "../postgrest-client";

2. 查询操作 (GET)

// 基础查询
const params: PostgrestParams = {
  select: '*',
  filter: {
    'id': `eq.${id}`,
    'status': `eq.active`
  },
  order: 'created_at.desc',
  limit: 20,
  offset: 0
};

const response = await postgrestGet('table_name', params);

// 复杂查询示例
const complexParams: PostgrestParams = {
  select: 'id,name,status,created_at,user_id,users(name)',  // 关联查询
  filter: {
    'created_at': `gte.2023-01-01`,
    'status': `in.(active,pending)`
  },
  or: 'name.ilike.*keyword*,description.ilike.*keyword*',  // OR条件
  order: 'created_at.desc,name.asc',
  limit: 50
};

const response = await postgrestGet('documents', complexParams);

3. 创建操作 (POST)

// 单条记录创建
const newRecord = {
  name: '文档名称',
  document_number: 'DOC001',
  type_id: 1,
  user_id: 123,
  status: 'pending'
};

const response = await postgrestPost('documents', newRecord);

// 批量创建
const batchRecords = [
  { name: '文档1', type_id: 1 },
  { name: '文档2', type_id: 2 }
];

const response = await postgrestPost('documents', batchRecords);

4. 更新操作 (PUT/PATCH)

// 根据ID更新
const updateData = {
  status: 'completed',
  audit_status: 1,
  updated_at: new Date().toISOString()
};

const response = await postgrestPut(
  'documents', 
  updateData, 
  { id: documentId }
);

// 根据条件批量更新
const response = await postgrestPut(
  'evaluation_results',
  { status: 'reviewed' },
  { document_id: docId, status: 'pending' }
);

5. 删除操作 (DELETE)

// 根据ID删除
const response = await postgrestDelete('documents', {
  filter: { 'id': `eq.${id}` }
});

// 条件删除
const response = await postgrestDelete('temp_files', {
  filter: { 'created_at': `lt.2023-01-01` }
});

API层实现规范

1. 文件结构

app/api/
├── [module]/              # 功能模块
│   ├── [feature].ts       # 具体功能API
│   └── types.ts           # 类型定义 (可选)
├── postgrest-client.ts    # PostgREST客户端
├── axios-client.ts        # HTTP客户端
└── error-handler.ts       # 错误处理

2. API文件模板

// app/api/evaluation_points/reviews.ts
import { postgrestGet, type PostgrestParams, postgrestPut, postgrestPost } from "../postgrest-client";
import { getDocument } from "~/api/files/documents";
import { formatDate } from "~/utils";

/**
 * 数据提取工具函数
 * 统一处理不同格式的API响应
 */
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;
}

// 类型定义
interface EvaluationResult {
  id: string | number;
  document_id: string | number;
  evaluation_point_id: string | number;
  evaluated_results?: {
    result?: boolean;
    message?: string;
    data?: string;
    [key: string]: unknown;
  };
  [key: string]: unknown;
}

interface EvaluationPoint {
  id: string | number;
  evaluation_point_groups_id: string | number;
  suggestion_message_type?: string;
  suggestion_message?: string;
  score?: number;
  updated_at?: string;
  [key: string]: unknown;
}

// 前端使用的结果类型
interface ReviewPointResult {
  id: string | number;
  title: string;
  groupName: string;
  status: string;
  content: string;
  suggestion: string;
  result?: boolean;
  score: number;
}

/**
 * 获取评查点数据
 * @param fileId 文件ID
 * @returns 评查点结果列表和统计数据
 */
export async function getReviewPoints(fileId: string) {
  try {
    // 步骤1: 获取文档基础数据
    const documentData = await getDocument(fileId);
    if (documentData.error) {
      return { error: documentData.error, status: documentData.status || 500 };
    }

    // 步骤2: 查询评查结果
    const evaluationResultsParams: PostgrestParams = {
      select: '*',
      filter: { 'document_id': `eq.${fileId}` }
    };
    const evaluationResultsResponse = await postgrestGet('evaluation_results', evaluationResultsParams);

    if (evaluationResultsResponse.error) {
      return { error: evaluationResultsResponse.error, status: evaluationResultsResponse.status };
    }

    const evaluationResultsData = extractApiData<EvaluationResult[]>(evaluationResultsResponse.data) || [];
    
    if (evaluationResultsData.length <= 0) {
      return { 
        data: [], 
        stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 },
        error: '获取评查结果数据失败' 
      };
    }

    // 步骤3: 获取评查点详情
    const evaluationPointIds = evaluationResultsData.map(item => item.evaluation_point_id).filter(Boolean);
    
    const evaluationPointsParams: PostgrestParams = {
      select: '*',
      filter: { 'id': `in.(${evaluationPointIds.join(',')})` }
    };
    const evaluationPointsResponse = await postgrestGet('evaluation_points', evaluationPointsParams);

    if (evaluationPointsResponse.error) {
      return { error: evaluationPointsResponse.error, status: evaluationPointsResponse.status };
    }

    const evaluationPointsData = extractApiData<EvaluationPoint[]>(evaluationPointsResponse.data) || [];

    // 步骤4: 数据处理和转换
    const resultData: ReviewPointResult[] = evaluationResultsData.map(result => {
      const point = evaluationPointsData.find(p => p.id === result.evaluation_point_id);
      
      return {
        id: result.id,
        title: result.evaluated_results?.message || '',
        groupName: point?.group_name || '',
        status: point?.suggestion_message_type || '',
        content: result.evaluated_results?.data || '',
        suggestion: point?.suggestion_message || '',
        result: result.evaluated_results?.result,
        score: point?.score || 0
      };
    });

    // 步骤5: 统计数据计算
    const stats = {
      total: resultData.length,
      success: resultData.filter(item => item.result === true).length,
      warning: resultData.filter(item => item.status === 'warning').length,
      error: resultData.filter(item => item.status === 'error').length,
      score: resultData.reduce((sum, item) => sum + item.score, 0)
    };

    return { 
      data: resultData, 
      stats, 
      document: documentData.data 
    };
  } catch (error) {
    console.error('获取评查数据失败:', error);
    return { 
      error: error instanceof Error ? error.message : '获取评查数据失败',
      status: 500 
    };
  }
}

/**
 * 更新评查结果
 * @param resultId 评查结果ID
 * @param editAuditStatusId 审核状态ID
 * @param result 评查结果
 * @param message 评查意见
 */
export async function updateReviewResult(
  resultId: string, 
  editAuditStatusId: string | number, 
  result: string, 
  message: string
): Promise<{
  data?: unknown;
  error?: string;
  status?: number;
}> {
  try {
    if (!resultId) {
      return { error: '评查结果ID不能为空', status: 400 };
    }
    
    // 获取当前数据
    const currentResultResponse = await postgrestGet('evaluation_results', {
      select: '*',
      filter: { id: `eq.${resultId}` }
    });
    
    if (currentResultResponse.error) {
      return { error: currentResultResponse.error, status: currentResultResponse.status };
    }
    
    const currentResultData = extractApiData<EvaluationResult[]>(currentResultResponse.data);
    
    if (!currentResultData || currentResultData.length === 0) {
      return { error: '未找到评查结果数据', status: 404 };
    }
    
    const currentResult = currentResultData[0];
    const currentEvaluatedResults = currentResult.evaluated_results || {};
    
    // 构建更新数据
    const isReview = result === 'review';
    const updatedEvaluatedResults = {
      ...currentEvaluatedResults,
      ...(isReview ? { message } : { result: result === 'true', message }),
    };
    
    // 更新评查结果
    const resultResponse = await postgrestPut(
      'evaluation_results',
      { evaluated_results: updatedEvaluatedResults },
      { id: resultId }
    );
    
    if (resultResponse.error) {
      return { error: resultResponse.error, status: resultResponse.status };
    }
    
    // 处理审核状态
    const editAuditStatusValue = isReview ? 0 : 1;
    
    if (editAuditStatusId && editAuditStatusId !== '') {
      // 更新现有记录
      const auditStatusResponse = await postgrestPut(
        'audit_status',
        { edit_audit_status: editAuditStatusValue },
        { id: editAuditStatusId }
      );
      
      if (auditStatusResponse.error) {
        return { error: auditStatusResponse.error, status: auditStatusResponse.status };
      }
    } else {
      // 创建新记录
      const newAuditStatus = {
        document_id: currentResult.document_id,
        evaluation_point_id: currentResult.evaluation_point_id,
        evaluation_result_id: resultId,
        edit_audit_status: editAuditStatusValue
      };
      
      const postResponse = await postgrestPost('audit_status', newAuditStatus);
      
      if (postResponse.error) {
        return { error: postResponse.error, status: postResponse.status };
      }
    }
    
    return { data: extractApiData<unknown>(resultResponse.data) };
  } catch (error) {
    console.error('更新评查结果失败:', error);
    return {
      error: error instanceof Error ? error.message : '更新评查结果失败',
      status: 500
    };
  }
}

3. 错误处理规范

// 统一的错误处理格式
interface ApiResponse<T> {
  data?: T;
  error?: string;
  status?: number;
}

// 在API函数中的错误处理
export async function apiFunction(): Promise<ApiResponse<DataType>> {
  try {
    const response = await postgrestGet('table_name', params);
    
    if (response.error) {
      return { 
        error: response.error, 
        status: response.status 
      };
    }
    
    const data = extractApiData<DataType>(response.data);
    
    if (!data) {
      return { 
        error: '数据格式错误', 
        status: 500 
      };
    }
    
    return { data };
  } catch (error) {
    console.error('API调用失败:', error);
    return {
      error: error instanceof Error ? error.message : '未知错误',
      status: 500
    };
  }
}

前端路由集成规范

1. Loader函数中调用API

// app/routes/reviews.tsx
import { getReviewPoints, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";

export async function loader({ request }: LoaderFunctionArgs) {
  try {
    const url = new URL(request.url);
    const id = url.searchParams.get('id') || undefined;
    const previousRoute = url.searchParams.get('previousRoute') || '';
    
    if (!id) {
      return Response.json({ result: false, message: '文件ID不能为空' });
    }

    // 获取评查点数据
    const reviewData = await getReviewPoints(id);

    if ('error' in reviewData && reviewData.error) {
      console.error("获取评查点数据错误:", reviewData.error);
      return Response.json({ result: false, message: reviewData.error });
    }

    // 确保数据格式正确
    if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) {
      return Response.json({
        previousRoute: previousRoute,
        document: reviewData.document,
        reviewPoints: reviewData.data,
        reviewInfo: reviewData.reviewInfo,
        statistics: reviewData.stats
      });
    } else {
      console.error("返回的评查数据格式不正确", JSON.stringify(reviewData, null, 2));
      return Response.json({ result: false, message: '返回的评查数据格式不正确' });
    }
  } catch (error) {
    console.error('获取评查数据失败:', error);
    return Response.json({ result: false, message: '获取评查数据失败' });
  }
}

2. 组件中处理API响应

// 在React组件中使用
export default function ReviewDetails() {
  const loaderData = useLoaderData<typeof loader>();
  const { document, reviewPoints, statistics, reviewInfo } = loaderData;
  const [isLoading, setIsLoading] = useState(false);
  
  // 处理loader错误
  useEffect(() => {
    if (Object.keys(loaderData).find(key => key === 'result') && !loaderData.result) {
      messageService.show({
        title: '错误',
        message: loaderData.message,
        type: 'error',
        confirmText: '确定',
        onConfirm: () => {
          navigate(-1);
        }
      });
    }
  }, [loaderData, navigate]);

  // 处理状态更新
  const handleReviewPointStatusChange = async (
    reviewPointResultId: string, 
    editAuditStatusId: string | number, 
    newStatus: string, 
    message: string
  ) => {
    try {
      const response = await updateReviewResult(
        reviewPointResultId, 
        editAuditStatusId, 
        newStatus, 
        message
      );
      
      if (response.error) {
        console.error('更新评查结果失败:', response.error);
        toastService.error(`更新评查结果失败: ${response.error}`);
        return;
      }
      
      // 更新本地状态
      setReviewData(prevData => {
        // 更新逻辑...
      });
      
      toastService.success('评查点状态已更新');
    } catch (error) {
      console.error('更新评查结果出错:', error);
      toastService.error('更新评查结果失败,请稍后重试');
    }
  };
}

数据类型定义规范

1. 数据库实体类型

// 数据库表对应的接口
interface Document {
  id: number;
  user_id: number | null;
  type_id: number;
  name: string;
  document_number: string;
  path: string;
  storage_type: string;
  file_size: number;
  upload_time: string;
  is_test_document: boolean;
  evaluation_level: string;
  status: 'pass' | 'warning' | 'waiting' | 'processing' | 'fail';
  file_status: 'Waiting' | 'Cutting' | 'Extractioning' | 'Evaluationing' | 'Processed';
  audit_status: number; // -1: 不通过, 0: 待审核, 1: 通过, 2: 警告, 3: 审核中
  ocr_result?: Record<string, unknown>;
  extracted_results?: unknown;
  summary?: unknown;
  remark?: string;
  created_at: string;
  updated_at: string;
}

2. 前端UI类型

// 前端组件使用的接口
interface ReviewFileUI {
  id: string;
  status: string;
  path: string;
  fileName: string;
  fileCode: string;
  fileType: string;
  fileTypeId: number;
  fileSize: number;
  uploadTime: string;
  reviewStatus: string;
  reviewStatusCode: number;
  issueCount: number;
  score?: number;
  auditStatus: number | null;
  issues: Array<{
    severity: 'info' | 'warning' | 'error' | 'critical';
    message: string;
  }>;
  createdBy: string;
  passCount: number;
  warningCount: number;
  failCount: number;
  manualCount: number;
}

3. API参数类型

// 搜索参数类型
interface DocumentSearchParams {
  keyword?: string;
  status?: string;
  fileType?: string;
  dateRange?: [string, string];
  page?: number;
  pageSize?: number;
  sortBy?: string;
  sortOrder?: 'asc' | 'desc';
}

PostgREST客户端功能

1. 查询参数转换

// 转换通用参数为PostgREST格式
export function transformParams(params: PostgrestParams): QueryParams {
  const result: QueryParams = {};
  
  // 处理select参数
  if (params.select) {
    result.select = params.select;
  }
  
  // 处理过滤条件
  if (params.filter) {
    Object.entries(params.filter).forEach(([key, value]) => {
      if (value !== undefined) {
        result[key] = value as string | number | boolean;
      }
    });
  }
  
  // 处理排序
  if (params.order) {
    result.order = params.order;
  }
  
  // 处理分页
  if (params.limit !== undefined) {
    result.limit = params.limit;
  }
  
  if (params.offset !== undefined) {
    result.offset = params.offset;
  }
  
  // 处理OR条件
  if (params.or) {
    if (typeof params.or === 'string') {
      result.or = params.or;
    } else if (Array.isArray(params.or)) {
      const orConditions = params.or.map(condition => {
        const [field, operator] = Object.entries(condition)[0];
        return `${field}.${operator}`;
      });
      result.or = `(${orConditions.join(',')})`;
    }
  }
  
  return result;
}

2. 开发环境日志

function logPostgrestQuery(endpoint: string, params?: QueryParams, method: string = 'GET'): void {
  if (process.env.NODE_ENV !== 'production') {
    const baseUrl = 'http://nas.7bm.co:3000';
    const normalizedEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
    
    // console.log('\n📦 PostgREST 查询日志 ======================start=============');
    // console.log(`📦 HTTP 方法: ${method}`);
    // console.log(`📦 API 端点: ${decodeUrlForDisplay(`${baseUrl}/${normalizedEndpoint}`)}`);
    
    // if (params && Object.keys(params).length > 0) {
    //   console.log('📦 查询参数:');
    //   Object.entries(params).forEach(([key, value]) => {
    //     if (value !== undefined) {
    //       console.log(`  - ${key}: ${JSON.stringify(value)}`);
    //     }
    //   });
    // }
    
    // console.log('PostgREST 查询日志=============================end============\n');
  }
}

3. 数据预处理

function preprocessData(data: Record<string, unknown>): Record<string, unknown> {
  const processed: Record<string, unknown> = {};
  
  for (const [key, value] of Object.entries(data)) {
    // 处理null值
    if (value === null) {
      processed[key] = null;
      continue;
    }
    
    // 处理布尔值字符串
    if (typeof value === 'string' && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
      processed[key] = value.toLowerCase() === 'true';
    }
    // 处理ID字段
    else if ((key === 'id' || key.endsWith('_id') || key === 'pid') && value !== undefined) {
      try {
        const numValue = Number(value);
        if (!isNaN(numValue)) {
          processed[key] = numValue;
        } else {
          processed[key] = value;
        }
      } catch {
        processed[key] = value;
      }
    }
    // 其他值保持不变
    else {
      processed[key] = value;
    }
  }
  
  return processed;
}

开发最佳实践

1. API命名规范

  • 获取数据: get[EntityName]s()get[EntityName]()
  • 创建数据: create[EntityName]()
  • 更新数据: update[EntityName]()
  • 删除数据: delete[EntityName]()

2. 错误处理策略

  • 所有API函数必须返回统一的响应格式
  • 在API层处理数据库错误,前端只处理业务逻辑错误
  • 使用TypeScript严格类型检查避免运行时错误

3. 性能优化

  • 使用select参数只获取需要的字段
  • 合理使用limitoffset进行分页
  • 避免N+1查询,使用关联查询获取相关数据

4. 调试技巧

  • 开发环境自动打印查询日志
  • 使用浏览器网络面板检查实际请求
  • 通过PostgREST文档验证查询语法

数据对接检查清单

新功能开发前

  • 确认数据库表结构和关系
  • 定义TypeScript接口类型
  • 规划API函数命名和参数
  • 设计错误处理策略

API实现阶段

  • 导入必要的postgrest方法
  • 实现extractApiData数据提取函数
  • 定义所有相关的类型接口
  • 编写查询参数构建逻辑
  • 实现数据转换和处理逻辑
  • 添加完整的错误处理

前端集成阶段

  • 在loader函数中调用API
  • 处理loading和error状态
  • 实现UI状态更新逻辑
  • 添加用户反馈(toast/message)
  • 测试各种边界情况

上线前检查

  • API函数测试通过
  • 错误处理验证完成
  • 性能测试满足要求
  • 日志输出正常
  • 代码注释完整

此数据对接规范基于实际项目经验总结,将根据项目发展持续更新和完善。