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
|
||||
@ -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;"]
|
||||
@ -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,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,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,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,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,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…
Reference in new issue