|
|
|
|
|
"""
|
|
|
|
|
|
同步管理 API v2
|
|
|
|
|
|
金融数据中台 - 数据同步控制接口
|
|
|
|
|
|
"""
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from typing import Annotated, List
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
|
|
|
|
|
from app.db.init_db import get_sqlite_db
|
|
|
|
|
|
from app.schemas import ResponseData
|
|
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/config", response_model=ResponseData)
|
|
|
|
|
|
async def get_sync_config(db: Session = Depends(get_sqlite_db)):
|
|
|
|
|
|
"""获取同步配置"""
|
|
|
|
|
|
from sqlalchemy import text
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = db.execute(text("""
|
|
|
|
|
|
SELECT config_key, config_value, description, updated_at
|
|
|
|
|
|
FROM sync_config
|
|
|
|
|
|
ORDER BY config_key
|
|
|
|
|
|
"""))
|
|
|
|
|
|
|
|
|
|
|
|
configs = {}
|
|
|
|
|
|
for row in result:
|
|
|
|
|
|
configs[row[0]] = {
|
|
|
|
|
|
"value": row[1],
|
|
|
|
|
|
"description": row[2],
|
|
|
|
|
|
"updated_at": row[3]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return ResponseData(
|
|
|
|
|
|
code=0,
|
|
|
|
|
|
message="success",
|
|
|
|
|
|
data={"configs": configs, "count": len(configs)}
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=500,
|
|
|
|
|
|
detail=f"Failed to get sync config: {str(e)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/config", response_model=ResponseData)
|
|
|
|
|
|
async def update_sync_config(
|
|
|
|
|
|
config: Annotated[dict, Query(description="配置项,JSON 格式")],
|
|
|
|
|
|
db: Session = Depends(get_sqlite_db)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
更新同步配置
|
|
|
|
|
|
|
|
|
|
|
|
示例:
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"sync_time": "17:00",
|
|
|
|
|
|
"sync_symbols": "IF2406,IC2406,IH2406,IM2406",
|
|
|
|
|
|
"sync_periods": "1m,5m,15m,30m,1h,1d",
|
|
|
|
|
|
"cache_ttl": "300"
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
"""
|
|
|
|
|
|
from sqlalchemy import text
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
for key, value in config.items():
|
|
|
|
|
|
db.execute(text("""
|
|
|
|
|
|
INSERT INTO sync_config (config_key, config_value, description, updated_at)
|
|
|
|
|
|
VALUES (:key, :value, :desc, :updated_at)
|
|
|
|
|
|
ON CONFLICT(config_key) DO UPDATE SET
|
|
|
|
|
|
config_value = :value,
|
|
|
|
|
|
updated_at = :updated_at
|
|
|
|
|
|
"""), {
|
|
|
|
|
|
"key": key,
|
|
|
|
|
|
"value": str(value),
|
|
|
|
|
|
"desc": f"配置项 {key}",
|
|
|
|
|
|
"updated_at": datetime.utcnow()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
|
|
|
|
|
|
return ResponseData(
|
|
|
|
|
|
code=0,
|
|
|
|
|
|
message="Config updated successfully",
|
|
|
|
|
|
data={"updated": list(config.keys())}
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
db.rollback()
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=500,
|
|
|
|
|
|
detail=f"Failed to update sync config: {str(e)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/trigger", response_model=ResponseData)
|
|
|
|
|
|
async def trigger_sync(
|
|
|
|
|
|
sync_type: Annotated[str, Query(description="同步类型:kline/realtime/all")] = "kline",
|
|
|
|
|
|
symbols: Annotated[str, Query(description="品种列表,逗号分隔,不传则使用默认")] = "",
|
|
|
|
|
|
periods: Annotated[str, Query(description="周期列表,逗号分隔,不传则使用默认")] = "",
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
手动触发同步任务
|
|
|
|
|
|
|
|
|
|
|
|
- **sync_type**: 同步类型 (kline=K 线数据,realtime=实时行情,all=全部)
|
|
|
|
|
|
- **symbols**: 品种列表 (逗号分隔,如 IF2406,IC2406)
|
|
|
|
|
|
- **periods**: 周期列表 (逗号分隔,如 1m,5m,15m)
|
|
|
|
|
|
"""
|
|
|
|
|
|
from app.services.data_sync_service import DataSyncService
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 解析品种列表
|
|
|
|
|
|
symbol_list = [s.strip() for s in symbols.split(",") if s.strip()] if symbols else None
|
|
|
|
|
|
|
|
|
|
|
|
# 解析周期列表
|
|
|
|
|
|
period_list = [p.strip() for p in periods.split(",") if p.strip()] if periods else None
|
|
|
|
|
|
|
|
|
|
|
|
if sync_type == "kline":
|
|
|
|
|
|
# 同步 K 线数据
|
|
|
|
|
|
if symbol_list:
|
|
|
|
|
|
results = []
|
|
|
|
|
|
for symbol in symbol_list:
|
|
|
|
|
|
for period in (period_list or DataSyncService.DEFAULT_PERIODS):
|
|
|
|
|
|
count = DataSyncService.sync_kline_data(symbol, period)
|
|
|
|
|
|
results.append({"symbol": symbol, "period": period, "count": count})
|
|
|
|
|
|
|
|
|
|
|
|
return ResponseData(
|
|
|
|
|
|
code=0,
|
|
|
|
|
|
message="Kline sync completed",
|
|
|
|
|
|
data={"results": results, "total": sum(r["count"] for r in results)}
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 同步所有默认品种
|
|
|
|
|
|
result = await DataSyncService.sync_all_symbols()
|
|
|
|
|
|
return ResponseData(
|
|
|
|
|
|
code=0,
|
|
|
|
|
|
message="All symbols synced",
|
|
|
|
|
|
data=result
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
elif sync_type == "realtime":
|
|
|
|
|
|
# 同步实时行情
|
|
|
|
|
|
symbol_list = symbol_list or DataSyncService.DEFAULT_SYMBOLS
|
|
|
|
|
|
count = DataSyncService.sync_realtime_quotes(symbol_list)
|
|
|
|
|
|
return ResponseData(
|
|
|
|
|
|
code=0,
|
|
|
|
|
|
message="Realtime quotes synced",
|
|
|
|
|
|
data={"count": count, "symbols": symbol_list}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
elif sync_type == "all":
|
|
|
|
|
|
# 同步全部
|
|
|
|
|
|
kline_result = await DataSyncService.sync_all_symbols()
|
|
|
|
|
|
realtime_count = DataSyncService.sync_realtime_quotes(
|
|
|
|
|
|
symbol_list or DataSyncService.DEFAULT_SYMBOLS
|
|
|
|
|
|
)
|
|
|
|
|
|
return ResponseData(
|
|
|
|
|
|
code=0,
|
|
|
|
|
|
message="All sync completed",
|
|
|
|
|
|
data={
|
|
|
|
|
|
"kline": kline_result,
|
|
|
|
|
|
"realtime": {"count": realtime_count}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=400,
|
|
|
|
|
|
detail=f"Invalid sync_type: {sync_type}. Must be 'kline', 'realtime', or 'all'"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
|
raise
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=500,
|
|
|
|
|
|
detail=f"Failed to trigger sync: {str(e)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/logs", response_model=ResponseData)
|
|
|
|
|
|
async def get_sync_logs(
|
|
|
|
|
|
sync_type: Annotated[str, Query(description="同步类型:kline/realtime/all")] = "",
|
|
|
|
|
|
symbol: Annotated[str, Query(description="品种代码")] = "",
|
|
|
|
|
|
status: Annotated[str, Query(description="状态:success/failed/all")] = "all",
|
|
|
|
|
|
limit: Annotated[int, Query(description="返回数量,默认 50,最大 200")] = 50,
|
|
|
|
|
|
db: Session = Depends(get_sqlite_db)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""查询同步日志"""
|
|
|
|
|
|
from sqlalchemy import text
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 限制 limit 最大值
|
|
|
|
|
|
if limit > 200:
|
|
|
|
|
|
limit = 200
|
|
|
|
|
|
if limit < 1:
|
|
|
|
|
|
limit = 1
|
|
|
|
|
|
|
|
|
|
|
|
# 构建查询
|
|
|
|
|
|
query = "SELECT * FROM sync_log WHERE 1=1"
|
|
|
|
|
|
params = {}
|
|
|
|
|
|
|
|
|
|
|
|
if sync_type:
|
|
|
|
|
|
query += " AND sync_type = :sync_type"
|
|
|
|
|
|
params["sync_type"] = sync_type
|
|
|
|
|
|
|
|
|
|
|
|
if symbol:
|
|
|
|
|
|
query += " AND symbol = :symbol"
|
|
|
|
|
|
params["symbol"] = symbol
|
|
|
|
|
|
|
|
|
|
|
|
if status != "all":
|
|
|
|
|
|
query += " AND status = :status"
|
|
|
|
|
|
params["status"] = status
|
|
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY start_time DESC LIMIT :limit"
|
|
|
|
|
|
params["limit"] = limit
|
|
|
|
|
|
|
|
|
|
|
|
result = db.execute(text(query), params)
|
|
|
|
|
|
|
|
|
|
|
|
logs = []
|
|
|
|
|
|
for row in result:
|
|
|
|
|
|
logs.append({
|
|
|
|
|
|
"id": row[0],
|
|
|
|
|
|
"sync_type": row[1],
|
|
|
|
|
|
"symbol": row[2],
|
|
|
|
|
|
"period": row[3],
|
|
|
|
|
|
"start_time": row[4],
|
|
|
|
|
|
"end_time": row[5],
|
|
|
|
|
|
"status": row[6],
|
|
|
|
|
|
"records_synced": row[7],
|
|
|
|
|
|
"error_message": row[8],
|
|
|
|
|
|
"created_at": row[9]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return ResponseData(
|
|
|
|
|
|
code=0,
|
|
|
|
|
|
message="success",
|
|
|
|
|
|
data={"logs": logs, "count": len(logs)}
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=500,
|
|
|
|
|
|
detail=f"Failed to get sync logs: {str(e)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/status", response_model=ResponseData)
|
|
|
|
|
|
async def get_sync_status():
|
|
|
|
|
|
"""获取同步状态"""
|
|
|
|
|
|
from app.tasks.sync_tasks import get_scheduler
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
scheduler = get_scheduler()
|
|
|
|
|
|
|
|
|
|
|
|
# 获取所有定时任务
|
|
|
|
|
|
jobs = scheduler.get_jobs()
|
|
|
|
|
|
job_status = []
|
|
|
|
|
|
for job in jobs:
|
|
|
|
|
|
job_status.append({
|
|
|
|
|
|
"id": job.id,
|
|
|
|
|
|
"name": job.name,
|
|
|
|
|
|
"next_run": str(job.next_run_time) if job.next_run_time else None,
|
|
|
|
|
|
"trigger": str(job.trigger)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return ResponseData(
|
|
|
|
|
|
code=0,
|
|
|
|
|
|
message="success",
|
|
|
|
|
|
data={
|
|
|
|
|
|
"scheduler_running": scheduler.running,
|
|
|
|
|
|
"jobs": job_status,
|
|
|
|
|
|
"job_count": len(jobs)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=500,
|
|
|
|
|
|
detail=f"Failed to get sync status: {str(e)}"
|
|
|
|
|
|
)
|