FEATURED · 精选文章

Django课程设计:学生心理测试系统从模型到部署完整解析

发布时间 / 2026/9/17 22:56:28
来源 / 创域科博编辑部
栏目 / 资讯中心
Django课程设计:学生心理测试系统从模型到部署完整解析 简介一套基于Django开发的学生心理测试系统完整课程设计资源附带可直接导入的数据库文件面向计算机相关专业本科生适用于Python课程大作业、实训项目或毕业设计参考。系统已获老师指导并以高分通过代码结构规整覆盖登录认证、心理测评、结果反馈、在线咨询、用户管理等模块能帮助读者理解Django MTV架构、URL路由、表单校验、ORM操作与会话保持等关键知识点。压缩包共121个文件体积仅1.83MB包含48个Python源文件、39个pyc缓存文件、7个HTML页面、5个CSS样式、4个SQL数据库脚本及若干图片素材前后端文件分离清晰数据库脚本预置了用户信息、心理测试题目与结果规则等核心表数据还原环境后即可运行。目前已有2657人学习下载。整套资料不仅提供完整项目源码与数据库初始化脚本还保留了项目运行所需的静态资源与页面模板既可用于快速完成课程设计也适合作为Django入门后的综合实战案例。1. 学生心理测试系统这项 Django 课程设计到底在交付什么秋季心理健康普查是高校学生处的常规工作但纸质量表从发放、回收、计分到汇总通常要折腾一两周反向计分题还容易算错。这份课程设计把「建量表、作答、自动计分、结果导出」整条流程做成 Django 工程标题里的源码加数据库也意味着交付物包含可运行的代码和一张设计好的初始库不是只有几个装饰性页面。适合要交课程设计的本科生、验收课设的助教以及想快速看一个完整业务如何分层的 Django 开发者。整个系统只用 Django 自带能力即可跑通不必引入消息队列或缓存组件。2. 从 MVT 到量表学生心理测试系统的模型设计与数据库落地2.1 Django 课设选型为什么不用 Flask心理测试系统的业务形态并不复杂管理员维护题库学生挑量表作答系统按维度结算分数。问题的关键在于这套流程里有多个对象要关联——量表挂维度、维度挂题目、题目挂选项。Django 的 ORM 在描述这种树形关系时几乎是声明式的定义外键后反向查询、联表聚合都由框架代劳换 Flask 加 SQLAlchemy 也不是不行但课程设计阶段多出来的装配成本并不划算。Django 还为这类系统提供了现成的后台django.contrib.admin 注册模型后题目维护可以直接在后台完成。学生端答题用 Session 记学号不需要单独做注册登录计分结果用 QuerySet 聚合后拼装即可。下表把这套方案对应关系列出来业务能力Django 提供的现成部分需要亲手编写数据表建模models migrate 自动建表模型字段设计题库维护Admin 站点新增编辑ModelAdmin 注册作答权限Session / Cookie学号登记表单自动计分视图与查询集计分与分级函数表中最后一行是整份课设的难点计分逻辑不属于框架能力要自己把答案散列换算成维度分再判级。理解这一点后面的代码就不会和框架混在一起。2.2 核心实体关系问卷、维度、题项与答题记录心理测试量表的计分方式不是“总题数求平均”这么简单。常见的自评量表会把题目划分到若干维度比如情绪状态、压力感、人际关系每个维度的题目独立计分再结合阈值给出参考等级。所以数据库里必须有一张维度表题目通过外键挂在维度下而不是平铺在大问卷表上。四张核心表的关系可以这样设计Questionnaire 作为一份问卷一对多关联 DimensionDimension 一对多关联 QuestionQuestion 一对多关联 Option选项携带分数学生每一次完整作答落在 AnswerRecord 里以“学号 问卷”作为唯一约束保证同一人重复提交时覆盖旧记录而不是无限追加。实体关键字段作用Questionnairetitle, desc, is_active一份量表的元信息Dimensionquestionnaire, name, min_score, max_score维度归属与分数区间Questiondimension, content, reverse_score, order_no题目、是否反向计分Optionquestion, text, score选项文字与分值AnswerRecordstudent_no, questionnaire, answers(JSON), finished_at一次作答的落库min_score 和 max_score 两个字段可以直接写在 Dimension 上计分时不用临时数题目数量也方便计分函数换算百分制。2.3 用 models.py 把上述关系变成数据表Django 中模型即表定义。一个课设规模的工程把五个模型放在同一个 models.py 即可不需要拆分多个模块from django.db import models class Questionnaire(models.Model): title models.CharField(max_length128) desc models.TextField(blankTrue) is_active models.BooleanField(defaultTrue) created_at models.DateTimeField(auto_now_addTrue) class Dimension(models.Model): questionnaire models.ForeignKey(Questionnaire, on_deletemodels.CASCADE, related_namedimensions) name models.CharField(max_length64) min_score models.PositiveIntegerField(default0) max_score models.PositiveIntegerField(default0) class Question(models.Model): dimension models.ForeignKey(Dimension, on_deletemodels.CASCADE, related_namequestions) content models.TextField() reverse_score models.BooleanField(defaultFalse) order_no models.PositiveIntegerField(default0) class Option(models.Model): question models.ForeignKey(Question, on_deletemodels.CASCADE, related_nameoptions) text models.CharField(max_length32) score models.PositiveIntegerField() class AnswerRecord(models.Model): student_no models.CharField(max_length32, db_indexTrue) name models.CharField(max_length32) questionnaire models.ForeignKey(Questionnaire, on_deletemodels.CASCADE) answers models.JSONField() finished_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (student_no, questionnaire)这里有两个设计说明。第一Question 的 reverse_score 字段解决反向计分题问卷里经常出现“我很少感到紧张”这类正向表述和“我经常突然心慌”这类反向表述如果全部按选项原始分累加正向和负向情绪会互相抵消所以计分函数里遇到 reverse_scoreTrue 要用 6 - score 翻转。第二answers 字段用 JSON 把题号和选项号存成一包避免为每一次选择单独建表课设规模下能显著减少查询次数。2.4 迁移、Admin 注册与后台维护模型定义好了接下来执行数据迁移并把模型暴露到后台。迁移是 Django 建表的入口命令如下python manage.py makemigrations survey python manage.py migratemakemigrations survey 只扫描 survey 这个 app 下的模型变更并生成迁移文件migrate 不带 app 参数会把 Django 内置的 auth、admin、session 相关表也一并建出。若想预览要执行的 SQL可以用 sqlmigrate 打印但不执行python manage.py sqlmigrate survey 0001Admin 注册部分建议给 Question 配置 list_display 和 list_filter让后台能按维度筛选题目。这也是“django admin界面美化”里最省事的一招from django.contrib import admin from .models import Questionnaire, Dimension, Question, Option, AnswerRecord admin.register(Question) class QuestionAdmin(admin.ModelAdmin): list_display (order_no, content, dimension, reverse_score) list_filter (dimension,) search_fields (content,)这样进入 /admin 后台后辅导员可以一边开量表页面一边在另一个窗口补题目不需要碰数据库命令行。list_display 里的字段必须是模型字段或方法直接写外键名会显示 Question dimension object加双下划线写法如 dimension__name 在 list_display 里不生效需要在模型上写属性方法这是新手容易卡住的点。3. 答题流程与自动计分Django 视图、路由和评分算法3.1 URLconf 的分层与职责课程设计里 URL 配置先别急着堆在根 urls.py按 app 拆分更清晰。survey 是业务 app它的 urls.py 只回答三个问题从哪页开始、答案提交到哪、结果在哪看。# survey/urls.py from django.urls import path from . import views urlpatterns [ path(survey/int:qid/start/, views.start_survey, namestart_survey), path(survey/int:qid/, views.survey_detail, namesurvey_detail), path(survey/int:qid/submit/, views.submit_answer, namesubmit_answer), path(result/int:record_id/, views.result_detail, nameresult_detail), ]四个视图对应四个动作start_survey 收集学号和姓名survey_detail 渲染问卷页面submit_answer 接收到来的选项并计算result_detail 展示维度得分和等级。把提交和处理分开而不是写进同一个视图里是为了服务端保存数据时保持响应逻辑唯一后续加防重复提交也方便。根 urls.py 里只要 include(survey.urls)再把 /admin 指到系统后台一个课设项目的路由框架就立住了。3.2 用 Session 承接未登录作答学生心理健康普查场景下让每个人注册账号成本太高常见做法是用学号加姓名完成登记数据存入 Session 而不是数据库。视图里做一次表单校验就够# survey/views.py from django.shortcuts import render, redirect, get_object_or_404 def start_survey(request, qid): questionnaire get_object_or_404(Questionnaire, idqid) if request.method POST: student_no request.POST.get(student_no, ).strip() name request.POST.get(name, ).strip() if len(student_no) 6 or not name: return render(request, survey/start.html, { questionnaire: questionnaire, error: 学号和姓名不能为空, }) request.session[student_no] student_no request.session[name] name return redirect(survey_detail, qidqid) return render(request, survey/start.html, {questionnaire: questionnaire})注意这里没有用 Django Form原因有三课程设计要求评审能看懂每个字段两字段表单用 Form 类是过度设计校验逻辑简单一个 if 足够。如果后面要加学院、年级字段建议重构为 ModelForm否则视图里的 request.POST.get 会越积越多。3.3 提交答案与覆盖式写入学生一次作答提交几十道题后端拿到的是 request.POST 里所有 name 前缀为 q_ 的字段。这里需要做两件事把答案转换成 question_id 到 option_id 的字典以及处理重复提交。先看视图def submit_answer(request, qid): questionnaire Questionnaire.objects.get(idqid) student_no request.session.get(student_no) name request.session.get(name) if not student_no or not name: return redirect(start_survey, qidqid) answers {} for key, val in request.POST.items(): if key.startswith(q_): question_id int(key[2:]) answers[question_id] int(val) record, created AnswerRecord.objects.update_or_create( student_nostudent_no, questionnairequestionnaire, defaults{name: name, answers: answers}, ) return redirect(result_detail, record_idrecord.id)update_or_create 是替代“先 filter 再 delete 再 create”的标准写法后者的删除动作在并发提交时会把别人刚写入的记录一并删掉。使用 unique_together 约束配合 update_or_create既满足第二次答题覆盖旧结果又不会产生垃圾行。若课设要求展示 ORM 删除操作可以在管理端提供“清除某学生记录”按钮用 AnswerRecord.objects.filter(student_no...).delete() 完成。这里顺带说一句并发问题SQLite 在极端并发写时会报 database is locked课设演示通常遇不到如果部署到公网被大量访问最省事的方案是把 DATABASES 配置改成 PostgreSQL模型层零改动。3.4 计分函数从答案散列到维度得分问卷页面提交的是 option_id在计分函数里需要一次性把涉及的题目和选项查出来再按维度聚合。避免在 for 循环里逐个查询数据库这是 Django 查询最常犯的性能问题题目数量少但也要养成习惯。# survey/services.py def compute_dimension_scores(record): from .models import Question, Option answer_items list(record.answers.items()) question_ids [int(qid) for qid, _ in answer_items] option_ids [int(oid) for _, oid in answer_items] questions {q.id: q for q in Question.objects.filter(id__inquestion_ids)} options {o.id: o for o in Option.objects.filter(id__inoption_ids)} dimension_scores {} for qid, oid in answer_items: q questions[int(qid)] opt options[int(oid)] score opt.score if q.reverse_score: score 6 - score # 5 点量表反向翻转 dim_name q.dimension.name dimension_scores[dim_name] dimension_scores.get(dim_name, 0) score return dimension_scores这段代码的关键在 dict comprehension 一次性建索引Question.objects.filter(id__in...) 发出一条 SQLOption 同理总共两条查询就完成整个计分过程。reverse_score 翻转的 6 - score 假设所有选项分值固定为 1 到 5如果你的量表是 4 点或 7 点需要把常数改成 max_score 1。一个更稳妥的做法是从 Dimension 表读取选项分区间用 min_score max_score - score 做翻转此处为可读性固定写成 6。维度原始分不能直接跨量表比较统一换算成 0 到 100 再判级。换算公式是 (维度分 - min_score) / (max_score - min_score) * 100把 Dimension 表上预留的区间用起来def grade_result(questionnaire, dimension_scores): grades {} for dim in questionnaire.dimensions.all(): raw dimension_scores.get(dim.name, dim.min_score) ratio (raw - dim.min_score) / (dim.max_score - dim.min_score) * 100 if ratio 30: level 状态良好 elif ratio 60: level 轻度波动 elif ratio 80: level 中度偏高 else: level 高度偏高建议预约线下沟通 grades[dim.name] {raw: raw, ratio: round(ratio, 1), level: level} return grades等级文案和阈值可以做成一张配置表换算后百分比等级文案建议动作 30状态良好无需处理30 ~ 60轻度波动自我关注60 ~ 80中度偏高建议预约沟通 80高度偏高需要线下介入result_detail 视图就是拿到 record 后调用 compute_dimension_scores再调用 grade_result把结果字典传给 templates/survey/result.html 渲染。结果页可以顺带展示每个维度的作答时间戳方便追溯答题过程。注意30、60、80 这些阈值只用于课程设计演示接入真实量表时应以量表手册里的常模为准不要直接用于实际筛查。4. 源码跑通与数据库初始化从创建 app 到量表数据灌入4.1 本地环境的最小启动命令无论拿到的源码结构如何第一步都是把解释器和依赖还原出来。Python 3 以上环境推荐用虚拟环境隔离项目依赖避免把包装进系统级 site-packages。这一步对 Windows 和 Linux 都适用只是激活命令的路径不同mkdir mental-survey cd mental-survey python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install django django-admin startproject mental_system . python manage.py startapp surveystartproject 后面那个点表示当前目录直接作为项目根目录而不是再嵌套一层 mental_system/mental_system。用点号生成的结构里 manage.py 和项目配置在同一层交给老师时目录更扁平。startapp survey 创建的是业务模块记得在 mental_system/settings.py 的 INSTALLED_APPS 里追加 survey。这一步常见错误是把 startproject 目录名写成 Python 保留字或带连字符比如 mental-system导致后续 import 报错。目录名统一用下划线连接。4.2 数据库迁移与超级用户模型全部写完并注册 INSTALLED_APPS 后建库命令按顺序执行python manage.py makemigrations survey python manage.py migrate python manage.py createsuperusermigrate 不带 app 参数会把 Django 自带的 auth、admin、session 等表一并创建SQLite 的 db.sqlite3 文件也在这一步生成。createsuperuser 用来进 Admin 后台录入量表数据账号建议学号风格密码按提示设置。“源码数据库”课设包的常见交付形态就是在 migrate 之后直接把 db.sqlite3 打进压缩包评审老师解压后执行 python manage.py runserver 即可访问连迁移都不用跑。这样做的前提是迁移文件和数据库保持同版本否则打开后台会报约束错误。如果是把别人的库拿过来自己改模型改完模型后必须重新 makemigrations再 migrate 增量结构不要直接删除 db.sqlite3 重建否则初始数据全丢。4.3 用 fixture 把初始量表灌进数据库直接在 Admin 后台手动录入一份完整量表工作量大且容易漏字段。更可控的做法是写一个 fixture JSON 文件用 loaddata 一次性载入。fixture 的格式是 Django 序列化的标准结构[ { model: survey.questionnaire, pk: 1, fields: { title: 大学生情绪压力自评量表, desc: 用于课程设计演示的通用自评量表, is_active: true, created_at: 2025-09-01T08:00:00Z } }, { model: survey.dimension, pk: 1, fields: { questionnaire: 1, name: 情绪, min_score: 5, max_score: 25 } } ]把整份量表按此格式补齐后放入 survey/fixtures/initial_data.json执行python manage.py loaddata initial_dataloaddata 默认搜索每个 app 的 fixtures 目录文件名不带 .json 也能识别。fixture 里的主键是显式声明的导入时即使库是空的也会按 pk 写入所以维度外键 questionnaire 可以直接填 1。created_at 这种 auto_now_add 字段在 fixture 里读取的是字符串时间如果 settings 开了 USE_TZ格式必须带 Z 或偏移量否则会抛解析异常。如果不想维护一长串 JSON也可以写一个 management command 用 bulk_create 批量建题。bulk_create 的优势是单条 SQL 插入多行性能好坑在于它不触发 model.save() 方法依赖 save 自动生成的字段值必须提前算好。比如 auto_now_add 的 created_at 在批量插入时会取当前时间这时要显式给每个对象赋值否则可能出现同批数据时间戳不一致。批量数据里若有重复主键整体会回滚想跳过冲突行可以传 ignore_conflictsTrue。4.4 三个一定会踩的配置坑本地 runserver 能跑不代表源码换一台电脑依然能跑。这三个坑按出现频率排序写一下。第一个是时区。settings.py 默认的 TIME_ZONE 是 UTC学生本地录入时间会比北京时间少 8 小时。课程设计项目建议直接改成 TIME_ZONE Asia/ShanghaiUSE_TZ 保持默认 True模板里展示时间时会自动按当前时区渲染。第二个是 ALLOWED_HOSTS。DEBUG 关闭后不在白名单里的 HOST 访问会返回 400DEBUG False ALLOWED_HOSTS [survey.example.edu.cn, 127.0.0.1]第三个是静态文件。Django 开发服务器会处理静态文件但关闭 DEBUG 后路径全部失效。部署前执行 python manage.py collectstatic把 STATIC_ROOT 指向一个部署目录。课程设计评审通常只跑 runserver这个坑往往被忽略等到上服务器才开始让人头痛。注意讲师把 zip 拷贝到另一台电脑时如果直接运行报 ModuleNotFoundError先检查 virtualenv 是否激活再看 requirements.txt 里是否完整列出了依赖。这两个原因占了课设跑不起来的一半比例。5. 结果导出与部署验证让 Django 系统在服务器上可复现5.1 用 ORM 聚合统计班级维度分布辅导员要的不只是单个学生的分数而是“某维度中度偏高人数”这种分布。最省代码的方式是读完全部 AnswerRecord 再在 Python 里 for 循环统计但数据量上来后响应会变慢。可以在登记页增加班级字段或者用学号前缀切片做班级聚合from django.db.models import Count from django.db.models.functions import Substr from survey.models import AnswerRecord dist ( AnswerRecord.objects .annotate(cls_noSubstr(student_no, 5, 2)) .values(cls_no, questionnaire__title) .annotate(cntCount(id)) )Substr(student_no, 5, 2) 取学号第 5 位开始的两个字符作为班级分组键前提是学号规则统一学号不可靠或包含字母时建议登记页增加班级下拉框直接存字段。values annotate 的组合会生成一条 GROUP BY SQL数据量级在课设范围完全够用。5.2 导出带 BOM 的 CSV结果交付给辅导员用的格式首选 CSV。用标准库处理即可重点是把响应编码指定为 utf-8-sig否则 Excel 直接打开会出现第一列乱码import csv from django.http import HttpResponse def export_results(request, qid): response HttpResponse(content_typetext/csv; charsetutf-8-sig) response[Content-Disposition] attachment; filenameresults.csv writer csv.writer(response) writer.writerow([学号, 姓名, 维度, 原始分, 百分比, 等级]) for record in AnswerRecord.objects.filter(questionnaire_idqid): grades grade_result(record.questionnaire, compute_dimension_scores(record)) for dim_name, info in grades.items(): writer.writerow([record.student_no, record.name, dim_name, info[raw], info[ratio], info[level]]) return response这个视图挂到带 qid 参数的 URL 上在后台管理页放一个导出链接即可。csv 模块逐行写出不会把全部结果拼在内存里导出几千条记录也不会撑爆响应对象。5.3 贴进宝塔与验收自测上线演示用宝塔的 Python 项目管理器是条快速路径上传源码选择解释器安装依赖启动脚本指向 mental_system 的 wsgi:application再把站点或公网 IP 反向代理到 gunicorn 监听端口。部署完过一遍自测清单/admin 能登录、后台能新建问卷、学生端完成一次作答、重复作答覆盖旧记录、CSV 导出无乱码、collectstatic 后后台样式完整。最常翻车的是 gunicorn 报 ModuleNotFoundError启动命令调用了系统全局的 gunicorn而不是虚拟环境里的那份。重启时用绝对路径指到 .venv/bin/gunicorn或者把虚拟环境路径写进 systemd unit 文件。静态文件没收集导致后台白屏回到第 4 章的 collectstatic 重跑一次再刷新浏览器清缓存通常就能解决。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻