commit
654d641547
@ -0,0 +1,63 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite3
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Frontend
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
.npm
|
||||
.yarn
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@ -0,0 +1,26 @@
|
||||
# 后端Dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
postgresql-client \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装Python依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制应用代码
|
||||
COPY app/ ./app/
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8000
|
||||
|
||||
# 启动命令
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@ -0,0 +1 @@
|
||||
# AmazingData金融数据服务平台 - 后端应用
|
||||
@ -0,0 +1 @@
|
||||
# API模块
|
||||
@ -0,0 +1,16 @@
|
||||
# API v1模块
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1 import auth, configs, base_data, stock, future, realtime, finance, cache, test
|
||||
|
||||
api_router = APIRouter(prefix="/api/v1")
|
||||
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["认证"])
|
||||
api_router.include_router(configs.router, prefix="/configs", tags=["配置管理"])
|
||||
api_router.include_router(base_data.router, prefix="/base", tags=["基础数据"])
|
||||
api_router.include_router(stock.router, prefix="/stock", tags=["股票数据"])
|
||||
api_router.include_router(future.router, prefix="/future", tags=["期货数据"])
|
||||
api_router.include_router(realtime.router, prefix="/realtime", tags=["实时数据"])
|
||||
api_router.include_router(finance.router, prefix="/finance", tags=["财务数据"])
|
||||
api_router.include_router(cache.router, prefix="/cache", tags=["缓存管理"])
|
||||
api_router.include_router(test.router, prefix="/test", tags=["测试中心"])
|
||||
@ -0,0 +1,101 @@
|
||||
"""
|
||||
基础数据路由
|
||||
"""
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.schemas.base import ResponseModel
|
||||
from app.services.base_data_service import BaseDataService
|
||||
from app.core.security import get_current_user
|
||||
from app.models.user import User
|
||||
from app.utils.date_utils import parse_date
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/codes", response_model=ResponseModel)
|
||||
async def get_code_list(
|
||||
security_type: str = Query(..., description="证券类型: EXTRA_STOCK_A, EXTRA_FUTURE, EXTRA_ETF, EXTRA_INDEX_A"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取代码列表"""
|
||||
service = BaseDataService(db)
|
||||
codes = service.get_code_list(security_type)
|
||||
return ResponseModel(data={"codes": codes[:100]}) # 限制返回数量
|
||||
|
||||
|
||||
@router.get("/codes/{code}/info", response_model=ResponseModel)
|
||||
async def get_code_info(
|
||||
code: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取证券信息"""
|
||||
service = BaseDataService(db)
|
||||
# 根据代码判断证券类型
|
||||
security_type = service.get_security_type(code)
|
||||
|
||||
# 获取对应类型的代码信息
|
||||
if security_type == "stock":
|
||||
info = service.get_code_info("EXTRA_STOCK_A")
|
||||
elif security_type == "future":
|
||||
info = service.get_code_info("EXTRA_FUTURE")
|
||||
else:
|
||||
info = None
|
||||
|
||||
if info is not None and not info.empty:
|
||||
code_info = info[info.get("code") == code]
|
||||
if not code_info.empty:
|
||||
return ResponseModel(data=code_info.to_dict("records")[0])
|
||||
|
||||
return ResponseModel(data={"code": code, "security_type": security_type})
|
||||
|
||||
|
||||
@router.get("/calendar", response_model=ResponseModel)
|
||||
async def get_trading_calendar(
|
||||
market: str = Query("SH", description="市场: SH, SZ, CFE"),
|
||||
start_date: str = Query(..., description="开始日期(YYYYMMDD)"),
|
||||
end_date: str = Query(..., description="结束日期(YYYYMMDD)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取交易日历"""
|
||||
service = BaseDataService(db)
|
||||
start = parse_date(start_date)
|
||||
end = parse_date(end_date)
|
||||
calendar = service.get_trading_calendar(market, start, end)
|
||||
|
||||
return ResponseModel(data={
|
||||
"market": market,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"trading_days": [d.isoformat() for d in calendar],
|
||||
"count": len(calendar)
|
||||
})
|
||||
|
||||
|
||||
@router.get("/calendar/trading-days", response_model=ResponseModel)
|
||||
async def get_trading_days(
|
||||
market: str = Query("SH", description="市场: SH, SZ, CFE"),
|
||||
days: int = Query(30, description="最近交易日数量"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取最近交易日列表"""
|
||||
from datetime import date, timedelta
|
||||
|
||||
service = BaseDataService(db)
|
||||
end_date = date.today()
|
||||
start_date = end_date - timedelta(days=days * 2) # 获取更多天数以确保有足够交易日
|
||||
|
||||
calendar = service.get_trading_calendar(market, start_date, end_date)
|
||||
recent_days = calendar[-days:] if len(calendar) > days else calendar
|
||||
|
||||
return ResponseModel(data={
|
||||
"market": market,
|
||||
"trading_days": [d.isoformat() for d in recent_days],
|
||||
"count": len(recent_days)
|
||||
})
|
||||
@ -0,0 +1,169 @@
|
||||
"""
|
||||
缓存管理路由
|
||||
"""
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.schemas.base import ResponseModel, PaginatedResponse
|
||||
from app.schemas.cache import (
|
||||
DetectMissingRequest, DetectMissingResponse,
|
||||
BatchCacheRequest, CacheTaskResponse, CacheStatusResponse
|
||||
)
|
||||
from app.services.cache_service import CacheService
|
||||
from app.core.security import get_current_user
|
||||
from app.models.user import User
|
||||
from app.utils.date_utils import parse_date
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/detect-missing", response_model=ResponseModel)
|
||||
async def detect_missing_data(
|
||||
request: DetectMissingRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""检测缺失数据"""
|
||||
service = CacheService(db)
|
||||
start = parse_date(request.start_date)
|
||||
end = parse_date(request.end_date)
|
||||
|
||||
task = service.detect_missing_data(
|
||||
request.security_type,
|
||||
request.period_type,
|
||||
start,
|
||||
end,
|
||||
request.code_list
|
||||
)
|
||||
|
||||
# 获取缺失详情
|
||||
details = service.get_task_details(task.id)
|
||||
missing_codes = [d for d in details if d.is_missing]
|
||||
|
||||
missing_info = []
|
||||
for code in request.code_list:
|
||||
code_details = [d for d in details if d.code == code and d.is_missing]
|
||||
if code_details:
|
||||
missing_info.append({
|
||||
"code": code,
|
||||
"missing_dates": [{
|
||||
"date": format_date(d.trade_date),
|
||||
"expected": d.expected_count,
|
||||
"actual": d.actual_count,
|
||||
"missing_ratio": (d.expected_count - d.actual_count) / d.expected_count if d.expected_count > 0 else 0
|
||||
} for d in code_details]
|
||||
})
|
||||
|
||||
return ResponseModel(data={
|
||||
"task_id": task.id,
|
||||
"total_codes": len(request.code_list),
|
||||
"missing_codes": missing_info
|
||||
})
|
||||
|
||||
|
||||
@router.post("/batch-cache", response_model=ResponseModel[CacheTaskResponse])
|
||||
async def batch_cache_data(
|
||||
request: BatchCacheRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""批量缓存数据"""
|
||||
service = CacheService(db)
|
||||
start = parse_date(request.start_date)
|
||||
end = parse_date(request.end_date)
|
||||
|
||||
task = service.batch_cache_data(
|
||||
request.security_type,
|
||||
request.period_type,
|
||||
start,
|
||||
end,
|
||||
request.code_list
|
||||
)
|
||||
|
||||
return ResponseModel(data=CacheTaskResponse.model_validate(task))
|
||||
|
||||
|
||||
@router.get("/tasks", response_model=ResponseModel)
|
||||
async def get_cache_tasks(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取缓存任务列表"""
|
||||
service = CacheService(db)
|
||||
result = service.get_tasks(page, page_size)
|
||||
|
||||
return ResponseModel(data={
|
||||
"items": [CacheTaskResponse.model_validate(t) for t in result["items"]],
|
||||
"total": result["total"],
|
||||
"page": result["page"],
|
||||
"page_size": result["page_size"],
|
||||
"total_pages": result["total_pages"]
|
||||
})
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}", response_model=ResponseModel)
|
||||
async def get_cache_task(
|
||||
task_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取缓存任务详情"""
|
||||
service = CacheService(db)
|
||||
task = service.get_task(task_id)
|
||||
|
||||
if not task:
|
||||
return ResponseModel(code=404, message="任务不存在")
|
||||
|
||||
details = service.get_task_details(task_id)
|
||||
|
||||
return ResponseModel(data={
|
||||
"task": CacheTaskResponse.model_validate(task),
|
||||
"details": [{
|
||||
"id": d.id,
|
||||
"code": d.code,
|
||||
"trade_date": d.trade_date.isoformat() if d.trade_date else None,
|
||||
"expected_count": d.expected_count,
|
||||
"actual_count": d.actual_count,
|
||||
"is_missing": bool(d.is_missing),
|
||||
"status": d.status,
|
||||
"error_message": d.error_message
|
||||
} for d in details]
|
||||
})
|
||||
|
||||
|
||||
@router.delete("/tasks/{task_id}", response_model=ResponseModel)
|
||||
async def cancel_cache_task(
|
||||
task_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""取消缓存任务"""
|
||||
service = CacheService(db)
|
||||
success = service.cancel_task(task_id)
|
||||
|
||||
if success:
|
||||
return ResponseModel(message="任务已取消")
|
||||
else:
|
||||
return ResponseModel(code=400, message="任务不存在或已完成")
|
||||
|
||||
|
||||
@router.get("/status/{code}", response_model=ResponseModel)
|
||||
async def get_cache_status(
|
||||
code: str,
|
||||
security_type: str = Query("stock", description="证券类型: stock, future"),
|
||||
period_type: str = Query("daily", description="周期类型: daily, min1, min5, etc."),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取代码缓存状态"""
|
||||
service = CacheService(db)
|
||||
status = service.get_cache_status(code, security_type, period_type)
|
||||
|
||||
return ResponseModel(data=status)
|
||||
|
||||
|
||||
from app.utils.date_utils import format_date
|
||||
@ -0,0 +1,113 @@
|
||||
"""
|
||||
财务数据路由
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.schemas.base import ResponseModel
|
||||
from app.services.finance_service import FinanceService
|
||||
from app.core.security import get_current_user
|
||||
from app.models.user import User
|
||||
from app.utils.date_utils import parse_date
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/balance-sheet", response_model=ResponseModel)
|
||||
async def get_balance_sheet(
|
||||
codes: str = Query(..., description="股票代码,多个用逗号分隔"),
|
||||
start_date: str = Query(..., description="开始报告期(YYYYMMDD)"),
|
||||
end_date: str = Query(..., description="结束报告期(YYYYMMDD)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取资产负债表"""
|
||||
service = FinanceService(db)
|
||||
code_list = [c.strip() for c in codes.split(",")]
|
||||
start = parse_date(start_date)
|
||||
end = parse_date(end_date)
|
||||
|
||||
data = service.get_balance_sheet(code_list, start, end)
|
||||
return ResponseModel(data=data)
|
||||
|
||||
|
||||
@router.get("/cash-flow", response_model=ResponseModel)
|
||||
async def get_cash_flow(
|
||||
codes: str = Query(..., description="股票代码,多个用逗号分隔"),
|
||||
start_date: str = Query(..., description="开始报告期(YYYYMMDD)"),
|
||||
end_date: str = Query(..., description="结束报告期(YYYYMMDD)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取现金流量表"""
|
||||
service = FinanceService(db)
|
||||
code_list = [c.strip() for c in codes.split(",")]
|
||||
start = parse_date(start_date)
|
||||
end = parse_date(end_date)
|
||||
|
||||
data = service.get_cash_flow(code_list, start, end)
|
||||
return ResponseModel(data=data)
|
||||
|
||||
|
||||
@router.get("/income", response_model=ResponseModel)
|
||||
async def get_income(
|
||||
codes: str = Query(..., description="股票代码,多个用逗号分隔"),
|
||||
start_date: str = Query(..., description="开始报告期(YYYYMMDD)"),
|
||||
end_date: str = Query(..., description="结束报告期(YYYYMMDD)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取利润表"""
|
||||
service = FinanceService(db)
|
||||
code_list = [c.strip() for c in codes.split(",")]
|
||||
start = parse_date(start_date)
|
||||
end = parse_date(end_date)
|
||||
|
||||
data = service.get_income_statement(code_list, start, end)
|
||||
return ResponseModel(data=data)
|
||||
|
||||
|
||||
@router.get("/profit-express", response_model=ResponseModel)
|
||||
async def get_profit_express(
|
||||
codes: str = Query(..., description="股票代码,多个用逗号分隔"),
|
||||
start_date: str = Query(..., description="开始报告期(YYYYMMDD)"),
|
||||
end_date: str = Query(..., description="结束报告期(YYYYMMDD)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取业绩快报"""
|
||||
service = FinanceService(db)
|
||||
code_list = [c.strip() for c in codes.split(",")]
|
||||
start = parse_date(start_date)
|
||||
end = parse_date(end_date)
|
||||
|
||||
# 从SDK获取业绩快报
|
||||
try:
|
||||
adapter = service.base_service._get_adapter()
|
||||
data = adapter.get_profit_express(code_list, start, end)
|
||||
return ResponseModel(data=data.to_dict("records") if not data.empty else [])
|
||||
except Exception as e:
|
||||
return ResponseModel(data=[], message=str(e))
|
||||
|
||||
|
||||
@router.get("/profit-notice", response_model=ResponseModel)
|
||||
async def get_profit_notice(
|
||||
codes: str = Query(..., description="股票代码,多个用逗号分隔"),
|
||||
start_date: str = Query(..., description="开始报告期(YYYYMMDD)"),
|
||||
end_date: str = Query(..., description="结束报告期(YYYYMMDD)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取业绩预告"""
|
||||
service = FinanceService(db)
|
||||
code_list = [c.strip() for c in codes.split(",")]
|
||||
start = parse_date(start_date)
|
||||
end = parse_date(end_date)
|
||||
|
||||
try:
|
||||
adapter = service.base_service._get_adapter()
|
||||
data = adapter.get_profit_notice(code_list, start, end)
|
||||
return ResponseModel(data=data.to_dict("records") if not data.empty else [])
|
||||
except Exception as e:
|
||||
return ResponseModel(data=[], message=str(e))
|
||||
@ -0,0 +1,109 @@
|
||||
"""
|
||||
实时数据路由
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.schemas.base import ResponseModel
|
||||
from app.services.realtime_service import RealtimeService
|
||||
from app.core.security import get_current_user
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/snapshot", response_model=ResponseModel)
|
||||
async def get_realtime_snapshot(
|
||||
codes: str = Query(..., description="代码列表,多个用逗号分隔"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取最新快照数据"""
|
||||
service = RealtimeService(db)
|
||||
code_list = [c.strip() for c in codes.split(",")]
|
||||
data = service.get_latest_snapshot(code_list)
|
||||
return ResponseModel(data=data)
|
||||
|
||||
|
||||
@router.post("/subscribe", response_model=ResponseModel)
|
||||
async def subscribe_realtime(
|
||||
codes: str = Query(..., description="代码列表,多个用逗号分隔"),
|
||||
types: str = Query("snapshot", description="订阅类型: snapshot, kline"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""开始实时数据订阅"""
|
||||
code_list = [c.strip() for c in codes.split(",")]
|
||||
return ResponseModel(data={
|
||||
"message": "订阅成功",
|
||||
"codes": code_list,
|
||||
"types": types.split(",")
|
||||
})
|
||||
|
||||
|
||||
@router.delete("/subscribe", response_model=ResponseModel)
|
||||
async def unsubscribe_realtime(
|
||||
codes: str = Query(None, description="代码列表,多个用逗号分隔,为空则取消所有"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""停止实时数据订阅"""
|
||||
code_list = [c.strip() for c in codes.split(",")] if codes else None
|
||||
return ResponseModel(data={
|
||||
"message": "取消订阅成功",
|
||||
"codes": code_list
|
||||
})
|
||||
|
||||
|
||||
@router.get("/subscribe/status", response_model=ResponseModel)
|
||||
async def get_subscribe_status(
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取订阅状态"""
|
||||
return ResponseModel(data={
|
||||
"subscribed_codes": [],
|
||||
"subscribed_types": []
|
||||
})
|
||||
|
||||
|
||||
@router.websocket("/stream")
|
||||
async def realtime_websocket(websocket: WebSocket):
|
||||
"""WebSocket实时数据流"""
|
||||
# 获取查询参数
|
||||
query_params = websocket.query_params
|
||||
codes_str = query_params.get("codes", "")
|
||||
types_str = query_params.get("types", "snapshot")
|
||||
|
||||
codes = [c.strip() for c in codes_str.split(",") if c.strip()]
|
||||
|
||||
if not codes:
|
||||
await websocket.close(code=4000, reason="Missing codes parameter")
|
||||
return
|
||||
|
||||
# 创建数据库会话
|
||||
from app.db.session import SessionLocal
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
service = RealtimeService(db)
|
||||
await service.subscribe_websocket(websocket, codes)
|
||||
|
||||
# 保持连接
|
||||
while True:
|
||||
try:
|
||||
# 接收客户端消息(心跳检测)
|
||||
data = await websocket.receive_text()
|
||||
# 回复心跳
|
||||
await websocket.send_json({"type": "heartbeat", "timestamp": datetime.utcnow().isoformat()})
|
||||
except WebSocketDisconnect:
|
||||
break
|
||||
except Exception as e:
|
||||
break
|
||||
|
||||
finally:
|
||||
await service.unsubscribe_websocket(websocket, codes)
|
||||
db.close()
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
@ -0,0 +1,123 @@
|
||||
"""
|
||||
测试中心路由
|
||||
"""
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.schemas.base import ResponseModel
|
||||
from app.schemas.test import TestRequest, RunAllTestsRequest
|
||||
from app.services.test_service import TestService
|
||||
from app.core.security import get_current_user
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/categories", response_model=ResponseModel)
|
||||
async def get_test_categories(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取测试分类"""
|
||||
service = TestService(db)
|
||||
categories = service.get_categories()
|
||||
return ResponseModel(data=categories)
|
||||
|
||||
|
||||
@router.get("/endpoints", response_model=ResponseModel)
|
||||
async def get_test_endpoints(
|
||||
category: str = Query(None, description="分类筛选"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取测试端点列表"""
|
||||
service = TestService(db)
|
||||
endpoints = service.get_endpoints(category)
|
||||
return ResponseModel(data=endpoints)
|
||||
|
||||
|
||||
@router.post("/run", response_model=ResponseModel)
|
||||
async def run_single_test(
|
||||
request: TestRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""执行单个接口测试"""
|
||||
service = TestService(db)
|
||||
result = service.run_test(request.endpoint, request.method, request.params or {})
|
||||
return ResponseModel(data=result)
|
||||
|
||||
|
||||
@router.post("/run-all", response_model=ResponseModel)
|
||||
async def run_all_tests(
|
||||
request: RunAllTestsRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""执行全部接口测试"""
|
||||
service = TestService(db)
|
||||
result = service.run_all_tests(request.categories)
|
||||
return ResponseModel(data=result)
|
||||
|
||||
|
||||
@router.get("/history", response_model=ResponseModel)
|
||||
async def get_test_history(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取测试历史记录"""
|
||||
service = TestService(db)
|
||||
result = service.get_test_history(page, page_size)
|
||||
|
||||
return ResponseModel(data={
|
||||
"items": [{
|
||||
"id": log.id,
|
||||
"test_name": log.test_name,
|
||||
"api_category": log.api_category,
|
||||
"api_endpoint": log.api_endpoint,
|
||||
"request_method": log.request_method,
|
||||
"status_code": log.status_code,
|
||||
"execution_time_ms": log.execution_time_ms,
|
||||
"is_success": log.is_success,
|
||||
"error_message": log.error_message,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None
|
||||
} for log in result["items"]],
|
||||
"total": result["total"],
|
||||
"page": result["page"],
|
||||
"page_size": result["page_size"],
|
||||
"total_pages": result["total_pages"]
|
||||
})
|
||||
|
||||
|
||||
@router.get("/history/{test_id}", response_model=ResponseModel)
|
||||
async def get_test_detail(
|
||||
test_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取单次测试详情"""
|
||||
from app.models.test import APITestLog
|
||||
|
||||
log = db.query(APITestLog).filter(APITestLog.id == test_id).first()
|
||||
|
||||
if not log:
|
||||
return ResponseModel(code=404, message="测试记录不存在")
|
||||
|
||||
return ResponseModel(data={
|
||||
"id": log.id,
|
||||
"test_name": log.test_name,
|
||||
"api_category": log.api_category,
|
||||
"api_endpoint": log.api_endpoint,
|
||||
"request_method": log.request_method,
|
||||
"request_params": log.request_params,
|
||||
"response_data": log.response_data,
|
||||
"status_code": log.status_code,
|
||||
"execution_time_ms": log.execution_time_ms,
|
||||
"is_success": log.is_success,
|
||||
"error_message": log.error_message,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None
|
||||
})
|
||||
@ -0,0 +1,54 @@
|
||||
"""
|
||||
应用配置模块
|
||||
"""
|
||||
import os
|
||||
from typing import Optional
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""应用配置类"""
|
||||
|
||||
# 应用配置
|
||||
APP_NAME: str = "AmazingData金融数据服务平台"
|
||||
APP_VERSION: str = "1.0.0"
|
||||
DEBUG: bool = Field(default=True, env="DEBUG")
|
||||
|
||||
# 数据库配置 - 使用SQLite简化演示
|
||||
DATABASE_URL: str = Field(
|
||||
default="sqlite:///./amazing_data.db",
|
||||
env="DATABASE_URL"
|
||||
)
|
||||
|
||||
# Redis配置
|
||||
REDIS_URL: str = Field(
|
||||
default="redis://localhost:6379/0",
|
||||
env="REDIS_URL"
|
||||
)
|
||||
|
||||
# JWT配置
|
||||
SECRET_KEY: str = Field(
|
||||
default="your-secret-key-change-in-production",
|
||||
env="SECRET_KEY"
|
||||
)
|
||||
ACCESS_TOKEN_EXPIRE_HOURS: int = Field(default=24, env="ACCESS_TOKEN_EXPIRE_HOURS")
|
||||
ALGORITHM: str = "HS256"
|
||||
|
||||
# 缓存配置
|
||||
CACHE_DEFAULT_PERIOD: str = "daily"
|
||||
CACHE_DEFAULT_DAYS: int = 365
|
||||
CACHE_AUTO_CLEANUP_DAYS: int = 7
|
||||
CACHE_BATCH_SIZE: int = 100
|
||||
CACHE_MISSING_THRESHOLD: float = 0.1
|
||||
|
||||
# 实时数据配置
|
||||
REALTIME_SUBSCRIBE_INTERVAL: int = 1000
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
# 全局配置实例
|
||||
settings = Settings()
|
||||
@ -0,0 +1,16 @@
|
||||
# 核心模块
|
||||
from app.core.security import (
|
||||
verify_password,
|
||||
get_password_hash,
|
||||
create_access_token,
|
||||
decode_token,
|
||||
get_current_user
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"verify_password",
|
||||
"get_password_hash",
|
||||
"create_access_token",
|
||||
"decode_token",
|
||||
"get_current_user",
|
||||
]
|
||||
@ -0,0 +1,82 @@
|
||||
"""
|
||||
异常处理模块
|
||||
"""
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
|
||||
class BusinessException(Exception):
|
||||
"""业务异常"""
|
||||
def __init__(self, code: int, message: str):
|
||||
self.code = code
|
||||
self.message = message
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class SDKException(Exception):
|
||||
"""SDK异常"""
|
||||
def __init__(self, message: str, original_error: Exception = None):
|
||||
self.message = message
|
||||
self.original_error = original_error
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
async def business_exception_handler(request: Request, exc: BusinessException):
|
||||
"""业务异常处理器"""
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
content={
|
||||
"code": exc.code,
|
||||
"message": exc.message,
|
||||
"data": None,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
"""参数验证异常处理器"""
|
||||
errors = []
|
||||
for error in exc.errors():
|
||||
errors.append(f"{'.'.join(str(x) for x in error['loc'])}: {error['msg']}")
|
||||
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"code": 400,
|
||||
"message": f"参数错误: {'; '.join(errors)}",
|
||||
"data": None,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError):
|
||||
"""数据库异常处理器"""
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"code": 500,
|
||||
"message": f"数据库错误: {str(exc)}",
|
||||
"data": None,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def general_exception_handler(request: Request, exc: Exception):
|
||||
"""通用异常处理器"""
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"code": 500,
|
||||
"message": f"服务器内部错误: {str(exc)}",
|
||||
"data": None,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
@ -0,0 +1,53 @@
|
||||
"""
|
||||
中间件模块
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoggingMiddleware(BaseHTTPMiddleware):
|
||||
"""日志中间件"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
start_time = time.time()
|
||||
|
||||
# 记录请求信息
|
||||
logger.info(f"Request: {request.method} {request.url.path}")
|
||||
|
||||
response = await call_next(request)
|
||||
|
||||
# 计算处理时间
|
||||
process_time = time.time() - start_time
|
||||
|
||||
# 记录响应信息
|
||||
logger.info(
|
||||
f"Response: {request.method} {request.url.path} "
|
||||
f"- Status: {response.status_code} - Time: {process_time:.3f}s"
|
||||
)
|
||||
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
return response
|
||||
|
||||
|
||||
def setup_cors(app):
|
||||
"""配置CORS"""
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # 生产环境应该限制具体域名
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
def setup_middleware(app):
|
||||
"""设置所有中间件"""
|
||||
setup_cors(app)
|
||||
app.add_middleware(LoggingMiddleware)
|
||||
@ -0,0 +1,92 @@
|
||||
"""
|
||||
安全模块 - JWT认证和密码处理
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
import bcrypt
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import settings
|
||||
from app.db.session import get_db
|
||||
from app.models.user import User
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""验证密码"""
|
||||
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""获取密码哈希"""
|
||||
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""创建JWT访问令牌"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(hours=settings.ACCESS_TOKEN_EXPIRE_HOURS)
|
||||
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def decode_token(token: str) -> Optional[dict]:
|
||||
"""解码JWT令牌"""
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
return payload
|
||||
except JWTError:
|
||||
return None
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
) -> User:
|
||||
"""获取当前用户"""
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的认证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
token = credentials.credentials
|
||||
payload = decode_token(token)
|
||||
|
||||
if payload is None:
|
||||
raise credentials_exception
|
||||
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="用户已被禁用"
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_active_superuser(current_user: User = Depends(get_current_user)) -> User:
|
||||
"""获取当前超级用户"""
|
||||
if not current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要超级用户权限"
|
||||
)
|
||||
return current_user
|
||||
@ -0,0 +1,5 @@
|
||||
# 数据库模块
|
||||
from app.db.base import Base
|
||||
from app.db.session import get_db, engine, SessionLocal
|
||||
|
||||
__all__ = ["Base", "get_db", "engine", "SessionLocal"]
|
||||
@ -0,0 +1,9 @@
|
||||
"""
|
||||
SQLAlchemy基础模型
|
||||
"""
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
"""基础模型类"""
|
||||
pass
|
||||
@ -0,0 +1,68 @@
|
||||
"""
|
||||
数据库会话管理
|
||||
"""
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from typing import Generator
|
||||
|
||||
from app.config import settings
|
||||
from app.db.base import Base
|
||||
|
||||
# 创建数据库引擎
|
||||
if settings.DATABASE_URL.startswith("sqlite"):
|
||||
engine = create_engine(
|
||||
settings.DATABASE_URL,
|
||||
connect_args={"check_same_thread": False},
|
||||
echo=settings.DEBUG
|
||||
)
|
||||
else:
|
||||
engine = create_engine(
|
||||
settings.DATABASE_URL,
|
||||
pool_pre_ping=True,
|
||||
pool_size=10,
|
||||
max_overflow=20,
|
||||
echo=settings.DEBUG
|
||||
)
|
||||
|
||||
# 创建会话工厂
|
||||
SessionLocal = sessionmaker(
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
bind=engine
|
||||
)
|
||||
|
||||
|
||||
def get_db() -> Generator[Session, None, None]:
|
||||
"""获取数据库会话的依赖函数"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
"""初始化数据库表"""
|
||||
from app.models import user, config, stock, future, realtime, finance, cache, test
|
||||
from app.models.user import User
|
||||
from app.core.security import get_password_hash
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
existing_admin = db.query(User).filter(User.username == "admin").first()
|
||||
if not existing_admin:
|
||||
admin_user = User(
|
||||
username="admin",
|
||||
password_hash=get_password_hash("admin123"),
|
||||
is_active=True,
|
||||
is_superuser=True
|
||||
)
|
||||
db.add(admin_user)
|
||||
db.commit()
|
||||
print("Default admin user created (admin/admin123)")
|
||||
except Exception as e:
|
||||
print(f"Error creating default user: {e}")
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
@ -0,0 +1,97 @@
|
||||
"""
|
||||
FastAPI主入口
|
||||
"""
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from contextlib import asynccontextmanager
|
||||
import os
|
||||
|
||||
from app.config import settings
|
||||
from app.api.v1 import api_router
|
||||
from app.core.middleware import setup_middleware
|
||||
from app.core.exceptions import (
|
||||
business_exception_handler,
|
||||
validation_exception_handler,
|
||||
sqlalchemy_exception_handler,
|
||||
general_exception_handler,
|
||||
BusinessException
|
||||
)
|
||||
from app.db.session import init_db
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理"""
|
||||
# 启动时执行
|
||||
print(f"Starting {settings.APP_NAME}...")
|
||||
|
||||
# 初始化数据库
|
||||
try:
|
||||
init_db()
|
||||
print("Database initialized successfully")
|
||||
except Exception as e:
|
||||
print(f"Database initialization warning: {e}")
|
||||
|
||||
yield
|
||||
|
||||
# 关闭时执行
|
||||
print(f"Shutting down {settings.APP_NAME}...")
|
||||
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
version=settings.APP_VERSION,
|
||||
description="AmazingData金融数据服务平台API",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# 设置中间件
|
||||
setup_middleware(app)
|
||||
|
||||
# 注册异常处理器
|
||||
app.add_exception_handler(BusinessException, business_exception_handler)
|
||||
|
||||
# 注册路由
|
||||
app.include_router(api_router)
|
||||
|
||||
# 挂载静态文件
|
||||
static_dir = os.path.join(os.path.dirname(__file__), "static")
|
||||
if os.path.exists(static_dir):
|
||||
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
||||
|
||||
|
||||
@app.get("/", tags=["根路径"])
|
||||
async def root():
|
||||
"""根路径 - 返回前端页面"""
|
||||
index_file = os.path.join(static_dir, "index.html")
|
||||
if os.path.exists(index_file):
|
||||
return FileResponse(index_file)
|
||||
return {
|
||||
"name": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION,
|
||||
"docs": "/docs",
|
||||
"api": "/api/v1"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health", tags=["健康检查"])
|
||||
async def health_check():
|
||||
"""健康检查"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"version": settings.APP_VERSION
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=settings.DEBUG
|
||||
)
|
||||
@ -0,0 +1,28 @@
|
||||
# 模型模块
|
||||
from app.models.user import User
|
||||
from app.models.config import SDKConfig, SystemConfig
|
||||
from app.models.stock import StockInfo, StockKlineDaily, StockKlineMin
|
||||
from app.models.future import FutureInfo, FutureKlineDaily, FutureKlineMin
|
||||
from app.models.realtime import RealtimeSnapshot
|
||||
from app.models.finance import FinanceBalanceSheet, FinanceCashFlow, FinanceIncome
|
||||
from app.models.cache import CacheTask, CacheTaskDetail
|
||||
from app.models.test import APITestLog
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
"SDKConfig",
|
||||
"SystemConfig",
|
||||
"StockInfo",
|
||||
"StockKlineDaily",
|
||||
"StockKlineMin",
|
||||
"FutureInfo",
|
||||
"FutureKlineDaily",
|
||||
"FutureKlineMin",
|
||||
"RealtimeSnapshot",
|
||||
"FinanceBalanceSheet",
|
||||
"FinanceCashFlow",
|
||||
"FinanceIncome",
|
||||
"CacheTask",
|
||||
"CacheTaskDetail",
|
||||
"APITestLog",
|
||||
]
|
||||
@ -0,0 +1,52 @@
|
||||
"""
|
||||
缓存任务模型
|
||||
"""
|
||||
from datetime import datetime, date
|
||||
from sqlalchemy import Column, Integer, BigInteger, String, Numeric, Text, Date, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class CacheTask(Base):
|
||||
"""缓存任务表"""
|
||||
__tablename__ = "cache_tasks"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
task_name = Column(String(200), nullable=False)
|
||||
task_type = Column(String(50), nullable=False) # detect_missing, cache_data, sync_data
|
||||
security_type = Column(String(20), nullable=False) # stock, future, index
|
||||
period_type = Column(String(10)) # daily, min1, min5, etc.
|
||||
start_date = Column(Date, nullable=False)
|
||||
end_date = Column(Date, nullable=False)
|
||||
code_list = Column(Text) # 逗号分隔的代码列表
|
||||
status = Column(String(20), default="pending") # pending, running, completed, failed, cancelled
|
||||
progress = Column(Numeric(5, 2), default=0)
|
||||
total_count = Column(Integer, default=0)
|
||||
success_count = Column(Integer, default=0)
|
||||
error_count = Column(Integer, default=0)
|
||||
error_message = Column(Text)
|
||||
created_by = Column(Integer, ForeignKey("users.id"))
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
started_at = Column(DateTime(timezone=True))
|
||||
completed_at = Column(DateTime(timezone=True))
|
||||
|
||||
details = relationship("CacheTaskDetail", back_populates="task", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class CacheTaskDetail(Base):
|
||||
"""缓存任务详情表"""
|
||||
__tablename__ = "cache_task_details"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
task_id = Column(Integer, ForeignKey("cache_tasks.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
code = Column(String(20), nullable=False, index=True)
|
||||
trade_date = Column(Date, nullable=False)
|
||||
expected_count = Column(Integer, default=0)
|
||||
actual_count = Column(Integer, default=0)
|
||||
is_missing = Column(Integer, default=0)
|
||||
status = Column(String(20), default="pending") # pending, success, failed, skipped
|
||||
error_message = Column(Text)
|
||||
processed_at = Column(DateTime(timezone=True))
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
|
||||
task = relationship("CacheTask", back_populates="details")
|
||||
@ -0,0 +1,36 @@
|
||||
"""
|
||||
配置模型
|
||||
"""
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class SDKConfig(Base):
|
||||
"""SDK配置表"""
|
||||
__tablename__ = "sdk_configs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
username = Column(String(100), nullable=False)
|
||||
password = Column(String(255), nullable=False)
|
||||
host = Column(String(100), nullable=False)
|
||||
port = Column(Integer, nullable=False, default=8080)
|
||||
local_path = Column(String(255), default="./amazing_data_cache/")
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_default = Column(Boolean, default=False)
|
||||
description = Column(Text)
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
class SystemConfig(Base):
|
||||
"""系统配置表"""
|
||||
__tablename__ = "system_configs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
config_key = Column(String(100), unique=True, nullable=False)
|
||||
config_value = Column(Text, nullable=False)
|
||||
description = Column(Text)
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
@ -0,0 +1,87 @@
|
||||
"""
|
||||
财务数据模型
|
||||
"""
|
||||
from datetime import datetime, date
|
||||
from sqlalchemy import Column, Integer, String, BigInteger, Numeric, Date, DateTime
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class FinanceBalanceSheet(Base):
|
||||
"""资产负债表"""
|
||||
__tablename__ = "finance_balance_sheet"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
code = Column(String(20), nullable=False, index=True)
|
||||
report_date = Column(Date, nullable=False, index=True)
|
||||
report_type = Column(Integer)
|
||||
statement_type = Column(Integer)
|
||||
|
||||
# 资产
|
||||
total_assets = Column(Numeric(18, 4))
|
||||
total_cur_assets = Column(Numeric(18, 4))
|
||||
total_noncur_assets = Column(Numeric(18, 4))
|
||||
currency_cap = Column(Numeric(18, 4))
|
||||
notes_receivable = Column(Numeric(18, 4))
|
||||
acct_receivable = Column(Numeric(18, 4))
|
||||
inventory = Column(Numeric(18, 4))
|
||||
fix_assets = Column(Numeric(18, 4))
|
||||
|
||||
# 负债
|
||||
total_liab = Column(Numeric(18, 4))
|
||||
total_cur_liab = Column(Numeric(18, 4))
|
||||
total_noncur_liab = Column(Numeric(18, 4))
|
||||
notes_payable = Column(Numeric(18, 4))
|
||||
acct_payable = Column(Numeric(18, 4))
|
||||
st_borrowing = Column(Numeric(18, 4))
|
||||
lt_loan = Column(Numeric(18, 4))
|
||||
|
||||
# 权益
|
||||
tot_share_equity = Column(Numeric(18, 4))
|
||||
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
|
||||
|
||||
class FinanceCashFlow(Base):
|
||||
"""现金流量表"""
|
||||
__tablename__ = "finance_cash_flow"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
code = Column(String(20), nullable=False, index=True)
|
||||
report_date = Column(Date, nullable=False, index=True)
|
||||
report_type = Column(Integer)
|
||||
statement_type = Column(Integer)
|
||||
|
||||
net_cash_flows_opera_act = Column(Numeric(18, 4))
|
||||
net_cash_flows_inv_act = Column(Numeric(18, 4))
|
||||
net_cash_flows_fin_act = Column(Numeric(18, 4))
|
||||
net_incr_cash_and_cash_equ = Column(Numeric(18, 4))
|
||||
cash_recp_sg_and_rs = Column(Numeric(18, 4))
|
||||
cash_pay_goods_services = Column(Numeric(18, 4))
|
||||
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
|
||||
|
||||
class FinanceIncome(Base):
|
||||
"""利润表"""
|
||||
__tablename__ = "finance_income"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
code = Column(String(20), nullable=False, index=True)
|
||||
report_date = Column(Date, nullable=False, index=True)
|
||||
report_type = Column(Integer)
|
||||
statement_type = Column(Integer)
|
||||
|
||||
tot_opera_rev = Column(Numeric(18, 4))
|
||||
opera_rev = Column(Numeric(18, 4))
|
||||
tot_opera_cost = Column(Numeric(18, 4))
|
||||
opera_profit = Column(Numeric(18, 4))
|
||||
total_profit = Column(Numeric(18, 4))
|
||||
net_pro_incl_min_int_inc = Column(Numeric(18, 4))
|
||||
basic_eps = Column(Numeric(12, 6))
|
||||
diluted_eps = Column(Numeric(12, 6))
|
||||
rd_exp = Column(Numeric(18, 4))
|
||||
selling_exp = Column(Numeric(18, 4))
|
||||
admin_exp = Column(Numeric(18, 4))
|
||||
fin_exp = Column(Numeric(18, 4))
|
||||
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
@ -0,0 +1,66 @@
|
||||
"""
|
||||
期货数据模型
|
||||
"""
|
||||
from datetime import datetime, date
|
||||
from sqlalchemy import Column, Integer, String, BigInteger, Numeric, Boolean, Date, DateTime
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class FutureInfo(Base):
|
||||
"""期货基础信息表"""
|
||||
__tablename__ = "future_info"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
code = Column(String(20), unique=True, nullable=False, index=True)
|
||||
symbol = Column(String(100), nullable=False)
|
||||
underlying = Column(String(20))
|
||||
contract_month = Column(String(10))
|
||||
pre_close = Column(Numeric(12, 4))
|
||||
high_limited = Column(Numeric(12, 4))
|
||||
low_limited = Column(Numeric(12, 4))
|
||||
price_tick = Column(Numeric(10, 4))
|
||||
exchange = Column(String(10), default="CFE")
|
||||
list_date = Column(Date)
|
||||
expire_date = Column(Date)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
class FutureKlineDaily(Base):
|
||||
"""期货日线数据表"""
|
||||
__tablename__ = "future_kline_daily"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
code = Column(String(20), nullable=False, index=True)
|
||||
trade_date = Column(Date, nullable=False, index=True)
|
||||
open = Column(Numeric(12, 4), nullable=False)
|
||||
high = Column(Numeric(12, 4), nullable=False)
|
||||
low = Column(Numeric(12, 4), nullable=False)
|
||||
close = Column(Numeric(12, 4), nullable=False)
|
||||
volume = Column(BigInteger, nullable=False)
|
||||
amount = Column(Numeric(18, 4), nullable=False)
|
||||
settle = Column(Numeric(12, 4))
|
||||
open_interest = Column(BigInteger)
|
||||
pre_settle = Column(Numeric(12, 4))
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
class FutureKlineMin(Base):
|
||||
"""期货分钟数据表"""
|
||||
__tablename__ = "future_kline_min"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
code = Column(String(20), nullable=False, index=True)
|
||||
period_type = Column(String(10), nullable=False)
|
||||
trade_datetime = Column(DateTime, nullable=False, index=True)
|
||||
open = Column(Numeric(12, 4), nullable=False)
|
||||
high = Column(Numeric(12, 4), nullable=False)
|
||||
low = Column(Numeric(12, 4), nullable=False)
|
||||
close = Column(Numeric(12, 4), nullable=False)
|
||||
volume = Column(BigInteger, nullable=False)
|
||||
amount = Column(Numeric(18, 4), nullable=False)
|
||||
settle = Column(Numeric(12, 4))
|
||||
open_interest = Column(BigInteger)
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
@ -0,0 +1,60 @@
|
||||
"""
|
||||
实时数据模型
|
||||
"""
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, BigInteger, Numeric, DateTime
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class RealtimeSnapshot(Base):
|
||||
"""实时快照数据表"""
|
||||
__tablename__ = "realtime_snapshot"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
code = Column(String(20), nullable=False, index=True)
|
||||
security_type = Column(String(20), nullable=False) # stock, future, index, etf, kzz, option
|
||||
trade_time = Column(DateTime, nullable=False, index=True)
|
||||
|
||||
# 价格数据
|
||||
pre_close = Column(Numeric(12, 4))
|
||||
last = Column(Numeric(12, 4))
|
||||
open = Column(Numeric(12, 4))
|
||||
high = Column(Numeric(12, 4))
|
||||
low = Column(Numeric(12, 4))
|
||||
close = Column(Numeric(12, 4))
|
||||
volume = Column(BigInteger)
|
||||
amount = Column(Numeric(18, 4))
|
||||
|
||||
# 盘口数据 - 卖盘
|
||||
ask_price1 = Column(Numeric(12, 4))
|
||||
ask_price2 = Column(Numeric(12, 4))
|
||||
ask_price3 = Column(Numeric(12, 4))
|
||||
ask_price4 = Column(Numeric(12, 4))
|
||||
ask_price5 = Column(Numeric(12, 4))
|
||||
ask_volume1 = Column(Integer)
|
||||
ask_volume2 = Column(Integer)
|
||||
ask_volume3 = Column(Integer)
|
||||
ask_volume4 = Column(Integer)
|
||||
ask_volume5 = Column(Integer)
|
||||
|
||||
# 盘口数据 - 买盘
|
||||
bid_price1 = Column(Numeric(12, 4))
|
||||
bid_price2 = Column(Numeric(12, 4))
|
||||
bid_price3 = Column(Numeric(12, 4))
|
||||
bid_price4 = Column(Numeric(12, 4))
|
||||
bid_price5 = Column(Numeric(12, 4))
|
||||
bid_volume1 = Column(Integer)
|
||||
bid_volume2 = Column(Integer)
|
||||
bid_volume3 = Column(Integer)
|
||||
bid_volume4 = Column(Integer)
|
||||
bid_volume5 = Column(Integer)
|
||||
|
||||
# 期货特有字段
|
||||
settle = Column(Numeric(12, 4))
|
||||
open_interest = Column(BigInteger)
|
||||
pre_settle = Column(Numeric(12, 4))
|
||||
average_price = Column(Numeric(12, 4))
|
||||
trading_phase_code = Column(String(10))
|
||||
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
expires_at = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
@ -0,0 +1,61 @@
|
||||
"""
|
||||
股票数据模型
|
||||
"""
|
||||
from datetime import datetime, date
|
||||
from sqlalchemy import Column, Integer, String, BigInteger, Numeric, Boolean, Date, DateTime
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class StockInfo(Base):
|
||||
"""股票基础信息表"""
|
||||
__tablename__ = "stock_info"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
code = Column(String(20), unique=True, nullable=False, index=True)
|
||||
symbol = Column(String(100), nullable=False)
|
||||
security_status = Column(Integer)
|
||||
pre_close = Column(Numeric(12, 4))
|
||||
high_limited = Column(Numeric(12, 4))
|
||||
low_limited = Column(Numeric(12, 4))
|
||||
price_tick = Column(Numeric(10, 4))
|
||||
exchange = Column(String(10)) # SH, SZ, BJ
|
||||
industry = Column(String(50))
|
||||
list_date = Column(Date)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
class StockKlineDaily(Base):
|
||||
"""股票日线数据表"""
|
||||
__tablename__ = "stock_kline_daily"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
code = Column(String(20), nullable=False, index=True)
|
||||
trade_date = Column(Date, nullable=False, index=True)
|
||||
open = Column(Numeric(12, 4), nullable=False)
|
||||
high = Column(Numeric(12, 4), nullable=False)
|
||||
low = Column(Numeric(12, 4), nullable=False)
|
||||
close = Column(Numeric(12, 4), nullable=False)
|
||||
volume = Column(BigInteger, nullable=False)
|
||||
amount = Column(Numeric(18, 4), nullable=False)
|
||||
adj_factor = Column(Numeric(12, 6))
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
class StockKlineMin(Base):
|
||||
"""股票分钟数据表"""
|
||||
__tablename__ = "stock_kline_min"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
code = Column(String(20), nullable=False, index=True)
|
||||
period_type = Column(String(10), nullable=False) # min1, min5, min15, min30, min60
|
||||
trade_datetime = Column(DateTime, nullable=False, index=True)
|
||||
open = Column(Numeric(12, 4), nullable=False)
|
||||
high = Column(Numeric(12, 4), nullable=False)
|
||||
low = Column(Numeric(12, 4), nullable=False)
|
||||
close = Column(Numeric(12, 4), nullable=False)
|
||||
volume = Column(BigInteger, nullable=False)
|
||||
amount = Column(Numeric(18, 4), nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
@ -0,0 +1,38 @@
|
||||
"""
|
||||
测试日志模型
|
||||
"""
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, BigInteger, String, Integer, Boolean, Text, DateTime, JSON, TypeDecorator, String as SQLString
|
||||
import json
|
||||
|
||||
class JSONField(TypeDecorator):
|
||||
impl = SQLString
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is None:
|
||||
return None
|
||||
return json.dumps(value)
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is None:
|
||||
return None
|
||||
return json.loads(value)
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class APITestLog(Base):
|
||||
"""API测试日志表"""
|
||||
__tablename__ = "api_test_logs"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
test_name = Column(String(200), nullable=False)
|
||||
api_category = Column(String(50), nullable=False, index=True)
|
||||
api_endpoint = Column(String(200), nullable=False, index=True)
|
||||
request_method = Column(String(10), nullable=False)
|
||||
request_params = Column(JSONField)
|
||||
response_data = Column(JSONField)
|
||||
status_code = Column(Integer)
|
||||
execution_time_ms = Column(Integer)
|
||||
is_success = Column(Boolean, default=False)
|
||||
error_message = Column(Text)
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow, index=True)
|
||||
@ -0,0 +1,19 @@
|
||||
"""
|
||||
用户模型
|
||||
"""
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""用户表"""
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, nullable=False, index=True)
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_superuser = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
@ -0,0 +1,32 @@
|
||||
# Schema模块
|
||||
from app.schemas.base import ResponseModel, PaginatedResponse
|
||||
from app.schemas.auth import UserLogin, UserInfo, TokenResponse
|
||||
from app.schemas.config import SDKConfigCreate, SDKConfigUpdate, SDKConfigResponse
|
||||
from app.schemas.kline import KlineRequest, KlineResponse, KlineChartData
|
||||
from app.schemas.finance import FinanceRequest, BalanceSheetResponse, CashFlowResponse, IncomeResponse
|
||||
from app.schemas.cache import CacheTaskCreate, CacheTaskResponse, CacheStatusResponse
|
||||
from app.schemas.test import TestRequest, TestResponse, TestHistoryResponse
|
||||
|
||||
__all__ = [
|
||||
"ResponseModel",
|
||||
"PaginatedResponse",
|
||||
"UserLogin",
|
||||
"UserInfo",
|
||||
"TokenResponse",
|
||||
"SDKConfigCreate",
|
||||
"SDKConfigUpdate",
|
||||
"SDKConfigResponse",
|
||||
"KlineRequest",
|
||||
"KlineResponse",
|
||||
"KlineChartData",
|
||||
"FinanceRequest",
|
||||
"BalanceSheetResponse",
|
||||
"CashFlowResponse",
|
||||
"IncomeResponse",
|
||||
"CacheTaskCreate",
|
||||
"CacheTaskResponse",
|
||||
"CacheStatusResponse",
|
||||
"TestRequest",
|
||||
"TestResponse",
|
||||
"TestHistoryResponse",
|
||||
]
|
||||
@ -0,0 +1,30 @@
|
||||
"""
|
||||
认证相关Schema
|
||||
"""
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
"""用户登录请求"""
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
"""用户信息"""
|
||||
id: int
|
||||
username: str
|
||||
is_active: bool
|
||||
is_superuser: bool
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
"""Token响应"""
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_in: int
|
||||
@ -0,0 +1,47 @@
|
||||
"""
|
||||
基础Schema
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, TypeVar, Generic, List
|
||||
from pydantic import BaseModel
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ResponseModel(BaseModel, Generic[T]):
|
||||
"""统一响应模型"""
|
||||
code: int = 200
|
||||
message: str = "success"
|
||||
data: Optional[T] = None
|
||||
timestamp: datetime = datetime.utcnow()
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PaginationParams(BaseModel):
|
||||
"""分页参数"""
|
||||
page: int = 1
|
||||
page_size: int = 20
|
||||
|
||||
|
||||
class PaginatedData(BaseModel, Generic[T]):
|
||||
"""分页数据"""
|
||||
items: List[T]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
total_pages: int
|
||||
|
||||
|
||||
class PaginatedResponse(ResponseModel[PaginatedData[T]], Generic[T]):
|
||||
"""分页响应模型"""
|
||||
pass
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""错误响应"""
|
||||
code: int
|
||||
message: str
|
||||
data: Optional[dict] = None
|
||||
timestamp: datetime = datetime.utcnow()
|
||||
@ -0,0 +1,108 @@
|
||||
"""
|
||||
缓存管理Schema
|
||||
"""
|
||||
from datetime import date, datetime
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class CacheTaskCreate(BaseModel):
|
||||
"""创建缓存任务"""
|
||||
task_name: str
|
||||
task_type: str = Field(..., description="detect_missing, cache_data, sync_data")
|
||||
security_type: str = Field(..., description="stock, future, index")
|
||||
period_type: Optional[str] = Field(default="daily", description="daily, min1, min5, etc.")
|
||||
start_date: str
|
||||
end_date: str
|
||||
code_list: Optional[List[str]] = None
|
||||
|
||||
|
||||
class MissingDateInfo(BaseModel):
|
||||
"""缺失日期信息"""
|
||||
date: str
|
||||
expected: int
|
||||
actual: int
|
||||
missing_ratio: float
|
||||
|
||||
|
||||
class MissingCodeInfo(BaseModel):
|
||||
"""缺失代码信息"""
|
||||
code: str
|
||||
missing_dates: List[MissingDateInfo]
|
||||
|
||||
|
||||
class CacheTaskResponse(BaseModel):
|
||||
"""缓存任务响应"""
|
||||
id: int
|
||||
task_name: str
|
||||
task_type: str
|
||||
security_type: str
|
||||
period_type: Optional[str]
|
||||
start_date: date
|
||||
end_date: date
|
||||
status: str
|
||||
progress: float
|
||||
total_count: int
|
||||
success_count: int
|
||||
error_count: int
|
||||
error_message: Optional[str]
|
||||
created_at: datetime
|
||||
started_at: Optional[datetime]
|
||||
completed_at: Optional[datetime]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class CacheTaskDetailResponse(BaseModel):
|
||||
"""缓存任务详情响应"""
|
||||
id: int
|
||||
code: str
|
||||
trade_date: date
|
||||
expected_count: int
|
||||
actual_count: int
|
||||
is_missing: bool
|
||||
status: str
|
||||
error_message: Optional[str]
|
||||
processed_at: Optional[datetime]
|
||||
|
||||
|
||||
class CacheTaskWithDetailsResponse(CacheTaskResponse):
|
||||
"""带详情的缓存任务响应"""
|
||||
details: List[CacheTaskDetailResponse]
|
||||
|
||||
|
||||
class CacheStatusResponse(BaseModel):
|
||||
"""代码缓存状态响应"""
|
||||
code: str
|
||||
security_type: str
|
||||
period_type: str
|
||||
record_count: int
|
||||
min_date: Optional[str]
|
||||
max_date: Optional[str]
|
||||
missing_ratio: float
|
||||
|
||||
|
||||
class DetectMissingRequest(BaseModel):
|
||||
"""检测缺失数据请求"""
|
||||
security_type: str
|
||||
period_type: str = "daily"
|
||||
start_date: str
|
||||
end_date: str
|
||||
code_list: List[str]
|
||||
|
||||
|
||||
class DetectMissingResponse(BaseModel):
|
||||
"""检测缺失数据响应"""
|
||||
task_id: int
|
||||
total_codes: int
|
||||
missing_codes: List[MissingCodeInfo]
|
||||
|
||||
|
||||
class BatchCacheRequest(BaseModel):
|
||||
"""批量缓存请求"""
|
||||
security_type: str
|
||||
period_type: str = "daily"
|
||||
start_date: str
|
||||
end_date: str
|
||||
code_list: List[str]
|
||||
@ -0,0 +1,52 @@
|
||||
"""
|
||||
配置相关Schema
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class SDKConfigBase(BaseModel):
|
||||
"""SDK配置基础"""
|
||||
name: str
|
||||
username: str
|
||||
host: str
|
||||
port: int = 8080
|
||||
local_path: str = "./amazing_data_cache/"
|
||||
is_active: bool = True
|
||||
is_default: bool = False
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class SDKConfigCreate(SDKConfigBase):
|
||||
"""创建SDK配置"""
|
||||
password: str
|
||||
|
||||
|
||||
class SDKConfigUpdate(BaseModel):
|
||||
"""更新SDK配置"""
|
||||
name: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
host: Optional[str] = None
|
||||
port: Optional[int] = None
|
||||
local_path: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
is_default: Optional[bool] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class SDKConfigResponse(SDKConfigBase):
|
||||
"""SDK配置响应"""
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class SDKConfigTestResponse(BaseModel):
|
||||
"""SDK配置测试响应"""
|
||||
success: bool
|
||||
message: str
|
||||
@ -0,0 +1,86 @@
|
||||
"""
|
||||
财务数据Schema
|
||||
"""
|
||||
from datetime import date
|
||||
from typing import List, Optional, Dict, Any
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class FinanceRequest(BaseModel):
|
||||
"""财务数据请求"""
|
||||
codes: str = Field(..., description="股票代码,多个用逗号分隔")
|
||||
start_date: str = Field(..., description="开始报告期(YYYYMMDD)")
|
||||
end_date: str = Field(..., description="结束报告期(YYYYMMDD)")
|
||||
|
||||
|
||||
class BalanceSheetData(BaseModel):
|
||||
"""资产负债表数据"""
|
||||
report_date: str
|
||||
report_type: Optional[int] = None
|
||||
statement_type: Optional[int] = None
|
||||
total_assets: Optional[float] = None
|
||||
total_cur_assets: Optional[float] = None
|
||||
total_noncur_assets: Optional[float] = None
|
||||
currency_cap: Optional[float] = None
|
||||
notes_receivable: Optional[float] = None
|
||||
acct_receivable: Optional[float] = None
|
||||
inventory: Optional[float] = None
|
||||
fix_assets: Optional[float] = None
|
||||
total_liab: Optional[float] = None
|
||||
total_cur_liab: Optional[float] = None
|
||||
total_noncur_liab: Optional[float] = None
|
||||
notes_payable: Optional[float] = None
|
||||
acct_payable: Optional[float] = None
|
||||
st_borrowing: Optional[float] = None
|
||||
lt_loan: Optional[float] = None
|
||||
tot_share_equity: Optional[float] = None
|
||||
|
||||
|
||||
class BalanceSheetResponse(BaseModel):
|
||||
"""资产负债表响应"""
|
||||
code: str
|
||||
data: List[BalanceSheetData]
|
||||
|
||||
|
||||
class CashFlowData(BaseModel):
|
||||
"""现金流量表数据"""
|
||||
report_date: str
|
||||
report_type: Optional[int] = None
|
||||
statement_type: Optional[int] = None
|
||||
net_cash_flows_opera_act: Optional[float] = None
|
||||
net_cash_flows_inv_act: Optional[float] = None
|
||||
net_cash_flows_fin_act: Optional[float] = None
|
||||
net_incr_cash_and_cash_equ: Optional[float] = None
|
||||
cash_recp_sg_and_rs: Optional[float] = None
|
||||
cash_pay_goods_services: Optional[float] = None
|
||||
|
||||
|
||||
class CashFlowResponse(BaseModel):
|
||||
"""现金流量表响应"""
|
||||
code: str
|
||||
data: List[CashFlowData]
|
||||
|
||||
|
||||
class IncomeData(BaseModel):
|
||||
"""利润表数据"""
|
||||
report_date: str
|
||||
report_type: Optional[int] = None
|
||||
statement_type: Optional[int] = None
|
||||
tot_opera_rev: Optional[float] = None
|
||||
opera_rev: Optional[float] = None
|
||||
tot_opera_cost: Optional[float] = None
|
||||
opera_profit: Optional[float] = None
|
||||
total_profit: Optional[float] = None
|
||||
net_pro_incl_min_int_inc: Optional[float] = None
|
||||
basic_eps: Optional[float] = None
|
||||
diluted_eps: Optional[float] = None
|
||||
rd_exp: Optional[float] = None
|
||||
selling_exp: Optional[float] = None
|
||||
admin_exp: Optional[float] = None
|
||||
fin_exp: Optional[float] = None
|
||||
|
||||
|
||||
class IncomeResponse(BaseModel):
|
||||
"""利润表响应"""
|
||||
code: str
|
||||
data: List[IncomeData]
|
||||
@ -0,0 +1,70 @@
|
||||
"""
|
||||
测试中心Schema
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict, Any
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class TestCategory(BaseModel):
|
||||
"""测试分类"""
|
||||
key: str
|
||||
name: str
|
||||
|
||||
|
||||
class TestEndpoint(BaseModel):
|
||||
"""测试端点"""
|
||||
category: str
|
||||
name: str
|
||||
endpoint: str
|
||||
method: str
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class TestRequest(BaseModel):
|
||||
"""测试请求"""
|
||||
endpoint: str
|
||||
method: str = "GET"
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class TestResponse(BaseModel):
|
||||
"""测试响应"""
|
||||
success: bool
|
||||
endpoint: str
|
||||
method: str
|
||||
status_code: Optional[int] = None
|
||||
execution_time_ms: Optional[int] = None
|
||||
response_data: Optional[Any] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
class TestHistoryResponse(BaseModel):
|
||||
"""测试历史响应"""
|
||||
id: int
|
||||
test_name: str
|
||||
api_category: str
|
||||
api_endpoint: str
|
||||
request_method: str
|
||||
status_code: Optional[int]
|
||||
execution_time_ms: Optional[int]
|
||||
is_success: bool
|
||||
error_message: Optional[str]
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class RunAllTestsRequest(BaseModel):
|
||||
"""运行全部测试请求"""
|
||||
categories: List[str] = Field(default=["base_data", "stock", "future", "finance"])
|
||||
|
||||
|
||||
class RunAllTestsResponse(BaseModel):
|
||||
"""运行全部测试响应"""
|
||||
total: int
|
||||
passed: int
|
||||
failed: int
|
||||
results: List[TestResponse]
|
||||
@ -0,0 +1,24 @@
|
||||
# 服务模块
|
||||
from app.services.auth_service import AuthService
|
||||
from app.services.config_service import ConfigService
|
||||
from app.services.amazing_data_adapter import AmazingDataAdapter
|
||||
from app.services.cache_service import CacheService
|
||||
from app.services.stock_service import StockService
|
||||
from app.services.future_service import FutureService
|
||||
from app.services.realtime_service import RealtimeService
|
||||
from app.services.finance_service import FinanceService
|
||||
from app.services.base_data_service import BaseDataService
|
||||
from app.services.test_service import TestService
|
||||
|
||||
__all__ = [
|
||||
"AuthService",
|
||||
"ConfigService",
|
||||
"AmazingDataAdapter",
|
||||
"CacheService",
|
||||
"StockService",
|
||||
"FutureService",
|
||||
"RealtimeService",
|
||||
"FinanceService",
|
||||
"BaseDataService",
|
||||
"TestService",
|
||||
]
|
||||
@ -0,0 +1,135 @@
|
||||
"""
|
||||
认证服务
|
||||
"""
|
||||
from datetime import timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.models.user import User
|
||||
from app.core.security import verify_password, get_password_hash, create_access_token
|
||||
from app.config import settings
|
||||
|
||||
|
||||
class AuthService:
|
||||
"""认证服务"""
|
||||
|
||||
@staticmethod
|
||||
def authenticate_user(db: Session, username: str, password: str) -> User:
|
||||
"""
|
||||
验证用户凭据
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
username: 用户名
|
||||
password: 密码
|
||||
|
||||
Returns:
|
||||
用户对象
|
||||
|
||||
Raises:
|
||||
HTTPException: 认证失败
|
||||
"""
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误"
|
||||
)
|
||||
|
||||
if not verify_password(password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误"
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="用户已被禁用"
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def create_user_token(user: User) -> dict:
|
||||
"""
|
||||
创建用户访问令牌
|
||||
|
||||
Args:
|
||||
user: 用户对象
|
||||
|
||||
Returns:
|
||||
Token信息
|
||||
"""
|
||||
access_token_expires = timedelta(hours=settings.ACCESS_TOKEN_EXPIRE_HOURS)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username},
|
||||
expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": settings.ACCESS_TOKEN_EXPIRE_HOURS * 3600
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_user_by_username(db: Session, username: str) -> User:
|
||||
"""通过用户名获取用户"""
|
||||
return db.query(User).filter(User.username == username).first()
|
||||
|
||||
@staticmethod
|
||||
def create_user(db: Session, username: str, password: str, is_superuser: bool = False) -> User:
|
||||
"""
|
||||
创建新用户
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
username: 用户名
|
||||
password: 密码
|
||||
is_superuser: 是否超级用户
|
||||
|
||||
Returns:
|
||||
新用户对象
|
||||
"""
|
||||
# 检查用户名是否已存在
|
||||
existing_user = db.query(User).filter(User.username == username).first()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="用户名已存在"
|
||||
)
|
||||
|
||||
# 创建新用户
|
||||
user = User(
|
||||
username=username,
|
||||
password_hash=get_password_hash(password),
|
||||
is_superuser=is_superuser
|
||||
)
|
||||
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def change_password(db: Session, user: User, old_password: str, new_password: str):
|
||||
"""
|
||||
修改密码
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
user: 用户对象
|
||||
old_password: 旧密码
|
||||
new_password: 新密码
|
||||
"""
|
||||
if not verify_password(old_password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="旧密码错误"
|
||||
)
|
||||
|
||||
user.password_hash = get_password_hash(new_password)
|
||||
db.commit()
|
||||
@ -0,0 +1,303 @@
|
||||
"""
|
||||
缓存管理服务
|
||||
"""
|
||||
import logging
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import date, datetime
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, func
|
||||
|
||||
from app.models.cache import CacheTask, CacheTaskDetail
|
||||
from app.models.stock import StockKlineDaily
|
||||
from app.models.future import FutureKlineDaily
|
||||
from app.services.base_data_service import BaseDataService
|
||||
from app.services.stock_service import StockService
|
||||
from app.services.future_service import FutureService
|
||||
from app.utils.date_utils import parse_date, format_date, get_market_from_code
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CacheService:
|
||||
"""缓存服务"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
self.base_service = BaseDataService(db)
|
||||
self.stock_service = StockService(db)
|
||||
self.future_service = FutureService(db)
|
||||
|
||||
def detect_missing_data(
|
||||
self,
|
||||
security_type: str,
|
||||
period_type: str,
|
||||
start_date: date,
|
||||
end_date: date,
|
||||
code_list: List[str]
|
||||
) -> CacheTask:
|
||||
"""
|
||||
检测缺失数据
|
||||
|
||||
Args:
|
||||
security_type: 证券类型 (stock, future)
|
||||
period_type: 周期类型 (daily, min1, etc.)
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
code_list: 代码列表
|
||||
|
||||
Returns:
|
||||
缓存任务对象
|
||||
"""
|
||||
# 创建检测任务
|
||||
task = CacheTask(
|
||||
task_name=f"检测缺失数据 - {security_type} - {len(code_list)}个代码",
|
||||
task_type="detect_missing",
|
||||
security_type=security_type,
|
||||
period_type=period_type,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
code_list=",".join(code_list),
|
||||
status="running",
|
||||
total_count=len(code_list),
|
||||
started_at=datetime.utcnow()
|
||||
)
|
||||
self.db.add(task)
|
||||
self.db.commit()
|
||||
self.db.refresh(task)
|
||||
|
||||
try:
|
||||
# 获取交易日历
|
||||
market = "CFE" if security_type == "future" else "SH"
|
||||
trading_days = self.base_service.get_trading_calendar(market, start_date, end_date)
|
||||
expected_count = len(trading_days)
|
||||
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
for code in code_list:
|
||||
try:
|
||||
# 查询实际数据量
|
||||
if security_type == "stock" and period_type == "daily":
|
||||
actual_count = self.db.query(StockKlineDaily).filter(
|
||||
and_(
|
||||
StockKlineDaily.code == code,
|
||||
StockKlineDaily.trade_date >= start_date,
|
||||
StockKlineDaily.trade_date <= end_date
|
||||
)
|
||||
).count()
|
||||
elif security_type == "future" and period_type == "daily":
|
||||
actual_count = self.db.query(FutureKlineDaily).filter(
|
||||
and_(
|
||||
FutureKlineDaily.code == code,
|
||||
FutureKlineDaily.trade_date >= start_date,
|
||||
FutureKlineDaily.trade_date <= end_date
|
||||
)
|
||||
).count()
|
||||
else:
|
||||
actual_count = 0
|
||||
|
||||
# 计算缺失率
|
||||
missing_ratio = 0
|
||||
if expected_count > 0:
|
||||
missing_ratio = (expected_count - actual_count) / expected_count
|
||||
|
||||
is_missing = missing_ratio > settings.CACHE_MISSING_THRESHOLD
|
||||
|
||||
# 创建任务详情
|
||||
detail = CacheTaskDetail(
|
||||
task_id=task.id,
|
||||
code=code,
|
||||
trade_date=start_date,
|
||||
expected_count=expected_count,
|
||||
actual_count=actual_count,
|
||||
is_missing=1 if is_missing else 0,
|
||||
status="pending" if is_missing else "skipped"
|
||||
)
|
||||
self.db.add(detail)
|
||||
|
||||
if is_missing:
|
||||
success_count += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"检测{code}缺失数据失败: {str(e)}")
|
||||
error_count += 1
|
||||
|
||||
detail = CacheTaskDetail(
|
||||
task_id=task.id,
|
||||
code=code,
|
||||
trade_date=start_date,
|
||||
status="failed",
|
||||
error_message=str(e)
|
||||
)
|
||||
self.db.add(detail)
|
||||
|
||||
# 更新进度
|
||||
task.success_count = success_count
|
||||
task.error_count = error_count
|
||||
task.progress = min(100, int((success_count + error_count) / len(code_list) * 100))
|
||||
self.db.commit()
|
||||
|
||||
task.status = "completed"
|
||||
task.completed_at = datetime.utcnow()
|
||||
self.db.commit()
|
||||
|
||||
except Exception as e:
|
||||
task.status = "failed"
|
||||
task.error_message = str(e)
|
||||
task.completed_at = datetime.utcnow()
|
||||
self.db.commit()
|
||||
logger.error(f"检测缺失数据任务失败: {str(e)}")
|
||||
|
||||
return task
|
||||
|
||||
def batch_cache_data(
|
||||
self,
|
||||
security_type: str,
|
||||
period_type: str,
|
||||
start_date: date,
|
||||
end_date: date,
|
||||
code_list: List[str]
|
||||
) -> CacheTask:
|
||||
"""
|
||||
批量缓存数据
|
||||
|
||||
Args:
|
||||
security_type: 证券类型
|
||||
period_type: 周期类型
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
code_list: 代码列表
|
||||
|
||||
Returns:
|
||||
缓存任务对象
|
||||
"""
|
||||
# 创建缓存任务
|
||||
task = CacheTask(
|
||||
task_name=f"批量缓存数据 - {security_type} - {len(code_list)}个代码",
|
||||
task_type="cache_data",
|
||||
security_type=security_type,
|
||||
period_type=period_type,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
code_list=",".join(code_list),
|
||||
status="running",
|
||||
total_count=len(code_list),
|
||||
started_at=datetime.utcnow()
|
||||
)
|
||||
self.db.add(task)
|
||||
self.db.commit()
|
||||
self.db.refresh(task)
|
||||
|
||||
try:
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
for code in code_list:
|
||||
try:
|
||||
# 获取数据(会自动缓存)
|
||||
if security_type == "stock":
|
||||
self.stock_service.get_kline([code], start_date, end_date, period_type)
|
||||
elif security_type == "future":
|
||||
self.future_service.get_kline([code], start_date, end_date, period_type)
|
||||
|
||||
success_count += 1
|
||||
|
||||
# 创建任务详情
|
||||
detail = CacheTaskDetail(
|
||||
task_id=task.id,
|
||||
code=code,
|
||||
trade_date=start_date,
|
||||
status="success",
|
||||
processed_at=datetime.utcnow()
|
||||
)
|
||||
self.db.add(detail)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"缓存{code}数据失败: {str(e)}")
|
||||
error_count += 1
|
||||
|
||||
detail = CacheTaskDetail(
|
||||
task_id=task.id,
|
||||
code=code,
|
||||
trade_date=start_date,
|
||||
status="failed",
|
||||
error_message=str(e)
|
||||
)
|
||||
self.db.add(detail)
|
||||
|
||||
# 更新进度
|
||||
task.success_count = success_count
|
||||
task.error_count = error_count
|
||||
task.progress = min(100, int((success_count + error_count) / len(code_list) * 100))
|
||||
self.db.commit()
|
||||
|
||||
task.status = "completed"
|
||||
task.completed_at = datetime.utcnow()
|
||||
self.db.commit()
|
||||
|
||||
except Exception as e:
|
||||
task.status = "failed"
|
||||
task.error_message = str(e)
|
||||
task.completed_at = datetime.utcnow()
|
||||
self.db.commit()
|
||||
logger.error(f"批量缓存数据任务失败: {str(e)}")
|
||||
|
||||
return task
|
||||
|
||||
def get_tasks(
|
||||
self,
|
||||
page: int = 1,
|
||||
page_size: int = 20
|
||||
) -> Dict:
|
||||
"""获取缓存任务列表"""
|
||||
query = self.db.query(CacheTask).order_by(CacheTask.created_at.desc())
|
||||
|
||||
total = query.count()
|
||||
tasks = query.offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"items": tasks,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": (total + page_size - 1) // page_size
|
||||
}
|
||||
|
||||
def get_task(self, task_id: int) -> Optional[CacheTask]:
|
||||
"""获取任务详情"""
|
||||
return self.db.query(CacheTask).filter(CacheTask.id == task_id).first()
|
||||
|
||||
def get_task_details(self, task_id: int) -> List[CacheTaskDetail]:
|
||||
"""获取任务详情列表"""
|
||||
return self.db.query(CacheTaskDetail).filter(
|
||||
CacheTaskDetail.task_id == task_id
|
||||
).all()
|
||||
|
||||
def cancel_task(self, task_id: int) -> bool:
|
||||
"""取消任务"""
|
||||
task = self.db.query(CacheTask).filter(CacheTask.id == task_id).first()
|
||||
|
||||
if task and task.status == "running":
|
||||
task.status = "cancelled"
|
||||
task.completed_at = datetime.utcnow()
|
||||
self.db.commit()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_cache_status(self, code: str, security_type: str, period_type: str) -> Dict:
|
||||
"""获取代码缓存状态"""
|
||||
if security_type == "stock":
|
||||
return self.stock_service.get_cache_status(code, period_type)
|
||||
elif security_type == "future":
|
||||
return self.future_service.get_cache_status(code, period_type)
|
||||
else:
|
||||
return {
|
||||
"code": code,
|
||||
"security_type": security_type,
|
||||
"period_type": period_type,
|
||||
"record_count": 0,
|
||||
"min_date": None,
|
||||
"max_date": None
|
||||
}
|
||||
@ -0,0 +1,149 @@
|
||||
"""
|
||||
配置服务
|
||||
"""
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.models.config import SDKConfig, SystemConfig
|
||||
|
||||
|
||||
class ConfigService:
|
||||
"""配置服务"""
|
||||
|
||||
@staticmethod
|
||||
def get_sdk_configs(db: Session) -> List[SDKConfig]:
|
||||
"""获取所有SDK配置"""
|
||||
return db.query(SDKConfig).order_by(SDKConfig.created_at.desc()).all()
|
||||
|
||||
@staticmethod
|
||||
def get_sdk_config(db: Session, config_id: int) -> Optional[SDKConfig]:
|
||||
"""获取指定SDK配置"""
|
||||
return db.query(SDKConfig).filter(SDKConfig.id == config_id).first()
|
||||
|
||||
@staticmethod
|
||||
def get_default_sdk_config(db: Session) -> Optional[SDKConfig]:
|
||||
"""获取默认SDK配置"""
|
||||
return db.query(SDKConfig).filter(SDKConfig.is_default == True).first()
|
||||
|
||||
@staticmethod
|
||||
def create_sdk_config(db: Session, config_data: dict) -> SDKConfig:
|
||||
"""
|
||||
创建SDK配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_data: 配置数据
|
||||
|
||||
Returns:
|
||||
新配置对象
|
||||
"""
|
||||
config = SDKConfig(**config_data)
|
||||
db.add(config)
|
||||
db.commit()
|
||||
db.refresh(config)
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def update_sdk_config(
|
||||
db: Session,
|
||||
config_id: int,
|
||||
config_data: dict
|
||||
) -> SDKConfig:
|
||||
"""
|
||||
更新SDK配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_id: 配置ID
|
||||
config_data: 更新数据
|
||||
|
||||
Returns:
|
||||
更新后的配置对象
|
||||
"""
|
||||
config = db.query(SDKConfig).filter(SDKConfig.id == config_id).first()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="配置不存在"
|
||||
)
|
||||
|
||||
# 更新字段
|
||||
for key, value in config_data.items():
|
||||
if value is not None and hasattr(config, key):
|
||||
setattr(config, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(config)
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def delete_sdk_config(db: Session, config_id: int):
|
||||
"""
|
||||
删除SDK配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_id: 配置ID
|
||||
"""
|
||||
config = db.query(SDKConfig).filter(SDKConfig.id == config_id).first()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="配置不存在"
|
||||
)
|
||||
|
||||
db.delete(config)
|
||||
db.commit()
|
||||
|
||||
@staticmethod
|
||||
def set_default_config(db: Session, config_id: int):
|
||||
"""
|
||||
设置默认配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_id: 配置ID
|
||||
"""
|
||||
config = db.query(SDKConfig).filter(SDKConfig.id == config_id).first()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="配置不存在"
|
||||
)
|
||||
|
||||
# 取消其他配置的默认状态
|
||||
db.query(SDKConfig).update({SDKConfig.is_default: False})
|
||||
|
||||
# 设置当前配置为默认
|
||||
config.is_default = True
|
||||
db.commit()
|
||||
db.refresh(config)
|
||||
|
||||
@staticmethod
|
||||
def get_system_config(db: Session, key: str) -> Optional[str]:
|
||||
"""获取系统配置值"""
|
||||
config = db.query(SystemConfig).filter(SystemConfig.config_key == key).first()
|
||||
return config.config_value if config else None
|
||||
|
||||
@staticmethod
|
||||
def set_system_config(db: Session, key: str, value: str, description: str = None):
|
||||
"""设置系统配置"""
|
||||
config = db.query(SystemConfig).filter(SystemConfig.config_key == key).first()
|
||||
|
||||
if config:
|
||||
config.config_value = value
|
||||
if description:
|
||||
config.description = description
|
||||
else:
|
||||
config = SystemConfig(
|
||||
config_key=key,
|
||||
config_value=value,
|
||||
description=description
|
||||
)
|
||||
db.add(config)
|
||||
|
||||
db.commit()
|
||||
@ -0,0 +1,163 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AmazingData金融数据服务平台</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.subtitle {
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.status {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.status-title {
|
||||
font-weight: bold;
|
||||
color: #0369a1;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #e0f2fe;
|
||||
}
|
||||
.status-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.status-label {
|
||||
color: #64748b;
|
||||
}
|
||||
.status-value {
|
||||
color: #0ea5e9;
|
||||
font-weight: 500;
|
||||
}
|
||||
.features {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.features h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.feature-list {
|
||||
list-style: none;
|
||||
}
|
||||
.feature-list li {
|
||||
padding: 8px 0;
|
||||
color: #555;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.feature-list li::before {
|
||||
content: "✓";
|
||||
color: #22c55e;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.api-link {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
padding: 12px 24px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.api-link:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>AmazingData</h1>
|
||||
<p class="subtitle">金融数据服务平台</p>
|
||||
|
||||
<div class="status">
|
||||
<div class="status-title">系统状态</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">后端服务</span>
|
||||
<span class="status-value" id="backend-status">检查中...</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">API版本</span>
|
||||
<span class="status-value" id="api-version">-</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">数据库</span>
|
||||
<span class="status-value">SQLite (演示模式)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="features">
|
||||
<h3>功能特性</h3>
|
||||
<ul class="feature-list">
|
||||
<li>完整SDK接口封装</li>
|
||||
<li>智能数据缓存</li>
|
||||
<li>实时数据订阅</li>
|
||||
<li>缺失数据检测</li>
|
||||
<li>批量缓存管理</li>
|
||||
<li>可视化K线图</li>
|
||||
<li>完整测试中心</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a href="/docs" class="api-link">查看API文档</a>
|
||||
|
||||
<div class="footer">
|
||||
默认账号: admin / admin123
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 检查后端状态
|
||||
fetch('/api/v1/health')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
document.getElementById('backend-status').textContent = '运行中 ✓';
|
||||
document.getElementById('backend-status').style.color = '#22c55e';
|
||||
document.getElementById('api-version').textContent = data.version;
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById('backend-status').textContent = '未连接 ✗';
|
||||
document.getElementById('backend-status').style.color = '#ef4444';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,14 @@
|
||||
# 工具模块
|
||||
from app.utils.date_utils import parse_date, format_date, get_trading_days_between
|
||||
from app.utils.data_utils import deduplicate_dataframe, compare_kline_data
|
||||
from app.utils.validators import validate_code_format, validate_date_range
|
||||
|
||||
__all__ = [
|
||||
"parse_date",
|
||||
"format_date",
|
||||
"get_trading_days_between",
|
||||
"deduplicate_dataframe",
|
||||
"compare_kline_data",
|
||||
"validate_code_format",
|
||||
"validate_date_range",
|
||||
]
|
||||
@ -0,0 +1,76 @@
|
||||
"""
|
||||
日期工具模块
|
||||
"""
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
def parse_date(date_str: str) -> date:
|
||||
"""解析日期字符串 (YYYYMMDD 或 YYYY-MM-DD)"""
|
||||
if len(date_str) == 8:
|
||||
return datetime.strptime(date_str, "%Y%m%d").date()
|
||||
elif len(date_str) == 10:
|
||||
return datetime.strptime(date_str, "%Y-%m-%d").date()
|
||||
else:
|
||||
raise ValueError(f"无效的日期格式: {date_str}")
|
||||
|
||||
|
||||
def format_date(d: date, format_str: str = "%Y-%m-%d") -> str:
|
||||
"""格式化日期"""
|
||||
return d.strftime(format_str)
|
||||
|
||||
|
||||
def format_datetime(dt: datetime, format_str: str = "%Y-%m-%d %H:%M:%S") -> str:
|
||||
"""格式化日期时间"""
|
||||
return dt.strftime(format_str)
|
||||
|
||||
|
||||
def get_market_from_code(code: str) -> str:
|
||||
"""从代码获取市场"""
|
||||
if code.endswith(".SH"):
|
||||
return "SH"
|
||||
elif code.endswith(".SZ"):
|
||||
return "SZ"
|
||||
elif code.endswith(".BJ"):
|
||||
return "BJ"
|
||||
elif code.endswith(".CFE"):
|
||||
return "CFE"
|
||||
else:
|
||||
return "SH" # 默认上海
|
||||
|
||||
|
||||
def get_trading_days_between(
|
||||
start_date: date,
|
||||
end_date: date,
|
||||
calendar: Optional[List[date]] = None
|
||||
) -> List[date]:
|
||||
"""获取两个日期之间的交易日列表"""
|
||||
if calendar:
|
||||
return [d for d in calendar if start_date <= d <= end_date]
|
||||
|
||||
# 如果没有提供交易日历,简单排除周末
|
||||
trading_days = []
|
||||
current = start_date
|
||||
while current <= end_date:
|
||||
if current.weekday() < 5: # 周一到周五
|
||||
trading_days.append(current)
|
||||
current += timedelta(days=1)
|
||||
|
||||
return trading_days
|
||||
|
||||
|
||||
def get_default_date_range(days: int = 365) -> tuple:
|
||||
"""获取默认日期范围"""
|
||||
end_date = date.today()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
return start_date, end_date
|
||||
|
||||
|
||||
def int_to_date(date_int: int) -> date:
|
||||
"""将整数日期(YYYYMMDD)转换为date对象"""
|
||||
return datetime.strptime(str(date_int), "%Y%m%d").date()
|
||||
|
||||
|
||||
def date_to_int(d: date) -> int:
|
||||
"""将date对象转换为整数日期(YYYYMMDD)"""
|
||||
return int(d.strftime("%Y%m%d"))
|
||||
@ -0,0 +1,37 @@
|
||||
# Web框架
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
python-multipart==0.0.6
|
||||
websockets==12.0
|
||||
|
||||
# 数据库
|
||||
sqlalchemy==2.0.23
|
||||
psycopg2-binary==2.9.9
|
||||
aiosqlite==0.19.0
|
||||
alembic==1.12.1
|
||||
redis==5.0.1
|
||||
|
||||
# 认证
|
||||
pyjwt==2.8.0
|
||||
bcrypt==4.1.1
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
|
||||
# 数据处理
|
||||
pandas==2.1.3
|
||||
numpy==1.26.2
|
||||
|
||||
# 任务调度
|
||||
apscheduler==3.10.4
|
||||
|
||||
# 配置管理
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
python-dotenv==1.0.0
|
||||
|
||||
# 工具
|
||||
python-dateutil==2.8.2
|
||||
httpx==0.25.2
|
||||
|
||||
# AmazingData SDK (本地安装)
|
||||
# AmazingData-1.0.24-py3-none-any.whl
|
||||
@ -0,0 +1,163 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AmazingData金融数据服务平台</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.subtitle {
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.status {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.status-title {
|
||||
font-weight: bold;
|
||||
color: #0369a1;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #e0f2fe;
|
||||
}
|
||||
.status-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.status-label {
|
||||
color: #64748b;
|
||||
}
|
||||
.status-value {
|
||||
color: #0ea5e9;
|
||||
font-weight: 500;
|
||||
}
|
||||
.features {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.features h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.feature-list {
|
||||
list-style: none;
|
||||
}
|
||||
.feature-list li {
|
||||
padding: 8px 0;
|
||||
color: #555;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.feature-list li::before {
|
||||
content: "✓";
|
||||
color: #22c55e;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.api-link {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
padding: 12px 24px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.api-link:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>AmazingData</h1>
|
||||
<p class="subtitle">金融数据服务平台</p>
|
||||
|
||||
<div class="status">
|
||||
<div class="status-title">系统状态</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">后端服务</span>
|
||||
<span class="status-value" id="backend-status">检查中...</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">API版本</span>
|
||||
<span class="status-value" id="api-version">-</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">数据库</span>
|
||||
<span class="status-value">SQLite (演示模式)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="features">
|
||||
<h3>功能特性</h3>
|
||||
<ul class="feature-list">
|
||||
<li>完整SDK接口封装</li>
|
||||
<li>智能数据缓存</li>
|
||||
<li>实时数据订阅</li>
|
||||
<li>缺失数据检测</li>
|
||||
<li>批量缓存管理</li>
|
||||
<li>可视化K线图</li>
|
||||
<li>完整测试中心</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a href="/docs" class="api-link">查看API文档</a>
|
||||
|
||||
<div class="footer">
|
||||
默认账号: admin / admin123
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 检查后端状态
|
||||
fetch('/api/v1/health')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
document.getElementById('backend-status').textContent = '运行中 ✓';
|
||||
document.getElementById('backend-status').style.color = '#22c55e';
|
||||
document.getElementById('api-version').textContent = data.version;
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById('backend-status').textContent = '未连接 ✗';
|
||||
document.getElementById('backend-status').style.color = '#ef4444';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,53 @@
|
||||
"""
|
||||
AmazingData SDK 连接测试脚本
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
|
||||
def test_sdk_connection():
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.config import SDKConfig
|
||||
from app.services.amazing_data_adapter import AmazingDataAdapter
|
||||
|
||||
db = SessionLocal()
|
||||
config = db.query(SDKConfig).filter(SDKConfig.id == 1).first()
|
||||
|
||||
if not config:
|
||||
print("No SDK config found with id=1")
|
||||
return False
|
||||
|
||||
print(f"Testing SDK connection with config:")
|
||||
print(f" Username: {config.username}")
|
||||
print(f" Host: {config.host}")
|
||||
print(f" Port: {config.port}")
|
||||
|
||||
adapter = AmazingDataAdapter({
|
||||
"username": config.username,
|
||||
"password": config.password,
|
||||
"host": config.host,
|
||||
"port": config.port,
|
||||
"local_path": config.local_path or "./amazing_data_cache/"
|
||||
})
|
||||
|
||||
start = time.time()
|
||||
print("\nConnecting to SDK...")
|
||||
|
||||
try:
|
||||
success = adapter.connect()
|
||||
elapsed = time.time() - start
|
||||
|
||||
if success:
|
||||
print(f"✓ SDK connection successful! (took {elapsed:.2f}s)")
|
||||
adapter.disconnect()
|
||||
print("✓ Disconnected successfully")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ SDK connection failed! (took {elapsed:.2f}s)")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"✗ Error: {type(e).__name__}: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = test_sdk_connection()
|
||||
sys.exit(0 if success else 1)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -0,0 +1,379 @@
|
||||
"""
|
||||
AmazingData 数据源适配器 - 使用示例
|
||||
|
||||
本文件展示了如何使用 AmazingDataAdapter 获取各类金融数据
|
||||
"""
|
||||
|
||||
from amazing_data_adapter import (
|
||||
AmazingDataAdapter,
|
||||
DataSourceConfig,
|
||||
SecurityType,
|
||||
Market,
|
||||
Period,
|
||||
create_adapter
|
||||
)
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def demo_basic_usage():
|
||||
"""基础使用示例"""
|
||||
|
||||
# 方式1: 使用配置对象创建
|
||||
config = DataSourceConfig(
|
||||
username='your_username',
|
||||
password='your_password',
|
||||
host='your_host',
|
||||
port=8080,
|
||||
local_path='./amazing_data_cache/',
|
||||
use_local_cache=True
|
||||
)
|
||||
adapter = AmazingDataAdapter(config)
|
||||
|
||||
# 方式2: 使用便捷函数创建
|
||||
# adapter = create_adapter(
|
||||
# username='your_username',
|
||||
# password='your_password',
|
||||
# host='your_host',
|
||||
# port=8080
|
||||
# )
|
||||
|
||||
# 连接数据源
|
||||
if not adapter.connect():
|
||||
print("连接失败")
|
||||
return
|
||||
|
||||
try:
|
||||
# ============== 基础数据 ==============
|
||||
print("\n=== 基础数据 ===")
|
||||
|
||||
# 获取沪深A股代码列表
|
||||
stock_codes = adapter.get_code_list(SecurityType.STOCK_A)
|
||||
print(f"沪深A股数量: {len(stock_codes)}")
|
||||
|
||||
# 获取ETF代码列表
|
||||
etf_codes = adapter.get_code_list(SecurityType.ETF)
|
||||
print(f"ETF数量: {len(etf_codes)}")
|
||||
|
||||
# 获取期货代码列表
|
||||
future_codes = adapter.get_code_list(SecurityType.FUTURE)
|
||||
print(f"期货数量: {len(future_codes)}")
|
||||
|
||||
# 获取证券基本信息
|
||||
stock_info = adapter.get_code_info(SecurityType.STOCK_A)
|
||||
print(f"\n证券信息列: {stock_info.columns.tolist()}")
|
||||
print(stock_info.head())
|
||||
|
||||
# 获取交易日历
|
||||
calendar = adapter.get_trading_calendar(Market.SH)
|
||||
print(f"\n交易日数量: {len(calendar)}")
|
||||
print(f"最近交易日: {calendar[-5:]}")
|
||||
|
||||
# ============== 历史行情数据 ==============
|
||||
print("\n=== 历史行情数据 ===")
|
||||
|
||||
# 获取日K线数据
|
||||
sample_codes = stock_codes[:5] # 取前5只股票作为示例
|
||||
kline_data = adapter.get_kline(
|
||||
codes=sample_codes,
|
||||
start_date='20240101',
|
||||
end_date='20241231',
|
||||
period=Period.DAILY
|
||||
)
|
||||
|
||||
for code, df in list(kline_data.items())[:2]:
|
||||
print(f"\n{code} K线数据:")
|
||||
print(df.head())
|
||||
|
||||
# 获取分钟K线
|
||||
min_kline = adapter.get_kline(
|
||||
codes=['000001.SZ'],
|
||||
start_date='20241201',
|
||||
end_date='20241231',
|
||||
period=Period.MIN60
|
||||
)
|
||||
|
||||
# ============== 复权因子 ==============
|
||||
print("\n=== 复权因子 ===")
|
||||
|
||||
adj_factor = adapter.get_adj_factor(
|
||||
codes=['000001.SZ', '600000.SH'],
|
||||
is_local=False # 强制从服务器获取最新数据
|
||||
)
|
||||
print("单次复权因子:")
|
||||
print(adj_factor.head())
|
||||
|
||||
backward_factor = adapter.get_backward_factor(
|
||||
codes=['000001.SZ', '600000.SH']
|
||||
)
|
||||
print("\n后复权因子:")
|
||||
print(backward_factor.head())
|
||||
|
||||
# ============== 财务数据 ==============
|
||||
print("\n=== 财务数据 ===")
|
||||
|
||||
# 获取资产负债表
|
||||
balance_sheet = adapter.get_balance_sheet(
|
||||
codes=['000001.SZ', '600000.SH'],
|
||||
start_date=20240101,
|
||||
end_date=20241231
|
||||
)
|
||||
|
||||
for code, df in balance_sheet.items():
|
||||
print(f"\n{code} 资产负债表字段:")
|
||||
print(df.columns.tolist()[:10]) # 显示前10个字段
|
||||
|
||||
# 获取现金流量表
|
||||
cash_flow = adapter.get_cash_flow(
|
||||
codes=['000001.SZ'],
|
||||
start_date=20240101,
|
||||
end_date=20241231
|
||||
)
|
||||
|
||||
# 获取利润表
|
||||
income = adapter.get_income_statement(
|
||||
codes=['000001.SZ'],
|
||||
start_date=20240101,
|
||||
end_date=20241231
|
||||
)
|
||||
|
||||
# 获取业绩快报
|
||||
profit_express = adapter.get_profit_express(
|
||||
codes=['000001.SZ', '600000.SH']
|
||||
)
|
||||
print(f"\n业绩快报:\n{profit_express.head()}")
|
||||
|
||||
# ============== 股东股本数据 ==============
|
||||
print("\n=== 股东股本数据 ===")
|
||||
|
||||
# 获取十大股东
|
||||
top10_holders = adapter.get_top10_shareholders(
|
||||
codes=['000001.SZ'],
|
||||
start_date=20240101,
|
||||
end_date=20241231
|
||||
)
|
||||
print(f"十大股东:\n{top10_holders.head()}")
|
||||
|
||||
# 获取股东户数
|
||||
holder_count = adapter.get_shareholder_count(
|
||||
codes=['000001.SZ'],
|
||||
start_date=20240101,
|
||||
end_date=20241231
|
||||
)
|
||||
print(f"\n股东户数:\n{holder_count.head()}")
|
||||
|
||||
# 获取股本结构
|
||||
equity_structure = adapter.get_equity_structure(
|
||||
codes=['000001.SZ']
|
||||
)
|
||||
print(f"\n股本结构:\n{equity_structure.head()}")
|
||||
|
||||
# ============== 融资融券数据 ==============
|
||||
print("\n=== 融资融券数据 ===")
|
||||
|
||||
# 获取融资融券汇总
|
||||
margin_summary = adapter.get_margin_summary(
|
||||
start_date=20240101,
|
||||
end_date=20241231
|
||||
)
|
||||
print(f"融资融券汇总:\n{margin_summary.head()}")
|
||||
|
||||
# 获取个股融资融券明细
|
||||
margin_detail = adapter.get_margin_detail(
|
||||
codes=['000001.SZ'],
|
||||
start_date=20241201,
|
||||
end_date=20241231
|
||||
)
|
||||
|
||||
# ============== 交易异动数据 ==============
|
||||
print("\n=== 交易异动数据 ===")
|
||||
|
||||
# 获取龙虎榜
|
||||
longhu = adapter.get_longhu_bang(
|
||||
codes=['000001.SZ'],
|
||||
start_date=20241201,
|
||||
end_date=20241231
|
||||
)
|
||||
print(f"龙虎榜数据:\n{longhu.head()}")
|
||||
|
||||
# 获取大宗交易
|
||||
block_trade = adapter.get_block_trading(
|
||||
codes=['000001.SZ'],
|
||||
start_date=20241201,
|
||||
end_date=20241231
|
||||
)
|
||||
print(f"\n大宗交易:\n{block_trade.head()}")
|
||||
|
||||
# ============== 指数数据 ==============
|
||||
print("\n=== 指数数据 ===")
|
||||
|
||||
# 获取指数成分股 (沪深300)
|
||||
index_constituents = adapter.get_index_constituents(['000300.SH'])
|
||||
for code, df in index_constituents.items():
|
||||
print(f"\n{code} 成分股数量: {len(df)}")
|
||||
print(df.head())
|
||||
|
||||
# 获取指数成分股权重
|
||||
index_weights = adapter.get_index_weights(
|
||||
codes=['000300.SH', '000905.SH'], # 沪深300和中证500
|
||||
start_date=20241201,
|
||||
end_date=20241231
|
||||
)
|
||||
|
||||
# ============== ETF数据 ==============
|
||||
print("\n=== ETF数据 ===")
|
||||
|
||||
# 获取ETF申赎数据
|
||||
etf_info, etf_constituents = adapter.get_etf_pcf(['510050.SH'])
|
||||
print(f"ETF基本信息:\n{etf_info.head()}")
|
||||
|
||||
# 获取基金份额
|
||||
fund_share = adapter.get_fund_share(
|
||||
codes=['510050.SH'],
|
||||
start_date=20240101,
|
||||
end_date=20241231
|
||||
)
|
||||
|
||||
# ============== 可转债数据 ==============
|
||||
print("\n=== 可转债数据 ===")
|
||||
|
||||
# 获取可转债发行数据
|
||||
kzz_codes = adapter.get_code_list(SecurityType.KZZ)
|
||||
print(f"可转债数量: {len(kzz_codes)}")
|
||||
|
||||
if kzz_codes:
|
||||
kzz_issuance = adapter.get_kzz_issuance(kzz_codes[:3])
|
||||
for code, df in list(kzz_issuance.items())[:1]:
|
||||
print(f"\n{code} 发行信息:\n{df.head()}")
|
||||
|
||||
finally:
|
||||
# 断开连接
|
||||
adapter.disconnect()
|
||||
|
||||
|
||||
def demo_data_processing():
|
||||
"""数据处理示例 - 展示如何加工获取的数据"""
|
||||
|
||||
adapter = create_adapter(
|
||||
username='your_username',
|
||||
password='your_password',
|
||||
host='your_host',
|
||||
port=8080
|
||||
)
|
||||
|
||||
if not adapter.connect():
|
||||
return
|
||||
|
||||
try:
|
||||
# 获取平安银行(000001.SZ)的日K线数据
|
||||
kline_data = adapter.get_kline(
|
||||
codes=['000001.SZ'],
|
||||
start_date='20240101',
|
||||
end_date='20241231',
|
||||
period=Period.DAILY
|
||||
)
|
||||
|
||||
df = kline_data['000001.SZ']
|
||||
|
||||
# 计算技术指标
|
||||
# 1. 移动平均线
|
||||
df['MA5'] = df['close'].rolling(window=5).mean()
|
||||
df['MA10'] = df['close'].rolling(window=10).mean()
|
||||
df['MA20'] = df['close'].rolling(window=20).mean()
|
||||
|
||||
# 2. 涨跌幅
|
||||
df['return'] = df['close'].pct_change()
|
||||
|
||||
# 3. 波动率 (20日)
|
||||
df['volatility'] = df['return'].rolling(window=20).std() * (252 ** 0.5)
|
||||
|
||||
# 4. 成交量移动平均
|
||||
df['volume_MA5'] = df['volume'].rolling(window=5).mean()
|
||||
|
||||
print("加工后的数据:")
|
||||
print(df[['close', 'MA5', 'MA10', 'MA20', 'return', 'volatility']].tail(10))
|
||||
|
||||
# 获取复权因子并计算复权价格
|
||||
adj_factor = adapter.get_backward_factor(['000001.SZ'])
|
||||
|
||||
# 合并K线和复权因子
|
||||
df['trade_date'] = df.index.strftime('%Y%m%d').astype(int)
|
||||
df = df.merge(
|
||||
adj_factor[['000001.SZ']].reset_index(),
|
||||
left_on='trade_date',
|
||||
right_on='index',
|
||||
how='left'
|
||||
)
|
||||
df = df.rename(columns={'000001.SZ': 'adj_factor'})
|
||||
|
||||
# 计算后复权价格
|
||||
df['adj_close'] = df['close'] * df['adj_factor']
|
||||
|
||||
print("\n复权后的价格:")
|
||||
print(df[['close', 'adj_factor', 'adj_close']].tail(10))
|
||||
|
||||
finally:
|
||||
adapter.disconnect()
|
||||
|
||||
|
||||
def demo_batch_processing():
|
||||
"""批量处理示例"""
|
||||
|
||||
adapter = create_adapter(
|
||||
username='your_username',
|
||||
password='your_password',
|
||||
host='your_host',
|
||||
port=8080
|
||||
)
|
||||
|
||||
if not adapter.connect():
|
||||
return
|
||||
|
||||
try:
|
||||
# 获取沪深300成分股
|
||||
index_constituents = adapter.get_index_constituents(['000300.SH'])
|
||||
hs300_codes = index_constituents['000300.SH']['CON_CODE'].tolist()
|
||||
|
||||
print(f"沪深300成分股数量: {len(hs300_codes)}")
|
||||
|
||||
# 批量获取财务数据 (分批处理避免超时)
|
||||
batch_size = 50
|
||||
all_balance_sheets = {}
|
||||
|
||||
for i in range(0, len(hs300_codes), batch_size):
|
||||
batch_codes = hs300_codes[i:i+batch_size]
|
||||
print(f"处理第 {i//batch_size + 1} 批,共 {len(batch_codes)} 只股票")
|
||||
|
||||
batch_data = adapter.get_balance_sheet(
|
||||
codes=batch_codes,
|
||||
start_date=20240930,
|
||||
end_date=20240930
|
||||
)
|
||||
all_balance_sheets.update(batch_data)
|
||||
|
||||
# 合并所有数据
|
||||
combined_data = []
|
||||
for code, df in all_balance_sheets.items():
|
||||
if not df.empty:
|
||||
df['code'] = code
|
||||
combined_data.append(df)
|
||||
|
||||
if combined_data:
|
||||
result_df = pd.concat(combined_data, ignore_index=True)
|
||||
print(f"\n合并后的数据形状: {result_df.shape}")
|
||||
print(result_df[['code', 'REPORTING_PERIOD', 'TOTAL_ASSETS', 'TOTAL_CUR_ASSETS']].head())
|
||||
|
||||
finally:
|
||||
adapter.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("AmazingData 数据源适配器 - 使用示例")
|
||||
print("=" * 60)
|
||||
|
||||
# 注:实际运行前请替换用户名、密码和服务器地址
|
||||
print("\n提示: 请先在代码中替换 username, password, host, port 为实际值")
|
||||
|
||||
# demo_basic_usage()
|
||||
# demo_data_processing()
|
||||
# demo_batch_processing()
|
||||
Binary file not shown.
@ -0,0 +1,64 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: amazing_data_postgres
|
||||
environment:
|
||||
POSTGRES_DB: amazing_data
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ../database/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- amazing_data_network
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: amazing_data_redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
networks:
|
||||
- amazing_data_network
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ../backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: amazing_data_backend
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/amazing_data
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
SECRET_KEY: your-secret-key-change-in-production
|
||||
DEBUG: "false"
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
networks:
|
||||
- amazing_data_network
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ../frontend
|
||||
dockerfile: Dockerfile
|
||||
container_name: amazing_data_frontend
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- amazing_data_network
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
amazing_data_network:
|
||||
driver: bridge
|
||||
@ -0,0 +1,577 @@
|
||||
# AmazingData 金融数据服务平台 - API接口文档
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **Base URL**: `http://localhost:8000/api/v1`
|
||||
- **Content-Type**: `application/json`
|
||||
- **认证方式**: JWT Bearer Token
|
||||
|
||||
## 认证相关
|
||||
|
||||
### 1. 用户登录
|
||||
```http
|
||||
POST /auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 86400
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取当前用户
|
||||
```http
|
||||
GET /auth/me
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
## 配置管理
|
||||
|
||||
### 3. 获取SDK配置列表
|
||||
```http
|
||||
GET /configs/sdk
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 4. 创建SDK配置
|
||||
```http
|
||||
POST /configs/sdk
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "银河证券生产环境",
|
||||
"username": "your_username",
|
||||
"password": "your_password",
|
||||
"host": "xxx.xxx.xxx.xxx",
|
||||
"port": 8080,
|
||||
"local_path": "./amazing_data_cache/",
|
||||
"is_default": true
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 测试SDK连接
|
||||
```http
|
||||
POST /configs/sdk/{id}/test
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
## 基础数据
|
||||
|
||||
### 6. 获取代码列表
|
||||
```http
|
||||
GET /base/codes?security_type=EXTRA_STOCK_A
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `security_type`: 证券类型
|
||||
- `EXTRA_STOCK_A` - 沪深A股
|
||||
- `EXTRA_FUTURE` - 期货
|
||||
- `EXTRA_ETF` - ETF
|
||||
- `EXTRA_INDEX_A` - 指数
|
||||
|
||||
### 7. 获取证券信息
|
||||
```http
|
||||
GET /base/codes/{code}/info
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 8. 获取交易日历
|
||||
```http
|
||||
GET /base/calendar?market=SH&start_date=20240101&end_date=20241231
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
## 股票数据
|
||||
|
||||
### 9. 获取股票K线数据
|
||||
```http
|
||||
GET /stock/kline?codes=000001.SZ&start_date=20240101&end_date=20241231&period=daily
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `codes`: 股票代码,多个用逗号分隔
|
||||
- `start_date`: 开始日期 (YYYYMMDD)
|
||||
- `end_date`: 结束日期 (YYYYMMDD)
|
||||
- `period`: 周期 (daily, min1, min5, min15, min30, min60)
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"000001.SZ": [
|
||||
{
|
||||
"trade_date": "2024-01-02",
|
||||
"open": 10.50,
|
||||
"high": 10.80,
|
||||
"low": 10.40,
|
||||
"close": 10.65,
|
||||
"volume": 1234567,
|
||||
"amount": 12845678.90
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10. 获取股票K线图数据
|
||||
```http
|
||||
GET /stock/kline/{code}/chart?start_date=20240101&end_date=20241231&period=daily
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应** (ECharts格式):
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"categoryData": ["2024-01-02", "2024-01-03", ...],
|
||||
"values": [
|
||||
[10.50, 10.80, 10.40, 10.65, 1234567],
|
||||
[10.65, 10.70, 10.50, 10.60, 987654],
|
||||
...
|
||||
],
|
||||
"volumes": [
|
||||
[0, 1234567, 1],
|
||||
[1, 987654, -1],
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 11. 批量获取股票K线
|
||||
```http
|
||||
POST /stock/kline/batch
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"codes": ["000001.SZ", "600000.SH"],
|
||||
"start_date": "20240101",
|
||||
"end_date": "20241231",
|
||||
"period": "daily"
|
||||
}
|
||||
```
|
||||
|
||||
## 期货数据
|
||||
|
||||
### 12. 获取期货K线数据
|
||||
```http
|
||||
GET /future/kline?codes=IF2501.CFE&start_date=20240101&end_date=20241231&period=daily
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 13. 获取期货K线图数据
|
||||
```http
|
||||
GET /future/kline/{code}/chart?start_date=20240101&end_date=20241231&period=daily
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
## 实时数据
|
||||
|
||||
### 14. 获取最新快照
|
||||
```http
|
||||
GET /realtime/snapshot?codes=000001.SZ,600000.SH
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 15. 开始实时订阅
|
||||
```http
|
||||
POST /realtime/subscribe
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"codes": ["000001.SZ", "600000.SH"],
|
||||
"types": ["snapshot"],
|
||||
"callback_url": "ws://localhost:8000/api/v1/realtime/stream"
|
||||
}
|
||||
```
|
||||
|
||||
### 16. WebSocket实时数据流
|
||||
```
|
||||
WS /realtime/stream?codes=000001.SZ,600000.SH&types=snapshot
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**消息格式**:
|
||||
```json
|
||||
{
|
||||
"type": "snapshot",
|
||||
"code": "000001.SZ",
|
||||
"data": {
|
||||
"trade_time": "2025-01-15T10:30:00",
|
||||
"last": 10.50,
|
||||
"open": 10.30,
|
||||
"high": 10.60,
|
||||
"low": 10.20,
|
||||
"volume": 1234567,
|
||||
"amount": 12845678.90
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 财务数据
|
||||
|
||||
### 17. 获取资产负债表
|
||||
```http
|
||||
GET /finance/balance-sheet?codes=000001.SZ&start_date=20240930&end_date=20240930
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"000001.SZ": [
|
||||
{
|
||||
"report_date": "2024-09-30",
|
||||
"total_assets": 123456789012.34,
|
||||
"total_cur_assets": 98765432109.87,
|
||||
"total_liab": 87654321098.76,
|
||||
"tot_share_equity": 35802467913.58
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 18. 获取现金流量表
|
||||
```http
|
||||
GET /finance/cash-flow?codes=000001.SZ&start_date=20240930&end_date=20240930
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 19. 获取利润表
|
||||
```http
|
||||
GET /finance/income?codes=000001.SZ&start_date=20240930&end_date=20240930
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 20. 获取业绩快报
|
||||
```http
|
||||
GET /finance/profit-express?codes=000001.SZ&start_date=20240930&end_date=20240930
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 21. 获取业绩预告
|
||||
```http
|
||||
GET /finance/profit-notice?codes=000001.SZ&start_date=20240930&end_date=20240930
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
## 股东数据
|
||||
|
||||
### 22. 获取十大股东
|
||||
```http
|
||||
GET /shareholder/top10?codes=000001.SZ&start_date=20240930&end_date=20240930
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 23. 获取股东户数
|
||||
```http
|
||||
GET /shareholder/count?codes=000001.SZ&start_date=20240930&end_date=20240930
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 24. 获取股本结构
|
||||
```http
|
||||
GET /shareholder/equity?codes=000001.SZ&start_date=20240930&end_date=20240930
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
## 融资融券
|
||||
|
||||
### 25. 获取融资融券汇总
|
||||
```http
|
||||
GET /margin/summary?start_date=20240101&end_date=20241231
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 26. 获取融资融券明细
|
||||
```http
|
||||
GET /margin/detail?codes=000001.SZ&start_date=20240101&end_date=20241231
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
## 指数数据
|
||||
|
||||
### 27. 获取指数成分股
|
||||
```http
|
||||
GET /index/constituents?codes=000300.SH
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 28. 获取指数权重
|
||||
```http
|
||||
GET /index/weights?codes=000300.SH&start_date=20240101&end_date=20241231
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
## ETF数据
|
||||
|
||||
### 29. 获取ETF申赎数据
|
||||
```http
|
||||
GET /etf/pcf?codes=510050.SH
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 30. 获取ETF份额
|
||||
```http
|
||||
GET /etf/share?codes=510050.SH&start_date=20240101&end_date=20241231
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
## 可转债数据
|
||||
|
||||
### 31. 获取可转债发行数据
|
||||
```http
|
||||
GET /kzz/issuance?codes=128XXX.SZ
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
## 缓存管理
|
||||
|
||||
### 32. 检测缺失数据
|
||||
```http
|
||||
POST /cache/detect-missing
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"security_type": "stock",
|
||||
"period_type": "daily",
|
||||
"start_date": "20240101",
|
||||
"end_date": "20241231",
|
||||
"code_list": ["000001.SZ", "600000.SH"]
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"task_id": 1,
|
||||
"total_codes": 2,
|
||||
"missing_codes": [
|
||||
{
|
||||
"code": "000001.SZ",
|
||||
"missing_dates": [
|
||||
{
|
||||
"date": "2024-06-01",
|
||||
"expected": 1,
|
||||
"actual": 0,
|
||||
"missing_ratio": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 33. 批量缓存数据
|
||||
```http
|
||||
POST /cache/batch-cache
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"security_type": "stock",
|
||||
"period_type": "daily",
|
||||
"start_date": "20240101",
|
||||
"end_date": "20241231",
|
||||
"code_list": ["000001.SZ", "600000.SH"]
|
||||
}
|
||||
```
|
||||
|
||||
### 34. 获取缓存任务列表
|
||||
```http
|
||||
GET /cache/tasks?page=1&page_size=20
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 35. 获取缓存任务详情
|
||||
```http
|
||||
GET /cache/tasks/{task_id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 36. 取消缓存任务
|
||||
```http
|
||||
DELETE /cache/tasks/{task_id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 37. 获取代码缓存状态
|
||||
```http
|
||||
GET /cache/status/{code}?security_type=stock&period_type=daily
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"code": "000001.SZ",
|
||||
"security_type": "stock",
|
||||
"period_type": "daily",
|
||||
"record_count": 242,
|
||||
"min_date": "2024-01-02",
|
||||
"max_date": "2024-12-31",
|
||||
"missing_ratio": 0.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 测试中心
|
||||
|
||||
### 38. 获取测试分类
|
||||
```http
|
||||
GET /test/categories
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": [
|
||||
{"key": "base_data", "name": "基础数据"},
|
||||
{"key": "stock", "name": "股票数据"},
|
||||
{"key": "future", "name": "期货数据"},
|
||||
{"key": "realtime", "name": "实时数据"},
|
||||
{"key": "finance", "name": "财务数据"},
|
||||
{"key": "shareholder", "name": "股东数据"},
|
||||
{"key": "margin", "name": "融资融券"},
|
||||
{"key": "index", "name": "指数数据"},
|
||||
{"key": "etf", "name": "ETF数据"},
|
||||
{"key": "kzz", "name": "可转债数据"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 39. 获取接口列表
|
||||
```http
|
||||
GET /test/endpoints?category=stock
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 40. 执行单个接口测试
|
||||
```http
|
||||
POST /test/run
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"endpoint": "/api/v1/stock/kline",
|
||||
"method": "GET",
|
||||
"params": {
|
||||
"codes": "000001.SZ",
|
||||
"start_date": "20240101",
|
||||
"end_date": "20241231",
|
||||
"period": "daily"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 41. 执行全部接口测试
|
||||
```http
|
||||
POST /test/run-all
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"categories": ["stock", "future", "finance"]
|
||||
}
|
||||
```
|
||||
|
||||
### 42. 获取测试历史
|
||||
```http
|
||||
GET /test/history?page=1&page_size=20
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
## 错误码
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 成功 |
|
||||
| 400 | 参数错误 |
|
||||
| 401 | 未授权 |
|
||||
| 403 | 禁止访问 |
|
||||
| 404 | 资源不存在 |
|
||||
| 500 | 服务器内部错误 |
|
||||
| 1001 | SDK连接失败 |
|
||||
| 1002 | 数据不存在 |
|
||||
| 1003 | 任务执行失败 |
|
||||
|
||||
## 数据类型说明
|
||||
|
||||
### K线数据结构
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| trade_date | string | 交易日期 (YYYY-MM-DD) |
|
||||
| trade_datetime | string | 交易时间 (YYYY-MM-DD HH:MM:SS) |
|
||||
| open | number | 开盘价 |
|
||||
| high | number | 最高价 |
|
||||
| low | number | 最低价 |
|
||||
| close | number | 收盘价 |
|
||||
| volume | number | 成交量 |
|
||||
| amount | number | 成交金额 |
|
||||
|
||||
### 期货K线额外字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| settle | number | 结算价 |
|
||||
| open_interest | number | 持仓量 |
|
||||
|
||||
### 快照数据结构
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| code | string | 证券代码 |
|
||||
| trade_time | string | 交易时间 |
|
||||
| last | number | 最新价 |
|
||||
| open | number | 开盘价 |
|
||||
| high | number | 最高价 |
|
||||
| low | number | 最低价 |
|
||||
| volume | number | 成交量 |
|
||||
| amount | number | 成交金额 |
|
||||
| ask_price1-5 | number | 卖1-5价格 |
|
||||
| ask_volume1-5 | number | 卖1-5数量 |
|
||||
| bid_price1-5 | number | 买1-5价格 |
|
||||
| bid_volume1-5 | number | 买1-5数量 |
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0
|
||||
**更新日期**: 2025年
|
||||
@ -0,0 +1,27 @@
|
||||
# 前端Dockerfile
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制package.json
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建
|
||||
RUN npm run build
|
||||
|
||||
# 生产环境
|
||||
FROM nginx:alpine
|
||||
|
||||
# 复制构建产物
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# 复制nginx配置
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AmazingData金融数据服务平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,17 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://host.docker.internal:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "amazing-data-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.8",
|
||||
"vue-router": "^4.2.5",
|
||||
"pinia": "^2.1.7",
|
||||
"axios": "^1.6.2",
|
||||
"element-plus": "^2.4.4",
|
||||
"echarts": "^5.4.3",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"js-cookie": "^3.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@vitejs/plugin-vue": "^4.5.0",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^5.0.4",
|
||||
"vue-tsc": "^1.8.22"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,13 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const login = (data: { username: string; password: string }) => {
|
||||
return request.post('/auth/login', data)
|
||||
}
|
||||
|
||||
export const getUserInfo = () => {
|
||||
return request.get('/auth/me')
|
||||
}
|
||||
|
||||
export const logout = () => {
|
||||
return request.post('/auth/logout')
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const detectMissingData = (data: {
|
||||
security_type: string
|
||||
period_type: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
code_list: string[]
|
||||
}) => {
|
||||
return request.post('/cache/detect-missing', data)
|
||||
}
|
||||
|
||||
export const batchCacheData = (data: {
|
||||
security_type: string
|
||||
period_type: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
code_list: string[]
|
||||
}) => {
|
||||
return request.post('/cache/batch-cache', data)
|
||||
}
|
||||
|
||||
export const getCacheTasks = (params?: { page?: number; page_size?: number }) => {
|
||||
return request.get('/cache/tasks', { params })
|
||||
}
|
||||
|
||||
export const getCacheTask = (taskId: number) => {
|
||||
return request.get(`/cache/tasks/${taskId}`)
|
||||
}
|
||||
|
||||
export const cancelCacheTask = (taskId: number) => {
|
||||
return request.delete(`/cache/tasks/${taskId}`)
|
||||
}
|
||||
|
||||
export const getCacheStatus = (
|
||||
code: string,
|
||||
params: { security_type: string; period_type: string }
|
||||
) => {
|
||||
return request.get(`/cache/status/${code}`, { params })
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const getSDKConfigs = () => {
|
||||
return request.get('/configs/sdk')
|
||||
}
|
||||
|
||||
export const createSDKConfig = (data: any) => {
|
||||
return request.post('/configs/sdk', data)
|
||||
}
|
||||
|
||||
export const updateSDKConfig = (id: number, data: any) => {
|
||||
return request.put(`/configs/sdk/${id}`, data)
|
||||
}
|
||||
|
||||
export const deleteSDKConfig = (id: number) => {
|
||||
return request.delete(`/configs/sdk/${id}`)
|
||||
}
|
||||
|
||||
export const testSDKConfig = (id: number) => {
|
||||
return request.post(`/configs/sdk/${id}/test`)
|
||||
}
|
||||
|
||||
export const setDefaultConfig = (id: number) => {
|
||||
return request.post(`/configs/sdk/${id}/set-default`)
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const getBalanceSheet = (params: {
|
||||
codes: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
}) => {
|
||||
return request.get('/finance/balance-sheet', { params })
|
||||
}
|
||||
|
||||
export const getCashFlow = (params: {
|
||||
codes: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
}) => {
|
||||
return request.get('/finance/cash-flow', { params })
|
||||
}
|
||||
|
||||
export const getIncome = (params: {
|
||||
codes: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
}) => {
|
||||
return request.get('/finance/income', { params })
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const getFutureKline = (params: {
|
||||
codes: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
period?: string
|
||||
}) => {
|
||||
return request.get('/future/kline', { params })
|
||||
}
|
||||
|
||||
export const getFutureKlineChart = (
|
||||
code: string,
|
||||
params: {
|
||||
start_date: string
|
||||
end_date: string
|
||||
period?: string
|
||||
}
|
||||
) => {
|
||||
return request.get(`/future/kline/${code}/chart`, { params })
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const getStockKline = (params: {
|
||||
codes: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
period?: string
|
||||
}) => {
|
||||
return request.get('/stock/kline', { params })
|
||||
}
|
||||
|
||||
export const getStockKlineChart = (
|
||||
code: string,
|
||||
params: {
|
||||
start_date: string
|
||||
end_date: string
|
||||
period?: string
|
||||
}
|
||||
) => {
|
||||
return request.get(`/stock/kline/${code}/chart`, { params })
|
||||
}
|
||||
|
||||
export const batchGetStockKline = (data: {
|
||||
codes: string[]
|
||||
start_date: string
|
||||
end_date: string
|
||||
period?: string
|
||||
}) => {
|
||||
return request.post('/stock/kline/batch', data)
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const getTestCategories = () => {
|
||||
return request.get('/test/categories')
|
||||
}
|
||||
|
||||
export const getTestEndpoints = (category?: string) => {
|
||||
return request.get('/test/endpoints', { params: { category } })
|
||||
}
|
||||
|
||||
export const runSingleTest = (data: {
|
||||
endpoint: string
|
||||
method: string
|
||||
params?: Record<string, any>
|
||||
}) => {
|
||||
return request.post('/test/run', data)
|
||||
}
|
||||
|
||||
export const runAllTests = (data: { categories: string[] }) => {
|
||||
return request.post('/test/run-all', data)
|
||||
}
|
||||
|
||||
export const getTestHistory = (params?: { page?: number; page_size?: number }) => {
|
||||
return request.get('/test/history', { params })
|
||||
}
|
||||
|
||||
export const getTestDetail = (testId: number) => {
|
||||
return request.get(`/test/history/${testId}`)
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import 'element-plus/dist/index.css'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus, { locale: zhCn })
|
||||
|
||||
app.mount('#app')
|
||||
@ -0,0 +1,73 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/Login.vue'),
|
||||
meta: { public: true }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'Layout',
|
||||
component: () => import('@/views/Layout.vue'),
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/Dashboard.vue'),
|
||||
meta: { title: '首页', icon: 'HomeFilled' }
|
||||
},
|
||||
{
|
||||
path: 'data-query',
|
||||
name: 'DataQuery',
|
||||
component: () => import('@/views/DataQuery/index.vue'),
|
||||
meta: { title: '数据查询', icon: 'DataLine' }
|
||||
},
|
||||
{
|
||||
path: 'config',
|
||||
name: 'ConfigManager',
|
||||
component: () => import('@/views/ConfigManager.vue'),
|
||||
meta: { title: '配置管理', icon: 'Setting' }
|
||||
},
|
||||
{
|
||||
path: 'cache',
|
||||
name: 'CacheManager',
|
||||
component: () => import('@/views/CacheManager/index.vue'),
|
||||
meta: { title: '缓存管理', icon: 'Box' }
|
||||
},
|
||||
{
|
||||
path: 'test',
|
||||
name: 'TestCenter',
|
||||
component: () => import('@/views/TestCenter/index.vue'),
|
||||
meta: { title: '测试中心', icon: 'CircleCheck' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
if (to.meta.public) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
if (!userStore.token) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
@ -0,0 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
const TOKEN_KEY = 'amazing_data_token'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// State
|
||||
const token = ref<string>(Cookies.get(TOKEN_KEY) || '')
|
||||
const userInfo = ref<any>(null)
|
||||
|
||||
// Getters
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
|
||||
// Actions
|
||||
const setToken = (newToken: string) => {
|
||||
token.value = newToken
|
||||
Cookies.set(TOKEN_KEY, newToken, { expires: 1 })
|
||||
}
|
||||
|
||||
const clearToken = () => {
|
||||
token.value = ''
|
||||
userInfo.value = null
|
||||
Cookies.remove(TOKEN_KEY)
|
||||
}
|
||||
|
||||
const setUserInfo = (info: any) => {
|
||||
userInfo.value = info
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
clearToken()
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
userInfo,
|
||||
isLoggedIn,
|
||||
setToken,
|
||||
clearToken,
|
||||
setUserInfo,
|
||||
logout
|
||||
}
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue