1. 项目背景与核心价值
这个选题系统本质上是一个用Python后端+Vue3前端构建的Web应用,主要解决高校毕业设计选题环节的三大痛点:学生选题扎堆、教师人工统计费时、选题过程不透明。我在实际开发中发现,市面上很多类似系统要么功能单一,要么操作复杂,而我们的方案通过合理的架构设计,在易用性和功能性之间找到了平衡点。
从技术选型来看,Python+Django提供了稳定的后端支持,Vue3的Composition API让前端状态管理更清晰。特别值得一提的是,我们创新性地采用了WebSocket实现选题过程的实时更新,学生提交选题后,所有相关师生能立即看到最新数据,避免了传统轮询方式带来的延迟和服务器压力。
2. 系统架构设计解析
2.1 技术栈选型依据
后端选择Django框架主要基于三点考虑:
- Django Admin能快速搭建管理后台,满足教师端的需求
- Django REST framework对API开发支持完善
- Python生态有丰富的教育领域库支持(如PyPDF2用于论文格式检查)
前端选用Vue3而不是React的原因:
- 高校实验室电脑配置普遍一般,Vue3的运行时性能更优
- 学校IT部门技术人员更熟悉Vue技术栈
- Composition API更适合处理复杂的选题状态逻辑
2.2 数据库设计要点
核心的选题关系采用三张表实现:
python复制class Teacher(models.Model):
name = models.CharField(max_length=50)
research_field = models.CharField(max_length=100)
class Topic(models.Model):
title = models.CharField(max_length=200)
teacher = models.ForeignKey(Teacher, on_delete=models.CASCADE)
capacity = models.IntegerField() # 可选人数上限
class Selection(models.Model):
student = models.ForeignKey(User, on_delete=models.CASCADE)
topic = models.ForeignKey(Topic, on_delete=models.CASCADE)
priority = models.IntegerField() # 志愿优先级
status = models.CharField(max_length=20) # 待审核/已通过/已拒绝
特别注意的点:
- 为topic表添加了capacity字段控制选题人数上限
- selection表的priority字段实现多志愿机制
- 使用status字段而非布尔值,便于扩展其他状态
3. 核心功能实现细节
3.1 选题流程的状态管理
前端使用Pinia管理复杂的选题状态:
javascript复制// stores/selection.js
export const useSelectionStore = defineStore('selection', {
state: () => ({
topics: [],
mySelections: [],
deadline: null
}),
actions: {
async fetchTopics() {
// 获取所有可选课题
},
async submitSelection(priorities) {
// 提交志愿顺序
}
}
})
关键实现技巧:
- 使用Map数据结构缓存课题数据,避免重复请求
- 添加deadline状态,前端自动禁用过期操作
- 对提交操作添加防抖处理,防止重复提交
3.2 实时通知系统的实现
采用Django Channels搭建WebSocket服务:
python复制# consumers.py
class SelectionConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.channel_layer.group_add(
"selection_updates",
self.channel_name
)
await self.accept()
async def selection_update(self, event):
await self.send(text_data=json.dumps(event["data"]))
前端连接代码:
javascript复制const socket = new WebSocket(`wss://${location.host}/ws/selection/`)
socket.onmessage = (e) => {
const data = JSON.parse(e.data)
selectionStore.updateFromServer(data)
}
重要提示:生产环境需要配置Nginx代理WebSocket连接,并添加心跳机制保持连接稳定
4. 关键业务逻辑实现
4.1 智能分配算法
当选题人数超过课题容量时,系统会执行以下分配逻辑:
- 第一轮:处理所有唯一志愿的申请
- 第二轮:按照学生绩点排序处理竞争性志愿
- 第三轮:对未分配学生进行调剂
算法核心代码:
python复制def allocate_topics():
# 第一阶段分配
for topic in Topic.objects.all():
applicants = Selection.objects.filter(topic=topic, priority=1)
if applicants.count() <= topic.capacity:
for selection in applicants:
selection.status = 'approved'
selection.save()
# 第二阶段分配(简化示例)
remaining_topics = Topic.objects.filter(
selection__status='approved').annotate(
approved_count=Count('selection')).filter(
approved_count__lt=F('capacity'))
for topic in remaining_topics:
candidates = Selection.objects.filter(
topic=topic,
status='pending'
).order_by('-student__gpa') # 按绩点排序
available_slots = topic.capacity - topic.approved_count
for selection in candidates[:available_slots]:
selection.status = 'approved'
selection.save()
4.2 数据导出功能
使用Pandas生成Excel报表:
python复制def export_selections(request):
queryset = Selection.objects.select_related('student', 'topic').all()
df = pd.DataFrame.from_records([
{
'学号': sel.student.username,
'姓名': sel.student.last_name,
'课题': sel.topic.title,
'导师': sel.topic.teacher.name,
'状态': sel.status
}
for sel in queryset
])
response = HttpResponse(
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
response['Content-Disposition'] = 'attachment; filename=selections.xlsx'
df.to_excel(response, index=False)
return response
优化技巧:
- 使用select_related减少数据库查询
- 添加内存缓存避免重复生成相同报表
- 对大文件使用流式响应
5. 部署与性能优化
5.1 生产环境部署方案
推荐使用Docker Compose部署:
yaml复制version: '3.8'
services:
db:
image: postgres:13
environment:
POSTGRES_PASSWORD: example
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:6
web:
build: .
command: gunicorn core.wsgi:application --bind 0.0.0.0:8000
volumes:
- .:/code
ports:
- "8000:8000"
depends_on:
- db
- redis
celery:
build: .
command: celery -A core worker -l info
volumes:
- .:/code
depends_on:
- redis
volumes:
postgres_data:
关键配置项:
- 使用PostgreSQL替代SQLite提升并发性能
- Redis作为Celery broker和缓存后端
- Gunicorn替代开发服务器
5.2 性能优化实践
-
数据库优化:
- 为常用查询字段添加索引
- 使用select_related/prefetch_related优化ORM查询
- 启用数据库连接池
-
前端优化:
- 使用Vue的keep-alive缓存常用组件
- 实现分页加载选题列表
- 对静态文件启用CDN加速
-
缓存策略:
python复制# settings.py CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://redis:6379/1", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } } } # views.py @cache_page(60 * 15) # 缓存15分钟 def topic_list(request): # ...
6. 安全防护措施
6.1 常见安全风险防范
-
CSRF防护:
- 确保Django的CsrfViewMiddleware启用
- 前端axios默认配置携带CSRF token
-
XSS防护:
- Vue默认已对动态绑定内容进行转义
- 使用DOMPurify处理富文本内容
-
SQL注入防护:
- 坚持使用ORM或参数化查询
- 对管理后台操作进行审计日志记录
6.2 权限控制实现
基于Django的权限系统扩展:
python复制# permissions.py
class IsTeacherOrAdmin(BasePermission):
def has_permission(self, request, view):
return request.user.is_staff or request.user.groups.filter(name='Teachers').exists()
# views.py
@permission_classes([IsTeacherOrAdmin])
class TopicCreateView(CreateAPIView):
# ...
前端路由守卫:
javascript复制router.beforeEach((to, from, next) => {
if (to.meta.requiresTeacher && !store.user.isTeacher) {
next('/forbidden')
} else {
next()
}
})
7. 测试方案设计
7.1 单元测试重点
- 测试分配算法:
python复制class AllocationTestCase(TestCase):
def setUp(self):
self.teacher = Teacher.objects.create(name="张教授")
self.topic = Topic.objects.create(
title="深度学习研究",
teacher=self.teacher,
capacity=2
)
def test_first_priority_allocation(self):
# 测试第一志愿分配逻辑
pass
- API接口测试:
python复制class TopicAPITest(APITestCase):
def test_create_topic(self):
self.client.force_login(self.teacher)
response = self.client.post('/api/topics/', {
'title': '新课题',
'capacity': 5
})
self.assertEqual(response.status_code, 201)
7.2 压力测试方案
使用Locust模拟高并发场景:
python复制from locust import HttpUser, task
class SelectionUser(HttpUser):
@task
def submit_selection(self):
self.client.post("/api/selections/", json={
"topic_id": 1,
"priority": 1
}, headers={"Authorization": "Bearer xxx"})
测试关键指标:
- 500并发下的平均响应时间
- 数据库连接池使用情况
- WebSocket消息延迟
8. 项目扩展方向
8.1 功能扩展建议
-
添加课题查重功能:
- 使用TF-IDF计算课题相似度
- 防止教师重复发布相似课题
-
实现双向选择:
- 教师可以主动选择学生
- 增加面试预约功能
-
集成论文管理系统:
- 开题报告提交
- 中期检查提醒
- 最终论文查重
8.2 技术深化方向
-
引入机器学习:
- 基于历史数据推荐课题
- 预测课题热门程度
-
微服务化改造:
- 将分配算法独立为服务
- 使用消息队列解耦组件
-
移动端适配:
- 开发微信小程序版本
- 添加消息推送功能
在实际部署过程中,我发现Nginx的WebSocket配置是个容易出问题的地方,正确的配置应该是:
nginx复制location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
另一个容易忽略的点是Django Channels的路由配置,需要确保asgi.py正确加载路由:
python复制# core/asgi.py
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter([
path("ws/selection/", SelectionConsumer.as_asgi()),
])
),
})