在准备环境前提交次全部更改。

This commit is contained in:
Neo
2026-02-19 08:35:13 +08:00
parent ded6dfb9d8
commit 4eac07da47
1387 changed files with 6107191 additions and 33002 deletions

View File

@@ -0,0 +1,159 @@
/**
* axios 实例 & JWT 拦截器。
*
* - 请求拦截器:自动从 localStorage 读取 access_token 并附加 Authorization header
* - 响应拦截器:遇到 401 时尝试用 refresh_token 刷新,刷新失败则清除令牌并跳转 /login
* - 并发刷新保护:多个请求同时 401 时只触发一次 refresh其余排队等待
*/
import axios, {
type AxiosError,
type AxiosRequestConfig,
type InternalAxiosRequestConfig,
} from "axios";
/* ------------------------------------------------------------------ */
/* 常量 */
/* ------------------------------------------------------------------ */
const ACCESS_TOKEN_KEY = "access_token";
const REFRESH_TOKEN_KEY = "refresh_token";
/* ------------------------------------------------------------------ */
/* axios 实例 */
/* ------------------------------------------------------------------ */
export const apiClient = axios.create({
baseURL: "/api",
});
/* ------------------------------------------------------------------ */
/* 请求拦截器 — 附加 JWT */
/* ------------------------------------------------------------------ */
apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem(ACCESS_TOKEN_KEY);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
/* ------------------------------------------------------------------ */
/* 响应拦截器 — 401 自动刷新 */
/* ------------------------------------------------------------------ */
/** 是否正在刷新中 */
let isRefreshing = false;
/** 等待刷新完成的请求队列 */
let pendingQueue: {
resolve: (token: string) => void;
reject: (err: unknown) => void;
}[] = [];
/** 刷新完成后,依次重放排队的请求 */
function processPendingQueue(token: string | null, error: unknown) {
pendingQueue.forEach(({ resolve, reject }) => {
if (token) {
resolve(token);
} else {
reject(error);
}
});
pendingQueue = [];
}
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as AxiosRequestConfig & {
_retried?: boolean;
};
// 非 401、无原始请求、或已重试过 → 直接抛出
if (
error.response?.status !== 401 ||
!originalRequest ||
originalRequest._retried
) {
return Promise.reject(error);
}
// 刷新端点本身 401 → 不再递归刷新
if (originalRequest.url === "/auth/refresh") {
clearTokensAndRedirect();
return Promise.reject(error);
}
// 已有刷新请求在飞 → 排队等待
if (isRefreshing) {
return new Promise<string>((resolve, reject) => {
pendingQueue.push({ resolve, reject });
}).then((newToken) => {
originalRequest.headers = {
...originalRequest.headers,
Authorization: `Bearer ${newToken}`,
};
originalRequest._retried = true;
return apiClient(originalRequest);
});
}
// 发起刷新
isRefreshing = true;
originalRequest._retried = true;
const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY);
if (!refreshToken) {
isRefreshing = false;
processPendingQueue(null, error);
clearTokensAndRedirect();
return Promise.reject(error);
}
try {
// 用独立 axios 调用避免被自身拦截器干扰
const { data } = await axios.post<{
access_token: string;
refresh_token: string;
}>("/api/auth/refresh", { refresh_token: refreshToken });
localStorage.setItem(ACCESS_TOKEN_KEY, data.access_token);
localStorage.setItem(REFRESH_TOKEN_KEY, data.refresh_token);
processPendingQueue(data.access_token, null);
// 重放原始请求
originalRequest.headers = {
...originalRequest.headers,
Authorization: `Bearer ${data.access_token}`,
};
return apiClient(originalRequest);
} catch (refreshError) {
processPendingQueue(null, refreshError);
clearTokensAndRedirect();
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
},
);
/* ------------------------------------------------------------------ */
/* 辅助 */
/* ------------------------------------------------------------------ */
function clearTokensAndRedirect() {
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
// 派发自定义事件,让 authStore 监听并重置状态
// 避免直接 import authStore 导致循环依赖
window.dispatchEvent(new Event("auth:force-logout"));
// 避免在登录页反复跳转
if (window.location.pathname !== "/login") {
window.location.href = "/login";
}
}

View File

@@ -0,0 +1,59 @@
/**
* 数据库查看器相关 API 调用。
*
* - fetchSchemas获取 Schema 列表
* - fetchTables获取指定 Schema 下的表列表(含行数)
* - fetchColumns获取指定表的列定义
* - executeQuery执行只读 SQL 查询
*/
import { apiClient } from './client';
/** 表信息 */
export interface TableInfo {
name: string;
row_count: number;
}
/** 列定义 */
export interface ColumnInfo {
name: string;
data_type: string;
is_nullable: boolean;
default_value: string | null;
}
/** 查询结果 */
export interface QueryResult {
columns: string[];
rows: unknown[][];
row_count: number;
}
/** 获取所有 Schema */
export async function fetchSchemas(): Promise<string[]> {
const { data } = await apiClient.get<{ schemas: string[] }>('/db/schemas');
return data.schemas;
}
/** 获取指定 Schema 下的表列表 */
export async function fetchTables(schema: string): Promise<TableInfo[]> {
const { data } = await apiClient.get<{ tables: TableInfo[] }>(
`/db/schemas/${encodeURIComponent(schema)}/tables`,
);
return data.tables;
}
/** 获取指定表的列定义 */
export async function fetchColumns(schema: string, table: string): Promise<ColumnInfo[]> {
const { data } = await apiClient.get<{ columns: ColumnInfo[] }>(
`/db/tables/${encodeURIComponent(schema)}/${encodeURIComponent(table)}/columns`,
);
return data.columns;
}
/** 执行只读 SQL 查询 */
export async function executeQuery(sql: string): Promise<QueryResult> {
const { data } = await apiClient.post<QueryResult>('/db/query', { sql });
return data;
}

View File

@@ -0,0 +1,44 @@
/**
* 环境配置相关 API 调用。
*
* - fetchEnvConfig获取键值对列表敏感值已掩码
* - updateEnvConfig批量更新键值对
* - exportEnvConfig导出去敏感值的配置文件浏览器下载
*/
import { apiClient } from './client';
import type { EnvConfigItem } from '../types';
/** 获取环境配置列表 */
export async function fetchEnvConfig(): Promise<EnvConfigItem[]> {
const { data } = await apiClient.get<{ items: EnvConfigItem[] }>('/env-config');
return data.items;
}
/** 批量更新环境配置 */
export async function updateEnvConfig(items: EnvConfigItem[]): Promise<void> {
await apiClient.put('/env-config', { items });
}
/** 导出配置文件(去敏感值),触发浏览器下载 */
export async function exportEnvConfig(): Promise<void> {
const response = await apiClient.get('/env-config/export', {
responseType: 'blob',
});
// 从响应头提取文件名,回退默认值
const disposition = response.headers['content-disposition'] as string | undefined;
let filename = 'env-config.txt';
if (disposition) {
const match = disposition.match(/filename="?([^";\s]+)"?/);
if (match) filename = match[1];
}
// 创建临时链接触发下载
const url = URL.createObjectURL(response.data as Blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}

View File

@@ -0,0 +1,38 @@
/**
* ETL 状态监控 API 调用。
*
* - fetchCursors获取各任务的数据游标最后抓取时间、记录数
* - fetchRecentRuns获取最近执行记录
*/
import { apiClient } from './client';
/** ETL 游标信息 */
export interface CursorInfo {
task_code: string;
last_fetch_time: string | null;
record_count: number | null;
}
/** 最近执行记录 */
export interface RecentRun {
id: string;
task_codes: string[];
status: string;
started_at: string;
finished_at: string | null;
duration_ms: number | null;
exit_code: number | null;
}
/** 获取各任务的数据游标 */
export async function fetchCursors(): Promise<CursorInfo[]> {
const { data } = await apiClient.get<CursorInfo[]>('/etl-status/cursors');
return data;
}
/** 获取最近执行记录 */
export async function fetchRecentRuns(): Promise<RecentRun[]> {
const { data } = await apiClient.get<RecentRun[]>('/etl-status/recent-runs');
return data;
}

View File

@@ -0,0 +1,47 @@
/**
* 任务执行相关 API 调用。
*
* - submitToQueue提交任务配置到执行队列
* - executeDirectly直接执行任务
* - fetchQueue获取当前队列
* - fetchHistory获取执行历史
* - deleteFromQueue从队列删除任务
* - cancelExecution取消执行中的任务
*/
import { apiClient } from './client';
import type { TaskConfig, QueuedTask, ExecutionLog } from '../types';
/** 提交任务配置到执行队列 */
export async function submitToQueue(config: TaskConfig): Promise<{ id: string }> {
const { data } = await apiClient.post<{ id: string }>('/execution/queue', config);
return data;
}
/** 直接执行任务(不经过队列) */
export async function executeDirectly(config: TaskConfig): Promise<{ execution_id: string }> {
const { data } = await apiClient.post<{ execution_id: string }>('/execution/run', config);
return data;
}
/** 获取当前任务队列 */
export async function fetchQueue(): Promise<QueuedTask[]> {
const { data } = await apiClient.get<QueuedTask[]>('/execution/queue');
return data;
}
/** 获取执行历史记录 */
export async function fetchHistory(limit = 50): Promise<ExecutionLog[]> {
const { data } = await apiClient.get<ExecutionLog[]>('/execution/history', { params: { limit } });
return data;
}
/** 从队列中删除待执行任务 */
export async function deleteFromQueue(id: string): Promise<void> {
await apiClient.delete(`/execution/queue/${id}`);
}
/** 取消执行中的任务 */
export async function cancelExecution(id: string): Promise<void> {
await apiClient.post(`/execution/${id}/cancel`);
}

View File

@@ -0,0 +1,48 @@
/**
* 调度任务相关 API 调用。
*/
import { apiClient } from './client';
import type { ScheduledTask, ScheduleConfig, TaskConfig } from '../types';
/** 获取调度任务列表 */
export async function fetchSchedules(): Promise<ScheduledTask[]> {
const { data } = await apiClient.get<ScheduledTask[]>('/schedules');
return data;
}
/** 创建调度任务 */
export async function createSchedule(payload: {
name: string;
task_codes: string[];
task_config: TaskConfig;
schedule_config: ScheduleConfig;
}): Promise<ScheduledTask> {
const { data } = await apiClient.post<ScheduledTask>('/schedules', payload);
return data;
}
/** 更新调度任务 */
export async function updateSchedule(
id: string,
payload: Partial<{
name: string;
task_codes: string[];
task_config: TaskConfig;
schedule_config: ScheduleConfig;
}>,
): Promise<ScheduledTask> {
const { data } = await apiClient.put<ScheduledTask>(`/schedules/${id}`, payload);
return data;
}
/** 删除调度任务 */
export async function deleteSchedule(id: string): Promise<void> {
await apiClient.delete(`/schedules/${id}`);
}
/** 启用/禁用调度任务 */
export async function toggleSchedule(id: string): Promise<ScheduledTask> {
const { data } = await apiClient.patch<ScheduledTask>(`/schedules/${id}/toggle`);
return data;
}

View File

@@ -0,0 +1,32 @@
/**
* 任务相关 API 调用。
*
* - fetchTaskRegistry获取按业务域分组的任务注册表
*/
import { apiClient } from './client';
import type { TaskConfig, TaskDefinition } from '../types';
/** 获取按业务域分组的任务注册表 */
export async function fetchTaskRegistry(): Promise<Record<string, TaskDefinition[]>> {
// 后端返回 { groups: { 域名: [TaskItem] } },需要解包
const { data } = await apiClient.get<{ groups: Record<string, TaskDefinition[]> }>('/tasks/registry');
return data.groups;
}
/** 获取按业务域分组的 DWD 表定义 */
export async function fetchDwdTables(): Promise<Record<string, string[]>> {
// 后端返回 { groups: { 域名: [DwdTableItem] } },需要解包并提取 table_name
const { data } = await apiClient.get<{ groups: Record<string, { table_name: string }[]> }>('/tasks/dwd-tables');
const result: Record<string, string[]> = {};
for (const [domain, items] of Object.entries(data.groups)) {
result[domain] = items.map((item) => item.table_name);
}
return result;
}
/** 验证任务配置并返回生成的 CLI 命令预览 */
export async function validateTaskConfig(config: TaskConfig): Promise<{ command: string }> {
const { data } = await apiClient.post<{ command: string }>('/tasks/validate', { config });
return data;
}