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