You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
461 lines
12 KiB
461 lines
12 KiB
import type { ApiResponse } from '@/types';
|
|
|
|
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1';
|
|
|
|
// 管理员 API 请求
|
|
async function adminRequest<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
const token = localStorage.getItem('token');
|
|
|
|
if (!token) {
|
|
throw new Error('请先登录后再操作');
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE_URL}${path}`, {
|
|
...options,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
...options.headers,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ message: '请求失败' }));
|
|
if (response.status === 401) {
|
|
// Token 过期或无效,清除本地存储
|
|
localStorage.removeItem('token');
|
|
localStorage.removeItem('user');
|
|
throw new Error('登录已过期,请重新登录');
|
|
}
|
|
throw new Error(error.message || `HTTP ${response.status}`);
|
|
}
|
|
|
|
const data: ApiResponse<T> = await response.json();
|
|
|
|
if (data.code !== 200) {
|
|
throw new Error(data.message || '请求失败');
|
|
}
|
|
|
|
return data.data;
|
|
}
|
|
|
|
// 用户管理
|
|
export interface AdminUser {
|
|
id: string;
|
|
username: string;
|
|
email: string;
|
|
role: 'admin' | 'user';
|
|
status: 'active' | 'banned';
|
|
createdAt: string;
|
|
lastLogin: string;
|
|
favoritesCount: number;
|
|
}
|
|
|
|
// 数据源配置
|
|
export interface DataSourceConfig {
|
|
id: string;
|
|
name: string;
|
|
type: 'akshare' | 'tushare' | 'custom';
|
|
url: string;
|
|
apiKey?: string;
|
|
enabled: boolean;
|
|
syncInterval: number;
|
|
lastSync?: string;
|
|
status: 'connected' | 'disconnected' | 'error';
|
|
}
|
|
|
|
// 数据检查项
|
|
export interface DataCheckItem {
|
|
id: string;
|
|
name: string;
|
|
type: 'stock' | 'sector' | 'index' | 'kline';
|
|
total: number;
|
|
current: number;
|
|
lastUpdate: string;
|
|
status: 'complete' | 'incomplete' | 'missing';
|
|
details?: string;
|
|
}
|
|
|
|
// 同步任务
|
|
export interface SyncTask {
|
|
id: string;
|
|
type: string;
|
|
status: 'pending' | 'running' | 'completed' | 'failed';
|
|
progress: number;
|
|
currentTask: string;
|
|
totalRecords: number;
|
|
processedRecords: number;
|
|
createdAt: string;
|
|
completedAt?: string;
|
|
error?: string;
|
|
}
|
|
|
|
// 管理员 API
|
|
export const adminApi = {
|
|
// ========== 数据源管理 ==========
|
|
|
|
// 获取数据源列表
|
|
getDataSources(): Promise<DataSourceConfig[]> {
|
|
return adminRequest('/admin/data-sources');
|
|
},
|
|
|
|
// 更新数据源配置
|
|
updateDataSource(id: string, config: Partial<DataSourceConfig>): Promise<DataSourceConfig> {
|
|
return adminRequest(`/admin/data-sources/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(config),
|
|
});
|
|
},
|
|
|
|
// 测试数据源连接
|
|
testDataSource(id: string): Promise<{ success: boolean; message: string }> {
|
|
return adminRequest(`/admin/data-sources/${id}/test`, {
|
|
method: 'POST',
|
|
});
|
|
},
|
|
|
|
// 手动触发同步
|
|
triggerSync(sourceId: string): Promise<{ taskId: string }> {
|
|
return adminRequest(`/admin/data-sources/${sourceId}/sync`, {
|
|
method: 'POST',
|
|
});
|
|
},
|
|
|
|
// ========== AKShare 特定接口 ==========
|
|
|
|
// 获取 AKShare 状态
|
|
getAKShareStatus(): Promise<{
|
|
connected: boolean;
|
|
version?: string;
|
|
supportedApis: string[];
|
|
}> {
|
|
return adminRequest('/admin/akshare/status');
|
|
},
|
|
|
|
// 获取 AKShare 配置
|
|
getAKShareConfig(): Promise<{
|
|
baseUrl: string;
|
|
timeout: number;
|
|
retryTimes: number;
|
|
rateLimit: number;
|
|
}> {
|
|
return adminRequest('/admin/akshare/config');
|
|
},
|
|
|
|
// 更新 AKShare 配置
|
|
updateAKShareConfig(config: {
|
|
baseUrl?: string;
|
|
timeout?: number;
|
|
retryTimes?: number;
|
|
rateLimit?: number;
|
|
}): Promise<void> {
|
|
return adminRequest('/admin/akshare/config', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(config),
|
|
});
|
|
},
|
|
|
|
// ========== 数据检测与缓冲 ==========
|
|
|
|
// 获取数据完整性检查
|
|
getDataCheck(): Promise<DataCheckItem[]> {
|
|
return adminRequest('/admin/data-check');
|
|
},
|
|
|
|
// 执行数据完整性检查
|
|
runDataCheck(): Promise<{ taskId: string }> {
|
|
return adminRequest('/admin/data-check', {
|
|
method: 'POST',
|
|
});
|
|
},
|
|
|
|
// 获取同步任务进度
|
|
getSyncTask(taskId: string): Promise<SyncTask> {
|
|
return adminRequest(`/admin/sync-tasks/${taskId}`);
|
|
},
|
|
|
|
// 一键缓冲缺失数据(自动补全一年内的数据)
|
|
bufferMissingData(options?: {
|
|
startDate?: string;
|
|
endDate?: string;
|
|
types?: ('stock' | 'sector' | 'kline')[];
|
|
}): Promise<{ taskId: string }> {
|
|
// 默认缓冲一年内数据
|
|
const endDate = new Date();
|
|
const startDate = new Date();
|
|
startDate.setFullYear(startDate.getFullYear() - 1);
|
|
|
|
return adminRequest('/admin/buffer', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
startDate: startDate.toISOString().split('T')[0],
|
|
endDate: endDate.toISOString().split('T')[0],
|
|
types: ['stock', 'sector', 'kline'],
|
|
autoCalculate: true, // 自动计算动量指标
|
|
...options,
|
|
}),
|
|
});
|
|
},
|
|
|
|
// 缓冲特定股票数据
|
|
bufferStockData(stockCode: string, days: number = 365): Promise<{ taskId: string }> {
|
|
return adminRequest('/admin/buffer/stock', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ stockCode, days }),
|
|
});
|
|
},
|
|
|
|
// 缓冲所有股票基础数据
|
|
bufferAllStocks(): Promise<{ taskId: string }> {
|
|
return adminRequest('/admin/buffer/stocks', {
|
|
method: 'POST',
|
|
});
|
|
},
|
|
|
|
// 缓冲版块数据
|
|
bufferSectors(): Promise<{ taskId: string }> {
|
|
return adminRequest('/admin/buffer/sectors', {
|
|
method: 'POST',
|
|
});
|
|
},
|
|
|
|
// 缓冲K线数据
|
|
bufferKLines(options?: {
|
|
stockCodes?: string[];
|
|
startDate?: string;
|
|
endDate?: string;
|
|
}): Promise<{ taskId: string }> {
|
|
const endDate = new Date();
|
|
const startDate = new Date();
|
|
startDate.setFullYear(startDate.getFullYear() - 1);
|
|
|
|
return adminRequest('/admin/buffer/kline', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
startDate: startDate.toISOString().split('T')[0],
|
|
endDate: endDate.toISOString().split('T')[0],
|
|
...options,
|
|
}),
|
|
});
|
|
},
|
|
|
|
// 计算动量指标
|
|
calculateMomentum(options?: {
|
|
stockCodes?: string[];
|
|
days?: number;
|
|
}): Promise<{ taskId: string }> {
|
|
return adminRequest('/admin/calculate/momentum', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
days: 20,
|
|
...options,
|
|
}),
|
|
});
|
|
},
|
|
|
|
// ========== 用户管理 ==========
|
|
|
|
// 获取用户列表
|
|
getUsers(params?: {
|
|
page?: number;
|
|
pageSize?: number;
|
|
search?: string;
|
|
role?: string;
|
|
status?: string;
|
|
}): Promise<{
|
|
users: AdminUser[];
|
|
total: number;
|
|
page: number;
|
|
pageSize: number;
|
|
}> {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.page) queryParams.append('page', params.page.toString());
|
|
if (params?.pageSize) queryParams.append('pageSize', params.pageSize.toString());
|
|
if (params?.search) queryParams.append('search', params.search);
|
|
if (params?.role && params.role !== 'all') queryParams.append('role', params.role);
|
|
if (params?.status && params.status !== 'all') queryParams.append('status', params.status);
|
|
|
|
return adminRequest(`/admin/users?${queryParams.toString()}`);
|
|
},
|
|
|
|
// 更新用户状态(封禁/解封)
|
|
updateUserStatus(userId: string, status: 'active' | 'banned'): Promise<void> {
|
|
return adminRequest(`/admin/users/${userId}/status`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ status }),
|
|
});
|
|
},
|
|
|
|
// 删除用户
|
|
deleteUser(userId: string): Promise<void> {
|
|
return adminRequest(`/admin/users/${userId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
|
|
// 批量操作用户
|
|
batchUpdateUsers(userIds: string[], action: 'ban' | 'unban' | 'delete'): Promise<void> {
|
|
return adminRequest('/admin/users/batch', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ userIds, action }),
|
|
});
|
|
},
|
|
|
|
// ========== 数据导入 ==========
|
|
|
|
// 上传导入文件
|
|
uploadImportFile(file: File, type: string): Promise<{ taskId: string; filename: string }> {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('type', type);
|
|
|
|
const token = localStorage.getItem('token');
|
|
|
|
return fetch(`${API_BASE_URL}/admin/import`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': token ? `Bearer ${token}` : '',
|
|
},
|
|
body: formData,
|
|
}).then(async (response) => {
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ message: '上传失败' }));
|
|
throw new Error(error.message);
|
|
}
|
|
const data = await response.json();
|
|
if (data.code !== 200) {
|
|
throw new Error(data.message);
|
|
}
|
|
return data.data;
|
|
});
|
|
},
|
|
|
|
// 获取导入任务列表
|
|
getImportTasks(): Promise<{
|
|
id: string;
|
|
name: string;
|
|
fileName: string;
|
|
status: string;
|
|
progress: number;
|
|
totalRecords: number;
|
|
importedRecords: number;
|
|
createdAt: string;
|
|
}[]> {
|
|
return adminRequest('/admin/import/tasks');
|
|
},
|
|
|
|
// ========== AI 配置 ==========
|
|
|
|
// 获取 AI 配置
|
|
getAIConfig(): Promise<{
|
|
provider: string;
|
|
model: string;
|
|
apiUrl: string;
|
|
temperature: number;
|
|
maxTokens: number;
|
|
enabled: boolean;
|
|
}> {
|
|
return adminRequest('/admin/ai-config');
|
|
},
|
|
|
|
// 更新 AI 配置
|
|
updateAIConfig(config: {
|
|
provider?: string;
|
|
model?: string;
|
|
apiKey?: string;
|
|
apiUrl?: string;
|
|
temperature?: number;
|
|
maxTokens?: number;
|
|
enabled?: boolean;
|
|
}): Promise<void> {
|
|
return adminRequest('/admin/ai-config', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(config),
|
|
});
|
|
},
|
|
|
|
// 测试 AI 连接
|
|
testAIConnection(): Promise<{ success: boolean; message: string }> {
|
|
return adminRequest('/admin/ai-config/test', {
|
|
method: 'POST',
|
|
});
|
|
},
|
|
|
|
// 获取动量计算配置
|
|
getMomentumConfig(): Promise<{
|
|
calculationPeriod: number;
|
|
weightPriceChange: number;
|
|
weightVolume: number;
|
|
weightTechnical: number;
|
|
thresholdStrong: number;
|
|
thresholdWeak: number;
|
|
}> {
|
|
return adminRequest('/admin/momentum-config');
|
|
},
|
|
|
|
// 更新动量计算配置
|
|
updateMomentumConfig(config: {
|
|
calculationPeriod?: number;
|
|
weightPriceChange?: number;
|
|
weightVolume?: number;
|
|
weightTechnical?: number;
|
|
thresholdStrong?: number;
|
|
thresholdWeak?: number;
|
|
}): Promise<void> {
|
|
return adminRequest('/admin/momentum-config', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(config),
|
|
});
|
|
},
|
|
|
|
// ========== 系统统计 ==========
|
|
|
|
// 获取系统统计
|
|
getSystemStats(): Promise<{
|
|
totalUsers: number;
|
|
totalStocks: number;
|
|
totalSectors: number;
|
|
dataCompleteness: number;
|
|
lastSync: string;
|
|
apiStatus: {
|
|
akshare: boolean;
|
|
database: boolean;
|
|
redis: boolean;
|
|
};
|
|
}> {
|
|
return adminRequest('/admin/stats');
|
|
},
|
|
|
|
// 获取数据保留策略
|
|
getDataRetention(): Promise<{
|
|
stockQuotesDays: number;
|
|
klineDays: number;
|
|
logsDays: number;
|
|
}> {
|
|
return adminRequest('/admin/data-retention');
|
|
},
|
|
|
|
// 更新数据保留策略
|
|
updateDataRetention(policy: {
|
|
stockQuotesDays?: number;
|
|
klineDays?: number;
|
|
logsDays?: number;
|
|
}): Promise<void> {
|
|
return adminRequest('/admin/data-retention', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(policy),
|
|
});
|
|
},
|
|
};
|
|
|
|
// WebSocket 连接用于实时获取同步进度
|
|
export function createSyncTaskWebSocket(taskId: string): WebSocket {
|
|
const wsUrl = (import.meta.env.VITE_WS_URL || 'ws://localhost:3000').replace(/^http/, 'ws');
|
|
const token = localStorage.getItem('token');
|
|
|
|
return new WebSocket(`${wsUrl}/ws/sync-tasks/${taskId}?token=${token}`);
|
|
}
|
|
|
|
// Ensure export
|
|
export default adminApi;
|