|
|
|
|
|
"""管理后台API路由 - 对应Go的api/admin_router.go"""
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Header, Query, Body
|
|
|
|
|
|
from typing import Optional, List
|
|
|
|
|
|
|
|
|
|
|
|
from app.models import (
|
|
|
|
|
|
Response, ConfigListRequest, ConfigUpdateRequest,
|
|
|
|
|
|
ReloadRequest, AdapterToggleRequest, AdapterConfigUpdateRequest,
|
|
|
|
|
|
APITestRequest, WSTestRequest, TestHistoryRequest,
|
|
|
|
|
|
DataSyncRequest, DataSyncType, DataSyncData, DataSyncResult
|
|
|
|
|
|
)
|
|
|
|
|
|
from app.services import ConfigService, AdapterService, TestService
|
|
|
|
|
|
from app.core.config import get_config
|
|
|
|
|
|
|
|
|
|
|
|
admin_router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
# 服务实例
|
|
|
|
|
|
config_service = ConfigService()
|
|
|
|
|
|
adapter_service = AdapterService()
|
|
|
|
|
|
test_service = TestService()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def verify_admin_token(x_admin_token: Optional[str] = Header(None)):
|
|
|
|
|
|
"""验证Admin Token"""
|
|
|
|
|
|
# TODO: 实现Token验证
|
|
|
|
|
|
return x_admin_token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# 系统管理接口
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.get("/admin/system/status", response_model=Response)
|
|
|
|
|
|
def get_system_status(
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""获取系统状态"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
data = config_service.get_system_status()
|
|
|
|
|
|
return Response(code=0, message="success", data=data)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.post("/admin/system/reload", response_model=Response)
|
|
|
|
|
|
def reload_config(
|
|
|
|
|
|
req: Optional[ReloadRequest] = None,
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""热加载配置"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if req is None:
|
|
|
|
|
|
req = ReloadRequest()
|
|
|
|
|
|
data = config_service.reload_config(req)
|
|
|
|
|
|
return Response(code=0, message="success", data=data)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.post("/admin/system/restart", response_model=Response)
|
|
|
|
|
|
def restart_service(
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""重启服务
|
|
|
|
|
|
|
|
|
|
|
|
注意: 此方法通过创建子进程实现服务重启,适用于开发环境。
|
|
|
|
|
|
生产环境建议使用Docker或systemd管理服务生命周期。
|
|
|
|
|
|
"""
|
|
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
|
|
def delayed_restart():
|
|
|
|
|
|
"""延迟重启函数"""
|
|
|
|
|
|
time.sleep(2) # 等待当前响应返回
|
|
|
|
|
|
|
|
|
|
|
|
# 获取当前Python解释器和启动参数
|
|
|
|
|
|
python = sys.executable
|
|
|
|
|
|
args = sys.argv[:]
|
|
|
|
|
|
|
|
|
|
|
|
# 在Windows上使用start命令,在Linux上使用nohup
|
|
|
|
|
|
if os.name == 'nt': # Windows
|
|
|
|
|
|
subprocess.Popen(
|
|
|
|
|
|
['start', 'python'] + args,
|
|
|
|
|
|
shell=True,
|
|
|
|
|
|
creationflags=subprocess.CREATE_NEW_CONSOLE
|
|
|
|
|
|
)
|
|
|
|
|
|
else: # Linux/Mac
|
|
|
|
|
|
subprocess.Popen(
|
|
|
|
|
|
[python] + args,
|
|
|
|
|
|
stdout=open('/dev/null', 'w'),
|
|
|
|
|
|
stderr=open('/dev/null', 'w'),
|
|
|
|
|
|
start_new_session=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 退出当前进程
|
|
|
|
|
|
os._exit(0)
|
|
|
|
|
|
|
|
|
|
|
|
# 在后台线程中执行重启
|
|
|
|
|
|
restart_thread = threading.Thread(target=delayed_restart, daemon=True)
|
|
|
|
|
|
restart_thread.start()
|
|
|
|
|
|
|
|
|
|
|
|
return Response(
|
|
|
|
|
|
code=0,
|
|
|
|
|
|
message="服务将在2秒后重启",
|
|
|
|
|
|
data={"status": "restarting", "delay_seconds": 2}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# 配置管理接口
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.get("/admin/config", response_model=Response)
|
|
|
|
|
|
def get_config_list(
|
|
|
|
|
|
type: Optional[str] = Query(None, description="配置类型筛选"),
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""获取配置列表"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.models import ConfigType
|
|
|
|
|
|
req = ConfigListRequest()
|
|
|
|
|
|
if type:
|
|
|
|
|
|
req.type = ConfigType(type)
|
|
|
|
|
|
|
|
|
|
|
|
data = config_service.get_config_list(req)
|
|
|
|
|
|
return Response(code=0, message="success", data=data)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.put("/admin/config", response_model=Response)
|
|
|
|
|
|
def update_config(
|
|
|
|
|
|
req: ConfigUpdateRequest,
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""更新配置"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
data = config_service.update_config(req)
|
|
|
|
|
|
return Response(code=0, message="success", data=data)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.post("/admin/config/reload", response_model=Response)
|
|
|
|
|
|
def reload_config_endpoint(
|
|
|
|
|
|
req: Optional[ReloadRequest] = None,
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""热加载配置"""
|
|
|
|
|
|
return reload_config(req, token)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# 适配器管理接口
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.get("/admin/adapters", response_model=Response)
|
|
|
|
|
|
def get_adapter_list(
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""获取适配器列表"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
data = adapter_service.get_adapter_list()
|
|
|
|
|
|
return Response(code=0, message="success", data=data)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.post("/admin/adapters/toggle", response_model=Response)
|
|
|
|
|
|
def toggle_adapter(
|
|
|
|
|
|
req: AdapterToggleRequest,
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""切换适配器状态"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
adapter_service.toggle_adapter(req)
|
|
|
|
|
|
return Response(code=0, message="success")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.put("/admin/adapters/config", response_model=Response)
|
|
|
|
|
|
def update_adapter_config(
|
|
|
|
|
|
req: AdapterConfigUpdateRequest,
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""更新适配器配置"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
adapter_service.update_adapter_config(req)
|
|
|
|
|
|
return Response(code=0, message="success")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# 测试管理接口
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.get("/admin/tests/api", response_model=Response)
|
|
|
|
|
|
def get_api_test_list(
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""获取API测试列表"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
data = test_service.get_api_test_list()
|
|
|
|
|
|
# 设置基础URL
|
|
|
|
|
|
config = get_config()
|
|
|
|
|
|
data.base_url = f"http://localhost:{config.server.port}"
|
|
|
|
|
|
return Response(code=0, message="success", data=data)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.post("/admin/tests/api/run", response_model=Response)
|
|
|
|
|
|
async def run_api_test(
|
|
|
|
|
|
req: APITestRequest,
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""执行API测试"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
config = get_config()
|
|
|
|
|
|
base_url = f"http://localhost:{config.server.port}"
|
|
|
|
|
|
data = await test_service.run_api_test(base_url, req)
|
|
|
|
|
|
return Response(code=0, message="success", data=data)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.get("/admin/tests/ws", response_model=Response)
|
|
|
|
|
|
def get_ws_test_list(
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""获取WebSocket测试列表"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
data = test_service.get_ws_test_list()
|
|
|
|
|
|
config = get_config()
|
|
|
|
|
|
data.ws_url = f"ws://localhost:{config.server.port}/v1/stream"
|
|
|
|
|
|
return Response(code=0, message="success", data=data)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.post("/admin/tests/ws/run", response_model=Response)
|
|
|
|
|
|
async def run_ws_test(
|
|
|
|
|
|
req: WSTestRequest,
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""执行WebSocket测试"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
config = get_config()
|
|
|
|
|
|
ws_url = f"ws://localhost:{config.server.port}/v1/stream"
|
|
|
|
|
|
data = await test_service.run_ws_test(ws_url, req)
|
|
|
|
|
|
return Response(code=0, message="success", data=data)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.get("/admin/tests/history", response_model=Response)
|
|
|
|
|
|
def get_test_history(
|
|
|
|
|
|
type: Optional[str] = Query(None, description="测试类型"),
|
|
|
|
|
|
limit: int = Query(default=20, ge=1, le=100, description="数量限制"),
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""获取测试历史"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
req = TestHistoryRequest(type=type, limit=limit)
|
|
|
|
|
|
data = test_service.get_test_history(req)
|
|
|
|
|
|
return Response(code=0, message="success", data=data)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.get("/admin/tests/internal", response_model=Response)
|
|
|
|
|
|
def get_internal_test_list(
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""获取内部接口测试列表(SDK封装层)"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
data = test_service.get_internal_test_list()
|
|
|
|
|
|
return Response(code=0, message="success", data=data)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.post("/admin/tests/internal/run", response_model=Response)
|
|
|
|
|
|
async def run_internal_test(
|
|
|
|
|
|
req: APITestRequest,
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""执行内部接口测试(SDK封装层)"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 获取股票adapter
|
|
|
|
|
|
adapter = adapter_service.get_active_adapter("stock")
|
|
|
|
|
|
if not adapter:
|
|
|
|
|
|
# 尝试连接adapter
|
|
|
|
|
|
config = get_config()
|
|
|
|
|
|
active_name = config.sources.stock.active
|
|
|
|
|
|
await adapter_service._connect_adapter(active_name)
|
|
|
|
|
|
adapter = adapter_service.get_active_adapter("stock")
|
|
|
|
|
|
if not adapter:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail="Stock adapter not available")
|
|
|
|
|
|
|
|
|
|
|
|
# 确保adapter已连接
|
|
|
|
|
|
if not hasattr(adapter, '_is_logged_in') or not adapter._is_logged_in:
|
|
|
|
|
|
if hasattr(adapter, 'config') and adapter.config:
|
|
|
|
|
|
# 使用已保存的配置重新连接
|
|
|
|
|
|
config_dict = {
|
|
|
|
|
|
'username': adapter.config.username,
|
|
|
|
|
|
'password': adapter.config.password,
|
|
|
|
|
|
'host': adapter.config.host,
|
|
|
|
|
|
'port': adapter.config.port,
|
|
|
|
|
|
'local_path': adapter.config.local_path,
|
|
|
|
|
|
'use_local_cache': adapter.config.use_local_cache
|
|
|
|
|
|
}
|
|
|
|
|
|
await adapter.connect(config_dict)
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise HTTPException(status_code=500, detail="Adapter not configured")
|
|
|
|
|
|
|
|
|
|
|
|
data = await test_service.run_internal_test(adapter, req)
|
|
|
|
|
|
return Response(code=0, message="success", data=data)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
error(f"Internal test error: {e}\n{traceback.format_exc()}")
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# 数据同步接口
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.post("/admin/data/sync", response_model=Response)
|
|
|
|
|
|
async def sync_data(
|
|
|
|
|
|
req: DataSyncRequest,
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""手动触发数据同步
|
|
|
|
|
|
|
|
|
|
|
|
支持同步类型:
|
|
|
|
|
|
- base: 基础K线数据 (OHLCV)
|
|
|
|
|
|
- quote: 行情指标数据 (均线、MACD、涨跌停等)
|
|
|
|
|
|
- finance: 财务数据 (市值、股本、利润等)
|
|
|
|
|
|
- full: 全量同步 (以上全部)
|
|
|
|
|
|
|
|
|
|
|
|
示例请求:
|
|
|
|
|
|
{
|
|
|
|
|
|
"symbols": ["600519.SH", "000001.SZ"],
|
|
|
|
|
|
"sync_type": "full",
|
|
|
|
|
|
"start_date": "20240101",
|
|
|
|
|
|
"end_date": "20240301",
|
|
|
|
|
|
"asset_class": "stock"
|
|
|
|
|
|
}
|
|
|
|
|
|
"""
|
|
|
|
|
|
import time
|
|
|
|
|
|
from app.services.adapter_service import AdapterService
|
|
|
|
|
|
from app.services.data_sync_service import DataSyncService
|
|
|
|
|
|
from app.models import Frequency
|
|
|
|
|
|
|
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 获取适配器
|
|
|
|
|
|
adapter_service = AdapterService()
|
|
|
|
|
|
adapter = adapter_service.get_active_adapter(req.asset_class)
|
|
|
|
|
|
|
|
|
|
|
|
if not adapter:
|
|
|
|
|
|
return Response(
|
|
|
|
|
|
code=400,
|
|
|
|
|
|
message=f"No active adapter found for asset class: {req.asset_class}",
|
|
|
|
|
|
data=None
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建同步服务
|
|
|
|
|
|
sync_service = DataSyncService(adapter)
|
|
|
|
|
|
|
|
|
|
|
|
# 设置默认日期
|
|
|
|
|
|
end_date = req.end_date or datetime.now().strftime("%Y%m%d")
|
|
|
|
|
|
start_date = req.start_date or (datetime.now() - timedelta(days=365)).strftime("%Y%m%d")
|
|
|
|
|
|
|
|
|
|
|
|
# 根据同步类型执行同步
|
|
|
|
|
|
base_results = []
|
|
|
|
|
|
quote_results = []
|
|
|
|
|
|
finance_results = []
|
|
|
|
|
|
|
|
|
|
|
|
if req.sync_type == DataSyncType.BASE or req.sync_type == DataSyncType.FULL:
|
|
|
|
|
|
freq = Frequency.FREQ_1D if req.freq == "1d" else Frequency.FREQ_1D
|
|
|
|
|
|
base_counts = await sync_service.sync_kline_base(
|
|
|
|
|
|
req.symbols, freq, start_date, end_date
|
|
|
|
|
|
)
|
|
|
|
|
|
base_results = [
|
|
|
|
|
|
DataSyncResult(symbol=k, count=v if v > 0 else 0, error=str(e) if v < 0 else None)
|
|
|
|
|
|
for k, v in base_counts.items()
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
if req.sync_type == DataSyncType.QUOTE or req.sync_type == DataSyncType.FULL:
|
|
|
|
|
|
quote_counts = await sync_service.sync_kline_quote(
|
|
|
|
|
|
req.symbols, start_date, end_date
|
|
|
|
|
|
)
|
|
|
|
|
|
quote_results = [
|
|
|
|
|
|
DataSyncResult(symbol=k, count=v if v > 0 else 0, error=str(e) if v < 0 else None)
|
|
|
|
|
|
for k, v in quote_counts.items()
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
if req.sync_type == DataSyncType.FINANCE or req.sync_type == DataSyncType.FULL:
|
|
|
|
|
|
finance_counts = await sync_service.sync_kline_finance(
|
|
|
|
|
|
req.symbols, start_date, end_date
|
|
|
|
|
|
)
|
|
|
|
|
|
finance_results = [
|
|
|
|
|
|
DataSyncResult(symbol=k, count=v if v > 0 else 0, error=str(e) if v < 0 else None)
|
|
|
|
|
|
for k, v in finance_counts.items()
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
total_time = int((time.time() - start_time) * 1000)
|
|
|
|
|
|
|
|
|
|
|
|
# 统计成功/失败数量
|
|
|
|
|
|
all_results = base_results + quote_results + finance_results
|
|
|
|
|
|
success_count = sum(1 for r in all_results if r.count > 0)
|
|
|
|
|
|
fail_count = sum(1 for r in all_results if r.error)
|
|
|
|
|
|
|
|
|
|
|
|
data = DataSyncData(
|
|
|
|
|
|
success=fail_count == 0,
|
|
|
|
|
|
message=f"Synced {success_count} symbols, {fail_count} failed, took {total_time}ms",
|
|
|
|
|
|
sync_type=req.sync_type,
|
|
|
|
|
|
base_results=base_results,
|
|
|
|
|
|
quote_results=quote_results,
|
|
|
|
|
|
finance_results=finance_results,
|
|
|
|
|
|
total_time_ms=total_time
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return Response(code=0, message="success", data=data)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error(f"[DataSync API] Failed: {e}")
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin_router.post("/admin/data/sync/incremental", response_model=Response)
|
|
|
|
|
|
async def sync_incremental(
|
|
|
|
|
|
symbols: List[str] = Body(..., description="标的代码列表"),
|
|
|
|
|
|
asset_class: str = Body("stock", description="资产类别"),
|
|
|
|
|
|
token: str = Depends(verify_admin_token)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""触发增量同步(最近30天)
|
|
|
|
|
|
|
|
|
|
|
|
用于每日定时任务,同步最近的数据
|
|
|
|
|
|
"""
|
|
|
|
|
|
from app.services.adapter_service import AdapterService
|
|
|
|
|
|
from app.services.data_sync_service import DataSyncService
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 获取适配器
|
|
|
|
|
|
adapter_service = AdapterService()
|
|
|
|
|
|
adapter = adapter_service.get_active_adapter(asset_class)
|
|
|
|
|
|
|
|
|
|
|
|
if not adapter:
|
|
|
|
|
|
return Response(
|
|
|
|
|
|
code=400,
|
|
|
|
|
|
message=f"No active adapter found for asset class: {asset_class}",
|
|
|
|
|
|
data=None
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建同步服务
|
|
|
|
|
|
sync_service = DataSyncService(adapter)
|
|
|
|
|
|
|
|
|
|
|
|
# 执行增量同步
|
|
|
|
|
|
results = await sync_service.sync_daily_incremental(symbols)
|
|
|
|
|
|
|
|
|
|
|
|
return Response(code=0, message="success", data=results)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error(f"[DataSync API] Incremental sync failed: {e}")
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|