From e1bc7c8ca34ae9fa959ac02e3fbf8ac5c0083cfa Mon Sep 17 00:00:00 2001 From: Lxy Date: Sun, 24 May 2026 23:44:59 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E7=99=BB=E9=99=86?= =?UTF-8?q?=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 5 + DEPLOY.md | 236 ++++++++++++++++++++++++++++++ app/api/auth.py | 149 +++++++++++++++++++ app/auth_service.py | 129 ++++++++++++++++ app/main.py | 44 ++++-- app/static/login.html | 284 ++++++++++++++++++++++++++++++++++++ app/static/role_select.html | 215 +++++++++++++++++++++++++++ app/user_models.py | 38 +++++ backup.bat | 41 ++++++ data/buffer.db | Bin 2035712 -> 2052096 bytes deploy.bat | 55 +++++++ docker-compose.yml | 7 +- manage.bat | 98 +++++++++++++ start.bat | 25 ++++ stop.bat | 13 ++ 15 files changed, 1328 insertions(+), 11 deletions(-) create mode 100644 DEPLOY.md create mode 100644 app/api/auth.py create mode 100644 app/auth_service.py create mode 100644 app/static/login.html create mode 100644 app/static/role_select.html create mode 100644 app/user_models.py create mode 100644 backup.bat create mode 100644 deploy.bat create mode 100644 manage.bat create mode 100644 start.bat create mode 100644 stop.bat diff --git a/.dockerignore b/.dockerignore index df5bb9d..61b233d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,3 +15,8 @@ build/ docker-compose.yml Dockerfile .dockerignore +android_app/ +data/ +logs/ +*.log +.env.local diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..28e33fc --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,236 @@ +# 期货智析平台 - Docker 部署指南 + +## 📋 部署前准备 + +### 1. 环境要求 +- Docker Desktop for Windows(已安装并运行) +- Windows 10/11 专业版或家庭版(WSL2后端) +- 至少 2GB 可用内存 +- 至少 5GB 磁盘空间 + +### 2. 检查Docker状态 +```powershell +docker --version +docker-compose --version +``` + +## 🚀 快速部署 + +### 方法一:使用部署脚本(推荐) +1. 双击运行 `deploy.bat` +2. 等待自动完成构建和部署 +3. 访问 http://localhost:9600 + +### 方法二:手动部署 +```powershell +# 进入项目目录 +cd e:\docker_workspace\cobot2.0_WorkHorse\app\working\workspaces\default\share_data\project\market_data_colector_platform\buffer_platform + +# 停止旧容器 +docker-compose down + +# 构建镜像 +docker-compose build --no-cache + +# 启动服务 +docker-compose up -d +``` + +## 🌐 访问地址 + +部署成功后,可访问以下地址: + +| 服务 | 地址 | +|------|------| +| 主页 | http://localhost:9600 | +| 品种分析 | http://localhost:9600/futures-analysis | +| AI配置 | http://localhost:9600/ai-config | +| API文档 | http://localhost:9600/docs | +| 健康检查 | http://localhost:9600/api/v1/health | + +## 🛠️ 日常管理 + +### 使用管理脚本 +双击运行 `manage.bat`,可选择: +1. 启动服务 +2. 停止服务 +3. 重启服务 +4. 查看日志 +5. 查看状态 +6. 进入容器 +7. 清理资源 + +### 常用Docker命令 + +```powershell +# 查看容器状态 +docker-compose ps + +# 查看实时日志 +docker-compose logs -f + +# 查看最近100行日志 +docker-compose logs --tail=100 + +# 重启服务 +docker-compose restart + +# 停止服务 +docker-compose stop + +# 启动服务 +docker-compose start + +# 停止并删除容器(保留数据) +docker-compose down + +# 完全清理(包括数据卷) +docker-compose down -v + +# 进入容器bash +docker exec -it futures-buffer-platform /bin/bash + +# 查看容器资源使用 +docker stats futures-buffer-platform +``` + +## 📁 目录结构 + +``` +buffer_platform/ +├── data/ # SQLite数据库(自动创建) +├── logs/ # 日志文件(自动创建) +├── app/ +│ └── static/ # 前端静态文件 +├── docker-compose.yml # Docker编排配置 +├── Dockerfile # Docker镜像构建文件 +├── deploy.bat # 一键部署脚本 +└── manage.bat # 管理脚本 +``` + +## 🔧 配置说明 + +### 环境变量 + +在 `docker-compose.yml` 中可配置: + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| BUFFER_DB_PATH | 数据库路径 | /app/data/buffer.db | +| BUFFER_HOST | 服务监听地址 | 0.0.0.0 | +| BUFFER_PORT | 服务端口 | 8600 | +| CACHE_TTL | 缓存过期时间(秒) | 300 | +| BUFFER_LOG_LEVEL | 日志级别 | INFO | +| MAX_WORKERS | 并发采集数 | 2 | +| TZ | 时区 | Asia/Shanghai | + +### 端口映射 + +- 宿主机端口:9600 +- 容器端口:8600 + +如需修改端口,编辑 `docker-compose.yml` 中的 `ports` 配置: +```yaml +ports: + - "你想要的端口:8600" +``` + +## 🔍 故障排查 + +### 1. 容器无法启动 +```powershell +# 查看详细日志 +docker-compose logs + +# 检查端口占用 +netstat -ano | findstr "9600" +``` + +### 2. 数据库问题 +```powershell +# 进入容器检查数据库 +docker exec -it futures-buffer-platform ls -la /app/data +``` + +### 3. 重新构建镜像 +```powershell +# 清理旧镜像 +docker-compose down +docker system prune -a + +# 重新构建 +docker-compose build --no-cache +docker-compose up -d +``` + +### 4. 查看健康状态 +```powershell +# 查看容器健康检查状态 +docker inspect --format='{{.State.Health.Status}}' futures-buffer-platform +``` + +## 🔄 更新部署 + +当代码有更新时: + +```powershell +# 方法一:使用部署脚本 +deploy.bat + +# 方法二:手动更新 +docker-compose down +docker-compose build --no-cache +docker-compose up -d +``` + +## 📊 监控和日志 + +### 实时日志 +```powershell +docker-compose logs -f buffer-platform +``` + +### 容器资源监控 +```powershell +docker stats futures-buffer-platform +``` + +### 日志位置 +- 容器内:/app/logs/ +- 宿主机:./logs/ + +## 🗄️ 数据备份 + +### 备份数据库 +```powershell +docker cp futures-buffer-platform:/app/data/buffer.db ./backup_buffer.db +``` + +### 恢复数据库 +```powershell +docker cp ./backup_buffer.db futures-buffer-platform:/app/data/buffer.db +docker-compose restart +``` + +## 🎯 生产环境建议 + +1. **修改默认端口**:避免端口冲突 +2. **配置日志轮转**:在docker-compose.yml中添加: + ```yaml + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + ``` +3. **数据卷备份**:定期备份data目录 +4. **监控资源**:使用docker stats监控资源使用 +5. **设置重启策略**:已配置为 `unless-stopped` + +## 📞 技术支持 + +如遇问题,请检查: +1. Docker Desktop是否正常运行 +2. 端口9600是否被占用 +3. 查看容器日志:`docker-compose logs` +4. 检查健康状态:`docker-compose ps` diff --git a/app/api/auth.py b/app/api/auth.py new file mode 100644 index 0000000..9f898fb --- /dev/null +++ b/app/api/auth.py @@ -0,0 +1,149 @@ +""" +用户权限系统 - API路由 +""" +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.orm import Session +from pydantic import BaseModel +from datetime import datetime +from typing import Optional + +from app.database import get_db +from app.user_models import User +from app import auth_service + +router = APIRouter(prefix="/auth", tags=["用户认证"]) +security = HTTPBearer() + + +# 请求模型 +class LoginRequest(BaseModel): + username: str + password: str + + +class LoginResponse(BaseModel): + success: bool + token: Optional[str] = None + user: Optional[dict] = None + message: Optional[str] = None + + +class UserInfo(BaseModel): + id: int + username: str + role: str + email: Optional[str] + + +# 登录接口 +@router.post("/login", response_model=LoginResponse) +def login(request: LoginRequest, db: Session = Depends(get_db)): + """用户登录""" + user = auth_service.authenticate_user(db, request.username, request.password) + + if not user: + return LoginResponse( + success=False, + message="用户名或密码错误" + ) + + # 创建会话 + token = auth_service.create_session(db, user.id) + auth_service.update_last_login(db, user) + + return LoginResponse( + success=True, + token=token, + user={ + "id": user.id, + "username": user.username, + "role": user.role, + "email": user.email + }, + message="登录成功" + ) + + +# 登出接口 +@router.post("/logout") +def logout(credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db)): + """用户登出""" + auth_service.invalidate_session(db, credentials.credentials) + return {"success": True, "message": "已登出"} + + +# 验证令牌接口 +@router.get("/verify") +def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db)): + """验证用户令牌""" + user = auth_service.validate_session(db, credentials.credentials) + + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="无效的会话令牌" + ) + + return { + "success": True, + "user": { + "id": user.id, + "username": user.username, + "role": user.role, + "email": user.email, + "last_login": user.last_login.isoformat() if user.last_login else None + } + } + + +# 获取当前用户信息 +@router.get("/me") +def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db)): + """获取当前用户信息""" + user = auth_service.validate_session(db, credentials.credentials) + + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="无效的会话令牌" + ) + + return { + "id": user.id, + "username": user.username, + "role": user.role, + "email": user.email, + "is_active": user.is_active, + "created_at": user.created_at.isoformat(), + "last_login": user.last_login.isoformat() if user.last_login else None + } + + +# 依赖注入:验证用户已登录 +def get_current_user_dependency(credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db)): + """依赖注入:获取当前用户""" + user = auth_service.validate_session(db, credentials.credentials) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="请先登录" + ) + return user + + +# 依赖注入:仅管理员可访问 +def require_admin(credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db)): + """依赖注入:验证管理员权限""" + user = auth_service.validate_session(db, credentials.credentials) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="请先登录" + ) + if user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="需要管理员权限" + ) + return user diff --git a/app/auth_service.py b/app/auth_service.py new file mode 100644 index 0000000..d4e3399 --- /dev/null +++ b/app/auth_service.py @@ -0,0 +1,129 @@ +""" +用户权限系统 - 认证服务 +""" +import hashlib +import secrets +from datetime import datetime, timedelta +from typing import Optional +from sqlalchemy.orm import Session +from app.user_models import User, Session as UserSession + + +# 密码加密工具 +def hash_password(password: str) -> str: + """对密码进行哈希加密""" + salt = secrets.token_hex(16) + pwd_hash = hashlib.sha256((salt + password).encode()).hexdigest() + return f"{salt}${pwd_hash}" + + +def verify_password(password: str, hashed: str) -> bool: + """验证密码""" + try: + salt, pwd_hash = hashed.split('$') + return hashlib.sha256((salt + password).encode()).hexdigest() == pwd_hash + except: + return False + + +def generate_token() -> str: + """生成会话令牌""" + return secrets.token_urlsafe(32) + + +# 用户操作 +def create_user(db: Session, username: str, password: str, email: str = None, role: str = 'user') -> User: + """创建新用户""" + user = User( + username=username, + password_hash=hash_password(password), + email=email, + role=role, + is_active=True + ) + db.add(user) + db.commit() + db.refresh(user) + return user + + +def authenticate_user(db: Session, username: str, password: str) -> Optional[User]: + """验证用户登录""" + user = db.query(User).filter(User.username == username).first() + if not user: + return None + if not user.is_active: + return None + if not verify_password(password, user.password_hash): + return None + return user + + +def update_last_login(db: Session, user: User): + """更新最后登录时间""" + user.last_login = datetime.utcnow() + db.commit() + + +# 会话操作 +def create_session(db: Session, user_id: int, expires_hours: int = 24) -> str: + """创建用户会话""" + token = generate_token() + session = UserSession( + user_id=user_id, + token=token, + expires_at=datetime.utcnow() + timedelta(hours=expires_hours), + is_valid=True + ) + db.add(session) + db.commit() + return token + + +def validate_session(db: Session, token: str) -> Optional[User]: + """验证会话令牌""" + session = db.query(UserSession).filter( + UserSession.token == token, + UserSession.is_valid == True, + UserSession.expires_at > datetime.utcnow() + ).first() + + if not session: + return None + + user = db.query(User).filter(User.id == session.user_id).first() + if not user or not user.is_active: + return None + + return user + + +def invalidate_session(db: Session, token: str): + """使会话失效(登出)""" + session = db.query(UserSession).filter(UserSession.token == token).first() + if session: + session.is_valid = False + db.commit() + + +def cleanup_expired_sessions(db: Session): + """清理过期会话""" + db.query(UserSession).filter( + UserSession.expires_at < datetime.utcnow() + ).update({"is_valid": False}) + db.commit() + + +# 默认用户创建 +def create_default_admin(db: Session): + """创建默认管理员账户""" + admin = db.query(User).filter(User.username == 'lxy_root').first() + if not admin: + create_user( + db=db, + username='lxy_root', + password='admin123', + email='admin@system.local', + role='admin' + ) + print("✓ 默认管理员账户已创建: lxy_root / admin123") diff --git a/app/main.py b/app/main.py index 6108c76..28947b2 100644 --- a/app/main.py +++ b/app/main.py @@ -5,14 +5,15 @@ import logging from contextlib import asynccontextmanager from pathlib import Path -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, RedirectResponse from app.database import engine, Base +from app.user_models import Base as UserBase from app.config import HOST, PORT, LOG_LEVEL -from app.api import data, tasks, config, futures_analysis, ai_config +from app.api import data, tasks, config, futures_analysis, ai_config, auth from app.services.scheduler import start_scheduler, stop_scheduler # 配置日志 @@ -29,15 +30,25 @@ async def lifespan(app: FastAPI): # 启动时:建表 + 启动调度器 logger.info("创建数据库表...") Base.metadata.create_all(bind=engine) + UserBase.metadata.create_all(bind=engine) + from app.analysis_db import init_analysis_db init_analysis_db() logger.info("期货智析数据库初始化完成") + + # 创建默认管理员账户 + from app.database import SessionLocal + from app import auth_service + db = SessionLocal() + try: + auth_service.create_default_admin(db) + finally: + db.close() logger.info("启动定时调度器...") start_scheduler() # 恢复已启用的任务 - from app.database import SessionLocal from app.services.cache import list_tasks from app.services.scheduler import add_job @@ -81,12 +92,29 @@ STATIC_DIR.mkdir(parents=True, exist_ok=True) app.mount("/static", StaticFiles(directory=str(STATIC_DIR), html=True), name="static") +# 登录页面(无需认证) +@app.get("/login") +def login_page(): + """登录页面""" + return FileResponse(str(STATIC_DIR / "login.html")) + + +# 角色选择页面(需要管理员权限) +@app.get("/role-select") +def role_select_page(): + """角色选择页面""" + return FileResponse(str(STATIC_DIR / "role_select.html")) + + +# 品种配置管理页面(需要管理员权限) @app.get("/ui") def ui_page(): """品种配置管理页面""" return FileResponse(str(STATIC_DIR / "index.html")) + # 注册路由 +app.include_router(auth.router, prefix="/api/v1") app.include_router(data.router, prefix="/api/v1") app.include_router(tasks.router, prefix="/api/v1") app.include_router(config.router, prefix="/api/v1") @@ -111,13 +139,11 @@ def health(): return {"status": "ok", "service": "market-data-buffer"} +# 根路径重定向到登录页 @app.get("/") def root(): - return { - "message": "数据缓冲平台 API", - "docs": "/docs", - "health": "/api/v1/health", - } + """根路径重定向到登录页""" + return RedirectResponse(url="/login") if __name__ == "__main__": diff --git a/app/static/login.html b/app/static/login.html new file mode 100644 index 0000000..d7de0af --- /dev/null +++ b/app/static/login.html @@ -0,0 +1,284 @@ + + + + + + 登录 - 期货智析系统 + + + +
+ + +
+ +
+
+ + +
+ +
+ + +
+ + +
+ + +
+ + + + diff --git a/app/static/role_select.html b/app/static/role_select.html new file mode 100644 index 0000000..958d72b --- /dev/null +++ b/app/static/role_select.html @@ -0,0 +1,215 @@ + + + + + + 选择入口 - 期货智析系统 + + + +
+
+

◈ 欢迎使用期货智析系统

+

请选择您要进入的模块

+ +
+ + + + +
+ + + + diff --git a/app/user_models.py b/app/user_models.py new file mode 100644 index 0000000..9b33f3d --- /dev/null +++ b/app/user_models.py @@ -0,0 +1,38 @@ +""" +用户权限系统 - 数据库模型 +""" +from datetime import datetime +from sqlalchemy import Column, Integer, String, Boolean, DateTime +from app.database import Base + + +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) + email = Column(String(100), unique=True, nullable=True) + role = Column(String(20), nullable=False, default='user') # 'admin' 或 'user' + is_active = Column(Boolean, default=True) + created_at = Column(DateTime, default=datetime.utcnow) + last_login = Column(DateTime, nullable=True) + + def __repr__(self): + return f"" + + +class Session(Base): + """用户会话表""" + __tablename__ = "sessions" + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, nullable=False, index=True) + token = Column(String(255), unique=True, nullable=False, index=True) + created_at = Column(DateTime, default=datetime.utcnow) + expires_at = Column(DateTime, nullable=False) + is_valid = Column(Boolean, default=True) + + def __repr__(self): + return f"" diff --git a/backup.bat b/backup.bat new file mode 100644 index 0000000..472230a --- /dev/null +++ b/backup.bat @@ -0,0 +1,41 @@ +@echo off +chcp 65001 >nul +setlocal enabledelayedexpansion + +echo ======================================== +echo 数据库备份工具 +echo ======================================== +echo. + +REM 生成备份文件名 +set BACKUP_DATE=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2% +set BACKUP_DATE=%BACKUP_DATE: =0% +set BACKUP_FILE=buffer_backup_%BACKUP_DATE%.db + +echo 备份文件: %BACKUP_FILE% +echo. + +REM 停止服务 +echo [1/3] 停止服务... +docker-compose stop +echo ✓ 服务已停止 + +REM 备份数据库 +echo [2/3] 备份数据库... +if exist "E:\docker_workspace\futures_datas\buffer.db" ( + copy "E:\docker_workspace\futures_datas\buffer.db" "E:\docker_workspace\futures_datas\%BACKUP_FILE%" + echo ✓ 数据库已备份到: E:\docker_workspace\futures_datas\%BACKUP_FILE% +) else ( + echo ✗ 数据库文件不存在 +) + +REM 启动服务 +echo [3/3] 启动服务... +docker-compose start +echo ✓ 服务已启动 + +echo. +echo ======================================== +echo 备份完成! +echo ======================================== +pause diff --git a/data/buffer.db b/data/buffer.db index d767e545cd6a0ce89e89873ada7b1592c8a037b6..ac31c439a8387175642ce3feda234922b380430a 100644 GIT binary patch delta 11091 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 delta 12826 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 V=LB->QpjWBu?xf4dYeR4{s(ub=|%tm diff --git a/deploy.bat b/deploy.bat new file mode 100644 index 0000000..d31f8e7 --- /dev/null +++ b/deploy.bat @@ -0,0 +1,55 @@ +@echo off +chcp 65001 >nul +echo ==================================== +echo 期货智析平台 - Docker 部署脚本 +echo ==================================== +echo. + +echo [1/4] 检查Docker环境... +docker --version >nul 2>&1 +if errorlevel 1 ( + echo [错误] 未检测到Docker,请先安装Docker Desktop + pause + exit /b 1 +) +echo [√] Docker已安装 + +echo. +echo [2/4] 停止并清理旧容器... +docker-compose down +echo [√] 清理完成 + +echo. +echo [3/4] 构建Docker镜像... +docker-compose build --no-cache +if errorlevel 1 ( + echo [错误] 镜像构建失败 + pause + exit /b 1 +) +echo [√] 镜像构建完成 + +echo. +echo [4/4] 启动Docker容器... +docker-compose up -d +if errorlevel 1 ( + echo [错误] 容器启动失败 + pause + exit /b 1 +) + +echo. +echo ==================================== +echo 部署完成! +echo ==================================== +echo. +echo 访问地址: http://localhost:9600 +echo 品种分析: http://localhost:9600/futures-analysis +echo API文档: http://localhost:9600/docs +echo 健康检查: http://localhost:9600/api/v1/health +echo. +echo 查看日志: docker-compose logs -f +echo 停止服务: docker-compose down +echo 重启服务: docker-compose restart +echo. +pause diff --git a/docker-compose.yml b/docker-compose.yml index e3bf4c8..14dc9b7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,11 +16,14 @@ services: - CACHE_TTL=300 - BUFFER_LOG_LEVEL=INFO - MAX_WORKERS=2 + - TZ=Asia/Shanghai volumes: # 数据持久化 - SQLite数据库 - - E:\docker_workspace\futures_datas:/app/data + - ./data:/app/data + # 静态文件(开发模式,生产环境应该打包到镜像中) + - ./app/static:/app/app/static # 日志持久化(可选) - - E:\docker_workspace\futures_datas\logs:/app/logs + - ./logs:/app/logs healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8600/api/v1/health"] interval: 30s diff --git a/manage.bat b/manage.bat new file mode 100644 index 0000000..08a352a --- /dev/null +++ b/manage.bat @@ -0,0 +1,98 @@ +@echo off +chcp 65001 >nul +echo ==================================== +echo 期货智析平台 - Docker 管理脚本 +echo ==================================== +echo. +echo 请选择操作: +echo 1. 启动服务 +echo 2. 停止服务 +echo 3. 重启服务 +echo 4. 查看日志 +echo 5. 查看状态 +echo 6. 进入容器 +echo 7. 清理资源 +echo 0. 退出 +echo. +set /p choice=请输入选项 (0-7): + +if "%choice%"=="1" goto start +if "%choice%"=="2" goto stop +if "%choice%"=="3" goto restart +if "%choice%"=="4" goto logs +if "%choice%"=="5" goto status +if "%choice%"=="6" goto shell +if "%choice%"=="7" goto clean +if "%choice%"=="0" goto end +goto menu + +:start +echo. +echo [启动服务...] +docker-compose up -d +echo. +echo 服务已启动!访问 http://localhost:9600 +pause +goto menu + +:stop +echo. +echo [停止服务...] +docker-compose stop +echo. +echo 服务已停止! +pause +goto menu + +:restart +echo. +echo [重启服务...] +docker-compose restart +echo. +echo 服务已重启! +pause +goto menu + +:logs +echo. +echo [查看日志 - 按Ctrl+C退出] +docker-compose logs -f --tail=100 +goto menu + +:status +echo. +echo [服务状态] +docker-compose ps +echo. +echo [容器资源使用] +docker stats --no-stream futures-buffer-platform +pause +goto menu + +:shell +echo. +echo [进入容器...] +docker exec -it futures-buffer-platform /bin/bash +goto menu + +:clean +echo. +echo [警告] 这将停止并删除容器,但保留数据卷 +set /p confirm=确认继续? (y/n): +if /i not "%confirm%"=="y" goto menu +echo. +echo [清理资源...] +docker-compose down +echo. +echo 资源已清理! +pause +goto menu + +:menu +cls +goto start + +:end +echo. +echo 再见! +exit diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..b879f5e --- /dev/null +++ b/start.bat @@ -0,0 +1,25 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo 启动期货智析缓冲平台 +echo ======================================== +echo. + +docker-compose start +echo. + +echo 等待服务启动... +timeout /t 3 /nobreak >nul + +echo. +echo ======================================== +echo 访问地址: +echo 品种分析页面: http://localhost:9600/futures-analysis +echo 配置管理页面: http://localhost:9600/ui +echo AI配置页面: http://localhost:9600/ai-config +echo API文档: http://localhost:9600/docs +echo ======================================== +echo. + +docker-compose ps +pause diff --git a/stop.bat b/stop.bat new file mode 100644 index 0000000..8342b08 --- /dev/null +++ b/stop.bat @@ -0,0 +1,13 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo 停止期货智析缓冲平台 +echo ======================================== +echo. + +docker-compose stop +echo. +echo ✓ 服务已停止 +echo. +echo 使用 start.bat 重新启动服务 +pause