做篮球联赛信息管理系统这个题是在去年帮朋友做课程设计时临时接的活儿。完整题目是“Django-Flask基于Python的CBA联赛信息管理系统”说人话就是用Python那套技术栈给职业篮球联赛做一个统一管球队、球员、赛程、比分、积分榜和新闻的平台。接之前我也觉得这玩意儿就是个增删改查真正动手才发现业务关系怎么梳理、排名怎么按联赛规则算、Django和Flask两套框架怎么在一个项目里和谐共存每一块都够写一大篇。这个系统能解决的问题很明确联赛运营方要维护球队和球员档案每场比赛结束后要录比分、算胜负、刷新积分榜运营编辑要发新闻公告管理员要管账号和权限。全靠Excel表格登记的话数据散落、重复录入不说打了一轮比赛要手动核对胜场、胜率、净胜分非常容易出错。做成信息系统以后录一场比赛比分、胜负关系、胜率、排名一次性自动算好更新前台给球迷看赛程和积分榜后台给运营人员做数据维护一套数据两头用。如果你正在为课程设计、毕业设计选题发愁或者想从零掌握Django业务开发、顺便搞明白Flask怎么当轻量服务这篇总结基本可以照着抄。1. 系统定位与整体设计思路1.1 它表面上是个CRUD实际是套业务系统很多人在项目答辩时最喜欢说“我做了一个管理系统”但老师第一个问题就是你这个系统到底管什么、给谁用、数据从哪来。这套篮球联赛信息管理系统也一样如果只把“球队表”“球员表”“比赛表”建好就算完那和用Excel没什么区别。真正的价值在“业务闭环”球队、球员档案是基础数据球员必须归属于某支球队位置、号码、身高体重这些字段直接决定之后能不能按条件筛选和统计。赛程编排是核心流程一场比赛要把主队、客队、时间、场馆、轮次关联起来状态要区分未开始、进行中、已结束因为只有“已结束”的比赛才能参与积分榜计算。比分录入是触发点录完比分后胜场、负场、胜率、积分、排名全部联动更新不是按一下刷新按钮临时去查几场比赛再手算。新闻公告是内容补充比赛完了要有战报伤病要有通知这部分对权限要求比较低内容编辑就能处理。账号权限是底线录入员、编辑、管理员各自能做什么不能做什么必须分清楚。这样拆下来系统的目标用户就很清晰前台面向普通球迷浏览数据后台面向联赛运营工作人员。围绕这两个使用场景我再决定技术方案。1.2 为什么用Django加Flask两套框架而不只选一个这是我被问得最多的问题印象里好像所有人都默认“一个项目只用一个Web框架”。其实双框架方案在真实业务里不少见关键在于职责切分。Django在这个项目里当“主力部队”。原因很简单管理后端需要用户认证、权限分组、ORM、Admin后台、表单处理这些恰好是Django的看家本领。特别是Django自带的Admin把模型注册进去就能直接增删改查对于联赛运营这种内部管理系统来说能省下大量开发时间。球队要加个临时外援、新闻要撤稿、场次要改时间管理员直接进后台就能操作不用我单独写一堆管理页面。Flask在项目里当“轻骑兵”。比赛数据接口比如现场比分推送、积分榜实时查询调用频率高、逻辑相对简单用Flask起一个轻量服务挂在另一个端口接口返回JSON比在Django里写一堆序列化代码要快得多部署时也能独立重启、独立扛压力。方案拆成两半之后两个服务共享同一个数据库。Django负责写业务数据和日常管理Flask负责对外提供数据接口和计算类服务比如给前台页面提供最新的积分榜JSON。这样做的另一个半隐藏好处是一个项目里同时覆盖了Django和Flask两种框架简历上和答辩时都有东西讲课程设计“技术亮点”这一栏也自然有了内容。1.3 功能模块与角色权限划分整个系统在代码层面分成下面几个Django应用对应关系非常直白应用名主要功能负责角色users登录、注册、权限管理系统管理员teams球队档案、主场球馆、教练信息信息录入员players球员档案归属球队、位置、号码信息录入员matches赛程、比分、比赛状态管理信息录入员standings积分榜排名计算与展示系统自动计算news战报、公告、新闻发布内容编辑权限这块我最初没想太细后来做后台才发现必须分。录入员只能改球队、球员、比赛数据不能改管理员账号内容编辑只能发布和管理新闻只有超级管理员能进Django Admin做系统级配置。这套权限用Django自带auth就能实现User表加一个role字段页面上按角色判断显示按钮就够了。2. 核心数据表设计与关键技术点2.1 球队、球员、比赛三张主表的字段设计数据表是整个系统的地基表设计错了后边所有查询都别扭。我实际建表时用的是Django的模型定义核心代码如下# teams/models.py from django.db import models class Team(models.Model): name models.CharField(球队名称, max_length50, uniqueTrue) short_name models.CharField(简称, max_length20) city models.CharField(所在城市, max_length30) home_arena models.CharField(主场球馆, max_length80) coach models.CharField(主教练, max_length30) founded_year models.IntegerField(成立年份) logo models.ImageField(队标, upload_toteams/, blankTrue) is_active models.BooleanField(是否参赛, defaultTrue) class Meta: ordering [name] def __str__(self): return self.name# players/models.py from django.db import models from teams.models import Team class Player(models.Model): POSITION_CHOICES [ (PG, 控球后卫), (SG, 得分后卫), (SF, 小前锋), (PF, 大前锋), (C, 中锋), ] team models.ForeignKey(Team, on_deletemodels.CASCADE, related_nameplayers, verbose_name所属球队) name models.CharField(球员姓名, max_length30) jersey_number models.IntegerField(球衣号码) position models.CharField(场上位置, max_length10, choicesPOSITION_CHOICES) height models.DecimalField(身高(米), max_digits3, decimal_places2) weight models.DecimalField(体重(千克), max_digits5, decimal_places1) birth_date models.DateField(出生日期) photo models.ImageField(球员照片, upload_toplayers/, blankTrue) is_active models.BooleanField(是否在队, defaultTrue) class Meta: ordering [team, jersey_number] unique_together (team, jersey_number) def __str__(self): return f{self.team.short_name} {self.jersey_number}号 {self.name}Team表里我特意加了is_active字段。为什么不直接删球队因为比赛表里有历史外键直接删会破坏历史赛程更稳妥的做法是标记“不再参赛”。Player表也是一样的思路球员退役或转会了就置为False不会影响以前比赛的技术统计。这个“逻辑删除优先于物理删除”的数据库设计习惯在管理系统里比什么花哨功能都重要。比赛表是整个系统的枢纽# matches/models.py from django.db import models from teams.models import Team class Match(models.Model): STATUS_CHOICES [ (scheduled, 未开始), (live, 进行中), (finished, 已结束), (postponed, 延期), ] season models.CharField(赛季, max_length20, default2024-2025) round_no models.IntegerField(轮次) home_team models.ForeignKey(Team, on_deletemodels.CASCADE, related_namehome_matches, verbose_name主队) away_team models.ForeignKey(Team, on_deletemodels.CASCADE, related_nameaway_matches, verbose_name客队) match_time models.DateTimeField(比赛时间) venue models.CharField(比赛场馆, max_length80) home_score models.IntegerField(主队得分, default0) away_score models.IntegerField(客队得分, default0) status models.CharField(比赛状态, max_length10, choicesSTATUS_CHOICES, defaultscheduled) class Meta: ordering [match_time, round_no] unique_together (season, round_no, home_team, away_team) def __str__(self): return f{self.home_team.short_name} vs {self.away_team.short_name}unique_together那一行容易被忽略实际很关键它保证同一个赛季同一轮次里同样的主客队对阵组合只能存在一次防止录入员手滑重复建赛程。比赛时间用DateTimeField而不是分开存日期和时刻因为后面要按照时间排序、判断进行中的场次一个字段最省事。2.2 积分榜为什么不用单独存一张表积分榜这个模块我推翻过一次。最初我建了一张Standing表球队名、胜场、负场、胜率全存进去每场比赛结束就UPDATE。结果发现两个问题一是录入员改了历史比分会造成积分榜数据不同步二是计算逻辑散落在代码里很难维护。后来我干脆不存积分榜表改成“计算快照”。每场状态变为finished的比赛触发一个函数重新计算当前赛季所有球队的排名然后把结果以JSON格式缓存到一张轻量表里供前台读取。计算规则完全按篮球联赛常见规则实现# standings/services.py from collections import defaultdict from django.db.models import Q from matches.models import Match def calculate_standings(season): stats defaultdict(lambda: {win: 0, lose: 0, points_for: 0, points_against: 0}) finished_matches Match.objects.filter(seasonseason, statusfinished) for m in finished_matches: stats[m.home_team_id][points_for] m.home_score stats[m.home_team_id][points_against] m.away_score stats[m.away_team_id][points_for] m.away_score stats[m.away_team_id][points_against] m.home_score if m.home_score m.away_score: stats[m.home_team_id][win] 1 stats[m.away_team_id][lose] 1 else: stats[m.away_team_id][win] 1 stats[m.home_team_id][lose] 1 ranking [] for team_id, s in stats.items(): total s[win] s[lose] win_rate s[win] / total if total else 0 net_points s[points_for] - s[points_against] ranking.append({ team_id: team_id, win: s[win], lose: s[lose], win_rate: round(win_rate, 3), net_points: net_points, points: s[win] * 2 s[lose] * 1, }) ranking.sort(keylambda x: (-x[points], -x[win_rate], -x[net_points])) for index, item in enumerate(ranking, start1): item[rank] index return ranking排序用的三个条件依次是积分、胜率、净胜分这是比较通用的篮球联赛排名规则。注意程序里不能用真实比赛比分直接比较是否相等因为篮球比赛常规时间没有平局但万一出现加时赛绝杀之外的录入错误用大于小于判断是安全的。2.3 关联查询的性能处理开发初期数据量小看不出问题。但在联赛打了十几轮、一场比赛关联主客两队时如果按最直接的方式在模板里写{{ match.home_team.name }}每显示一个比赛都会额外发一次SQL查询页面卡顿很明显。解决办法就两个函数select_related和prefetch_related。# matches/views.py from django.views.generic import ListView from matches.models import Match class MatchListView(ListView): model Match template_name matches/match_list.html context_object_name matches def get_queryset(self): return Match.objects.select_related(home_team, away_team) \ .filter(statusscheduled) \ .order_by(match_time)select_related用于外键字段会生成一条带JOIN的SQL把主队和客队信息一次性查出来prefetch_related用于反向关联比如渲染球队详情页时一次性把该队所有球员查出来。这一招是最简单也最有效的性能优化答辩时如果老师问“数据多了怎么办”你就把这个讲清楚比背一堆八股强得多。3. 从零到能跑的完整实操过程3.1 环境准备与项目初始化环境这块别纠结版本稳定就行。我用的是Python 3.10、Django 4.2、Flask 2.3MySQL 8.0。操作步骤python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install django4.2.* flask2.3.* pymysql django-cors-headers pillow django-admin startproject cba_system . python manage.py startapp users python manage.py startapp teams python manage.py startapp players python manage.py startapp matches python manage.py startapp standings python manage.py startapp news一个容易踩的细节是startproject后面那个点别漏了漏了会在项目根目录里再套一层目录后面所有命令的路径都会对不上。application目录建好以后先不要急着写业务代码把app全部注册到settings.py的INSTALLED_APPS里再去建表否则后面makemigrations会提示找不到模型。3.2 Django配置与数据库迁移连接MySQL前先去数据库里建好库注意字符集CREATE DATABASE cba_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;然后修改settings.pyDATABASES { default: { ENGINE: django.db.backends.mysql, NAME: cba_system, USER: root, PASSWORD: 你的密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }字符集必须用utf8mb4不是utf8。因为球员可能来自不同地区名字里如果有生僻字utf8的老字符集会直接报错或者存成乱码。这一点我在第4部分还会细说。接着运行迁移python manage.py makemigrations python manage.py migrate python manage.py createsuperusercreatesuperuser创建的是管理员账号用于登录Django Admin后台。我习惯把Admin后台先配置好再写前端页面因为管理后台能把基础数据录进去后面调试列表页、详情页都有真实数据可用。3.3 业务页面与URL路由实现前台页面我用的是Django的通用视图加原生模板没有额外引入重型前端框架。以赛程列表为例# matches/urls.py from django.urls import path from matches import views urlpatterns [ path(matches/, views.MatchListView.as_view(), namematch_list), path(match/int:pk/, views.MatchDetailView.as_view(), namematch_detail), ]模板里写一个简单的卡片布局显示对阵双方、时间和状态。我的建议是前期不要花太多时间在页面上先把数据展示正确再套Bootstrap样式。你完全可以用一套现成的后台模板改改布局管理系统这类项目评委看的核心永远是功能完整度和数据流程是否正确。对于比赛录比分这个操作我直接用Django Admin来解决不给录入员单独写表单页面。这是因为比分录入场景太简单Admin自带表单校验、历史记录和权限控制够用且不容易写错。等到需要自定义业务规则了再重写页面这是很多初级开发者不懂的道理能用现成方案时别给自己造轮子。3.4 Flask服务模块的落地与联调Flask服务的目录我单独放在项目根目录下的service文件夹里和Django代码区分开避免两个框架的代码混在一起不好维护。它的职责有两个对外提供积分榜JSON接口接收比分推送请求。# service/score_api.py from flask import Flask, jsonify, request import pymysql app Flask(__name__) DB_CONFIG { host: 127.0.0.1, user: root, password: 你的密码, database: cba_system, charset: utf8mb4, } app.route(/api/standings/season, methods[GET]) def standings(season): conn pymysql.connect(**DB_CONFIG) cur conn.cursor() # 实际项目里从缓存表读取计算快照避免每次实时算全量数据 cur.execute(SELECT * FROM standings_cache WHERE season%s, (season,)) rows cur.fetchall() cur.close() conn.close() return jsonify({code: 0, data: rows}) if __name__ __main__: app.run(host127.0.0.1, port5001, debugTrue)Django子和Flask子是两套进程共享同一个数据库。前台页面通过Ajax调用Flask接口获取积分榜Django只管渲染页面骨架和赛程数据。这里有一个特别需要注意的点跨端口访问会触发浏览器的CORS限制需要在Flask端配置CORS或者让Django的接口做反向代理转发。我在Flask里用django-cors-headers的兄弟方案直接用Flask-CORS解决配置三行代码就好。数据一致性问题靠数据库兜底。Flask从缓存表读排名快照Django在每场比赛结束时把最新排名写入缓存表。两块服务各自专注自己的事如果接口挂了不影响Django后台正常工作这就是拆开部署的最大好处。4. 常见问题与排查技巧实录4.1 开发期最容易踩的八个坑这个系统从开发到调试我踩了一堆坑整理成速查表照着排查能省很多时间现象根本原因解决办法图片上传后页面显示404MEDIA_ROOT和MEDIA_URL没配置或没加路由settings配置MEDIA路径urls里加static()转发Admin提交报CSRF验证失败外站POST请求或浏览器禁用Cookie模板form里加{% csrf_token %}API接口按需豁免中文数据写入数据库变问号表或连接字符集不是utf8mb4建库时指定utf8mb4连接参数加charset比赛时间差了8小时USE_TZTrue且TIME_ZONE没设设置TIME_ZONE‘Asia/Shanghai’按需管理USE_TZSQLite并发写入报database is locked开发期用了SQLite多人同时录入换成MySQL临时用SQLite就要接受单写限制积分榜刷新后排名和手工对不上缓存表没在事务里更新用Django的transaction.atomic包住重算逻辑Flask接口中文JSON返回乱码Flask默认JSON编码问题app.config[‘JSON_AS_ASCII’]False修改历史比分后积分不变没触发重算函数在Model的save方法或信号里统一调用重算第四行的时区问题几乎每个人都会碰到。Django开启USE_TZ后存进数据库的时间是UTC页面展示如果不做本地化就会出现前台显示的晚上八点变成了凌晨四点。我的做法是TIME_ZONE设置成Asia/Shanghai模板显示时间时用timezone.localtime()转换前后台展示全部统一成北京时间。4.2 比分录入的“事务”教训积分榜重算函数第一次上线时出现过一次非常尴尬的事录完一场比分页面刷新后主队的胜利多了1分客队的失败却没变。排查了半天发现是重算函数里好几条SQL语句没有放在同一个事务里前两条UPDATE成功了第三条因为外键约束报错回滚结果数据只改了一半。后来我果断把重算逻辑包进transaction.atomic里。加上之后要么全部更新成功要么全部回滚数据库永远不会出现“赢了没输”这种半完成状态。这个教训对做信息管理系统的同学特别有参考价值任何涉及多张表联动的写操作都应该考虑事务不要想当然认为代码顺序执行就万事大吉。还有一种情况是录比分时主客队填反了。我特意在比赛详情页加了一个“比分校验”的提示如果主队得分加客队得分超过一个约定阈值或者两队分差超过平时见过的极大值就弹一个确认框。这个小功能在答辩演示时能加不少分因为它体现的是“从业务场景出发考虑异常”不是从代码角度堆功能。5. 部署上线与扩展方向5.1 用Gunicorn加Nginx把系统真正跑起来开发环境用runserver够用但真要让系统稳定跑必须用生产级服务器。Django部分我用Gunicorn作为WSGI服务器Nginx负责静态文件、反向代理和负载均衡。核心操作如下pip install gunicorn gunicorn cba_system.wsgi:application -b 127.0.0.1:8000 --workers3Nginx配置核心就三点把80端口请求转发给8000端口把/static/和/media/路径直接指向静态文件目录把/api/路径反向代理到Flask的5001端口。为了让Django的静态文件能被Nginx直接伺服需要先执行collectstatic。python manage.py collectstatic --noinputsettings.py里要调整的地方包括DEBUGFalse、ALLOWED_HOSTS里加上服务器域名或IP还有STATIC_ROOT和MEDIA_ROOT的绝对路径。上线前最好把manage.py check --deploy跑一遍它会自动提示部署相关的安全风险。5.2 这个系统还能怎么扩展如果只是交作业做到这里已经够了。但把眼界放大一点这个系统的扩展空间其实很大而且很多方向都有现成思路可以参考。一是数据可视化。当前积分榜是表格形式可以引入ECharts把球队胜率、球员得分走势做成折线图和柱状图对球迷用户来说展示效果直接上升一个档次。二是实时比赛推送。Flask服务可以加WebSocket支持比赛进行中向前端页面实时推送比分变化这就把“赛后录比分”升级成“赛中看直播数据”了。三是Excel导入导出。球队名单、赛程表这种批量数据靠手工一条条录入效率太低。用Django的第三方库实现Excel批量导入运营人员按模板填好表直接上传能省下大量录入时间。导出功能则可以直接在Admin里加按钮一键导出当前赛季完整积分榜。四是用信号机制替代手动调用重算。Django的post_save信号可以在比赛保存后自动触发积分榜重算避免漏掉调用让模块之间更解耦。我在实际做这个项目时最深的感受是系统难的不是某一项技术而是把这么多环节组织成一条顺畅的数据流。每场比赛从编排、录入比分、重算排名到前台展示中间任何一环断了用户都会觉得系统“有问题”。如果你也在做类似的信息管理系统先把数据库设计和业务流程图画清楚再动手写代码后面能少推翻三四次。建议你先从球队、球员、比赛三张主表做起跑通一条“建队-录人-排赛-录比分-看排名”的完整链路再往外围扩展新闻和权限这样看着进度慢实际上是最稳的路线。