前段时间帮朋友做了一套健身房管理系统技术栈锁定在 Python Vue后端用的 DjangoIDE 用的是 Pycharm中间也仔细对比过 Flask。整套系统从会员建档、课程表安排、私教预约到月末营收报表全部跑通上线现在前台每天就靠它排课和验卡。这篇文章把设计和实现里的关键决策、核心代码和踩坑记录整理出来给准备用 PythonVue 做管理类系统或者正在纠结 Django/Flask 怎么选的朋友一个参考。不管你是课程设计、毕业设计还是真想给自家健身房做一套内部工具这套思路都适用。1. 项目定位与技术选型为什么是 PythonVue1.1 这个系统到底解决了什么问题健身房日常运营最烦人的不是“没人来”而是数据太散。今天会员来了要开卡教练要排课会员要预约私教前台要记录签到月底还要算教练课时费。传统做法是Excel加微信群一张卡到期了没人提醒预约冲突了前台自己协调。这套系统的核心就是把“人、卡、课、约、销”五条线统一管起来。我按角色拆了一遍流程管理员关心营收和卡过期情况前台需要快速开卡、续卡、签到教练要看自己的排课表会员要能预约课程、查看剩余次数。所以系统并不是单纯做增删改查而是在增删改查之上把业务规则写清楚。比如“一张会员卡剩余次数为0时不能预约”“同一时间段同一会员不能重复预约”“课程已满员时自动拒绝新预约”。搞清楚这些后面写代码才有方向。1.2 Django还是Flask别纠结先看你的需求标题里同时出现 django 和 flask很多人就在这卡住了。我给一个非常实际的判断标准如果系统里有复杂的用户角色、数据模型十几个、后台管理需求多直接上 Django如果只是给一个小场景写几个 API、前端自己管状态、数据库就一两张表那 Flask 更轻便。这段对比建议保存下来对比项DjangoFlask自带ORM有功能完整没有需要自己装 SQLAlchemy自带Admin后台有开箱即用没有要自己写用户认证系统内置权限体系完善需要扩展库学习曲线稍陡但体系化平缓自由度高适合场景完整业务系统轻量API、原型项目中文资料量非常多非常多我的选择是 Django理由是健身房管理系统的数据关系比较重会员、卡、课程、教练、预约、签到环环相扣Django 的 ORM 和 Admin 能节省大量重复工作量。不过 Flask 并没有被扔掉后面部署章节我会单独讲 Flask 怎么处理这类项目因为有些课程设计要求用 Flask思路是通用的。顺带提一句 Flask 和 FastAPI 的比较也是后台常被问到的FastAPI 性能更好、自带接口文档但生态相对年轻Flask 老牌稳定、扩展丰富。如果你做的系统未来要应对高并发接口FastAPI 值得尝试如果追求“稳”和“资料好找”Flask 更省心。三者选谁都不丢人关键是项目体量匹配。1.3 开发工具选型Pycharm带来的效率编辑器之争我不参与但做 Python 后端Pycharm 的体验确实省事。专业版自带 Django 支持新建项目时可以直接选 Django自动生成 manage.py、settings.py、urls.py 这些骨架省去手敲命令和手动建目录的功夫。调试功能也强断点打在 ORM 查询、请求视图上能看到每一步的变量变化。社区版是免费的功能也够用。我一般这样分工Pycharm 专门跑 Python 后端Vue 前端用 VS Code 写。不是说 Pycharm 不能写 Vue而是前端热更新、ESLint 这类体验VS Code 更顺手。如果你习惯一个工具搞定所有事Pycharm 装 Vue 插件也能用只是偶尔会卡。2. 开发前环境搭建从零把环境跑起来2.1 Python和Pycharm的安装这一步看起来基础但很多人栽在版本选择上。Python 不要追求最新建议装 3.8 到 3.12 之间的稳定版本我当时用的是 3.10。到 python.org 下载对应系统版本安装时务必勾选“Add Python to PATH”不然后面执行 python 命令会提示找不到。装完验证一下python --version pip --version然后安装 Pycharm从官网下载 Community Edition 就好社区版免费使用日常开发完全够。注意一个细节Pycharm 新建项目时要选好解释器建议选“New environment using Virtualenv”这样每个项目的依赖包都隔离在独立虚拟环境里不会出现 A 项目需要 pandasB 项目一升级把 A 搞挂的情况。如果你是第一次装打开 Pycharm 后进入 Settings - Project - Python Interpreter能看到当前项目用的解释器和已安装包列表。以后 pip 安装第三方库都能在这里确认是否安装成功。2.2 创建Django项目和应用后端骨架我习惯用命令行创建这样做每一步都透明。先建项目django-admin startproject gym_project cd gym_project python manage.py startapp members python manage.py startapp courses python manage.py startapp appointmentsstartapp 就是“创建app”很多人分不清 project 和 app 的关系。project 是整个系统的配置中心app 是业务模块。比如 members 管会员courses 管课程appointments 管预约每个 app 都有自己的 models、views、urls互不干扰。创建完 app 后一定要去 settings.py 里的 INSTALLED_APPS 注册INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, members, courses, appointments, ]接着改语言和时区这两个不改成中文环境的话Admin 后台全是英文也不符合国内使用习惯LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ True首次运行前先执行数据库迁移SQLite 起步不需要额外配置python manage.py migrate python manage.py createsuperusercreatesuperuser 会创建一个管理员账号之后登录 Django Admin 就能看到默认的用户管理。这一步做完后端骨架已经能跑起来。2.3 Vue环境配置与脚手架创建前端用 Vue最先要做的是 Node.js 环境安装。去 nodejs.org 下载 LTS 版本装完验证node -v npm -vnpm 是 Node 自带的包管理器类似 pip。国内环境下直接 npm install 经常很慢建议先把下载源切到国内镜像npm config set registry https://registry.npmmirror.com然后创建 Vue 项目。Vue 官方现在推荐 Vite 脚手架npm create vitelatest gym_front -- --template vue cd gym_front npm install npm run dev跑起来后浏览器打开终端提示的地址看到 Vite 默认页面就说明环境通了。接着装项目依赖npm install vue-router4 axios element-plusvue-router 管路由axios 用来请求后端接口element-plus 是一套 Vue3 组件库表格、表单、弹窗直接拿来用做管理系统效率很高。很多人问“vue项目源码怎么发给别人”其实只要把整个项目目录打包发给对方对方装好 Node 后执行 npm install 和 npm run dev 就能跑前提是不把 node_modules 这个大目录打进去它可以用 npm install 重新生成。3. 后端核心模块设计与API实现3.1 数据模型设计会员、课程、私教怎么建模健身房管理系统的核心是数据关系。我的 models 设计大致分五张主表会员表 Member存储姓名、手机号、性别、注册日期、会员卡外键。这里有个注意点不要把卡的有效期直接存放在 Member 里因为会员可能中途续卡、换卡卡和会员应该是一对多关系。课程表 Course包括课程名称、教练外键、上课时间、可容纳人数、已预约人数。预约表 Appointment记录哪个会员预约了哪节课程再带上预约时间、状态字段。状态可以设计成“已预约 / 已上课 / 已取消”方便后续统计。签到表 CheckIn会员上课时签到记录签到时间用来计算剩余次数。卡类型表 CardType包括次卡、月卡、年卡不同卡类型对应不同有效期和总次数。用 Django 写出来是这样from django.db import models from django.contrib.auth.models import User class Member(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, nullTrue, blankTrue) name models.CharField(max_length50) phone models.CharField(max_length20, uniqueTrue) gender models.CharField(max_length10, choices[(M, 男), (F, 女)]) created_at models.DateTimeField(auto_now_addTrue) class CardType(models.Model): name models.CharField(max_length50) duration_days models.IntegerField(default30) total_times models.IntegerField(default0) class MembershipCard(models.Model): member models.ForeignKey(Member, on_deletemodels.CASCADE, related_namecards) card_type models.ForeignKey(CardType, on_deletemodels.PROTECT) start_date models.DateField() end_date models.DateField() remaining_times models.IntegerField(default0) class Coach(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, nullTrue, blankTrue) name models.CharField(max_length50) specialty models.CharField(max_length100) class Course(models.Model): name models.CharField(max_length100) coach models.ForeignKey(Coach, on_deletemodels.PROTECT, related_namecourses) start_time models.DateTimeField() capacity models.IntegerField(default10) booked_count models.IntegerField(default0) class Appointment(models.Model): member models.ForeignKey(Member, on_deletemodels.CASCADE, related_nameappointments) course models.ForeignKey(Course, on_deletemodels.CASCADE, related_nameappointments) status models.CharField(max_length20, defaultbooked) created_at models.DateTimeField(auto_now_addTrue)几个设计上的坑提醒一下金额字段不要用 FloatField价格、余额这类涉及钱的数据一律用 DecimalField否则会出现 0.1 加 0.2 不等于 0.3 的浮点问题查账时哭都来不及。外键删除策略要想清楚。会员删除了他的卡和预约记录是级联删除还是保留我建议操作记录类的数据用 CASCADE 没毛病但教练表的课程记录建议用 PROTECT防止误删教练把历史课程都带走。3.2 用户认证与权限区分管理员和会员Django 自带 auth 用户系统可以直接拿 User 表用。但健身房系统里有管理员、前台、教练、会员四种角色User 表自带字段不够用需要扩展。两种方案一种是自定义 User 模型加 role 字段另一种是建立 UserProfile 一对一扩展表。我建议在新项目最开始就把 AUTH_USER_MODEL 替换成自定义用户模型一旦数据库迁移过再改会非常痛苦。写起来是这样from django.contrib.auth.models import AbstractUser class User(AbstractUser): ROLE_CHOICES [ (admin, 管理员), (staff, 前台), (coach, 教练), (member, 会员), ] role models.CharField(max_length10, choicesROLE_CHOICES, defaultmember)然后在 settings.py 里加一行AUTH_USER_MODEL members.User注意这行配置必须在第一次 migrate 之前设置好。权限控制方面Django 自带装饰器适合简单场景from django.contrib.auth.decorators import login_required login_required def my_view(request): pass但前后端分离后Vue 端请求接口用的是 token这里我选用 Django REST Framework 加 simplejwt。登录接口返回 access 和 refresh 两个 tokenVue 端把 access token 存在本地请求时放到 Authorization 请求头里。Vue 路由通过“有没有 token”判断是否允许进入页面后端通过“token 是否有效”决定是否返回数据。3.3 常用查询、更新和删除操作这一块是“django执行查询-删除对象”热搜背后的真实需求。很多人学了 model 定义但一进 shell 就不知道怎么写查询。我给你整理一组最常用的查询所有会员members Member.objects.all()按条件筛选vip_members Member.objects.filter(cards__card_type__name年卡) today_expire MembershipCard.objects.filter(end_datedate.today())取单个对象不存在时报错member Member.objects.get(phone13800138000)排序recent_members Member.objects.all().order_by(-created_at)限制数量latest_five Member.objects.all()[:5]删除单个对象member Member.objects.get(pk1) member.delete()批量删除Member.objects.filter(created_at__ltcutoff_date).delete()更新字段单条先修改再 savemember.phone 13900139000 member.save()多条数据批量更新用 update避免循环 saveMembershipCard.objects.filter(card_type__name月卡).update(remaining_times10)这里有一个非常重要的概念QuerySet 是惰性的。filter 返回的是一个查询集真正执行 SQL 是在你遍历它、调用 list()、或者使用 if 判断时。很多人 debug 时发现“我明明 filter 了怎么没有报错也没数据”多半是没意识到惰性求值。批量删除要谨慎。如果会员被删了他名下的所有预约记录按外键 CASCADE 也会一起删除。所以删除前建议用 count() 看一眼影响范围避免把历史数据误清空。3.4 业务逻辑示例预约课程与冲突检测预约是整个系统里最容易出 bug 的环节。先理清规则会员只能预约未开始的课程同一节课不能重复预约课程已满员不能预约。如果用原生 SQL 写跨表判断很繁琐用 Django ORM 加上事务逻辑很清晰。先写一个查询判断from django.db import transaction from django.utils import timezone from datetime import date transaction.atomic def book_course(member_id, course_id): course Course.objects.select_for_update().get(pkcourse_id) if course.start_time timezone.now(): return {error: 课程已开始无法预约} if course.booked_count course.capacity: return {error: 课程已满员} if Appointment.objects.filter(member_idmember_id, course_idcourse_id, statusbooked).exists(): return {error: 不能重复预约同一课程} Appointment.objects.create(member_idmember_id, course_idcourse_id) course.booked_count 1 course.save() return {success: True}这里有个细节很多人忽略用了 select_for_update()。它的作用是给这条课程记录加锁避免两个会员同时点击预约时booked_count 被同时读到同一个值导致明明只剩一个名额两个人却都预约成功。配合 transaction.atomic 使用保证“检查库存”和“扣减库存”是一个整体要么一起成功要么一起失败。前端的提示也要配合好。后端返回的 error 信息直接给 Vue 弹窗展示用户就知道是“满员”还是“重复预约”。这种场景用 DRF 的 APIView 或者普通 JsonResponse 都能做核心是业务规则要放在后端统一校验前端只是展示。4. Vue前端从页面到交互4.1 路由与菜单结构设计Vue 前端的骨架是路由。系统页面不算太多但分成会员管理、课程预约、私教排课、营收统计几个大块。用 vue-router 配置时我习惯把路由按模块拆开而不是全部写在 main.js 里。一个典型的 index.js 配置import { createRouter, createWebHistory } from vue-router import Layout from /layout/index.vue const routes [ { path: /login, component: () import(/views/login/index.vue) }, { path: /, component: Layout, redirect: /dashboard, children: [ { path: dashboard, name: Dashboard, component: () import(/views/dashboard/index.vue) }, { path: members, name: Members, component: () import(/views/member/index.vue) }, { path: courses, name: Courses, component: () import(/views/course/index.vue) } ] } ] const router createRouter({ history: createWebHistory(), routes })懒加载用 () import() 的好处是首屏不用加载全部页面哪个模块被访问才去下载对应 JS 文件管理系统页面多的时候体验差别很明显。还有登录守卫。之前已经提到过 token路由层面要做一层拦截router.beforeEach((to, from, next) { const token localStorage.getItem(access_token) if (to.path /login) { next() } else if (!token) { next(/login) } else { next() } })这层守卫不是安全屏障因为接口层还有校验但能提升用户体验没登录就跳转登录页而不是等接口报 401。4.2 组件复用与插槽管理系统里大量重复的“搜索表单 数据表格 分页”结构。手写一遍可以但是十个页面都手写就是灾难。我抽了一个通用组件叫 TableCard接收标题、表格列配置、数据源然后通过插槽把“操作列”这个变化留给页面自己决定。Vue 插槽的写法很好用template div classcard div classcard-header span{{ title }}/span slot nameheader-actions/slot /div el-table :datatableData el-table-column v-forcol in columns :keycol.prop :propcol.prop :labelcol.label / el-table-column label操作 template #defaultscope slot nameactions :rowscope.row/slot /template /el-table-column /el-table /div /template script setup defineProps({ title: String, columns: Array, tableData: Array }) /script父页面调用时直接往插槽里塞按钮TableCard title会员列表 :columnsmemberColumns :table-datamemberList template #header-actions el-button typeprimary clickopenAddDialog新增会员/el-button /template template #actions{ row } el-button sizesmall clickeditMember(row)编辑/el-button el-button sizesmall typedanger clickdeleteMember(row)删除/el-button /template /TableCard这里的 #defaultscope 是作用域插槽父组件通过 { row } 拿到当前行的数据。很多初学 Vue 的人分不清普通插槽和作用域插槽一句话解释普通插槽只是往组件里塞内容而作用域插槽还能把子组件的数据传出来给套用的模板用。4.3 Axios封装Vue 项目里不封装 axios 会非常痛苦几十个页面都直接用 axios.get后面接口地址一变就要全局替换。我的做法是建一个 api 目录按模块分文件。先封装 axios 实例import axios from axios const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(access_token) if (token) { config.headers.Authorization Bearer token } return config }) service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(access_token) window.location.href /login } return Promise.reject(error) } ) export default service然后每个业务模块一个文件比如 member.jsimport service from /utils/request export function getMemberList(params) { return service.get(/members/, { params }) } export function createMember(data) { return service.post(/members/, data) } export function deleteMember(id) { return service.delete(/members/${id}/) }页面里调用就是import { getMemberList, deleteMember } from /api/member封装的价值不只是少写几行代码更重要的是统一处理了登录失效、错误提示、loading 状态。遇到接口报错先在拦截器里排查不要每个页面单独 try catch 写一堆重复代码。另外提一句“vue 低代码平台”的话题。如果你不想手写这些 CRUD 页面市面上确实有低代码方案可以快速生成管理后台但不管怎么生成路由、组件、接口封装这套底层逻辑是一样的。自己先动手写一遍再用低代码平台会顺手很多直接上手低代码的人遇到问题往往一头雾水。5. 联调、打包与部署5.1 跨域问题处理前端跑在 5173 端口后端跑在 8000 端口端口不同就存在跨域。开发阶段最简单的方案是用 Vite 代理在 vite.config.js 里配置export default { server: { proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } } }这样前端代码里的 /api 开头的请求都会被转发到后端浏览器看到的还是同源请求绕开了跨域限制连 CORS 头都不用配。如果分开了域名部署比如前端在 www.gym.com后端在 api.gym.com那就需要后端开启 CORS。Django 装一个 django-cors-headerssettings.py 配置INSTALLED_APPS [ corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ] CORS_ALLOWED_ORIGINS [ https://www.gym.com, ]注意 CORS_ALLOWED_ORIGINS 不要配成 *生产环境只写自己的前端域名否则任何人都能跨域调你的接口。5.2 前端打包与后端部署生产环境部署有两种思路。第一种是前后端分开部署前端打包出静态文件丢给 NginxNginx 再把 /api 请求转发给 Django第二种是把 Vue 打包后的 dist 交给 Django 托管所有请求走同一个域名。推荐前者逻辑清晰静态文件和动态请求分离排错也容易。部署前先执行npm run builddist 目录就是最终产物。然后后端生产环境不要用 python manage.py runserver那是开发服务器性能不行。用 gunicorn 跑 Djangopip install gunicorn gunicorn gym_project.wsgi:application -b 0.0.0.0:8000 --workers 3同时 settings.py 里调整DEBUG False ALLOWED_HOSTS [www.gym.com, api.gym.com]静态文件收集python manage.py collectstatic如果用的是 Nginx配置大概长这样server { listen 80; server_name www.gym.com; location / { root /var/www/gym_front/dist; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }利益相关说明生产环境部署最容易被忽略的是 ALLOWED_HOSTS。DEBUGTrue 时 Django 不校验 Host改成 False 后如果没配域名直接 400 Bad Request。5.3 Flask部署的场景如果你的课程设计或团队要求用 Flask那后端结构完全是另一套思路。Flask 没有自带 ORM通常配 SQLAlchemy没有自带 Admin可以用 Flask-Admin用户认证用 Flask-Login 或 JWT 扩展。如果只是一个轻量 API 服务Flask 代码量确实比 Django 少很多。Flask 部署比 Django 还要简单同样的 gunicorngunicorn -w 4 -b 0.0.0.0:8000 wsgi:appFlask 和 FastAPI 再对比一次如果你的后端将来要处理大量并发请求、还要自动生成 OpenAPI 文档FastAPI 更合适如果你更熟悉 Flask 的写法、项目不大维持 Flask 也是靠谱的。选型最怕的不是选错而是做一半从 Flask 换到 FastAPI重构成本太高。我的建议是“小步快跑前先定好技术栈中途不要横跳”。6. 常见问题与避坑手册6.1 Pycharm导入已有Django项目别人发给你一个 Django 项目或者你自己从 GitHub 拉下来用 Pycharm 打开后经常发现跑不起来。正确打开姿势是File - Open - 选择项目根目录然后等 Pycharm 识别。识别成功后右下角会提示配置 Python 解释器选择已有的虚拟环境目录或者新建一个虚拟环境。关键是打开后要装依赖。项目一般有 requirements.txt在终端执行pip install -r requirements.txt然后在 Pycharm 的 Run Configuration 里添加 Django server设置好 settings 路径就能一键启动。启动时报错常见有三类一是解释器没选对虚拟环境里没装 Django二是 settings.py 的数据库配置和本地不匹配三是端口被占用8000 端口被上次没关干净的服务占着改一下端口或者杀掉旧进程即可。6.2 虚拟环境与第三方库安装后台经常搜到“python安装numpy库的方法”“pycharm怎么安装pandas包”“下载第三方库htmltestrunner”这类问题本质上都是同一个操作。我想强调一个习惯不要用全局环境装包一定要在项目虚拟环境里装。Pycharm 里可以直接在 Settings - Project - Python Interpreter 里点加号搜索安装包也可以直接在项目终端执行pip install numpy pandas pip install htmltestrunner如果下载慢或者超时切到国内镜像源pip install numpy pandas -i https://pypi.tuna.tsinghua.edu.cn/simple版本问题要留意。Python 3.8 装最新版 pandas 可能会报“找不到匹配的版本”因为新版库放弃了对低版本 Python 的支持。遇见这种问题不用慌指定旧版本安装pip install pandas1.5.3还有一个容易被忽视的装完新库后一定要同步更新 requirements.txtpip freeze requirements.txt不然换一台电脑依赖列表不全项目根本跑不起来。这里也有个坑pip freeze 会把所有依赖包括传递依赖都冻进去格式能用就是有点冗余对于洁癖选手可以手工整理一下。6.3 Vue常见环境报错Vue 项目跑不起来大部分问题集中在四个方向。第一个是 Vite 创建项目后报错 “failed to load tsconfig vue/tsconfig/tsconfig.web.json”。这个错误是项目引用了 vue/tsconfig 的配置但依赖没装。解决办法是安装依赖npm install -D vue/tsconfig安装后再看 tsconfig.json 里的 extends 配置是否指向了正确的包名。第二个是“vite不是内部命令”。一般是 npm install 失败导致 node_modules 不完整或者是用了 npm run dev但 script 里根本没配 vite。先重新执行 npm install再看 package.json 里的 scripts。第三个是 npm install 报 ERESOLVE 依赖冲突。Node.js 版本和某些依赖版本不兼容时容易出现两行命令解决npm install --legacy-peer-deps第四个是运行正常但样式不对。可能原因是没有引入组件库样式Element Plus 必须在使用前引入import ElementPlus from element-plus import element-plus/dist/index.css6.4 项目交付与源码分享代码写完要交付给别人或者给老师检查最关键的是控制体积和说明文档。前端项目里 node_modules 可能几百兆后端项目里 .venv 也是体积大户。打包发送前确认 .gitignore 里排除了这两个目录node_modules/ dist/ .venv/ venv/ __pycache__/ *.pyc交付包建议包含三样源码压缩包、README.md、数据库初始化脚本。README 里至少写清Python 版本和 Node 版本要求后端安装依赖和启动命令前端安装依赖和启动命令需要修改的配置数据库连接、API 地址前端源码是怎么发给别人的问题本质就是把“环境依赖”和“运行方式”交代清楚。对方拿到压缩包后先解压后端执行 pip install -r requirements.txt前端执行 npm install两边各自启动就能跑起来。如果对方打开空白页优先检查浏览器控制台的请求是否发出后端接口是否返回 200。7. 实操心得与后续扩展建议这套系统做完我最大的体会是管理系统最难的从来不是写代码而是把业务规则想清楚。预约冲突怎么解决、过期卡怎么处理、教练离职后他的课程怎么办这些规则在数据库设计阶段不敲定后面改起来就是牵一发动全身。再说一个让我印象深刻的细节。刚开始我把“剩余次数”直接做成一个字段存着每次预约就减一。后来发现不对如果会员退了一节课或者后台误操作反签到剩余次数就对不上了。后来改成“以签到记录为准剩余次数用总次数减去已使用记录数实时计算”虽然查询稍重一点但数据不会越跑越乱。这个小调整让我明白一个道理数据表里能推导出来的字段尽量别冗余存储除非你非常清楚这个冗余会带来什么样的性能收益。如果你后续要扩展可以考虑加一个数据大屏把今日到店人数、本月营收、热门课程排行实时投到健身房前台。那时候后端接口把统计逻辑写好前端用 ECharts 图表展示整套系统的价值会和现在完全不是一个量级。我自己正在做的就是这个方向等验收完再写一篇专门讲报表统计的实践记录。