|
|
|
|
|
"""主应用入口 - 对应Go的cmd/server/main.go"""
|
|
|
|
|
|
import os
|
|
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import FastAPI
|
|
|
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
|
from fastapi.responses import HTMLResponse
|
|
|
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
|
|
|
|
|
|
|
|
from app.api import router, admin_router
|
|
|
|
|
|
from app.websocket import WebSocketServer
|
|
|
|
|
|
from app.core.config import get_config, get_settings
|
|
|
|
|
|
from app.core.logger import info, error, setup_logging
|
|
|
|
|
|
from app.core.metrics import MetricsMiddleware, get_metrics_response, set_app_info
|
|
|
|
|
|
from app.core.rate_limiter import RateLimitMiddleware, RateLimitConfig
|
|
|
|
|
|
from app.repositories.database import init_db
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 获取配置
|
|
|
|
|
|
config = get_config()
|
|
|
|
|
|
settings = get_settings()
|
|
|
|
|
|
|
|
|
|
|
|
# 设置日志
|
|
|
|
|
|
setup_logging()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
|
async def lifespan(app: FastAPI):
|
|
|
|
|
|
"""应用生命周期管理"""
|
|
|
|
|
|
# 启动时执行
|
|
|
|
|
|
info("Starting Market Data Service...")
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化数据库
|
|
|
|
|
|
try:
|
|
|
|
|
|
init_db()
|
|
|
|
|
|
info("Database initialized")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error(f"Database initialization failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
yield
|
|
|
|
|
|
|
|
|
|
|
|
# 关闭时执行
|
|
|
|
|
|
info("Shutting down Market Data Service...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 创建FastAPI应用
|
|
|
|
|
|
app = FastAPI(
|
|
|
|
|
|
title="统一行情数据服务",
|
|
|
|
|
|
description="提供股票和期货的标准化行情数据查询服务",
|
|
|
|
|
|
version="1.0.0",
|
|
|
|
|
|
lifespan=lifespan
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 添加CORS中间件
|
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
|
CORSMiddleware,
|
|
|
|
|
|
allow_origins=["*"],
|
|
|
|
|
|
allow_credentials=True,
|
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 添加Prometheus指标中间件
|
|
|
|
|
|
app.add_middleware(MetricsMiddleware)
|
|
|
|
|
|
|
|
|
|
|
|
# 添加限流中间件(默认每分钟60请求,滑动窗口算法)
|
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
|
RateLimitMiddleware,
|
|
|
|
|
|
config=RateLimitConfig(
|
|
|
|
|
|
requests_per_minute=120, # 每分钟120请求
|
|
|
|
|
|
burst_size=20, # 突发20请求
|
|
|
|
|
|
strategy="sliding_window" # 使用滑动窗口算法
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 注册API路由
|
|
|
|
|
|
app.include_router(router, prefix="/v1")
|
|
|
|
|
|
app.include_router(admin_router, prefix="/v1")
|
|
|
|
|
|
|
|
|
|
|
|
# WebSocket服务器
|
|
|
|
|
|
ws_server = WebSocketServer()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.websocket("/v1/stream")
|
|
|
|
|
|
async def websocket_endpoint(websocket):
|
|
|
|
|
|
"""WebSocket端点"""
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
client_id = str(uuid.uuid4())
|
|
|
|
|
|
await ws_server.handle(websocket, client_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/metrics")
|
|
|
|
|
|
async def metrics():
|
|
|
|
|
|
"""Prometheus指标端点"""
|
|
|
|
|
|
return get_metrics_response()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 管理后台页面HTML(完整版)
|
|
|
|
|
|
ADMIN_HTML = '''<!DOCTYPE html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
|
<title>行情数据服务 - 管理后台</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
|
|
|
|
|
|
background: #f0f2f5;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
.layout { display: flex; min-height: 100vh; }
|
|
|
|
|
|
.sidebar {
|
|
|
|
|
|
width: 200px;
|
|
|
|
|
|
background: #001529;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
height: 100vh;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
.logo {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
.nav-menu { padding: 16px 0; }
|
|
|
|
|
|
.nav-item {
|
|
|
|
|
|
padding: 12px 24px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.nav-item:hover { background: rgba(255,255,255,0.05); }
|
|
|
|
|
|
.nav-item.active { background: #1890ff; }
|
|
|
|
|
|
.main-content {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
margin-left: 200px;
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.header {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
|
margin: -24px -24px 24px -24px;
|
|
|
|
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.page-title { font-size: 20px; font-weight: 500; }
|
|
|
|
|
|
.card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
.card-title {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
padding-bottom: 12px;
|
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stats-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-value {
|
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #1890ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-label { color: #666; margin-top: 4px; }
|
|
|
|
|
|
.btn {
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn-primary { background: #1890ff; color: #fff; }
|
|
|
|
|
|
.btn-primary:hover { background: #40a9ff; }
|
|
|
|
|
|
.btn-success { background: #52c41a; color: #fff; }
|
|
|
|
|
|
.btn-success:hover { background: #73d13d; }
|
|
|
|
|
|
.btn-danger { background: #ff4d4f; color: #fff; }
|
|
|
|
|
|
.btn-danger:hover { background: #ff7875; }
|
|
|
|
|
|
.btn-sm { padding: 4px 12px; font-size: 12px; }
|
|
|
|
|
|
.hidden { display: none; }
|
|
|
|
|
|
.page { display: none; }
|
|
|
|
|
|
.page.active { display: block; }
|
|
|
|
|
|
|
|
|
|
|
|
/* 测试页面样式 */
|
|
|
|
|
|
.test-tabs {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-tab {
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-tab:hover { background: #f0f0f0; }
|
|
|
|
|
|
.test-tab.active { background: #1890ff; color: #fff; }
|
|
|
|
|
|
.test-panel { display: none; }
|
|
|
|
|
|
.test-panel.active { display: block; }
|
|
|
|
|
|
|
|
|
|
|
|
.test-category {
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-category-title {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1890ff;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
background: #e6f7ff;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border: 1px solid #f0f0f0;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-item:hover {
|
|
|
|
|
|
border-color: #1890ff;
|
|
|
|
|
|
background: #f6ffed;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-item-info { flex: 1; }
|
|
|
|
|
|
.test-item-name {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-item-desc {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-item-meta {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-item-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-result {
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-result.show { display: block; }
|
|
|
|
|
|
.test-result.success {
|
|
|
|
|
|
background: #f6ffed;
|
|
|
|
|
|
border: 1px solid #b7eb8f;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-result.error {
|
|
|
|
|
|
background: #fff2f0;
|
|
|
|
|
|
border: 1px solid #ffccc7;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-result-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-result-body {
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
|
max-height: 300px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
background: rgba(0,0,0,0.03);
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.status-badge {
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
.status-success { background: #52c41a; color: #fff; }
|
|
|
|
|
|
.status-error { background: #ff4d4f; color: #fff; }
|
|
|
|
|
|
.status-running { background: #1890ff; color: #fff; }
|
|
|
|
|
|
|
|
|
|
|
|
.batch-actions {
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading-spinner {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 14px;
|
|
|
|
|
|
height: 14px;
|
|
|
|
|
|
border: 2px solid #f3f3f3;
|
|
|
|
|
|
border-top: 2px solid #1890ff;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
animation: spin 1s linear infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(0deg); } }
|
|
|
|
|
|
</style>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
// 基础页面切换函数(必须在HTML调用之前定义)
|
|
|
|
|
|
function showPage(pageName) {
|
|
|
|
|
|
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
|
|
|
|
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
|
|
|
|
document.getElementById(pageName).classList.add('active');
|
|
|
|
|
|
if (event && event.target) {
|
|
|
|
|
|
event.target.classList.add('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function switchTestTab(tab) {
|
|
|
|
|
|
document.querySelectorAll('.test-tab').forEach(t => t.classList.remove('active'));
|
|
|
|
|
|
document.querySelectorAll('.test-panel').forEach(p => p.classList.remove('active'));
|
|
|
|
|
|
if (event && event.target) {
|
|
|
|
|
|
event.target.classList.add('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
document.getElementById(tab + '-test-panel').classList.add('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="layout">
|
|
|
|
|
|
<aside class="sidebar">
|
|
|
|
|
|
<div class="logo">📊 行情数据服务</div>
|
|
|
|
|
|
<nav class="nav-menu">
|
|
|
|
|
|
<div class="nav-item active" onclick="showPage('dashboard')">
|
|
|
|
|
|
<span>📈</span> 系统概览
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="nav-item" onclick="showPage('config')">
|
|
|
|
|
|
<span>⚙️</span> 配置管理
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="nav-item" onclick="showPage('adapters')">
|
|
|
|
|
|
<span>🔌</span> 数据源适配
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="nav-item" onclick="showPage('tests')">
|
|
|
|
|
|
<span>🧪</span> 接口测试
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
<main class="main-content">
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<h1 class="page-title">系统概览</h1>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<button class="btn btn-success" onclick="reloadConfig()">🔄 热加载配置</button>
|
|
|
|
|
|
<button class="btn btn-danger" onclick="restartService()">🔁 重启服务</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 系统概览页面 -->
|
|
|
|
|
|
<div id="dashboard" class="page active">
|
|
|
|
|
|
<div class="stats-grid">
|
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
|
<div class="stat-value" id="stat-status">运行中</div>
|
|
|
|
|
|
<div class="stat-label">运行状态</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
|
<div class="stat-value" id="stat-uptime">-</div>
|
|
|
|
|
|
<div class="stat-label">运行时长</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
|
<div class="stat-value">1.0.0</div>
|
|
|
|
|
|
<div class="stat-label">系统版本</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
|
<div class="stat-value" id="stat-threads">-</div>
|
|
|
|
|
|
<div class="stat-label">线程数量</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-title">API文档</div>
|
|
|
|
|
|
<p>访问 <a href="/docs">/docs</a> 查看Swagger API文档</p>
|
|
|
|
|
|
<p>访问 <a href="/redoc">/redoc</a> 查看ReDoc API文档</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 配置管理页面 -->
|
|
|
|
|
|
<div id="config" class="page">
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-title">系统配置</div>
|
|
|
|
|
|
<div id="config-list">加载中...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 数据源适配页面 -->
|
|
|
|
|
|
<div id="adapters" class="page">
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-title">数据源适配器管理</div>
|
|
|
|
|
|
<div class="batch-actions">
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="loadAdapterList()">🔄 刷新列表</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="adapter-list">加载中...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-title">当前数据源状态</div>
|
|
|
|
|
|
<div id="source-status">加载中...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 适配器配置编辑模态框 -->
|
|
|
|
|
|
<div id="adapter-config-modal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:1000;justify-content:center;align-items:center;">
|
|
|
|
|
|
<div style="background:#fff;border-radius:8px;padding:24px;max-width:500px;width:90%;max-height:80vh;overflow-y:auto;">
|
|
|
|
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
|
|
|
|
|
|
<h3 style="margin:0;">配置适配器: <span id="modal-adapter-name"></span></h3>
|
|
|
|
|
|
<button onclick="closeAdapterModal()" style="border:none;background:none;font-size:20px;cursor:pointer;">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="adapter-config-form">
|
|
|
|
|
|
<div style="margin-bottom:16px;">
|
|
|
|
|
|
<label style="display:block;margin-bottom:4px;font-weight:500;">状态</label>
|
|
|
|
|
|
<select id="modal-adapter-enabled" style="width:100%;padding:8px;border:1px solid #d9d9d9;border-radius:4px;">
|
|
|
|
|
|
<option value="true">启用</option>
|
|
|
|
|
|
<option value="false">禁用</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="modal-config-fields"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:24px;">
|
|
|
|
|
|
<button class="btn" onclick="closeAdapterModal()">取消</button>
|
|
|
|
|
|
<button class="btn btn-primary" onclick="saveAdapterConfig()">保存</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 接口测试页面 -->
|
|
|
|
|
|
<div id="tests" class="page">
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-title">接口测试</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="test-tabs">
|
|
|
|
|
|
<div class="test-tab active" onclick="switchTestTab('quick')">快速测试</div>
|
|
|
|
|
|
<div class="test-tab" onclick="switchTestTab('api')">API 测试套件</div>
|
|
|
|
|
|
<div class="test-tab" onclick="switchTestTab('internal')">对内接口测试</div>
|
|
|
|
|
|
<div class="test-tab" onclick="switchTestTab('ws')">WebSocket 测试</div>
|
|
|
|
|
|
<div class="test-tab" onclick="switchTestTab('history')">测试历史</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 快速测试面板 -->
|
|
|
|
|
|
<div id="quick-test-panel" class="test-panel active">
|
|
|
|
|
|
<div class="batch-actions">
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="runAllQuickTests()">▶ 运行全部快速测试</button>
|
|
|
|
|
|
<button class="btn btn-sm" onclick="clearAllQuickResults()">🗑 清除结果</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="test-category">
|
|
|
|
|
|
<div class="test-category-title">股票接口</div>
|
|
|
|
|
|
<div class="test-item">
|
|
|
|
|
|
<div class="test-item-info">
|
|
|
|
|
|
<div class="test-item-name">查询股票K线</div>
|
|
|
|
|
|
<div class="test-item-desc">GET /v1/stock/klines/{symbol}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-item-actions">
|
|
|
|
|
|
<input type="text" id="test-stock-symbol" placeholder="000001.SZ" value="000001.SZ" style="padding:4px 8px;border:1px solid #d9d9d9;border-radius:4px;width:100px;margin-right:8px;font-size:12px;">
|
|
|
|
|
|
<select id="test-stock-freq" style="padding:4px 8px;border:1px solid #d9d9d9;border-radius:4px;margin-right:8px;font-size:12px;">
|
|
|
|
|
|
<option value="1d">日线</option>
|
|
|
|
|
|
<option value="1m">1分钟</option>
|
|
|
|
|
|
<option value="5m">5分钟</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="quickTestStockKlines()">测试</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result" id="test-result-stock-klines"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="test-item">
|
|
|
|
|
|
<div class="test-item-info">
|
|
|
|
|
|
<div class="test-item-name">查询股票列表</div>
|
|
|
|
|
|
<div class="test-item-desc">GET /v1/stock/symbols</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-item-actions">
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="quickTestStockSymbols()">测试</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result" id="test-result-stock-symbols"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="test-category">
|
|
|
|
|
|
<div class="test-category-title">期货接口</div>
|
|
|
|
|
|
<div class="test-item">
|
|
|
|
|
|
<div class="test-item-info">
|
|
|
|
|
|
<div class="test-item-name">查询期货K线</div>
|
|
|
|
|
|
<div class="test-item-desc">GET /v1/futures/klines/{symbol}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-item-actions">
|
|
|
|
|
|
<input type="text" id="test-futures-symbol" placeholder="CU2504.SHFE" value="CU2504.SHFE" style="padding:4px 8px;border:1px solid #d9d9d9;border-radius:4px;width:120px;margin-right:8px;font-size:12px;">
|
|
|
|
|
|
<select id="test-futures-freq" style="padding:4px 8px;border:1px solid #d9d9d9;border-radius:4px;margin-right:8px;font-size:12px;">
|
|
|
|
|
|
<option value="1d">日线</option>
|
|
|
|
|
|
<option value="1m">1分钟</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="quickTestFuturesKlines()">测试</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result" id="test-result-futures-klines"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="test-item">
|
|
|
|
|
|
<div class="test-item-info">
|
|
|
|
|
|
<div class="test-item-name">查询期货列表</div>
|
|
|
|
|
|
<div class="test-item-desc">GET /v1/futures/symbols</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-item-actions">
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="quickTestFuturesSymbols()">测试</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result" id="test-result-futures-symbols"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="test-category">
|
|
|
|
|
|
<div class="test-category-title">管理接口</div>
|
|
|
|
|
|
<div class="test-item">
|
|
|
|
|
|
<div class="test-item-info">
|
|
|
|
|
|
<div class="test-item-name">健康检查</div>
|
|
|
|
|
|
<div class="test-item-desc">GET /v1/admin/health</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-item-actions">
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="quickTestHealth()">测试</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result" id="test-result-health"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="test-item">
|
|
|
|
|
|
<div class="test-item-info">
|
|
|
|
|
|
<div class="test-item-name">数据源状态</div>
|
|
|
|
|
|
<div class="test-item-desc">GET /v1/admin/source/status</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-item-actions">
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="quickTestSourceStatus()">测试</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result" id="test-result-source-status"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- API 测试面板 -->
|
|
|
|
|
|
<div id="api-test-panel" class="test-panel">
|
|
|
|
|
|
<div class="batch-actions">
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="runAllAPITests()">▶ 运行全部 API 测试</button>
|
|
|
|
|
|
<button class="btn btn-sm" onclick="clearAllAPIResults()">🗑 清除结果</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="api-test-list">加载中...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 对内接口测试面板 -->
|
|
|
|
|
|
<div id="internal-test-panel" class="test-panel">
|
|
|
|
|
|
<div class="batch-actions">
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="runAllInternalTests()">▶ 运行全部对内接口测试</button>
|
|
|
|
|
|
<button class="btn btn-sm" onclick="clearAllInternalResults()">🗑 清除结果</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="internal-test-list">加载中...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- WebSocket 测试面板 -->
|
|
|
|
|
|
<div id="ws-test-panel" class="test-panel">
|
|
|
|
|
|
<div class="batch-actions">
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="runAllWSTests()">▶ 运行全部 WebSocket 测试</button>
|
|
|
|
|
|
<button class="btn btn-sm" onclick="clearAllWSResults()">🗑 清除结果</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="ws-test-list">加载中...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 测试历史面板 -->
|
|
|
|
|
|
<div id="history-test-panel" class="test-panel">
|
|
|
|
|
|
<div class="batch-actions">
|
|
|
|
|
|
<button class="btn btn-sm" onclick="loadTestHistory()">🔄 刷新</button>
|
|
|
|
|
|
<button class="btn btn-danger btn-sm" onclick="clearTestHistory()">🗑 清空历史</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="test-history-list">加载中...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
// API Key 存储
|
|
|
|
|
|
let apiKey = localStorage.getItem('apiKey') || '';
|
|
|
|
|
|
|
|
|
|
|
|
// 页面切换增强(在底部script中补充数据加载逻辑)
|
|
|
|
|
|
const _originalShowPage = showPage;
|
|
|
|
|
|
showPage = function(pageName) {
|
|
|
|
|
|
_originalShowPage(pageName);
|
|
|
|
|
|
|
|
|
|
|
|
if (pageName === 'tests') {
|
|
|
|
|
|
loadAPITestList();
|
|
|
|
|
|
loadInternalTestList();
|
|
|
|
|
|
loadWSTestList();
|
|
|
|
|
|
} else if (pageName === 'config') {
|
|
|
|
|
|
loadConfigList();
|
|
|
|
|
|
} else if (pageName === 'adapters') {
|
|
|
|
|
|
loadAdapterList();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const _originalSwitchTestTab = switchTestTab;
|
|
|
|
|
|
switchTestTab = function(tab) {
|
|
|
|
|
|
_originalSwitchTestTab(tab);
|
|
|
|
|
|
if (tab === 'history') loadTestHistory();
|
|
|
|
|
|
if (tab === 'internal') loadInternalTestList();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ============ API 测试 ============
|
|
|
|
|
|
let apiTestCases = [];
|
|
|
|
|
|
|
|
|
|
|
|
async function loadAPITestList() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/tests/api', {
|
|
|
|
|
|
headers: { 'X-Admin-Token': apiKey }
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.code === 0) {
|
|
|
|
|
|
apiTestCases = data.data.categories || [];
|
|
|
|
|
|
renderAPITestList(data.data.base_url);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
document.getElementById('api-test-list').innerHTML = '加载失败: ' + e.message;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderAPITestList(baseUrl) {
|
|
|
|
|
|
let html = '';
|
|
|
|
|
|
apiTestCases.forEach(cat => {
|
|
|
|
|
|
html += `<div class="test-category">
|
|
|
|
|
|
<div class="test-category-title">${cat.name}</div>`;
|
|
|
|
|
|
cat.items.forEach(item => {
|
|
|
|
|
|
html += `<div class="test-item" id="api-test-${item.id}">
|
|
|
|
|
|
<div class="test-item-info">
|
|
|
|
|
|
<div class="test-item-name">
|
|
|
|
|
|
<span class="status-badge" style="background:#f0f0f0;color:#666;margin-right:8px;">${item.method}</span>
|
|
|
|
|
|
${item.name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-item-desc">${item.description}</div>
|
|
|
|
|
|
<div class="test-item-meta">${item.path}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-item-actions">
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="runAPITest('${item.id}')" id="btn-api-${item.id}">测试</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result" id="api-result-${item.id}"></div>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
});
|
|
|
|
|
|
document.getElementById('api-test-list').innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runAPITest(testId) {
|
|
|
|
|
|
const btn = document.getElementById(`btn-api-${testId}`);
|
|
|
|
|
|
const resultDiv = document.getElementById(`api-result-${testId}`);
|
|
|
|
|
|
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
btn.innerHTML = '<span class="loading-spinner"></span>';
|
|
|
|
|
|
resultDiv.className = 'test-result';
|
|
|
|
|
|
resultDiv.classList.add('show');
|
|
|
|
|
|
resultDiv.innerHTML = '运行中...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/tests/api/run', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'X-Admin-Token': apiKey
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({ id: testId })
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.code === 0 && data.data) {
|
|
|
|
|
|
const result = data.data;
|
|
|
|
|
|
const isSuccess = result.success;
|
|
|
|
|
|
|
|
|
|
|
|
resultDiv.className = 'test-result show ' + (isSuccess ? 'success' : 'error');
|
|
|
|
|
|
resultDiv.innerHTML = `
|
|
|
|
|
|
<div class="test-result-header">
|
|
|
|
|
|
<span>${isSuccess ? '✅ 测试通过' : '❌ 测试失败'}</span>
|
|
|
|
|
|
<span>${result.latency}ms | HTTP ${result.status_code}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="margin-bottom:8px;">
|
|
|
|
|
|
<strong>请求:</strong> ${result.request.method} ${result.request.url}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
${result.request.body ? `<div style="margin-bottom:8px;"><strong>请求体:</strong> <pre>${JSON.stringify(result.request.body, null, 2)}</pre></div>` : ''}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>响应:</strong>
|
|
|
|
|
|
<div class="test-result-body">${typeof result.response === 'object' ? JSON.stringify(result.response, null, 2) : result.response}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
${result.error ? `<div style="color:#ff4d4f;margin-top:8px;"><strong>错误:</strong> ${result.error}</div>` : ''}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
btn.innerHTML = isSuccess ? '✓ 通过' : '✗ 失败';
|
|
|
|
|
|
btn.className = isSuccess ? 'btn btn-success btn-sm' : 'btn btn-danger btn-sm';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.message || '测试执行失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
resultDiv.className = 'test-result show error';
|
|
|
|
|
|
resultDiv.innerHTML = `<div class="test-result-header"><span>❌ 执行错误</span></div><div>${e.message}</div>`;
|
|
|
|
|
|
btn.innerHTML = '✗ 失败';
|
|
|
|
|
|
btn.className = 'btn btn-danger btn-sm';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runAllAPITests() {
|
|
|
|
|
|
const allIds = [];
|
|
|
|
|
|
apiTestCases.forEach(cat => cat.items.forEach(item => allIds.push(item.id)));
|
|
|
|
|
|
|
|
|
|
|
|
for (const id of allIds) {
|
|
|
|
|
|
await runAPITest(id);
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 500)); // 间隔500ms避免请求过快
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearAllAPIResults() {
|
|
|
|
|
|
document.querySelectorAll('[id^="api-result-"]').forEach(el => {
|
|
|
|
|
|
el.className = 'test-result';
|
|
|
|
|
|
el.innerHTML = '';
|
|
|
|
|
|
});
|
|
|
|
|
|
document.querySelectorAll('[id^="btn-api-"]').forEach(el => {
|
|
|
|
|
|
el.innerHTML = '测试';
|
|
|
|
|
|
el.className = 'btn btn-primary btn-sm';
|
|
|
|
|
|
el.disabled = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============ 对内接口测试 ============
|
|
|
|
|
|
let internalTestCases = [];
|
|
|
|
|
|
|
|
|
|
|
|
async function loadInternalTestList() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/tests/internal', {
|
|
|
|
|
|
headers: { 'X-Admin-Token': apiKey }
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.code === 0) {
|
|
|
|
|
|
internalTestCases = data.data.categories || [];
|
|
|
|
|
|
renderInternalTestList();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
document.getElementById('internal-test-list').innerHTML = '加载失败: ' + e.message;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderInternalTestList() {
|
|
|
|
|
|
let html = '';
|
|
|
|
|
|
internalTestCases.forEach(cat => {
|
|
|
|
|
|
html += `<div class="test-category">
|
|
|
|
|
|
<div class="test-category-title">${cat.name}</div>`;
|
|
|
|
|
|
cat.items.forEach(item => {
|
|
|
|
|
|
html += `<div class="test-item" id="internal-test-${item.id}">
|
|
|
|
|
|
<div class="test-item-info">
|
|
|
|
|
|
<div class="test-item-name">
|
|
|
|
|
|
<span class="status-badge" style="background:#722ed1;color:#fff;margin-right:8px;">SDK</span>
|
|
|
|
|
|
${item.name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-item-desc">${item.description}</div>
|
|
|
|
|
|
<div class="test-item-meta">${item.method}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-item-actions">
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="runInternalTest('${item.id}')" id="btn-internal-${item.id}">测试</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result" id="internal-result-${item.id}"></div>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
});
|
|
|
|
|
|
document.getElementById('internal-test-list').innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runInternalTest(testId) {
|
|
|
|
|
|
const btn = document.getElementById(`btn-internal-${testId}`);
|
|
|
|
|
|
const resultDiv = document.getElementById(`internal-result-${testId}`);
|
|
|
|
|
|
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
btn.innerHTML = '<span class="loading-spinner"></span>';
|
|
|
|
|
|
resultDiv.className = 'test-result';
|
|
|
|
|
|
resultDiv.classList.add('show');
|
|
|
|
|
|
resultDiv.innerHTML = '运行中...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/tests/internal/run', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'X-Admin-Token': apiKey
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({ id: testId })
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.code === 0 && data.data) {
|
|
|
|
|
|
const result = data.data;
|
|
|
|
|
|
const isSuccess = result.success;
|
|
|
|
|
|
|
|
|
|
|
|
resultDiv.className = 'test-result show ' + (isSuccess ? 'success' : 'error');
|
|
|
|
|
|
resultDiv.innerHTML = `
|
|
|
|
|
|
<div class="test-result-header">
|
|
|
|
|
|
<span>${isSuccess ? '✅ 测试通过' : '❌ 测试失败'}</span>
|
|
|
|
|
|
<span>${result.latency}ms</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="margin-bottom:8px;">
|
|
|
|
|
|
<strong>接口:</strong> ${result.interface}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="margin-bottom:8px;">
|
|
|
|
|
|
<strong>参数:</strong> <pre>${JSON.stringify(result.params, null, 2)}</pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>响应:</strong>
|
|
|
|
|
|
<div class="test-result-body">${typeof result.response === 'object' ? JSON.stringify(result.response, null, 2) : result.response}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
${result.error ? `<div style="color:#ff4d4f;margin-top:8px;"><strong>错误:</strong> ${result.error}</div>` : ''}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
btn.innerHTML = isSuccess ? '✓ 通过' : '✗ 失败';
|
|
|
|
|
|
btn.className = isSuccess ? 'btn btn-success btn-sm' : 'btn btn-danger btn-sm';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.message || '测试执行失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
resultDiv.className = 'test-result show error';
|
|
|
|
|
|
resultDiv.innerHTML = `<div class="test-result-header"><span>❌ 执行错误</span></div><div>${e.message}</div>`;
|
|
|
|
|
|
btn.innerHTML = '✗ 失败';
|
|
|
|
|
|
btn.className = 'btn btn-danger btn-sm';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runAllInternalTests() {
|
|
|
|
|
|
const allIds = [];
|
|
|
|
|
|
internalTestCases.forEach(cat => cat.items.forEach(item => allIds.push(item.id)));
|
|
|
|
|
|
|
|
|
|
|
|
for (const id of allIds) {
|
|
|
|
|
|
await runInternalTest(id);
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 300));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearAllInternalResults() {
|
|
|
|
|
|
document.querySelectorAll('[id^="internal-result-"]').forEach(el => {
|
|
|
|
|
|
el.className = 'test-result';
|
|
|
|
|
|
el.innerHTML = '';
|
|
|
|
|
|
});
|
|
|
|
|
|
document.querySelectorAll('[id^="btn-internal-"]').forEach(el => {
|
|
|
|
|
|
el.innerHTML = '测试';
|
|
|
|
|
|
el.className = 'btn btn-primary btn-sm';
|
|
|
|
|
|
el.disabled = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============ WebSocket 测试 ============
|
|
|
|
|
|
let wsTestCases = [];
|
|
|
|
|
|
|
|
|
|
|
|
async function loadWSTestList() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/tests/ws', {
|
|
|
|
|
|
headers: { 'X-Admin-Token': apiKey }
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.code === 0) {
|
|
|
|
|
|
wsTestCases = data.data.cases || [];
|
|
|
|
|
|
renderWSTestList(data.data.ws_url);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
document.getElementById('ws-test-list').innerHTML = '加载失败: ' + e.message;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderWSTestList(wsUrl) {
|
|
|
|
|
|
let html = `<div class="test-category">
|
|
|
|
|
|
<div class="test-category-title">WebSocket 连接测试 (${wsUrl})</div>`;
|
|
|
|
|
|
wsTestCases.forEach(item => {
|
|
|
|
|
|
const symbolsStr = item.symbols ? item.symbols.join(', ') : '';
|
|
|
|
|
|
html += `<div class="test-item" id="ws-test-${item.id}">
|
|
|
|
|
|
<div class="test-item-info">
|
|
|
|
|
|
<div class="test-item-name">${item.name}</div>
|
|
|
|
|
|
<div class="test-item-desc">${item.description}</div>
|
|
|
|
|
|
<div class="test-item-meta">动作: ${item.action} | 标的: ${symbolsStr}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-item-actions">
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="runWSTest('${item.id}')" id="btn-ws-${item.id}">测试</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result" id="ws-result-${item.id}"></div>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
document.getElementById('ws-test-list').innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runWSTest(testId) {
|
|
|
|
|
|
const btn = document.getElementById(`btn-ws-${testId}`);
|
|
|
|
|
|
const resultDiv = document.getElementById(`ws-result-${testId}`);
|
|
|
|
|
|
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
btn.innerHTML = '<span class="loading-spinner"></span>';
|
|
|
|
|
|
resultDiv.className = 'test-result show';
|
|
|
|
|
|
resultDiv.innerHTML = '连接中...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/tests/ws/run', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'X-Admin-Token': apiKey
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({ id: testId })
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.code === 0 && data.data) {
|
|
|
|
|
|
const result = data.data;
|
|
|
|
|
|
const isSuccess = result.success;
|
|
|
|
|
|
|
|
|
|
|
|
resultDiv.className = 'test-result show ' + (isSuccess ? 'success' : 'error');
|
|
|
|
|
|
|
|
|
|
|
|
let messagesHtml = '';
|
|
|
|
|
|
if (result.messages && result.messages.length > 0) {
|
|
|
|
|
|
messagesHtml = `<div style="margin-top:8px;"><strong>消息记录:</strong></div>`;
|
|
|
|
|
|
result.messages.forEach((msg, idx) => {
|
|
|
|
|
|
messagesHtml += `<div class="test-result-body" style="margin-top:4px;">[${idx+1}] ${JSON.stringify(msg, null, 2)}</div>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resultDiv.innerHTML = `
|
|
|
|
|
|
<div class="test-result-header">
|
|
|
|
|
|
<span>${isSuccess ? '✅ 连接成功' : '❌ 连接失败'}</span>
|
|
|
|
|
|
<span>${result.latency}ms</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
${messagesHtml}
|
|
|
|
|
|
${result.error ? `<div style="color:#ff4d4f;margin-top:8px;"><strong>错误:</strong> ${result.error}</div>` : ''}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
btn.innerHTML = isSuccess ? '✓ 通过' : '✗ 失败';
|
|
|
|
|
|
btn.className = isSuccess ? 'btn btn-success btn-sm' : 'btn btn-danger btn-sm';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.message || '测试执行失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
resultDiv.className = 'test-result show error';
|
|
|
|
|
|
resultDiv.innerHTML = `<div class="test-result-header"><span>❌ 执行错误</span></div><div>${e.message}</div>`;
|
|
|
|
|
|
btn.innerHTML = '✗ 失败';
|
|
|
|
|
|
btn.className = 'btn btn-danger btn-sm';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runAllWSTests() {
|
|
|
|
|
|
for (const item of wsTestCases) {
|
|
|
|
|
|
await runWSTest(item.id);
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 1000)); // WS测试间隔更长
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearAllWSResults() {
|
|
|
|
|
|
document.querySelectorAll('[id^="ws-result-"]').forEach(el => {
|
|
|
|
|
|
el.className = 'test-result';
|
|
|
|
|
|
el.innerHTML = '';
|
|
|
|
|
|
});
|
|
|
|
|
|
document.querySelectorAll('[id^="btn-ws-"]').forEach(el => {
|
|
|
|
|
|
el.innerHTML = '测试';
|
|
|
|
|
|
el.className = 'btn btn-primary btn-sm';
|
|
|
|
|
|
el.disabled = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============ 测试历史 ============
|
|
|
|
|
|
async function loadTestHistory() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/tests/history?limit=20', {
|
|
|
|
|
|
headers: { 'X-Admin-Token': apiKey }
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.code === 0) {
|
|
|
|
|
|
renderTestHistory(data.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
document.getElementById('test-history-list').innerHTML = '加载失败: ' + e.message;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderTestHistory(data) {
|
|
|
|
|
|
let html = '';
|
|
|
|
|
|
|
|
|
|
|
|
if (data.api_tests && data.api_tests.length > 0) {
|
|
|
|
|
|
html += `<div class="test-category">
|
|
|
|
|
|
<div class="test-category-title">API 测试历史</div>`;
|
|
|
|
|
|
data.api_tests.slice().reverse().forEach(test => {
|
|
|
|
|
|
const time = new Date(test.timestamp).toLocaleString();
|
|
|
|
|
|
html += `<div class="test-item">
|
|
|
|
|
|
<div class="test-item-info">
|
|
|
|
|
|
<div class="test-item-name">
|
|
|
|
|
|
<span class="status-badge ${test.success ? 'status-success' : 'status-error'}">${test.success ? '通过' : '失败'}</span>
|
|
|
|
|
|
${test.name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-item-meta">${time} | ${test.latency}ms | HTTP ${test.status_code}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.ws_tests && data.ws_tests.length > 0) {
|
|
|
|
|
|
html += `<div class="test-category">
|
|
|
|
|
|
<div class="test-category-title">WebSocket 测试历史</div>`;
|
|
|
|
|
|
data.ws_tests.slice().reverse().forEach(test => {
|
|
|
|
|
|
const time = new Date(test.timestamp).toLocaleString();
|
|
|
|
|
|
html += `<div class="test-item">
|
|
|
|
|
|
<div class="test-item-info">
|
|
|
|
|
|
<div class="test-item-name">
|
|
|
|
|
|
<span class="status-badge ${test.success ? 'status-success' : 'status-error'}">${test.success ? '通过' : '失败'}</span>
|
|
|
|
|
|
${test.case_id}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-item-meta">${time} | ${test.latency}ms</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ((!data.api_tests || data.api_tests.length === 0) && (!data.ws_tests || data.ws_tests.length === 0)) {
|
|
|
|
|
|
html = '<p style="color:#999;text-align:center;padding:40px;">暂无测试历史</p>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('test-history-list').innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearTestHistory() {
|
|
|
|
|
|
document.getElementById('test-history-list').innerHTML = '<p style="color:#999;text-align:center;padding:40px;">暂无测试历史</p>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============ 适配器管理 ============
|
|
|
|
|
|
let currentAdapters = [];
|
|
|
|
|
|
let editingAdapter = null;
|
|
|
|
|
|
|
|
|
|
|
|
async function loadAdapterList() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/adapters', {
|
|
|
|
|
|
headers: { 'X-Admin-Token': apiKey }
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.code === 0 && data.data) {
|
|
|
|
|
|
currentAdapters = data.data.adapters || [];
|
|
|
|
|
|
renderAdapterList();
|
|
|
|
|
|
loadSourceStatus();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
document.getElementById('adapter-list').innerHTML = '<p style="color:#ff4d4f;">加载失败: ' + e.message + '</p>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderAdapterList() {
|
|
|
|
|
|
if (currentAdapters.length === 0) {
|
|
|
|
|
|
document.getElementById('adapter-list').innerHTML = '<p style="color:#999;">暂无适配器</p>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let html = '<table style="width:100%;border-collapse:collapse;">';
|
|
|
|
|
|
html += '<tr style="background:#fafafa;">';
|
|
|
|
|
|
html += '<th style="padding:12px;text-align:left;border:1px solid #f0f0f0;">适配器</th>';
|
|
|
|
|
|
html += '<th style="padding:12px;text-align:left;border:1px solid #f0f0f0;">类型</th>';
|
|
|
|
|
|
html += '<th style="padding:12px;text-align:left;border:1px solid #f0f0f0;">状态</th>';
|
|
|
|
|
|
html += '<th style="padding:12px;text-align:left;border:1px solid #f0f0f0;">描述</th>';
|
|
|
|
|
|
html += '<th style="padding:12px;text-align:center;border:1px solid #f0f0f0;">操作</th>';
|
|
|
|
|
|
html += '</tr>';
|
|
|
|
|
|
|
|
|
|
|
|
currentAdapters.forEach(adapter => {
|
|
|
|
|
|
const statusClass = adapter.status === 'active' ? 'status-success' :
|
|
|
|
|
|
adapter.status === 'standby' ? 'status-running' : '';
|
|
|
|
|
|
const statusText = adapter.status === 'active' ? '运行中' :
|
|
|
|
|
|
adapter.status === 'standby' ? '待机' : '已禁用';
|
|
|
|
|
|
|
|
|
|
|
|
html += `<tr>
|
|
|
|
|
|
<td style="padding:12px;border:1px solid #f0f0f0;">
|
|
|
|
|
|
<strong>${adapter.name}</strong>
|
|
|
|
|
|
<div style="font-size:11px;color:#999;">v${adapter.version}</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td style="padding:12px;border:1px solid #f0f0f0;text-transform:uppercase;">${adapter.type}</td>
|
|
|
|
|
|
<td style="padding:12px;border:1px solid #f0f0f0;">
|
|
|
|
|
|
<span class="status-badge ${statusClass}">${statusText}</span>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td style="padding:12px;border:1px solid #f0f0f0;color:#666;font-size:12px;">${adapter.description || '-'}</td>
|
|
|
|
|
|
<td style="padding:12px;border:1px solid #f0f0f0;text-align:center;">
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="editAdapter('${adapter.name}')">配置</button>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
html += '</table>';
|
|
|
|
|
|
document.getElementById('adapter-list').innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadSourceStatus() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/source/status', {
|
|
|
|
|
|
headers: { 'X-API-Key': apiKey }
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.code === 0 && data.data) {
|
|
|
|
|
|
let html = '<div class="stats-grid" style="grid-template-columns:repeat(2,1fr);">';
|
|
|
|
|
|
|
|
|
|
|
|
// 股票数据源
|
|
|
|
|
|
const stock = data.data.stock;
|
|
|
|
|
|
html += `<div class="stat-card">
|
|
|
|
|
|
<div style="font-size:12px;color:#666;margin-bottom:4px;">股票数据源</div>
|
|
|
|
|
|
<div style="font-size:20px;font-weight:bold;color:#1890ff;">${stock.active_source}</div>
|
|
|
|
|
|
<div style="font-size:11px;color:#999;margin-top:4px;">状态: ${stock.status}</div>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
|
|
|
|
|
// 期货数据源
|
|
|
|
|
|
const futures = data.data.futures;
|
|
|
|
|
|
html += `<div class="stat-card">
|
|
|
|
|
|
<div style="font-size:12px;color:#666;margin-bottom:4px;">期货数据源</div>
|
|
|
|
|
|
<div style="font-size:20px;font-weight:bold;color:#1890ff;">${futures.active_source}</div>
|
|
|
|
|
|
<div style="font-size:11px;color:#999;margin-top:4px;">状态: ${futures.status}</div>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
// 切换数据源按钮
|
|
|
|
|
|
html += '<div style="margin-top:16px;">';
|
|
|
|
|
|
html += '<button class="btn btn-primary btn-sm" onclick="switchToAdapter("custom")">切换到自定义数据源</button>';
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('source-status').innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
document.getElementById('source-status').innerHTML = '<p style="color:#ff4d4f;">加载失败: ' + e.message + '</p>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function switchToAdapter(source) {
|
|
|
|
|
|
if (!confirm(`确定要切换到 ${source} 数据源吗?`)) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/source/switch', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'X-API-Key': apiKey
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
asset_class: 'all',
|
|
|
|
|
|
source: source,
|
|
|
|
|
|
sync_backfill: false
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.code === 0) {
|
|
|
|
|
|
alert('切换成功');
|
|
|
|
|
|
loadSourceStatus();
|
|
|
|
|
|
loadAdapterList();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
alert('切换失败: ' + data.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert('错误: ' + e.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function editAdapter(name) {
|
|
|
|
|
|
editingAdapter = currentAdapters.find(a => a.name === name);
|
|
|
|
|
|
if (!editingAdapter) return;
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('modal-adapter-name').textContent = name;
|
|
|
|
|
|
document.getElementById('modal-adapter-enabled').value = editingAdapter.status !== 'disabled' ? 'true' : 'false';
|
|
|
|
|
|
|
|
|
|
|
|
// 生成配置字段
|
|
|
|
|
|
let configHtml = '';
|
|
|
|
|
|
if (editingAdapter.config) {
|
|
|
|
|
|
for (const [key, value] of Object.entries(editingAdapter.config)) {
|
|
|
|
|
|
configHtml += `<div style="margin-bottom:12px;">
|
|
|
|
|
|
<label style="display:block;margin-bottom:4px;font-size:12px;color:#666;">${key}</label>
|
|
|
|
|
|
<input type="text" id="config-${key}" value="${value}" style="width:100%;padding:8px;border:1px solid #d9d9d9;border-radius:4px;">
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
document.getElementById('modal-config-fields').innerHTML = configHtml;
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('adapter-config-modal').style.display = 'flex';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeAdapterModal() {
|
|
|
|
|
|
document.getElementById('adapter-config-modal').style.display = 'none';
|
|
|
|
|
|
editingAdapter = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveAdapterConfig() {
|
|
|
|
|
|
if (!editingAdapter) return;
|
|
|
|
|
|
|
|
|
|
|
|
const enabled = document.getElementById('modal-adapter-enabled').value === 'true';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. 切换启用状态
|
|
|
|
|
|
const toggleResponse = await fetch('/v1/admin/adapters/toggle', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'X-Admin-Token': apiKey
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
name: editingAdapter.name,
|
|
|
|
|
|
enable: enabled
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const toggleData = await toggleResponse.json();
|
|
|
|
|
|
if (toggleData.code !== 0) {
|
|
|
|
|
|
throw new Error(toggleData.message || '切换状态失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 保存配置
|
|
|
|
|
|
const newConfig = {};
|
|
|
|
|
|
if (editingAdapter.config) {
|
|
|
|
|
|
for (const key of Object.keys(editingAdapter.config)) {
|
|
|
|
|
|
const input = document.getElementById(`config-${key}`);
|
|
|
|
|
|
if (input) {
|
|
|
|
|
|
newConfig[key] = input.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const configResponse = await fetch('/v1/admin/adapters/config', {
|
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'X-Admin-Token': apiKey
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
name: editingAdapter.name,
|
|
|
|
|
|
config: newConfig
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const configData = await configResponse.json();
|
|
|
|
|
|
if (configData.code !== 0) {
|
|
|
|
|
|
throw new Error(configData.message || '保存配置失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
alert('保存成功');
|
|
|
|
|
|
closeAdapterModal();
|
|
|
|
|
|
loadAdapterList();
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert('保存失败: ' + e.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============ 快速测试运行全部 ============
|
|
|
|
|
|
async function runAllQuickTests() {
|
|
|
|
|
|
await quickTestStockKlines();
|
|
|
|
|
|
await quickTestStockSymbols();
|
|
|
|
|
|
await quickTestFuturesKlines();
|
|
|
|
|
|
await quickTestFuturesSymbols();
|
|
|
|
|
|
await quickTestHealth();
|
|
|
|
|
|
await quickTestSourceStatus();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearAllQuickResults() {
|
|
|
|
|
|
['test-result-stock-klines', 'test-result-stock-symbols',
|
|
|
|
|
|
'test-result-futures-klines', 'test-result-futures-symbols',
|
|
|
|
|
|
'test-result-health', 'test-result-source-status'].forEach(id => {
|
|
|
|
|
|
const el = document.getElementById(id);
|
|
|
|
|
|
if (el) {
|
|
|
|
|
|
el.className = 'test-result';
|
|
|
|
|
|
el.innerHTML = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============ 配置页面快速测试 ============
|
|
|
|
|
|
async function quickTestStockKlines() {
|
|
|
|
|
|
const symbol = document.getElementById('test-stock-symbol').value || '000001.SZ';
|
|
|
|
|
|
const freq = document.getElementById('test-stock-freq').value || '1d';
|
|
|
|
|
|
const resultDiv = document.getElementById('test-result-stock-klines');
|
|
|
|
|
|
|
|
|
|
|
|
resultDiv.className = 'test-result show';
|
|
|
|
|
|
resultDiv.innerHTML = '请求中...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 固定测试日期:2026年3月2日到2026年3月6日
|
|
|
|
|
|
const startStr = '20260302';
|
|
|
|
|
|
const endStr = '20260306';
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`/v1/stock/klines/${symbol}?start=${startStr}&end=${endStr}&freq=${freq}`, {
|
|
|
|
|
|
headers: { 'X-API-Key': apiKey }
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
resultDiv.className = 'test-result show ' + (data.code === 0 ? 'success' : 'error');
|
|
|
|
|
|
resultDiv.innerHTML = `
|
|
|
|
|
|
<div class="test-result-header">
|
|
|
|
|
|
<span>${data.code === 0 ? '✅ 成功' : '❌ 失败'}</span>
|
|
|
|
|
|
<span>HTTP ${response.status}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result-body">${JSON.stringify(data, null, 2)}</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
resultDiv.className = 'test-result show error';
|
|
|
|
|
|
resultDiv.innerHTML = `<div class="test-result-header"><span>❌ 错误</span></div><div>${e.message}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function quickTestStockSymbols() {
|
|
|
|
|
|
const resultDiv = document.getElementById('test-result-stock-symbols');
|
|
|
|
|
|
resultDiv.className = 'test-result show';
|
|
|
|
|
|
resultDiv.innerHTML = '请求中...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/stock/symbols?page=1&size=10', {
|
|
|
|
|
|
headers: { 'X-API-Key': apiKey }
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
resultDiv.className = 'test-result show ' + (data.code === 0 ? 'success' : 'error');
|
|
|
|
|
|
resultDiv.innerHTML = `
|
|
|
|
|
|
<div class="test-result-header">
|
|
|
|
|
|
<span>${data.code === 0 ? '✅ 成功' : '❌ 失败'}</span>
|
|
|
|
|
|
<span>HTTP ${response.status}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result-body">${JSON.stringify(data, null, 2)}</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
resultDiv.className = 'test-result show error';
|
|
|
|
|
|
resultDiv.innerHTML = `<div class="test-result-header"><span>❌ 错误</span></div><div>${e.message}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function quickTestFuturesKlines() {
|
|
|
|
|
|
const symbol = document.getElementById('test-futures-symbol').value || 'CU2504.SHFE';
|
|
|
|
|
|
const freq = document.getElementById('test-futures-freq').value || '1d';
|
|
|
|
|
|
const resultDiv = document.getElementById('test-result-futures-klines');
|
|
|
|
|
|
|
|
|
|
|
|
resultDiv.className = 'test-result show';
|
|
|
|
|
|
resultDiv.innerHTML = '请求中...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 固定测试日期:2026年3月2日到2026年3月6日
|
|
|
|
|
|
const startStr = '20260302';
|
|
|
|
|
|
const endStr = '20260306';
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`/v1/futures/klines/${symbol}?start=${startStr}&end=${endStr}&freq=${freq}`, {
|
|
|
|
|
|
headers: { 'X-API-Key': apiKey }
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
resultDiv.className = 'test-result show ' + (data.code === 0 ? 'success' : 'error');
|
|
|
|
|
|
resultDiv.innerHTML = `
|
|
|
|
|
|
<div class="test-result-header">
|
|
|
|
|
|
<span>${data.code === 0 ? '✅ 成功' : '❌ 失败'}</span>
|
|
|
|
|
|
<span>HTTP ${response.status}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result-body">${JSON.stringify(data, null, 2)}</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
resultDiv.className = 'test-result show error';
|
|
|
|
|
|
resultDiv.innerHTML = `<div class="test-result-header"><span>❌ 错误</span></div><div>${e.message}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function quickTestFuturesSymbols() {
|
|
|
|
|
|
const resultDiv = document.getElementById('test-result-futures-symbols');
|
|
|
|
|
|
resultDiv.className = 'test-result show';
|
|
|
|
|
|
resultDiv.innerHTML = '请求中...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/futures/symbols?page=1&size=10', {
|
|
|
|
|
|
headers: { 'X-API-Key': apiKey }
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
resultDiv.className = 'test-result show ' + (data.code === 0 ? 'success' : 'error');
|
|
|
|
|
|
resultDiv.innerHTML = `
|
|
|
|
|
|
<div class="test-result-header">
|
|
|
|
|
|
<span>${data.code === 0 ? '✅ 成功' : '❌ 失败'}</span>
|
|
|
|
|
|
<span>HTTP ${response.status}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result-body">${JSON.stringify(data, null, 2)}</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
resultDiv.className = 'test-result show error';
|
|
|
|
|
|
resultDiv.innerHTML = `<div class="test-result-header"><span>❌ 错误</span></div><div>${e.message}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function quickTestHealth() {
|
|
|
|
|
|
const resultDiv = document.getElementById('test-result-health');
|
|
|
|
|
|
resultDiv.className = 'test-result show';
|
|
|
|
|
|
resultDiv.innerHTML = '请求中...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/health');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
resultDiv.className = 'test-result show success';
|
|
|
|
|
|
resultDiv.innerHTML = `
|
|
|
|
|
|
<div class="test-result-header">
|
|
|
|
|
|
<span>✅ 健康</span>
|
|
|
|
|
|
<span>HTTP ${response.status}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result-body">${JSON.stringify(data, null, 2)}</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
resultDiv.className = 'test-result show error';
|
|
|
|
|
|
resultDiv.innerHTML = `<div class="test-result-header"><span>❌ 错误</span></div><div>${e.message}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function quickTestSourceStatus() {
|
|
|
|
|
|
const resultDiv = document.getElementById('test-result-source-status');
|
|
|
|
|
|
resultDiv.className = 'test-result show';
|
|
|
|
|
|
resultDiv.innerHTML = '请求中...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/source/status', {
|
|
|
|
|
|
headers: { 'X-API-Key': apiKey }
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
resultDiv.className = 'test-result show ' + (data.code === 0 ? 'success' : 'error');
|
|
|
|
|
|
resultDiv.innerHTML = `
|
|
|
|
|
|
<div class="test-result-header">
|
|
|
|
|
|
<span>${data.code === 0 ? '✅ 成功' : '❌ 失败'}</span>
|
|
|
|
|
|
<span>HTTP ${response.status}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result-body">${JSON.stringify(data, null, 2)}</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
resultDiv.className = 'test-result show error';
|
|
|
|
|
|
resultDiv.innerHTML = `<div class="test-result-header"><span>❌ 错误</span></div><div>${e.message}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadConfigList() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/config', {
|
|
|
|
|
|
headers: { 'X-Admin-Token': apiKey }
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.code === 0) {
|
|
|
|
|
|
const configs = data.data.configs || [];
|
|
|
|
|
|
let html = '<table style="width:100%;border-collapse:collapse;">';
|
|
|
|
|
|
html += '<tr style="background:#fafafa;"><th style="padding:12px;text-align:left;border:1px solid #f0f0f0;">配置项</th><th style="padding:12px;text-align:left;border:1px solid #f0f0f0;">值</th><th style="padding:12px;text-align:left;border:1px solid #f0f0f0;">描述</th></tr>';
|
|
|
|
|
|
configs.forEach(cfg => {
|
|
|
|
|
|
html += `<tr>
|
|
|
|
|
|
<td style="padding:12px;border:1px solid #f0f0f0;">${cfg.key}</td>
|
|
|
|
|
|
<td style="padding:12px;border:1px solid #f0f0f0;"><code>${cfg.value}</code></td>
|
|
|
|
|
|
<td style="padding:12px;border:1px solid #f0f0f0;color:#666;">${cfg.description || '-'}</td>
|
|
|
|
|
|
</tr>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
html += '</table>';
|
|
|
|
|
|
document.getElementById('config-list').innerHTML = html;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
document.getElementById('config-list').innerHTML = '<p style="color:#999;">暂无配置数据</p>';
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
document.getElementById('config-list').innerHTML = '<p style="color:#ff4d4f;">加载失败: ' + e.message + '</p>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============ 原有功能 ============
|
|
|
|
|
|
async function reloadConfig() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/system/reload', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {'Content-Type': 'application/json'}
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
alert(data.message || '热加载完成');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert('热加载失败: ' + e.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function restartService() {
|
|
|
|
|
|
if (confirm('确定要重启服务吗?')) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/system/restart', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {'Content-Type': 'application/json'}
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
alert(data.message || '重启命令已发送');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert('重启失败: ' + e.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadSystemStatus() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/v1/admin/system/status');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.data) {
|
|
|
|
|
|
document.getElementById('stat-uptime').textContent = data.data.uptime;
|
|
|
|
|
|
document.getElementById('stat-threads').textContent = data.data.threads;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Failed to load system status:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 页面加载时获取状态
|
|
|
|
|
|
loadSystemStatus();
|
|
|
|
|
|
setInterval(loadSystemStatus, 30000);
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/admin", response_class=HTMLResponse)
|
|
|
|
|
|
async def admin_page():
|
|
|
|
|
|
"""管理后台页面"""
|
|
|
|
|
|
return ADMIN_HTML
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/")
|
|
|
|
|
|
async def root():
|
|
|
|
|
|
"""根路径重定向到管理后台"""
|
|
|
|
|
|
return {"message": "Market Data Service API", "docs": "/docs", "admin": "/admin"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
import uvicorn
|
|
|
|
|
|
|
|
|
|
|
|
# 从环境变量或配置获取端口
|
|
|
|
|
|
port = settings.port or config.server.port
|
|
|
|
|
|
|
|
|
|
|
|
uvicorn.run(
|
|
|
|
|
|
"app.main:app",
|
|
|
|
|
|
host="0.0.0.0",
|
|
|
|
|
|
port=port,
|
|
|
|
|
|
reload=config.server.mode == "debug"
|
|
|
|
|
|
)
|