资讯动态

Django+Vue心理健康管理系统:量表计分、报告导出与隐私回归

发布时间:2026/9/18 19:37:24 来源:尧图企业网站定制
简介这份资源是面向计算机专业毕业设计学生的完整论文文档选题为基于Django与Vue的大学生心理健康管理系统适合正在准备毕设、需要参考B/S架构项目实现与论文写作规范的本科生及指导教师。压缩包共1个文件为docx格式的毕业论文正文包含中英文摘要、绪论、系统开发技术、需求分析、功能设计与实现、测试与总结等标准章节整体约3.94MB。论文以MySQL为数据库采用Python语言与Django框架开发围绕管理员、学生、心理导师三类角色设计了个人信息修改、用户与导师信息管理、专题辅导、辅导预约、咨询信息与导师回复、热门文章、心理测评等模块并新增首页推送最新资讯功能兼顾动态性与交互友好。文中同步说明了PyCharm开发环境、Django框架与MySQL的技术选型理由可作为同类心理健康类系统选题的章节模板与代码结构参考。目前已有75人学习适合需要快速确定选题、梳理功能模块与撰写论文的读者借鉴。1. 大学生心理健康管理系统为什么适合用 Django Vue 做前后端分离这个题目的难点从来不在页面数量而在数据敏感度和角色边界。一份抑郁自评量表的原始作答、因子分和风险等级学生本人、咨询师、学院管理员三方的可见范围完全不同一旦在毕设里把这块做成「登录后都能看」答辩时基本会被追问到答不上来。用 Django 做后端等于直接复用成熟的用户模型、ORM 权限和后台管理省下来的时间可以砸在量表计分和心理预警逻辑上用 Vue 做前端则是因为答题页、报告页、咨询师工作台三块交互密度高但共享状态有限天然适合路由拆分的单页结构。这套组合真正能写深的地方在于量表结构怎么建模、因子分怎么算、测评记录怎么查和删、报告怎么导出、上线后隐私怎么回归。适合正在写这个课题的学生也适合想拿它练一遍前后端分离全链路的开发者。2. Django 侧的量表建模、测评查询与删除边界2.1 MTV 模式在心理测评系统里各自落在哪一层Django 的 MTV 常被当成 MVC 换了个说法但放到这个课题里三层的落点非常具体。Model 层承载量表、题目映射、测评记录三张核心表量表结构用 JSONField 存题号到因子的映射避免为每套量表单独建表Template 层在前后端分离后并没有消失它退化成了 Django admin 的页面模板和报告导出的 HTML 模板量表维护、批量导入题库这类低频操作直接挂 admin 最省事View 层交给 DRF 的 ViewSet负责答题提交、报告拉取、统计聚合三类接口。判断一个功能该放哪层有个简单标准涉及跨表统计和权限收缩的放 View涉及字段结构和校验规则的放 Model只有后台和导出才碰 Template。这样拆完前端只需要面对十几个稳定接口而不是几十个零散 API。2.2 创建 assessment app 与量表、记录表字段设计先建应用并把模型写清楚这是后面所有查询和删除操作的基础。# 在项目根目录执行创建测评业务独立 app python manage.py startapp assessment # 建表并生成迁移文件 python manage.py makemigrations assessment python manage.py migrate# assessment/models.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): ROLE_CHOICES ((student, 学生), (counselor, 咨询师), (admin, 管理员)) role models.CharField(max_length16, choicesROLE_CHOICES, defaultstudent) # 列表页永远不返回完整手机号只存脱敏结果 phone_masked models.CharField(max_length20, blankTrue) college models.CharField(max_length64, blankTrue) class Scale(models.Model): 一套量表例如 SCL-90、SDS、SAS code models.CharField(max_length32, uniqueTrue) title models.CharField(max_length128) options models.JSONField(defaultlist) # [{value:1,label:没有}, ...] factor_map models.JSONField(defaultdict) # {躯体化: [1,4,12], ...} reverse_items models.JSONField(defaultlist) # 反向计分题号如 [5, 12] is_active models.BooleanField(defaultTrue) class AssessmentRecord(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_namerecords) # PROTECT量表一旦有测评记录就不允许删防止历史报告失真 scale models.ForeignKey(Scale, on_deletemodels.PROTECT, related_namerecords) answers models.JSONField(defaultdict) # {1: 3, 2: 1, ...} total_score models.FloatField(default0) factor_scores models.JSONField(defaultdict) risk_level models.CharField(max_length16, defaultnormal) is_deleted models.BooleanField(defaultFalse) submitted_at models.DateTimeField(auto_now_addTrue) class Meta: indexes [models.Index(fields[user, scale, -submitted_at])]字段设计里有三个决定后续代价的选择。factor_map用 JSON 而不是独立关联表是为了让新增量表不需要改表结构代价是丧失数据库层面的引用完整性校验只能放在序列化器里on_deletemodels.PROTECT拦住的是「删量表顺带删掉几百份测评档案」这种事故比 CASCADE 更符合心理档案的留存要求is_deleted字段是软删除的载体学生端「撤销本次测评」走的是它而不是真的 DELETE。2.3 测评记录的查询聚合与删除两种写法的代价差在哪查询侧最常写错的是把整份answers拉回来做统计单条记录几百个键值列表页一次拉 50 条就是几万次序列化。from django.db.models import Avg, Count from assessment.models import AssessmentRecord # 1) 某学生最近 5 次测评only 限定字段避免把 answers 整包取出 records (AssessmentRecord.objects .filter(user_idstu_id, is_deletedFalse) .only(id, total_score, risk_level, submitted_at) .order_by(-submitted_at)[:5]) # 2) 按学院统计高风险人数一次 SQL 完成分组不要在 Python 里循环 stat (AssessmentRecord.objects .filter(is_deletedFalse, risk_level__in[high, severe]) .values(user__college) .annotate(cntCount(id)) .order_by(-cnt)) # 3) 某量表平均分与样本量用于咨询师端的总览卡片 summary (AssessmentRecord.objects .filter(scale__codeSDS, is_deletedFalse) .aggregate(avg_scoreAvg(total_score), totalCount(id)))only()生成的 SQL 只 SELECT 指定列序列化开销能降一个量级.values()加annotate()走的是数据库分组比取出全量再collections.Counter快得多aggregate()返回单行字典适合做概览数字。三个写法的共同点是能在 SQL 层收敛的计算绝不带回 Python。删除侧的风险更高因为测评档案有留存要求。# 反面写法物理删除整批记录档案不可追溯学院回查时无法举证 AssessmentRecord.objects.filter(user_idstu_id).delete() # 推荐写法撤销操作只做软删除保留原始作答 updated (AssessmentRecord.objects .filter(user_idstu_id, is_deletedFalse) .update(is_deletedTrue)) print(f本次标记 {updated} 条记录为已删除) # 确需物理清理时先取主键清单再按 id 批量删避免条件写错误伤全表 ids list(AssessmentRecord.objects .filter(scale_idold_scale_id, submitted_at__ltcutoff) .values_list(id, flatTrue)) AssessmentRecord.objects.filter(id__inids).delete()注意QuerySet.delete()有个容易忽略的行为它会触发级联删除外键上写着 CASCADE 的关联数据会被一起带走且删除后对象的pk会被置为 None。上面的scale用的是 PROTECT执行物理删除时会直接抛ProtectedError这是有意留的最后一道闸。软删除的update()不触发信号也不调用save()所以updated_at这类auto_now字段不会自动更新需要手动写进update()参数里。2.4 序列化器与角色权限让因子分按角色可见分数永远由后端算前端传上来的total_score一律不认。from rest_framework import serializers from assessment.models import AssessmentRecord class RecordSerializer(serializers.ModelSerializer): scale_code serializers.CharField(sourcescale.code, read_onlyTrue) total_score serializers.FloatField(read_onlyTrue) class Meta: model AssessmentRecord fields [id, scale_code, answers, total_score, factor_scores, risk_level, submitted_at] def validate_answers(self, value): if not isinstance(value, dict) or not value: raise serializers.ValidationError(答题数据不能为空) return value def to_representation(self, instance): data super().to_representation(instance) request self.context.get(request) # 因子分明细只对咨询师和管理员开放 if not request or request.user.role student: data.pop(factor_scores, None) return data把total_score声明为read_only是接口层的第一道防线比在视图里手工剔除字段更难写漏。to_representation做的是输出裁剪跟输入校验互不干扰适合放这种按角色增减字段的逻辑。视图侧再叠一层角色判断即可学生只能查自己的记录咨询师能查本院管理员全量这层用 DRF 的get_queryset重写比写装饰器更稳。3. Vue 侧的工程搭建、路由参数与打包后布局异常3.1 从装依赖到跑通答题页的最短路径前端工程不需要脚手架之外的东西依赖越少打包后的问题越少。# 1) 初始化工程Vite 模板比旧版 vue-cli 启动快得多 npm create vitelatest psy-frontend -- --template vue cd psy-frontend # 2) 运行时依赖 npm install vue-router4 pinia axios echarts # 3) 本地起服务默认 5173 端口 npm run dev # 4) 打包产物在 dist 目录 npm run build包主要版本区间在本系统里的用途vue-router4.x答题页/scale/:scaleId、报告页/report/:recordIdpinia2.x登录态、答题进度草稿axios1.x请求封装、token 注入、401 跳登录echarts5.x因子分雷达图、历次测评趋势线安装卡住多半是网络源问题切国内镜像再重装即可别急着删node_modules反复重来。npm run dev能起来但页面空白先看浏览器控制台有没有Failed to resolve import那通常是路径别名没在vite.config.js里配。3.2 路由参数设计与登录守卫把权限判断前移路由层能挡住的越权不要留到接口层才发现。// src/router/index.js import { createRouter, createWebHistory } from vue-router import { useUserStore } from /stores/user const routes [ { path: /login, component: () import(/views/Login.vue) }, { path: /scale/:scaleId, name: scale, component: () import(/views/ScaleAnswer.vue), meta: { auth: true }, props: true, // 把 scaleId 直接作为 props 传给组件组件里不用再读 $route }, { path: /report/:recordId, name: report, component: () import(/views/ReportDetail.vue), meta: { auth: true, roles: [counselor, admin] }, }, ] const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes, }) router.beforeEach((to) { const user useUserStore() if (to.meta.auth !user.token) { return { path: /login, query: { redirect: to.fullPath } } } if (to.meta.roles !to.meta.roles.includes(user.role)) { return { path: /403 } } return true }) export default routerprops: true省掉了组件里useRoute().params的样板代码也让组件更容易单测。query.redirect保留原始目标地址登录后跳回去避免学生答到一半被踢下线后找不到页面。这里只做「有没有资格进入页面」的判断真正的数据归属校验必须在后端做一遍前端守卫只是体验优化。3.3 Pinia 与 Vuex 的取舍答题进度该放在哪新项目没有理由再选 Vuex。Pinia 的 store 就是普通函数没有 mutation 概念TypeScript 推断也更直接模块拆分靠多文件而不是modules嵌套。本系统里需要共享的状态只有三块登录凭证、当前答题草稿、量表元数据缓存。// src/stores/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , role: , profile: null, }), getters: { isLogin: (s) !!s.token, }, actions: { setLogin(payload) { this.token payload.token this.role payload.role this.profile payload.profile localStorage.setItem(token, payload.token) }, logout() { this.token this.role this.profile null localStorage.removeItem(token) }, }, })答题草稿不要放 localStorage量表作答属于敏感数据落在本地磁盘上等于多一份不可控副本。做法是放在内存 store 里配合beforeunload提示页面刷新即丢失代价是用户体验略差但对心理测评来说这个取舍是划算的。3.4 打包后布局异常的四个高频原因本地开发正常、npm run build之后错位基本逃不出这四类。现象原因处理方式白屏资源 404base仍是/部署在子路径vite.config.js里设base: /psy/刷新页面 404history 模式缺少回退Nginx 加try_files $uri $uri/ /index.html弹窗、图表样式错乱组件库按需引入时样式未随包抽取保留unplugin-vue-components的resolvers别手删 import高度塌陷、滚动条消失height: 100%依赖父级链打包后 CSS 顺序变化根容器改min-height: 100vh# 前端静态资源的回退配置history 路由必须加 location / { root /var/www/psy-frontend/dist; try_files $uri $uri/ /index.html; }排查顺序建议固定下来先看 Network 面板资源是否 200再看 Elements 里类名是否真的存在最后才怀疑 CSS 优先级。多数「打包后布局异常」在第二步就能定位是样式没被打进产物而不是布局代码写错了。4. 心理健康管理系统的量表计分、雷达图与报告导出4.1 正向反向计分与因子分的计算实现计分规则要在前后端保持一致实际做法以后端为准、前端只做即时预览。// src/utils/score.js // raw: {题号: 选项分值} scale: {options, factor_map, reverse_items} export function calcScore(raw, scale) { const max scale.options.length // 例如 5 级评分 const factor {} let total 0 for (const [no, val] of Object.entries(raw)) { const itemNo Number(no) // 反向题5 级量表里 5 计 11 计 5 const score scale.reverse_items.includes(itemNo) ? max 1 - val : val total score for (const [name, items] of Object.entries(scale.factor_map)) { if (items.includes(itemNo)) factor[name] (factor[name] || 0) score } } // 因子分 该因子总分 / 该因子题目数便于跨量表比较 const factorScores {} for (const [name, items] of Object.entries(scale.factor_map)) { factorScores[name] Number(((factor[name] || 0) / items.length).toFixed(2)) } return { total, factorScores } }max 1 - val是反向计分的通用写法换成 4 级或 7 级量表都不用改逻辑只要options.length正确。除数是因子题目数而不是总题数这样不同因子之间可以直接画在同一张雷达图上。后端 Python 版本保持完全相同的公式两边对不上时以接口返回的factor_scores覆盖前端预览值。4.2 用 ECharts 画因子分雷达图与历次趋势雷达图适合展示 6 到 9 个因子超过 9 个维度标签会互相压盖改用条形图更清楚。import * as echarts from echarts export function renderRadar(el, factorScores) { const chart echarts.init(el) chart.setOption({ radar: { indicator: Object.keys(factorScores).map((name) ({ name, max: 5 })), radius: 65%, }, series: [{ type: radar, data: [{ value: Object.values(factorScores), name: 本次测评, areaStyle: { opacity: 0.25 }, }], }], }) // 容器尺寸变化时手动 resize打包后按需引入不会自动监听 window.addEventListener(resize, () chart.resize()) return chart }max: 5要与量表级数一致否则因子分 3.2 在雷达图上会看起来接近满分。resize必须手动绑组件卸载时记得移除监听否则路由来回切换会累积多个监听器表现为页面越用越卡。4.3 StreamingHttpResponse 导出报告时 content_type 与 content_disposition 怎么给报告导出用流式响应边生成边下发避免一次性把整份 PDF 或 CSV 拼进内存。from urllib.parse import quote from django.http import StreamingHttpResponse, HttpResponseForbidden from django.shortcuts import get_object_or_404 from assessment.models import AssessmentRecord def export_report(request, record_id): record get_object_or_404(AssessmentRecord, pkrecord_id) if not can_view(request.user, record): # 本人、咨询师、管理员三类角色 return HttpResponseForbidden() def rows(): yield 因子,得分\n for name, val in record.factor_scores.items(): yield f{name},{val}\n filename f测评报告_{record_id}.csv resp StreamingHttpResponse(rows(), content_typetext/csv; charsetutf-8) # 中文文件名必须 URL 编码并放在 filename*UTF-8 里 resp[Content-Disposition] fattachment; filename*UTF-8{quote(filename)} resp[X-Accel-Buffering] no # 经 Nginx 反代时关闭缓冲 return resp参数推荐取值说明content_typetext/csv; charsetutf-8不写 charset 时中文列名会乱码写错成text/html浏览器会直接渲染Content-Dispositionattachment; filename*UTF-8...attachment触发下载inline是浏览器内打开含中文文件名用filename*而非filenameX-Accel-Bufferingno只在经过 Nginx 时生效不加会导致响应被整段缓冲流式失去意义生成器函数里不要做数据库查询一次性把数据读进内存再 yield否则每条记录都会触发一次 SQL。导出动作本身要写审计日志谁在什么时间下载了谁的报告这条记录在隐私合规检查时是硬性要求。5. 上线前的隐私回归与 Waitress Nginx 部署校验Windows Server 上跑 Django 最省事的组合是 Waitress 加 Nginx 反代不依赖额外编译环境。# 关掉 DEBUG 后启动线程数按 CPU 核数的 2 到 4 倍给 set DJANGO_SETTINGS_MODULEpsy_project.settings.prod waitress-serve --listen127.0.0.1:8000 --threads8 psy_project.wsgi:applicationserver { listen 80; server_name psy.example.com; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 120s; # 报告导出较慢默认 60s 容易被截断 } location / { root /var/www/psy-frontend/dist; try_files $uri $uri/ /index.html; } }上线前必做的一轮回归重点不是功能而是数据口径。# assessment/management/commands/audit_records.py from django.core.management.base import BaseCommand from assessment.models import AssessmentRecord class Command(BaseCommand): help 核查测评记录总数与软删除数量防止误物理删除 def handle(self, *args, **options): total AssessmentRecord.objects.count() soft_deleted AssessmentRecord.objects.filter(is_deletedTrue).count() active AssessmentRecord.objects.filter(is_deletedFalse).count() self.stdout.write(f总数 {total}软删除 {soft_deleted}有效 {active}) if active 0 and total 0: self.stderr.write(有效记录为零检查是否误执行了物理删除)python manage.py audit_records # 记录数对不上台账时立即人工核查 python manage.py check --deploy # 检查 DEBUG、SECRET_KEY、HTTPS 相关配置再补三个具体动作。一是把 admin 路径从/admin/换掉后台是心理档案的集中出口默认路径等于公开入口。二是接口返回一律走序列化器禁止在视图里直接JsonResponse(record.__dict__)那会把answers原样吐出去。三是压测只打只读接口/api/records/这类查询接口用 50 并发跑 10 分钟观察 Nginx 的proxy_read_timeout是否触发写接口不要压测评数据的写入顺序比吞吐重要得多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价