ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django+Vue心理健康管理系统毕设源码实战:模型、预约与部署排错

Django+Vue心理健康管理系统毕设源码实战:模型、预约与部署排错 简介这是一份面向计算机专业毕业设计的完整论文文档聚焦基于B/S架构的大学生心理健康管理系统适合准备毕设、课程设计或论文撰写的本专科学生参考。系统采用Django后端与Vue前端数据库为MySQL按管理员、学生、心理导师三类角色展开需求涵盖个人信息修改、用户与导师管理、专题辅导、辅导预约、咨询信息、导师回复、热门文章、心理测评等模块并加入首页最新信息推送设计兼顾功能完整性与交互体验。压缩包内仅1个docx文件约3.94MB包含摘要、目录、绪论、系统开发技术、需求分析与数据库设计等章节可作为论文结构模板、功能清单与数据库设计思路直接借鉴。目前已有75人学习下载适合需要快速确定选题方向、梳理系统模块、撰写技术章节并完成论文排版的读者参考。1. 从一份毕业设计源码看 DjangoVue 心理健康管理系统的落地路径带过几届毕设答辩之后会发现「大学生心理健康管理系统」是出现频率最高的选题之一需求描述听起来不算硬做起来却处处有坑。三个角色的数据边界要分清楚、辅导预约不能重复占号、心理测评要算分、咨询记录还牵扯隐私很多同学卡住的不是页面写不出来而是模型关系和状态流转没想明白最后用一堆 if-else 硬怼改一处崩三处。这套源码给了一条相对完整的路径MySQL 存数据Django 用 MVT 分层做业务和数据出口Vue 承担学生端、导师端的交互管理员在后台做用户、专题辅导、热门文章、心理测评的维护首页再把最新信息推出来。它适合正在做同类毕设、或者第一次把 Django 和 Vue 拆成前后端两个工程来部署的人。下面按模型、接口、前端、业务难点、部署的顺序拆一遍重点放在能抄下来就跑的参数和排错顺序上。2. Django 侧的数据模型与 App 划分2.1 三个角色为什么不建议塞进一个 App新手最常见的做法是建一个app把学生、导师、管理员、预约、测评、文章全写进models.py几百行堆在一起。跑到后期就会发现makemigrations生成的迁移文件互相耦合删一个字段全表报错。更稳的切法是把「人的身份」和「业务对象」分开身份用一个 App业务按领域再拆。心理导师本身也是登录用户所以不要给他单独建一张用户表而应该复用 Django 的用户体系通过角色字段区分。App 名称承担的模型说明usersUser自定义、TutorProfile登录、角色、导师的职称/擅长方向appointmentSlot、Appointment可预约时段与预约单counselConsult、Reply咨询信息与导师回复assessPaper、Question、AnswerRecord心理测评试卷、题目、作答记录contentArticle、Category、Banner热门文章、文章类型、首页推送自定义用户模型建议在项目第一次migrate之前就定下来中途替换AUTH_USER_MODEL会很痛苦。# users/models.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): ROLE_CHOICES ( (admin, 管理员), (student, 学生), (tutor, 心理导师), ) role models.CharField(角色, max_length10, choicesROLE_CHOICES, defaultstudent) real_name models.CharField(姓名, max_length20, blankTrue) phone models.CharField(手机号, max_length11, blankTrue) class Meta: db_table sys_user # 显式指定表名方便 DBA 直接查 verbose_name 用户 class TutorProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_nametutor_profile) title models.CharField(职称, max_length30) field models.CharField(擅长方向, max_length50) # 如情绪管理、人际关系 def __str__(self): return self.user.real_namerole用choices而不是布尔字段是为了后面扩展第四个角色比如辅导员时不用改表结构OneToOneField保证一个导师账号只能挂一份档案related_name一定要写否则反向查询只能写tutorprofile很难读。2.2 预约表的关键字段与唯一约束预约是这个系统里唯一会出现并发写的地方。学生 A 和学生 B 同时点同一个导师的 14:00 时段如果只靠前端置灰后端照样能插两条记录。字段设计上把「时段」独立成表比直接用两个时间字段更省事导师一次排班可以批量生成若干 Slot。# appointment/models.py class Slot(models.Model): tutor models.ForeignKey(users.TutorProfile, on_deletemodels.CASCADE, related_nameslots) start_time models.DateTimeField(开始时间) end_time models.DateTimeField(结束时间) is_open models.BooleanField(是否开放, defaultTrue) class Meta: unique_together (tutor, start_time) # 同一导师同一时刻只能有一个时段 class Appointment(models.Model): STATUS ( (pending, 待确认), (confirmed, 已确认), (canceled, 已取消), (done, 已完成), ) student models.ForeignKey(users.User, on_deletemodels.CASCADE, related_nameappointments) slot models.OneToOneField(Slot, on_deletemodels.CASCADE, related_nameappointment) status models.CharField(状态, max_length10, choicesSTATUS, defaultpending) reason models.TextField(预约说明, blankTrue) created_at models.DateTimeField(auto_now_addTrue)slot用OneToOneField是最省心的写法一个时段最多被一条预约占用数据库层面直接卡死重复占号不需要在视图里写查询判断。代价是「取消后时段要能重新开放」所以取消操作不能删 Appointment而是把状态改成canceled同时把 Slot 的关联解除——如果业务要求取消后能再次预约就把OneToOneField换成ForeignKey 应用层校验或者干脆取消时删除这条 Appointment保留一条日志表做审计。注意auto_now_addTrue的字段在后台管理和接口里都是只读的想做「手工补录历史预约」时会很别扭测试阶段可以临时改成defaulttimezone.now。2.3 心理测评的题目与计分放在哪一层心理测评模块的典型结构是一张试卷Paper挂多道题Question每题若干选项每个选项对应分值学生提交后生成一条 AssessmentRecord里面存总分和结论区间。计分逻辑不要写在序列化器里也不要写在前端放在模型的类方法中最容易复用导出报表时也能直接调。# assess/models.py class Question(models.Model): paper models.ForeignKey(Paper, on_deletemodels.CASCADE, related_namequestions) content models.CharField(题干, max_length200) score models.PositiveSmallIntegerField(分值, default0) class AssessmentRecord(models.Model): student models.ForeignKey(users.User, on_deletemodels.CASCADE) paper models.ForeignKey(Paper, on_deletemodels.CASCADE) total_score models.PositiveSmallIntegerField(default0) result_text models.CharField(结论, max_length100, blankTrue) def calc(self, question_ids): 根据作答题目重新计算总分 self.total_score Question.objects.filter( id__inquestion_ids, paperself.paper ).aggregate(smodels.Sum(score))[s] or 0 self.result_text self.paper.level_of(self.total_score) self.save(update_fields[total_score, result_text])aggregate一次查询拿到总分比在 Python 里循环sum()更省内存update_fields只更新两列避免整行覆盖。level_of是 Paper 上的一个方法负责把分数映射到「正常 / 轻度 / 中度」这类区间描述——把区间配置放数据库还是写死在代码里取决于导师是否需要自己调整阈值毕设场景写死更省事但要在论文里说明局限。3. 接口层DRF 视图集、序列化器与权限3.1 序列化器嵌套太深会拖慢列表接口预约列表页通常要同时显示学生姓名和导师姓名。如果序列化器里直接嵌UserSerializer一页 10 条就要额外查 20 次用户表典型的 N1。更轻的做法是用source把需要的字段平铺出来。# appointment/serializers.py from rest_framework import serializers from .models import Appointment class AppointmentListSerializer(serializers.ModelSerializer): student_name serializers.CharField(sourcestudent.real_name, read_onlyTrue) tutor_name serializers.CharField(sourceslot.tutor.user.real_name, read_onlyTrue) start_time serializers.DateTimeField(sourceslot.start_time, read_onlyTrue) status_display serializers.CharField(sourceget_status_display, read_onlyTrue) class Meta: model Appointment fields [id, student_name, tutor_name, start_time, status, status_display, reason, created_at]source支持点号跨关系get_status_display是 Django 为choices自动生成的方法能直接拿到中文状态。列表接口里再配select_related一次 SQL 就能把三层关系拉齐class AppointmentViewSet(viewsets.ModelViewSet): serializer_class AppointmentListSerializer permission_classes [IsAuthenticated] filter_backends [DjangoFilterBackend] filterset_fields [status] def get_queryset(self): qs Appointment.objects.select_related(student, slot__tutor__user) user self.request.user if user.role student: return qs.filter(studentuser) if user.role tutor: return qs.filter(slot__tutor__useruser) return qs # 管理员看全部 def perform_create(self, serializer): serializer.save(studentself.request.user, statuspending)get_queryset是数据隔离的第一道闸学生只能看到自己的预约导师只能看到挂在自己名下的预约管理员不受限。perform_create里强制把当前登录用户写进student即使前端传了别人的 ID 也会被覆盖掉这是比在序列化器里做校验更难绕过的一层。3.2 权限矩阵与对象级校验列表级过滤只解决「看得到什么」删除和修改还要处理「能不能动别人那条」。把角色和动作列成表写代码时对照着来比事后补漏可靠。资源学生心理导师管理员预约单建/看/取消自己的看/改自己名下的状态全部咨询信息建/看自己的看分配给自己的全部导师回复只读建/改自己的全部热门文章只读只读增删改心理测评试卷只读只读增删改对象级校验依靠has_object_permissionfrom rest_framework import permissions class IsOwnerOrAdmin(permissions.BasePermission): def has_object_permission(self, request, view, obj): if request.user.role admin: return True if request.method in permissions.SAFE_METHODS: return obj.student_id request.user.id return obj.student_id request.user.id and obj.status pendingSAFE_METHODS覆盖 GET、HEAD、OPTIONS读操作放宽写操作额外要求状态还是pending已经确认的预约学生不能自己改必须走取消再重约。这段逻辑写在权限类里比在 ViewSet 的update方法里加 if 更容易在多个视图间复用。3.3 首页最新推送接口的分页与缓存首页推送的是「最新的文章 最近的专题辅导」数据量小但访问频次最高被放在首页就意味着每次打开都要查一次。做法是单独开一个只读接口限制返回条数并加短时缓存。from django.core.cache import cache from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import AllowAny from rest_framework.response import Response api_view([GET]) permission_classes([AllowAny]) def home_feed(request): data cache.get(home_feed) if data is None: articles Article.objects.filter(is_topTrue).values( id, title, cover, created_at)[:8] data {articles: list(articles)} cache.set(home_feed, data, 300) # 5 分钟过期 return Response(data)values()直接返回字典比走序列化器轻[:8]的切片会翻译成 SQL 的 LIMIT不会把整表拉到内存。缓存的过期时间要跟运营节奏匹配——文章一天更新一次300 秒足够如果后台一发文就要立刻可见得在保存文章的信号里cache.delete(home_feed)主动失效否则会出现「后台发了、首页没变」的经典问题。4. Vue 前端路由、请求封装与测评答题页4.1 起项目与开发期跨域Django 默认跑在 8000Vue 开发服务器跑在 5173浏览器直接发请求会被同源策略拦掉。开发期最省事的方案是在 Vite 里配代理不用改 Django 的 CORS 配置。// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true, rewrite: path path.replace(/^\/api/, /api) // 后端路由本身带 /api 时保持不变 } } } })target指向本机 DjangochangeOrigin会把请求头的 Host 改成目标地址避免后端做域名校验时被拒。前端所有请求统一走/api前缀打包上线后由 Nginx 把这个前缀转发给后端本地和线上行为一致不用维护两套 baseURL。4.2 axios 封装与登录态注入token 存哪里是个老问题。localStorage 简单但容易被 XSS 读走httpOnly Cookie 更安全但前后端分离下要处理 CSRF。毕设场景普遍用 localStorage 加短有效期关键是统一封装别在每个页面里手写 header。// src/utils/request.js import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) config.headers.Authorization Bearer ${token} return config }) service.interceptors.response.use( res res.data, err { if (err.response?.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(err.response?.data?.detail || 请求失败) return Promise.reject(err) } ) export default service请求拦截器负责注入 token响应拦截器统一处理 401 和错误提示页面里就只需要写const res await getAppointments()。把 401 处理放在拦截器里能避免「token 过期后十几个页面各弹一次错误」的混乱。4.3 路由守卫与菜单按角色渲染三个角色的菜单完全不同如果只靠隐藏菜单项、路由不拦学生手动敲地址照样能进后台页面。路由路径允许角色组件/login全部含未登录Login.vue/home全部Home.vue/appointmentstudent、tutorAppointment.vue/assessstudentAssess.vue/admin/usersadminUserManage.vue// src/router/index.js router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.meta.public) return next() if (!token) return next(/login) if (to.meta.roles !to.meta.roles.includes(role)) return next(/home) next() })守卫只做「能不能进」真正的数据权限仍然由后端返回前端拦截器拦不住直接调接口的人。这一点在写论文的系统安全章节时值得单独提一句。4.4 测评答题页的作答状态设计答题页的坑在于题目是分页加载的用户可能中途返回上一页改答案。用一个以题目 ID 为 key 的对象存答案比用数组按下标存更稳因为下标会随着分页或排序变化错位。const answers reactive({}) // { questionId: optionScore } const loading ref(false) async function submit() { const payload Object.entries(answers).map(([qid, score]) ({ question: Number(qid), score })) if (payload.length ! total.value) return ElMessage.warning(还有题目未作答) loading.value true try { const res await submitPaper({ paper: paperId, items: payload }) router.push({ name: assess-result, params: { id: res.id } }) } finally { loading.value false } }提交前校验必答数量防止空卷入库loading用来禁用提交按钮避免连点产生两条记录。后端接收items后按第 2 章里的calc重新算分不信任前端传来的总分——这是防篡改最基础的一步。5. 业务难点预约冲突、回复状态机与推送实时性5.1 防重复占号的三种解法对比同一个导师的时段被两个人抢是小系统里最典型的并发问题。三种做法的取舍差别很大选错一种后面要返工。方案实现位置优点代价数据库唯一约束OneToOneField/unique_together绝对可靠代码最少取消后时段需额外处理事务 行锁select_for_update能做复杂校验并发高时锁等待前端置灰页面按钮体验好完全不可靠真正常用的是「唯一约束兜底 前端置灰提升体验」的组合。创建预约时把 IntegrityError 转成友好提示from django.db import transaction, IntegrityError transaction.atomic def create_appointment(student, slot_id, reason): slot Slot.objects.select_for_update().get(idslot_id, is_openTrue) if hasattr(slot, appointment) and slot.appointment.status ! canceled: raise ValueError(该时段已被预约) try: return Appointment.objects.create(studentstudent, slotslot, reasonreason) except IntegrityError: raise ValueError(手慢了该时段刚被约走)select_for_update会在这条 Slot 上加上行锁直到事务提交第二个请求会排队而不是直接写。hasattr判断反向一对一有没有值IntegrityError作为最后一道保险——即使前面判断被绕过数据库也不会存下两条。5.2 咨询信息与导师回复的状态流转咨询和回复的关系是「一条咨询下挂多条回复」方向由导师发起。状态机不复杂但必须有明确的合法迁移路径否则会出现「已关闭的咨询还能继续回复」。当前状态允许操作目标状态open导师回复 / 学生补充replied / openreplied导师继续回复 / 学生确认结案replied / closedclosed只读closedclass Consult(models.Model): STATUS ((open, 待回复), (replied, 已回复), (closed, 已结案)) student models.ForeignKey(users.User, on_deletemodels.CASCADE, related_nameconsults) tutor models.ForeignKey(users.TutorProfile, on_deletemodels.SET_NULL, nullTrue) status models.CharField(max_length10, choicesSTATUS, defaultopen) is_anonymous models.BooleanField(是否匿名, defaultFalse) def can_reply(self): return self.status in (open, replied)is_anonymous这个字段容易被忽略但对心理健康类系统很关键匿名咨询在导师端展示时要把学生姓名替换成「匿名用户 3 号」并且同一学生对同一导师的编号要保持稳定否则导师无法追踪对话上下文。on_deleteSET_NULL保证导师离职删除账号时咨询记录不会跟着消失。5.3 首页推送的数据一致性后台发了一篇置顶文章首页却还是旧的这类问题十有八九是缓存没失效。除了前面的定时过期还可以用信号做主动清理from django.db.models.signals import post_save, post_delete from django.dispatch import receiver receiver([post_save, post_delete], senderArticle) def clear_home_feed(sender, instance, **kwargs): cache.delete(home_feed)信号里只做缓存删除不做耗时逻辑否则保存文章会明显变慢。如果推送内容还要包含「预约成功提醒」这类个性化信息就不能整块缓存应该把公共部分文章列表缓存、个人部分实时查或者只缓存 ID 列表再去查详情。6. 部署与排错Waitress Nginx 跑通 Django 与 Vue6.1 Windows 下用 Waitress 起 DjangoDjango 自带的runserver是单进程开发服务器挂上去没多久就卡。Windows 环境跑不了 Gunicorn依赖 fcntlWaitress 是纯 Python 实现直接 pip 装就能用pip install waitress # 在项目根目录manage.py 所在目录执行 waitress-serve --listen0.0.0.0:8000 --threads8 config.wsgi:application--listen指定绑定地址和端口--threads8按并发量调毕设规模 4 到 8 足够。config.wsgi:application要换成实际的模块路径——如果项目叫xlyy就写xlyy.wsgi:application写错会报ModuleNotFoundError这是最常见的启动失败原因。启动前记得python manage.py collectstatic把后台的静态文件收拢到STATIC_ROOT否则/admin页面会变成一片无样式的白板。6.2 Nginx 反代与 Vue 静态资源前端打包产物放 Nginx/api和/admin转发给 Waitressserver { listen 80; server_name localhost; root D:/project/dist; index index.html; location / { try_files $uri $uri/ /index.html; # history 路由刷新不 404 } location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { alias D:/project/staticfiles/; } }try_files那一行是前后端分离项目的必备项缺了它用户一刷新非首页路由就 404。alias和root的区别在于alias会把 location 前缀替换掉配静态文件目录时用alias更符合直觉。6.3 打包后布局异常的排查顺序Vue 项目npm run build之后布局崩掉通常是三类原因按这个顺序查最快先看浏览器控制台有没有 404 的 CSS 或字体文件有就说明publicPath没配Vite 里设base: ./再看有没有元素宽度全变成 0多半是父容器高度依赖 JS 计算而某次接口失败导致数据没渲染最后看是否只在首次进入时错位、点一下窗口就正常那是动态组件或图表没拿到容器尺寸需要在nextTick之后再初始化。接口 404 的排查顺序相反先看 Nginx 的error.log确认请求有没有到 Nginx再看 Django 控制台的访问日志确认有没有到后端中间的断点一目了然。绝大多数「接口 404」其实是代理路径多了一个或少了一个/api对着proxy_pass和前端baseURL数斜杠就能解决。本文还有配套的精品资源点击获取
返回列表