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 @@ + + +
+ + +智能期货期权分析系统
+请选择您要进入的模块
+ +