☰
Django+Flask构建个性化健身房管理系统:规则匹配与部署实践
2026/10/14 23:36:44 网站建设 项目流程

1. 为什么会做一个"个性化"的健身房管理系统

去年秋天,一位开健身房的朋友找我诉苦。他说会员流失越来越严重,办卡的人不少,但真正坚持来练的没几个。我问他有没有会员数据记录,他给我看了一个Excel文件夹——每个教练各自记一份,有的用手机备忘录,有的写在纸上。会员什么时候该续卡、体测数据变化、上过哪些课、练到什么强度,完全是一笔糊涂账。

我问他想要什么系统,他最初的需求很简单:"能记录会员信息、能管课程、能提醒续卡。"但我从他那堆乱糟糟的表格里翻了翻,发现真正有价值的事情不是"记录",而是"匹配"。有些会员办了年卡只来三次,有些会员练了三个月体重没变化,因为教练给他排的课程和身体状态完全不匹配。于是我把需求升级了一下:不仅要做一个能管理会员和课程的信息系统,还要做一个能根据每个人的身体数据、训练偏好、消费习惯给出个性化训练建议的系统。这就是"个性化健身房管理系统"的由来。

这个项目适合谁?适合正在学Python Web开发想找一个练手项目的同学,适合健身行业从业者想了解数字化管理怎么落地,也适合独立开发者接私活时参考。技术栈就是用Python生态里最主流的三样东西:Django、Flask和一套简单的推荐逻辑。

2. Django和Flask不是二选一:双框架怎么分工协作

2.1 为什么不用一个框架搞定

很多人在做管理系统时会陷入一个思维定式:选一个框架,一个项目干到底。我一开始也在Django和Flask之间犹豫。Django自带Admin后台、ORM、认证系统,做管理后台简直是开箱即用;但它的"重量级"在处理轻量的推荐接口时有点笨重。Flask灵活、轻巧,写一个推荐API服务非常顺手,但要做完整的后台管理、权限控制、表单校验,就得自己拼一堆扩展。

后来我改变思路:为什么不两个一起用?Django负责核心管理后台——会员管理、课程排期、订单记录、教练档案,这些是传统CRUD,Django的成熟生态能省大量时间。Flask负责独立的推荐API服务——摄入身体指标数据,输出个性化训练方案,这部分逻辑独立性强,未来可能要做成单独的服务给C端小程序调用,用Flask更干净。

2.2 两个服务之间的通信设计

Django跑在8000端口,Flask跑在5001端口,两者通过HTTP接口通信。Django把会员的体测数据打包发给Flask,Flask跑完推荐逻辑把结果返回给Django存库。通信这块我用的是最简单的requests库,没有引入消息队列这些重工具。原因很实在:这套系统并发量根本不会太高,一个健身房几百个会员,拿消息队列属于杀鸡用牛刀,出了问题反而难排查。

服务的目录结构是这样设计的:

gym_project/ ├── django_gym/ # Django主项目 │ ├── member/ # 会员管理模块 │ ├── course/ # 课程管理模块 │ ├── appointment/ # 预约模块 │ └── recommend_api/ # 调用Flask服务的客户端封装 └── flask_recommend/ # Flask推荐服务 ├── recommend.py # 推荐逻辑主入口 ├── rules.py # 规则匹配引擎 └── models.py # 推荐结果的数据结构定义

这样拆分之后有个明显的好处:Django的部署和Flask的部署完全隔离,哪天推荐算法重构了,只动Flask服务,不影响主业务。反过来说,Django后台改动也不会波及推荐服务。

2.3 踩到的第一个坑:跨域问题

第一次联调时,Django端发请求给Flask,Flask返回的JSON在浏览器控制台直接报跨域错误。我排查了很久才发现是Flask服务没有配置CORS头。给Flask装一个flask-cors扩展,几行代码就能解决:

from flask_cors import CORS from flask import Flask app = Flask(__name__) CORS(app, resources={r"/api/*": {"origins": "http://127.0.0.1:8000"}})

这里我故意把origins限制为Django的地址而不是用*,是因为推荐接口涉及会员隐私数据,不允许随便被第三方域名调用。安全这块,哪怕是个内部小系统,也不能图省事开全通。

3. 个性化的核心:数据模型与推荐逻辑怎么设计

3.1 会员数据模型是地基

所谓"个性化",前提是系统有足够的个体数据。我设计数据模型时重点安排了四个表:会员表、体测记录表、训练偏好表、课程表。

会员表包括基础信息:姓名、年龄、性别、手机号、会员类型、到期时间。体测记录表记录每次体测的十三项数据:身高、体重、体脂率、骨骼肌量、BMI、基础代谢、身体水分等。训练偏好表是从会员的预约历史和行为中提取出来的画像,比如倾向于晚上还是早上锻炼、喜欢团课还是私教课、能接受的中高强度是哪个梯级。课程表则描述了课程的类型标签、强度等级、适用人群、消耗预估。

这里有个容易忽略的细节:体测记录一定要记录测量的时间戳,而且允许一天多条记录。我去了解过实际的健身业务,发现体测数据通常不是每天测一次,但一天内可能测多次取平均值。如果表设计成每天只能存一条,后期数据分析会出各种问题。

3.2 推荐逻辑:先规则,后加权

很多人一听推荐算法就觉得要从机器学习开始。实际做下来,健身房这类场景用规则引擎加简单加权分数,效果就已经非常好了。原因很简单:运动建议的维度有限,无非是目标导向(减脂、增肌、康复、塑形)、身体条件(体脂率、BMI、基础代谢)、偏好(强度、时间、课程类型)。

推荐的第一个版本是规则匹配。举个例子:

  • 如果BMI大于24且体脂率大于25%,输出减脂方向的课程建议
  • 如果骨骼肌量低于同年龄正常范围,输出增肌方向的训练计划
  • 如果有腰肩颈疼痛记录,优先推荐康复类小班课

规则可以编写成配置文件,不写死在代码里,方便健身教练自己调整:

{ "rule_groups": [ { "condition": {"bmi": {"gt": 24}, "body_fat_rate": {"gt": 25}}, "recommend": ["hiit", "cycling", "core_training"] }, { "condition": {"muscle_mass": {"lt": 32}, "gender": "male"}, "recommend": ["strength_training", "functional_training"] } ] }

规则匹配完,如果有多门课程都符合条件,再用加权评分排序。加权项包括:会员历史约过的课程类型匹配度(加30分)、该课程在当前时间段的热门度(加20分)、个人在问卷里标记的兴趣课程(加25分)、教练本月尚未满负荷的可排课时间(加25分)。最终按分数取前三门推送。

这个算法虽然简单,但在实测中有个意外收获:由于加入了教练负荷权重,推荐结果天然平衡了教练的工作量,不再出现某个教练被约满、另一个教练空转的情况。等于一个推荐逻辑顺手解决了排课的负载均衡问题,这是当初没预料到的红利。

3.3 自定义推荐API的返回格式

Flask推荐服务返回的数据我设计成固定的格式,方便Django端消费:

{ "member_id": 1024, "recommended_courses": [ {"course_id": 3, "reason": "体脂率超标,推荐高强度间歇训练", "score": 87}, {"course_id": 7, "reason": "历史偏好晚间课程,匹配成功", "score": 79} ], "suggested_frequency": 3, "suggested_level": "middle" }

注意这里的reason字段。我坚持每个推荐结果都要带一句可读的理由,而不是只给课程编号。健身教练看到推荐结果时,需要理解系统为什么推荐这堂课,才能向会员解释。如果推荐结果没有解释,教练信任度会大打折扣——测试阶段这个细节对系统落地帮助很大。

4. 核心功能模块拆解:会员管理、课程排期与预约机制

4.1 会员管理:Django Admin打底,再加自定义操作

会员管理模块是这个系统最基础的骨架,Django自带的Admin后台已经能解决80%的需求。注册Member模型之后,后台自动支持增删改查、搜索、筛选,这为我省了不少时间。但我又定制了几个业务需要的动作。

第一个动作是批量导入会员。健身房从Excel里导会员信息是高频需求,我写了一个管理命令,支持读取xlsx文件,自动校验手机号格式和重号:

from django.core.management.base import BaseCommand from member.models import Member import openpyxl class Command(BaseCommand): help = '批量导入会员数据' def add_arguments(self, parser): parser.add_argument('file', type=str, help='Excel文件路径') def handle(self, *args, **options): wb = openpyxl.load_workbook(options['file']) sheet = wb.active for row in sheet.iter_rows(min_row=2, values_only=True): phone = str(row[1]) if Member.objects.filter(phone=phone).exists(): self.stdout.write(self.style.WARNING(f'手机号已存在: {phone}')) continue Member.objects.create( name=row[0], phone=phone, age=int(row[2]), gender=row[3], member_type=row[4], expire_date=row[5] ) self.stdout.write(self.style.SUCCESS('导入完成'))

第二个动作是到期自动预警。我写了一个独立的cron脚本,每天早晨扫描当天起30天内即将到期的会员,自动生成提醒记录,同时把提醒推送给运营人员。这个功能看起来简单,但实际价值非常显著——很多健身房会员流失是因为没人提醒续卡,而不是不想续。

4.2 课程排期:时间段冲突是重灾区

课程排期的数据表设计要考虑清楚:一个课程实例(CourseSession)包含课程名称、教练、日期、开始时间、结束时间、容量上限、已预约人数。排课冲突判断是这里的核心难点。

同一教练在同一时间段不能安排两节课,同一教室在同一时间段也不能安排两节课。我在保存排期时会做一次重叠检查:

from django.db.models import Q def check_conflict(coach_id, room_id, start_time, end_time, exclude_session_id=None): sessions = CourseSession.objects.filter( Q(coach_id=coach_id) | Q(room_id=room_id), date=start_time.date() ) if exclude_session_id: sessions = sessions.exclude(id=exclude_session_id) for session in sessions: if start_time < session.end_time and end_time > session.start_time: return {"conflict": True, "detail": session} return {"conflict": False}

这四行判断逻辑值得好好说道说道。判断两个时间段是否重叠,只需要检查"新开始时间早于已有结束时间,且新结束时间晚于已有开始时间"。很多新手会写成start_time > session.start_time and end_time < session.end_time这种不完整的判断,以为时间包含在里面才叫冲突,其实交叉也算冲突。我被这个逻辑坑过两次,特意写出来提醒。

4.3 预约机制:并发问题的处理经验

会员预约课程是个典型的并发场景。两个会员同时点击同一个课程时段的最后一个名额,系统必须保证只有一个能成功。Django下我用select_for_update配合事务处理:

from django.db import transaction @transaction.atomic def make_appointment(member, session_id): session = CourseSession.objects.select_for_update().get(id=session_id) if session.booked_count >= session.capacity: return {"code": 4001, "msg": "该课程已约满"} Appointment.objects.create(member=member, session=session) session.booked_count += 1 session.save(update_fields=["booked_count"]) return {"code": 200, "msg": "预约成功"}

select_for_update会对这行记录加行级锁,另一个事务必须等锁释放才能读取这条记录,所以不会出现两个请求都读到"还有一个名额"的情况。但这个用法要求所在数据库支持行级锁,SQLite在并发写入时表现不好,我开发时直接用了MySQL。如果你的项目用的是SQLite,并发场景建议自己加乐观锁或者用Redis计数器,不要在SQLite上依赖数据库锁。

5. 前端页面:从零手写到Django模板的取舍

5.1 为什么没有用前后端分离

现在很多Web项目上来就是Vue + RESTful API,一个人开发时这其实是给自己挖坑。这个项目总共就二三十个页面,主要使用者是健身房前台和教练,他们需要的是打开浏览器就能用、刷新不丢状态、表单简单直接。Django模板渲染天然适合这种场景,服务端渲染页面,不需要考虑跨域、token管理、路由守卫这些额外复杂度。

我最终决定用Django模板 + Bootstrap来写界面,Django端直接渲染HTML片段嵌入模板,整个系统只有一个API压力测试页面是用Flask直接输出的JSON。

5.2 会员详情页的个性化展示

会员详情页是这套系统的门面,我花了比较多心思。页面顶部是会员基础信息卡片,下方是这样几个区块:最新的体测数据表格、历史体测趋势折线图、已上过的课程记录、推荐课程列表。曲线图渲染这块我用的是ECharts,直接在模板里引入CDN资源,每个会员详情页渲染一次。

推荐课程列表区会调用Flask的推荐接口,把会员最新的体测数据传过去,即时渲染推荐结果。这个页面让我朋友第一次感受到"个性化"三个字到底意味着什么——前台打开某个会员的资料,系统直接告诉他"这个人体脂偏高,应该推HIIT课",而不是教练凭感觉瞎推荐。

页面性能上有个优化点:体测趋势图的数据接口要控制单次返回的条数,如果一个会员有上千条体测记录,一次性全量渲染会卡。我做了个折中方案,默认取最近三十次记录,首页只展示近六次的明细,要看全部再点击加载。

6. 部署上线遇到的坑与排查链路

6.1 静态文件集体404的根因

开发阶段一切正常,部署到生产环境后静态文件全部404。排查思路先确认Nginx配置,然后看collectstatic的输出,最后定位到问题:Django的STATIC_ROOT路径和Nginx的alias路径不一致。我设置STATIC_ROOT后跑collectstatic把文件收集到了项目里的staticfiles目录,但Nginx配置的alias指向的是另一个目录,两边没对上。

解决方式是把路径统一成绝对路径,在Nginx配置文件里明确指定:

location /static/ { alias /var/www/gym_project/staticfiles/; }

这个坑很基础,但往往要白折腾半小时。我的建议一直是:部署清单上第一件事就核对静态文件路径,不要等上线了再回头看。

6.2 Gunicorn和Flask推荐服务的进程管理

Django端我用Gunicorn启动,配了4个worker进程。Flask推荐服务单独用了一个Gunicorn实例跑在5001端口,配了2个worker。两个服务都在systemd里注册成常驻服务,开机自启、异常自动拉起。

这里有个教训:Gunicorn的worker数量不能拍脑袋定。我的经验值参考公式是"2到4个进程加1"(2 x CPU核心数 + 1),但这针对的是单台物理机的场景。我实际用了4和2个worker,跑下来CPU和内存都稳定,就没有再调了。如果有超卖服务器内存很紧张,可以按3个Django worker + 1个Flask worker起步,后面用ab压测再决定要不要加。

6.3 时区问题导致预约时间错乱

测试阶段发现下午六点预约的课程,管理后台显示的是凌晨两点的时间。排查过程先想到settings.py里的TIME_ZONE,检查发现设的是UTC,改成Asia/Shanghai之后Django后台显示正常了,但Flask返回的时间还是UTC。定位到Flask服务的代码里date字段用了datetime.utcnow(),改成本地时间东亚时区的写法:

from datetime import datetime import zoneinfo local_tz = zoneinfo.ZoneInfo("Asia/Shanghai") now = datetime.now(local_tz)

这个问题的根源是两个服务各写各的时间逻辑,没有一个统一的规范。事后我补了一个约束:所有服务统一使用带时区的ISO 8601格式传递时间参数,入库统一转成UTC,展示时再转成北京时间。这样一来,前端展示、API传输、数据库存储三层彻底分开,再也没出现过时间错乱。

7. 换一个角度:给做类似系统的你五条建议

第一,别一上来就设计复杂的推荐算法。先做规则匹配,把基础逻辑跑通,收集到真实业务反馈后再迭代算法。很多人前期纠结算法精度,结果连基础CRUD都没做好,项目烂尾的比比皆是。我第一版推荐逻辑其实就是一堆if-else,但已经足够惊艳客户了。

第二,表单校验别马虎。健身房管理系统的数据录入者大多不是技术人员,随手录入的错误数据会让后续所有分析失真。手机号校验、体脂率范围校验、体测日期不能晚于今天,这些看起来不起眼的字段校验,才是保系统下限的关键工程。

第三,数据报表比数据录入更受客户重视。这个项目做完后,客户用得最多的不是推荐功能,而是续卡预警和出勤率统计报表。建议你在做管理系统时,把报表功能放在和CRUD同等重要的位置。我后来花了一周时间补了按月统计出勤率、课程预约率、会员活跃度排行三张报表,客户满意度直接上升一个档次。

第四,权限管理一定要做,哪怕系统只给几个人用。健身房前台只能看到会员基础信息和预约操作,教练能看到体测数据但不能修改套餐价格,店长才能看财务报表。Django自带的Group和Permission机制完全够用,配置好这些权限,后面就不会出现"前台不小心改了会员卡到期时间"这种事故。

第五,数据库选型要提前想清楚。开发环境用SQLite很舒服,但并发预约、体测数据写入多了之后,SQLite的锁机制会成为明显瓶颈。我建议这种系统从一开始就用MySQL或PostgreSQL,哪怕开发期也用容器跑一个实例,避免上线前换库的麻烦。

这个项目的后续我还有两个打算:一是把推荐逻辑从"规则匹配"升级成能学习用户行为的轻量级模型,二是给会员端做一个可以直接在手机上查看推荐训练计划的小程序。目前这套系统的代码已经整理得比较规整,核心模块的接口文档也补全了,如果你想拿它当毕业设计或者接健身房私活的基础框架,完全可以从这套拆分思路开始搭建,会比从零起步省心很多。

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

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

立即咨询