feat: 实现数据库驱动的API配置管理和项目结构重组

## 新功能
- 实现管理后台API配置管理(OpenAI/Anthropic/Qwen)
- API配置保存到数据库,实时生效无需重启
- API密钥遮罩显示(前10位+后4位)
- 完整endpoint URL自动显示

## 后端改进
- 新增 config_service.py 用于加载数据库配置
- LLMService 支持动态配置注入,回退到环境变量
- 更新 exam.py 和 question.py 使用数据库配置
- 扩展 schemas.py 支持所有API配置字段

## 前端改进
- 重写 AdminSettings.jsx 增强UI体验
- API密钥显示/隐藏切换
- 当前使用的提供商可视化标识
- 移除"需要重启"的误导性提示

## 项目结构重组
- 移动所有脚本到 scripts/ 目录
- 移动所有文档到 docs/ 目录
- 清理 Python 缓存文件

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-01 19:24:12 +08:00
parent 0ea8e5aa1e
commit a01f3540c5
47 changed files with 1051 additions and 129 deletions

View File

@@ -1,7 +1,7 @@
# Database Configuration # Database Configuration
# For Docker: postgresql+asyncpg://qquiz:qquiz_password@postgres:5432/qquiz_db # For Docker: mysql+aiomysql://qquiz:qquiz_password@mysql:3306/qquiz_db
# For Local: postgresql+asyncpg://localhost:5432/qquiz_db # For Local: mysql+aiomysql://qquiz:qquiz_password@localhost:3306/qquiz_db
DATABASE_URL=postgresql+asyncpg://localhost:5432/qquiz_db DATABASE_URL=mysql+aiomysql://qquiz:qquiz_password@localhost:3306/qquiz_db
# JWT Secret (Please change this in production!) # JWT Secret (Please change this in production!)
SECRET_KEY=your-super-secret-key-change-in-production-minimum-32-characters SECRET_KEY=your-super-secret-key-change-in-production-minimum-32-characters

View File

@@ -39,22 +39,33 @@ docker-compose up -d
#### 前置要求 #### 前置要求
- Python 3.11+ - Python 3.11+
- Node.js 18+ - Node.js 18+
- PostgreSQL 15+ - 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 用户:**
```bash ```bash
# 1. 配置环境变量 # 1. 配置环境变量
cp .env.example .env cp .env.example .env
# 编辑 .env修改 DATABASE_URL 为本地数据库地址 # 编辑 .env修改 DATABASE_URL 为本地数据库地址
# 2. 启动 PostgreSQL # 2. 启动 MySQL
# macOS: brew services start postgresql # macOS: brew services start mysql
# Linux: sudo systemctl start postgresql # Linux: sudo systemctl start mysql
# 3. 运行启动脚本 # 3. 运行启动脚本
chmod +x run_local.sh chmod +x scripts/run_local.sh
./run_local.sh ./scripts/run_local.sh
``` ```
**MySQL 安装指南:** 详见 [docs/MYSQL_SETUP.md](docs/MYSQL_SETUP.md)
## 默认账户 ## 默认账户
**管理员账户:** **管理员账户:**
@@ -69,23 +80,35 @@ chmod +x run_local.sh
QQuiz/ QQuiz/
├── backend/ # FastAPI 后端 ├── backend/ # FastAPI 后端
│ ├── alembic/ # 数据库迁移 │ ├── alembic/ # 数据库迁移
│ ├── routers/ # API 路由 (Step 2) │ ├── routers/ # API 路由
│ ├── services/ # 业务逻辑 (Step 2) │ ├── services/ # 业务逻辑
│ ├── models.py # 数据模型 │ ├── models.py # 数据模型
│ ├── database.py # 数据库配置 │ ├── database.py # 数据库配置
│ ├── main.py # 应用入口 │ ├── main.py # 应用入口
│ └── requirements.txt # Python 依赖 │ └── requirements.txt # Python 依赖
├── frontend/ # React 前端 ├── frontend/ # React 前端
│ ├── src/ │ ├── src/
│ │ ├── api/ # API 客户端 (Step 3) │ │ ├── api/ # API 客户端
│ │ ├── pages/ # 页面组件 (Step 4) │ │ ├── pages/ # 页面组件
│ │ ├── components/ # 通用组件 (Step 4) │ │ ├── components/ # 通用组件
│ │ └── App.jsx # 应用入口 │ │ └── App.jsx # 应用入口
│ ├── package.json # Node 依赖 │ ├── package.json # Node 依赖
│ └── vite.config.js # Vite 配置 │ └── vite.config.js # Vite 配置
├── docker-compose.yml # Docker 编排 ✅ ├── scripts/ # 部署和启动脚本
├── .env.example # 环境变量模板 ✅ │ ├── fix_and_start.bat # Windows 快速启动(推荐)
└── run_local.sh # 本地运行脚本 ✅ │ ├── start_with_docker_db.bat # Docker 数据库启动
│ ├── setup.bat # 环境配置脚本
│ └── run_local.sh # Linux/macOS 启动脚本
├── docs/ # 文档目录
│ ├── QUICK_START.md # 快速入门指南
│ ├── WINDOWS_DEPLOYMENT.md # Windows 部署指南
│ ├── DOCKER_MIRROR_SETUP.md # Docker 镜像加速配置
│ └── PROJECT_STRUCTURE.md # 项目架构详解
├── test_data/ # 测试数据
│ └── sample_questions.txt # 示例题目
├── docker-compose.yml # Docker 编排
├── .env.example # 环境变量模板
└── README.md # 项目说明
``` ```
## 核心业务流程 ## 核心业务流程
@@ -128,9 +151,10 @@ QQuiz/
**后端:** **后端:**
- FastAPI - 现代化 Python Web 框架 - FastAPI - 现代化 Python Web 框架
- SQLAlchemy - ORM - SQLAlchemy 2.0 - 异步 ORM
- Alembic - 数据库迁移 - Alembic - 数据库迁移
- PostgreSQL - 数据库 - MySQL 8.0 - 数据库
- aiomysql - MySQL 异步驱动
- Pydantic - 数据验证 - Pydantic - 数据验证
**前端:** **前端:**

View File

@@ -1,13 +1,15 @@
fastapi==0.109.0 fastapi==0.109.0
uvicorn[standard]==0.27.0 uvicorn[standard]==0.27.0
sqlalchemy==2.0.25 sqlalchemy==2.0.25
asyncpg==0.29.0 aiomysql==0.2.0
pymysql==1.1.0
alembic==1.13.1 alembic==1.13.1
pydantic==2.5.3 pydantic==2.5.3
pydantic-settings==2.1.0 pydantic-settings==2.1.0
python-dotenv==1.0.0 python-dotenv==1.0.0
python-multipart==0.0.6 python-multipart==0.0.6
passlib[bcrypt]==1.7.4 passlib==1.7.4
bcrypt==4.0.1
python-jose[cryptography]==3.3.0 python-jose[cryptography]==3.3.0
aiofiles==23.2.1 aiofiles==23.2.1
httpx==0.26.0 httpx==0.26.0

View File

@@ -24,11 +24,26 @@ async def get_system_config(
result = await db.execute(select(SystemConfig)) result = await db.execute(select(SystemConfig))
configs = {config.key: config.value for config in result.scalars().all()} configs = {config.key: config.value for config in result.scalars().all()}
# Mask API keys (show only first 10 and last 4 characters)
def mask_api_key(key):
if not key or len(key) < 20:
return key
return f"{key[:10]}...{key[-4:]}"
return { return {
"allow_registration": configs.get("allow_registration", "true").lower() == "true", "allow_registration": configs.get("allow_registration", "true").lower() == "true",
"max_upload_size_mb": int(configs.get("max_upload_size_mb", "10")), "max_upload_size_mb": int(configs.get("max_upload_size_mb", "10")),
"max_daily_uploads": int(configs.get("max_daily_uploads", "20")), "max_daily_uploads": int(configs.get("max_daily_uploads", "20")),
"ai_provider": configs.get("ai_provider", "openai") "ai_provider": configs.get("ai_provider", "openai"),
# API Configuration
"openai_api_key": mask_api_key(configs.get("openai_api_key")),
"openai_base_url": configs.get("openai_base_url", "https://api.openai.com/v1"),
"openai_model": configs.get("openai_model", "gpt-4o-mini"),
"anthropic_api_key": mask_api_key(configs.get("anthropic_api_key")),
"anthropic_model": configs.get("anthropic_model", "claude-3-haiku-20240307"),
"qwen_api_key": mask_api_key(configs.get("qwen_api_key")),
"qwen_base_url": configs.get("qwen_base_url", "https://dashscope.aliyuncs.com/compatible-mode/v1"),
"qwen_model": configs.get("qwen_model", "qwen-plus")
} }

View File

@@ -83,9 +83,12 @@ async def login(
# Create access token # Create access token
access_token = create_access_token( access_token = create_access_token(
data={"sub": user.id} data={"sub": str(user.id)} # JWT 'sub' must be a string
) )
print(f"✅ Login successful: user={user.username}, id={user.id}")
print(f"🔑 Generated token (first 50 chars): {access_token[:50]}...")
return { return {
"access_token": access_token, "access_token": access_token,
"token_type": "bearer" "token_type": "bearer"

View File

@@ -17,7 +17,8 @@ from schemas import (
) )
from services.auth_service import get_current_user from services.auth_service import get_current_user
from services.document_parser import document_parser from services.document_parser import document_parser
from services.llm_service import llm_service from services.llm_service import LLMService
from services.config_service import load_llm_config
from utils import is_allowed_file, calculate_content_hash from utils import is_allowed_file, calculate_content_hash
router = APIRouter() router = APIRouter()
@@ -151,6 +152,10 @@ async def async_parse_and_save(
if not text_content or len(text_content.strip()) < 10: if not text_content or len(text_content.strip()) < 10:
raise Exception("Document appears to be empty or too short") raise Exception("Document appears to be empty or too short")
# Load LLM configuration from database
llm_config = await load_llm_config(db)
llm_service = LLMService(config=llm_config)
# Parse questions using LLM # Parse questions using LLM
print(f"[Exam {exam_id}] Calling LLM to extract questions...") print(f"[Exam {exam_id}] Calling LLM to extract questions...")
questions_data = await llm_service.parse_document(text_content) questions_data = await llm_service.parse_document(text_content)

View File

@@ -13,7 +13,8 @@ from schemas import (
AnswerSubmit, AnswerCheckResponse AnswerSubmit, AnswerCheckResponse
) )
from services.auth_service import get_current_user from services.auth_service import get_current_user
from services.llm_service import llm_service from services.llm_service import LLMService
from services.config_service import load_llm_config
router = APIRouter() router = APIRouter()
@@ -177,6 +178,10 @@ async def check_answer(
# Check answer based on question type # Check answer based on question type
if question.type == QuestionType.SHORT: if question.type == QuestionType.SHORT:
# Load LLM configuration from database
llm_config = await load_llm_config(db)
llm_service = LLMService(config=llm_config)
# Use AI to grade short answer # Use AI to grade short answer
grading = await llm_service.grade_short_answer( grading = await llm_service.grade_short_answer(
question.content, question.content,

View File

@@ -45,6 +45,15 @@ class SystemConfigUpdate(BaseModel):
max_upload_size_mb: Optional[int] = None max_upload_size_mb: Optional[int] = None
max_daily_uploads: Optional[int] = None max_daily_uploads: Optional[int] = None
ai_provider: Optional[str] = None ai_provider: Optional[str] = None
# API Configuration
openai_api_key: Optional[str] = None
openai_base_url: Optional[str] = None
openai_model: Optional[str] = None
anthropic_api_key: Optional[str] = None
anthropic_model: Optional[str] = None
qwen_api_key: Optional[str] = None
qwen_base_url: Optional[str] = None
qwen_model: Optional[str] = None
class SystemConfigResponse(BaseModel): class SystemConfigResponse(BaseModel):
@@ -52,6 +61,15 @@ class SystemConfigResponse(BaseModel):
max_upload_size_mb: int max_upload_size_mb: int
max_daily_uploads: int max_daily_uploads: int
ai_provider: str ai_provider: str
# API Configuration
openai_api_key: Optional[str] = None
openai_base_url: Optional[str] = None
openai_model: Optional[str] = None
anthropic_api_key: Optional[str] = None
anthropic_model: Optional[str] = None
qwen_api_key: Optional[str] = None
qwen_base_url: Optional[str] = None
qwen_model: Optional[str] = None
# ============ Exam Schemas ============ # ============ Exam Schemas ============

View File

@@ -28,13 +28,24 @@ async def get_current_user(
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
print(f"🔍 Received token (first 50 chars): {token[:50] if token else 'None'}...")
# Decode token # Decode token
payload = decode_access_token(token) payload = decode_access_token(token)
if payload is None: if payload is None:
print(f"❌ Token decode failed - Invalid or expired token")
raise credentials_exception raise credentials_exception
user_id: int = payload.get("sub") user_id = payload.get("sub")
if user_id is None: if user_id is None:
print(f"❌ No 'sub' in payload: {payload}")
raise credentials_exception
# Convert user_id to int if it's a string
try:
user_id = int(user_id)
except (ValueError, TypeError):
print(f"❌ Invalid user_id format: {user_id}")
raise credentials_exception raise credentials_exception
# Get user from database # Get user from database
@@ -42,8 +53,10 @@ async def get_current_user(
user = result.scalar_one_or_none() user = result.scalar_one_or_none()
if user is None: if user is None:
print(f"❌ User not found with id: {user_id}")
raise credentials_exception raise credentials_exception
print(f"✅ User authenticated: {user.username} (id={user.id})")
return user return user

View File

@@ -0,0 +1,43 @@
"""
Configuration Service - Load system configuration from database
"""
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import Dict
from models import SystemConfig
async def load_llm_config(db: AsyncSession) -> Dict[str, str]:
"""
Load LLM configuration from database.
Returns a dictionary with all LLM-related configuration:
{
'ai_provider': 'openai',
'openai_api_key': 'sk-...',
'openai_base_url': 'https://api.openai.com/v1',
'openai_model': 'gpt-4o-mini',
...
}
"""
# Fetch all config from database
result = await db.execute(select(SystemConfig))
db_configs = {config.key: config.value for config in result.scalars().all()}
# Build configuration dictionary
config = {
'ai_provider': db_configs.get('ai_provider', 'openai'),
# OpenAI
'openai_api_key': db_configs.get('openai_api_key'),
'openai_base_url': db_configs.get('openai_base_url', 'https://api.openai.com/v1'),
'openai_model': db_configs.get('openai_model', 'gpt-4o-mini'),
# Anthropic
'anthropic_api_key': db_configs.get('anthropic_api_key'),
'anthropic_model': db_configs.get('anthropic_model', 'claude-3-haiku-20240307'),
# Qwen
'qwen_api_key': db_configs.get('qwen_api_key'),
'qwen_base_url': db_configs.get('qwen_base_url', 'https://dashscope.aliyuncs.com/compatible-mode/v1'),
'qwen_model': db_configs.get('qwen_model', 'qwen-plus')
}
return config

View File

@@ -15,28 +15,53 @@ from utils import calculate_content_hash
class LLMService: class LLMService:
"""Service for interacting with various LLM providers""" """Service for interacting with various LLM providers"""
def __init__(self): def __init__(self, config: Optional[Dict[str, str]] = None):
self.provider = os.getenv("AI_PROVIDER", "openai") """
Initialize LLM Service with optional configuration.
If config is not provided, falls back to environment variables.
Args:
config: Dictionary with keys like 'ai_provider', 'openai_api_key', etc.
"""
# Get provider from config or environment
self.provider = (config or {}).get("ai_provider") or os.getenv("AI_PROVIDER", "openai")
if self.provider == "openai": if self.provider == "openai":
api_key = (config or {}).get("openai_api_key") or os.getenv("OPENAI_API_KEY")
base_url = (config or {}).get("openai_base_url") or os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
self.model = (config or {}).get("openai_model") or os.getenv("OPENAI_MODEL", "gpt-4o-mini")
if not api_key:
raise ValueError("OpenAI API key not configured")
self.client = AsyncOpenAI( self.client = AsyncOpenAI(
api_key=os.getenv("OPENAI_API_KEY"), api_key=api_key,
base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") base_url=base_url
) )
self.model = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
elif self.provider == "anthropic": elif self.provider == "anthropic":
api_key = (config or {}).get("anthropic_api_key") or os.getenv("ANTHROPIC_API_KEY")
self.model = (config or {}).get("anthropic_model") or os.getenv("ANTHROPIC_MODEL", "claude-3-haiku-20240307")
if not api_key:
raise ValueError("Anthropic API key not configured")
self.client = AsyncAnthropic( self.client = AsyncAnthropic(
api_key=os.getenv("ANTHROPIC_API_KEY") api_key=api_key
) )
self.model = os.getenv("ANTHROPIC_MODEL", "claude-3-haiku-20240307")
elif self.provider == "qwen": elif self.provider == "qwen":
api_key = (config or {}).get("qwen_api_key") or os.getenv("QWEN_API_KEY")
base_url = (config or {}).get("qwen_base_url") or os.getenv("QWEN_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1")
self.model = (config or {}).get("qwen_model") or os.getenv("QWEN_MODEL", "qwen-plus")
if not api_key:
raise ValueError("Qwen API key not configured")
self.client = AsyncOpenAI( self.client = AsyncOpenAI(
api_key=os.getenv("QWEN_API_KEY"), api_key=api_key,
base_url=os.getenv("QWEN_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1") base_url=base_url
) )
self.model = os.getenv("QWEN_MODEL", "qwen-plus")
else: else:
raise ValueError(f"Unsupported AI provider: {self.provider}") raise ValueError(f"Unsupported AI provider: {self.provider}")

View File

@@ -37,6 +37,8 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire}) to_encode.update({"exp": expire})
print(f"🔑 Creating token with SECRET_KEY (first 20 chars): {SECRET_KEY[:20]}...")
print(f"📦 Token payload: {to_encode}")
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt return encoded_jwt
@@ -44,9 +46,12 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -
def decode_access_token(token: str) -> Optional[dict]: def decode_access_token(token: str) -> Optional[dict]:
"""Decode a JWT access token""" """Decode a JWT access token"""
try: try:
print(f"🔑 SECRET_KEY (first 20 chars): {SECRET_KEY[:20]}...")
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
print(f"✅ Token decoded successfully: {payload}")
return payload return payload
except JWTError: except JWTError as e:
print(f"❌ JWT decode error: {type(e).__name__}: {str(e)}")
return None return None

View File

@@ -1,17 +1,19 @@
services: services:
postgres: mysql:
image: postgres:15-alpine image: mysql:8.0
container_name: qquiz_postgres container_name: qquiz_mysql
environment: environment:
POSTGRES_USER: qquiz MYSQL_ROOT_PASSWORD: root_password
POSTGRES_PASSWORD: qquiz_password MYSQL_DATABASE: qquiz_db
POSTGRES_DB: qquiz_db MYSQL_USER: qquiz
MYSQL_PASSWORD: qquiz_password
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - mysql_data:/var/lib/mysql
ports: ports:
- "5432:5432" - "3306:3306"
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U qquiz"] test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "qquiz", "-pqquiz_password"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -22,7 +24,7 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: qquiz_backend container_name: qquiz_backend
environment: environment:
- DATABASE_URL=postgresql+asyncpg://qquiz:qquiz_password@postgres:5432/qquiz_db - DATABASE_URL=mysql+aiomysql://qquiz:qquiz_password@mysql:3306/qquiz_db
env_file: env_file:
- .env - .env
volumes: volumes:
@@ -31,7 +33,7 @@ services:
ports: ports:
- "8000:8000" - "8000:8000"
depends_on: depends_on:
postgres: mysql:
condition: service_healthy condition: service_healthy
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
@@ -46,11 +48,11 @@ services:
ports: ports:
- "3000:3000" - "3000:3000"
environment: environment:
- REACT_APP_API_URL=http://localhost:8000 - VITE_API_URL=http://localhost:8000
depends_on: depends_on:
- backend - backend
command: npm start command: npm start
volumes: volumes:
postgres_data: mysql_data:
upload_files: upload_files:

262
docs/MYSQL_SETUP.md Normal file
View File

@@ -0,0 +1,262 @@
# MySQL 安装与配置指南
QQuiz 使用 MySQL 8.0 作为数据库,你可以选择 Docker 部署或本地安装。
## 方式一:使用 Docker (推荐)
### 优点
- 无需手动安装 MySQL
- 自动配置和初始化
- 隔离环境,不影响系统
### 使用步骤
1. **安装 Docker Desktop**
- 下载地址https://www.docker.com/products/docker-desktop/
- 安装后启动 Docker Desktop
2. **运行启动脚本**
```bash
scripts\fix_and_start.bat
```
选择 **[1] Use Docker**
3. **完成!**
- Docker 会自动下载 MySQL 镜像
- 自动创建数据库和用户
- 自动启动服务
---
## 方式二:本地安装 MySQL
### 下载 MySQL
1. 访问 MySQL 官网下载页面:
https://dev.mysql.com/downloads/installer/
2. 选择 **MySQL Installer for Windows**
3. 下载 `mysql-installer-community-8.0.x.x.msi`
### 安装步骤
1. **运行安装程序**
- 双击下载的 .msi 文件
2. **选择安装类型**
- 选择 "Developer Default" 或 "Server only"
- 点击 Next
3. **配置 MySQL Server**
- **Config Type**: Development Computer
- **Port**: 3306 (默认)
- **Authentication Method**: 选择 "Use Strong Password Encryption"
4. **设置 Root 密码**
- 输入并记住 root 用户的密码
- 建议密码:`root` (开发环境)
5. **Windows Service 配置**
- ✅ Configure MySQL Server as a Windows Service
- Service Name: MySQL80
- ✅ Start the MySQL Server at System Startup
6. **完成安装**
- 点击 Execute 开始安装
- 等待安装完成
- 点击 Finish
### 验证安装
打开命令提示符,运行:
```bash
mysql --version
```
应该显示:`mysql Ver 8.0.x for Win64 on x86_64`
### 配置 QQuiz 数据库
**方式 A使用脚本自动创建 (推荐)**
运行:
```bash
scripts\fix_and_start.bat
```
选择 **[2] Use Local MySQL**
**方式 B手动创建**
1. 打开 MySQL 命令行客户端:
```bash
mysql -u root -p
```
2. 输入 root 密码
3. 创建数据库和用户:
```sql
CREATE DATABASE qquiz_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'qquiz'@'localhost' IDENTIFIED BY 'qquiz_password';
GRANT ALL PRIVILEGES ON qquiz_db.* TO 'qquiz'@'localhost';
FLUSH PRIVILEGES;
EXIT;
```
---
## 数据库配置说明
### .env 文件配置
确保 `.env` 文件中的数据库连接字符串正确:
**本地 MySQL:**
```env
DATABASE_URL=mysql+aiomysql://qquiz:qquiz_password@localhost:3306/qquiz_db
```
**Docker MySQL:**
```env
DATABASE_URL=mysql+aiomysql://qquiz:qquiz_password@mysql:3306/qquiz_db
```
### 连接参数说明
- `mysql+aiomysql://` - 使用 aiomysql 异步驱动
- `qquiz` - 数据库用户名
- `qquiz_password` - 数据库密码
- `localhost` 或 `mysql` - 数据库主机地址
- `3306` - MySQL 默认端口
- `qquiz_db` - 数据库名称
---
## 常见问题
### 1. 端口 3306 被占用
**错误信息:**
```
Error: Port 3306 is already in use
```
**解决方案:**
- 检查是否已经有 MySQL 运行:`netstat -ano | findstr :3306`
- 停止现有的 MySQL 服务
- 或修改 `.env` 中的端口号
### 2. 无法连接到 MySQL
**错误信息:**
```
Can't connect to MySQL server on 'localhost'
```
**解决方案:**
1. **检查 MySQL 服务是否运行**
- 按 Win+R输入 `services.msc`
- 查找 "MySQL80" 服务
- 确认状态为 "正在运行"
2. **启动 MySQL 服务**
```bash
net start MySQL80
```
3. **检查防火墙设置**
- 确保防火墙允许 MySQL 端口 3306
### 3. 密码验证失败
**错误信息:**
```
Access denied for user 'qquiz'@'localhost'
```
**解决方案:**
重新创建用户并设置密码:
```sql
mysql -u root -p
DROP USER IF EXISTS 'qquiz'@'localhost';
CREATE USER 'qquiz'@'localhost' IDENTIFIED BY 'qquiz_password';
GRANT ALL PRIVILEGES ON qquiz_db.* TO 'qquiz'@'localhost';
FLUSH PRIVILEGES;
```
### 4. 字符集问题
**解决方案:**
确保数据库使用 UTF-8 字符集:
```sql
ALTER DATABASE qquiz_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
---
## 管理工具推荐
### 1. MySQL Workbench (官方)
- 下载https://dev.mysql.com/downloads/workbench/
- 功能可视化数据库管理、SQL 编辑器、备份还原
### 2. DBeaver (免费开源)
- 下载https://dbeaver.io/download/
- 功能多数据库支持、数据导入导出、ER 图
### 3. phpMyAdmin (Web 界面)
- 适合习惯 Web 界面的用户
---
## 数据库备份与恢复
### 备份数据库
```bash
mysqldump -u qquiz -p qquiz_db > backup.sql
```
### 恢复数据库
```bash
mysql -u qquiz -p qquiz_db < backup.sql
```
---
## 切换回 PostgreSQL
如果需要切换回 PostgreSQL
1. 修改 `requirements.txt`
```
asyncpg==0.29.0 # 替换 aiomysql
```
2. 修改 `.env`
```
DATABASE_URL=postgresql+asyncpg://qquiz:qquiz_password@localhost:5432/qquiz_db
```
3. 修改 `docker-compose.yml`
- 将 `mysql` 服务改回 `postgres`
4. 重新安装依赖:
```bash
pip install -r requirements.txt
```
---
## 技术支持
如遇到其他问题,请:
1. 检查 MySQL 错误日志
2. 确认防火墙和网络配置
3. 查看项目 issues: https://github.com/handsomezhuzhu/QQuiz/issues

View File

@@ -1,21 +1,38 @@
/** /**
* Admin Settings Page * Admin Settings Page - Enhanced with API Configuration
*/ */
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { adminAPI } from '../api/client' import { adminAPI } from '../api/client'
import { useAuth } from '../context/AuthContext' import { useAuth } from '../context/AuthContext'
import { Settings, Save, Loader } from 'lucide-react' import { Settings, Save, Loader, Key, Link as LinkIcon, Eye, EyeOff } from 'lucide-react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
export const AdminSettings = () => { export const AdminSettings = () => {
const { user } = useAuth() const { user } = useAuth()
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [showApiKeys, setShowApiKeys] = useState({
openai: false,
anthropic: false,
qwen: false
})
const [config, setConfig] = useState({ const [config, setConfig] = useState({
allow_registration: true, allow_registration: true,
max_upload_size_mb: 10, max_upload_size_mb: 10,
max_daily_uploads: 20, max_daily_uploads: 20,
ai_provider: 'openai' ai_provider: 'openai',
// OpenAI
openai_api_key: '',
openai_base_url: 'https://api.openai.com/v1',
openai_model: 'gpt-4o-mini',
// Anthropic
anthropic_api_key: '',
anthropic_model: 'claude-3-haiku-20240307',
// Qwen
qwen_api_key: '',
qwen_base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
qwen_model: 'qwen-plus'
}) })
useEffect(() => { useEffect(() => {
@@ -54,6 +71,36 @@ export const AdminSettings = () => {
}) })
} }
const toggleApiKeyVisibility = (provider) => {
setShowApiKeys({
...showApiKeys,
[provider]: !showApiKeys[provider]
})
}
// Get complete API endpoint URL
const getCompleteEndpoint = (provider) => {
const endpoints = {
openai: '/chat/completions',
anthropic: '/messages',
qwen: '/chat/completions'
}
let baseUrl = ''
if (provider === 'openai') {
baseUrl = config.openai_base_url || 'https://api.openai.com/v1'
} else if (provider === 'anthropic') {
baseUrl = 'https://api.anthropic.com/v1'
} else if (provider === 'qwen') {
baseUrl = config.qwen_base_url || 'https://dashscope.aliyuncs.com/compatible-mode/v1'
}
// Remove trailing slash
baseUrl = baseUrl.replace(/\/$/, '')
return `${baseUrl}${endpoints[provider]}`
}
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center"> <div className="min-h-screen bg-gray-100 flex items-center justify-center">
@@ -66,7 +113,7 @@ export const AdminSettings = () => {
<div className="min-h-screen bg-gray-100"> <div className="min-h-screen bg-gray-100">
{/* Header */} {/* Header */}
<div className="bg-white shadow"> <div className="bg-white shadow">
<div className="max-w-4xl mx-auto px-4 py-6"> <div className="max-w-5xl mx-auto px-4 py-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Settings className="h-8 w-8 text-primary-600" /> <Settings className="h-8 w-8 text-primary-600" />
<div> <div>
@@ -78,8 +125,11 @@ export const AdminSettings = () => {
</div> </div>
{/* Content */} {/* Content */}
<div className="max-w-4xl mx-auto px-4 py-8"> <div className="max-w-5xl mx-auto px-4 py-8 space-y-6">
{/* Basic Settings */}
<div className="bg-white rounded-xl shadow-md p-6 space-y-6"> <div className="bg-white rounded-xl shadow-md p-6 space-y-6">
<h2 className="text-xl font-bold text-gray-900 mb-4">基础设置</h2>
{/* Allow Registration */} {/* Allow Registration */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
@@ -144,31 +194,244 @@ export const AdminSettings = () => {
<option value="qwen">Qwen (通义千问)</option> <option value="qwen">Qwen (通义千问)</option>
</select> </select>
<p className="text-sm text-gray-500 mt-1"> <p className="text-sm text-gray-500 mt-1">
需在 .env 文件中配置对应的 API Key 选择后在下方配置对应的 API 密钥
</p>
</div>
</div>
{/* OpenAI Configuration */}
<div className={`bg-white rounded-xl shadow-md p-6 space-y-4 ${config.ai_provider === 'openai' ? 'ring-2 ring-primary-500' : ''}`}>
<div className="flex items-center gap-2">
<Key className="h-5 w-5 text-green-600" />
<h2 className="text-xl font-bold text-gray-900">OpenAI 配置</h2>
{config.ai_provider === 'openai' && (
<span className="px-2 py-1 text-xs font-medium bg-primary-100 text-primary-700 rounded-full">当前使用</span>
)}
</div>
{/* API Key */}
<div>
<label className="block font-medium text-gray-900 mb-2">
API Key
</label>
<div className="relative">
<input
type={showApiKeys.openai ? 'text' : 'password'}
value={config.openai_api_key || ''}
onChange={(e) => handleChange('openai_api_key', e.target.value)}
placeholder="sk-proj-..."
className="w-full px-4 py-2 pr-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm"
/>
<button
type="button"
onClick={() => toggleApiKeyVisibility('openai')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showApiKeys.openai ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
<p className="text-sm text-gray-500 mt-1"> https://platform.openai.com/api-keys 获取</p>
</div>
{/* Base URL */}
<div>
<label className="block font-medium text-gray-900 mb-2">
Base URL
</label>
<div className="relative">
<LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
value={config.openai_base_url}
onChange={(e) => handleChange('openai_base_url', e.target.value)}
placeholder="https://api.openai.com/v1"
className="w-full pl-10 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm"
/>
</div>
<p className="text-xs text-gray-500 mt-1">
完整 endpoint: <code className="bg-gray-100 px-2 py-0.5 rounded">{getCompleteEndpoint('openai')}</code>
</p> </p>
</div> </div>
{/* Save Button */} {/* Model */}
<div className="pt-4"> <div>
<button <label className="block font-medium text-gray-900 mb-2">
onClick={handleSave} 模型
disabled={saving} </label>
className="w-full bg-primary-600 text-white py-3 rounded-lg font-medium hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2" <select
value={config.openai_model}
onChange={(e) => handleChange('openai_model', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
> >
{saving ? ( <option value="gpt-4o">gpt-4o (最强)</option>
<> <option value="gpt-4o-mini">gpt-4o-mini (推荐)</option>
<Loader className="h-5 w-5 animate-spin" /> <option value="gpt-4-turbo">gpt-4-turbo</option>
保存中... <option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
</> </select>
) : (
<>
<Save className="h-5 w-5" />
保存设置
</>
)}
</button>
</div> </div>
</div> </div>
{/* Anthropic Configuration */}
<div className={`bg-white rounded-xl shadow-md p-6 space-y-4 ${config.ai_provider === 'anthropic' ? 'ring-2 ring-primary-500' : ''}`}>
<div className="flex items-center gap-2">
<Key className="h-5 w-5 text-orange-600" />
<h2 className="text-xl font-bold text-gray-900">Anthropic 配置</h2>
{config.ai_provider === 'anthropic' && (
<span className="px-2 py-1 text-xs font-medium bg-primary-100 text-primary-700 rounded-full">当前使用</span>
)}
</div>
{/* API Key */}
<div>
<label className="block font-medium text-gray-900 mb-2">
API Key
</label>
<div className="relative">
<input
type={showApiKeys.anthropic ? 'text' : 'password'}
value={config.anthropic_api_key || ''}
onChange={(e) => handleChange('anthropic_api_key', e.target.value)}
placeholder="sk-ant-..."
className="w-full px-4 py-2 pr-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm"
/>
<button
type="button"
onClick={() => toggleApiKeyVisibility('anthropic')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showApiKeys.anthropic ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
<p className="text-sm text-gray-500 mt-1"> https://console.anthropic.com/settings/keys 获取</p>
</div>
{/* Base URL (fixed for Anthropic) */}
<div>
<label className="block font-medium text-gray-900 mb-2">
Base URL (固定)
</label>
<div className="relative">
<LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
value="https://api.anthropic.com/v1"
disabled
className="w-full pl-10 px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 font-mono text-sm"
/>
</div>
<p className="text-xs text-gray-500 mt-1">
完整 endpoint: <code className="bg-gray-100 px-2 py-0.5 rounded">{getCompleteEndpoint('anthropic')}</code>
</p>
</div>
{/* Model */}
<div>
<label className="block font-medium text-gray-900 mb-2">
模型
</label>
<select
value={config.anthropic_model}
onChange={(e) => handleChange('anthropic_model', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
<option value="claude-3-5-sonnet-20241022">claude-3-5-sonnet (最强)</option>
<option value="claude-3-haiku-20240307">claude-3-haiku (推荐)</option>
<option value="claude-3-opus-20240229">claude-3-opus</option>
</select>
</div>
</div>
{/* Qwen Configuration */}
<div className={`bg-white rounded-xl shadow-md p-6 space-y-4 ${config.ai_provider === 'qwen' ? 'ring-2 ring-primary-500' : ''}`}>
<div className="flex items-center gap-2">
<Key className="h-5 w-5 text-blue-600" />
<h2 className="text-xl font-bold text-gray-900">通义千问 配置</h2>
{config.ai_provider === 'qwen' && (
<span className="px-2 py-1 text-xs font-medium bg-primary-100 text-primary-700 rounded-full">当前使用</span>
)}
</div>
{/* API Key */}
<div>
<label className="block font-medium text-gray-900 mb-2">
API Key
</label>
<div className="relative">
<input
type={showApiKeys.qwen ? 'text' : 'password'}
value={config.qwen_api_key || ''}
onChange={(e) => handleChange('qwen_api_key', e.target.value)}
placeholder="sk-..."
className="w-full px-4 py-2 pr-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm"
/>
<button
type="button"
onClick={() => toggleApiKeyVisibility('qwen')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showApiKeys.qwen ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
<p className="text-sm text-gray-500 mt-1"> https://dashscope.console.aliyun.com/apiKey 获取</p>
</div>
{/* Base URL */}
<div>
<label className="block font-medium text-gray-900 mb-2">
Base URL
</label>
<div className="relative">
<LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
value={config.qwen_base_url}
onChange={(e) => handleChange('qwen_base_url', e.target.value)}
placeholder="https://dashscope.aliyuncs.com/compatible-mode/v1"
className="w-full pl-10 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono text-sm"
/>
</div>
<p className="text-xs text-gray-500 mt-1">
完整 endpoint: <code className="bg-gray-100 px-2 py-0.5 rounded">{getCompleteEndpoint('qwen')}</code>
</p>
</div>
{/* Model */}
<div>
<label className="block font-medium text-gray-900 mb-2">
模型
</label>
<select
value={config.qwen_model}
onChange={(e) => handleChange('qwen_model', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
<option value="qwen-max">qwen-max (最强)</option>
<option value="qwen-plus">qwen-plus (推荐)</option>
<option value="qwen-turbo">qwen-turbo</option>
</select>
</div>
</div>
{/* Save Button */}
<div className="bg-white rounded-xl shadow-md p-6">
<button
onClick={handleSave}
disabled={saving}
className="w-full bg-primary-600 text-white py-3 rounded-lg font-medium hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{saving ? (
<>
<Loader className="h-5 w-5 animate-spin" />
保存中...
</>
) : (
<>
<Save className="h-5 w-5" />
保存所有设置
</>
)}
</button>
</div>
</div> </div>
</div> </div>
) )

View File

@@ -1,5 +1,5 @@
@echo off @echo off
REM 激活虚拟环境的批处理脚本 REM 激活虚拟环境的批处理脚本
cd /d "%~dp0backend" cd /d "%~dp0..\backend"
call venv\Scripts\activate.bat call venv\Scripts\activate.bat
cmd /k cmd /k

View File

@@ -17,7 +17,7 @@ echo 全自动部署脚本
echo ========================================== echo ==========================================
echo. echo.
cd /d "%~dp0" cd /d "%~dp0.."
REM ============================================ REM ============================================
REM 步骤 1: 检查环境 REM 步骤 1: 检查环境
@@ -197,7 +197,7 @@ REM ============================================
echo [7/8] 启动后端服务... echo [7/8] 启动后端服务...
echo. echo.
start "QQuiz Backend" cmd /k "cd /d %~dp0backend && call venv\Scripts\activate.bat && echo ======================================== && echo QQuiz 后端服务 && echo ======================================== && echo. && echo API 地址: http://localhost:8000 && echo API 文档: http://localhost:8000/docs && echo. && echo 按 Ctrl+C 停止服务 && echo. && uvicorn main:app --reload" start "QQuiz Backend" cmd /k "cd /d %~dp0..\backend && call venv\Scripts\activate.bat && echo ======================================== && echo QQuiz 后端服务 && echo ======================================== && echo. && echo API 地址: http://localhost:8000 && echo API 文档: http://localhost:8000/docs && echo. && echo 按 Ctrl+C 停止服务 && echo. && uvicorn main:app --reload"
echo ✓ 后端服务已在新窗口中启动 echo ✓ 后端服务已在新窗口中启动
echo 等待服务启动... echo 等待服务启动...
@@ -210,7 +210,7 @@ REM ============================================
echo [8/8] 启动前端服务... echo [8/8] 启动前端服务...
echo. echo.
start "QQuiz Frontend" cmd /k "cd /d %~dp0frontend && echo ======================================== && echo QQuiz 前端服务 && echo ======================================== && echo. && echo 前端地址: http://localhost:3000 && echo. && echo 按 Ctrl+C 停止服务 && echo. && npm start" start "QQuiz Frontend" cmd /k "cd /d %~dp0..\frontend && echo ======================================== && echo QQuiz 前端服务 && echo ======================================== && echo. && echo 前端地址: http://localhost:3000 && echo. && echo 按 Ctrl+C 停止服务 && echo. && npm start"
echo ✓ 前端服务已在新窗口中启动 echo ✓ 前端服务已在新窗口中启动
echo 等待服务启动... echo 等待服务启动...

View File

@@ -0,0 +1,93 @@
@echo off
title PostgreSQL Status Check
color 0B
echo.
echo ========================================
echo PostgreSQL Database Status Check
echo ========================================
echo.
REM Check PostgreSQL installation
echo [1] Checking PostgreSQL Installation...
if exist "C:\Program Files\PostgreSQL\18" (
echo OK - PostgreSQL 18 is installed
echo Location: C:\Program Files\PostgreSQL\18
) else (
echo ERROR - PostgreSQL 18 not found!
pause
exit /b 1
)
echo.
REM Check PostgreSQL service
echo [2] Checking PostgreSQL Service...
sc query postgresql-x64-18 >nul 2>&1
if %errorlevel% equ 0 (
echo OK - PostgreSQL service exists
echo.
echo Service details:
sc query postgresql-x64-18
echo.
) else (
echo WARNING - PostgreSQL service not found!
echo Trying alternative names...
sc query | findstr /i "postgres"
)
echo.
REM Check if port 5432 is listening
echo [3] Checking Port 5432...
netstat -ano | findstr ":5432" | findstr "LISTENING" >nul
if %errorlevel% equ 0 (
echo OK - PostgreSQL is listening on port 5432
netstat -ano | findstr ":5432" | findstr "LISTENING"
) else (
echo ERROR - Port 5432 is NOT listening!
echo PostgreSQL service is probably not running.
echo.
echo To start the service, you can:
echo 1. Open Services (services.msc)
echo 2. Find "postgresql-x64-18" service
echo 3. Right-click and select "Start"
echo.
echo OR run: net start postgresql-x64-18
)
echo.
REM Try to connect to database
echo [4] Testing Database Connection...
set PGPASSWORD=postgres
"C:\Program Files\PostgreSQL\18\pgAdmin 4\runtime\psql.exe" -h localhost -U postgres -c "SELECT version();" postgres >nul 2>&1
if %errorlevel% equ 0 (
echo OK - Successfully connected to PostgreSQL!
echo.
"C:\Program Files\PostgreSQL\18\pgAdmin 4\runtime\psql.exe" -h localhost -U postgres -c "SELECT version();" postgres
echo.
REM Check if qquiz database exists
echo [5] Checking QQuiz Database...
"C:\Program Files\PostgreSQL\18\pgAdmin 4\runtime\psql.exe" -h localhost -U postgres -c "\l" postgres | findstr "qquiz_db" >nul
if %errorlevel% equ 0 (
echo OK - qquiz_db database exists
) else (
echo INFO - qquiz_db database does not exist yet
echo This is normal for first-time setup
)
) else (
echo ERROR - Cannot connect to PostgreSQL!
echo.
echo Possible reasons:
echo 1. PostgreSQL service is not running
echo 2. Default password 'postgres' is incorrect
echo 3. PostgreSQL is not configured to accept local connections
echo.
echo Please start the PostgreSQL service first!
)
echo.
echo ========================================
echo Check Complete
echo ========================================
echo.
pause

View File

@@ -1,6 +1,8 @@
@echo off @echo off
title QQuiz - System Status Check title QQuiz - System Status Check
cd /d "%~dp0.."
echo. echo.
echo ======================================== echo ========================================
echo QQuiz System Status Check echo QQuiz System Status Check

View File

@@ -8,7 +8,7 @@ echo QQuiz - Automatic Fix and Start
echo ======================================== echo ========================================
echo. echo.
cd /d "%~dp0" cd /d "%~dp0.."
REM Check if .env exists REM Check if .env exists
if not exist ".env" ( if not exist ".env" (
@@ -50,28 +50,28 @@ if %errorlevel% equ 1 (
exit /b 1 exit /b 1
) )
echo Starting PostgreSQL in Docker... echo Starting MySQL in Docker...
docker-compose up -d postgres docker-compose up -d mysql
if %errorlevel% neq 0 ( if %errorlevel% neq 0 (
echo. echo.
echo Docker failed to start. Trying to fix... echo Docker failed to start. Trying to fix...
docker-compose down docker-compose down
docker-compose up -d postgres docker-compose up -d mysql
) )
echo Waiting for database... echo Waiting for database...
timeout /t 8 /nobreak >nul timeout /t 10 /nobreak >nul
) else ( ) else (
echo. echo.
echo Using Local PostgreSQL... echo Using Local MySQL...
echo. echo.
echo Make sure PostgreSQL is running on port 5432 echo Make sure MySQL is running on port 3306
echo. echo.
echo If you see connection errors, you need to: echo If you see connection errors, you need to:
echo 1. Start PostgreSQL service echo 1. Start MySQL service
echo 2. Or install PostgreSQL from https://www.postgresql.org/download/ echo 2. Or install MySQL from https://dev.mysql.com/downloads/installer/
echo 3. Or choose option 1 to use Docker instead echo 3. Or choose option 1 to use Docker instead
echo. echo.
pause pause
@@ -111,11 +111,11 @@ echo.
echo Starting services... echo Starting services...
echo. echo.
start "QQuiz Backend" cmd /k "cd /d %~dp0backend && call venv\Scripts\activate.bat && echo Backend: http://localhost:8000 && echo Docs: http://localhost:8000/docs && echo. && uvicorn main:app --reload" 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 timeout /t 8 /nobreak >nul
start "QQuiz Frontend" cmd /k "cd /d %~dp0frontend && echo Frontend: http://localhost:3000 && echo. && npm start" start "QQuiz Frontend" cmd /k "cd /d %~dp0..frontend && echo Frontend: http://localhost:3000 && echo. && npm start"
echo. echo.
echo ======================================== echo ========================================

View File

@@ -2,6 +2,8 @@
chcp 65001 >nul chcp 65001 >nul
title QQuiz - 查看日志 title QQuiz - 查看日志
cd /d "%~dp0.."
echo. echo.
echo ========================================== echo ==========================================
echo QQuiz 服务日志 echo QQuiz 服务日志

View File

@@ -8,7 +8,7 @@ echo 推送 QQuiz 到 GitHub
echo ========================================== echo ==========================================
echo. echo.
cd /d "%~dp0" cd /d "%~dp0.."
REM 检查是否有远程仓库 REM 检查是否有远程仓库
git remote -v | findstr "origin" >nul git remote -v | findstr "origin" >nul
@@ -48,7 +48,7 @@ echo [重要] 如果是首次推送,需要输入 GitHub 认证:
echo Username: handsomezhuzhu echo Username: handsomezhuzhu
echo Password: 使用 Personal Access Token (不是密码!) echo Password: 使用 Personal Access Token (不是密码!)
echo. echo.
echo 如何获取 Token: 参考 GITHUB_PUSH_GUIDE.md echo 如何获取 Token: 参考 docs/GITHUB_PUSH_GUIDE.md
echo. echo.
pause pause
@@ -74,7 +74,7 @@ if %errorlevel% equ 0 (
echo 3. 网络连接问题 echo 3. 网络连接问题
echo. echo.
echo 解决方案: echo 解决方案:
echo 1. 阅读 GITHUB_PUSH_GUIDE.md 配置认证 echo 1. 阅读 docs/GITHUB_PUSH_GUIDE.md 配置认证
echo 2. 确认 Personal Access Token 有效 echo 2. 确认 Personal Access Token 有效
echo 3. 检查网络连接 echo 3. 检查网络连接
echo. echo.

View File

@@ -8,7 +8,7 @@ echo QQuiz 快速配置
echo ========================================== echo ==========================================
echo. echo.
cd /d "%~dp0" cd /d "%~dp0.."
REM 创建 .env 文件 REM 创建 .env 文件
echo 正在创建配置文件... echo 正在创建配置文件...
@@ -51,6 +51,6 @@ pause >nul
notepad .env notepad .env
echo. echo.
echo 配置完成!现在可以运行 auto_setup_and_run.bat 启动系统 echo 配置完成!现在可以运行 scripts\auto_setup_and_run.bat 启动系统
echo. echo.
pause pause

View File

@@ -0,0 +1,31 @@
@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

View File

@@ -0,0 +1,47 @@
@echo off
title QQuiz - Restart Docker Services
color 0B
cd /d "%~dp0.."
echo.
echo ========================================
echo Restarting QQuiz Docker Services
echo ========================================
echo.
echo [1/4] Stopping existing containers...
docker-compose down
echo OK
echo.
echo [2/4] Rebuilding backend image...
docker-compose build backend --no-cache
echo OK
echo.
echo [3/4] Starting all services...
docker-compose up -d
echo OK
echo.
echo [4/4] Waiting for services to be ready...
timeout /t 15 /nobreak >nul
echo.
echo ========================================
echo Services Restarted!
echo ========================================
echo.
echo Checking service status...
docker-compose ps
echo.
echo Frontend: http://localhost:3000
echo Backend: http://localhost:8000
echo Database: MySQL on port 3306
echo.
echo Login: admin / admin123
echo.
pause

View File

@@ -8,7 +8,7 @@ echo QQuiz Backend - 本地启动
echo ========================================== echo ==========================================
echo. echo.
cd /d "%~dp0backend" cd /d "%~dp0..\backend"
REM 检查虚拟环境是否存在 REM 检查虚拟环境是否存在
if not exist "venv\Scripts\activate.bat" ( if not exist "venv\Scripts\activate.bat" (

View File

@@ -8,7 +8,7 @@ echo QQuiz Frontend - 本地启动
echo ========================================== echo ==========================================
echo. echo.
cd /d "%~dp0frontend" cd /d "%~dp0..\frontend"
REM 检查 node_modules 是否存在 REM 检查 node_modules 是否存在
if not exist "node_modules" ( if not exist "node_modules" (

View File

@@ -1,11 +1,11 @@
@echo off @echo off
cd /d "%~dp0" cd /d "%~dp0.."
echo Creating .env configuration file... echo Creating .env configuration file...
echo. echo.
( (
echo DATABASE_URL=postgresql+asyncpg://qquiz:qquiz_password@localhost:5432/qquiz_db 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 SECRET_KEY=qquiz-secret-key-for-development-change-in-production-32chars
echo AI_PROVIDER=openai echo AI_PROVIDER=openai
echo OPENAI_API_KEY=sk-your-openai-api-key-here echo OPENAI_API_KEY=sk-your-openai-api-key-here

View File

@@ -8,7 +8,7 @@ echo QQuiz - Auto Deploy Script
echo ======================================== echo ========================================
echo. echo.
cd /d "%~dp0" cd /d "%~dp0.."
REM Check if .env exists REM Check if .env exists
if not exist ".env" ( if not exist ".env" (
@@ -40,17 +40,13 @@ if %errorlevel% neq 0 (
echo OK - Node.js installed echo OK - Node.js installed
echo. echo.
echo [3/7] Creating PostgreSQL database... echo [3/7] Creating MySQL database...
echo. echo.
echo Please enter PostgreSQL admin password when prompted echo Please enter MySQL root password when prompted
echo (Default password is usually: postgres) echo (Leave blank if no password set)
echo. echo.
set PGPASSWORD=postgres mysql -u root -p -e "DROP DATABASE IF EXISTS qquiz_db; DROP USER IF EXISTS 'qquiz'@'localhost'; CREATE DATABASE qquiz_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'qquiz'@'localhost' IDENTIFIED BY 'qquiz_password'; GRANT ALL PRIVILEGES ON qquiz_db.* TO 'qquiz'@'localhost'; FLUSH PRIVILEGES;" 2>nul
psql -U postgres -h localhost -c "DROP DATABASE IF EXISTS qquiz_db;" 2>nul
psql -U postgres -h localhost -c "DROP USER IF EXISTS qquiz;" 2>nul
psql -U postgres -h localhost -c "CREATE USER qquiz WITH PASSWORD 'qquiz_password';" 2>nul
psql -U postgres -h localhost -c "CREATE DATABASE qquiz_db OWNER qquiz;" 2>nul
echo Database setup complete echo Database setup complete
echo. echo.
@@ -88,11 +84,11 @@ echo.
echo [7/7] Starting services... echo [7/7] Starting services...
echo. echo.
start "QQuiz Backend" cmd /k "cd /d %~dp0backend && call venv\Scripts\activate.bat && echo Backend API: http://localhost:8000 && echo API Docs: http://localhost:8000/docs && echo. && uvicorn main:app --reload" start "QQuiz Backend" cmd /k "cd /d %~dp0..\backend && call venv\Scripts\activate.bat && echo Backend API: http://localhost:8000 && echo API Docs: http://localhost:8000/docs && echo. && uvicorn main:app --reload"
timeout /t 5 /nobreak >nul timeout /t 5 /nobreak >nul
start "QQuiz Frontend" cmd /k "cd /d %~dp0frontend && echo Frontend: http://localhost:3000 && echo. && npm start" start "QQuiz Frontend" cmd /k "cd /d %~dp0..\frontend && echo Frontend: http://localhost:3000 && echo. && npm start"
timeout /t 3 /nobreak >nul timeout /t 3 /nobreak >nul

View File

@@ -8,7 +8,7 @@ echo Starting QQuiz Backend Server
echo ======================================== echo ========================================
echo. echo.
cd /d "%~dp0backend" cd /d "%~dp0..\backend"
if not exist "venv\Scripts\activate.bat" ( if not exist "venv\Scripts\activate.bat" (
echo Creating virtual environment... echo Creating virtual environment...

View File

@@ -8,7 +8,7 @@ echo Starting QQuiz Frontend Server
echo ======================================== echo ========================================
echo. echo.
cd /d "%~dp0frontend" cd /d "%~dp0..\frontend"
if not exist "node_modules" ( if not exist "node_modules" (
echo Installing dependencies (first time only)... echo Installing dependencies (first time only)...

View File

@@ -0,0 +1,44 @@
@echo off
title Start PostgreSQL Service
color 0B
echo.
echo ========================================
echo Starting PostgreSQL Service
echo ========================================
echo.
echo Attempting to start PostgreSQL service...
echo.
net start postgresql-x64-18
if %errorlevel% equ 0 (
echo.
echo ========================================
echo SUCCESS!
echo ========================================
echo.
echo PostgreSQL service is now running
echo Port: 5432
echo.
echo You can now run: scripts\fix_and_start.bat
echo.
) else (
echo.
echo ========================================
echo Failed to start service
echo ========================================
echo.
echo Please try starting manually:
echo 1. Press Win+R
echo 2. Type: services.msc
echo 3. Find "postgresql-x64-18"
echo 4. Right-click and select "Start"
echo.
echo OR run this script as Administrator:
echo Right-click this bat file and select "Run as administrator"
echo.
)
pause

View File

@@ -2,6 +2,8 @@
chcp 65001 >nul chcp 65001 >nul
title QQuiz - 启动服务 title QQuiz - 启动服务
cd /d "%~dp0.."
echo. echo.
echo ========================================== echo ==========================================
echo QQuiz - 智能刷题与题库管理平台 echo QQuiz - 智能刷题与题库管理平台

View File

@@ -2,6 +2,8 @@
chcp 65001 >nul chcp 65001 >nul
title QQuiz - 启动服务 (国内优化版) title QQuiz - 启动服务 (国内优化版)
cd /d "%~dp0.."
echo. echo.
echo ========================================== echo ==========================================
echo QQuiz - 智能刷题与题库管理平台 echo QQuiz - 智能刷题与题库管理平台
@@ -56,7 +58,7 @@ if %errorlevel% neq 0 (
if %errorlevel% equ 1 ( if %errorlevel% equ 1 (
echo. echo.
echo 请按照提示配置 Docker Desktop 镜像加速器... echo 请按照提示配置 Docker Desktop 镜像加速器...
call setup_docker_mirror.bat call scripts\setup_docker_mirror.bat
echo. echo.
echo 配置完成后,请重新运行此脚本 echo 配置完成后,请重新运行此脚本
pause pause
@@ -90,9 +92,9 @@ if %errorlevel% neq 0 (
echo 3. 配置错误 - 检查 .env 文件 echo 3. 配置错误 - 检查 .env 文件
echo. echo.
echo 解决方案: echo 解决方案:
echo 1. 配置镜像加速器: 运行 setup_docker_mirror.bat echo 1. 配置镜像加速器: 运行 scripts\setup_docker_mirror.bat
echo 2. 查看详细错误: docker-compose logs echo 2. 查看详细错误: docker-compose logs
echo 3. 阅读文档: DOCKER_MIRROR_SETUP.md echo 3. 阅读文档: docs\DOCKER_MIRROR_SETUP.md
echo. echo.
pause pause
exit /b 1 exit /b 1

View File

@@ -8,7 +8,7 @@ echo QQuiz - Starting with Docker DB
echo ======================================== echo ========================================
echo. echo.
cd /d "%~dp0" cd /d "%~dp0.."
echo [1/4] Checking Docker... echo [1/4] Checking Docker...
docker --version >nul 2>&1 docker --version >nul 2>&1
@@ -21,31 +21,32 @@ if %errorlevel% neq 0 (
echo OK - Docker installed echo OK - Docker installed
echo. echo.
echo [2/4] Starting PostgreSQL in Docker... echo [2/4] Starting MySQL in Docker...
docker-compose up -d postgres docker-compose up -d mysql
if %errorlevel% neq 0 ( if %errorlevel% neq 0 (
echo ERROR: Failed to start PostgreSQL echo ERROR: Failed to start MySQL
echo Try: docker-compose down echo Try: docker-compose down
echo Then run this script again echo Then run this script again
pause pause
exit /b 1 exit /b 1
) )
echo OK - PostgreSQL started echo OK - MySQL started
echo Waiting for database to be ready... echo Waiting for database to be ready...
timeout /t 5 /nobreak >nul echo.
timeout /t 10 /nobreak >nul
echo. echo.
echo [3/4] Starting Backend... echo [3/4] Starting Backend...
start "QQuiz Backend" cmd /k "cd /d %~dp0backend && 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" 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... echo Waiting for backend to start...
timeout /t 8 /nobreak >nul timeout /t 8 /nobreak >nul
echo. echo.
echo [4/4] Starting Frontend... echo [4/4] Starting Frontend...
start "QQuiz Frontend" cmd /k "cd /d %~dp0frontend && echo ======================================== && echo QQuiz Frontend Server && echo ======================================== && echo. && echo URL: http://localhost:3000 && echo. && npm start" 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 ======================================== echo ========================================

View File

@@ -2,6 +2,8 @@
chcp 65001 >nul chcp 65001 >nul
title QQuiz - 停止服务 title QQuiz - 停止服务
cd /d "%~dp0.."
echo. echo.
echo ========================================== echo ==========================================
echo 停止 QQuiz 服务 echo 停止 QQuiz 服务

View File

@@ -0,0 +1,15 @@
@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