Răsfoiți Sursa

-dev:增加了docker与前端编译

LuoChinWen 4 săptămâni în urmă
părinte
comite
7e2974cc6d

+ 47 - 0
backend/.dockerignore

@@ -0,0 +1,47 @@
+# Git
+.git
+.gitignore
+
+# Python
+__pycache__
+*.py[cod]
+*$py.class
+*.so
+.Python
+.pytest_cache
+.hypothesis
+*.egg-info
+dist
+build
+
+# 虚拟环境
+venv
+.venv
+env
+.env
+
+# IDE
+.idea
+.vscode
+*.swp
+*.swo
+
+# 测试
+test/
+*.test.py
+test_*.py
+pytest.ini
+.coverage
+htmlcov
+
+# 文档
+*.md
+!README.md
+
+# 本地数据库(容器内会重新创建)
+*.db
+
+# 临时文件
+*.log
+*.tmp
+.DS_Store

+ 142 - 0
backend/DOCKER_DEPLOYMENT.md

@@ -0,0 +1,142 @@
+# 后端 Docker 部署指南
+
+## 快速开始
+
+### 1. 准备配置文件
+
+```bash
+cd backend
+copy config.docker.yaml config.yaml
+```
+
+编辑 `config.yaml`,修改以下关键配置:
+
+```yaml
+jwt:
+  secret_key: "你的安全密钥"  # 必须修改!
+
+oauth:
+  enabled: true/false  # 根据需要启用
+  # ... 其他 OAuth 配置
+```
+
+### 2. 启动服务
+
+**Windows:**
+```cmd
+cd backend\scripts
+docker-build.bat
+docker-start.bat
+```
+
+**Linux/Mac:**
+```bash
+cd backend
+chmod +x scripts/*.sh
+./scripts/docker-build.sh
+./scripts/docker-start.sh
+```
+
+**或直接使用 docker-compose:**
+```bash
+cd backend
+docker-compose up -d
+```
+
+### 3. 验证服务
+
+```bash
+# 健康检查
+curl http://localhost:8000/health
+
+# API 文档
+浏览器访问: http://localhost:8000/docs
+```
+
+## 常用命令
+
+| 操作 | 命令 |
+|------|------|
+| 构建镜像 | `docker build -t lq-label-backend:latest .` |
+| 启动服务 | `docker-compose up -d` |
+| 停止服务 | `docker-compose down` |
+| 查看日志 | `docker-compose logs -f` |
+| 重启服务 | `docker-compose restart` |
+| 进入容器 | `docker exec -it lq-label-backend bash` |
+
+## 数据持久化
+
+数据库文件存储在 `backend/data/` 目录,通过 Docker volume 挂载:
+
+```
+backend/
+├── data/
+│   └── annotation_platform.db  # SQLite 数据库
+└── config.yaml                  # 配置文件
+```
+
+## 生产环境配置
+
+### 1. 生成安全密钥
+
+```python
+python -c "import secrets; print(secrets.token_urlsafe(32))"
+```
+
+### 2. 修改 config.yaml
+
+```yaml
+jwt:
+  secret_key: "生成的安全密钥"
+
+server:
+  reload: false  # 关闭热重载
+```
+
+### 3. 配置反向代理(可选)
+
+使用 Nginx 作为反向代理:
+
+```nginx
+server {
+    listen 80;
+    server_name your-domain.com;
+
+    location / {
+        proxy_pass http://localhost:8000;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+    }
+}
+```
+
+## 故障排查
+
+### 容器无法启动
+
+```bash
+# 查看详细日志
+docker-compose logs
+
+# 检查配置文件
+docker exec lq-label-backend cat /app/config.yaml
+```
+
+### 数据库连接问题
+
+```bash
+# 检查数据目录权限
+ls -la backend/data/
+
+# 进入容器检查
+docker exec -it lq-label-backend ls -la /app/data/
+```
+
+### 端口冲突
+
+修改 `docker-compose.yml` 中的端口映射:
+
+```yaml
+ports:
+  - "8001:8000"  # 改为其他端口
+```

+ 39 - 0
backend/Dockerfile

@@ -0,0 +1,39 @@
+# 标注平台后端 Dockerfile
+# 使用 Python 3.11 slim 镜像作为基础
+FROM python:3.11-slim
+
+# 设置工作目录
+WORKDIR /app
+
+# 设置环境变量
+ENV PYTHONDONTWRITEBYTECODE=1 \
+    PYTHONUNBUFFERED=1 \
+    PIP_NO_CACHE_DIR=1 \
+    PIP_DISABLE_PIP_VERSION_CHECK=1
+
+# 安装系统依赖
+RUN apt-get update && apt-get install -y --no-install-recommends \
+    gcc \
+    && rm -rf /var/lib/apt/lists/*
+
+# 复制依赖文件
+COPY requirements.txt .
+
+# 安装 Python 依赖
+RUN pip install --no-cache-dir -r requirements.txt
+
+# 复制应用代码
+COPY . .
+
+# 创建数据目录
+RUN mkdir -p /app/data
+
+# 暴露端口
+EXPOSE 8000
+
+# 健康检查
+HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
+    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
+
+# 启动命令
+CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

+ 35 - 0
backend/config.docker.yaml

@@ -0,0 +1,35 @@
+# Docker 环境配置文件模板
+# 复制此文件为 config.yaml 并修改配置
+
+# JWT 配置
+jwt:
+  # 生产环境请使用强随机密钥
+  # 生成方式: python -c "import secrets; print(secrets.token_urlsafe(32))"
+  secret_key: "CHANGE_THIS_TO_A_SECURE_RANDOM_KEY"
+  algorithm: "HS256"
+  access_token_expire_minutes: 15
+  refresh_token_expire_days: 7
+
+# OAuth 2.0 单点登录配置
+oauth:
+  enabled: false
+  base_url: ""
+  client_id: ""
+  client_secret: ""
+  redirect_uri: ""
+  scope: "profile email"
+  authorize_endpoint: "/oauth/login"
+  token_endpoint: "/oauth/token"
+  userinfo_endpoint: "/oauth/userinfo"
+  revoke_endpoint: "/oauth/revoke"
+
+# 数据库配置
+database:
+  # Docker 环境中使用挂载的数据目录
+  path: "/app/data/annotation_platform.db"
+
+# 服务器配置
+server:
+  host: "0.0.0.0"
+  port: 8000
+  reload: false  # 生产环境关闭热重载

+ 30 - 0
backend/docker-compose.yml

@@ -0,0 +1,30 @@
+# 标注平台后端 Docker Compose 配置
+version: '3.8'
+
+services:
+  backend:
+    build:
+      context: .
+      dockerfile: Dockerfile
+    container_name: lq-label-backend
+    ports:
+      - "8000:8000"
+    volumes:
+      # 持久化数据库文件
+      - ./data:/app/data
+      # 挂载配置文件(可选,方便修改配置)
+      - ./config.yaml:/app/config.yaml:ro
+    environment:
+      - DATABASE_PATH=/app/data/annotation_platform.db
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
+      interval: 30s
+      timeout: 10s
+      retries: 3
+      start_period: 10s
+
+# 数据卷定义
+volumes:
+  backend-data:
+    driver: local

+ 21 - 0
backend/scripts/docker-build.bat

@@ -0,0 +1,21 @@
+@echo off
+REM Docker 镜像构建脚本 (Windows)
+
+echo 开始构建 Docker 镜像...
+
+cd /d "%~dp0.."
+
+docker build -t lq-label-backend:latest .
+
+if %ERRORLEVEL% EQU 0 (
+    echo.
+    echo 镜像构建完成!
+    echo 运行以下命令启动服务:
+    echo   cd backend
+    echo   docker compose up -d
+) else (
+    echo.
+    echo 构建失败,请检查错误信息
+)
+
+pause

+ 24 - 0
backend/scripts/docker-build.sh

@@ -0,0 +1,24 @@
+#!/bin/bash
+# Docker 镜像构建脚本
+
+set -e
+
+# 颜色定义
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+echo -e "${GREEN}开始构建 Docker 镜像...${NC}"
+
+# 获取脚本所在目录的父目录(backend 目录)
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+BACKEND_DIR="$(dirname "$SCRIPT_DIR")"
+
+cd "$BACKEND_DIR"
+
+# 构建镜像
+docker build -t lq-label-backend:latest .
+
+echo -e "${GREEN}镜像构建完成!${NC}"
+echo -e "${YELLOW}运行以下命令启动服务:${NC}"
+echo "  cd backend && docker-compose up -d"

+ 41 - 0
backend/scripts/docker-start.bat

@@ -0,0 +1,41 @@
+@echo off
+REM Docker 服务启动脚本 (Windows)
+
+cd /d "%~dp0.."
+
+REM 检查 config.yaml 是否存在
+if not exist "config.yaml" (
+    echo 错误: config.yaml 不存在
+    echo 请复制 config.docker.yaml 为 config.yaml 并修改配置
+    echo   copy config.docker.yaml config.yaml
+    pause
+    exit /b 1
+)
+
+REM 创建数据目录
+if not exist "data" mkdir data
+
+echo 启动 Docker 服务...
+
+REM 尝试使用 docker compose (新版) 或 docker-compose (旧版)
+docker compose up -d
+if %ERRORLEVEL% NEQ 0 (
+    docker-compose up -d
+)
+
+if %ERRORLEVEL% EQU 0 (
+    echo.
+    echo 服务启动完成!
+    echo API 地址: http://localhost:8000
+    echo 健康检查: http://localhost:8000/health
+    echo.
+    echo 常用命令:
+    echo   查看日志: docker-compose logs -f
+    echo   停止服务: docker-compose down
+    echo   重启服务: docker-compose restart
+) else (
+    echo.
+    echo 启动失败,请检查错误信息
+)
+
+pause

+ 41 - 0
backend/scripts/docker-start.sh

@@ -0,0 +1,41 @@
+#!/bin/bash
+# Docker 服务启动脚本
+
+set -e
+
+# 颜色定义
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+NC='\033[0m'
+
+# 获取脚本所在目录的父目录(backend 目录)
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+BACKEND_DIR="$(dirname "$SCRIPT_DIR")"
+
+cd "$BACKEND_DIR"
+
+# 检查 config.yaml 是否存在
+if [ ! -f "config.yaml" ]; then
+    echo -e "${RED}错误: config.yaml 不存在${NC}"
+    echo -e "${YELLOW}请复制 config.docker.yaml 为 config.yaml 并修改配置${NC}"
+    echo "  cp config.docker.yaml config.yaml"
+    exit 1
+fi
+
+# 创建数据目录
+mkdir -p data
+
+echo -e "${GREEN}启动 Docker 服务...${NC}"
+
+# 启动服务
+docker-compose up -d
+
+echo -e "${GREEN}服务启动完成!${NC}"
+echo -e "API 地址: http://localhost:8000"
+echo -e "健康检查: http://localhost:8000/health"
+echo ""
+echo -e "${YELLOW}常用命令:${NC}"
+echo "  查看日志: docker-compose logs -f"
+echo "  停止服务: docker-compose down"
+echo "  重启服务: docker-compose restart"

+ 21 - 0
backend/scripts/docker-stop.bat

@@ -0,0 +1,21 @@
+@echo off
+REM Docker 服务停止脚本 (Windows)
+
+cd /d "%~dp0.."
+
+echo 停止 Docker 服务...
+
+docker compose down
+if %ERRORLEVEL% NEQ 0 (
+    docker-compose down
+)
+
+if %ERRORLEVEL% EQU 0 (
+    echo.
+    echo 服务已停止
+) else (
+    echo.
+    echo 停止失败,请检查错误信息
+)
+
+pause

+ 20 - 0
backend/scripts/docker-stop.sh

@@ -0,0 +1,20 @@
+#!/bin/bash
+# Docker 服务停止脚本
+
+set -e
+
+# 颜色定义
+GREEN='\033[0;32m'
+NC='\033[0m'
+
+# 获取脚本所在目录的父目录(backend 目录)
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+BACKEND_DIR="$(dirname "$SCRIPT_DIR")"
+
+cd "$BACKEND_DIR"
+
+echo -e "${GREEN}停止 Docker 服务...${NC}"
+
+docker-compose down
+
+echo -e "${GREEN}服务已停止${NC}"

+ 43 - 0
web/apps/lq_label/nginx.conf

@@ -0,0 +1,43 @@
+# lq_label 前端 Nginx 配置
+# 将此文件复制到 Nginx 配置目录,或 include 到主配置中
+
+server {
+    listen 80;
+    server_name localhost;  # 修改为你的域名
+
+    # 前端静态文件目录
+    # 根据实际部署路径修改
+    root /path/to/lq_label/dist/apps/lq_label;
+    index index.html;
+
+    # Gzip 压缩
+    gzip on;
+    gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
+    gzip_min_length 1000;
+
+    # 静态资源缓存
+    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+        expires 1y;
+        add_header Cache-Control "public, immutable";
+    }
+
+    # API 代理到后端
+    location /api/ {
+        proxy_pass http://localhost:8000/;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+
+    # SPA 路由支持 - 所有路由返回 index.html
+    location / {
+        try_files $uri $uri/ /index.html;
+    }
+
+    # 错误页面
+    error_page 500 502 503 504 /50x.html;
+    location = /50x.html {
+        root /usr/share/nginx/html;
+    }
+}

+ 53 - 9
web/apps/lq_label/src/services/api.ts

@@ -120,6 +120,34 @@ apiClient.interceptors.response.use(
       _retry?: boolean;
     };
 
+    // Handle network errors (CORS, connection refused, etc.)
+    if (!error.response) {
+      console.error('Network Error:', error.message);
+      
+      // Check if it's a CORS error or connection error
+      if (error.message === 'Network Error' || error.code === 'ERR_NETWORK') {
+        // Check if user has auth tokens - if yes, might be a CORS issue with expired token
+        const tokens = getStoredTokens();
+        if (tokens) {
+          // Clear auth data and redirect to login
+          clearStoredAuth();
+          toast.error('网络连接失败或登录已过期,请重新登录', '连接错误', 2000);
+          
+          setTimeout(() => {
+            window.location.href = '/login';
+          }, 500);
+        } else {
+          toast.error('无法连接到服务器,请检查网络连接', '网络错误');
+        }
+      }
+      
+      return Promise.reject({
+        status: 0,
+        message: error.message || '网络错误',
+        originalError: error,
+      });
+    }
+
     // Handle 401 Unauthorized errors (token expired or invalid)
     if (error.response?.status === 401) {
       const errorData = error.response.data as any;
@@ -189,11 +217,13 @@ apiClient.interceptors.response.use(
           processQueue(refreshError);
           clearStoredAuth();
 
-          // Show error message
-          toast.error('登录已过期,请重新登录');
+          // Show error message and wait a bit before redirecting
+          toast.error('登录已过期,请重新登录', '认证失败', 2000);
 
-          // Redirect to login page
-          window.location.href = '/login';
+          // Redirect to login page after a short delay to allow toast to show
+          setTimeout(() => {
+            window.location.href = '/login';
+          }, 500);
 
           return Promise.reject(refreshError);
         } finally {
@@ -201,12 +231,17 @@ apiClient.interceptors.response.use(
         }
       } else {
         // 401 error but not token expiration (invalid credentials, etc.)
+        // OR token refresh already attempted but still failed
         // Clear auth data and redirect to login
         clearStoredAuth();
-        toast.error('认证失败,请重新登录');
         
-        // Redirect to login page
-        window.location.href = '/login';
+        // Show error message and wait a bit before redirecting
+        toast.error('认证失败,请重新登录', '认证失败', 2000);
+        
+        // Redirect to login page after a short delay to allow toast to show
+        setTimeout(() => {
+          window.location.href = '/login';
+        }, 500);
         
         return Promise.reject(error);
       }
@@ -253,9 +288,18 @@ apiClient.interceptors.response.use(
       originalData: error.response?.data,
     });
 
-    // Show error toast (skip for 401 errors as they're handled above)
+    // Show error toast (skip for 401 errors as they're handled above with their own messages)
     if (error.response?.status !== 401) {
-      toast.error(errorMessage);
+      // Determine toast type based on status code
+      if (error.response?.status === 403) {
+        toast.warning(errorMessage, '权限不足');
+      } else if (error.response?.status === 404) {
+        toast.warning(errorMessage, '资源不存在');
+      } else if (error.response?.status && error.response.status >= 500) {
+        toast.error(errorMessage, '服务器错误');
+      } else {
+        toast.error(errorMessage, '请求失败');
+      }
     }
 
     // Return a rejected promise with formatted error