Django+Vue前后端分离实战:家教信息管理系统开发全流程
2026/9/12 15:47:13 网站建设 项目流程

简介:基于Python、Django与Vue技术开发的家教信息管理系统项目源码,面向正在准备毕业设计或课程设计的计算机相关专业学生,用于解决家长和学生寻找家教的平台化需求。系统采用前后端分离架构,前台提供首页、家教详情、用户中心、家教入驻等模块;后台则覆盖总览、家教管理、分类管理、标签管理、评论管理、用户管理、运营管理、日志管理和系统信息,模块划分清晰,方便直接演示或二次开发。资源包共434个文件,核心代码以Python(47个py)、Vue(44个vue)和TypeScript(49个ts)为主,辅以JavaScript、JSON、LESS、HTML等工程文件;同时还包含164个jpeg、37个jpg、28个png、38个svg等大量图片素材,压缩包整体约22.85MB。已有372人学习下载,适合作为毕业设计的完整参考方案,既能帮助理解前后端数据交互和后台管理逻辑,也能在此基础上快速搭建家教平台原型,开展功能扩展与课题文档撰写。资源包目录结构较清晰,图片素材与源码分类存放,便于按模块查看和修改。

1. 家教信息管理系统为什么值得用 Django + Vue 重写一遍

每年毕业季都会有人把“家教信息管理系统”当成纯增删改查来交差,但真正让答辩老师愿意往下看的,往往不是功能多,而是前后端职责分得清、权限边界站得住。用 Django 做后端,是因为它自带的 ORM、Admin、迁移机制能把“学员、教员、课程、订单”这四张核心表在一天内立起来;用 Vue 做前端,是因为选课、排课、订单确认这类交互天然适合组件化,状态一多,jQuery 那套会先崩。这个标题真正在讲的是:在课程设计的时间预算内,如何用最少代码搭出一个有真实业务感的系统,并且让答辩时每一页代码都能讲出理由。适合正在选题、中期被要求“不要只做 CRUD”、以及想在毕设里展示前后端分离思路的人。下面这套方案以“可复现”为准,不依赖任何虚构源码包。

2. 先把 Django 后端的地基打好:模型、权限与 Session

2.1 用 Django 原生 User 扩展出“教员”和“学员”两个角色

家教系统最容易被问倒的地方是“用户体系怎么设计”。常见做法是复用django.contrib.auth自带的User,再通过OneToOneField扩展出不同角色。不要自己去写密码哈希和登录校验,那是给自己挖坑。

# apps/accounts/models.py from django.db import models from django.contrib.auth.models import User class Profile(models.Model): ROLE_CHOICES = [ ('T', 'Tutor'), ('S', 'Student'), ] user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') role = models.CharField(max_length=1, choices=ROLE_CHOICES) phone = models.CharField(max_length=11) real_name = models.CharField(max_length=20) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.get_role_display()}:{self.real_name}"

这段代码的逻辑是:ProfileUser一对一关联,role字段用choices限定枚举值,后续视图里判断request.user.profile.role就能区分角色。参数说明中值得注意的有两点:on_delete=models.CASCADE表示删除用户时联动删除资料;related_name='profile'user.profile的访问方式更简短。这种设计的好处是 Django Admin、Session 和权限框架都可以直接使用,不用改底层认证逻辑。

2.2 四张核心表的建模与迁移顺序

家教系统的业务主线是“家长找老师 -> 排课 -> 结算”,对应模型应该是教员、学员、课程订单和排课记录。表之间不要用太多外键,能存 ID 的字段不要急着关联,先保证跑通。

# apps/courses/models.py from django.db import models from django.conf import settings class Tutor(models.Model): user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) subject = models.CharField(max_length=20, verbose_name='擅长科目') intro = models.TextField(blank=True) price_per_hour = models.DecimalField(max_digits=6, decimal_places=2) class Course(models.Model): tutor = models.ForeignKey(Tutor, on_delete=models.CASCADE, related_name='courses') name = models.CharField(max_length=50) schedule = models.JSONField(default=list, blank=True) class Order(models.Model): student = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) course = models.ForeignKey(Course, on_delete=models.CASCADE) status = models.CharField(max_length=10, default='pending') created_at = models.DateTimeField(auto_now_add=True)

执行python manage.py makemigrationspython manage.py migrate时,Django 会按依赖顺序建表。这里用settings.AUTH_USER_MODEL而非直接写User,是为了遵循 Django 官方建议:即使现在没换自定义用户模型,将来扩展也不会改出问题。schedule字段用 JSONField 是为了省去一张排期子表,课程设计的规模下足以支撑“每周几、几点”这类查询。

2.3 Admin 后台别默认使用,改造成按角色筛选

很多毕设的扣分点在于 Admin 后台既没美化、也没限制,读题老师一眼就能看到所有学生的手机号。常见做法是重写ModelAdminget_querysethas_view_permission,把超管和普通运营分开。

# apps/courses/admin.py from django.contrib import admin from .models import Tutor, Course, Order @admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display = ['id', 'student', 'course', 'status', 'created_at'] list_filter = ['status'] search_fields = ['student__username', 'course__name'] def get_queryset(self, request): qs = super().get_queryset(request) if request.user.is_superuser: return qs return qs.filter(student=request.user)

list_display控制列表页显示的列,list_filter按状态过滤,search_fields里写student__username是因为跨表查询用双下划线。重写get_queryset后,非超管只能看到自己的订单,答辩时如果被问“普通用户能看到别人的数据吗”,这一处就是最直接的回答。

3. Vue 前端从零到可联调:路由、状态与接口层

3.1 用 Vite 初始化项目并配置开发代理

Vue 部分的脚手架不推荐vue create那套 Webpack 配置,现在新项目基本都用 Vite,启动快、依赖少。初始化命令是npm create vue@latest,选上 Router 和 Pinia,TypeScript 视情况勾选。为了让前后端在开发环境共存,核心配置在于vite.config.js的代理:

// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true } } } })

代理的作用是把/api开头的请求转发给 Django,避免开发时处理跨域,也省去在 Axios 里拼完整域名。changeOrigin: true会让请求头里的 Host 变成目标地址,Django 端的CSRF_TRUSTED_ORIGINS不需要额外加localhost:5173,因为请求在服务端看来自127.0.0.1:8000。等到部署阶段再换 Nginx 的proxy_pass,开发期这套配置已经够用。

3.2 Pinia 存登录态与订单筛选条件

Vue 页面之间的状态共享用 Pinia 管理,登录信息、当前角色这类数据放进 Store,刷新页面后从localStorage恢复。下面是一个简化版 Store:

// src/stores/auth.js import { defineStore } from 'pinia' export const useAuthStore = defineStore('auth', { state: () => ({ token: localStorage.getItem('token') || '', role: localStorage.getItem('role') || '' }), actions: { setToken(token, role) { this.token = token this.role = role localStorage.setItem('token', token) localStorage.setItem('role', role) }, logout() { this.token = '' this.role = '' localStorage.removeItem('token') localStorage.removeItem('role') } } })

同步写到localStorage是为了刷新后不丢状态,但要注意role字段决定了导航菜单渲染哪些项。演示时可以直接在 DevTools 里改role观察路由守卫的拦截效果,这一手在答辩时很加分。另外注意不要把密码存进localStorage,只存服务端下发的 token。

3.3 用路由守卫区分“访客、学员、教员”三类页面

Vue Router 的beforeEach是控制页面访问权的关键。meta.requiresRole配合 Store 里的角色判断,比在每个页面里写if判断更整洁。

// src/router/index.js router.beforeEach((to, from, next) => { const auth = useAuthStore() if (to.meta.requiresAuth && !auth.token) { next('/login') } else if (to.meta.requiresRole && to.meta.requiresRole !== auth.role) { next('/403') } else { next() } })

meta.requiresRole需要在路由表里逐条标记,例如/tutor/orders标为requiresRole: 'T'/student/courses标为'S'。路由守卫只做了前端拦截,真正的数据权限还得靠 Django 接口二次校验,前端拦截的意义是减少无效请求、提升体验,不要指望它保证安全。

3.4 封装 Axios 拦截器统一处理错误提示

每写一个页面就axios.get一次的做法在答辩时会被追问“错误处理在哪里”。正确做法是封装一个request.js,统一注入 token、统一拦截 401。

// src/utils/request.js import axios from 'axios' import { useAuthStore } from '@/stores/auth' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const auth = useAuthStore() if (auth.token) { config.headers.Authorization = `Bearer ${auth.token}` } return config }) service.interceptors.response.use( response => response.data, error => { if (error.response?.status === 401) { const auth = useAuthStore() auth.logout() router.push('/login') } return Promise.reject(error) } )

拦截器里error.response?.status用了可选链,避免接口 500 时取不到response导致二次报错。这套封装的另一个好处是后续接 JWT 或 Token 鉴权时,只需要改请求拦截器一处,业务代码不用动。

4. 前后端数据打通:DRF 视图、跨域与文件上传

4.1 用 Django REST Framework 写第一个只读接口

虽然标题只写了 Django,但做前后端分离时不用 DRF 的话,手写 JSON 序列化非常痛苦。DRF 的ModelViewSetRouter四行代码就能暴露一个完整 REST 接口。

# apps/courses/views.py from rest_framework import viewsets, permissions from .models import Course, Order from .serializers import CourseSerializer, OrderSerializer class CourseViewSet(viewsets.ReadOnlyModelViewSet): queryset = Course.objects.select_related('tutor__user').all() serializer_class = CourseSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly]

ReadOnlyModelViewSet自带listretrieve,适合课程列表这种不需要写的接口。queryset里用了select_related,因为Course外键到了TutorOneToOne到了User,不加这个优化,列表页渲染 50 条课程会产生 50 次附加查询,速度差异在浏览器 Network 面板里肉眼可见。

配套的序列化器指定字段时,要注意不能直接把User对象抛给 JSON:

# apps/courses/serializers.py class CourseSerializer(serializers.ModelSerializer): tutor_name = serializers.CharField(source='tutor.user.username', read_only=True) class Meta: model = Course fields = ['id', 'name', 'tutor_name', 'schedule']

source='tutor.user.username'这个写法可以在序列化器里跨模型取字段,比在视图里拼接更干净。只暴露需要的字段,等于变相避免了把intro这类大段文本全部返回给前端。

4.2 DRF 的 token 认证与 Session 共存配置

课程设计阶段不需要上 JWT 全家桶,DRF 的TokenAuthentication就够用。在settings.py里加入rest_framework.authtoken应用,执行迁移后,为用户创建 token 的接口可以通过obtain_auth_token视图实现。

# config/urls.py from rest_framework.authtoken.views import obtain_auth_token urlpatterns = [ path('api/auth/login', obtain_auth_token, name='api_login'), ]

前端登录页调用这个接口,传usernamepassword,返回的 token 存进 Pinia。注意 DRF 默认认证顺序里SessionAuthentication排在前面,如果settings.py里的DEFAULT_AUTHENTICATION_CLASSES没配置,浏览器访问会出现 CSRF 报错。所以要么显式声明认证类:

REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.TokenAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ] }

加上这个配置后,Admin 后台功能不受影响,接口统一走 Token。需要说明的参数是DEFAULT_PERMISSION_CLASSES设为全站需登录后,像课程列表这类公开数据需要在视图类里单独放宽,避免用户没登录连主页都打不开。

4.3 Django 接收 Vue 上传头像的后端代码

教员头像上传是家教系统里的常见功能。Vue 端用FormData提交,Django 端接收时要注意request.FILES的键名与前端字段一致。

# apps/accounts/views.py from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated @api_view(['POST']) @permission_classes([IsAuthenticated]) def upload_avatar(request): file = request.FILES.get('avatar') if not file: return Response({'error': 'no file'}, status=400) ext = file.name.split('.')[-1].lower() if ext not in ['jpg', 'png', 'jpeg', 'webp']: return Response({'error': 'unsupported type'}, status=400) profile = request.user.profile profile.avatar.save(f'avatar_{request.user.id}.{ext}', file) return Response({'url': profile.avatar.url})

这里有一个关键参数:profile.avatar.save(name, file)ImageField的方法,它会把文件写入MEDIA_ROOT,并在数据库里保存相对路径。如果直接执行file.save()是无效的;如果MEDIA_ROOT没配置,文件会写到当前目录。判断文件类型不能只看扩展名,但作为课程设计,扩展名校验已经能通过“有无做过基本检查”的提问。

4.4 用 Django Signals 一键生成测试数据

答辩前最忌讳手动在 Admin 里一条条加数据。用一个 Django 命令脚本生成 30 条课程、10 个教员、20 个订单记录,能在现场演示时快速展示列表、筛选和图表。

# apps/courses/management/commands/seed_data.py from django.core.management.base import BaseCommand from django.contrib.auth.models import User from apps.courses.models import Tutor, Course class Command(BaseCommand): help = 'Generate demo data' def handle(self, *args, **options): for i in range(10): user = User.objects.create_user(f'tutor{i}', password='pass123') tutor = Tutor.objects.create(user=user, subject='math') Course.objects.create(tutor=tutor, name=f'math-{i}', schedule=[i % 5]) self.stdout.write('done')

把文件放到management/commands/目录后,执行python manage.py seed_data即可。这个脚本里使用了create_user而非create,是因为后者不会对密码做哈希。写注释时把这一步写清楚,答辩时被问到“为什么密码不存明文”就能直接回答。

4.5 联调时常见的 3 个报错排查顺序

前后端联调第一类报错是 403,原因是 CSRF。Token 认证下这个报错几乎只出现在没配置DEFAULT_AUTHENTICATION_CLASSES时。第二类报错是跨域,浏览器控制台提示CORS policy,这通常发生在没开代理、直连后端时。最简单的方法是用回 Vite 代理,不要引入django-cors-headers来处理,因为 Nginx 部署时还要再删一次配置。第三类报错是POST 404,原因是 Django URL 里没有对应路由,此时看后端终端日志比看浏览器 Network 更有效。

5. 答辩能让老师点头的 3 个操作:性能观测、安全检查和部署验证

最后一章不写总结,写三个当场上手就能验证的细节。

第一个操作是观测 Django 查询次数。安装django-debug-toolbar后在设置中加入debug_toolbar中间件和show_toolbar的回调函数,刷新课程列表页即可看到右侧栏的 SQL 查询次数。如果某个页面查询次数超过 20,说明存在 N+1 查询,在对应queryset里补上select_related即可优化。这是答辩中少数能现场演示“优化前后对比”的点。

第二个操作是安全配置的最小清单。确认settings.pyDEBUG = FalseALLOWED_HOSTS为准入域名、SESSION_COOKIE_HTTPONLY = True。再检查接口有没有permission_classes,比如订单删除接口必须是IsAuthenticated且对象属于当前用户。此时可准备一个 403 截图放在答辩 PPT 里,比讲十页理论更有力。

第三个操作是部署验证。如果是部署到云服务器,比如使用宝塔面板,常见做法是后端用gunicornuwsgi跑 Django,前端npm run build后把dist文件交给 Nginx。一个小技巧是:不要把dist放进 Django 的templates目录,而是在 Nginx 中配置动静分离,location /api走反向代理到后端,location /指向前端静态文件。启动后用curl -I http://域名/api/courses检查后端响应头,确认Content-Typeapplication/json即可认为前后端连通。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询