diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9f1bf84 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,33 @@ +.venv/ +__pycache__/ +*/__pycache__ +*.pyc + +# 运行时数据(通过 volume 挂载) +data/ +logs/ +*.db +*.log + +# 前端依赖和构建产物(Docker 内重新构建) +web/frontend/node_modules/ +web/frontend/dist/ + +# geetest 临时图片 +bg.jpg +fullbg.jpg +slice.jpg + +# Git +.git/ +.gitignore + +# IDE +.vscode/ +.idea/ + +# 文档 +*.md + +# 测试日志 +11.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7c95caa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +# ===== 阶段1: 构建前端 ===== +FROM node:20-slim AS frontend-builder + +WORKDIR /build +COPY web/frontend/package.json web/frontend/package-lock.json* ./ +RUN npm ci || npm install +COPY web/frontend/ ./ +RUN npm run build + +# ===== 阶段2: Python 运行时 ===== +FROM python:3.12-slim AS runtime + +# 安装 OpenCV 系统依赖 +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgl1 \ + libglib2.0-0 \ + && rm -rf /var/lib/apt/lists/* + +# 安装 uv 用于快速安装依赖 +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +WORKDIR /app + +# 先复制依赖文件,利用 Docker 缓存层 +COPY pyproject.toml uv.lock* ./ +RUN uv venv /app/.venv && uv pip install -e . + +# 复制项目代码 +COPY core/ ./core/ +COPY utils/ ./utils/ +COPY web/backend/ ./web/backend/ +COPY README.md ./ + +# 复制前端构建产物 +COPY --from=frontend-builder /build/dist ./web/frontend/dist + +# 创建数据和日志目录 +RUN mkdir -p /app/data /app/logs + +# 环境变量 +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PATH="/app/.venv/bin:$PATH" + +EXPOSE 8000 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')" || exit 1 + +# 启动命令 +CMD ["python", "-m", "uvicorn", "web.backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] diff --git a/README.md b/README.md index defeee8..d2343d6 100644 --- a/README.md +++ b/README.md @@ -83,3 +83,63 @@ cd web/frontend && npm install && npm run dev ## 许可证 MIT License + +## 部署 + +### Docker 部署(推荐,生产环境) + +```bash +# 一键部署(自动构建镜像 + 启动服务) +./deploy.sh + +# 或手动操作: +docker compose up -d --build +``` + +部署完成后访问 `http://服务器IP:8000`,默认账号 `admin / admin123`。 + +**Docker 常用命令:** + +```bash +docker compose logs -f # 查看实时日志 +docker compose restart # 重启服务 +docker compose down # 停止服务 +docker compose up -d --build # 重新构建并启动(代码更新后) +``` + +**数据持久化:** 数据库和日志通过 volume 挂载到宿主机 `./data` 和 `./logs` 目录,容器重建后数据不丢失。 + +### 本地开发 + +```bash +# 一键启动(自动检查依赖,前后端分离开发模式) +./start_web.sh + +# 或手动启动: +# 后端 +uv pip install -e . +.venv/bin/python -m uvicorn web.backend.main:app --reload --port 8000 + +# 前端 +cd web/frontend && npm install && npm run dev +``` + +访问 `http://localhost:5173`(开发模式),默认账号 `admin / admin123`。 + +### 代理配置说明 + +在 Web 界面"代理配置"页面设置: + +1. **API 代理**:填入代理 API 地址(支持返回 JSON 格式),系统自动获取并验证代理 +2. **白名单**:如代理需要白名单,填入 UID 和 UKEY,系统自动同步出口 IP +3. **出口 IP 漂移**:移动网络出口 IP 频繁漂移时,系统会保留多个出口 IP 在白名单中 + +**代理 API JSON 格式:** + +```json +// 正常返回 +{"code":0,"success":"true","msg":"","data":[{"IP":"1.2.3.4","Port":8080}]} + +// 白名单错误 +{"code":-1,"msg":"51.请先添加白名单:1.2.3.4","data":""} +``` diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..19e94eb --- /dev/null +++ b/deploy.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -e + +# ===== 斗鱼批量登录 Docker 部署脚本 ===== + +ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$ROOT_DIR" + +echo "" +echo "==============================" +echo " 斗鱼批量登录 - Docker 部署" +echo "==============================" + +# 检查 Docker +if ! command -v docker &> /dev/null; then + echo "❌ 未安装 Docker,请先安装:" + echo " curl -fsSL https://get.docker.com | sh" + exit 1 +fi + +# 检查 docker compose +if docker compose version &> /dev/null; then + COMPOSE="docker compose" +elif command -v docker-compose &> /dev/null; then + COMPOSE="docker-compose" +else + echo "❌ 未安装 docker compose,请先安装 Docker Compose" + exit 1 +fi + +echo " 使用: $COMPOSE" +echo "" + +# 创建数据和日志目录 +mkdir -p data logs + +# 构建并启动 +echo "正在构建镜像(首次构建约3-5分钟)..." +$COMPOSE build + +echo "" +echo "正在启动服务..." +$COMPOSE up -d + +echo "" +echo "等待服务启动..." +sleep 3 + +# 健康检查 +for i in $(seq 1 10); do + if curl -s http://localhost:8000/api/health | grep -q "ok" 2>/dev/null; then + echo "" + echo "==============================" + echo " ✅ 部署成功!" + echo "==============================" + echo " 访问地址: http://localhost:8000" + echo " 默认账号: admin / admin123" + echo "" + echo " 查看日志: $COMPOSE logs -f" + echo " 停止服务: $COMPOSE down" + echo " 重启服务: $COMPOSE restart" + echo "==============================" + exit 0 + fi + echo " 等待中... ($i/10)" + sleep 2 +done + +echo "" +echo "❌ 服务启动超时,查看日志:" +$COMPOSE logs --tail 30 +exit 1 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b13aeb8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + douyu-login: + build: . + container_name: douyu-login + restart: unless-stopped + ports: + - "8000:8000" + volumes: + # 持久化数据库和日志 + - ./data:/app/data + - ./logs:/app/logs + environment: + - TZ=Asia/Shanghai + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"] + interval: 30s + timeout: 5s + retries: 3 diff --git a/web/backend/main.py b/web/backend/main.py index 7575cf3..4122930 100644 --- a/web/backend/main.py +++ b/web/backend/main.py @@ -1,9 +1,13 @@ """FastAPI 入口""" +import os +from pathlib import Path import uvicorn from contextlib import asynccontextmanager -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 .database import init_db from .routers import auth, users, accounts, login, proxy, cookies, logs @@ -45,6 +49,31 @@ def health(): return {"status": "ok"} +# ---- 生产环境:serve 前端静态文件 ---- +# Docker 部署时前端构建产物会被复制到 web/frontend/dist +_FRONTEND_DIST = Path(__file__).resolve().parents[2] / "web" / "frontend" / "dist" +_INDEX_HTML = _FRONTEND_DIST / "index.html" + +if _INDEX_HTML.exists(): + # 挂载静态资源目录(js/css/图片等) + _ASSETS_DIR = _FRONTEND_DIST / "assets" + if _ASSETS_DIR.exists(): + app.mount("/assets", StaticFiles(directory=str(_ASSETS_DIR)), name="assets") + + @app.get("/{full_path:path}") + async def serve_spa(full_path: str, request: Request): + """SPA fallback:非 /api 路径返回 index.html""" + # 排除 API 路径 + if full_path.startswith("api"): + return {"detail": "Not Found"} + # 尝试返回静态文件 + file_path = _FRONTEND_DIST / full_path + if file_path.is_file(): + return FileResponse(str(file_path)) + # SPA fallback 到 index.html + return FileResponse(str(_INDEX_HTML)) + + def run(): uvicorn.run("web.backend.main:app", host="0.0.0.0", port=8000, reload=True)