feat: 初始化代码

master
Lxy 2 months ago
commit 65dd83bb37

@ -0,0 +1,37 @@
# AmazingData 数据服务平台 - 环境变量配置
# 复制此文件为 .env 并修改相应配置
# ==================== 应用配置 ====================
APP_NAME=AmazingData Platform
APP_ENV=development
DEBUG=true
SECRET_KEY=your-secret-key-change-in-production
# ==================== 数据库配置 (MySQL) ====================
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=root123
DB_NAME=amazingdata_platform
# ==================== AmazingData SDK 配置 ====================
AMAZING_DATA_USERNAME=11200008169
AMAZING_DATA_PASSWORD=11200008169@2026
AMAZING_DATA_HOST=140.206.44.234
AMAZING_DATA_PORT=8600
# ==================== 服务配置 ====================
BACKEND_HOST=0.0.0.0
BACKEND_PORT=8000
FRONTEND_PORT=3000
# ==================== 数据配置 ====================
DATA_SAVE_PATH=./data
REALTIME_SAVE_DAYS=7
CACHE_AUTO_SAVE_INTERVAL=60
MAX_CONCURRENT_TASKS=5
# ==================== JWT 配置 ====================
JWT_SECRET_KEY=your-jwt-secret-key
JWT_ALGORITHM=HS256
JWT_EXPIRE_MINUTES=1440

42
.gitignore vendored

@ -0,0 +1,42 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
*.egg
*.egg-info/
dist/
build/
*.whl
# Virtual Environment
venv/
env/
ENV/
# IDE
.vscode/
.idea/
*.swp
*.swo
# Environment
.env
.env.local
# Node
node_modules/
frontend/dist/
# Data
data/*.json
data/**/*.json
*.db
*.sqlite
# OS
.DS_Store
Thumbs.db
# Logs
*.log

@ -0,0 +1,17 @@
FROM python:3.11-slim
WORKDIR /app
# 安装依赖
COPY backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 复制后端代码
COPY backend/ ./backend/
# 创建数据目录
RUN mkdir -p /app/data
EXPOSE 8000
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]

@ -0,0 +1,20 @@
FROM node:18-alpine AS builder
WORKDIR /app
# 复制前端代码
COPY frontend/package*.json ./
RUN npm install --registry=https://registry.npmmirror.com
COPY frontend/ ./
RUN npm run build
# 使用 Nginx 提供静态文件
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Binary file not shown.

@ -0,0 +1,11 @@
from fastapi import APIRouter
from backend.api import auth, historical, realtime, batch, cache, settings
api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["认证"])
api_router.include_router(historical.router, prefix="/historical", tags=["历史数据"])
api_router.include_router(realtime.router, prefix="/realtime", tags=["实时订阅"])
api_router.include_router(batch.router, prefix="/batch", tags=["批量操作"])
api_router.include_router(cache.router, prefix="/cache", tags=["缓存管理"])
api_router.include_router(settings.router, prefix="/settings", tags=["系统配置"])

@ -0,0 +1,102 @@
"""
AmazingData 数据服务平台 - 认证 API
"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from datetime import datetime
from backend.models.database import get_db
from backend.models.tables import User
from backend.models.schemas import LoginRequest, LoginResponse, UserInfo, BaseResponse
from backend.auth.jwt_handler import verify_password, create_access_token
from backend.auth.dependencies import get_current_active_user
router = APIRouter()
@router.post("/login", response_model=LoginResponse)
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
"""用户登录"""
user = db.query(User).filter(User.username == form_data.username).first()
if not user or not verify_password(form_data.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
# 更新最后登录时间
user.last_login = datetime.utcnow()
db.commit()
# 创建 Token
access_token = create_access_token(data={"sub": user.username, "role": user.role})
return LoginResponse(
access_token=access_token,
token_type="bearer",
user_info={
"id": user.id,
"username": user.username,
"role": user.role,
"is_active": user.is_active,
"last_login": user.last_login
}
)
@router.post("/login-json", response_model=LoginResponse)
async def login_json(request: LoginRequest, db: Session = Depends(get_db)):
"""用户登录JSON 格式)"""
user = db.query(User).filter(User.username == request.username).first()
if not user or not verify_password(request.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password"
)
if not user.is_active:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
user.last_login = datetime.utcnow()
db.commit()
access_token = create_access_token(data={"sub": user.username, "role": user.role})
return LoginResponse(
access_token=access_token,
token_type="bearer",
user_info={
"id": user.id,
"username": user.username,
"role": user.role,
"is_active": user.is_active,
"last_login": user.last_login
}
)
@router.get("/me", response_model=BaseResponse)
async def get_current_user_info(current_user: User = Depends(get_current_active_user)):
"""获取当前用户信息"""
return BaseResponse(
data={
"id": current_user.id,
"username": current_user.username,
"role": current_user.role,
"is_active": current_user.is_active,
"last_login": current_user.last_login
}
)
@router.post("/logout", response_model=BaseResponse)
async def logout(current_user: User = Depends(get_current_active_user)):
"""用户登出"""
return BaseResponse(message="Logout successful")

@ -0,0 +1,163 @@
"""
AmazingData 数据服务平台 - 批量操作 API
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import Optional, List
from datetime import datetime
from backend.models.database import get_db
from backend.models.schemas import (
BaseResponse, BatchTaskRequest, BatchTaskStatus
)
from backend.models.tables import BatchTask, User
from backend.auth.dependencies import get_current_user
from backend.services.data_service import data_service
router = APIRouter()
@router.post("/execute", response_model=BaseResponse)
async def execute_batch_task(
request: BatchTaskRequest,
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user)
):
"""执行批量任务"""
import threading
# 创建任务记录
task = BatchTask(
task_type=request.task_type,
task_params={
"codes": request.codes,
"use_main_contract": request.use_main_contract,
"trading_days": request.trading_days,
"batch_size": request.batch_size
},
status="pending",
output_path=request.save_path,
created_by=current_user.username if current_user else "anonymous"
)
db.add(task)
db.commit()
db.refresh(task)
def batch_worker():
"""批量工作线程"""
try:
task.status = "running"
task.started_at = datetime.utcnow()
db.commit()
if request.task_type == "stock":
result = data_service.batch_get_stock_kline(
codes=request.codes,
trading_days=request.trading_days,
save_path=request.save_path,
batch_size=request.batch_size
)
elif request.task_type == "future":
result = data_service.batch_get_future_kline(
underlying_codes=request.codes,
use_main_contract=request.use_main_contract,
trading_days=request.trading_days,
save_path=request.save_path
)
else:
task.status = "error"
task.error_message = f"Unknown task type: {request.task_type}"
db.commit()
return
if "error" in result:
task.status = "error"
task.error_message = result["error"]
else:
task.status = "completed"
task.success_count = len(result)
task.completed_at = datetime.utcnow()
db.commit()
except Exception as e:
task.status = "error"
task.error_message = str(e)
db.commit()
thread = threading.Thread(target=batch_worker, daemon=True)
thread.start()
return BaseResponse(
data={
"task_id": task.id,
"status": "pending",
"message": "Batch task queued"
}
)
@router.get("/tasks", response_model=BaseResponse)
async def list_batch_tasks(
status: Optional[str] = None,
task_type: Optional[str] = None,
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user)
):
"""列出批量任务"""
query = db.query(BatchTask)
if status:
query = query.filter(BatchTask.status == status)
if task_type:
query = query.filter(BatchTask.task_type == task_type)
tasks = query.order_by(BatchTask.created_at.desc()).all()
return BaseResponse(data={
"tasks": [
{
"id": t.id,
"task_type": t.task_type,
"total_count": t.total_count,
"processed_count": t.processed_count,
"success_count": t.success_count,
"failed_count": t.failed_count,
"status": t.status,
"output_path": t.output_path,
"error_message": t.error_message,
"started_at": t.started_at.isoformat() if t.started_at else None,
"completed_at": t.completed_at.isoformat() if t.completed_at else None,
"created_at": t.created_at.isoformat() if t.created_at else None
}
for t in tasks
],
"total": len(tasks)
})
@router.get("/tasks/{task_id}", response_model=BaseResponse)
async def get_batch_task(
task_id: int,
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user)
):
"""获取批量任务详情"""
task = db.query(BatchTask).filter(BatchTask.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return BaseResponse(data={
"id": task.id,
"task_type": task.task_type,
"task_params": task.task_params,
"total_count": task.total_count,
"processed_count": task.processed_count,
"success_count": task.success_count,
"failed_count": task.failed_count,
"status": task.status,
"output_path": task.output_path,
"error_message": task.error_message,
"started_at": task.started_at.isoformat() if task.started_at else None,
"completed_at": task.completed_at.isoformat() if task.completed_at else None,
"created_at": task.created_at.isoformat() if task.created_at else None
})

@ -0,0 +1,170 @@
"""
AmazingData 数据服务平台 - 缓存管理 API
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func
from typing import Optional
import os
import json
from datetime import datetime
from backend.models.database import get_db
from backend.models.schemas import BaseResponse, CacheFileItem, CacheStats
from backend.models.tables import CacheRecord, User
from backend.auth.dependencies import get_current_user
from backend.config import settings
router = APIRouter()
@router.get("/list", response_model=BaseResponse)
async def list_cache_files(
file_type: Optional[str] = Query(None, description="文件类型: stock/future/realtime"),
trading_day: Optional[str] = Query(None, description="交易日"),
code: Optional[str] = Query(None, description="代码"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user)
):
"""列出缓存文件"""
query = db.query(CacheRecord)
if file_type:
query = query.filter(CacheRecord.file_type == file_type)
if trading_day:
query = query.filter(CacheRecord.trading_day == trading_day)
if code:
query = query.filter(CacheRecord.code == code)
total = query.count()
records = query.order_by(CacheRecord.created_at.desc())\
.offset((page - 1) * page_size)\
.limit(page_size)\
.all()
return BaseResponse(data={
"files": [
{
"id": r.id,
"filename": r.filename,
"file_type": r.file_type,
"trading_day": r.trading_day,
"code": r.code,
"period": r.period,
"record_count": r.record_count,
"file_size": r.file_size,
"file_path": r.file_path,
"created_at": r.created_at.isoformat() if r.created_at else None
}
for r in records
],
"total": total,
"page": page,
"page_size": page_size
})
@router.get("/stats", response_model=BaseResponse)
async def get_cache_stats(
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user)
):
"""获取缓存统计"""
# 数据库统计
total_files = db.query(CacheRecord).count()
total_size = db.query(CacheRecord).with_entities(
func.coalesce(func.sum(CacheRecord.file_size), 0)
).scalar()
# 按类型统计
by_type = {}
type_stats = db.query(CacheRecord.file_type, func.count(CacheRecord.id))\
.group_by(CacheRecord.file_type).all()
for t, count in type_stats:
by_type[t] = count
# 按日期统计
by_day = {}
day_stats = db.query(CacheRecord.trading_day, func.count(CacheRecord.id))\
.filter(CacheRecord.trading_day.isnot(None))\
.group_by(CacheRecord.trading_day).all()
for d, count in day_stats:
by_day[d] = count
# 文件系统统计
data_path = settings.DATA_SAVE_PATH
disk_stats = {}
if os.path.exists(data_path):
for root, dirs, files in os.walk(data_path):
for f in files:
if f.endswith('.json'):
filepath = os.path.join(root, f)
disk_stats[f] = os.path.getsize(filepath)
return BaseResponse(data={
"total_files": total_files,
"total_size": total_size,
"by_type": by_type,
"by_day": by_day,
"disk_files": len(disk_stats),
"disk_size": sum(disk_stats.values())
})
@router.get("/data/{file_type}/{trading_day}", response_model=BaseResponse)
async def get_cache_data(
file_type: str,
trading_day: str,
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user)
):
"""获取缓存数据"""
# 查找文件
if file_type == "stock":
filename = f"kline_{trading_day}.json"
filepath = os.path.join(settings.DATA_SAVE_PATH, "stock", filename)
elif file_type == "future":
filename = f"futures_{trading_day}.json"
filepath = os.path.join(settings.DATA_SAVE_PATH, "future", filename)
else:
raise HTTPException(status_code=400, detail="Invalid file type")
if not os.path.exists(filepath):
raise HTTPException(status_code=404, detail="File not found")
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
return BaseResponse(data=data)
@router.delete("/cleanup", response_model=BaseResponse)
async def cleanup_old_cache(
days: int = Query(30, ge=1),
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user)
):
"""清理旧缓存"""
from datetime import timedelta
cutoff_date = datetime.utcnow() - timedelta(days=days)
# 删除数据库记录
old_records = db.query(CacheRecord).filter(CacheRecord.created_at < cutoff_date).all()
deleted_count = 0
for record in old_records:
# 删除文件
if record.file_path and os.path.exists(record.file_path):
try:
os.remove(record.file_path)
except Exception:
pass
db.delete(record)
deleted_count += 1
db.commit()
return BaseResponse(data={"deleted_count": deleted_count})

@ -0,0 +1,113 @@
"""
AmazingData 数据服务平台 - 历史数据 API
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import Optional, List
from backend.models.database import get_db
from backend.models.schemas import (
BaseResponse, SingleKlineRequest, BatchStockRequest,
BatchFutureRequest, KlineDataResponse
)
from backend.services.data_service import data_service
from backend.auth.dependencies import get_current_user
from backend.models.tables import User
router = APIRouter()
@router.post("/single", response_model=BaseResponse)
async def get_single_kline(
request: SingleKlineRequest,
current_user: Optional[User] = Depends(get_current_user)
):
"""获取单只股票/期货K线数据"""
result = data_service.get_single_kline(
code=request.code,
trading_day=request.trading_day,
period=request.period.value,
save_path=request.save_path
)
if "error" in result:
return BaseResponse(code=500, message=result["error"], data=result)
return BaseResponse(data=result)
@router.post("/batch-stocks", response_model=BaseResponse)
async def batch_get_stock_kline(
request: BatchStockRequest,
current_user: Optional[User] = Depends(get_current_user)
):
"""批量获取股票K线数据"""
result = data_service.batch_get_stock_kline(
codes=request.codes,
trading_days=request.trading_days,
save_path=request.save_path,
batch_size=request.batch_size
)
if "error" in result:
return BaseResponse(code=500, message=result["error"], data=result)
return BaseResponse(data={"files": result, "count": len(result)})
@router.post("/batch-futures", response_model=BaseResponse)
async def batch_get_future_kline(
request: BatchFutureRequest,
current_user: Optional[User] = Depends(get_current_user)
):
"""批量获取期货K线数据"""
result = data_service.batch_get_future_kline(
underlying_codes=request.underlying_codes,
use_main_contract=request.use_main_contract,
trading_days=request.trading_days,
save_path=request.save_path
)
if "error" in result:
return BaseResponse(code=500, message=result["error"], data=result)
return BaseResponse(data={"files": result, "count": len(result)})
@router.get("/stock-codes", response_model=BaseResponse)
async def get_stock_codes(current_user: Optional[User] = Depends(get_current_user)):
"""获取股票代码列表"""
codes = data_service.get_stock_codes()
return BaseResponse(data={"codes": codes, "count": len(codes)})
@router.get("/future-codes", response_model=BaseResponse)
async def get_future_codes(current_user: Optional[User] = Depends(get_current_user)):
"""获取期货代码列表"""
codes = data_service.get_future_codes()
return BaseResponse(data={"codes": codes, "count": len(codes)})
@router.get("/trading-days", response_model=BaseResponse)
async def get_trading_days(
year: Optional[int] = None,
month: Optional[int] = None,
current_user: Optional[User] = Depends(get_current_user)
):
"""获取交易日列表(简化实现)"""
from datetime import date, timedelta
today = date.today()
if year:
target_date = date(year, month or 1, 1)
else:
target_date = today
# 简单返回最近30天实际应查询交易日历
trading_days = []
for i in range(30):
d = target_date - timedelta(days=i)
if d.weekday() < 5: # 排除周末
trading_days.append(d.strftime('%Y%m%d'))
return BaseResponse(data={"trading_days": trading_days})

@ -0,0 +1,227 @@
"""
AmazingData 数据服务平台 - 实时订阅 API
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import Optional, List
from datetime import datetime
from backend.models.database import get_db
from backend.models.schemas import (
BaseResponse, SubscribeRequest, SubscribeResponse, TaskStatus
)
from backend.models.tables import SubscriptionTask, User
from backend.auth.dependencies import get_current_user
from backend.services.data_service import data_service
router = APIRouter()
# 存储运行中的订阅任务
active_subscriptions = {}
@router.post("/subscribe", response_model=BaseResponse)
async def subscribe_kline(
request: SubscribeRequest,
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user)
):
"""启动实时K线订阅任务"""
try:
import AmazingData as ad
import threading
import os
import json
# 创建任务记录
task_name = request.task_name or f"subscribe_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
task = SubscriptionTask(
task_name=task_name,
codes=[c.value if hasattr(c, 'value') else c for c in request.codes],
periods=[p.value if hasattr(p, 'value') else p for p in request.periods],
save_path=request.save_path,
duration=request.duration,
save_interval=request.save_interval,
status="running",
started_at=datetime.utcnow(),
created_by=current_user.username if current_user else "anonymous"
)
db.add(task)
db.commit()
db.refresh(task)
# 启动订阅线程
def subscribe_worker():
"""订阅工作线程"""
try:
# 登录
ad.login(
username=data_service.AMAZING_DATA_USERNAME if hasattr(data_service, 'AMAZING_DATA_USERNAME') else "11200008169",
password=data_service.AMAZING_DATA_PASSWORD if hasattr(data_service, 'AMAZING_DATA_PASSWORD') else "11200008169@2026",
host=data_service.AMAZING_DATA_HOST if hasattr(data_service, 'AMAZING_DATA_HOST') else "140.206.44.234",
port=data_service.AMAZING_DATA_PORT if hasattr(data_service, 'AMAZING_DATA_PORT') else 8600
)
save_path = request.save_path or "./data/realtime"
os.makedirs(save_path, exist_ok=True)
# 周期映射
period_map = {
"min1": ad.constant.Period.min1.value if hasattr(ad.constant, 'Period') else 1,
"min5": ad.constant.Period.min5.value if hasattr(ad.constant, 'Period') else 5,
"min15": ad.constant.Period.min15.value if hasattr(ad.constant, 'Period') else 15,
"min30": ad.constant.Period.min30.value if hasattr(ad.constant, 'Period') else 30,
"min60": ad.constant.Period.min60.value if hasattr(ad.constant, 'Period') else 60,
}
# 为每个品种和周期创建订阅
for code in request.codes:
code_val = code.value if hasattr(code, 'value') else code
for period in request.periods:
period_val = period.value if hasattr(period, 'value') else period
period_value = period_map.get(period_val, 5)
def on_data(data):
"""数据回调"""
try:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"{code_val.replace('.', '_')}_{period_val}_{timestamp}.json"
filepath = os.path.join(save_path, filename)
result = {
"code": code_val,
"period": period_val,
"timestamp": datetime.now().isoformat(),
"data": data
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"Save data error: {e}")
# 订阅K线
ad.subscribe_kline(
code=code_val,
period=period_value,
callback=on_data
)
# 保持运行
import time
if request.duration > 0:
time.sleep(request.duration)
else:
# 无限运行,直到被停止
while task.status == "running":
time.sleep(1)
# 取消订阅
for code in request.codes:
code_val = code.value if hasattr(code, 'value') else code
ad.unsubscribe_kline(code=code_val)
ad.logout(data_service.AMAZING_DATA_USERNAME if hasattr(data_service, 'AMAZING_DATA_USERNAME') else "11200008169")
# 更新任务状态
task.status = "stopped"
task.stopped_at = datetime.utcnow()
db.commit()
except Exception as e:
task.status = "error"
task.stopped_at = datetime.utcnow()
db.commit()
print(f"Subscribe error: {e}")
thread = threading.Thread(target=subscribe_worker, daemon=True)
thread.start()
active_subscriptions[task.id] = thread
return BaseResponse(
data={
"task_id": task.id,
"task_name": task_name,
"status": "running",
"message": "Subscription started"
}
)
except Exception as e:
return BaseResponse(code=500, message=str(e))
@router.post("/stop/{task_id}", response_model=BaseResponse)
async def stop_subscription(
task_id: int,
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user)
):
"""停止订阅任务"""
task = db.query(SubscriptionTask).filter(SubscriptionTask.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
task.status = "stopped"
task.stopped_at = datetime.utcnow()
db.commit()
return BaseResponse(message="Subscription stopped")
@router.get("/tasks", response_model=BaseResponse)
async def list_subscription_tasks(
status: Optional[str] = None,
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user)
):
"""列出订阅任务"""
query = db.query(SubscriptionTask)
if status:
query = query.filter(SubscriptionTask.status == status)
tasks = query.order_by(SubscriptionTask.created_at.desc()).all()
return BaseResponse(data={
"tasks": [
{
"id": t.id,
"task_name": t.task_name,
"codes": t.codes,
"periods": t.periods,
"status": t.status,
"started_at": t.started_at.isoformat() if t.started_at else None,
"stopped_at": t.stopped_at.isoformat() if t.stopped_at else None,
"created_at": t.created_at.isoformat() if t.created_at else None
}
for t in tasks
],
"total": len(tasks)
})
@router.get("/tasks/{task_id}", response_model=BaseResponse)
async def get_subscription_task(
task_id: int,
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user)
):
"""获取订阅任务详情"""
task = db.query(SubscriptionTask).filter(SubscriptionTask.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return BaseResponse(data={
"id": task.id,
"task_name": task.task_name,
"codes": task.codes,
"periods": task.periods,
"save_path": task.save_path,
"duration": task.duration,
"save_interval": task.save_interval,
"status": task.status,
"started_at": task.started_at.isoformat() if task.started_at else None,
"stopped_at": task.stopped_at.isoformat() if task.stopped_at else None,
"created_at": task.created_at.isoformat() if task.created_at else None
})

@ -0,0 +1,131 @@
"""
AmazingData 数据服务平台 - 系统配置 API
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
from backend.models.database import get_db
from backend.models.schemas import BaseResponse, ConfigItem, ConfigUpdateRequest, TestConnectionResponse
from backend.models.tables import SystemConfig, User
from backend.auth.dependencies import get_current_user, require_admin
from backend.services.config_service import config_service
from backend.services.data_service import data_service
router = APIRouter()
@router.get("/list", response_model=BaseResponse)
async def list_configs(
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user)
):
"""获取所有配置"""
configs = config_service.get_all_configs(db)
return BaseResponse(data={
"configs": [
{
"id": c.id,
"config_key": c.config_key,
"config_value": c.config_value,
"config_type": c.config_type,
"description": c.description
}
for c in configs
],
"total": len(configs)
})
@router.get("/{key}", response_model=BaseResponse)
async def get_config(
key: str,
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user)
):
"""获取单个配置"""
config = config_service.get_config(db, key)
if not config:
raise HTTPException(status_code=404, detail="Config not found")
return BaseResponse(data={
"id": config.id,
"config_key": config.config_key,
"config_value": config.config_value,
"config_type": config.config_type,
"description": config.description
})
@router.put("/{key}", response_model=BaseResponse)
async def update_config(
key: str,
request: ConfigUpdateRequest,
db: Session = Depends(get_db),
current_user: User = Depends(require_admin)
):
"""更新配置(需要管理员权限)"""
config = config_service.update_config(db, key, request.config_value)
if not config:
raise HTTPException(status_code=404, detail="Config not found")
return BaseResponse(data={
"id": config.id,
"config_key": config.config_key,
"config_value": config.config_value,
"config_type": config.config_type,
"description": config.description
})
@router.put("/batch", response_model=BaseResponse)
async def batch_update_configs(
configs: Dict[str, str],
db: Session = Depends(get_db),
current_user: User = Depends(require_admin)
):
"""批量更新配置(需要管理员权限)"""
success = config_service.batch_update_configs(db, configs)
if not success:
return BaseResponse(code=500, message="Batch update failed")
return BaseResponse(message="Batch update successful")
@router.get("/amazing-data/config", response_model=BaseResponse)
async def get_amazing_data_config(
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user)
):
"""获取 AmazingData 连接配置"""
config = config_service.get_amazing_data_config(db)
# 隐藏密码
config["password"] = "***"
return BaseResponse(data=config)
@router.post("/test-connection", response_model=TestConnectionResponse)
async def test_connection(
current_user: Optional[User] = Depends(get_current_user)
):
"""测试 AmazingData 连接"""
result = data_service.test_connection()
return TestConnectionResponse(**result)
@router.get("/system/info", response_model=BaseResponse)
async def get_system_info(
current_user: Optional[User] = Depends(get_current_user)
):
"""获取系统信息"""
import platform
import sys
return BaseResponse(data={
"platform": platform.system(),
"platform_version": platform.version(),
"python_version": platform.python_version(),
"app_env": "development",
"app_name": "AmazingData Platform"
})

@ -0,0 +1,62 @@
"""
AmazingData 数据服务平台 - 认证依赖
"""
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from backend.models.database import get_db
from backend.models.tables import User
from backend.auth.jwt_handler import decode_access_token
from typing import Optional
# OAuth2 密码流
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login", auto_error=False)
async def get_current_user(
token: Optional[str] = Depends(oauth2_scheme),
db: Session = Depends(get_db)
) -> Optional[User]:
"""获取当前用户(可选认证)"""
if not token:
return None
payload = decode_access_token(token)
if payload is None:
return None
username: str = payload.get("sub")
if username is None:
return None
user = db.query(User).filter(User.username == username).first()
if user is None or not user.is_active:
return None
return user
async def get_current_active_user(
current_user: Optional[User] = Depends(get_current_user)
) -> User:
"""获取当前活跃用户(需要认证)"""
if current_user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return current_user
async def require_admin(
current_user: User = Depends(get_current_active_user)
) -> User:
"""要求管理员权限"""
if current_user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin privileges required"
)
return current_user

@ -0,0 +1,44 @@
"""
AmazingData 数据服务平台 - JWT 认证处理
"""
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from backend.config import settings
# 密码加密上下文
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""验证密码"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""获取密码哈希"""
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""创建 JWT Token"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.JWT_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
return encoded_jwt
def decode_access_token(token: str) -> Optional[dict]:
"""解码 JWT Token"""
try:
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
return payload
except JWTError:
return None

@ -0,0 +1,65 @@
"""
AmazingData 数据服务平台 - 配置管理
"""
from pydantic_settings import BaseSettings
from typing import Optional
import os
class Settings(BaseSettings):
"""应用配置"""
# 应用配置
APP_NAME: str = "AmazingData Platform"
APP_ENV: str = "development"
DEBUG: bool = True
SECRET_KEY: str = "your-secret-key-change-in-production"
# 数据库配置
DB_HOST: str = "localhost"
DB_PORT: int = 3306
DB_USER: str = "root"
DB_PASSWORD: str = "root123"
DB_NAME: str = "amazingdata_platform"
# AmazingData SDK 配置
AMAZING_DATA_USERNAME: str = "11200008169"
AMAZING_DATA_PASSWORD: str = "11200008169@2026"
AMAZING_DATA_HOST: str = "140.206.44.234"
AMAZING_DATA_PORT: int = 8600
# 服务配置
BACKEND_HOST: str = "0.0.0.0"
BACKEND_PORT: int = 8000
FRONTEND_PORT: int = 3000
# 数据配置
DATA_SAVE_PATH: str = "./data"
REALTIME_SAVE_DAYS: int = 7
CACHE_AUTO_SAVE_INTERVAL: int = 60
MAX_CONCURRENT_TASKS: int = 5
# JWT 配置
JWT_SECRET_KEY: str = "your-jwt-secret-key"
JWT_ALGORITHM: str = "HS256"
JWT_EXPIRE_MINUTES: int = 1440
@property
def DATABASE_URL(self) -> str:
"""获取数据库连接 URL"""
return f"mysql+pymysql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}?charset=utf8mb4"
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = True
# 创建全局配置实例
settings = Settings()
def get_settings() -> Settings:
"""获取配置(用于依赖注入)"""
return settings

@ -0,0 +1,95 @@
"""
AmazingData 数据服务平台 - FastAPI 主应用
"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from contextlib import asynccontextmanager
import os
from backend.config import settings
from backend.models.database import init_db
from backend.api import api_router
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期管理"""
# 启动时执行
print("Starting AmazingData Platform...")
# 初始化数据库
try:
init_db()
print("Database initialized successfully")
except Exception as e:
print(f"Database initialization warning: {e}")
# 创建数据目录
os.makedirs(os.path.join(settings.DATA_SAVE_PATH, "single"), exist_ok=True)
os.makedirs(os.path.join(settings.DATA_SAVE_PATH, "stock"), exist_ok=True)
os.makedirs(os.path.join(settings.DATA_SAVE_PATH, "future"), exist_ok=True)
os.makedirs(os.path.join(settings.DATA_SAVE_PATH, "realtime"), exist_ok=True)
os.makedirs(os.path.join(settings.DATA_SAVE_PATH, "batch"), exist_ok=True)
print("AmazingData Platform started successfully!")
yield
# 关闭时执行
print("Shutting down AmazingData Platform...")
# 创建 FastAPI 应用
app = FastAPI(
title="AmazingData Platform",
description="AmazingData 数据服务平台 - 提供股票、期货K线数据获取和实时订阅服务",
version="1.0.0",
lifespan=lifespan
)
# CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 注册路由
app.include_router(api_router, prefix="/api/v1")
@app.get("/")
async def root():
"""根路径"""
return {
"name": "AmazingData Platform",
"version": "1.0.0",
"status": "running",
"docs": "/docs"
}
@app.get("/health")
async def health_check():
"""健康检查"""
return {"status": "healthy"}
# 挂载静态文件(前端构建产物)
frontend_dist = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "frontend", "dist")
if os.path.exists(frontend_dist):
app.mount("/", StaticFiles(directory=frontend_dist, html=True), name="frontend")
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"backend.main:app",
host=settings.BACKEND_HOST,
port=settings.BACKEND_PORT,
reload=settings.DEBUG
)

@ -0,0 +1,23 @@
from backend.models.database import Base, engine, SessionLocal, get_db, init_db
from backend.models.tables import (
SystemConfig,
User,
SubscriptionTask,
BatchTask,
CacheRecord,
OperationLog
)
__all__ = [
"Base",
"engine",
"SessionLocal",
"get_db",
"init_db",
"SystemConfig",
"User",
"SubscriptionTask",
"BatchTask",
"CacheRecord",
"OperationLog"
]

@ -0,0 +1,38 @@
"""
AmazingData 数据服务平台 - 数据库配置
"""
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from backend.config import settings
# 创建数据库引擎
engine = create_engine(
settings.DATABASE_URL,
pool_pre_ping=True,
pool_recycle=3600,
echo=settings.DEBUG
)
# 创建会话工厂
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# 创建基类
Base = declarative_base()
def get_db():
"""获取数据库会话(用于依赖注入)"""
db = SessionLocal()
try:
yield db
finally:
db.close()
def init_db():
"""初始化数据库(创建所有表)"""
from backend.models import tables # 导入所有表模型
Base.metadata.create_all(bind=engine)
print("Database tables created successfully!")

@ -0,0 +1,210 @@
"""
AmazingData 数据服务平台 - Pydantic 数据模型
"""
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
from datetime import datetime
from enum import Enum
# ==================== 通用响应模型 ====================
class ResponseCode(int, Enum):
SUCCESS = 200
ERROR = 500
UNAUTHORIZED = 401
FORBIDDEN = 403
NOT_FOUND = 404
class BaseResponse(BaseModel):
"""基础响应模型"""
code: int = ResponseCode.SUCCESS
message: str = "success"
data: Optional[Any] = None
# ==================== 认证模型 ====================
class LoginRequest(BaseModel):
"""登录请求"""
username: str = Field(..., min_length=3, max_length=50)
password: str = Field(..., min_length=6)
class LoginResponse(BaseModel):
"""登录响应"""
access_token: str
token_type: str = "bearer"
user_info: Dict[str, Any]
class UserInfo(BaseModel):
"""用户信息"""
id: int
username: str
role: str
is_active: bool
last_login: Optional[datetime]
# ==================== 历史数据模型 ====================
class PeriodEnum(str, Enum):
"""K线周期枚举"""
DAILY = "day"
MIN1 = "min1"
MIN5 = "min5"
MIN15 = "min15"
MIN30 = "min30"
MIN60 = "min60"
class SingleKlineRequest(BaseModel):
"""单只K线请求"""
code: str = Field(..., description="代码,如 000001.SZ 或 ag2605.SHF")
trading_day: Optional[str] = Field(None, description="交易日,格式 YYYYMMDD默认最近交易日")
period: PeriodEnum = Field(PeriodEnum.DAILY, description="K线周期")
save_path: Optional[str] = Field("./data/single", description="保存路径")
class BatchStockRequest(BaseModel):
"""批量股票请求"""
codes: Optional[List[str]] = Field(None, description="股票代码列表None表示全部A股")
trading_days: Optional[List[str]] = Field(None, description="交易日列表")
save_path: Optional[str] = Field("./data/stock", description="保存路径")
batch_size: int = Field(100, description="批次大小")
class BatchFutureRequest(BaseModel):
"""批量期货请求"""
underlying_codes: Optional[List[str]] = Field(None, description="品种代码列表")
use_main_contract: bool = Field(True, description="是否使用主力合约")
trading_days: Optional[List[str]] = Field(None, description="交易日列表")
save_path: Optional[str] = Field("./data/future", description="保存路径")
class KlineDataResponse(BaseModel):
"""K线数据响应"""
code: str
trading_day: str
period: str
count: int
data: List[Dict[str, Any]]
file_path: str
# ==================== 实时订阅模型 ====================
class SubscribePeriodEnum(str, Enum):
"""订阅周期枚举"""
MIN1 = "min1"
MIN5 = "min5"
MIN15 = "min15"
MIN30 = "min30"
MIN60 = "min60"
class SubscribeRequest(BaseModel):
"""订阅请求"""
codes: List[str] = Field(..., description="品种代码列表")
periods: List[SubscribePeriodEnum] = Field([SubscribePeriodEnum.MIN5], description="订阅周期")
save_path: Optional[str] = Field("./data/realtime", description="保存路径")
duration: int = Field(0, description="运行时长(秒)0=无限")
save_interval: int = Field(60, description="保存间隔(秒)")
task_name: Optional[str] = Field(None, description="任务名称")
class SubscribeResponse(BaseModel):
"""订阅响应"""
task_id: int
task_name: str
status: str
message: str
class TaskStatus(BaseModel):
"""任务状态"""
id: int
task_name: str
codes: List[str]
periods: List[str]
status: str
started_at: Optional[datetime]
stopped_at: Optional[datetime]
created_at: datetime
# ==================== 批量任务模型 ====================
class BatchTaskRequest(BaseModel):
"""批量任务请求"""
task_type: str = Field(..., description="任务类型: stock/future")
codes: Optional[List[str]] = Field(None, description="代码列表")
use_main_contract: bool = Field(True, description="是否主力合约")
trading_days: Optional[List[str]] = Field(None, description="交易日列表")
save_path: Optional[str] = Field("./data/batch", description="保存路径")
batch_size: int = Field(100, description="批次大小")
class BatchTaskStatus(BaseModel):
"""批量任务状态"""
id: int
task_type: str
total_count: int
processed_count: int
success_count: int
failed_count: int
status: str
output_path: Optional[str]
error_message: Optional[str]
started_at: Optional[datetime]
completed_at: Optional[datetime]
# ==================== 缓存管理模型 ====================
class CacheFileItem(BaseModel):
"""缓存文件项"""
id: int
filename: str
file_type: str
trading_day: Optional[str]
code: Optional[str]
period: Optional[str]
record_count: int
file_size: int
file_path: str
created_at: datetime
class CacheStats(BaseModel):
"""缓存统计"""
total_files: int
total_size: int
by_type: Dict[str, int]
by_day: Dict[str, int]
# ==================== 系统配置模型 ====================
class ConfigItem(BaseModel):
"""配置项"""
id: int
config_key: str
config_value: str
config_type: str
description: Optional[str]
class ConfigUpdateRequest(BaseModel):
"""配置更新请求"""
config_value: str
class TestConnectionResponse(BaseModel):
"""连接测试响应"""
success: bool
message: str
details: Optional[Dict[str, Any]] = None

@ -0,0 +1,106 @@
"""
AmazingData 数据服务平台 - 数据库表模型
"""
from sqlalchemy import Column, Integer, String, Text, DateTime, BigInteger, JSON, Boolean, ForeignKey, Index
from sqlalchemy.sql import func
from backend.models.database import Base
class SystemConfig(Base):
"""系统配置表"""
__tablename__ = "system_config"
id = Column(Integer, primary_key=True, autoincrement=True)
config_key = Column(String(100), unique=True, nullable=False, index=True)
config_value = Column(Text)
config_type = Column(String(20), default="string")
description = Column(String(255))
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class User(Base):
"""用户表"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True)
username = Column(String(50), unique=True, nullable=False, index=True)
password_hash = Column(String(255), nullable=False)
role = Column(String(20), default="user")
is_active = Column(Boolean, default=True)
last_login = Column(DateTime)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class SubscriptionTask(Base):
"""订阅任务表"""
__tablename__ = "subscription_tasks"
id = Column(Integer, primary_key=True, autoincrement=True)
task_name = Column(String(100), nullable=False)
codes = Column(JSON, nullable=False)
periods = Column(JSON, nullable=False)
save_path = Column(String(255))
duration = Column(Integer, default=0)
save_interval = Column(Integer, default=60)
status = Column(String(20), default="pending", index=True)
subscription_id = Column(String(100))
started_at = Column(DateTime)
stopped_at = Column(DateTime)
created_by = Column(String(50))
created_at = Column(DateTime, server_default=func.now(), index=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class BatchTask(Base):
"""批量任务表"""
__tablename__ = "batch_tasks"
id = Column(Integer, primary_key=True, autoincrement=True)
task_type = Column(String(20), nullable=False, index=True)
task_params = Column(JSON)
total_count = Column(Integer, default=0)
processed_count = Column(Integer, default=0)
success_count = Column(Integer, default=0)
failed_count = Column(Integer, default=0)
status = Column(String(20), default="pending", index=True)
output_path = Column(String(255))
error_message = Column(Text)
started_at = Column(DateTime)
completed_at = Column(DateTime)
created_by = Column(String(50))
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class CacheRecord(Base):
"""数据缓存记录表"""
__tablename__ = "cache_records"
id = Column(Integer, primary_key=True, autoincrement=True)
filename = Column(String(255), nullable=False, index=True)
file_type = Column(String(20), nullable=False, index=True)
trading_day = Column(String(8), index=True)
code = Column(String(20))
period = Column(String(10))
record_count = Column(Integer, default=0)
file_size = Column(BigInteger, default=0)
file_path = Column(String(255))
created_at = Column(DateTime, server_default=func.now())
class OperationLog(Base):
"""操作日志表"""
__tablename__ = "operation_logs"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, index=True)
operation = Column(String(50), nullable=False, index=True)
resource = Column(String(100))
detail = Column(Text)
ip_address = Column(String(45))
status = Column(String(20), default="success")
error_message = Column(Text)
created_at = Column(DateTime, server_default=func.now(), index=True)

@ -0,0 +1,15 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
sqlalchemy==2.0.23
pymysql==1.1.0
cryptography==41.0.7
pydantic==2.5.2
pydantic-settings==2.1.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6
websockets==12.0
pandas==2.1.4
numpy==1.26.2
python-dotenv==1.0.0
aiofiles==23.2.1

@ -0,0 +1,4 @@
from backend.services.data_service import AmazingDataPlatformService
from backend.services.config_service import ConfigService
__all__ = ["AmazingDataPlatformService", "ConfigService"]

@ -0,0 +1,65 @@
"""
AmazingData 数据服务平台 - 配置服务
"""
from typing import List, Optional, Dict, Any
from sqlalchemy.orm import Session
from backend.models.tables import SystemConfig
from backend.config import settings
class ConfigService:
"""配置服务"""
@staticmethod
def get_all_configs(db: Session) -> List[SystemConfig]:
"""获取所有配置"""
return db.query(SystemConfig).all()
@staticmethod
def get_config(db: Session, key: str) -> Optional[SystemConfig]:
"""获取单个配置"""
return db.query(SystemConfig).filter(SystemConfig.config_key == key).first()
@staticmethod
def get_config_value(db: Session, key: str, default: str = None) -> str:
"""获取配置值"""
config = db.query(SystemConfig).filter(SystemConfig.config_key == key).first()
return config.config_value if config else default
@staticmethod
def update_config(db: Session, key: str, value: str) -> Optional[SystemConfig]:
"""更新配置"""
config = db.query(SystemConfig).filter(SystemConfig.config_key == key).first()
if config:
config.config_value = value
db.commit()
db.refresh(config)
return config
@staticmethod
def batch_update_configs(db: Session, configs: Dict[str, str]) -> bool:
"""批量更新配置"""
try:
for key, value in configs.items():
config = db.query(SystemConfig).filter(SystemConfig.config_key == key).first()
if config:
config.config_value = value
db.commit()
return True
except Exception:
db.rollback()
return False
@staticmethod
def get_amazing_data_config(db: Session) -> Dict[str, Any]:
"""获取 AmazingData 配置"""
return {
"username": ConfigService.get_config_value(db, "amazing_data_username", settings.AMAZING_DATA_USERNAME),
"password": ConfigService.get_config_value(db, "amazing_data_password", settings.AMAZING_DATA_PASSWORD),
"host": ConfigService.get_config_value(db, "amazing_data_host", settings.AMAZING_DATA_HOST),
"port": int(ConfigService.get_config_value(db, "amazing_data_port", str(settings.AMAZING_DATA_PORT))),
}
config_service = ConfigService()

@ -0,0 +1,323 @@
"""
AmazingData 数据服务平台 - 数据服务
封装现有的 data_service.py 功能
"""
import sys
import os
import json
import threading
from datetime import datetime, date
from typing import Dict, List, Optional, Any, Callable
from pathlib import Path
# 添加父目录到路径以导入现有模块
parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, parent_dir)
from backend.config import settings
class AmazingDataPlatformService:
"""AmazingData 数据平台服务"""
def __init__(self):
self.adapter = None
self.connected = False
self._lock = threading.Lock()
self.data_save_path = settings.DATA_SAVE_PATH
def connect(self) -> bool:
"""连接 AmazingData"""
try:
import AmazingData as ad
ret = ad.login(
username=settings.AMAZING_DATA_USERNAME,
password=settings.AMAZING_DATA_PASSWORD,
host=settings.AMAZING_DATA_HOST,
port=settings.AMAZING_DATA_PORT
)
if ret:
self.connected = True
self.adapter = ad
return True
return False
except Exception as e:
print(f"Connect error: {e}")
return False
def disconnect(self) -> bool:
"""断开连接"""
try:
if self.connected and self.adapter:
import AmazingData as ad
ad.logout(settings.AMAZING_DATA_USERNAME)
self.connected = False
return True
except Exception as e:
print(f"Disconnect error: {e}")
return False
def get_single_kline(
self,
code: str,
trading_day: Optional[str] = None,
period: str = "day",
save_path: Optional[str] = None
) -> Dict[str, Any]:
"""获取单只股票/期货K线数据"""
try:
import AmazingData as ad
if not self.connected:
self.connect()
# 获取历史数据
history = ad.HistoryData()
# 转换周期
period_map = {
"day": ad.constant.Period.day.value,
"min1": ad.constant.Period.min1.value,
"min5": ad.constant.Period.min5.value,
"min15": ad.constant.Period.min15.value,
"min30": ad.constant.Period.min30.value,
"min60": ad.constant.Period.min60.value,
}
period_value = period_map.get(period, ad.constant.Period.day.value)
# 如果没有指定交易日,使用最近交易日
if not trading_day:
trading_day = self._get_latest_trading_day()
# 获取数据
start_date = f"{trading_day[:4]}-{trading_day[4:6]}-{trading_day[6:8]}"
end_date = start_date
df = history.get_kline_data(
code=code,
period=period_value,
start_date=start_date,
end_date=end_date
)
# 转换数据
data_list = []
if df is not None and len(df) > 0:
data_list = df.to_dict('records')
# 保存文件
if save_path is None:
save_path = os.path.join(self.data_save_path, "single")
os.makedirs(save_path, exist_ok=True)
safe_code = code.replace('.', '_')
filename = f"{safe_code}_{trading_day}_{period}.json"
filepath = os.path.join(save_path, filename)
result = {
"code": code,
"trading_day": trading_day,
"period": period,
"data": data_list,
"count": len(data_list)
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
result["file_path"] = filepath
return result
except Exception as e:
return {"error": str(e), "code": code, "trading_day": trading_day or "", "data": [], "count": 0}
def batch_get_stock_kline(
self,
codes: Optional[List[str]] = None,
trading_days: Optional[List[str]] = None,
save_path: Optional[str] = None,
batch_size: int = 100
) -> Dict[str, str]:
"""批量获取股票K线数据"""
try:
import AmazingData as ad
if not self.connected:
self.connect()
if save_path is None:
save_path = os.path.join(self.data_save_path, "stock")
os.makedirs(save_path, exist_ok=True)
# 获取股票代码
if codes is None:
base = ad.BaseData()
codes = base.get_code_list("EXTRA_STOCK_A")
# 获取交易日
if trading_days is None:
trading_days = [self._get_latest_trading_day()]
result_files = {}
history = ad.HistoryData()
for trading_day in trading_days:
start_date = f"{trading_day[:4]}-{trading_day[4:6]}-{trading_day[6:8]}"
day_data = {}
for i, code in enumerate(codes):
try:
period_value = ad.constant.Period.day.value
df = history.get_kline_data(
code=code,
period=period_value,
start_date=start_date,
end_date=start_date
)
if df is not None and len(df) > 0:
day_data[code] = df.to_dict('records')
except Exception:
continue
# 保存文件
filename = f"kline_{trading_day}.json"
filepath = os.path.join(save_path, filename)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(day_data, f, ensure_ascii=False, indent=2)
result_files[trading_day] = filepath
return result_files
except Exception as e:
return {"error": str(e)}
def batch_get_future_kline(
self,
underlying_codes: Optional[List[str]] = None,
use_main_contract: bool = True,
trading_days: Optional[List[str]] = None,
save_path: Optional[str] = None
) -> Dict[str, str]:
"""批量获取期货K线数据"""
try:
import AmazingData as ad
if not self.connected:
self.connect()
if save_path is None:
save_path = os.path.join(self.data_save_path, "future")
os.makedirs(save_path, exist_ok=True)
# 获取交易日
if trading_days is None:
trading_days = [self._get_latest_trading_day()]
result_files = {}
history = ad.HistoryData()
for trading_day in trading_days:
start_date = f"{trading_day[:4]}-{trading_day[4:6]}-{trading_day[6:8]}"
all_data = []
# 获取期货代码
if underlying_codes:
codes = []
for uc in underlying_codes:
if '.' in uc:
codes.append(uc)
else:
# 简单主力合约识别
codes.append(f"{uc}2605.SHF")
else:
base = ad.BaseData()
codes = base.get_code_list("EXTRA_FUTURE")[:50] # 限制数量
for code in codes:
try:
period_value = ad.constant.Period.day.value
df = history.get_kline_data(
code=code,
period=period_value,
start_date=start_date,
end_date=start_date
)
if df is not None and len(df) > 0:
all_data.extend(df.to_dict('records'))
except Exception:
continue
# 保存文件
filename = f"futures_{trading_day}.json"
filepath = os.path.join(save_path, filename)
result = {
"metadata": {
"source": "AmazingData",
"trading_day": trading_day,
"fetch_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
"total_codes": len(codes),
"total_records": len(all_data)
},
"data": all_data
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
result_files[trading_day] = filepath
return result_files
except Exception as e:
return {"error": str(e)}
def get_stock_codes(self) -> List[str]:
"""获取股票代码列表"""
try:
import AmazingData as ad
if not self.connected:
self.connect()
base = ad.BaseData()
return base.get_code_list("EXTRA_STOCK_A")
except Exception:
return []
def get_future_codes(self) -> List[str]:
"""获取期货代码列表"""
try:
import AmazingData as ad
if not self.connected:
self.connect()
base = ad.BaseData()
return base.get_code_list("EXTRA_FUTURE")
except Exception:
return []
def _get_latest_trading_day(self) -> str:
"""获取最近交易日"""
today = date.today()
return today.strftime('%Y%m%d')
def test_connection(self) -> Dict[str, Any]:
"""测试连接"""
try:
import AmazingData as ad
ret = ad.login(
username=settings.AMAZING_DATA_USERNAME,
password=settings.AMAZING_DATA_PASSWORD,
host=settings.AMAZING_DATA_HOST,
port=settings.AMAZING_DATA_PORT
)
if ret:
ad.logout(settings.AMAZING_DATA_USERNAME)
return {"success": True, "message": "Connection successful"}
return {"success": False, "message": "Login failed"}
except Exception as e:
return {"success": False, "message": str(e)}
# 全局服务实例
data_service = AmazingDataPlatformService()

@ -0,0 +1,144 @@
-- =====================================================
-- AmazingData 数据服务平台 - MySQL 数据库初始化脚本
-- 版本: 1.0
-- 日期: 2026-04-09
-- =====================================================
CREATE DATABASE IF NOT EXISTS amazingdata_platform DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE amazingdata_platform;
-- =====================================================
-- 1. 系统配置表
-- =====================================================
CREATE TABLE IF NOT EXISTS `system_config` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`config_key` VARCHAR(100) NOT NULL UNIQUE COMMENT '配置键',
`config_value` TEXT COMMENT '配置值',
`config_type` VARCHAR(20) DEFAULT 'string' COMMENT '类型: string/number/boolean/json',
`description` VARCHAR(255) COMMENT '描述',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_config_key` (`config_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统配置表';
-- =====================================================
-- 2. 用户表
-- =====================================================
CREATE TABLE IF NOT EXISTS `users` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
`password_hash` VARCHAR(255) NOT NULL COMMENT '密码哈希',
`role` VARCHAR(20) DEFAULT 'user' COMMENT '角色: admin/user',
`is_active` TINYINT(1) DEFAULT 1 COMMENT '是否激活',
`last_login` DATETIME COMMENT '最后登录时间',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- =====================================================
-- 3. 订阅任务表
-- =====================================================
CREATE TABLE IF NOT EXISTS `subscription_tasks` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`task_name` VARCHAR(100) NOT NULL COMMENT '任务名称',
`codes` JSON NOT NULL COMMENT '品种代码列表',
`periods` JSON NOT NULL COMMENT '订阅周期列表',
`save_path` VARCHAR(255) COMMENT '保存路径',
`duration` INT DEFAULT 0 COMMENT '运行时长(秒), 0=无限',
`save_interval` INT DEFAULT 60 COMMENT '保存间隔(秒)',
`status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/running/stopped/error',
`subscription_id` VARCHAR(100) COMMENT '订阅实例ID',
`started_at` DATETIME COMMENT '开始时间',
`stopped_at` DATETIME COMMENT '停止时间',
`created_by` VARCHAR(50) COMMENT '创建人',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_status` (`status`),
INDEX `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订阅任务表';
-- =====================================================
-- 4. 批量任务表
-- =====================================================
CREATE TABLE IF NOT EXISTS `batch_tasks` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`task_type` VARCHAR(20) NOT NULL COMMENT '类型: stock/future',
`task_params` JSON COMMENT '任务参数',
`total_count` INT DEFAULT 0 COMMENT '总数量',
`processed_count` INT DEFAULT 0 COMMENT '已处理数量',
`success_count` INT DEFAULT 0 COMMENT '成功数量',
`failed_count` INT DEFAULT 0 COMMENT '失败数量',
`status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态',
`output_path` VARCHAR(255) COMMENT '输出路径',
`error_message` TEXT COMMENT '错误信息',
`started_at` DATETIME,
`completed_at` DATETIME,
`created_by` VARCHAR(50),
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_status` (`status`),
INDEX `idx_task_type` (`task_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='批量任务表';
-- =====================================================
-- 5. 数据缓存记录表
-- =====================================================
CREATE TABLE IF NOT EXISTS `cache_records` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`filename` VARCHAR(255) NOT NULL COMMENT '文件名',
`file_type` VARCHAR(20) NOT NULL COMMENT '类型: stock/future/realtime',
`trading_day` VARCHAR(8) COMMENT '交易日',
`code` VARCHAR(20) COMMENT '代码',
`period` VARCHAR(10) COMMENT '周期',
`record_count` INT DEFAULT 0 COMMENT '记录数',
`file_size` BIGINT DEFAULT 0 COMMENT '文件大小(字节)',
`file_path` VARCHAR(255) COMMENT '完整路径',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_file_type` (`file_type`),
INDEX `idx_trading_day` (`trading_day`),
INDEX `idx_filename` (`filename`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据缓存记录表';
-- =====================================================
-- 6. 操作日志表
-- =====================================================
CREATE TABLE IF NOT EXISTS `operation_logs` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` INT COMMENT '用户ID',
`operation` VARCHAR(50) NOT NULL COMMENT '操作类型',
`resource` VARCHAR(100) COMMENT '操作资源',
`detail` TEXT COMMENT '详细信息',
`ip_address` VARCHAR(45) COMMENT 'IP地址',
`status` VARCHAR(20) DEFAULT 'success' COMMENT '状态',
`error_message` TEXT COMMENT '错误信息',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_user_id` (`user_id`),
INDEX `idx_operation` (`operation`),
INDEX `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';
-- =====================================================
-- 初始数据
-- =====================================================
-- 系统配置初始数据
INSERT INTO `system_config` (`config_key`, `config_value`, `config_type`, `description`) VALUES
('amazing_data_username', '11200008169', 'string', 'AmazingData 用户名'),
('amazing_data_password', '11200008169@2026', 'string', 'AmazingData 密码'),
('amazing_data_host', '140.206.44.234', 'string', 'AmazingData 服务器地址'),
('amazing_data_port', '8600', 'number', 'AmazingData 端口'),
('realtime_save_days', '7', 'number', '实时数据保存天数'),
('cache_auto_save_interval', '60', 'number', '缓存自动保存间隔(秒)'),
('max_concurrent_tasks', '5', 'number', '最大并发任务数'),
('data_save_path', './data', 'string', '数据保存路径');
-- 默认管理员账号 (密码: admin123)
-- 密码哈希使用 bcrypt 生成
INSERT INTO `users` (`username`, `password_hash`, `role`) VALUES
('admin', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYILp92S.0i', 'admin');
-- =====================================================
-- 完成
-- =====================================================
SELECT 'Database initialization completed successfully!' AS message;

@ -0,0 +1,67 @@
version: '3.8'
services:
# MySQL 数据库
mysql:
image: mysql:8.0
container_name: amazingdata-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: amazingdata_platform
MYSQL_USER: amazingdata
MYSQL_PASSWORD: amazingdata123
ports:
- "3307:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 20s
retries: 10
# 后端服务
backend:
build:
context: .
dockerfile: Dockerfile.backend
container_name: amazingdata-backend
restart: always
ports:
- "8000:8000"
environment:
- DB_HOST=mysql
- DB_PORT=3306
- DB_USER=amazingdata
- DB_PASSWORD=amazingdata123
- DB_NAME=amazingdata_platform
- AMAZING_DATA_USERNAME=${AMAZING_DATA_USERNAME:-11200008169}
- AMAZING_DATA_PASSWORD=${AMAZING_DATA_PASSWORD:-11200008169@2026}
- AMAZING_DATA_HOST=${AMAZING_DATA_HOST:-140.206.44.234}
- AMAZING_DATA_PORT=${AMAZING_DATA_PORT:-8600}
- SECRET_KEY=${SECRET_KEY:-your-secret-key}
- JWT_SECRET_KEY=${JWT_SECRET_KEY:-your-jwt-secret-key}
volumes:
- ./data:/app/data
depends_on:
mysql:
condition: service_healthy
command: uvicorn backend.main:app --host 0.0.0.0 --port 8000
# 前端服务
frontend:
build:
context: .
dockerfile: Dockerfile.frontend
container_name: amazingdata-frontend
restart: always
ports:
- "3000:80"
depends_on:
- backend
volumes:
mysql_data:
driver: local

@ -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.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,26 @@
{
"name": "amazingdata-frontend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.5",
"pinia": "^2.1.7",
"element-plus": "^2.4.4",
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.2",
"echarts": "^5.4.3",
"dayjs": "^1.11.10"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.2",
"vite": "^5.0.8",
"sass": "^1.69.5"
}
}

@ -0,0 +1,26 @@
<template>
<el-config-provider :locale="locale">
<router-view />
</el-config-provider>
</template>
<script setup>
import { ref } from 'vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
const locale = ref(zhCn)
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #app {
height: 100%;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', Arial, sans-serif;
}
</style>

@ -0,0 +1,50 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
const api = axios.create({
baseURL: '',
timeout: 30000
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response) => {
const data = response.data
if (data.code && data.code !== 200) {
ElMessage.error(data.message || '请求失败')
return Promise.reject(new Error(data.message))
}
return response
},
(error) => {
if (error.response) {
const { status, data } = error.response
if (status === 401) {
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
window.location.href = '/login'
} else {
ElMessage.error(data?.detail || data?.message || '请求失败')
}
} else {
ElMessage.error('网络错误')
}
return Promise.reject(error)
}
)
export default api

@ -0,0 +1,22 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import App from './App.vue'
import router from './router'
const app = createApp(App)
const pinia = createPinia()
// 注册所有 Element Plus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(pinia)
app.use(router)
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')

@ -0,0 +1,81 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { requiresAuth: false }
},
{
path: '/',
component: () => import('@/views/Layout.vue'),
redirect: '/dashboard',
meta: { requiresAuth: true },
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { title: '仪表盘' }
},
{
path: 'historical',
name: 'Historical',
component: () => import('@/views/Historical.vue'),
meta: { title: '历史数据' }
},
{
path: 'realtime',
name: 'Realtime',
component: () => import('@/views/Realtime.vue'),
meta: { title: '实时订阅' }
},
{
path: 'batch',
name: 'Batch',
component: () => import('@/views/Batch.vue'),
meta: { title: '批量操作' }
},
{
path: 'cache',
name: 'Cache',
component: () => import('@/views/Cache.vue'),
meta: { title: '缓存管理' }
},
{
path: 'settings',
name: 'Settings',
component: () => import('@/views/Settings.vue'),
meta: { title: '系统配置' }
},
{
path: 'api-test',
name: 'ApiTest',
component: () => import('@/views/ApiTest.vue'),
meta: { title: 'API测试' }
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth !== false && !authStore.isLoggedIn) {
next('/login')
} else if (to.path === '/login' && authStore.isLoggedIn) {
next('/')
} else {
next()
}
})
export default router

@ -0,0 +1,43 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import api from '@/api'
export const useAuthStore = defineStore('auth', () => {
const token = ref(localStorage.getItem('token') || '')
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || 'null'))
const isLoggedIn = computed(() => !!token.value)
async function login(username, password) {
const formData = new URLSearchParams()
formData.append('username', username)
formData.append('password', password)
const res = await api.post('/api/v1/auth/login', formData, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
})
token.value = res.data.access_token
userInfo.value = res.data.user_info
localStorage.setItem('token', res.data.access_token)
localStorage.setItem('userInfo', JSON.stringify(res.data.user_info))
return res.data
}
function logout() {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
}
return {
token,
userInfo,
isLoggedIn,
login,
logout
}
})

@ -0,0 +1,807 @@
<template>
<div class="api-test">
<el-row :gutter="20">
<el-col :span="4">
<el-card class="api-list-card">
<template #header>
<span>API 接口列表</span>
</template>
<el-tree
:data="apiTree"
:props="treeProps"
@node-click="handleNodeClick"
highlight-current
default-expand-all
/>
</el-card>
</el-col>
<el-col :span="20">
<el-card v-if="currentApi">
<template #header>
<div class="api-header">
<el-tag :type="getMethodType(currentApi.method)" size="large">
{{ currentApi.method }}
</el-tag>
<span class="api-path">{{ currentApi.path }}</span>
<span class="api-desc">{{ currentApi.description }}</span>
</div>
</template>
<el-tabs v-model="activeTab">
<el-tab-pane label="参数输入" name="params">
<el-form :model="paramsForm" label-width="120px">
<el-form-item label="请求头">
<el-switch v-model="useAuth" active-text="Token" inactive-text="Token" />
</el-form-item>
<template v-if="currentApi.params && currentApi.params.length > 0">
<el-divider>请求参数</el-divider>
<el-form-item v-for="param in currentApi.params" :key="param.name" :label="param.name">
<template v-if="param.type === 'string'">
<el-input v-model="paramsForm[param.name]" :placeholder="param.description" />
</template>
<template v-else-if="param.type === 'number'">
<el-input-number v-model="paramsForm[param.name]" :placeholder="param.description" />
</template>
<template v-else-if="param.type === 'boolean'">
<el-switch v-model="paramsForm[param.name]" />
</template>
<template v-else-if="param.type === 'select'">
<el-select v-model="paramsForm[param.name]" :placeholder="param.description">
<el-option v-for="opt in param.options" :key="opt" :label="opt" :value="opt" />
</el-select>
</template>
<template v-else-if="param.type === 'array'">
<el-input v-model="paramsForm[param.name]" :placeholder="param.description + ' (逗号分隔)'" />
</template>
<template v-else-if="param.type === 'json'">
<el-input v-model="paramsForm[param.name]" type="textarea" :rows="4" :placeholder="param.description" />
</template>
<span class="param-desc">{{ param.description }}</span>
</el-form-item>
</template>
<template v-if="currentApi.bodyParams && currentApi.bodyParams.length > 0">
<el-divider>请求体参数</el-divider>
<el-form-item v-for="param in currentApi.bodyParams" :key="param.name" :label="param.name">
<template v-if="param.type === 'string'">
<el-input v-model="bodyForm[param.name]" :placeholder="param.description" />
</template>
<template v-else-if="param.type === 'number'">
<el-input-number v-model="bodyForm[param.name]" />
</template>
<template v-else-if="param.type === 'boolean'">
<el-switch v-model="bodyForm[param.name]" />
</template>
<template v-else-if="param.type === 'select'">
<el-select v-model="bodyForm[param.name]" :placeholder="param.description">
<el-option v-for="opt in param.options" :key="opt" :label="opt" :value="opt" />
</el-select>
</template>
<template v-else-if="param.type === 'array'">
<el-input v-model="bodyForm[param.name]" :placeholder="param.description + ' (逗号分隔)'" />
</template>
<template v-else-if="param.type === 'json'">
<el-input v-model="bodyForm[param.name]" type="textarea" :rows="4" :placeholder="param.description" />
</template>
<span class="param-desc">{{ param.description }}</span>
</el-form-item>
</template>
<el-form-item>
<el-button type="primary" :loading="loading" @click="executeApi">
发送请求
</el-button>
<el-button @click="clearParams"></el-button>
<el-button @click="setDefaultParams"></el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="返回结果" name="result">
<div v-if="response" class="response-area">
<el-descriptions :column="2" border>
<el-descriptions-item label="状态码">
<el-tag :type="response.status >= 200 && response.status < 300 ? 'success' : 'danger'">
{{ response.status }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="耗时">{{ response.duration }}ms</el-descriptions-item>
<el-descriptions-item label="请求时间">{{ response.timestamp }}</el-descriptions-item>
</el-descriptions>
<el-divider>响应数据</el-divider>
<pre class="response-json">{{ formatJson(response.data) }}</pre>
</div>
<el-empty v-else description="请先发送请求" />
</el-tab-pane>
<el-tab-pane label="历史记录" name="history">
<el-table :data="historyList" style="width: 100%">
<el-table-column prop="path" label="接口" width="200" />
<el-table-column prop="method" label="方法" width="80">
<template #default="{ row }">
<el-tag :type="getMethodType(row.method)" size="small">{{ row.method }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status >= 200 && row.status < 300 ? 'success' : 'danger'" size="small">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="timestamp" label="时间" width="160" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button size="small" @click="viewHistory(row)"></el-button>
<el-button size="small" type="danger" @click="deleteHistory(row)"></el-button>
</template>
</el-table-column>
</el-table>
<el-button style="margin-top: 10px" type="danger" @click="clearHistory"></el-button>
</el-tab-pane>
</el-tabs>
</el-card>
<el-empty v-else description="请从左侧选择一个API接口" />
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/api'
const currentApi = ref(null)
const activeTab = ref('params')
const loading = ref(false)
const response = ref(null)
const historyList = ref([])
const useAuth = ref(true)
const paramsForm = reactive({})
const bodyForm = reactive({})
const treeProps = {
children: 'children',
label: 'label'
}
const apiTree = ref([
{
label: '认证接口',
children: [
{
label: '用户登录',
api: {
path: '/api/v1/auth/login',
method: 'POST',
description: '用户登录获取Token',
params: [
{ name: 'username', type: 'string', description: '用户名', default: 'admin' },
{ name: 'password', type: 'string', description: '密码', default: 'admin123' }
],
bodyParams: []
}
},
{
label: 'JSON登录',
api: {
path: '/api/v1/auth/login-json',
method: 'POST',
description: 'JSON格式登录',
bodyParams: [
{ name: 'username', type: 'string', description: '用户名', default: 'admin' },
{ name: 'password', type: 'string', description: '密码', default: 'admin123' }
]
}
},
{
label: '获取当前用户',
api: {
path: '/api/v1/auth/me',
method: 'GET',
description: '获取当前登录用户信息',
params: [],
bodyParams: []
}
},
{
label: '退出登录',
api: {
path: '/api/v1/auth/logout',
method: 'POST',
description: '用户退出登录',
params: [],
bodyParams: []
}
}
]
},
{
label: '历史数据接口',
children: [
{
label: '单只K线查询',
api: {
path: '/api/v1/historical/single',
method: 'POST',
description: '获取单只股票/期货K线数据',
bodyParams: [
{ name: 'code', type: 'string', description: '代码,如 000001.SZ', default: '000001.SZ' },
{ name: 'trading_day', type: 'string', description: '交易日 YYYYMMDD', default: '20260410' },
{ name: 'period', type: 'select', description: 'K线周期', options: ['day', 'min1', 'min5', 'min15', 'min30', 'min60'], default: 'day' },
{ name: 'save_path', type: 'string', description: '保存路径', default: './data/single' }
]
}
},
{
label: '批量股票获取',
api: {
path: '/api/v1/historical/batch-stocks',
method: 'POST',
description: '批量获取股票K线数据',
bodyParams: [
{ name: 'codes', type: 'array', description: '股票代码列表(逗号分隔)', default: '' },
{ name: 'trading_days', type: 'array', description: '交易日列表(逗号分隔)', default: '20260410' },
{ name: 'save_path', type: 'string', description: '保存路径', default: './data/stock' },
{ name: 'batch_size', type: 'number', description: '批次大小', default: 100 }
]
}
},
{
label: '批量期货获取',
api: {
path: '/api/v1/historical/batch-futures',
method: 'POST',
description: '批量获取期货K线数据',
bodyParams: [
{ name: 'underlying_codes', type: 'array', description: '品种代码列表', default: '' },
{ name: 'use_main_contract', type: 'boolean', description: '是否主力合约', default: true },
{ name: 'trading_days', type: 'array', description: '交易日列表', default: '20260410' },
{ name: 'save_path', type: 'string', description: '保存路径', default: './data/future' }
]
}
},
{
label: '获取股票代码',
api: {
path: '/api/v1/historical/stock-codes',
method: 'GET',
description: '获取股票代码列表',
params: [],
bodyParams: []
}
},
{
label: '获取期货代码',
api: {
path: '/api/v1/historical/future-codes',
method: 'GET',
description: '获取期货代码列表',
params: [],
bodyParams: []
}
},
{
label: '获取交易日',
api: {
path: '/api/v1/historical/trading-days',
method: 'GET',
description: '获取交易日列表',
params: [
{ name: 'year', type: 'number', description: '年份', default: 2026 },
{ name: 'month', type: 'number', description: '月份', default: 4 }
],
bodyParams: []
}
}
]
},
{
label: '实时订阅接口',
children: [
{
label: '创建订阅',
api: {
path: '/api/v1/realtime/subscribe',
method: 'POST',
description: '创建实时K线订阅任务',
bodyParams: [
{ name: 'codes', type: 'array', description: '品种代码列表(逗号分隔)', default: 'ag2605.SHF' },
{ name: 'periods', type: 'array', description: '订阅周期列表(逗号分隔)', default: 'min5' },
{ name: 'save_path', type: 'string', description: '保存路径', default: './data/realtime' },
{ name: 'duration', type: 'number', description: '运行时长(秒)0=无限', default: 0 },
{ name: 'save_interval', type: 'number', description: '保存间隔(秒)', default: 60 },
{ name: 'task_name', type: 'string', description: '任务名称', default: '' }
]
}
},
{
label: '订阅任务列表',
api: {
path: '/api/v1/realtime/tasks',
method: 'GET',
description: '获取订阅任务列表',
params: [
{ name: 'status', type: 'select', description: '状态筛选', options: ['', 'pending', 'running', 'stopped', 'error'], default: '' }
],
bodyParams: []
}
},
{
label: '停止订阅',
api: {
path: '/api/v1/realtime/stop/{task_id}',
method: 'POST',
description: '停止订阅任务',
params: [
{ name: 'task_id', type: 'number', description: '任务ID', default: 1 }
],
bodyParams: []
}
}
]
},
{
label: '批量操作接口',
children: [
{
label: '执行批量任务',
api: {
path: '/api/v1/batch/execute',
method: 'POST',
description: '执行批量任务',
bodyParams: [
{ name: 'task_type', type: 'select', description: '任务类型', options: ['stock', 'future'], default: 'stock' },
{ name: 'codes', type: 'array', description: '代码列表(逗号分隔)', default: '' },
{ name: 'use_main_contract', type: 'boolean', description: '是否主力合约', default: true },
{ name: 'trading_days', type: 'array', description: '交易日列表', default: '20260410' },
{ name: 'save_path', type: 'string', description: '保存路径', default: './data/batch' },
{ name: 'batch_size', type: 'number', description: '批次大小', default: 100 }
]
}
},
{
label: '批量任务列表',
api: {
path: '/api/v1/batch/tasks',
method: 'GET',
description: '获取批量任务列表',
params: [
{ name: 'status', type: 'select', description: '状态筛选', options: ['', 'pending', 'running', 'completed', 'error'], default: '' },
{ name: 'task_type', type: 'select', description: '类型筛选', options: ['', 'stock', 'future'], default: '' }
],
bodyParams: []
}
},
{
label: '批量任务详情',
api: {
path: '/api/v1/batch/tasks/{task_id}',
method: 'GET',
description: '获取批量任务详情',
params: [
{ name: 'task_id', type: 'number', description: '任务ID', default: 1 }
],
bodyParams: []
}
}
]
},
{
label: '缓存管理接口',
children: [
{
label: '缓存文件列表',
api: {
path: '/api/v1/cache/list',
method: 'GET',
description: '获取缓存文件列表',
params: [
{ name: 'file_type', type: 'select', description: '文件类型', options: ['', 'stock', 'future', 'realtime'], default: '' },
{ name: 'trading_day', type: 'string', description: '交易日', default: '' },
{ name: 'code', type: 'string', description: '代码', default: '' },
{ name: 'page', type: 'number', description: '页码', default: 1 },
{ name: 'page_size', type: 'number', description: '每页数量', default: 20 }
],
bodyParams: []
}
},
{
label: '缓存统计',
api: {
path: '/api/v1/cache/stats',
method: 'GET',
description: '获取缓存统计信息',
params: [],
bodyParams: []
}
},
{
label: '获取缓存数据',
api: {
path: '/api/v1/cache/data/{file_type}/{trading_day}',
method: 'GET',
description: '获取缓存数据',
params: [
{ name: 'file_type', type: 'select', description: '文件类型', options: ['stock', 'future'], default: 'stock' },
{ name: 'trading_day', type: 'string', description: '交易日', default: '20260410' }
],
bodyParams: []
}
},
{
label: '清理缓存',
api: {
path: '/api/v1/cache/cleanup',
method: 'DELETE',
description: '清理旧缓存数据',
params: [
{ name: 'days', type: 'number', description: '清理天数', default: 30 }
],
bodyParams: []
}
}
]
},
{
label: '系统配置接口',
children: [
{
label: '获取所有配置',
api: {
path: '/api/v1/settings/list',
method: 'GET',
description: '获取所有系统配置',
params: [],
bodyParams: []
}
},
{
label: '获取单个配置',
api: {
path: '/api/v1/settings/{key}',
method: 'GET',
description: '获取单个配置项',
params: [
{ name: 'key', type: 'string', description: '配置键', default: 'amazing_data_username' }
],
bodyParams: []
}
},
{
label: '更新配置',
api: {
path: '/api/v1/settings/{key}',
method: 'PUT',
description: '更新配置项(需管理员权限)',
params: [
{ name: 'key', type: 'string', description: '配置键', default: 'amazing_data_username' }
],
bodyParams: [
{ name: 'config_value', type: 'string', description: '配置值', default: '11200008169' }
]
}
},
{
label: '批量更新配置',
api: {
path: '/api/v1/settings/batch',
method: 'PUT',
description: '批量更新配置(需管理员权限)',
bodyParams: [
{ name: 'configs', type: 'json', description: '配置对象JSON', default: '{"amazing_data_username": "11200008169"}' }
]
}
},
{
label: 'AmazingData配置',
api: {
path: '/api/v1/settings/amazing-data/config',
method: 'GET',
description: '获取AmazingData连接配置',
params: [],
bodyParams: []
}
},
{
label: '测试连接',
api: {
path: '/api/v1/settings/test-connection',
method: 'POST',
description: '测试AmazingData连接',
params: [],
bodyParams: []
}
},
{
label: '系统信息',
api: {
path: '/api/v1/settings/system/info',
method: 'GET',
description: '获取系统信息',
params: [],
bodyParams: []
}
}
]
}
])
function getMethodType(method) {
const types = {
GET: 'success',
POST: 'primary',
PUT: 'warning',
DELETE: 'danger'
}
return types[method] || 'info'
}
function handleNodeClick(data) {
if (data.api) {
currentApi.value = data.api
clearParams()
setDefaultParams()
activeTab.value = 'params'
response.value = null
}
}
function clearParams() {
Object.keys(paramsForm).forEach(key => delete paramsForm[key])
Object.keys(bodyForm).forEach(key => delete bodyForm[key])
}
function setDefaultParams() {
if (!currentApi.value) return
if (currentApi.value.params) {
currentApi.value.params.forEach(param => {
if (param.default !== undefined) {
paramsForm[param.name] = param.default
}
})
}
if (currentApi.value.bodyParams) {
currentApi.value.bodyParams.forEach(param => {
if (param.default !== undefined) {
bodyForm[param.name] = param.default
}
})
}
}
function buildPath(path) {
let result = path
Object.keys(paramsForm).forEach(key => {
if (result.includes(`{${key}}`)) {
result = result.replace(`{${key}}`, paramsForm[key])
}
})
return result
}
function buildQueryParams() {
const queryParams = {}
Object.keys(paramsForm).forEach(key => {
if (!currentApi.value.path.includes(`{${key}}`) && paramsForm[key] !== '' && paramsForm[key] !== undefined) {
queryParams[key] = paramsForm[key]
}
})
return queryParams
}
function buildBodyData() {
const bodyData = {}
Object.keys(bodyForm).forEach(key => {
const param = currentApi.value.bodyParams?.find(p => p.name === key)
if (param) {
if (param.type === 'array' && bodyForm[key]) {
bodyData[key] = bodyForm[key].split(',').map(s => s.trim()).filter(s => s)
} else if (param.type === 'json' && bodyForm[key]) {
try {
bodyData[key] = JSON.parse(bodyForm[key])
} catch {
bodyData[key] = bodyForm[key]
}
} else if (bodyForm[key] !== '' && bodyForm[key] !== undefined) {
bodyData[key] = bodyForm[key]
}
}
})
return bodyData
}
async function executeApi() {
if (!currentApi.value) return
loading.value = true
const startTime = Date.now()
try {
const path = buildPath(currentApi.value.path)
const queryParams = buildQueryParams()
const bodyData = buildBodyData()
let res
const method = currentApi.value.method.toLowerCase()
if (method === 'get') {
res = await api.get(path, { params: queryParams })
} else if (method === 'post') {
if (currentApi.value.path === '/api/v1/auth/login') {
const formData = new URLSearchParams()
formData.append('username', bodyForm.username || paramsForm.username || 'admin')
formData.append('password', bodyForm.password || paramsForm.password || 'admin123')
res = await api.post(path, formData, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
})
} else {
res = await api.post(path, bodyData, { params: queryParams })
}
} else if (method === 'put') {
if (currentApi.value.path === '/api/v1/settings/batch') {
res = await api.put(path, JSON.parse(bodyForm.configs || '{}'))
} else {
res = await api.put(path, bodyData, { params: queryParams })
}
} else if (method === 'delete') {
res = await api.delete(path, { params: queryParams })
}
const endTime = Date.now()
response.value = {
status: res.status,
duration: endTime - startTime,
timestamp: new Date().toLocaleString(),
data: res.data
}
addHistory({
path: currentApi.value.path,
method: currentApi.value.method,
status: res.status,
duration: endTime - startTime,
timestamp: new Date().toLocaleString(),
data: res.data,
params: { ...paramsForm },
body: { ...bodyForm }
})
activeTab.value = 'result'
ElMessage.success('请求成功')
} catch (e) {
const endTime = Date.now()
response.value = {
status: e.response?.status || 500,
duration: endTime - startTime,
timestamp: new Date().toLocaleString(),
data: e.response?.data || { error: e.message }
}
addHistory({
path: currentApi.value.path,
method: currentApi.value.method,
status: e.response?.status || 500,
duration: endTime - startTime,
timestamp: new Date().toLocaleString(),
data: e.response?.data || { error: e.message },
params: { ...paramsForm },
body: { ...bodyForm }
})
activeTab.value = 'result'
ElMessage.error('请求失败: ' + (e.response?.data?.detail || e.message))
} finally {
loading.value = false
}
}
function formatJson(data) {
try {
return JSON.stringify(data, null, 2)
} catch {
return String(data)
}
}
function addHistory(item) {
historyList.value.unshift(item)
if (historyList.value.length > 50) {
historyList.value.pop()
}
localStorage.setItem('apiTestHistory', JSON.stringify(historyList.value))
}
function viewHistory(item) {
response.value = {
status: item.status,
duration: item.duration,
timestamp: item.timestamp,
data: item.data
}
activeTab.value = 'result'
}
function deleteHistory(item) {
const index = historyList.value.indexOf(item)
if (index > -1) {
historyList.value.splice(index, 1)
localStorage.setItem('apiTestHistory', JSON.stringify(historyList.value))
}
}
function clearHistory() {
historyList.value = []
localStorage.removeItem('apiTestHistory')
ElMessage.success('历史记录已清空')
}
onMounted(() => {
const savedHistory = localStorage.getItem('apiTestHistory')
if (savedHistory) {
try {
historyList.value = JSON.parse(savedHistory)
} catch {
historyList.value = []
}
}
})
</script>
<style scoped>
.api-test {
height: calc(100vh - 120px);
}
.api-list-card {
height: 100%;
overflow: auto;
}
.api-header {
display: flex;
align-items: center;
gap: 12px;
}
.api-path {
font-weight: bold;
color: #303133;
}
.api-desc {
color: #909399;
font-size: 14px;
}
.param-desc {
margin-left: 10px;
color: #909399;
font-size: 12px;
}
.response-area {
margin-top: 20px;
}
.response-json {
background: #f5f5f5;
padding: 16px;
border-radius: 4px;
overflow: auto;
max-height: 500px;
font-family: monospace;
font-size: 14px;
white-space: pre-wrap;
}
</style>

@ -0,0 +1,164 @@
<template>
<div class="batch">
<el-row :gutter="20">
<el-col :span="12">
<el-card>
<template #header>
<span>创建批量任务</span>
</template>
<el-form :model="form" label-width="100px">
<el-form-item label="任务类型">
<el-select v-model="form.task_type">
<el-option label="股票" value="stock" />
<el-option label="期货" value="future" />
</el-select>
</el-form-item>
<el-form-item label="品种代码">
<el-input v-model="form.codes" placeholder="多个代码用逗号分隔,留空表示全部" />
</el-form-item>
<el-form-item v-if="form.task_type === 'future'" label="主力合约">
<el-switch v-model="form.use_main_contract" />
</el-form-item>
<el-form-item label="交易日">
<el-date-picker v-model="form.trading_days" type="dates" placeholder="选择多个日期" value-format="YYYYMMDD" />
</el-form-item>
<el-form-item label="保存路径">
<el-input v-model="form.save_path" placeholder="./data/batch" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleExecute"></el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<template #header>
<div class="card-header">
<span>任务列表</span>
<el-button size="small" @click="loadTasks"></el-button>
</div>
</template>
<el-table :data="tasks" style="width: 100%">
<el-table-column prop="task_type" label="类型" width="60">
<template #default="{ row }">
{{ row.task_type === 'stock' ? '股票' : '期货' }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="success_count" label="成功" width="60" />
<el-table-column prop="failed_count" label="失败" width="60" />
<el-table-column prop="created_at" label="创建时间" width="160" />
<el-table-column label="操作" width="80">
<template #default="{ row }">
<el-button size="small" @click="viewDetail(row)"></el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<!-- 任务详情对话框 -->
<el-dialog v-model="detailVisible" title="任务详情" width="600px">
<el-descriptions :column="1" border v-if="currentTask">
<el-descriptions-item label="任务ID">{{ currentTask.id }}</el-descriptions-item>
<el-descriptions-item label="类型">{{ currentTask.task_type }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ currentTask.status }}</el-descriptions-item>
<el-descriptions-item label="成功数">{{ currentTask.success_count }}</el-descriptions-item>
<el-descriptions-item label="失败数">{{ currentTask.failed_count }}</el-descriptions-item>
<el-descriptions-item label="输出路径">{{ currentTask.output_path }}</el-descriptions-item>
<el-descriptions-item v-if="currentTask.error_message" label="错误信息">{{ currentTask.error_message }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/api'
const loading = ref(false)
const tasks = ref([])
const detailVisible = ref(false)
const currentTask = ref(null)
const form = reactive({
task_type: 'stock',
codes: '',
use_main_contract: true,
trading_days: null,
save_path: './data/batch'
})
function getStatusType(status) {
const map = {
pending: 'info',
running: 'warning',
completed: 'success',
error: 'danger'
}
return map[status] || 'info'
}
async function handleExecute() {
loading.value = true
try {
const codes = form.codes ? form.codes.split(',').map(c => c.trim()) : null
await api.post('/api/v1/batch/execute', {
task_type: form.task_type,
codes: codes,
use_main_contract: form.use_main_contract,
trading_days: form.trading_days,
save_path: form.save_path
})
ElMessage.success('批量任务已提交')
loadTasks()
} catch (e) {
ElMessage.error('提交失败')
} finally {
loading.value = false
}
}
async function loadTasks() {
try {
const res = await api.get('/api/v1/batch/tasks')
tasks.value = res.data.data.tasks || []
} catch (e) {
console.error('Load tasks error:', e)
}
}
function viewDetail(task) {
currentTask.value = task
detailVisible.value = true
}
onMounted(() => {
loadTasks()
})
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

@ -0,0 +1,229 @@
<template>
<div class="cache">
<el-row :gutter="20" style="margin-bottom: 20px">
<el-col :span="6">
<el-card>
<div class="stat-item">
<div class="stat-label">总文件数</div>
<div class="stat-value">{{ stats.total_files || 0 }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<div class="stat-item">
<div class="stat-label">总大小</div>
<div class="stat-value">{{ formatSize(stats.total_size) }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<div class="stat-item">
<div class="stat-label">磁盘文件</div>
<div class="stat-value">{{ stats.disk_files || 0 }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<div class="stat-item">
<div class="stat-label">磁盘大小</div>
<div class="stat-value">{{ formatSize(stats.disk_size) }}</div>
</div>
</el-card>
</el-col>
</el-row>
<el-card>
<template #header>
<div class="card-header">
<span>缓存文件列表</span>
<div>
<el-button type="danger" size="small" @click="handleCleanup"></el-button>
<el-button size="small" @click="loadStats"></el-button>
</div>
</div>
</template>
<el-form :inline="true" style="margin-bottom: 16px">
<el-form-item label="类型">
<el-select v-model="filters.file_type" clearable placeholder="全部" @change="loadFiles">
<el-option label="股票" value="stock" />
<el-option label="期货" value="future" />
<el-option label="实时" value="realtime" />
</el-select>
</el-form-item>
<el-form-item label="交易日">
<el-input v-model="filters.trading_day" placeholder="YYYYMMDD" @keyup.enter="loadFiles" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadFiles"></el-button>
</el-form-item>
</el-form>
<el-table :data="files" style="width: 100%">
<el-table-column prop="filename" label="文件名" />
<el-table-column prop="file_type" label="类型" width="80" />
<el-table-column prop="trading_day" label="交易日" width="100" />
<el-table-column prop="code" label="代码" width="120" />
<el-table-column prop="record_count" label="记录数" width="80" />
<el-table-column prop="file_size" label="大小" width="100">
<template #default="{ row }">
{{ formatSize(row.file_size) }}
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="160" />
<el-table-column label="操作" width="80">
<template #default="{ row }">
<el-button size="small" @click="viewData(row)"></el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="page"
:page-size="pageSize"
:total="total"
layout="total, prev, pager, next"
style="margin-top: 16px; justify-content: center"
@current-change="loadFiles"
/>
</el-card>
<!-- 数据查看对话框 -->
<el-dialog v-model="dataVisible" title="数据预览" width="800px">
<pre style="max-height: 500px; overflow: auto; background: #f5f5f5; padding: 16px; border-radius: 4px">{{ jsonData }}</pre>
</el-dialog>
<!-- 清理对话框 -->
<el-dialog v-model="cleanupVisible" title="清理旧缓存" width="400px">
<el-form>
<el-form-item label="清理天数">
<el-input-number v-model="cleanupDays" :min="1" :max="365" />
<span style="margin-left: 10px">天前的数据将被清理</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="cleanupVisible = false">取消</el-button>
<el-button type="danger" @click="confirmCleanup"></el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import api from '@/api'
const stats = ref({})
const files = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = 20
const dataVisible = ref(false)
const cleanupVisible = ref(false)
const cleanupDays = ref(30)
const jsonData = ref('')
const filters = reactive({
file_type: '',
trading_day: ''
})
function formatSize(bytes) {
if (!bytes) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
let i = 0
let size = bytes
while (size >= 1024 && i < units.length - 1) {
size /= 1024
i++
}
return size.toFixed(1) + ' ' + units[i]
}
async function loadStats() {
try {
const res = await api.get('/api/v1/cache/stats')
stats.value = res.data.data || {}
} catch (e) {
console.error('Load stats error:', e)
}
}
async function loadFiles() {
try {
const res = await api.get('/api/v1/cache/list', {
params: {
file_type: filters.file_type || undefined,
trading_day: filters.trading_day || undefined,
page: page.value,
page_size: pageSize
}
})
files.value = res.data.data.files || []
total.value = res.data.data.total || 0
} catch (e) {
console.error('Load files error:', e)
}
}
async function viewData(row) {
try {
const res = await api.get(`/api/v1/cache/data/${row.file_type}/${row.trading_day}`)
jsonData.value = JSON.stringify(res.data.data, null, 2)
dataVisible.value = true
} catch (e) {
ElMessage.error('获取数据失败')
}
}
function handleCleanup() {
cleanupVisible.value = true
}
async function confirmCleanup() {
try {
const res = await api.delete('/api/v1/cache/cleanup', {
params: { days: cleanupDays.value }
})
ElMessage.success(`已清理 ${res.data.data.deleted_count} 条记录`)
cleanupVisible.value = false
loadStats()
loadFiles()
} catch (e) {
ElMessage.error('清理失败')
}
}
onMounted(() => {
loadStats()
loadFiles()
})
</script>
<style scoped>
.stat-item {
text-align: center;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #303133;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

@ -0,0 +1,187 @@
<template>
<div class="dashboard">
<el-row :gutter="20">
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background: #409EFF">
<el-icon><Calendar /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ stats.totalFiles }}</div>
<div class="stat-label">缓存文件数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background: #67C23A">
<el-icon><Files /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ formatSize(stats.totalSize) }}</div>
<div class="stat-label">缓存大小</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background: #E6A23C">
<el-icon><VideoCamera /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ stats.activeTasks }}</div>
<div class="stat-label">活跃订阅</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background: #F56C6C">
<el-icon><Connection /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ connectionStatus }}</div>
<div class="stat-label">连接状态</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px">
<el-col :span="12">
<el-card>
<template #header>
<div class="card-header">
<span>快捷操作</span>
</div>
</template>
<div class="quick-actions">
<el-button type="primary" @click="$router.push('/historical')"></el-button>
<el-button type="success" @click="$router.push('/realtime')"></el-button>
<el-button type="warning" @click="$router.push('/batch')"></el-button>
<el-button type="info" @click="$router.push('/cache')"></el-button>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<template #header>
<div class="card-header">
<span>系统信息</span>
</div>
</template>
<el-descriptions :column="1" border>
<el-descriptions-item label="平台名称">AmazingData Platform</el-descriptions-item>
<el-descriptions-item label="版本">v1.0.0</el-descriptions-item>
<el-descriptions-item label="状态">运行中</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import api from '@/api'
const stats = ref({
totalFiles: 0,
totalSize: 0,
activeTasks: 0
})
const connectionStatus = ref('检查中...')
function formatSize(bytes) {
if (!bytes) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
let i = 0
let size = bytes
while (size >= 1024 && i < units.length - 1) {
size /= 1024
i++
}
return size.toFixed(1) + ' ' + units[i]
}
async function loadStats() {
try {
const res = await api.get('/api/v1/cache/stats')
if (res.data.data) {
stats.value.totalFiles = res.data.data.total_files || 0
stats.value.totalSize = res.data.data.total_size || 0
}
} catch (e) {
console.error('Load stats error:', e)
}
try {
const res = await api.post('/api/v1/settings/test-connection')
connectionStatus.value = res.data.success ? '已连接' : '未连接'
} catch (e) {
connectionStatus.value = '未连接'
}
}
onMounted(() => {
loadStats()
})
</script>
<style scoped>
.stat-card .stat-content {
display: flex;
align-items: center;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 24px;
margin-right: 16px;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #303133;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-top: 4px;
}
.quick-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.card-header {
font-weight: bold;
}
</style>

@ -0,0 +1,181 @@
<template>
<div class="historical">
<el-tabs v-model="activeTab">
<el-tab-pane label="单只查询" name="single">
<el-card>
<el-form :model="singleForm" label-width="100px">
<el-form-item label="品种代码">
<el-input v-model="singleForm.code" placeholder="如 000001.SZ 或 ag2605.SHF" />
</el-form-item>
<el-form-item label="交易日">
<el-date-picker v-model="singleForm.trading_day" type="date" placeholder="选择日期" value-format="YYYYMMDD" />
</el-form-item>
<el-form-item label="周期">
<el-select v-model="singleForm.period">
<el-option label="日线" value="day" />
<el-option label="1分钟" value="min1" />
<el-option label="5分钟" value="min5" />
<el-option label="15分钟" value="min15" />
<el-option label="30分钟" value="min30" />
<el-option label="60分钟" value="min60" />
</el-select>
</el-form-item>
<el-form-item label="保存路径">
<el-input v-model="singleForm.save_path" placeholder="./data/single" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleSingleQuery"></el-button>
</el-form-item>
</el-form>
<div v-if="singleResult" style="margin-top: 20px">
<el-alert :title="`获取成功,共 ${singleResult.count} 条记录`" type="success" show-icon />
<p style="margin-top: 10px">文件路径: {{ singleResult.file_path }}</p>
</div>
</el-card>
</el-tab-pane>
<el-tab-pane label="批量股票" name="batch_stock">
<el-card>
<el-form :model="batchStockForm" label-width="100px">
<el-form-item label="股票代码">
<el-input v-model="batchStockForm.codes" placeholder="多个代码用逗号分隔,留空表示全部" />
</el-form-item>
<el-form-item label="交易日">
<el-date-picker v-model="batchStockForm.trading_days" type="dates" placeholder="选择多个日期" value-format="YYYYMMDD" />
</el-form-item>
<el-form-item label="保存路径">
<el-input v-model="batchStockForm.save_path" placeholder="./data/stock" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleBatchStock"></el-button>
</el-form-item>
</el-form>
<div v-if="batchStockResult" style="margin-top: 20px">
<el-alert :title="`批量获取完成,共 ${batchStockResult.count} 个文件`" type="success" show-icon />
</div>
</el-card>
</el-tab-pane>
<el-tab-pane label="批量期货" name="batch_future">
<el-card>
<el-form :model="batchFutureForm" label-width="100px">
<el-form-item label="品种代码">
<el-input v-model="batchFutureForm.codes" placeholder="如 ag, au 等" />
</el-form-item>
<el-form-item label="主力合约">
<el-switch v-model="batchFutureForm.use_main_contract" />
</el-form-item>
<el-form-item label="交易日">
<el-date-picker v-model="batchFutureForm.trading_days" type="dates" placeholder="选择多个日期" value-format="YYYYMMDD" />
</el-form-item>
<el-form-item label="保存路径">
<el-input v-model="batchFutureForm.save_path" placeholder="./data/future" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleBatchFuture"></el-button>
</el-form-item>
</el-form>
<div v-if="batchFutureResult" style="margin-top: 20px">
<el-alert :title="`批量获取完成,共 ${batchFutureResult.count} 个文件`" type="success" show-icon />
</div>
</el-card>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/api'
const activeTab = ref('single')
const loading = ref(false)
const singleForm = reactive({
code: '',
trading_day: null,
period: 'day',
save_path: './data/single'
})
const batchStockForm = reactive({
codes: '',
trading_days: null,
save_path: './data/stock'
})
const batchFutureForm = reactive({
codes: '',
use_main_contract: true,
trading_days: null,
save_path: './data/future'
})
const singleResult = ref(null)
const batchStockResult = ref(null)
const batchFutureResult = ref(null)
async function handleSingleQuery() {
if (!singleForm.code) {
ElMessage.warning('请输入品种代码')
return
}
loading.value = true
try {
const res = await api.post('/api/v1/historical/single', {
code: singleForm.code,
trading_day: singleForm.trading_day,
period: singleForm.period,
save_path: singleForm.save_path
})
singleResult.value = res.data.data
ElMessage.success('获取成功')
} catch (e) {
ElMessage.error('获取失败')
} finally {
loading.value = false
}
}
async function handleBatchStock() {
loading.value = true
try {
const codes = singleForm.codes ? singleForm.codes.split(',').map(c => c.trim()) : null
const res = await api.post('/api/v1/historical/batch-stocks', {
codes: codes,
trading_days: batchStockForm.trading_days,
save_path: batchStockForm.save_path
})
batchStockResult.value = res.data.data
ElMessage.success('批量获取完成')
} catch (e) {
ElMessage.error('批量获取失败')
} finally {
loading.value = false
}
}
async function handleBatchFuture() {
loading.value = true
try {
const codes = batchFutureForm.codes ? batchFutureForm.codes.split(',').map(c => c.trim()) : null
const res = await api.post('/api/v1/historical/batch-futures', {
underlying_codes: codes,
use_main_contract: batchFutureForm.use_main_contract,
trading_days: batchFutureForm.trading_days,
save_path: batchFutureForm.save_path
})
batchFutureResult.value = res.data.data
ElMessage.success('批量获取完成')
} catch (e) {
ElMessage.error('批量获取失败')
} finally {
loading.value = false
}
}
</script>

@ -0,0 +1,128 @@
<template>
<el-container class="layout-container">
<el-aside width="200px">
<div class="logo">AmazingData</div>
<el-menu :default-active="$route.path" router background-color="#304156" text-color="#bfcbd9" active-text-color="#409EFF">
<el-menu-item index="/dashboard">
<el-icon><HomeFilled /></el-icon>
<span>仪表盘</span>
</el-menu-item>
<el-menu-item index="/historical">
<el-icon><Calendar /></el-icon>
<span>历史数据</span>
</el-menu-item>
<el-menu-item index="/realtime">
<el-icon><VideoCamera /></el-icon>
<span>实时订阅</span>
</el-menu-item>
<el-menu-item index="/batch">
<el-icon><List /></el-icon>
<span>批量操作</span>
</el-menu-item>
<el-menu-item index="/cache">
<el-icon><Files /></el-icon>
<span>缓存管理</span>
</el-menu-item>
<el-menu-item index="/settings">
<el-icon><Setting /></el-icon>
<span>系统配置</span>
</el-menu-item>
<el-menu-item index="/api-test">
<el-icon><DocumentChecked /></el-icon>
<span>API测试</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header>
<div class="header-content">
<h3>{{ $route.meta.title || 'AmazingData 数据服务平台' }}</h3>
<div class="user-info">
<el-dropdown @command="handleCommand">
<span class="user-name">
{{ authStore.userInfo?.username || '用户' }}
<el-icon><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</el-header>
<el-main>
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import { useAuthStore } from '@/stores/auth'
import { useRouter } from 'vue-router'
const authStore = useAuthStore()
const router = useRouter()
function handleCommand(command) {
if (command === 'logout') {
authStore.logout()
router.push('/login')
}
}
</script>
<style scoped>
.layout-container {
height: 100vh;
}
.el-aside {
background-color: #304156;
}
.logo {
height: 60px;
line-height: 60px;
text-align: center;
color: #fff;
font-size: 18px;
font-weight: bold;
background-color: #263445;
}
.el-header {
background-color: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
padding: 0 20px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
}
.header-content h3 {
margin: 0;
color: #303133;
}
.user-name {
cursor: pointer;
color: #606266;
display: flex;
align-items: center;
gap: 4px;
}
.el-main {
background-color: #f0f2f5;
padding: 20px;
}
</style>

@ -0,0 +1,108 @@
<template>
<div class="login-container">
<el-card class="login-card">
<template #header>
<div class="card-header">
<h2>AmazingData 数据服务平台</h2>
<p>用户登录</p>
</div>
</template>
<el-form :model="form" :rules="rules" ref="formRef" @submit.prevent="handleLogin">
<el-form-item prop="username">
<el-input v-model="form.username" placeholder="用户名" prefix-icon="User" size="large" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="form.password" type="password" placeholder="密码" prefix-icon="Lock" size="large" show-password @keyup.enter="handleLogin" />
</el-form-item>
<el-form-item>
<el-button type="primary" size="large" style="width: 100%" :loading="loading" @click="handleLogin">
登录
</el-button>
</el-form-item>
</el-form>
<div class="footer">
<p>默认账号: admin / admin123</p>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus'
const router = useRouter()
const authStore = useAuthStore()
const formRef = ref(null)
const loading = ref(false)
const form = reactive({
username: 'admin',
password: 'admin123'
})
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
}
async function handleLogin() {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
loading.value = true
try {
await authStore.login(form.username, form.password)
ElMessage.success('登录成功')
router.push('/')
} catch (e) {
ElMessage.error('登录失败: ' + (e.message || '未知错误'))
} finally {
loading.value = false
}
})
}
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-card {
width: 400px;
}
.card-header {
text-align: center;
}
.card-header h2 {
margin: 0 0 8px;
color: #303133;
}
.card-header p {
margin: 0;
color: #909399;
}
.footer {
text-align: center;
margin-top: 16px;
color: #909399;
font-size: 12px;
}
</style>

@ -0,0 +1,157 @@
<template>
<div class="realtime">
<el-row :gutter="20">
<el-col :span="12">
<el-card>
<template #header>
<span>创建订阅任务</span>
</template>
<el-form :model="form" label-width="100px">
<el-form-item label="任务名称">
<el-input v-model="form.task_name" placeholder="留空自动生成" />
</el-form-item>
<el-form-item label="品种代码">
<el-input v-model="form.codes" placeholder="多个代码用逗号分隔" />
</el-form-item>
<el-form-item label="订阅周期">
<el-select v-model="form.periods" multiple placeholder="选择周期">
<el-option label="1分钟" value="min1" />
<el-option label="5分钟" value="min5" />
<el-option label="15分钟" value="min15" />
<el-option label="30分钟" value="min30" />
<el-option label="60分钟" value="min60" />
</el-select>
</el-form-item>
<el-form-item label="保存路径">
<el-input v-model="form.save_path" placeholder="./data/realtime" />
</el-form-item>
<el-form-item label="运行时长">
<el-input-number v-model="form.duration" :min="0" :step="60" />
<span style="margin-left: 10px; color: #909399">0=无限</span>
</el-form-item>
<el-form-item label="保存间隔">
<el-input-number v-model="form.save_interval" :min="10" :step="10" />
<span style="margin-left: 10px; color: #909399"></span>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleSubscribe"></el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<template #header>
<div class="card-header">
<span>订阅任务列表</span>
<el-button size="small" @click="loadTasks"></el-button>
</div>
</template>
<el-table :data="tasks" style="width: 100%">
<el-table-column prop="task_name" label="任务名称" />
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 'running' ? 'success' : row.status === 'error' ? 'danger' : 'info'">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="started_at" label="开始时间" width="160" />
<el-table-column label="操作" width="80">
<template #default="{ row }">
<el-button v-if="row.status === 'running'" size="small" type="danger" @click="handleStop(row.id)"></el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import api from '@/api'
const loading = ref(false)
const tasks = ref([])
const form = reactive({
task_name: '',
codes: '',
periods: ['min5'],
save_path: './data/realtime',
duration: 0,
save_interval: 60
})
async function handleSubscribe() {
if (!form.codes) {
ElMessage.warning('请输入品种代码')
return
}
loading.value = true
try {
const codes = form.codes.split(',').map(c => c.trim())
const res = await api.post('/api/v1/realtime/subscribe', {
task_name: form.task_name,
codes: codes,
periods: form.periods,
save_path: form.save_path,
duration: form.duration,
save_interval: form.save_interval
})
ElMessage.success('订阅任务已启动')
loadTasks()
} catch (e) {
ElMessage.error('启动失败')
} finally {
loading.value = false
}
}
async function handleStop(taskId) {
try {
await ElMessageBox.confirm('确定要停止此订阅任务吗?', '确认')
await api.post(`/api/v1/realtime/stop/${taskId}`)
ElMessage.success('任务已停止')
loadTasks()
} catch (e) {
if (e !== 'cancel') {
ElMessage.error('停止失败')
}
}
}
async function loadTasks() {
try {
const res = await api.get('/api/v1/realtime/tasks')
tasks.value = res.data.data.tasks || []
} catch (e) {
console.error('Load tasks error:', e)
}
}
onMounted(() => {
loadTasks()
})
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

@ -0,0 +1,121 @@
<template>
<div class="settings">
<el-card>
<template #header>
<span>AmazingData 连接配置</span>
</template>
<el-form :model="form" label-width="120px" style="max-width: 500px">
<el-form-item label="用户名">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" show-password placeholder="留空不修改" />
</el-form-item>
<el-form-item label="服务器地址">
<el-input v-model="form.host" />
</el-form-item>
<el-form-item label="端口">
<el-input-number v-model="form.port" :min="1" :max="65535" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSave"></el-button>
<el-button @click="testConnection"></el-button>
</el-form-item>
</el-form>
</el-card>
<el-card style="margin-top: 20px">
<template #header>
<span>系统信息</span>
</template>
<el-descriptions :column="1" border>
<el-descriptions-item label="平台名称">AmazingData Platform</el-descriptions-item>
<el-descriptions-item label="版本">v1.0.0</el-descriptions-item>
<el-descriptions-item label="环境">{{ systemInfo.app_env || 'development' }}</el-descriptions-item>
<el-descriptions-item label="Python">{{ systemInfo.python_version || '-' }}</el-descriptions-item>
<el-descriptions-item label="操作系统">{{ systemInfo.platform || '-' }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card style="margin-top: 20px">
<template #header>
<span>API 文档</span>
</template>
<p>后端提供 Swagger API 文档访问地址<a href="/docs" target="_blank">/docs</a></p>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/api'
const form = reactive({
username: '',
password: '',
host: '',
port: 8600
})
const systemInfo = ref({})
async function loadConfig() {
try {
const res = await api.get('/api/v1/settings/amazing-data/config')
const data = res.data.data
form.username = data.username || ''
form.host = data.host || ''
form.port = data.port || 8600
} catch (e) {
console.error('Load config error:', e)
}
}
async function loadSystemInfo() {
try {
const res = await api.get('/api/v1/settings/system/info')
systemInfo.value = res.data.data || {}
} catch (e) {
console.error('Load system info error:', e)
}
}
async function handleSave() {
try {
const configs = {
amazing_data_username: form.username,
amazing_data_host: form.host,
amazing_data_port: String(form.port)
}
if (form.password) {
configs.amazing_data_password = form.password
}
await api.put('/api/v1/settings/batch', configs)
ElMessage.success('配置保存成功')
} catch (e) {
ElMessage.error('保存失败')
}
}
async function testConnection() {
try {
const res = await api.post('/api/v1/settings/test-connection')
if (res.data.success) {
ElMessage.success('连接测试成功')
} else {
ElMessage.error('连接测试失败: ' + res.data.message)
}
} catch (e) {
ElMessage.error('连接测试失败')
}
}
onMounted(() => {
loadConfig()
loadSystemInfo()
})
</script>

@ -0,0 +1,25 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
},
build: {
outDir: 'dist',
assetsDir: 'assets'
}
})

@ -0,0 +1,28 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 前端静态文件
location / {
try_files $uri $uri/ /index.html;
}
# API 代理到后端
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;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 健康检查
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
Loading…
Cancel
Save