文章目录

系统架构设计
技术栈选择
- 后端: FastAPI (Python 3.10+)
- 前端: Vue 3 + TypeScript + Element Plus
- 数据库: PostgreSQL 14
- 文件存储: MinIO (对象存储)
- 实时通信: WebSocket
- 搜索引擎: Elasticsearch (作业内容搜索)
- 部署: Docker + Kubernetes (可选)
系统模块划分
.
├── backend
│ ├── app
│ │ ├── auth (认证授权)
│ │ ├── crud (数据库操作)
│ │ ├── db (数据库连接)
│ │ ├── models (ORM模型)
│ │ ├── schemas (Pydantic模型)
│ │ ├── services (核心业务逻辑)
│ │ ├── static (静态文件)
│ │ ├── utils (工具函数)
│ │ └── websocket (实时通信)
│ └── main.py
└── frontend
├── src
│ ├── api (接口封装)
│ ├── assets (静态资源)
│ ├── components (公共组件)
│ ├── composables (组合式函数)
│ ├── router (路由)
│ ├── stores (Pinia状态管理)
│ ├── types (TS类型定义)
│ ├── utils (工具函数)
│ └── views (页面组件)
└── vite.config.ts
数据库设计
核心数据表结构
- 用户基础表(users)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
email VARCHAR(100) UNIQUE,
phone VARCHAR(20),
avatar_url VARCHAR(255),
role ENUM('admin', 'teacher', 'student') NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
last_login TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
- 管理员扩展表(admins)
CREATE TABLE admins (
user_id INT PRIMARY KEY REFERENCES users(id),
department VARCHAR(100),
position VARCHAR(100),
permissions JSONB
);
- 教师扩展表(teachers)
CREATE TABLE teachers (
user_id INT PRIMARY KEY REFERENCES users(id),
teacher_no VARCHAR(20) UNIQUE,
department VARCHAR(100),
position VARCHAR(50),
professional_title VARCHAR(50)
);
- 学生扩展表(students)
CREATE TABLE students (
user_id INT PRIMARY KEY REFERENCES users(id),
student_no VARCHAR(20) UNIQUE,
class_id INT REFERENCES classes(id),
enrollment_date DATE,
advisor_id INT REFERENCES teachers(user_id)
);
- 班级表(classes)
CREATE TABLE classes (
id SERIAL PRIMARY KEY,
class_name VARCHAR(50) NOT NULL,
grade VARCHAR(20),
major VARCHAR(100),
counselor_id INT REFERENCES teachers(user_id)
);
- 课程表(courses)
CREATE TABLE courses (
id SERIAL PRIMARY KEY,
course_code VARCHAR(20) UNIQUE NOT NULL,
course_name VARCHAR(100) NOT NULL,
credit NUMERIC(3,1),
hours INT,
semester VARCHAR(20),
teacher_id INT REFERENCES teachers(user_id)
);
- 课程-学生关联表(course_students)
CREATE TABLE course_students (
course_id INT REFERENCES courses(id),
student_id INT REFERENCES students(user_id),
PRIMARY KEY (course_id, student_id)
);
- 作业表(assignments)
CREATE TABLE assignments (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
description TEXT,
course_id INT REFERENCES courses(id),
teacher_id INT REFERENCES teachers(user_id),
deadline TIMESTAMP,
total_score NUMERIC(5,1),
attachment_url VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
- 作业提交表(submissions)
CREATE TABLE submissions (
id SERIAL PRIMARY KEY,
assignment_id INT REFERENCES assignments(id),
student_id INT REFERENCES students(user_id),
content TEXT,
attachment_url VARCHAR(255),
submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status ENUM('submitted', 'graded') DEFAULT 'submitted',
score NUMERIC(5,1),
feedback TEXT,
graded_at TIMESTAMP,
grader_id INT REFERENCES teachers(user_id)
);
- 平时成绩表(grades)
CREATE TABLE grades (
course_id INT REFERENCES courses(id),
student_id INT REFERENCES students(user_id),
assignment_score NUMERIC(5,1) DEFAULT 0,
participation_score NUMERIC(5,1) DEFAULT 0,
total_score NUMERIC(5,1) GENERATED ALWAYS AS (assignment_score + participation_score) STORED,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (course_id, student_id)
);
- 系统公告表(announcements)
CREATE TABLE announcements (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
publisher_id INT REFERENCES users(id),
is_top BOOLEAN DEFAULT FALSE,
status ENUM('draft', 'published') DEFAULT 'published',
publish_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expire_time TIMESTAMP
);
- 系统配置表(configurations)
CREATE TABLE configurations (
config_key VARCHAR(100) PRIMARY KEY,
config_value JSONB NOT NULL,
description TEXT
);
后端API实现 (FastAPI)
1. 认证授权模块
# auth/router.py
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from app.auth.schemas import Token, UserCreate
from app.auth.service import (
authenticate_user,
create_access_token,
get_current_active_user,
create_user
)
router = APIRouter(tags=["认证授权"])
@router.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=400, detail="用户名或密码错误")
access_token = create_access_token(data={"sub": user.username})
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/register", status_code=201)
async def register(user_data: UserCreate):
return create_user(user_data)
@router.get("/me")
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user
2. 管理员管理模块
# admin/router.py
from fastapi import APIRouter, Depends
from app.schemas.admin import AdminCreate, AdminUpdate
from app.services.admin import AdminService
from app.auth.dependencies import require_admin_role
router = APIRouter(prefix="/api/admin", tags=["管理员管理"], dependencies=[Depends(require_admin_role)])
@router.post("/", summary="创建管理员账号")
async def create_admin_account(admin_data: AdminCreate):
return await AdminService.create_admin(admin_data)
@router.get("/", summary="获取管理员列表")
async def get_admin_list(page: int = 1, size: int = 10):
return await AdminService.get_admin_list(page, size)
@router.put("/{admin_id}", summary="更新管理员信息")
async def update_admin_info(admin_id: int, update_data: AdminUpdate):
return await AdminService.update_admin(admin_id, update_data)
@router.delete("/{admin_id}", summary="禁用管理员账号")
async def disable_admin_account(admin_id: int):
return await AdminService.disable_admin(admin_id)
3. 作业管理模块
# assignment/router.py
from fastapi import APIRouter, UploadFile, File, Depends
from fastapi.responses import FileResponse
from app.schemas.assignment import AssignmentCreate, AssignmentUpdate
from app.services.assignment import AssignmentService
from app.auth.dependencies import require_teacher_role
router = APIRouter(prefix="/api/assignments", tags=["作业管理"])
@router.post("/", dependencies=[Depends(require_teacher_role)])
async def create_assignment(
assignment_data: AssignmentCreate,
attachment: UploadFile = File(None)
):
return await AssignmentService.create_assignment(
assignment_data,
attachment if attachment else None
)
@router.get("/{assignment_id}")
async def get_assignment_details(assignment_id: int):
return await AssignmentService.get_assignment(assignment_id)
@router.get("/course/{course_id}")
async def get_course_assignments(course_id: int):
return await AssignmentService.get_course_assignments(course_id)
@router.put("/{assignment_id}", dependencies=[Depends(require_teacher_role)])
async def update_assignment(
assignment_id: int,
update_data: AssignmentUpdate,
attachment: UploadFile = File(None)
):
return await AssignmentService.update_assignment(
assignment_id,
update_data,
attachment if attachment else None
)
@router.get("/{assignment_id}/download")
async def download_assignment_attachment(assignment_id: int):
file_path = await AssignmentService.get_assignment_file_path(assignment_id)
return FileResponse(file_path)
4. 作业批改模块
# grading/router.py
from fastapi import APIRouter, Depends
from app.schemas.grading import SubmissionGrade, GradeUpdate
from app.services.grading import GradingService
from app.auth.dependencies import require_teacher_role
router = APIRouter(prefix="/api/grading", tags=["作业批改"])
@router.get("/submissions/{assignment_id}")
async def get_assignment_submissions(
assignment_id: int,
page: int = 1,
size: int = 10,
graded: bool = None
):
return await GradingService.get_submissions(
assignment_id, page, size, graded
)
@router.post("/grade/{submission_id}", dependencies=[Depends(require_teacher_role)])
async def grade_submission(
submission_id: int,
grade_data: SubmissionGrade
):
return await GradingService.grade_submission(submission_id, grade_data)
@router.put("/grade/{submission_id}", dependencies=[Depends(require_teacher_role)])
async def update_grade(
submission_id: int,
update_data: GradeUpdate
):
return await GradingService.update_grade(submission_id, update_data)
@router.get("/stats/{assignment_id}")
async def get_assignment_grading_stats(assignment_id: int):
return await GradingService.get_assignment_stats(assignment_id)
5. 平时成绩统计模块
# grade/router.py
from fastapi import APIRouter, Depends
from app.services.grade import GradeService
from app.auth.dependencies import get_current_user
router = APIRouter(prefix="/api/grades", tags=["成绩统计"])
@router.get("/course/{course_id}")
async def get_course_grades(
course_id: int,
student_id: int = None
):
return await GradeService.get_course_grades(course_id, student_id)
@router.get("/student/{student_id}")
async def get_student_grades(student_id: int):
return await GradeService.get_student_grades(student_id)
@router.get("/teacher/{teacher_id}")
async def get_teacher_course_grades(teacher_id: int):
return await GradeService.get_teacher_course_grades(teacher_id)
@router.put("/participation", dependencies=[Depends(require_teacher_role)])
async def update_participation_score(
course_id: int,
student_id: int,
score: float
):
return await GradeService.update_participation_score(
course_id, student_id, score
)
前端Vue实现
核心页面实现
1. 作业提交页面 (学生)
<template>
<div class="assignment-submit">
<el-card>
<template #header>
<div class="card-header">
<h2>{{ assignment.title }}</h2>
<el-tag :type="deadlineStatus.type">
{{ deadlineStatus.text }}
</el-tag>
</div>
</template>
<el-descriptions :column="1" border>
<el-descriptions-item label="课程名称">
{{ course.course_name }}
</el-descriptions-item>
<el-descriptions-item label="任课教师">
{{ teacher.name }}
</el-descriptions-item>
<el-descriptions-item label="截止时间">
{{ formatDateTime(assignment.deadline) }}
</el-descriptions-item>
<el-descriptions-item label="作业要求">
<div v-html="assignment.description"></div>
</el-descriptions-item>
<el-descriptions-item label="附件" v-if="assignment.attachment_url">
<el-button type="primary" link @click="downloadAttachment">
下载作业附件
</el-button>
</el-descriptions-item>
</el-descriptions>
<el-divider />
<el-form :model="form" label-width="120px" v-if="!isGraded">
<el-form-item label="作业内容">
<el-input
v-model="form.content"
type="textarea"
:rows="8"
placeholder="请输入作业内容"
/>
</el-form-item>
<el-form-item label="作业附件">
<el-upload
action="#"
:auto-upload="false"
:limit="3"
:on-change="handleFileChange"
:file-list="fileList"
>
<el-button type="primary">选择文件</el-button>
<template #tip>
<div class="el-upload__tip">
支持PDF、Word、Zip等格式,单个文件不超过20MB
</div>
</template>
</el-upload>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="submitAssignment"
:loading="submitting"
>
提交作业
</el-button>
<el-button @click="saveDraft">保存草稿</el-button>
</el-form-item>
</el-form>
<div v-else>
<h3>批改结果</h3>
<el-descriptions :column="1" border>
<el-descriptions-item label="得分">
<el-tag type="success" size="large">
{{ submission.score }} / {{ assignment.total_score }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="教师评语">
{{ submission.feedback }}
</el-descriptions-item>
<el-descriptions-item label="批改时间">
{{ formatDateTime(submission.graded_at) }}
</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import {
getAssignmentDetail,
submitAssignment,
getStudentSubmission
} from '@/api/assignment'
import { downloadFile } from '@/api/file'
import { formatDateTime } from '@/utils/date'
import type { UploadFile } from 'element-plus'
const route = useRoute()
const assignmentId = parseInt(route.params.id as string)
// 作业数据
const assignment = ref<any>({})
const course = ref<any>({})
const teacher = ref<any>({})
const submission = ref<any>(null)
const fileList = ref<UploadFile[]>([])
const form = ref({
content: '',
attachments: [] as File[]
})
const submitting = ref(false)
// 获取作业详情
const fetchAssignment = async () => {
const res = await getAssignmentDetail(assignmentId)
assignment.value = res.data.assignment
course.value = res.data.course
teacher.value = res.data.teacher
// 获取学生提交记录
const subRes = await getStudentSubmission(assignmentId)
if (subRes.data) {
submission.value = subRes.data
form.value.content = subRes.data.content
}
}
// 计算截止时间状态
const deadlineStatus = computed(() => {
if (!assignment.value.deadline) return { type: 'info', text: '无截止时间' }
const now = new Date()
const deadline = new Date(assignment.value.deadline)
const diff = deadline.getTime() - now.getTime()
if (diff <= 0) {
return { type: 'danger', text: '已截止' }
} else if (diff < 24 * 60 * 60 * 1000) {
return { type: 'warning', text: '即将截止' }
} else {
return { type: 'success', text: '进行中' }
}
})
// 是否已批改
const isGraded = computed(() => {
return submission.value?.status === 'graded'
})
// 文件选择处理
const handleFileChange = (file: UploadFile) => {
form.value.attachments.push(file.raw as File)
}
// 下载作业附件
const downloadAttachment = async () => {
await downloadFile(assignment.value.attachment_url)
}
// 提交作业
const submitAssignment = async () => {
submitting.value = true
try {
const formData = new FormData()
formData.append('content', form.value.content)
form.value.attachments.forEach(file => {
formData.append('attachments', file)
})
await submitAssignment(assignmentId, formData)
ElMessage.success('作业提交成功')
await fetchAssignment()
} finally {
submitting.value = false
}
}
// 保存草稿
const saveDraft = () => {
// 实现草稿保存逻辑
ElMessage.success('草稿保存成功')
}
onMounted(() => {
fetchAssignment()
})
</script>
2. 作业批改页面 (教师)
<template>
<div class="grading-page">
<el-card>
<template #header>
<div class="card-header">
<h2>作业批改 - {{ assignment.title }}</h2>
<div>
<el-tag>已提交: {{ stats.submitted }}</el-tag>
<el-tag type="success">已批改: {{ stats.graded }}</el-tag>
<el-tag type="info">未提交: {{ stats.unsubmitted }}</el-tag>
</div>
</div>
</template>
<div class="toolbar">
<el-input
v-model="searchQuery"
placeholder="搜索学生姓名/学号"
style="width: 300px"
clearable
>
<template #append>
<el-button :icon="Search" />
</template>
</el-input>
<el-select
v-model="filterStatus"
placeholder="筛选状态"
clearable
style="width: 150px; margin-left: 10px"
>
<el-option label="已批改" value="graded" />
<el-option label="未批改" value="ungraded" />
</el-select>
<el-button
type="primary"
@click="exportGrades"
style="margin-left: auto"
>
导出成绩
</el-button>
</div>
<el-table :data="filteredSubmissions" border style="width: 100%">
<el-table-column prop="student.student_no" label="学号" width="120" />
<el-table-column prop="student.name" label="姓名" width="100" />
<el-table-column label="提交状态" width="100">
<template #default="{ row }">
<el-tag :type="row.submitted_at ? 'success' : 'danger'">
{{ row.submitted_at ? '已提交' : '未提交' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="提交时间" width="180">
<template #default="{ row }">
{{ row.submitted_at ? formatDateTime(row.submitted_at) : '-' }}
</template>
</el-table-column>
<el-table-column label="批改状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'graded' ? 'success' : 'warning'">
{{ row.status === 'graded' ? '已批改' : '未批改' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="得分" width="120">
<template #default="{ row }">
<span v-if="row.status === 'graded'">
{{ row.score }} / {{ assignment.total_score }}
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button
size="small"
@click="viewSubmission(row)"
:disabled="!row.submitted_at"
>
查看
</el-button>
<el-button
size="small"
type="primary"
@click="gradeSubmission(row)"
:disabled="!row.submitted_at"
>
批改
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="totalSubmissions"
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[10, 20, 50, 100]"
/>
</el-card>
<!-- 批改对话框 -->
<grading-dialog
v-model="dialogVisible"
:submission="currentSubmission"
:assignment="assignment"
@graded="handleGraded"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { Search } from '@element-plus/icons-vue'
import {
getAssignmentSubmissions,
getAssignmentStats,
exportAssignmentGrades
} from '@/api/grading'
import GradingDialog from './components/GradingDialog.vue'
import { formatDateTime } from '@/utils/date'
const route = useRoute()
const assignmentId = parseInt(route.params.id as string)
// 表格数据
const submissions = ref<any[]>([])
const stats = ref({
submitted: 0,
graded: 0,
unsubmitted: 0
})
const assignment = ref<any>({})
const currentPage = ref(1)
const pageSize = ref(10)
const searchQuery = ref('')
const filterStatus = ref('')
// 对话框控制
const dialogVisible = ref(false)
const currentSubmission = ref<any>(null)
// 获取作业提交列表
const fetchSubmissions = async () => {
const res = await getAssignmentSubmissions(
assignmentId,
currentPage.value,
pageSize.value,
filterStatus.value === 'graded'
)
submissions.value = res.data.list
assignment.value = res.data.assignment
}
// 获取作业统计信息
const fetchStats = async () => {
const res = await getAssignmentStats(assignmentId)
stats.value = res.data
}
// 过滤后的提交列表
const filteredSubmissions = computed(() => {
let result = submissions.value
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(
sub =>
sub.student.name.toLowerCase().includes(query) ||
sub.student.student_no.toLowerCase().includes(query)
)
}
return result
})
// 总提交数
const totalSubmissions = computed(() => {
return filteredSubmissions.value.length
})
// 查看作业详情
const viewSubmission = (submission: any) => {
currentSubmission.value = submission
dialogVisible.value = true
}
// 批改作业
const gradeSubmission = (submission: any) => {
currentSubmission.value = submission
dialogVisible.value = true
}
// 批改完成处理
const handleGraded = () => {
dialogVisible.value = false
fetchSubmissions()
fetchStats()
}
// 导出成绩
const exportGrades = async () => {
await exportAssignmentGrades(assignmentId)
}
onMounted(() => {
fetchSubmissions()
fetchStats()
})
// 监听分页变化
watch([currentPage, pageSize], () => {
fetchSubmissions()
})
// 监听筛选条件变化
watch(filterStatus, () => {
currentPage.value = 1
fetchSubmissions()
})
</script>
特色功能实现
1. 作业自动查重
# services/assignment.py
async def check_submission_similarity(submission_id: int):
"""
使用文本相似度算法检查作业相似度
"""
current_sub = await get_submission(submission_id)
if not current_sub.content:
return None
# 获取同作业其他提交
other_subs = await get_other_submissions(
current_sub.assignment_id,
exclude_id=submission_id
)
similarities = []
for sub in other_subs:
if not sub.content:
continue
# 使用TF-IDF计算相似度
similarity = calculate_text_similarity(
current_sub.content,
sub.content
)
if similarity > 0.6: # 相似度阈值
similarities.append({
'submission_id': sub.id,
'student_name': sub.student.name,
'student_no': sub.student.student_no,
'similarity': round(similarity * 100, 2)
})
# 保存查重结果
await save_similarity_result(submission_id, similarities)
return sorted(similarities, key=lambda x: x['similarity'], reverse=True)
2. 成绩统计分析
# services/grade.py
async def get_course_grade_distribution(course_id: int):
"""
获取课程成绩分布统计
"""
grades = await get_course_grades(course_id)
if not grades:
return None
# 分数段统计
score_ranges = [0] * 5 # 0-59, 60-69, 70-79, 80-89, 90-100
total_scores = [grade.total_score for grade in grades]
for score in total_scores:
if score < 60:
score_ranges[0] += 1
elif score < 70:
score_ranges[1] += 1
elif score < 80:
score_ranges[2] += 1
elif score < 90:
score_ranges[3] += 1
else:
score_ranges[4] += 1
# 计算统计指标
avg_score = sum(total_scores) / len(total_scores)
max_score = max(total_scores)
min_score = min(total_scores)
return {
'score_ranges': score_ranges,
'average_score': round(avg_score, 1),
'max_score': max_score,
'min_score': min_score,
'student_count': len(total_scores)
}
3. 实时作业提醒 (WebSocket)
# websocket/manager.py
class ConnectionManager:
def __init__(self):
self.active_connections: Dict[int, WebSocket] = {}
async def connect(self, user_id: int, websocket: WebSocket):
await websocket.accept()
self.active_connections[user_id] = websocket
def disconnect(self, user_id: int):
if user_id in self.active_connections:
del self.active_connections[user_id]
async def send_personal_message(self, message: str, user_id: int):
if user_id in self.active_connections:
await self.active_connections[user_id].send_text(message)
async def broadcast(self, message: str):
for connection in self.active_connections.values():
await connection.send_text(message)
# websocket/router.py
@router.websocket("/ws/{user_id}")
async def websocket_endpoint(
websocket: WebSocket,
user_id: int,
token: str,
manager: ConnectionManager = Depends(get_manager)
):
# 验证token
if not verify_websocket_token(token, user_id):
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return
await manager.connect(user_id, websocket)
try:
while True:
# 保持连接
await websocket.receive_text()
except WebSocketDisconnect:
manager.disconnect(user_id)
系统安全与优化
安全措施
-
API安全
- JWT认证 + CSRF保护
- 敏感操作审计日志
- 请求速率限制 (使用FastAPI的
slowapi
)
-
数据安全
- 数据库字段级加密 (如密码、敏感信息)
- 定期数据备份
- 文件上传病毒扫描
-
权限控制
# auth/dependencies.py
def require_admin_role(current_user: User = Depends(get_current_active_user)):
if current_user.role != 'admin':
raise HTTPException(
status_code=403,
detail="只有管理员可以访问此功能"
)
return current_user
def require_teacher_role(current_user: User = Depends(get_current_active_user)):
if current_user.role != 'teacher':
raise HTTPException(
status_code=403,
detail="只有教师可以访问此功能"
)
return current_user
性能优化
-
数据库优化
- 常用查询添加索引
- 复杂查询使用物化视图
- 读写分离
-
缓存策略
- Redis缓存热点数据 (如公告、配置)
- 作业列表缓存
-
异步处理
- 使用Celery处理耗时任务 (如作业查重、成绩导出)
部署方案
Docker Compose 配置
version: '3.8'
services:
backend:
build: ./backend
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/app_db
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
- minio
frontend:
build: ./frontend
ports:
- "8080:80"
depends_on:
- backend
db:
image: postgres:14
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- POSTGRES_DB=app_db
volumes:
- pg_data:/var/lib/postgresql/data
redis:
image: redis:6
volumes:
- redis_data:/data
minio:
image: minio/minio
ports:
- "9000:9000"
- "9001:9001"
environment:
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=minioadmin
volumes:
- minio_data:/data
command: server /data --console-address ":9001"
celery:
build: ./backend
command: celery -A app.celery_app worker --loglevel=info
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/app_db
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
volumes:
pg_data:
redis_data:
minio_data:
扩展功能路线图
-
移动端支持
- 开发响应式Web界面
- 开发微信小程序版本
-
AI辅助批改
- 使用NLP技术自动评分
- 语法错误自动检测
-
学习分析
- 学生学习行为分析
- 作业完成时间预测
-
多维度评价
- 同学互评功能
- 多维度评分体系