parent
c56ccf5fb2
commit
9fd50387cb
@ -0,0 +1,25 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 安装 Python 依赖
|
||||
RUN pip install --no-cache-dir \
|
||||
akshare \
|
||||
fastapi \
|
||||
uvicorn \
|
||||
pandas \
|
||||
numpy
|
||||
|
||||
# 创建启动脚本
|
||||
COPY main.py /app/main.py
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# 启动 AKShare HTTP 服务
|
||||
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
Binary file not shown.
@ -0,0 +1,214 @@
|
||||
"""
|
||||
AKShare HTTP API 服务
|
||||
提供股票数据接口
|
||||
"""
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import akshare as ak
|
||||
import pandas as pd
|
||||
from typing import Optional, List
|
||||
import json
|
||||
|
||||
app = FastAPI(
|
||||
title="AKShare HTTP API",
|
||||
description="AKShare 数据接口 HTTP 服务",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# 配置 CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
def dataframe_to_records(df: pd.DataFrame) -> List[dict]:
|
||||
"""将 DataFrame 转换为可 JSON 序列化的记录列表"""
|
||||
if df is None or df.empty:
|
||||
return []
|
||||
# 处理 NaN 值
|
||||
df = df.replace({pd.NaT: None})
|
||||
df = df.where(pd.notnull(df), None)
|
||||
return df.to_dict('records')
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""健康检查"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "AKShare HTTP API",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/stock_zh_a_spot")
|
||||
async def stock_zh_a_spot():
|
||||
"""
|
||||
获取 A 股实时行情数据
|
||||
"""
|
||||
try:
|
||||
df = ak.stock_zh_a_spot_em()
|
||||
records = dataframe_to_records(df)
|
||||
# 字段映射,统一返回格式
|
||||
mapped_records = []
|
||||
for record in records:
|
||||
mapped_records.append({
|
||||
"code": record.get("代码"),
|
||||
"name": record.get("名称"),
|
||||
"price": record.get("最新价", 0),
|
||||
"change": record.get("涨跌额", 0),
|
||||
"change_percent": record.get("涨跌幅", 0),
|
||||
"volume": record.get("成交量", 0),
|
||||
"turnover": record.get("成交额", 0),
|
||||
"open": record.get("开盘价", 0),
|
||||
"high": record.get("最高价", 0),
|
||||
"low": record.get("最低价", 0),
|
||||
"pre_close": record.get("昨收", 0),
|
||||
"turnover_rate": record.get("换手率", 0),
|
||||
"amplitude": record.get("振幅", 0),
|
||||
"market_cap": record.get("总市值", 0),
|
||||
"pe": record.get("市盈率-动态", 0),
|
||||
"pb": record.get("市净率", 0),
|
||||
"industry": record.get("行业", ""),
|
||||
})
|
||||
return mapped_records
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/stock_zh_a_hist")
|
||||
async def stock_zh_a_hist(
|
||||
symbol: str = Query(..., description="股票代码,如 000001"),
|
||||
period: str = Query("daily", description="周期: daily/weekly/monthly"),
|
||||
start_date: Optional[str] = Query(None, description="开始日期 YYYYMMDD"),
|
||||
end_date: Optional[str] = Query(None, description="结束日期 YYYYMMDD"),
|
||||
adjust: str = Query("qfq", description="复权方式: qfq-前复权, hfq-后复权, 不复权")
|
||||
):
|
||||
"""
|
||||
获取 A 股历史 K 线数据
|
||||
"""
|
||||
try:
|
||||
# 转换周期参数
|
||||
period_map = {
|
||||
"daily": "daily",
|
||||
"weekly": "weekly",
|
||||
"monthly": "monthly"
|
||||
}
|
||||
ak_period = period_map.get(period, "daily")
|
||||
|
||||
# 转换复权参数
|
||||
adjust_map = {
|
||||
"qfq": "qfq",
|
||||
"hfq": "hfq",
|
||||
"": ""
|
||||
}
|
||||
ak_adjust = adjust_map.get(adjust, "qfq")
|
||||
|
||||
df = ak.stock_zh_a_hist(
|
||||
symbol=symbol,
|
||||
period=ak_period,
|
||||
start_date=start_date or "19700101",
|
||||
end_date=end_date or "20500101",
|
||||
adjust=ak_adjust
|
||||
)
|
||||
records = dataframe_to_records(df)
|
||||
|
||||
# 字段映射
|
||||
mapped_records = []
|
||||
for record in records:
|
||||
mapped_records.append({
|
||||
"date": record.get("日期"),
|
||||
"open": record.get("开盘", 0),
|
||||
"high": record.get("最高", 0),
|
||||
"low": record.get("最低", 0),
|
||||
"close": record.get("收盘", 0),
|
||||
"volume": record.get("成交量", 0),
|
||||
"turnover": record.get("成交额", 0),
|
||||
"amplitude": record.get("振幅", 0),
|
||||
"change": record.get("涨跌幅", 0),
|
||||
"change_amount": record.get("涨跌额", 0),
|
||||
"turnover_rate": record.get("换手率", 0),
|
||||
})
|
||||
return mapped_records
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/stock_zh_index_spot")
|
||||
async def stock_zh_index_spot():
|
||||
"""
|
||||
获取股票指数实时行情
|
||||
"""
|
||||
try:
|
||||
df = ak.index_zh_a_spot_em()
|
||||
records = dataframe_to_records(df)
|
||||
mapped_records = []
|
||||
for record in records:
|
||||
mapped_records.append({
|
||||
"code": record.get("代码"),
|
||||
"name": record.get("名称"),
|
||||
"price": record.get("最新价", 0),
|
||||
"change": record.get("涨跌额", 0),
|
||||
"change_percent": record.get("涨跌幅", 0),
|
||||
"volume": record.get("成交量", 0),
|
||||
"turnover": record.get("成交额", 0),
|
||||
"open": record.get("开盘价", 0),
|
||||
"high": record.get("最高价", 0),
|
||||
"low": record.get("最低价", 0),
|
||||
"pre_close": record.get("昨收", 0),
|
||||
})
|
||||
return mapped_records
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/stock_sector_spot")
|
||||
async def stock_sector_spot():
|
||||
"""
|
||||
获取板块行情数据
|
||||
"""
|
||||
try:
|
||||
df = ak.stock_board_industry_name_em()
|
||||
records = dataframe_to_records(df)
|
||||
mapped_records = []
|
||||
for record in records:
|
||||
mapped_records.append({
|
||||
"code": record.get("代码"),
|
||||
"name": record.get("名称"),
|
||||
"change_percent": record.get("涨跌幅", 0),
|
||||
})
|
||||
return mapped_records
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/stock_sector_cons")
|
||||
async def stock_sector_cons(
|
||||
symbol: str = Query(..., description="板块名称")
|
||||
):
|
||||
"""
|
||||
获取板块成分股
|
||||
"""
|
||||
try:
|
||||
df = ak.stock_board_industry_cons_em(symbol=symbol)
|
||||
records = dataframe_to_records(df)
|
||||
mapped_records = []
|
||||
for record in records:
|
||||
mapped_records.append({
|
||||
"code": record.get("代码"),
|
||||
"name": record.get("名称"),
|
||||
"price": record.get("最新价", 0),
|
||||
"change_percent": record.get("涨跌幅", 0),
|
||||
})
|
||||
return mapped_records
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
@ -0,0 +1,20 @@
|
||||
"""
|
||||
AKShare HTTP 服务启动脚本
|
||||
Windows 用户双击运行或 python start.py 启动
|
||||
"""
|
||||
import uvicorn
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting AKShare HTTP API Server...")
|
||||
print("URL: http://localhost:8000")
|
||||
print("Press Ctrl+C to stop")
|
||||
print("-" * 50)
|
||||
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=False,
|
||||
log_level="info"
|
||||
)
|
||||
@ -0,0 +1,59 @@
|
||||
import { Router } from 'express';
|
||||
import * as adminController from '../controllers/adminController';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 所有管理员路由需要认证
|
||||
router.use(authMiddleware);
|
||||
|
||||
// ========== 系统统计 ==========
|
||||
router.get('/stats', adminController.getSystemStats);
|
||||
|
||||
// ========== AKShare 数据源 ==========
|
||||
router.get('/akshare/status', adminController.getAKShareStatus);
|
||||
router.get('/akshare/config', adminController.getAKShareConfig);
|
||||
router.put('/akshare/config', adminController.updateAKShareConfig);
|
||||
router.post('/akshare/test', adminController.testAKShareConnection);
|
||||
|
||||
// ========== 数据源管理 ==========
|
||||
router.get('/data-sources', adminController.getDataSources);
|
||||
router.put('/data-sources/:id', adminController.updateDataSource);
|
||||
router.post('/data-sources/:id/test', adminController.testDataSource);
|
||||
router.post('/data-sources/:id/sync', adminController.triggerSync);
|
||||
|
||||
// ========== 数据检测 ==========
|
||||
router.get('/data-check', adminController.getDataCheck);
|
||||
router.post('/data-check', adminController.runDataCheck);
|
||||
|
||||
// ========== 数据缓冲 ==========
|
||||
router.post('/buffer', adminController.bufferMissingData);
|
||||
router.post('/buffer/stocks', adminController.bufferStocks);
|
||||
router.post('/buffer/sectors', adminController.bufferSectors);
|
||||
router.post('/buffer/kline', adminController.bufferKLines);
|
||||
router.post('/calculate/momentum', adminController.calculateMomentum);
|
||||
|
||||
// ========== 同步任务 ==========
|
||||
router.get('/sync-tasks/:taskId', adminController.getSyncTask);
|
||||
|
||||
// ========== 用户管理 ==========
|
||||
router.get('/users', adminController.getUsers);
|
||||
router.put('/users/:userId/status', adminController.updateUserStatus);
|
||||
router.delete('/users/:userId', adminController.deleteUser);
|
||||
router.post('/users/batch', adminController.batchUpdateUsers);
|
||||
|
||||
// ========== 数据导入 ==========
|
||||
router.get('/import/tasks', adminController.getImportTasks);
|
||||
|
||||
// ========== AI 配置 ==========
|
||||
router.get('/ai-config', adminController.getAIConfig);
|
||||
router.put('/ai-config', adminController.updateAIConfig);
|
||||
router.post('/ai-config/test', adminController.testAIConnection);
|
||||
router.get('/momentum-config', adminController.getMomentumConfig);
|
||||
router.put('/momentum-config', adminController.updateMomentumConfig);
|
||||
|
||||
// ========== 数据保留策略 ==========
|
||||
router.get('/data-retention', adminController.getDataRetention);
|
||||
router.put('/data-retention', adminController.updateDataRetention);
|
||||
|
||||
export default router;
|
||||
@ -0,0 +1,460 @@
|
||||
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;
|
||||
Loading…
Reference in new issue