ARTICLE DETAIL

资讯详情

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

基于Django+Vue的健身房管理系统:前后端分离实战解析

基于Django+Vue的健身房管理系统:前后端分离实战解析 简介这是一套面向高校毕业设计的健身房管理系统完整方案基于PythonDjangoVueMySQL开发采用前后端分离架构适合计算机相关专业学生参考或二次开发同时也为全栈学习者提供了从需求分析、数据库设计到系统实现的完整案例。压缩包共420个文件约46.19MB涵盖Python源码、Vue组件、SQL脚本、论文文档与视频演示图片图标和批处理脚本也有收录便于对照代码与部署流程学习。目前已有1141人浏览学习可用于毕业设计选题、项目演示或健身房管理系统快速原型搭建。资源覆盖管理员、会员、员工三类角色包含会员卡管理、教练信息、健身项目、器材与活动管理等模块随附完整源码、数据库、毕业论文及视频教程可帮助理解DjangoVue的接口交互与MySQL表结构设计支持按目录部署演示。1. 健身房管理系统这个毕设题为什么值得用 Django Vue 前后端分离健身房管理系统是毕业设计里最常见的“管理系统”类题目之一但很多同学把它做成了纯增删改查的 CRUD 页面——会员表加个表单、课程表加个列表答辩时老师一问“会员卡到期提醒怎么实现”“私教课和会员怎么关联统计”现场就卡壳。这份资源不是那种糊弄型项目它是基于 Python Django Vue MySQL 的前后端分离完整工程包含可运行的源码、数据库脚本、毕业论文和视频教程覆盖会员管理、卡种管理、私教课预约、到期提醒等完整业务闭环。适合三类人正愁毕设题目想直接在此基础上扩展的在校生、想拿一套完整前后端分离项目练手补简历的初学者、以及需要快速理解 Django Vue 联调套路的一线从业者。2. Django 后端建模与接口从 models 到 REST API 的一次跑通拿到项目第一步不是急着跑前端而是把 Django 后端的模型层和接口层先啃明白。这套资源的后端核心是 Django Django REST FrameworkDRF业务模型围绕健身房的真实场景展开会员、会员卡、私教课、订单。2.1 业务模型设计会员、会员卡、私教课三张表怎么关联健身房系统的关键不在“会员表”本身而在会员卡和私教课这种带时间状态的数据。先看 models.py 里最核心的会员与会员卡部分from django.db import models class Member(models.Model): card_no models.CharField(卡号, max_length32, uniqueTrue) name models.CharField(姓名, max_length32) phone models.CharField(手机号, max_length20) gender models.SmallIntegerField(性别, choices((0, 女), (1, 男)), default1) created_at models.DateTimeField(注册时间, auto_now_addTrue) def __str__(self): return f{self.card_no}-{self.name} class MembershipCard(models.Model): TYPE_CHOICES ((1, 月卡), (2, 季卡), (3, 年卡)) STATUS_CHOICES ((0, 停用), (1, 正常)) member models.ForeignKey(Member, on_deletemodels.CASCADE, related_namecards) card_type models.SmallIntegerField(卡类型, choicesTYPE_CHOICES, default1) start_date models.DateField(开卡日期) end_date models.DateField(到期日期) status models.SmallIntegerField(状态, choicesSTATUS_CHOICES, default1) class Meta: db_table membership_card class PrivateLesson(models.Model): coach models.CharField(教练姓名, max_length32) course_name models.CharField(课程名称, max_length64) member models.ForeignKey(Member, on_deletemodels.CASCADE, related_namelessons) lesson_time models.DateTimeField(上课时间) duration models.IntegerField(课时长分钟, default60)这段设计的核心在ForeignKey的related_name。我把会员卡和私教课都挂到 Member 下用related_namecards和related_namelessons之后查询一个会员的所有卡种记录只需要member.cards.all()不需要手写 join 条件DRF 序列化时嵌套输出也方便。choices字段是给数据库写入可读性约束的前端下拉框的值直接和这些数字对应避免存字符串带来脏数据。2.2 用 DRF 把业务逻辑暴露成 REST 接口模型建好后资源里用的是 DRF 的ModelViewSet它把 list、create、retrieve、update、delete 五个动作全部内置。实际项目里我一般不会让 ViewSet 裸奔而是配一个自定义的 Serializer 控制返回字段from rest_framework import serializers, viewsets from .models import Member, MembershipCard, PrivateLesson class MemberCardSerializer(serializers.ModelSerializer): class Meta: model MembershipCard fields [id, card_type, start_date, end_date, status] class MemberSerializer(serializers.ModelSerializer): cards MemberCardSerializer(manyTrue, read_onlyTrue) class Meta: model Member fields [id, card_no, name, phone, gender, created_at, cards] class MemberViewSet(viewsets.ModelViewSet): queryset Member.objects.all().order_by(-created_at) serializer_class MemberSerializer search_fields [name, card_no, phone]Serializer 里cards MemberCardSerializer(manyTrue, read_onlyTrue)是关键。因为我前面在模型里定义了related_namecards序列化器才能直接嵌套这个关联集合前端拿到会员列表时一个接口就把会员和他的卡种信息全部返回不用再发起第二次请求。search_fields是给 DRF 的搜索后端用的URL 里带?search张三就能模糊匹配姓名、卡号、手机号。queryset里我习惯加.order_by(-created_at)否则新注册会员不排前面测试时容易误以为数据没写进去。2.3 登录鉴权用 SimpleJWT 而非 Session这个资源选的是前后端分离登录就不能用 Django 默认的 Session 机制——Vue 页面和后端不在同一个域名下Session 的 Cookie 处理会有一堆跨域问题。资源里的做法是 Django REST Framework SimpleJWT生成 token 给前端保存。settings.py 里需要做这样的配置REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: ( rest_framework_simplejwt.authentication.JWTAuthentication, ), } from datetime import timedelta SIMPLE_JWT { ACCESS_TOKEN_LIFETIME: timedelta(days1), REFRESH_TOKEN_LIFETIME: timedelta(days7), }ACCESS_TOKEN_LIFETIME设成 1 天毕设演示场景下够用也能减少刷新 token 的前端逻辑。如果做真实线上项目建议把这个值缩到 30 分钟再加 refresh 刷新流程。我这边看资源的 demo 视频登录接口走的是POST /api/token/提交 username 和 password返回access和refresh前端把 access 存到 localStorage后续每个请求在 Header 里带Authorization: Bearer token。记住一点JWT 一旦签发在过期前无法主动失效所以会员被禁用后必须改密码或等 token 过期这是毕设答辩时老师喜欢追问的一个点。3. Vue 前端对接axios 封装、路由守卫与会员模块页面后端接口通了前端才是真正让人头疼的部分。这个资源的前端基于 Vue 全家桶使用 vue-cli 创建工程配合 axios 做请求、Vue Router 做页面跳转。我拆完这套前端的第一感受是目录结构规整没有把请求散落在各个组件里这一点值得照着抄。3.1 项目初始化与开发环境代理配置找到资源里的前端目录后先执行依赖安装npm install如果 node_modules 安装缓慢可以换成 cnpm 或 pnpm。安装完成后不要直接npm run serve先看根目录的vue.config.js我用这套项目时一定会在这里加一个 devServer 代理把/api前缀转发到后端 8000 端口module.exports { devServer: { port: 8080, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } } }这段配置解决的是开发环境跨域问题。浏览器里 Vue 页面跑在 8080 端口后端跑在 8000如果不做代理前端直接请求http://127.0.0.1:8000/api/members/会被浏览器的同源策略拦死。加了代理后前端代码里写/api/members/请求会经 webpack-dev-server 转发到后端浏览器看到的请求是同源的跨域问题就没了。后端 Django 那边实际接收到的路径还是/api/members/所以接口路由本身不用变。3.2 axios 统一封装拦截器处理 token 和 401资源里所有接口请求都走封装好的 request 实例这一点值得照搬。在src/utils/request.js里做了请求拦截和响应拦截import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default requestbaseURL设置成/api后组件里调用request.get(/members/)实际请求的就是/api/members/结合上文 vue.config.js 的代理开发环境就能直接跑通。响应拦截器里判断 401 并自动跳转登录页这是前后端分离项目的一个标准兜底策略。注意response response.data这一步它把 axios 外层包裹的{data, status, headers}解掉组件里拿到的直接是后端返回的业务数据不然每次调用都要写response.data.data很容易把人绕晕。资源里所有 api 模块文件都统一放在src/api/目录下一个模块对应一组接口不直接在组件里写 URL。3.3 路由守卫与动态菜单前端页面权限通过 Vue Router 的全局前置守卫控制。看资源里的router/index.js核心逻辑是判断目标路由是否需要登录router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.isPublic) { next() } else if (token) { next() } else { next(/login) } })to.meta.isPublic是在路由配置里给登录页、注册页这类不需要鉴权的页面加的标记。比如{ path: /login, name: Login, component: () import(/views/Login.vue), meta: { isPublic: true } }这样未登录的用户访问任何业务页面都会被重定向到/login。注意这里只做了前端路由拦截真正的数据安全还得靠后端的 JWT 验证——前端拦截只是提升用户体验后端不校验接口权限的话别人拿 curl 直接调接口照样能拿到数据这是答辩避坑里要讲清楚的点。3.4 会员列表页列表加载和状态展示会员管理是健身房系统的核心页面。资源的会员列表页结构很典型表格展示会员信息、会员卡状态、操作按钮。核心代码模式是这样template el-table :datamemberList v-loadingloading el-table-column propcard_no label卡号 width140 / el-table-column propname label姓名 width120 / el-table-column label卡类型 template #default{ row } el-tag v-ifrow.cards row.cards.length 0 {{ cardTypeMap[row.cards[0].card_type] }} /el-tag el-tag v-else typeinfo无卡/el-tag /template /el-table-column /el-table /template script import { getMemberList } from /api/member export default { data() { return { memberList: [], loading: false, cardTypeMap: { 1: 月卡, 2: 季卡, 3: 年卡 } } }, created() { this.fetchList() }, methods: { async fetchList() { this.loading true try { const data await getMemberList({ page: 1, page_size: 20 }) this.memberList data.results || data } finally { this.loading false } } } } /script这个页面调用getMemberList拿数据后通过row.cards[0].card_type取当前会员的卡类型再用cardTypeMap映射成中文显示。卡类型没有硬编码在模板里而是统一放到 data 的映射对象中这是避免魔法数字的好习惯。v-loading是 Element UI 的加载指令列表接口慢的时候给用户一个反馈。注意我在fetchList里用了try/finally而不是try/catch因为错误拦截已经在 axios 响应拦截器里统一处理了组件里只需要保证 loading 状态必然被复位。4. MySQL 数据库表结构、脚本导入与 ORM 关联查询前后端代码理顺后数据库是第三个绕不开的环节。这个资源的数据库脚本是 MySQL 版本建表语句、初始化数据都打包在 sql 文件里。我把它导入本地 MySQL 后又把 Django 的 ORM 模型和表结构对照看了一遍发现了两者映射的几个关键细节。4.1 建库建表脚本的注意事项资源里的数据库脚本是手动建表的方式和 Django migrate 自动建表并存。看脚本里的会员表CREATE DATABASE IF NOT EXISTS gym_system DEFAULT CHARACTER SET utf8mb4; USE gym_system; CREATE TABLE member ( id int NOT NULL AUTO_INCREMENT, card_no varchar(32) NOT NULL UNIQUE, name varchar(32) NOT NULL, phone varchar(20) DEFAULT NULL, gender smallint DEFAULT 1, created_at datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时最值得注意的就是DEFAULT CHARACTER SET utf8mb4。MySQL 8 默认字符集才是 utf8mb45.7 及以下版本如果直接用默认配置导入中文会出现乱码。我导入前会确认数据库版本5.7 环境里执行建库脚本前先检查SHOW VARIABLES LIKE character_set_database;如果返回的不是 utf8mb4则必须先执行ALTER DATABASE gym_system CHARACTER SET utf8mb4;。card_no加唯一索引是业务层面的合法约束——健身房的会员卡号不允许重复这个约束能让后端不用在代码里检查重复数据库直接拒绝写入。4.2 从 sql 脚本导入数据拿到脚本后最稳的导入方式是在命令行执行mysql -uroot -p gym_system.sql密码输入正确后会静默完成。也可以进入 mysql 交互环境再 sourcemysql -uroot -p source /path/to/gym_system.sql;我推荐第一种方式因为第二种方式如果 sql 文件里有相对路径相关的操作比如LOAD DATA容易出问题。导入完成后验证一句USE gym_system; SHOW TABLES; SELECT COUNT(*) FROM member;如果SHOW TABLES看到的表和资源文档里列出的表清单不一致优先检查 sql 脚本头部是否有DROP TABLE IF EXISTS没有的话重复执行脚本会直接报错这不是脚本坏了而是表已经存在。至于 Django 这边资源里还有一套 migrations 文件实际运行时用python manage.py migrate会创建一套独立的表结构。这里有个坑如果同时导入了 sql 脚本又跑了 migrateDjango 自动生成的表比如auth_user会和手动建的表混在一起我一般选择只走其中一条路最省心的是直接跑 migrate 然后通过后台或 fixture 初始化数据。4.3 用 ORM 做多表关联查询而不写裸 SQLDjango 项目里尽量别手写 join 语句ORM 够用且更安全。看一个资源里会员卡续费提醒的场景需要找出所有卡在 7 天内过期的会员from datetime import date, timedelta from .models import MembershipCard, Member expire_soon date.today() timedelta(days7) # 关联查询卡状态正常且在7天内过期 members Member.objects.filter( cards__status1, cards__end_date__lteexpire_soon, cards__end_date__gtedate.today() ).distinct().prefetch_related(cards) for m in members: valid_cards [c for c in m.cards.all() if c.status 1] print(m.name, valid_cards[0].end_date)cards__status这种双下划线语法是 ORM 关联查询的通用约定它翻译成 SQL 就是通过外键字段 membership_card 表做 join条件是membership_card.status1。prefetch_related(cards)的作用是避免循环里挨个查卡信息——不加这个每打印一个会员就多一条查询语句几百个会员会造成 N1 查询问题。distinct()是因为一个会员可能有多张卡join 后可能产生重复行。这里的日期比较用的是__lte小于等于和__gte大于等于对应 SQL 的和记忆方式就是 Less Than or Equal 和 Greater Than or Equal。5. 前后端联调的四个坑跨域、数据库连接、静态文件和中文乱码我根据资源里的教程视频和实际跑通的经历把最容易卡住大家的四个问题整理成一份避坑清单。每个问题都是真实踩过的按“现象 → 原因 → 解决”的方式记录。5.1 跨域前端请求后端被浏览器拦截现象启动 Vue 开发服务器后登录接口报Access to XMLHttpRequest at http://localhost:8000/api/token/ from origin http://localhost:8080 has been blocked by CORS policy。原因浏览器同源策略拦截了 8080 端口到 8000 端口的跨域请求。只在 vue.config.js 配置了代理但后端没开 CORS 时情同此理——代理只对开发服务器生效如果前端直接请求后端 IP后端必须允许跨域。解决后端安装 django-cors-headers并在 settings.py 里做两件事。第一INSTALLED_APPS 加corsheadersMIDDLEWARE 里把CorsMiddleware放到最前面要放在CommonMiddleware之前第二配置CORS_ALLOWED_ORIGINS白名单CORS_ALLOWED_ORIGINS [ http://localhost:8080, http://127.0.0.1:8080, ]如果嫌白名单麻烦开发环境可以直接设CORS_ALLOW_ALL_ORIGINS True但上线前必须改回白名单否则任何网站都能跨域请求你的接口。5.2 数据库连接Django 连不上 MySQL 或认证报错现象python manage.py migrate时报django.db.utils.OperationalError: (1045, Access denied for user rootlocalhost)或者(1049, Unknown database gym_system)。原因两种情况最常见。第一种是密码不对或 root 只允许 localhost 登录第二种是数据库还没创建Django 的DATABASES配置里写了gym_system但 MySQL 里没有这个库。解决先确认数据库存在且密码正确mysql -uroot -p CREATE DATABASE IF NOT EXISTS gym_system CHARACTER SET utf8mb4;然后检查 Django 的settings.py中DATABASES配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: gym_system, USER: root, PASSWORD: 你的密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }MySQL 8 默认安装时 root 用户用的是 caching_sha2_password 认证Django 3.2 之前的版本连接会有问题。遇到Authentication plugin caching_sha2_password cannot be loaded时把 MySQL 用户改成 mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;现在新版本 Django 已经兼容 caching_sha2_password但这句 SQL 对老环境还是很管用的。5.3 静态文件登录后 Django 后台样式全丢现象通过前端页面登录没问题但直接访问 Django admin 后台http://127.0.0.1:8000/admin/发现页面没有任何样式全是纯 HTML。原因开发模式下DEBUGTrue时 Django 能自动服务静态文件但某些配置下STATIC_URL不对或者项目用了whitenoise但没有配置到 MIDDLEWARE导致静态文件请求返回 404。解决检查 settings.py 是否配置了静态文件路径STATIC_URL /static/ STATIC_ROOT BASE_DIR / staticfiles开发环境如果/static/请求 404确认本地是否创建了static/目录。如果目录存在依然 404看 INSTALLED_APPS 里有没有django.contrib.staticfiles——这个自带的 app 被误删是静态文件挂掉最常见的原因。到部署阶段还需要执行python manage.py collectstatic把分散在各个 app 里的静态文件收集到STATIC_ROOT否则DEBUGFalse时 admin 后台依然没样式。5.4 中文乱码接口返回“姓名”显示成乱码或问号现象数据库里存入中文后页面显示为???或类似ä¸Â的乱码字符。原因从 MySQL 到 Django 之间字符集链路断了。链路有四环MySQL 库字符集、MySQL 表字符集、Django 数据库连接字符集、前端页面字符集任何一环不是 utf8mb4 就会出乱码。???说明数据入库时已经变成乱码不是显示的问题ä¸Â这种是 UTF-8 编码被当成 Latin1 解码的典型症状。解决按顺序排查。先看库和表的字符集SELECT default_character_set_name FROM information_schema.SCHEMATA WHERE schema_name gym_system; SELECT table_name, table_collation FROM information_schema.TABLES WHERE table_schema gym_system;不是 utf8mb4 就执行ALTER DATABASE gym_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE member CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;注意CONVERT TO会重写表数据大表操作前先备份。Django 这边在DATABASES的OPTIONS里显式加charset: utf8mb4这一步能解决连接层乱码。改配置后重启 Django 进程再测避免缓存误导判断。6. 验收路径管理员-会员-教练三视角怎么测才不留死角答辩前建议按“角色视角”而不是“页面顺序”去验收。因为毕设答辩时老师不会按你的菜单点他很可能随意问“教练能查到自己的课表吗”“新会员办了卡前台能看到吗”。我从这套项目里总结出一条三视角自检路径每次接手类似项目都会强制走一遍。管理员视角登录后看首页统计数字是否与数据库一致选一个会员删除确认关联的卡记录是否按预期处理新增一个卡种去前端下拉框确认新卡种立即出现。会员视角注册新会员开一张月卡把日期改到快过期确认到期提醒出现修改会员手机号后刷新列表确认数据没有被重置。教练视角给会员预约一节私教课确认教练在课表页面能看到这节课过课后状态变为已完成。这三个视角本质是在验证三层逻辑管理员验证写操作和数据统计会员验证状态流转和时间计算教练验证多角色下的数据隔离。资源里的接口自测也有现成路径我习惯用 curl 快速验证核心接口curl -X POST http://127.0.0.1:8000/api/token/ \ -H Content-Type: application/json \ -d {username: admin, password: admin123}拿到access字段后记住 token 加上Authorization头调用会员接口curl http://127.0.0.1:8000/api/members/ \ -H Authorization: Bearer 你的token如果返回 200 且中文显示正常说明 JWT 鉴权、跨域、数据库连接、字符集这一整条链路都是通的如果返回 401先看 token 是否过期再看 Header 拼写是不是Bearer后面缺了空格——这个空格我第一次联调时漏掉排查了半小时才定位。从那次以后我每次接到 Django Vue 项目都强制走一遍这个接口签名校验和角色验收路径再没在答辩现场翻过车。希望帮到你。本文还有配套的精品资源点击获取
返回列表