mirror of
https://github.com/handsomezhuzhu/QQuiz.git
synced 2026-02-20 20:10:14 +00:00
Merge pull request #1 from handsomezhuzhu/codex/fix-security-vulnerabilities-in-qquiz
Document secure secrets and prune unused assets
This commit is contained in:
10
.env.example
10
.env.example
@@ -7,8 +7,14 @@ DATABASE_URL=sqlite+aiosqlite:///./qquiz.db
|
|||||||
# For Local: mysql+aiomysql://qquiz:qquiz_password@localhost:3306/qquiz_db
|
# For Local: mysql+aiomysql://qquiz:qquiz_password@localhost:3306/qquiz_db
|
||||||
# DATABASE_URL=mysql+aiomysql://qquiz:qquiz_password@localhost:3306/qquiz_db
|
# DATABASE_URL=mysql+aiomysql://qquiz:qquiz_password@localhost:3306/qquiz_db
|
||||||
|
|
||||||
# JWT Secret (Please change this in production!)
|
# JWT Secret (must be at least 32 characters; generate randomly for production)
|
||||||
SECRET_KEY=your-super-secret-key-change-in-production-minimum-32-characters
|
SECRET_KEY=
|
||||||
|
|
||||||
|
# Default admin username (must be at least 3 characters; default: admin)
|
||||||
|
ADMIN_USERNAME=admin
|
||||||
|
|
||||||
|
# Default admin password (must be at least 12 characters; generate randomly for production)
|
||||||
|
ADMIN_PASSWORD=
|
||||||
|
|
||||||
# AI Provider Configuration
|
# AI Provider Configuration
|
||||||
AI_PROVIDER=gemini
|
AI_PROVIDER=gemini
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ FROM python:3.11-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装操作系统依赖(python-magic 需要 libmagic)
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends libmagic1 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 复制后端依赖文件
|
# 复制后端依赖文件
|
||||||
COPY backend/requirements.txt ./
|
COPY backend/requirements.txt ./
|
||||||
|
|
||||||
|
|||||||
53
README.md
53
README.md
@@ -55,11 +55,13 @@ docker run -d \
|
|||||||
从源码构建,一个容器包含前后端和 SQLite 数据库:
|
从源码构建,一个容器包含前后端和 SQLite 数据库:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 配置环境变量
|
# 1. 配置环境变量(必须提供强密码和密钥)
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# 编辑 .env,填入你的 API Key
|
# 编辑 .env,填入至少 32 位的 SECRET_KEY 和至少 12 位的 ADMIN_PASSWORD(建议使用随机生成值)
|
||||||
|
|
||||||
# 2. 启动服务
|
# 2. 启动服务(未设置强密码/密钥会直接报错终止)
|
||||||
|
SECRET_KEY=$(openssl rand -base64 48) \
|
||||||
|
ADMIN_PASSWORD=$(openssl rand -base64 16) \
|
||||||
docker-compose -f docker-compose-single.yml up -d
|
docker-compose -f docker-compose-single.yml up -d
|
||||||
|
|
||||||
# 3. 访问应用: http://localhost:8000
|
# 3. 访问应用: http://localhost:8000
|
||||||
@@ -71,7 +73,9 @@ docker-compose -f docker-compose-single.yml up -d
|
|||||||
前后端分离 + MySQL:
|
前后端分离 + MySQL:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 启动服务
|
# 启动服务(建议直接在命令行生成强密钥和管理员密码)
|
||||||
|
SECRET_KEY=$(openssl rand -base64 48) \
|
||||||
|
ADMIN_PASSWORD=$(openssl rand -base64 16) \
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|
||||||
# 前端: http://localhost:3000
|
# 前端: http://localhost:3000
|
||||||
@@ -85,14 +89,6 @@ docker-compose up -d
|
|||||||
- Node.js 18+
|
- Node.js 18+
|
||||||
- MySQL 8.0+ 或 Docker (用于运行 MySQL)
|
- MySQL 8.0+ 或 Docker (用于运行 MySQL)
|
||||||
|
|
||||||
**Windows 用户:**
|
|
||||||
```bash
|
|
||||||
# 双击运行以下脚本之一:
|
|
||||||
scripts\fix_and_start.bat # 推荐:自动修复并启动(支持 Docker/本地数据库)
|
|
||||||
scripts\start_with_docker_db.bat # 使用 Docker 数据库启动
|
|
||||||
scripts\setup.bat # 仅配置环境变量
|
|
||||||
```
|
|
||||||
|
|
||||||
**Linux/macOS 用户:**
|
**Linux/macOS 用户:**
|
||||||
```bash
|
```bash
|
||||||
# 1. 配置环境变量
|
# 1. 配置环境变量
|
||||||
@@ -110,23 +106,6 @@ chmod +x scripts/run_local.sh
|
|||||||
|
|
||||||
**MySQL 安装指南:** 详见 [docs/MYSQL_SETUP.md](docs/MYSQL_SETUP.md)
|
**MySQL 安装指南:** 详见 [docs/MYSQL_SETUP.md](docs/MYSQL_SETUP.md)
|
||||||
|
|
||||||
### Docker 构建加速(中国用户)
|
|
||||||
|
|
||||||
如果你在中国大陆,Docker 构建速度可能较慢。我们提供了使用国内镜像的可选配置:
|
|
||||||
|
|
||||||
**详细指南:** 参见 [docs/CHINA_MIRROR_GUIDE.md](docs/CHINA_MIRROR_GUIDE.md)
|
|
||||||
|
|
||||||
**快速使用:**
|
|
||||||
```bash
|
|
||||||
# 后端使用中国镜像构建
|
|
||||||
docker build -f backend/Dockerfile.china -t qquiz-backend ./backend
|
|
||||||
|
|
||||||
# 前端使用中国镜像构建
|
|
||||||
docker build -f frontend/Dockerfile.china -t qquiz-frontend ./frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
构建速度可提升 3-5 倍 ⚡
|
|
||||||
|
|
||||||
## GitHub Actions 自动构建设置
|
## GitHub Actions 自动构建设置
|
||||||
|
|
||||||
如果你 fork 了本项目并想启用自动构建 Docker 镜像功能:
|
如果你 fork 了本项目并想启用自动构建 Docker 镜像功能:
|
||||||
@@ -161,9 +140,9 @@ docker pull ghcr.io/handsomezhuzhu/qquiz:latest
|
|||||||
|
|
||||||
**管理员账户:**
|
**管理员账户:**
|
||||||
- 用户名: `admin`
|
- 用户名: `admin`
|
||||||
- 密码: `admin123`
|
- 密码: 取自环境变量 `ADMIN_PASSWORD`(必须至少 12 位,建议随机生成)
|
||||||
|
|
||||||
⚠️ **重要**: 首次登录后请立即修改密码!
|
⚠️ **重要**: 在部署前就必须设置强管理员密码;如果需要轮换密码,请更新环境变量后重启服务。
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
@@ -186,16 +165,9 @@ QQuiz/
|
|||||||
│ ├── package.json # Node 依赖
|
│ ├── package.json # Node 依赖
|
||||||
│ └── vite.config.js # Vite 配置
|
│ └── vite.config.js # Vite 配置
|
||||||
├── scripts/ # 部署和启动脚本
|
├── scripts/ # 部署和启动脚本
|
||||||
│ ├── fix_and_start.bat # Windows 快速启动(推荐)
|
│ └── run_local.sh # Linux/macOS 启动脚本
|
||||||
│ ├── start_with_docker_db.bat # Docker 数据库启动
|
|
||||||
│ ├── setup.bat # 环境配置脚本
|
|
||||||
│ ├── run_local.sh # Linux/macOS 启动脚本
|
|
||||||
│ └── [其他辅助脚本...]
|
|
||||||
├── docs/ # 文档目录
|
├── docs/ # 文档目录
|
||||||
│ ├── AI_CONFIGURATION.md # AI 提供商配置指南(重要)
|
|
||||||
│ ├── MYSQL_SETUP.md # MySQL 安装配置指南
|
│ ├── MYSQL_SETUP.md # MySQL 安装配置指南
|
||||||
│ ├── CHINA_MIRROR_GUIDE.md # 中国镜像加速指南
|
|
||||||
│ ├── WINDOWS_DEPLOYMENT.md # Windows 部署详细指南
|
|
||||||
│ └── PROJECT_STRUCTURE.md # 项目架构详解
|
│ └── PROJECT_STRUCTURE.md # 项目架构详解
|
||||||
├── test_data/ # 测试数据
|
├── test_data/ # 测试数据
|
||||||
│ └── sample_questions.txt # 示例题目
|
│ └── sample_questions.txt # 示例题目
|
||||||
@@ -233,7 +205,8 @@ QQuiz/
|
|||||||
| 变量 | 说明 | 默认值 |
|
| 变量 | 说明 | 默认值 |
|
||||||
|------|------|--------|
|
|------|------|--------|
|
||||||
| `DATABASE_URL` | 数据库连接字符串 | - |
|
| `DATABASE_URL` | 数据库连接字符串 | - |
|
||||||
| `SECRET_KEY` | JWT 密钥 | - |
|
| `SECRET_KEY` | JWT 密钥(必须至少 32 位随机字符串) | - |
|
||||||
|
| `ADMIN_PASSWORD` | 默认管理员密码(必须至少 12 位,建议随机生成) | - |
|
||||||
| `AI_PROVIDER` | AI 提供商 (gemini/openai/anthropic/qwen) | gemini |
|
| `AI_PROVIDER` | AI 提供商 (gemini/openai/anthropic/qwen) | gemini |
|
||||||
| `GEMINI_API_KEY` | Google Gemini API 密钥 | - |
|
| `GEMINI_API_KEY` | Google Gemini API 密钥 | - |
|
||||||
| `GEMINI_BASE_URL` | Gemini API 地址(可选,支持代理) | https://generativelanguage.googleapis.com |
|
| `GEMINI_BASE_URL` | Gemini API 地址(可选,支持代理) | https://generativelanguage.googleapis.com |
|
||||||
|
|||||||
@@ -86,6 +86,15 @@ async def init_default_config(db: AsyncSession):
|
|||||||
"ai_provider": os.getenv("AI_PROVIDER", "openai"),
|
"ai_provider": os.getenv("AI_PROVIDER", "openai"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Validate admin credentials
|
||||||
|
admin_username = os.getenv("ADMIN_USERNAME", "admin")
|
||||||
|
if not admin_username or len(admin_username) < 3:
|
||||||
|
raise ValueError("ADMIN_USERNAME must be at least 3 characters long")
|
||||||
|
|
||||||
|
admin_password = os.getenv("ADMIN_PASSWORD")
|
||||||
|
if not admin_password or len(admin_password) < 12:
|
||||||
|
raise ValueError("ADMIN_PASSWORD must be set and at least 12 characters long")
|
||||||
|
|
||||||
for key, value in default_configs.items():
|
for key, value in default_configs.items():
|
||||||
result = await db.execute(select(SystemConfig).where(SystemConfig.key == key))
|
result = await db.execute(select(SystemConfig).where(SystemConfig.key == key))
|
||||||
existing = result.scalar_one_or_none()
|
existing = result.scalar_one_or_none()
|
||||||
@@ -95,19 +104,40 @@ async def init_default_config(db: AsyncSession):
|
|||||||
db.add(config)
|
db.add(config)
|
||||||
print(f"✅ Created default config: {key} = {value}")
|
print(f"✅ Created default config: {key} = {value}")
|
||||||
|
|
||||||
# Create default admin user if not exists
|
# Create or update default admin user
|
||||||
result = await db.execute(select(User).where(User.username == "admin"))
|
result = await db.execute(select(User).where(User.username == admin_username))
|
||||||
admin = result.scalar_one_or_none()
|
admin = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
default_admin_id = admin.id if admin else None
|
||||||
|
|
||||||
if not admin:
|
if not admin:
|
||||||
admin_user = User(
|
admin_user = User(
|
||||||
username="admin",
|
username=admin_username,
|
||||||
hashed_password=pwd_context.hash("admin123"), # Change this password!
|
hashed_password=pwd_context.hash(admin_password),
|
||||||
is_admin=True
|
is_admin=True
|
||||||
)
|
)
|
||||||
db.add(admin_user)
|
db.add(admin_user)
|
||||||
print("✅ Created default admin user (username: admin, password: admin123)")
|
await db.commit()
|
||||||
print("⚠️ IMPORTANT: Please change the admin password immediately!")
|
await db.refresh(admin_user)
|
||||||
|
default_admin_id = admin_user.id
|
||||||
|
print(f"✅ Created default admin user (username: {admin_username})")
|
||||||
|
else:
|
||||||
|
# Update password if it has changed (verify current password doesn't match)
|
||||||
|
if not pwd_context.verify(admin_password, admin.hashed_password):
|
||||||
|
admin.hashed_password = pwd_context.hash(admin_password)
|
||||||
|
print(f"🔄 Updated default admin password (username: {admin_username})")
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
if default_admin_id is not None:
|
||||||
|
result = await db.execute(
|
||||||
|
select(SystemConfig).where(SystemConfig.key == "default_admin_id")
|
||||||
|
)
|
||||||
|
default_admin_config = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not default_admin_config:
|
||||||
|
db.add(SystemConfig(key="default_admin_id", value=str(default_admin_id)))
|
||||||
|
elif default_admin_config.value != str(default_admin_id):
|
||||||
|
default_admin_config.value = str(default_admin_id)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,28 @@ QQuiz FastAPI Application - 单容器模式(前后端整合)
|
|||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import HTMLResponse, FileResponse
|
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from slowapi.errors import RateLimitExceeded
|
||||||
|
from slowapi.middleware import SlowAPIMiddleware
|
||||||
|
|
||||||
from database import init_db, init_default_config, get_db_context
|
from database import init_db, init_default_config, get_db_context
|
||||||
|
from rate_limit import limiter
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
async def rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=429,
|
||||||
|
content={"detail": "Rate limit exceeded. Please try again later."}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
"""Application lifespan events"""
|
"""Application lifespan events"""
|
||||||
@@ -50,6 +60,10 @@ app = FastAPI(
|
|||||||
lifespan=lifespan
|
lifespan=lifespan
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.state.limiter = limiter
|
||||||
|
app.add_exception_handler(RateLimitExceeded, rate_limit_exceeded_handler)
|
||||||
|
app.add_middleware(SlowAPIMiddleware)
|
||||||
|
|
||||||
# Configure CORS
|
# Configure CORS
|
||||||
cors_origins = os.getenv("CORS_ORIGINS", "http://localhost:3000").split(",")
|
cors_origins = os.getenv("CORS_ORIGINS", "http://localhost:3000").split(",")
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
|
|||||||
10
backend/rate_limit.py
Normal file
10
backend/rate_limit.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""Rate limiting utilities for the FastAPI application."""
|
||||||
|
|
||||||
|
from slowapi import Limiter
|
||||||
|
from slowapi.util import get_remote_address
|
||||||
|
|
||||||
|
|
||||||
|
limiter = Limiter(key_func=get_remote_address)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["limiter"]
|
||||||
@@ -12,6 +12,7 @@ python-multipart==0.0.6
|
|||||||
passlib==1.7.4
|
passlib==1.7.4
|
||||||
bcrypt==4.0.1
|
bcrypt==4.0.1
|
||||||
python-jose[cryptography]==3.3.0
|
python-jose[cryptography]==3.3.0
|
||||||
|
python-magic==0.4.27
|
||||||
aiofiles==23.2.1
|
aiofiles==23.2.1
|
||||||
httpx==0.26.0
|
httpx==0.26.0
|
||||||
openai==1.10.0
|
openai==1.10.0
|
||||||
@@ -19,3 +20,4 @@ anthropic==0.8.1
|
|||||||
python-docx==1.1.0
|
python-docx==1.1.0
|
||||||
PyPDF2==3.0.1
|
PyPDF2==3.0.1
|
||||||
openpyxl==3.1.2
|
openpyxl==3.1.2
|
||||||
|
slowapi==0.1.9
|
||||||
|
|||||||
@@ -24,6 +24,18 @@ router = APIRouter()
|
|||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_default_admin_id(db: AsyncSession) -> Optional[int]:
|
||||||
|
result = await db.execute(
|
||||||
|
select(SystemConfig).where(SystemConfig.key == "default_admin_id")
|
||||||
|
)
|
||||||
|
config = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if config and config.value.isdigit():
|
||||||
|
return int(config.value)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config", response_model=SystemConfigResponse)
|
@router.get("/config", response_model=SystemConfigResponse)
|
||||||
async def get_system_config(
|
async def get_system_config(
|
||||||
current_admin: User = Depends(get_current_admin_user),
|
current_admin: User = Depends(get_current_admin_user),
|
||||||
@@ -203,8 +215,10 @@ async def update_user(
|
|||||||
detail="User not found"
|
detail="User not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
protected_admin_id = await get_default_admin_id(db)
|
||||||
|
|
||||||
# 不允许修改默认管理员的管理员状态
|
# 不允许修改默认管理员的管理员状态
|
||||||
if user.username == "admin" and user_data.is_admin is not None:
|
if protected_admin_id and user.id == protected_admin_id and user_data.is_admin is not None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Cannot modify default admin user's admin status"
|
detail="Cannot modify default admin user's admin status"
|
||||||
@@ -240,8 +254,10 @@ async def delete_user(
|
|||||||
detail="User not found"
|
detail="User not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
protected_admin_id = await get_default_admin_id(db)
|
||||||
|
|
||||||
# 不允许删除默认管理员
|
# 不允许删除默认管理员
|
||||||
if user.username == "admin":
|
if protected_admin_id and user.id == protected_admin_id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Cannot delete default admin user"
|
detail="Cannot delete default admin user"
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
"""
|
"""
|
||||||
Authentication Router
|
Authentication Router
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
from database import get_db
|
from database import get_db
|
||||||
from models import User, SystemConfig
|
from models import User, SystemConfig
|
||||||
from schemas import UserCreate, UserLogin, Token, UserResponse
|
from schemas import UserCreate, UserLogin, Token, UserResponse
|
||||||
from utils import hash_password, verify_password, create_access_token
|
from utils import hash_password, verify_password, create_access_token
|
||||||
|
from rate_limit import limiter
|
||||||
from services.auth_service import get_current_user
|
from services.auth_service import get_current_user
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||||
@@ -61,7 +64,9 @@ async def register(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/login", response_model=Token)
|
@router.post("/login", response_model=Token)
|
||||||
|
@limiter.limit("5/minute")
|
||||||
async def login(
|
async def login(
|
||||||
|
request: Request,
|
||||||
user_data: UserLogin,
|
user_data: UserLogin,
|
||||||
db: AsyncSession = Depends(get_db)
|
db: AsyncSession = Depends(get_db)
|
||||||
):
|
):
|
||||||
@@ -86,8 +91,7 @@ async def login(
|
|||||||
data={"sub": str(user.id)} # JWT 'sub' must be a string
|
data={"sub": str(user.id)} # JWT 'sub' must be a string
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"✅ Login successful: user={user.username}, id={user.id}")
|
logger.info("Login successful", extra={"user_id": user.id})
|
||||||
print(f"🔑 Generated token (first 50 chars): {access_token[:50]}...")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Exam Router - Handles exam creation, file upload, and deduplication
|
Exam Router - Handles exam creation, file upload, and deduplication
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, BackgroundTasks
|
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, BackgroundTasks, Request
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select, func, and_
|
from sqlalchemy import select, func, and_
|
||||||
@@ -10,6 +10,7 @@ from datetime import datetime, timedelta
|
|||||||
import os
|
import os
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import json
|
import json
|
||||||
|
import magic
|
||||||
|
|
||||||
from database import get_db
|
from database import get_db
|
||||||
from models import User, Exam, Question, ExamStatus, SystemConfig
|
from models import User, Exam, Question, ExamStatus, SystemConfig
|
||||||
@@ -24,8 +25,57 @@ from services.config_service import load_llm_config
|
|||||||
from services.progress_service import progress_service
|
from services.progress_service import progress_service
|
||||||
from utils import is_allowed_file, calculate_content_hash
|
from utils import is_allowed_file, calculate_content_hash
|
||||||
from dedup_utils import is_duplicate_question
|
from dedup_utils import is_duplicate_question
|
||||||
|
from rate_limit import limiter
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
ALLOWED_MIME_TYPES = {
|
||||||
|
"text/plain",
|
||||||
|
"application/pdf",
|
||||||
|
"application/msword",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"application/vnd.ms-excel",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_upload_file(file: UploadFile) -> None:
|
||||||
|
"""Validate uploaded file by extension and MIME type."""
|
||||||
|
|
||||||
|
if not file.filename or not is_allowed_file(file.filename):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid file type. Allowed: txt, pdf, doc, docx, xlsx, xls",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sample = await file.read(2048)
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Could not read uploaded file for validation",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not sample:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Uploaded file is empty",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mime_type = magic.from_buffer(sample, mime=True)
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Could not determine file type",
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
file.file.seek(0)
|
||||||
|
|
||||||
|
if mime_type not in ALLOWED_MIME_TYPES:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid file content type",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def check_upload_limits(user_id: int, file_size: int, db: AsyncSession):
|
async def check_upload_limits(user_id: int, file_size: int, db: AsyncSession):
|
||||||
@@ -485,7 +535,9 @@ async def async_parse_and_save(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/create", response_model=ExamUploadResponse, status_code=status.HTTP_201_CREATED)
|
@router.post("/create", response_model=ExamUploadResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
@limiter.limit("10/minute")
|
||||||
async def create_exam_with_upload(
|
async def create_exam_with_upload(
|
||||||
|
request: Request,
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
title: str = Form(...),
|
title: str = Form(...),
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
@@ -498,11 +550,7 @@ async def create_exam_with_upload(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Validate file
|
# Validate file
|
||||||
if not file.filename or not is_allowed_file(file.filename):
|
await validate_upload_file(file)
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Invalid file type. Allowed: txt, pdf, doc, docx, xlsx, xls"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Read file content
|
# Read file content
|
||||||
file_content = await file.read()
|
file_content = await file.read()
|
||||||
@@ -538,7 +586,9 @@ async def create_exam_with_upload(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/{exam_id}/append", response_model=ExamUploadResponse)
|
@router.post("/{exam_id}/append", response_model=ExamUploadResponse)
|
||||||
|
@limiter.limit("10/minute")
|
||||||
async def append_document_to_exam(
|
async def append_document_to_exam(
|
||||||
|
request: Request,
|
||||||
exam_id: int,
|
exam_id: int,
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
@@ -572,11 +622,7 @@ async def append_document_to_exam(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Validate file
|
# Validate file
|
||||||
if not file.filename or not is_allowed_file(file.filename):
|
await validate_upload_file(file)
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Invalid file type. Allowed: txt, pdf, doc, docx, xlsx, xls"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Read file content
|
# Read file content
|
||||||
file_content = await file.read()
|
file_content = await file.read()
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import os
|
|||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
# JWT settings
|
# JWT settings
|
||||||
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-this")
|
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||||
|
if not SECRET_KEY or len(SECRET_KEY) < 32:
|
||||||
|
raise ValueError("SECRET_KEY must be set and at least 32 characters long")
|
||||||
|
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
|
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
|
||||||
|
|
||||||
|
|||||||
@@ -11,38 +11,12 @@ services:
|
|||||||
container_name: qquiz
|
container_name: qquiz
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
# 数据库配置(SQLite 默认)
|
# 数据库配置(SQLite 默认,使用持久化卷)
|
||||||
- DATABASE_URL=sqlite+aiosqlite:////app/data/qquiz.db
|
- DATABASE_URL=sqlite+aiosqlite:////app/data/qquiz.db
|
||||||
|
|
||||||
# JWT 密钥(生产环境请修改)
|
|
||||||
- SECRET_KEY=your-super-secret-key-change-in-production-minimum-32-characters
|
|
||||||
|
|
||||||
# AI 提供商配置
|
|
||||||
- AI_PROVIDER=gemini
|
|
||||||
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
|
||||||
- GEMINI_BASE_URL=${GEMINI_BASE_URL:-https://generativelanguage.googleapis.com}
|
|
||||||
- GEMINI_MODEL=${GEMINI_MODEL:-gemini-2.0-flash-exp}
|
|
||||||
|
|
||||||
# OpenAI 配置(可选)
|
|
||||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
|
||||||
- OPENAI_BASE_URL=${OPENAI_BASE_URL:-https://api.openai.com/v1}
|
|
||||||
- OPENAI_MODEL=${OPENAI_MODEL:-gpt-4o-mini}
|
|
||||||
|
|
||||||
# Anthropic 配置(可选)
|
|
||||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
|
||||||
- ANTHROPIC_MODEL=${ANTHROPIC_MODEL:-claude-3-haiku-20240307}
|
|
||||||
|
|
||||||
# Qwen 配置(可选)
|
|
||||||
- QWEN_API_KEY=${QWEN_API_KEY:-}
|
|
||||||
- QWEN_BASE_URL=${QWEN_BASE_URL:-https://dashscope.aliyuncs.com/compatible-mode/v1}
|
|
||||||
- QWEN_MODEL=${QWEN_MODEL:-qwen-plus}
|
|
||||||
|
|
||||||
# 系统配置
|
|
||||||
- ALLOW_REGISTRATION=true
|
|
||||||
- MAX_UPLOAD_SIZE_MB=10
|
|
||||||
- MAX_DAILY_UPLOADS=20
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
# 持久化数据卷
|
# 持久化数据卷
|
||||||
- qquiz_data:/app/data # 数据库文件
|
- qquiz_data:/app/data # 数据库文件
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ services:
|
|||||||
container_name: qquiz_backend
|
container_name: qquiz_backend
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=mysql+aiomysql://qquiz:qquiz_password@mysql:3306/qquiz_db
|
- DATABASE_URL=mysql+aiomysql://qquiz:qquiz_password@mysql:3306/qquiz_db
|
||||||
|
- SECRET_KEY=${SECRET_KEY:?Set SECRET_KEY to a random string of at least 32 characters}
|
||||||
|
- ADMIN_PASSWORD=${ADMIN_PASSWORD:?Set ADMIN_PASSWORD to a strong password of at least 12 characters}
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -1,201 +0,0 @@
|
|||||||
# AI 提供商配置指南
|
|
||||||
|
|
||||||
QQuiz 支持多个 AI 提供商,推荐使用 Google Gemini 以获得最佳的 PDF 解析体验。
|
|
||||||
|
|
||||||
## 🌟 推荐:Google Gemini
|
|
||||||
|
|
||||||
### 优势
|
|
||||||
|
|
||||||
- ✅ **原生 PDF 理解**:直接处理 PDF(最多1000页),完整保留图片、表格、公式
|
|
||||||
- ✅ **免费额度充足**:每天免费 15 次/分钟,1500 次/天
|
|
||||||
- ✅ **多模态理解**:支持图片、图表、公式的理解
|
|
||||||
- ✅ **自定义代理**:支持配置自定义 Base URL 实现 Key 轮训等功能
|
|
||||||
|
|
||||||
### 获取 API Key
|
|
||||||
|
|
||||||
1. 访问 https://aistudio.google.com/apikey
|
|
||||||
2. 使用 Google 账号登录
|
|
||||||
3. 点击 "Create API Key"
|
|
||||||
4. 复制生成的 API Key(格式:`AIza...`)
|
|
||||||
|
|
||||||
### 配置方式
|
|
||||||
|
|
||||||
#### 方式一:环境变量配置(推荐首次部署)
|
|
||||||
|
|
||||||
编辑 `.env` 文件:
|
|
||||||
|
|
||||||
```env
|
|
||||||
AI_PROVIDER=gemini
|
|
||||||
GEMINI_API_KEY=AIza-your-actual-gemini-api-key
|
|
||||||
GEMINI_BASE_URL=https://generativelanguage.googleapis.com
|
|
||||||
GEMINI_MODEL=gemini-2.0-flash-exp
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 方式二:管理员后台配置(推荐生产环境)
|
|
||||||
|
|
||||||
1. 使用管理员账号登录(默认:admin / admin123)
|
|
||||||
2. 进入"系统设置"
|
|
||||||
3. 选择 AI 提供商:"Google Gemini (推荐)"
|
|
||||||
4. 填入 API Key
|
|
||||||
5. 点击"保存所有设置"
|
|
||||||
|
|
||||||
**优势**:无需重启服务即可生效,支持在线修改配置
|
|
||||||
|
|
||||||
### 自定义 Gemini Base URL(可选)
|
|
||||||
|
|
||||||
如果需要使用 Key 轮训服务或代理(例如提高速度、负载均衡),可以配置自定义 Base URL:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# 使用自定义代理服务
|
|
||||||
GEMINI_BASE_URL=https://your-proxy-service.com/proxy/gemini-self
|
|
||||||
```
|
|
||||||
|
|
||||||
**使用场景**:
|
|
||||||
- Key 轮训(多个 API Key 负载均衡)
|
|
||||||
- 国内加速代理
|
|
||||||
- 自建中转服务
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 其他 AI 提供商
|
|
||||||
|
|
||||||
### OpenAI (GPT)
|
|
||||||
|
|
||||||
**限制**:⚠️ 仅支持文本解析,PDF 文件会通过文本提取处理,丢失图片和格式信息
|
|
||||||
|
|
||||||
#### 获取 API Key
|
|
||||||
- 访问:https://platform.openai.com/api-keys
|
|
||||||
- 创建新的 Secret Key
|
|
||||||
|
|
||||||
#### 配置
|
|
||||||
```env
|
|
||||||
AI_PROVIDER=openai
|
|
||||||
OPENAI_API_KEY=sk-your-openai-api-key
|
|
||||||
OPENAI_BASE_URL=https://api.openai.com/v1
|
|
||||||
OPENAI_MODEL=gpt-4o-mini
|
|
||||||
```
|
|
||||||
|
|
||||||
**推荐模型**:
|
|
||||||
- `gpt-4o-mini`(推荐,性价比高)
|
|
||||||
- `gpt-4o`(最强,成本高)
|
|
||||||
- `gpt-3.5-turbo`(便宜,效果一般)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Anthropic (Claude)
|
|
||||||
|
|
||||||
**限制**:⚠️ 仅支持文本解析,PDF 文件会通过文本提取处理,丢失图片和格式信息
|
|
||||||
|
|
||||||
#### 获取 API Key
|
|
||||||
- 访问:https://console.anthropic.com/settings/keys
|
|
||||||
- 创建新的 API Key
|
|
||||||
|
|
||||||
#### 配置
|
|
||||||
```env
|
|
||||||
AI_PROVIDER=anthropic
|
|
||||||
ANTHROPIC_API_KEY=sk-ant-your-anthropic-api-key
|
|
||||||
ANTHROPIC_MODEL=claude-3-haiku-20240307
|
|
||||||
```
|
|
||||||
|
|
||||||
**推荐模型**:
|
|
||||||
- `claude-3-haiku-20240307`(推荐,速度快)
|
|
||||||
- `claude-3-5-sonnet-20241022`(最强,成本高)
|
|
||||||
- `claude-3-opus-20240229`(超强,成本很高)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Qwen (通义千问)
|
|
||||||
|
|
||||||
**限制**:⚠️ 仅支持文本解析,PDF 文件会通过文本提取处理,丢失图片和格式信息
|
|
||||||
|
|
||||||
#### 获取 API Key
|
|
||||||
- 访问:https://dashscope.console.aliyun.com/apiKey
|
|
||||||
- 使用阿里云账号登录
|
|
||||||
- 创建新的 API Key
|
|
||||||
|
|
||||||
#### 配置
|
|
||||||
```env
|
|
||||||
AI_PROVIDER=qwen
|
|
||||||
QWEN_API_KEY=sk-your-qwen-api-key
|
|
||||||
QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
|
|
||||||
QWEN_MODEL=qwen-plus
|
|
||||||
```
|
|
||||||
|
|
||||||
**推荐模型**:
|
|
||||||
- `qwen-plus`(推荐,平衡)
|
|
||||||
- `qwen-max`(最强,成本高)
|
|
||||||
- `qwen-turbo`(快速,效果一般)
|
|
||||||
- `qwen-long`(超长文本支持)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## AI 提供商对比表
|
|
||||||
|
|
||||||
| 提供商 | PDF 原生支持 | 图片/表格/公式 | 文本解析 | 免费额度 | 推荐度 |
|
|
||||||
|--------|--------------|----------------|----------|----------|--------|
|
|
||||||
| **Google Gemini** | ✅ 支持 | ✅ 完整保留 | ✅ | ⭐⭐⭐⭐⭐ | 最推荐 |
|
|
||||||
| OpenAI (GPT) | ❌ 不支持 | ❌ 丢失 | ✅ | 💰 | ⭐⭐⭐⭐ |
|
|
||||||
| Anthropic (Claude) | ❌ 不支持 | ❌ 丢失 | ✅ | 💰 | ⭐⭐⭐⭐ |
|
|
||||||
| Qwen (通义千问) | ❌ 不支持 | ❌ 丢失 | ✅ | ✅ | ⭐⭐⭐ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### Q1: 如何判断 AI 提供商是否配置正确?
|
|
||||||
|
|
||||||
上传一个包含题目的文档,如果能成功解析出题目,说明配置正确。查看后端日志可以看到详细的解析过程。
|
|
||||||
|
|
||||||
### Q2: 可以同时配置多个 AI 提供商吗?
|
|
||||||
|
|
||||||
可以。配置多个提供商的 API Key,通过 `AI_PROVIDER` 环境变量或管理员后台切换使用哪个提供商。
|
|
||||||
|
|
||||||
### Q3: Gemini 原生 PDF 处理和文本提取有什么区别?
|
|
||||||
|
|
||||||
- **原生 PDF 处理**(Gemini):AI 直接"看"PDF,能识别图片中的文字、理解表格结构、识别数学公式
|
|
||||||
- **文本提取**(其他提供商):先用 PyPDF2/python-docx 提取纯文本,然后交给 AI,丢失所有图片和格式
|
|
||||||
|
|
||||||
**示例**:如果试卷中有带图的选择题,Gemini 能理解图片内容并提取题目,其他提供商只能提取文字部分。
|
|
||||||
|
|
||||||
### Q4: Gemini 自定义 Base URL 有什么用?
|
|
||||||
|
|
||||||
- **场景一**:使用多个 API Key 的轮训服务,避免单个 Key 频率限制
|
|
||||||
- **场景二**:使用国内加速代理,提高访问速度
|
|
||||||
- **场景三**:自建中转服务,统一管理和监控 API 调用
|
|
||||||
|
|
||||||
**格式要求**:Base URL 应该是完整的域名,例如 `https://your-service.com`,后端会自动拼接 `/v1beta/models/{model}:generateContent`
|
|
||||||
|
|
||||||
### Q5: 如果文档中没有提供答案怎么办?
|
|
||||||
|
|
||||||
QQuiz 会自动使用 AI 生成参考答案,标注为"AI参考答案:"。这个功能对所有 AI 提供商都支持。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 推荐配置方案
|
|
||||||
|
|
||||||
### 方案一:仅使用 Gemini(推荐)
|
|
||||||
```env
|
|
||||||
AI_PROVIDER=gemini
|
|
||||||
GEMINI_API_KEY=your-key
|
|
||||||
```
|
|
||||||
- ✅ 最佳 PDF 支持
|
|
||||||
- ✅ 免费额度充足
|
|
||||||
- ✅ 配置简单
|
|
||||||
|
|
||||||
### 方案二:Gemini + OpenAI(备用)
|
|
||||||
```env
|
|
||||||
AI_PROVIDER=gemini
|
|
||||||
GEMINI_API_KEY=your-gemini-key
|
|
||||||
OPENAI_API_KEY=your-openai-key
|
|
||||||
```
|
|
||||||
- 主用 Gemini,当遇到限制时手动切换到 OpenAI
|
|
||||||
|
|
||||||
### 方案三:全配置(多选)
|
|
||||||
配置所有提供商,根据需求灵活切换
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**推荐流程**:
|
|
||||||
1. 首次部署:使用 Gemini(免费额度充足)
|
|
||||||
2. 生产环境:使用管理员后台配置,支持在线切换
|
|
||||||
3. 高频使用:配置 Gemini 自定义 Base URL 使用 Key 轮训服务
|
|
||||||
@@ -1,533 +0,0 @@
|
|||||||
# QQuiz Windows 部署指南
|
|
||||||
|
|
||||||
## 🚀 方式一:Docker Desktop(推荐)
|
|
||||||
|
|
||||||
这是 Windows 上最简单的部署方式。
|
|
||||||
|
|
||||||
### 前置要求
|
|
||||||
|
|
||||||
1. **安装 Docker Desktop**
|
|
||||||
- 下载地址:https://www.docker.com/products/docker-desktop/
|
|
||||||
- 系统要求:Windows 10/11 64-bit (Pro/Enterprise/Education)
|
|
||||||
- 需要启用 WSL 2
|
|
||||||
|
|
||||||
2. **启用 WSL 2**(如果未启用)
|
|
||||||
```powershell
|
|
||||||
# 以管理员身份运行 PowerShell
|
|
||||||
wsl --install
|
|
||||||
# 重启计算机
|
|
||||||
```
|
|
||||||
|
|
||||||
### 部署步骤
|
|
||||||
|
|
||||||
#### 1. 配置环境变量
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# 在项目根目录(E:\QQuiz)打开 PowerShell
|
|
||||||
cd E:\QQuiz
|
|
||||||
|
|
||||||
# 复制环境变量模板
|
|
||||||
copy .env.example .env
|
|
||||||
|
|
||||||
# 使用记事本编辑 .env
|
|
||||||
notepad .env
|
|
||||||
```
|
|
||||||
|
|
||||||
**必须修改的配置:**
|
|
||||||
|
|
||||||
```env
|
|
||||||
# JWT 密钥(必须修改!至少 32 字符)
|
|
||||||
SECRET_KEY=your-very-long-secret-key-change-this-to-something-secure
|
|
||||||
|
|
||||||
# 选择一个 AI 提供商并配置 API Key
|
|
||||||
|
|
||||||
# 方案 1: 使用 OpenAI
|
|
||||||
AI_PROVIDER=openai
|
|
||||||
OPENAI_API_KEY=sk-your-openai-api-key
|
|
||||||
OPENAI_MODEL=gpt-4o-mini
|
|
||||||
|
|
||||||
# 方案 2: 使用通义千问(国内推荐)
|
|
||||||
# AI_PROVIDER=qwen
|
|
||||||
# QWEN_API_KEY=sk-your-qwen-api-key
|
|
||||||
# QWEN_MODEL=qwen-plus
|
|
||||||
|
|
||||||
# 方案 3: 使用 Claude
|
|
||||||
# AI_PROVIDER=anthropic
|
|
||||||
# ANTHROPIC_API_KEY=sk-ant-your-key
|
|
||||||
# ANTHROPIC_MODEL=claude-3-haiku-20240307
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 启动 Docker Desktop
|
|
||||||
|
|
||||||
- 打开 Docker Desktop 应用
|
|
||||||
- 等待 Docker Engine 启动(底部状态显示 "Running")
|
|
||||||
|
|
||||||
#### 3. 启动服务
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# 在项目根目录
|
|
||||||
cd E:\QQuiz
|
|
||||||
|
|
||||||
# 启动所有服务(第一次会比较慢,需要下载镜像)
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# 查看服务状态
|
|
||||||
docker-compose ps
|
|
||||||
|
|
||||||
# 查看日志
|
|
||||||
docker-compose logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. 访问应用
|
|
||||||
|
|
||||||
- **前端**: http://localhost:3000
|
|
||||||
- **后端 API**: http://localhost:8000
|
|
||||||
- **API 文档**: http://localhost:8000/docs
|
|
||||||
|
|
||||||
#### 5. 默认账户登录
|
|
||||||
|
|
||||||
- **用户名**: `admin`
|
|
||||||
- **密码**: `admin123`
|
|
||||||
|
|
||||||
⚠️ **首次登录后请立即修改密码!**
|
|
||||||
|
|
||||||
### 常用 Docker 命令
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# 查看运行状态
|
|
||||||
docker-compose ps
|
|
||||||
|
|
||||||
# 查看日志
|
|
||||||
docker-compose logs -f backend
|
|
||||||
docker-compose logs -f frontend
|
|
||||||
|
|
||||||
# 停止服务
|
|
||||||
docker-compose stop
|
|
||||||
|
|
||||||
# 重启服务
|
|
||||||
docker-compose restart
|
|
||||||
|
|
||||||
# 停止并删除容器
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# 重新构建并启动
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# 进入容器内部
|
|
||||||
docker-compose exec backend bash
|
|
||||||
docker-compose exec postgres psql -U qquiz qquiz_db
|
|
||||||
```
|
|
||||||
|
|
||||||
### 问题排查
|
|
||||||
|
|
||||||
#### 问题 1: Docker Desktop 无法启动
|
|
||||||
|
|
||||||
**解决方案:**
|
|
||||||
```powershell
|
|
||||||
# 确保 WSL 2 已安装
|
|
||||||
wsl --list --verbose
|
|
||||||
|
|
||||||
# 如果版本是 1,升级到 2
|
|
||||||
wsl --set-version Ubuntu 2
|
|
||||||
|
|
||||||
# 设置默认版本为 2
|
|
||||||
wsl --set-default-version 2
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 问题 2: 端口被占用
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# 查看端口占用
|
|
||||||
netstat -ano | findstr :3000
|
|
||||||
netstat -ano | findstr :8000
|
|
||||||
netstat -ano | findstr :5432
|
|
||||||
|
|
||||||
# 结束占用进程(替换 PID)
|
|
||||||
taskkill /F /PID <进程ID>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 问题 3: 容器启动失败
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# 查看详细错误日志
|
|
||||||
docker-compose logs backend
|
|
||||||
|
|
||||||
# 重新构建
|
|
||||||
docker-compose down
|
|
||||||
docker-compose build --no-cache
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ 方式二:本地源码部署
|
|
||||||
|
|
||||||
适合开发调试,需要手动安装各种依赖。
|
|
||||||
|
|
||||||
### 前置要求
|
|
||||||
|
|
||||||
1. **Python 3.11+**
|
|
||||||
- 下载:https://www.python.org/downloads/
|
|
||||||
- 安装时勾选 "Add Python to PATH"
|
|
||||||
|
|
||||||
2. **Node.js 18+**
|
|
||||||
- 下载:https://nodejs.org/
|
|
||||||
- 推荐安装 LTS 版本
|
|
||||||
|
|
||||||
3. **PostgreSQL 15+**
|
|
||||||
- 下载:https://www.enterprisedb.com/downloads/postgres-postgresql-downloads
|
|
||||||
- 记住安装时设置的密码
|
|
||||||
|
|
||||||
### 部署步骤
|
|
||||||
|
|
||||||
#### 1. 安装 PostgreSQL
|
|
||||||
|
|
||||||
1. 下载并安装 PostgreSQL 15
|
|
||||||
2. 安装完成后,打开 **pgAdmin 4** 或 **SQL Shell (psql)**
|
|
||||||
|
|
||||||
3. 创建数据库:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- 使用 psql 或 pgAdmin 执行
|
|
||||||
CREATE DATABASE qquiz_db;
|
|
||||||
CREATE USER qquiz WITH PASSWORD 'qquiz_password';
|
|
||||||
GRANT ALL PRIVILEGES ON DATABASE qquiz_db TO qquiz;
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 配置环境变量
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cd E:\QQuiz
|
|
||||||
copy .env.example .env
|
|
||||||
notepad .env
|
|
||||||
```
|
|
||||||
|
|
||||||
**修改数据库连接为本地:**
|
|
||||||
|
|
||||||
```env
|
|
||||||
# 数据库(本地部署)
|
|
||||||
DATABASE_URL=postgresql+asyncpg://qquiz:qquiz_password@localhost:5432/qquiz_db
|
|
||||||
|
|
||||||
# JWT 密钥
|
|
||||||
SECRET_KEY=your-super-secret-key-at-least-32-characters-long
|
|
||||||
|
|
||||||
# AI 配置(选择一个)
|
|
||||||
AI_PROVIDER=openai
|
|
||||||
OPENAI_API_KEY=sk-your-openai-api-key
|
|
||||||
OPENAI_MODEL=gpt-4o-mini
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. 启动后端
|
|
||||||
|
|
||||||
**打开第一个 PowerShell 窗口:**
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cd E:\QQuiz\backend
|
|
||||||
|
|
||||||
# 创建虚拟环境
|
|
||||||
python -m venv venv
|
|
||||||
|
|
||||||
# 激活虚拟环境
|
|
||||||
.\venv\Scripts\Activate.ps1
|
|
||||||
|
|
||||||
# 如果遇到执行策略错误,运行:
|
|
||||||
# Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
|
||||||
|
|
||||||
# 安装依赖
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# 运行数据库迁移
|
|
||||||
alembic upgrade head
|
|
||||||
|
|
||||||
# 启动后端服务
|
|
||||||
uvicorn main:app --reload
|
|
||||||
```
|
|
||||||
|
|
||||||
**成功后会看到:**
|
|
||||||
```
|
|
||||||
INFO: Uvicorn running on http://127.0.0.1:8000
|
|
||||||
INFO: Application startup complete.
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. 启动前端
|
|
||||||
|
|
||||||
**打开第二个 PowerShell 窗口:**
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cd E:\QQuiz\frontend
|
|
||||||
|
|
||||||
# 安装依赖(第一次需要)
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# 启动开发服务器
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
**成功后会自动打开浏览器:**
|
|
||||||
```
|
|
||||||
VITE v5.0.11 ready in 1234 ms
|
|
||||||
|
|
||||||
➜ Local: http://localhost:3000/
|
|
||||||
➜ Network: use --host to expose
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5. 访问应用
|
|
||||||
|
|
||||||
- **前端**: http://localhost:3000
|
|
||||||
- **后端**: http://localhost:8000
|
|
||||||
- **API 文档**: http://localhost:8000/docs
|
|
||||||
|
|
||||||
### 问题排查
|
|
||||||
|
|
||||||
#### 问题 1: PowerShell 执行策略错误
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# 错误信息:无法加载文件 xxx.ps1,因为在此系统上禁止运行脚本
|
|
||||||
|
|
||||||
# 解决方案:
|
|
||||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
|
||||||
|
|
||||||
# 然后重新激活虚拟环境
|
|
||||||
.\venv\Scripts\Activate.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 问题 2: pip 安装依赖失败
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# 使用国内镜像加速
|
|
||||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 问题 3: PostgreSQL 连接失败
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# 确认 PostgreSQL 服务正在运行
|
|
||||||
# 打开 "服务" (services.msc)
|
|
||||||
# 找到 "postgresql-x64-15",确保状态为 "正在运行"
|
|
||||||
|
|
||||||
# 或使用命令行
|
|
||||||
sc query postgresql-x64-15
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 问题 4: npm 安装慢
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# 使用淘宝镜像
|
|
||||||
npm config set registry https://registry.npmmirror.com
|
|
||||||
|
|
||||||
# 然后重新安装
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 测试部署是否成功
|
|
||||||
|
|
||||||
### 1. 检查后端健康状态
|
|
||||||
|
|
||||||
浏览器访问:http://localhost:8000/health
|
|
||||||
|
|
||||||
**预期返回:**
|
|
||||||
```json
|
|
||||||
{"status": "healthy"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 查看 API 文档
|
|
||||||
|
|
||||||
访问:http://localhost:8000/docs
|
|
||||||
|
|
||||||
应该能看到 Swagger UI 界面,显示所有 API 接口。
|
|
||||||
|
|
||||||
### 3. 测试登录
|
|
||||||
|
|
||||||
1. 访问 http://localhost:3000
|
|
||||||
2. 使用默认账户登录:
|
|
||||||
- 用户名:`admin`
|
|
||||||
- 密码:`admin123`
|
|
||||||
3. 成功后应该进入 Dashboard
|
|
||||||
|
|
||||||
### 4. 测试创建题库
|
|
||||||
|
|
||||||
1. 点击「题库管理」
|
|
||||||
2. 点击「创建题库」
|
|
||||||
3. 输入题库名称
|
|
||||||
4. 上传测试文档(可以创建一个简单的 TXT 文件)
|
|
||||||
|
|
||||||
**测试文档示例 (test_questions.txt):**
|
|
||||||
```txt
|
|
||||||
1. Python 是一种什么类型的语言?
|
|
||||||
A. 编译型语言
|
|
||||||
B. 解释型语言
|
|
||||||
C. 汇编语言
|
|
||||||
D. 机器语言
|
|
||||||
答案:B
|
|
||||||
解析:Python 是一种解释型、面向对象的高级编程语言。
|
|
||||||
|
|
||||||
2. 以下哪个不是 Python 的数据类型?
|
|
||||||
A. list
|
|
||||||
B. tuple
|
|
||||||
C. array
|
|
||||||
D. dict
|
|
||||||
答案:C
|
|
||||||
解析:Python 内置的数据类型包括 list、tuple、dict 等,array 需要导入 array 模块。
|
|
||||||
```
|
|
||||||
|
|
||||||
5. 上传后等待解析完成(状态会从「处理中」变为「就绪」)
|
|
||||||
6. 点击「开始刷题」测试刷题功能
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Windows 特定配置
|
|
||||||
|
|
||||||
### 1. 设置环境变量(可选)
|
|
||||||
|
|
||||||
**通过 GUI 设置:**
|
|
||||||
1. 右键「此电脑」→「属性」
|
|
||||||
2. 「高级系统设置」→「环境变量」
|
|
||||||
3. 在「用户变量」中添加:
|
|
||||||
- `OPENAI_API_KEY`: 你的 API Key
|
|
||||||
- `SECRET_KEY`: 你的密钥
|
|
||||||
|
|
||||||
### 2. 配置防火墙(如果需要局域网访问)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# 允许端口访问(以管理员身份运行)
|
|
||||||
netsh advfirewall firewall add rule name="QQuiz Frontend" dir=in action=allow protocol=TCP localport=3000
|
|
||||||
netsh advfirewall firewall add rule name="QQuiz Backend" dir=in action=allow protocol=TCP localport=8000
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 创建启动脚本
|
|
||||||
|
|
||||||
**创建 `start.bat` 文件:**
|
|
||||||
|
|
||||||
```batch
|
|
||||||
@echo off
|
|
||||||
echo Starting QQuiz...
|
|
||||||
|
|
||||||
REM 检查 Docker Desktop 是否运行
|
|
||||||
docker info >nul 2>&1
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo Starting Docker Desktop...
|
|
||||||
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
|
|
||||||
timeout /t 10
|
|
||||||
)
|
|
||||||
|
|
||||||
REM 启动服务
|
|
||||||
cd /d "%~dp0"
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo QQuiz is starting...
|
|
||||||
echo Frontend: http://localhost:3000
|
|
||||||
echo Backend: http://localhost:8000
|
|
||||||
echo.
|
|
||||||
pause
|
|
||||||
```
|
|
||||||
|
|
||||||
**使用方法:**
|
|
||||||
- 双击 `start.bat` 即可启动服务
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 性能优化建议
|
|
||||||
|
|
||||||
### 1. Docker Desktop 配置
|
|
||||||
|
|
||||||
1. 打开 Docker Desktop
|
|
||||||
2. 设置 → Resources
|
|
||||||
3. 调整资源分配:
|
|
||||||
- **CPUs**: 4 核(推荐)
|
|
||||||
- **Memory**: 4 GB(推荐)
|
|
||||||
- **Swap**: 1 GB
|
|
||||||
- **Disk image size**: 60 GB
|
|
||||||
|
|
||||||
### 2. WSL 2 优化
|
|
||||||
|
|
||||||
**限制 WSL 2 内存占用(可选):**
|
|
||||||
|
|
||||||
创建 `%USERPROFILE%\.wslconfig` 文件:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[wsl2]
|
|
||||||
memory=4GB
|
|
||||||
processors=4
|
|
||||||
swap=1GB
|
|
||||||
```
|
|
||||||
|
|
||||||
重启 WSL:
|
|
||||||
```powershell
|
|
||||||
wsl --shutdown
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 开发工具推荐
|
|
||||||
|
|
||||||
- **代码编辑器**: VS Code(安装 Python、ESLint、Prettier 插件)
|
|
||||||
- **API 测试**: Postman 或 Insomnia
|
|
||||||
- **数据库管理**: pgAdmin 4 或 DBeaver
|
|
||||||
- **终端**: Windows Terminal(更好的 PowerShell 体验)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 常见问题汇总
|
|
||||||
|
|
||||||
### Q: 如何完全重置项目?
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Docker 方式
|
|
||||||
docker-compose down -v # 删除容器和数据卷
|
|
||||||
docker-compose up -d # 重新启动
|
|
||||||
|
|
||||||
# 本地方式
|
|
||||||
# 1. 删除数据库
|
|
||||||
DROP DATABASE qquiz_db;
|
|
||||||
CREATE DATABASE qquiz_db;
|
|
||||||
# 2. 重新运行迁移
|
|
||||||
cd E:\QQuiz\backend
|
|
||||||
alembic upgrade head
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q: 如何查看日志?
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Docker 方式
|
|
||||||
docker-compose logs -f backend
|
|
||||||
docker-compose logs -f frontend
|
|
||||||
|
|
||||||
# 本地方式
|
|
||||||
# 直接在运行的 PowerShell 窗口中查看
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q: 如何停止服务?
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Docker 方式
|
|
||||||
docker-compose stop
|
|
||||||
|
|
||||||
# 本地方式
|
|
||||||
# 在运行的 PowerShell 窗口中按 Ctrl+C
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q: 如何更新代码后重启?
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Docker 方式
|
|
||||||
docker-compose restart
|
|
||||||
|
|
||||||
# 本地方式(uvicorn 和 vite 会自动重载)
|
|
||||||
# 无需操作,保存文件后自动刷新
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 获取帮助
|
|
||||||
|
|
||||||
如果遇到问题:
|
|
||||||
|
|
||||||
1. **查看日志**: `docker-compose logs -f`
|
|
||||||
2. **检查文档**: 阅读 `DEPLOYMENT.md`
|
|
||||||
3. **查看 API 文档**: http://localhost:8000/docs
|
|
||||||
4. **GitHub Issues**: 提交问题报告
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
祝你部署顺利!🎉
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
@echo off
|
|
||||||
REM 激活虚拟环境的批处理脚本
|
|
||||||
cd /d "%~dp0..\backend"
|
|
||||||
call venv\Scripts\activate.bat
|
|
||||||
cmd /k
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
@echo off
|
|
||||||
title QQuiz - System Status Check
|
|
||||||
|
|
||||||
cd /d "%~dp0.."
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo QQuiz System Status Check
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [1] Checking Python...
|
|
||||||
python --version
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo ERROR: Python not found!
|
|
||||||
) else (
|
|
||||||
echo OK
|
|
||||||
)
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [2] Checking Node.js...
|
|
||||||
node --version
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo ERROR: Node.js not found!
|
|
||||||
) else (
|
|
||||||
echo OK
|
|
||||||
)
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [3] Checking PostgreSQL...
|
|
||||||
psql --version
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo WARNING: PostgreSQL command not found in PATH
|
|
||||||
) else (
|
|
||||||
echo OK
|
|
||||||
)
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [4] Checking if backend is running...
|
|
||||||
curl -s http://localhost:8000/health >nul 2>&1
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo ERROR: Backend is NOT running on port 8000
|
|
||||||
echo.
|
|
||||||
echo Backend must be started first!
|
|
||||||
echo Run the backend window manually:
|
|
||||||
echo cd backend
|
|
||||||
echo venv\Scripts\activate.bat
|
|
||||||
echo uvicorn main:app --reload
|
|
||||||
) else (
|
|
||||||
echo OK - Backend is running
|
|
||||||
)
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [5] Checking if frontend is running...
|
|
||||||
curl -s http://localhost:3000 >nul 2>&1
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo WARNING: Frontend is NOT running on port 3000
|
|
||||||
) else (
|
|
||||||
echo OK - Frontend is running
|
|
||||||
)
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [6] Checking ports...
|
|
||||||
echo Checking port 8000 (Backend):
|
|
||||||
netstat -ano | findstr :8000
|
|
||||||
echo.
|
|
||||||
echo Checking port 3000 (Frontend):
|
|
||||||
netstat -ano | findstr :3000
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [7] Checking .env configuration...
|
|
||||||
if exist ".env" (
|
|
||||||
echo OK - .env file exists
|
|
||||||
findstr /C:"OPENAI_API_KEY=sk-" .env >nul
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo WARNING: OPENAI_API_KEY may not be configured properly
|
|
||||||
) else (
|
|
||||||
echo OK - API Key appears to be configured
|
|
||||||
)
|
|
||||||
) else (
|
|
||||||
echo ERROR: .env file not found!
|
|
||||||
)
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo ========================================
|
|
||||||
echo Diagnosis Complete
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
pause
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
@echo off
|
|
||||||
title QQuiz - Fix and Start
|
|
||||||
color 0E
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo QQuiz - Automatic Fix and Start
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
cd /d "%~dp0.."
|
|
||||||
|
|
||||||
REM Check if .env exists
|
|
||||||
if not exist ".env" (
|
|
||||||
echo Creating .env file...
|
|
||||||
copy .env.example .env >nul
|
|
||||||
echo.
|
|
||||||
echo IMPORTANT: Edit .env and set your OPENAI_API_KEY
|
|
||||||
echo Opening .env file...
|
|
||||||
timeout /t 2 /nobreak >nul
|
|
||||||
notepad .env
|
|
||||||
echo.
|
|
||||||
echo Save and close .env, then press any key to continue...
|
|
||||||
pause >nul
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Choose database option:
|
|
||||||
echo.
|
|
||||||
echo [1] Use Docker (Recommended - Easy)
|
|
||||||
echo [2] Use Local PostgreSQL (Advanced)
|
|
||||||
echo.
|
|
||||||
choice /C 12 /M "Select option"
|
|
||||||
|
|
||||||
if %errorlevel% equ 1 (
|
|
||||||
echo.
|
|
||||||
echo Using Docker PostgreSQL...
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM Check Docker
|
|
||||||
docker --version >nul 2>&1
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo ERROR: Docker not found!
|
|
||||||
echo.
|
|
||||||
echo Please install Docker Desktop:
|
|
||||||
echo https://www.docker.com/products/docker-desktop/
|
|
||||||
echo.
|
|
||||||
echo After installing Docker, run this script again.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Starting MySQL in Docker...
|
|
||||||
docker-compose up -d mysql
|
|
||||||
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo.
|
|
||||||
echo Docker failed to start. Trying to fix...
|
|
||||||
docker-compose down
|
|
||||||
docker-compose up -d mysql
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Waiting for database...
|
|
||||||
timeout /t 10 /nobreak >nul
|
|
||||||
|
|
||||||
) else (
|
|
||||||
echo.
|
|
||||||
echo Using Local MySQL...
|
|
||||||
echo.
|
|
||||||
echo Make sure MySQL is running on port 3306
|
|
||||||
echo.
|
|
||||||
echo If you see connection errors, you need to:
|
|
||||||
echo 1. Start MySQL service
|
|
||||||
echo 2. Or install MySQL from https://dev.mysql.com/downloads/installer/
|
|
||||||
echo 3. Or choose option 1 to use Docker instead
|
|
||||||
echo.
|
|
||||||
pause
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo Starting Backend...
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
cd backend
|
|
||||||
|
|
||||||
if not exist "venv\Scripts\activate.bat" (
|
|
||||||
echo Creating virtual environment...
|
|
||||||
python -m venv venv
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Installing dependencies...
|
|
||||||
call venv\Scripts\activate.bat
|
|
||||||
pip install -q -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Running database migrations...
|
|
||||||
alembic upgrade head
|
|
||||||
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo.
|
|
||||||
echo WARNING: Database migration failed
|
|
||||||
echo The app will try to create tables automatically
|
|
||||||
echo.
|
|
||||||
)
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Starting services...
|
|
||||||
echo.
|
|
||||||
|
|
||||||
start "QQuiz Backend" cmd /k "cd /d %~dp0..backend && call venv\Scripts\activate.bat && echo Backend: http://localhost:8000 && echo Docs: http://localhost:8000/docs && echo. && uvicorn main:app --reload"
|
|
||||||
|
|
||||||
timeout /t 8 /nobreak >nul
|
|
||||||
|
|
||||||
start "QQuiz Frontend" cmd /k "cd /d %~dp0..frontend && echo Frontend: http://localhost:3000 && echo. && npm start"
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo SUCCESS! QQuiz is starting...
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
echo Frontend: http://localhost:3000
|
|
||||||
echo Backend: http://localhost:8000
|
|
||||||
echo.
|
|
||||||
echo Login: admin / admin123
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
timeout /t 5 /nobreak >nul
|
|
||||||
start http://localhost:3000
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo System is running...
|
|
||||||
echo Close backend/frontend windows to stop
|
|
||||||
echo.
|
|
||||||
pause
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
@echo off
|
|
||||||
chcp 65001 >nul
|
|
||||||
echo ========================================
|
|
||||||
echo 重新构建并启动后端服务
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
cd /d "%~dp0.."
|
|
||||||
|
|
||||||
echo [1/3] 停止后端容器...
|
|
||||||
docker-compose stop backend
|
|
||||||
if %ERRORLEVEL% NEQ 0 (
|
|
||||||
echo ❌ 停止后端失败
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
echo ✅ 后端已停止
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [2/3] 重新构建后端镜像(这可能需要几分钟)...
|
|
||||||
docker-compose build backend
|
|
||||||
if %ERRORLEVEL% NEQ 0 (
|
|
||||||
echo ❌ 构建失败
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
echo ✅ 构建完成
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [3/3] 启动后端容器...
|
|
||||||
docker-compose up -d backend
|
|
||||||
if %ERRORLEVEL% NEQ 0 (
|
|
||||||
echo ❌ 启动失败
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
echo ✅ 后端已启动
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo ========================================
|
|
||||||
echo 重新构建完成!
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
echo 现在可以查看后端日志:
|
|
||||||
echo docker-compose logs -f backend
|
|
||||||
echo.
|
|
||||||
echo 或者按任意键退出...
|
|
||||||
pause >nul
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
@echo off
|
|
||||||
title QQuiz - Restart Backend
|
|
||||||
color 0B
|
|
||||||
|
|
||||||
cd /d "%~dp0.."
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo Restarting Backend Container
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo Restarting backend...
|
|
||||||
docker-compose restart backend
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Waiting for backend to start...
|
|
||||||
timeout /t 5 /nobreak >nul
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo Backend Restarted!
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
echo Backend URL: http://localhost:8000
|
|
||||||
echo API Docs: http://localhost:8000/docs
|
|
||||||
echo.
|
|
||||||
echo View logs: scripts\view_backend_logs.bat
|
|
||||||
echo.
|
|
||||||
|
|
||||||
pause
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
@echo off
|
|
||||||
chcp 65001 >nul
|
|
||||||
title QQuiz Backend - 本地运行
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ==========================================
|
|
||||||
echo QQuiz Backend - 本地启动
|
|
||||||
echo ==========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
cd /d "%~dp0..\backend"
|
|
||||||
|
|
||||||
REM 检查虚拟环境是否存在
|
|
||||||
if not exist "venv\Scripts\activate.bat" (
|
|
||||||
echo [1/5] 创建虚拟环境...
|
|
||||||
python -m venv venv
|
|
||||||
echo [完成]
|
|
||||||
echo.
|
|
||||||
) else (
|
|
||||||
echo [1/5] 虚拟环境已存在
|
|
||||||
echo.
|
|
||||||
)
|
|
||||||
|
|
||||||
echo [2/5] 激活虚拟环境...
|
|
||||||
call venv\Scripts\activate.bat
|
|
||||||
echo [完成]
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [3/5] 安装依赖...
|
|
||||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo [错误] 依赖安装失败
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
echo [完成]
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [4/5] 运行数据库迁移...
|
|
||||||
alembic upgrade head
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo [警告] 数据库迁移失败,请检查 PostgreSQL 是否运行
|
|
||||||
echo.
|
|
||||||
)
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [5/5] 启动后端服务...
|
|
||||||
echo.
|
|
||||||
echo ==========================================
|
|
||||||
echo 后端服务启动中...
|
|
||||||
echo ==========================================
|
|
||||||
echo.
|
|
||||||
echo API 地址: http://localhost:8000
|
|
||||||
echo API 文档: http://localhost:8000/docs
|
|
||||||
echo.
|
|
||||||
echo 按 Ctrl+C 停止服务
|
|
||||||
echo ==========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
uvicorn main:app --reload
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
@echo off
|
|
||||||
chcp 65001 >nul
|
|
||||||
title QQuiz Frontend - 本地运行
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ==========================================
|
|
||||||
echo QQuiz Frontend - 本地启动
|
|
||||||
echo ==========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
cd /d "%~dp0..\frontend"
|
|
||||||
|
|
||||||
REM 检查 node_modules 是否存在
|
|
||||||
if not exist "node_modules" (
|
|
||||||
echo [1/2] 安装前端依赖(首次运行需要几分钟)...
|
|
||||||
echo.
|
|
||||||
echo [提示] 使用淘宝镜像加速下载...
|
|
||||||
call npm config set registry https://registry.npmmirror.com
|
|
||||||
call npm install
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo [错误] 依赖安装失败
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
echo [完成]
|
|
||||||
echo.
|
|
||||||
) else (
|
|
||||||
echo [1/2] 依赖已安装
|
|
||||||
echo.
|
|
||||||
)
|
|
||||||
|
|
||||||
echo [2/2] 启动前端服务...
|
|
||||||
echo.
|
|
||||||
echo ==========================================
|
|
||||||
echo 前端服务启动中...
|
|
||||||
echo ==========================================
|
|
||||||
echo.
|
|
||||||
echo 前端地址: http://localhost:3000
|
|
||||||
echo.
|
|
||||||
echo 按 Ctrl+C 停止服务
|
|
||||||
echo ==========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
call npm start
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
@echo off
|
|
||||||
cd /d "%~dp0.."
|
|
||||||
|
|
||||||
echo Creating .env configuration file...
|
|
||||||
echo.
|
|
||||||
|
|
||||||
(
|
|
||||||
echo DATABASE_URL=mysql+aiomysql://qquiz:qquiz_password@localhost:3306/qquiz_db
|
|
||||||
echo SECRET_KEY=qquiz-secret-key-for-development-change-in-production-32chars
|
|
||||||
echo AI_PROVIDER=openai
|
|
||||||
echo OPENAI_API_KEY=sk-your-openai-api-key-here
|
|
||||||
echo OPENAI_BASE_URL=https://api.openai.com/v1
|
|
||||||
echo OPENAI_MODEL=gpt-4o-mini
|
|
||||||
echo ALLOW_REGISTRATION=true
|
|
||||||
echo MAX_UPLOAD_SIZE_MB=10
|
|
||||||
echo MAX_DAILY_UPLOADS=20
|
|
||||||
echo CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
|
||||||
echo UPLOAD_DIR=./uploads
|
|
||||||
) > .env
|
|
||||||
|
|
||||||
echo Done! .env file created.
|
|
||||||
echo.
|
|
||||||
echo IMPORTANT: Please edit OPENAI_API_KEY in .env file
|
|
||||||
echo.
|
|
||||||
echo Opening .env file now...
|
|
||||||
timeout /t 2 /nobreak >nul
|
|
||||||
|
|
||||||
notepad .env
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Configuration complete!
|
|
||||||
echo Now you can run: start_app.bat
|
|
||||||
echo.
|
|
||||||
pause
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
@echo off
|
|
||||||
title QQuiz Backend Server
|
|
||||||
color 0B
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo Starting QQuiz Backend Server
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
cd /d "%~dp0..\backend"
|
|
||||||
|
|
||||||
if not exist "venv\Scripts\activate.bat" (
|
|
||||||
echo Creating virtual environment...
|
|
||||||
python -m venv venv
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Activating virtual environment...
|
|
||||||
call venv\Scripts\activate.bat
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Installing/updating dependencies...
|
|
||||||
pip install -q -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Running database migrations...
|
|
||||||
alembic upgrade head
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo Backend Server Starting...
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
echo API URL: http://localhost:8000
|
|
||||||
echo API Docs: http://localhost:8000/docs
|
|
||||||
echo Health: http://localhost:8000/health
|
|
||||||
echo.
|
|
||||||
echo Press Ctrl+C to stop
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
|
||||||
|
|
||||||
pause
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
@echo off
|
|
||||||
title QQuiz Frontend Server
|
|
||||||
color 0B
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo Starting QQuiz Frontend Server
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
cd /d "%~dp0..\frontend"
|
|
||||||
|
|
||||||
if not exist "node_modules" (
|
|
||||||
echo Installing dependencies (first time only)...
|
|
||||||
call npm config set registry https://registry.npmmirror.com
|
|
||||||
call npm install
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo Frontend Server Starting...
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
echo Frontend URL: http://localhost:3000
|
|
||||||
echo.
|
|
||||||
echo Make sure backend is running on port 8000!
|
|
||||||
echo.
|
|
||||||
echo Press Ctrl+C to stop
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
npm start
|
|
||||||
|
|
||||||
pause
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
@echo off
|
|
||||||
title QQuiz - Start with Docker Database
|
|
||||||
color 0B
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo QQuiz - Starting with Docker DB
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
cd /d "%~dp0.."
|
|
||||||
|
|
||||||
echo [1/4] Checking Docker...
|
|
||||||
docker --version >nul 2>&1
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo ERROR: Docker not found!
|
|
||||||
echo Please install Docker Desktop from https://www.docker.com/
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
echo OK - Docker installed
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [2/4] Starting MySQL in Docker...
|
|
||||||
docker-compose up -d mysql
|
|
||||||
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo ERROR: Failed to start MySQL
|
|
||||||
echo Try: docker-compose down
|
|
||||||
echo Then run this script again
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo OK - MySQL started
|
|
||||||
echo Waiting for database to be ready...
|
|
||||||
echo.
|
|
||||||
timeout /t 10 /nobreak >nul
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [3/4] Starting Backend...
|
|
||||||
start "QQuiz Backend" cmd /k "cd /d %~dp0..backend && call venv\Scripts\activate.bat && echo ======================================== && echo QQuiz Backend Server && echo ======================================== && echo. && echo API: http://localhost:8000 && echo Docs: http://localhost:8000/docs && echo. && alembic upgrade head && echo. && uvicorn main:app --reload"
|
|
||||||
|
|
||||||
echo Waiting for backend to start...
|
|
||||||
timeout /t 8 /nobreak >nul
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo [4/4] Starting Frontend...
|
|
||||||
start "QQuiz Frontend" cmd /k "cd /d %~dp0..frontend && echo ======================================== && echo QQuiz Frontend Server && echo ======================================== && echo. && echo URL: http://localhost:3000 && echo. && npm start"
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo SUCCESS! QQuiz is starting...
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
echo Frontend: http://localhost:3000
|
|
||||||
echo Backend: http://localhost:8000
|
|
||||||
echo Database: Running in Docker
|
|
||||||
echo.
|
|
||||||
echo Login:
|
|
||||||
echo Username: admin
|
|
||||||
echo Password: admin123
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
timeout /t 5 /nobreak >nul
|
|
||||||
start http://localhost:3000
|
|
||||||
|
|
||||||
echo System running...
|
|
||||||
echo To stop: Close the backend/frontend windows
|
|
||||||
echo To stop database: docker-compose down
|
|
||||||
echo.
|
|
||||||
pause
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
@echo off
|
|
||||||
chcp 65001 >nul
|
|
||||||
title QQuiz - 停止服务
|
|
||||||
|
|
||||||
cd /d "%~dp0.."
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ==========================================
|
|
||||||
echo 停止 QQuiz 服务
|
|
||||||
echo ==========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
if %errorlevel% equ 0 (
|
|
||||||
echo.
|
|
||||||
echo [完成] 服务已停止
|
|
||||||
) else (
|
|
||||||
echo.
|
|
||||||
echo [错误] 停止服务失败
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
|
||||||
pause
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
@echo off
|
|
||||||
title QQuiz - Backend Logs
|
|
||||||
color 0B
|
|
||||||
|
|
||||||
cd /d "%~dp0.."
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ========================================
|
|
||||||
echo QQuiz Backend Logs (Real-time)
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
echo Press Ctrl+C to stop viewing logs
|
|
||||||
echo.
|
|
||||||
|
|
||||||
docker-compose logs -f --tail=50 backend
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
@echo off
|
|
||||||
chcp 65001 >nul
|
|
||||||
echo ========================================
|
|
||||||
echo 实时查看后端日志
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
echo 按 Ctrl+C 可以停止查看日志
|
|
||||||
echo.
|
|
||||||
|
|
||||||
cd /d "%~dp0.."
|
|
||||||
|
|
||||||
docker-compose logs -f backend
|
|
||||||
Reference in New Issue
Block a user