docker
This commit is contained in:
parent
a39fd6186c
commit
8a35c0b97f
10
backend/.dockerignore
Normal file
10
backend/.dockerignore
Normal file
@ -0,0 +1,10 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.env
|
||||
*.db
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
alembic/versions/
|
||||
@ -1,33 +1,33 @@
|
||||
# Service
|
||||
CH_HOST=0.0.0.0
|
||||
CH_PORT=8000
|
||||
CH_DEBUG=true
|
||||
CH_DEBUG=false
|
||||
|
||||
# Database
|
||||
CH_DATABASE_URL=sqlite+aiosqlite:///./classhub.db
|
||||
# Database (Docker 部署时使用 volume 路径)
|
||||
CH_DATABASE_URL=sqlite+aiosqlite:///./data/classhub.db
|
||||
|
||||
# JWT
|
||||
# JWT (务必修改 secret)
|
||||
CH_JWT_SECRET=change-me-in-production
|
||||
CH_JWT_EXPIRY_HOURS=72
|
||||
|
||||
# Tencent COS
|
||||
CH_COS_SECRET_ID=AKIDUxR3TfeWDbqFQQDn9INJ1U8annY7TbWN
|
||||
CH_COS_SECRET_KEY=jJitIUTFyf5WvDPXiigS2PaaMtCJSQCn
|
||||
# Tencent COS (对象存储)
|
||||
CH_COS_SECRET_ID=your-cos-secret-id
|
||||
CH_COS_SECRET_KEY=your-cos-secret-key
|
||||
CH_COS_REGION=ap-guangzhou
|
||||
CH_COS_BUCKET=hku-icb-1311994147
|
||||
CH_COS_BASE_URL=https://hku-icb-1311994147.cos.ap-guangzhou.myqcloud.com
|
||||
CH_COS_BUCKET=your-bucket-name
|
||||
CH_COS_BASE_URL=https://your-bucket.cos.ap-guangzhou.myqcloud.com
|
||||
|
||||
# SMTP Email
|
||||
CH_SMTP_HOST=gz-smtp.qcloudmail.com
|
||||
# SMTP Email (邮件通知)
|
||||
CH_SMTP_HOST=smtp.example.com
|
||||
CH_SMTP_PORT=465
|
||||
CH_SMTP_USER=noreply@hkuicb.info
|
||||
CH_SMTP_PASSWORD=hX2gqEPjaRhULXF69
|
||||
CH_SMTP_FROM_EMAIL=noreply@hkuicb.info
|
||||
CH_SMTP_USER=noreply@example.com
|
||||
CH_SMTP_PASSWORD=your-smtp-password
|
||||
CH_SMTP_FROM_EMAIL=noreply@example.com
|
||||
CH_SMTP_FROM_NAME=HKU ICB Class Hub
|
||||
|
||||
# Frontend URL
|
||||
CH_FRONTEND_URL=http://localhost:3000
|
||||
# Frontend URL (CORS 用)
|
||||
CH_FRONTEND_URL=http://your-server-ip
|
||||
|
||||
# Super Admin Seed
|
||||
CH_SUPER_ADMIN_EMAIL=admin@hkuicb.info
|
||||
CH_SUPER_ADMIN_PASSWORD=admin123
|
||||
# Super Admin Seed (首次启动自动创建)
|
||||
CH_SUPER_ADMIN_EMAIL=admin@example.com
|
||||
CH_SUPER_ADMIN_PASSWORD=change-me-please
|
||||
|
||||
17
backend/Dockerfile
Normal file
17
backend/Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies first (layer caching)
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Create data directory for SQLite
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@ -1,9 +1,11 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.core.deps import require_role
|
||||
from app.db.database import get_db
|
||||
from app.db.models import User
|
||||
from app.db.models import User, Class_
|
||||
from app.schemas.assignment import (
|
||||
AssignmentCreate, AssignmentUpdate, AssignmentOut,
|
||||
AssignmentDetailOut, SubmissionGrade, SubmissionOut,
|
||||
@ -26,7 +28,15 @@ from app.services.cos_service import upload_file
|
||||
router = APIRouter(prefix="/api/assignments", tags=["assignments"])
|
||||
|
||||
|
||||
def _build_assignment_out(a: any, user_id: int) -> AssignmentOut:
|
||||
async def _get_roster_count(db: AsyncSession, class_id: int) -> int:
|
||||
from app.db.models import StudentRoster
|
||||
result = await db.execute(
|
||||
select(func.count(StudentRoster.id)).where(StudentRoster.class_id == class_id)
|
||||
)
|
||||
return result.scalar() or 0
|
||||
|
||||
|
||||
def _build_assignment_out(a: any, user_id: int, total_members: int = 0) -> AssignmentOut:
|
||||
submission_count = len(a.submissions) if a.submissions else 0
|
||||
my_submitted = any(s.student_id == user_id for s in (a.submissions or []))
|
||||
return AssignmentOut(
|
||||
@ -40,6 +50,7 @@ def _build_assignment_out(a: any, user_id: int) -> AssignmentOut:
|
||||
attachment_urls=a.get_attachment_urls_list(),
|
||||
status=a.status,
|
||||
submission_count=submission_count,
|
||||
total_members=total_members,
|
||||
my_submitted=my_submitted,
|
||||
created_at=a.created_at,
|
||||
updated_at=a.updated_at,
|
||||
@ -79,7 +90,8 @@ async def get_assignments(
|
||||
|
||||
assignments, total = await list_assignments(db, effective_class_id, page, page_size)
|
||||
total_pages = (total + page_size - 1) // page_size
|
||||
items = [_build_assignment_out(a, user.id) for a in assignments]
|
||||
roster_count = await _get_roster_count(db, effective_class_id)
|
||||
items = [_build_assignment_out(a, user.id, roster_count) for a in assignments]
|
||||
return PageResponse(items=items, total=total, page=page, page_size=page_size, total_pages=total_pages)
|
||||
|
||||
|
||||
@ -95,7 +107,8 @@ async def create_new_assignment(
|
||||
raise HTTPException(status_code=400, detail="You are not assigned to a class")
|
||||
|
||||
assignment = await create_assignment(db, effective_class_id, user.id, data)
|
||||
return _build_assignment_out(assignment, user.id)
|
||||
roster_count = await _get_roster_count(db, effective_class_id)
|
||||
return _build_assignment_out(assignment, user.id, roster_count)
|
||||
|
||||
|
||||
@router.post("/{assignment_id}/attachments")
|
||||
@ -138,7 +151,7 @@ async def get_assignment_detail(
|
||||
if user.role != "super_admin" and assignment.class_id != user.class_id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
base = _build_assignment_out(assignment, user.id)
|
||||
base = _build_assignment_out(assignment, user.id, await _get_roster_count(db, assignment.class_id))
|
||||
|
||||
# Student only sees their own submission
|
||||
if user.role == "student":
|
||||
@ -168,7 +181,7 @@ async def update_existing_assignment(
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
updated = await update_assignment(db, assignment, data)
|
||||
return _build_assignment_out(updated, user.id)
|
||||
return _build_assignment_out(updated, user.id, await _get_roster_count(db, updated.class_id))
|
||||
|
||||
|
||||
@router.delete("/{assignment_id}")
|
||||
|
||||
@ -53,6 +53,7 @@ class AssignmentOut(BaseModel):
|
||||
attachment_urls: list[str] | None
|
||||
status: str
|
||||
submission_count: int
|
||||
total_members: int
|
||||
my_submitted: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
42
docker-compose.yml
Normal file
42
docker-compose.yml
Normal file
@ -0,0 +1,42 @@
|
||||
services:
|
||||
backend:
|
||||
build: ./backend
|
||||
restart: unless-stopped
|
||||
env_file: ./backend/.env
|
||||
environment:
|
||||
- CH_DATABASE_URL=sqlite+aiosqlite:///./data/classhub.db
|
||||
- CH_FRONTEND_URL=http://localhost
|
||||
volumes:
|
||||
- classhub-data:/app/data
|
||||
expose:
|
||||
- "8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
args:
|
||||
- NEXT_PUBLIC_API_URL=
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "3000"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- backend
|
||||
- frontend
|
||||
|
||||
volumes:
|
||||
classhub-data:
|
||||
29
nginx/nginx.conf
Normal file
29
nginx/nginx.conf
Normal file
@ -0,0 +1,29 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 20m;
|
||||
|
||||
# API requests → backend
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Frontend
|
||||
location / {
|
||||
proxy_pass http://frontend:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Next.js HMR (hot module replacement) WebSocket
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user