Files
leaudit-platform-frontend/app/api/postgrest-client.ts
T
LiangShiyong d09d5b709d Merge branch 'PingChuan' into shiy-login
# Conflicts:
#	app/config/api-config.ts
fix: 1. 修复无法加载数据的问题:没有从入口页中进来会缺少数据。
2. 加强后端接口关于token的校验错误和权限校验错误的管理。

feat: 1. 对接后端的数据看板的接口。
2. 将系统设置单独抽出来作为管理员的固定一个入口。
2025-11-22 15:57:22 +08:00

673 lines
22 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.
// app/api/postgrest-client.ts
// import { AsyncLocalStorage } from 'async_hooks';
import { apiRequest, type QueryParams } from './axios-client';
import { handleApiError } from './error-handler';
/**
* 请求上下文接口
*/
// interface RequestContext {
// jwt?: string;
// }
/**
* 创建异步本地存储用于存储请求上下文
*/
// const requestContext = new AsyncLocalStorage<RequestContext>();
/**
* 在指定的上下文中运行函数
* @param context 上下文对象
* @param fn 要执行的函数
* @returns 函数执行结果
*/
// export function runWithContext<T>(
// context: RequestContext,
// fn: () => T | Promise<T>
// ): T | Promise<T> {
// return requestContext.run(context, fn);
// }
/**
* 获取当前上下文中的 JWT
* @returns JWT token 或 undefined
*/
// function getContextJWT(): string | undefined {
// const context = requestContext.getStore();
// return context?.jwt;
// }
/**
* PostgresREST 特定的查询参数接口
*/
export interface PostgrestParams {
// 查询参数
select?: string;
// 排序参数
order?: string;
// 分页参数
limit?: number;
offset?: number;
// 过滤参数
filter?: Record<string, unknown>;
// 指定 PostgreSQL schema
schema?: string;
// 支持OR条件查询
or?: Array<Record<string, unknown>> | string;
// 其他参数
[key: string]: unknown;
// 自定义头部参数
headers?: Record<string, string>;
// JWT Token(自动添加到 Authorization 头部)
token?: string;
}
/**
* 将编码后的URL解码为可读格式
* @param url 编码后的URL
* @returns 解码后的可读URL
*/
function decodeUrlForDisplay(url: string): string {
try {
// 首先解码整个URL
const decodedUrl = decodeURIComponent(url);
// 如果URL中包含@符号作为前缀,则移除它
if (decodedUrl.startsWith('@')) {
return decodedUrl.substring(1);
}
return decodedUrl;
} catch (error) {
// 如果解码失败,返回原始URL
console.error('URL解码失败:', error);
return url;
}
}
/**
* 合并 JWT Token 到请求头
* @param existingHeaders 已有的请求头
* @param explicitToken 显式传入的 JWT Token(可选)
* @returns 合并后的请求头
*/
function mergeAuthHeaders(
existingHeaders: Record<string, string> = {},
explicitToken?: string
): Record<string, string> {
const headers = { ...existingHeaders };
// 如果已经有 Authorization 头部(不区分大小写),不覆盖
const hasAuth = Object.keys(headers).some(
key => key.toLowerCase() === 'authorization'
);
if (hasAuth) {
console.log('🔑 [mergeAuthHeaders] 已存在 Authorization 头,不覆盖');
return headers;
}
// 优先使用显式传入的 token,否则尝试从客户端 localStorage 获取
const token = explicitToken || (typeof window !== 'undefined' ? localStorage.getItem('access_token') : undefined);
// 如果有有效的 token(显式传入或从客户端获取),添加到 Authorization 头部
if (token && token !== 'undefined') {
// console.log('🔑 [mergeAuthHeaders] 添加 Authorization 头,token 来源:', explicitToken ? 'explicitToken' : 'localStorage');
// console.log('🔑 [mergeAuthHeaders] Token 前10位:', token.substring(0, 10));
headers['Authorization'] = `Bearer ${token}`;
} else {
console.warn('⚠️ [mergeAuthHeaders] 没有可用的 tokenexplicitToken:', explicitToken, 'window:', typeof window);
}
return headers;
}
/**
* 打印 PostgREST 查询日志
* @param endpoint 端点
* @param params 参数
* @param method HTTP 方法
* @param data 请求体数据(用于POST/PATCH请求)
*/
function logPostgrestQuery(endpoint: string, params?: QueryParams, method: string = 'GET', data?: Record<string, unknown>): void {
if (process.env.NODE_ENV !== 'production') {
// const baseUrl = 'http://172.16.0.119:9000/admin';
// const baseUrl = 'http://172.18.0.100:3000';
const baseUrl = 'http://nas.7bm.co:3000';
// 确保 endpoint 格式正确
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
let fullUrl = `${baseUrl}/${normalizedEndpoint}`;
// 如果有查询参数,将其添加到URL中
if (params && Object.keys(params).length > 0) {
const queryString = Object.entries(params)
.filter(([, value]) => value !== undefined)
.map(([key, value]) => {
// 处理特殊的PostgREST操作符(如eq, gt, lt等)
if (typeof value === 'string' && value.includes('.')) {
return `${key}=${encodeURIComponent(value)}`;
}
return `${key}=${encodeURIComponent(String(value))}`;
})
.join('&');
if (queryString) {
fullUrl += `?${queryString}`;
}
}
// console.log('\n📦 PostgREST 查询日志 ========================');
// console.log(`📦 HTTP 方法: ${method}`);
// console.log(`📦 完整 URL: ${decodeUrlForDisplay(fullUrl)}`);
// 打印请求体数据(仅适用于POST/PATCH/PUT请求)
// if (['POST', 'PATCH', 'PUT'].includes(method) && data) {
// console.log('📦 请求体数据:');
// console.log(JSON.stringify(data, null, 2));
// }
// console.log('📦 PostgREST 查询日志 ========================\n');
}
}
/**
* 将通用查询参数转换为 PostgresREST 支持的格式
* @param params 查询参数
* @returns 转换后的 PostgresREST 参数
*/
export function transformParams(params: PostgrestParams): QueryParams {
const result: QueryParams = {};
// 处理 select 参数
if (params.select) {
result.select = params.select;
}
// 处理 order 参数
if (params.order) {
result.order = params.order;
}
// 处理 limit 和 offset 参数
if (params.limit !== undefined) {
result.limit = params.limit;
}
if (params.offset !== undefined) {
result.offset = params.offset;
}
// 处理 schema 参数
if (params.schema) {
result.schema = params.schema;
}
// 处理或条件 (OR) - 两种格式支持
if (params.or) {
// 如果是字符串格式 (例如 "name.ilike.*keyword*,code.ilike.*keyword*")
if (typeof params.or === 'string') {
result.or = params.or;
}
// 如果是数组格式, 转换为 PostgREST 的 or=(condition1,condition2) 格式
else if (Array.isArray(params.or)) {
const orConditions: string[] = [];
params.or.forEach(condition => {
const entries = Object.entries(condition);
if (entries.length === 1) {
const [field, operator] = entries[0];
orConditions.push(`${field}.${operator}`);
}
});
if (orConditions.length > 0) {
result.or = `(${orConditions.join(',')})`;
}
}
}
// 处理过滤条件 - PostgresREST 格式
if (params.filter) {
Object.entries(params.filter).forEach(([key, value]) => {
// 如果值不为 undefined,则添加到查询参数中
if (value !== undefined) {
// 支持 PostgreSQL 的比较操作符 (eq, gt, lt, gte, lte, like, ilike 等)
result[key] = value as string | number | boolean;
}
});
}
// 处理其他额外参数
Object.entries(params).forEach(([key, value]) => {
// 跳过已处理的特殊参数(包括 headers 和 token
if (!['select', 'order', 'limit', 'offset', 'filter', 'schema', 'or', 'headers', 'token'].includes(key) && value !== undefined) {
result[key] = value as string | number | boolean;
}
});
return result;
}
/**
* 发送 GET 请求到 PostgresREST 接口
* @param endpoint 端点
* @param params 查询参数(可包含 token 和 headers
* @returns 响应数据
*/
export async function postgrestGet<T>(endpoint: string, params?: PostgrestParams): Promise<{data: T; headers?: Record<string, string>; error?: never} | {data?: never; error: string; status?: number}> {
try {
const queryParams = params ? transformParams(params) : {};
// 确保端点没有前导斜杠,因为API_BASE_URL已经包含了路径前缀
const apiEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
// 打印查询信息
logPostgrestQuery(apiEndpoint, queryParams, 'GET');
// 合并 JWT Token 到请求头
const headers = mergeAuthHeaders(params?.headers, params?.token);
const response = await apiRequest<T>(
apiEndpoint,
{
method: 'GET',
headers: headers
},
queryParams
);
if (response.error) {
// 🔑 检测令牌过期错误
const isTokenExpired = response.error.includes('令牌已过期') ||
response.error.includes('令牌') ||
response.error.includes('token') ||
response.error.includes('expired') ||
response.error.includes('认证') ||
response.error.includes('未授权');
if (isTokenExpired && typeof window !== 'undefined') {
console.error('🔑 [PostgREST Client - GET] 检测到令牌过期,清除会话并重定向到登录页');
localStorage.removeItem('access_token');
localStorage.removeItem('user_info');
sessionStorage.clear();
window.location.href = '/login?expired=true';
}
throw new Error(response.error);
}
// 返回数据和响应头
return {
data: response.data as T,
headers: response.headers
};
} catch (error) {
const apiError = handleApiError(error);
return { error: apiError.message, status: apiError.status };
}
}
/**
* 处理 PostgreSQL 特定错误
* @param error 错误对象或消息
* @param responseText 原始响应文本
* @returns 格式化的错误信息
*/
function handlePostgresError(error: unknown, responseText?: string): { message: string, status?: number } {
let errorMessage = error instanceof Error ? error.message : String(error);
let statusCode: number | undefined = undefined;
// 如果有原始响应文本,尝试从中提取更详细的错误信息
if (responseText) {
try {
const errorData = JSON.parse(responseText);
// 处理 PostgreSQL 错误码
if (errorData.code) {
switch (errorData.code) {
case '23505': // 唯一约束冲突
errorMessage = '记录已存在,无法创建重复数据';
if (errorData.details) {
// 尝试提取冲突的字段名
const match = errorData.details.match(/Key \((.+?)\)=/);
if (match && match[1]) {
const field = match[1];
errorMessage = `${field} 已存在,请使用不同的值`;
}
}
statusCode = 409; // Conflict
break;
case '23503': // 外键约束失败
errorMessage = '引用的记录不存在';
if (errorData.details) {
// 尝试提取外键字段
const match = errorData.details.match(/Key \((.+?)\)=/);
if (match && match[1]) {
const field = match[1];
errorMessage = `所引用的 ${field} 不存在或无效`;
}
}
statusCode = 400; // Bad Request
break;
case '42P01': // 表不存在
errorMessage = '所请求的数据表不存在';
statusCode = 404; // Not Found
break;
case '42703': // 列不存在
errorMessage = '所引用的字段不存在';
if (errorData.details) {
const match = errorData.details.match(/column "(.+?)"/);
if (match && match[1]) {
const column = match[1];
errorMessage = `字段 "${column}" 不存在`;
}
}
statusCode = 400; // Bad Request
break;
default:
// 使用 PostgreSQL 提供的错误消息
if (errorData.message) {
errorMessage = errorData.message;
}
break;
}
}
// 处理 HTTP 状态码
if (errorData.status) {
statusCode = errorData.status;
}
} catch (e) {
console.error('解析错误响应失败:', e);
// 如果解析失败,尝试直接使用响应文本
if (responseText && responseText.length < 200) {
errorMessage = responseText;
}
}
}
return { message: errorMessage, status: statusCode };
}
/**
* 发送 POST 请求到 PostgresREST 接口
* @param endpoint 端点(表名)
* @param data 请求体数据
* @param token JWT Token(可选)
* @returns 响应数据
*/
export async function postgrestPost<T, D = Record<string, unknown>>(endpoint: string, data: D, token?: string): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 确保端点没有前导斜杠
const apiEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
// 预处理数据,确保所有字段类型符合 PostgreSQL 要求
const processedData = preprocessData(data as Record<string, unknown>);
// 打印查询信息
logPostgrestQuery(apiEndpoint, undefined, 'POST', processedData);
// 确保数据是合法的JSON对象
const requestBody = JSON.stringify(processedData);
// console.log(`准备发送 PostgreSQL 插入请求到: ${apiEndpoint}`);
// console.log(`请求体: ${requestBody}`);
// 合并 JWT Token 到请求头
const headers = mergeAuthHeaders({
'Content-Type': 'application/json',
'Accept': 'application/json',
'Prefer': 'return=representation'
}, token);
try {
const response = await apiRequest<T>(
apiEndpoint,
{
method: 'POST',
body: requestBody,
headers: headers
}
);
if (response.error) {
console.error(`POST请求失败: ${response.error}`);
// 🔑 检测令牌过期错误
const isTokenExpired = response.error.includes('令牌已过期') ||
response.error.includes('令牌') ||
response.error.includes('token') ||
response.error.includes('expired') ||
response.error.includes('认证') ||
response.error.includes('未授权');
if (isTokenExpired && typeof window !== 'undefined') {
console.error('🔑 [PostgREST Client] 检测到令牌过期,清除会话并重定向到登录页');
localStorage.removeItem('access_token');
localStorage.removeItem('user_info');
sessionStorage.clear();
window.location.href = '/login?expired=true';
}
throw new Error(response.error);
}
// console.log(`POST请求成功,响应: `, response.data);
return { data: response.data as T };
} catch (error) {
// 捕获并处理 API 请求错误
let errorText = '';
// 如果错误对象中包含响应文本,尝试提取更详细的错误信息
if (error instanceof Error && 'responseText' in error) {
errorText = (error as {responseText: string}).responseText;
}
// 处理 PostgreSQL 特定错误
const pgError = handlePostgresError(error, errorText);
console.error(`POST请求错误: ${pgError.message}`);
return {
error: pgError.message,
status: pgError.status || 500
};
}
} catch (error) {
console.error(`POST请求处理错误: ${error instanceof Error ? error.message : String(error)}`);
const apiError = handleApiError(error);
return { error: apiError.message, status: apiError.status };
}
}
/**
* 预处理数据,确保类型与 PostgreSQL 兼容
* @param data 原始数据
* @returns 处理后的数据
*/
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' && /^\d+$/.test(value) && key !== 'code' && !key.endsWith('_id')) {
// 对于可能需要保持字符串格式的值,我们不进行数字转换
processed[key] = value;
}
// 将字符串 'true'/'false' 转为布尔值
else if (typeof value === 'string' && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
processed[key] = value.toLowerCase() === 'true';
}
// 尝试转换 '0'/'1' 为 boolean (如果字段名暗示是布尔值)
else if (typeof value === 'string' && (value === '0' || value === '1') &&
(key.startsWith('is_') || key.startsWith('has_') || key.endsWith('_enabled'))) {
processed[key] = value === '1';
}
// 对于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;
}
/**
* 发送 PUT 请求到 PostgresREST 接口
* @param endpoint 端点
* @param data 请求体数据
* @param filters 过滤条件
* @param token JWT Token(可选)
* @returns 响应数据
*/
export async function postgrestPut<T, D extends object>(
endpoint: string,
data: D,
filters?: Record<string | number, string | number>,
token?: string
): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 确保端点没有前导斜杠
const apiEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
// 构建完整的URL,包含过滤条件
const fullEndpoint = apiEndpoint;
const queryParams: QueryParams = {};
if (filters) {
// 将过滤条件转换为PostgREST格式的查询参数
Object.entries(filters).forEach(([key, value]) => {
queryParams[key] = `eq.${value}`;
});
}
// 打印查询信息
logPostgrestQuery(fullEndpoint, queryParams, 'PATCH', data as unknown as Record<string, unknown>);
// 合并 JWT Token 到请求头
const headers = mergeAuthHeaders({
'Prefer': 'return=representation'
}, token);
const response = await apiRequest<T>(
fullEndpoint,
{
method: 'PATCH',
body: JSON.stringify(data),
headers: headers
},
queryParams
);
if (response.error) {
// 🔑 检测令牌过期错误
const isTokenExpired = response.error.includes('令牌已过期') ||
response.error.includes('令牌') ||
response.error.includes('token') ||
response.error.includes('expired') ||
response.error.includes('认证') ||
response.error.includes('未授权');
if (isTokenExpired && typeof window !== 'undefined') {
console.error('🔑 [PostgREST Client - PATCH] 检测到令牌过期,清除会话并重定向到登录页');
localStorage.removeItem('access_token');
localStorage.removeItem('user_info');
sessionStorage.clear();
window.location.href = '/login?expired=true';
}
throw new Error(response.error);
}
if (!response.data) {
throw new Error('更新成功但未返回数据');
}
return { data: response.data };
} catch (error) {
const apiError = handleApiError(error);
return { error: apiError.message, status: apiError.status };
}
}
/**
* 发送 DELETE 请求到 PostgresREST 接口
* @param endpoint 端点
* @param params 查询参数,用于指定要删除的记录(可包含 token 和 headers
* @returns 响应数据
*/
export async function postgrestDelete<T>(endpoint: string, params?: PostgrestParams): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 确保端点没有前导斜杠
const apiEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
// 转换查询参数
const queryParams = params ? transformParams(params) : {};
// 合并 JWT Token 到请求头
const headers = mergeAuthHeaders({
'Prefer': 'return=representation', // 默认请求返回被删除的记录
...(params?.headers || {})
}, params?.token);
// 打印查询信息
logPostgrestQuery(apiEndpoint, queryParams, 'DELETE');
const response = await apiRequest<T>(
apiEndpoint,
{
method: 'DELETE',
headers
},
queryParams
);
if (response.error) {
// 🔑 检测令牌过期错误
const isTokenExpired = response.error.includes('令牌已过期') ||
response.error.includes('令牌') ||
response.error.includes('token') ||
response.error.includes('expired') ||
response.error.includes('认证') ||
response.error.includes('未授权');
if (isTokenExpired && typeof window !== 'undefined') {
console.error('🔑 [PostgREST Client - DELETE] 检测到令牌过期,清除会话并重定向到登录页');
localStorage.removeItem('access_token');
localStorage.removeItem('user_info');
sessionStorage.clear();
window.location.href = '/login?expired=true';
}
throw new Error(response.error);
}
return { data: response.data as T };
} catch (error) {
const apiError = handleApiError(error);
return { error: apiError.message, status: apiError.status };
}
}