
“django-flask基于python的电竞赛事报名裁判管理系统”这个标题初看有点杂糅两个Web框架同时出现乍一看像“为了技术而技术”。但实际做完之后你会发现这种组合并不是拍脑袋而是被需求逼出来的。我最近帮一家赛事运营方把整个办赛流程从“微信群接龙Excel登记”搬到这套系统上从选手报名、裁判分配、赛程编排到计分统计一口气跑通了。这篇文章就把这套系统的做法、框架分工、核心代码和踩过的坑从里到外掰开讲一遍。无论你是打算自己搭一套赛事系统还是正在纠结Django和Flask怎么共存都能从这里找到能直接用的方案。1. 项目定位办赛方真正缺的不是“登记表”1.1 办一场电竞赛事到底在痛什么先还原一下没有系统的时候办赛方是怎么工作的。报名阶段通常是一张在线表格或者微信群接龙选手自己填战队名、队员身份、联系方式运营同学再手工复制到Excel里做资格审核。到了比赛日签到环节靠喊名字裁判分配靠经验“谁有空谁上”计分靠纸质单子赛后统计成绩又要人工录入。十几个人的小型赛事尚可撑住一旦报名人数超过一百或者分多个项目组别并行开赛这套手工流程立刻就会崩。更麻烦的是裁判管理。电竞裁判不像传统体育那样有严格的职业认证体系但一样需要“懂规则、能执裁、无利益冲突”。同一个战队关系紧密的人不能当该队比赛的裁判同一所高校或同一家网吧战队的人也要回避。这种事光靠主办方脑袋记不住必须让系统来处理冲突关系。所以这套“电竞赛事报名裁判管理系统”的定位就很清楚它不是一个简单的报名表而是把“报名—审核—编组—分配裁判—执裁计分—成绩汇总”这条完整链路全部接管。核心价值不是替代Excel而是让多角色选手、裁判、运营在同一套数据上协作避免信息错乱。1.2 系统解决的几类核心问题第一类问题是报名数据标准化。选手通过网页填写队伍信息、成员身份、联系方式后端统一校验身份证格式、年龄范围、项目组别是否开放重复报名在数据库唯一约束层面就被拦住。第二类问题是裁判工作的数字化。裁判登录后能看到自己被分配的场次、选手名单、需要回避的战队评分直接在线录入。第三类问题是运营的数据可视化。所有报名进度、审核状态、赛程安排、成绩排名都在一个后台里呈现随时可以导出报表。从我实际跑完的赛程来看这套系统最让我满意的一点是把“例外情况”的处理放进来了。比如比赛当天有人弃权运营可以在后台一键调整对阵把后续场次状态重新激活裁判临时请假可以重新分配裁判系统会自动检查新的裁判是否与参赛队伍存在回避关系。这些“意外处理”能力才是赛事系统区别于普通报名系统的关键。2. 技术选型Django与Flask为什么同时存在2.1 两种框架的分工逻辑不是“炫技”先说结论这个项目的管理主站用Django面向赛场的高频接口用Flask前面放一层Nginx按路径把请求分发到两个服务。Django负责重逻辑包括数据建模、管理员后台、用户认证、报名审核、权限控制。Flask负责轻逻辑比如选手检录时更新到场状态、裁判提交评分、前端轮询当前场次状态这些请求量不大但要求响应快、逻辑独立的接口。为什么不让Flask也干全部或者全部交给Django因为Django的Admin后台和ORM生态太成熟报名审核、裁判管理这类功能几乎可以靠框架自带能力快速搭建。但Django的“重”也体现在中间件链、CSRF校验、表单处理这些环节上对于高频的JSON读写接口有时候显得路径太长。Flask轻巧灵活写一个纯API端点几行代码就能搞定也不容易被Django的全局配置影响。两者配合各取所长。注意两个框架不是部署在同一个进程里。它们各自用Gunicorn启动Nginx做反向代理。/manage、/admin、/register这些路径给Django/api/venue、/api/score这些路径给Flask。对外看起来是一个系统对内是两套服务。2.2 数据层如何保持统一框架可以分开数据库绝对不能分家否则数据同步会变成灾难。我的做法是核心业务表全部由Django的ORM管理使用Django Migration做版本控制。Flask服务只负责读取和写入这些表中的特定字段通过SQLAlchemy连接同一个MySQL数据库。为了避免两个框架同时修改同一张表结构导致迁移冲突Flask侧约定“只读写不建表”所有结构变更都从Django侧发起。有些独立的小表比如“比赛日检录流水”“裁判评分暂存”可以由Flask侧用SQLAlchemy建表。这些表的生命周期短、结构简单、与主业务表关联弱不会引发迁移冲突。我建议把这类独立表统一加前缀比如score_tmp_、checkin_log_一眼就能看出归属权。跨服务的写操作要特别注意事务边界。比如选手检录Flask侧应该只更新registrations表里的checked_in字段然后返回JSON给前端至于这个报名对应的赛事是否已经锁定、比赛是否已经开始状态校验逻辑放在Django侧做。如果两边的校验逻辑混在一起出了问题很难排查。2.3 核心数据模型怎么拆这张表是整套系统的主干设计好以后所有功能都不需要大改。表名关键字段职责说明eventsid, name, category, start_time, max_players, status赛事主表一个赛事可包含多个项目组别playersid, user_id, real_name, id_card, phone, team_name选手基础资料一张表关联用户体系registrationsid, event_id, player_id, status, apply_time, checked_in报名与检录状态表核心流转都在这matchesid, event_id, round, player_a_id, player_b_id, referee_id, status, match_time比赛场次表同时记录对阵双方与裁判refereesid, user_id, real_name, level, game_types, current_team裁判资料与擅长项目用于自动分配scoresid, match_id, referee_id, player_id, score, submit_time裁判评分明细表一场比赛可有多条记录这里最关键的设计是matchs表里同时存了referee_id和两个选手的ID。这样“裁判与选手是否冲突”的检查就变成一条简单的SQL查询你总是能从当前这场匹配里直接拿到三方关系。成绩统计时也不用去关联报名表再追溯选手直接读player_id就好性能非常友好。数据库索引一定不要省。registrations.event_id status这个联合索引几乎是日常查询的主入口报名列表中所有运营操作都会先按赛事筛选状态。matches.event_id round联合索引同理。这些索引在数据量到几千条时感觉不出来但比赛日并发查询一上来没有索引的查询会被锁得怀疑人生。3. 核心功能落地报名、编排、评分怎么做3.1 报名模块截团时间、人数上限与并发控制报名接口看起来简单无非是插入一条记录但高并发下最容易出“超卖”问题。如果赛事设置了最多32支队伍前端按钮显示“已满”但最后几毫秒里有20个人同时点击提交数据库层面不做约束就可能插入超过32条记录。我在Django里的做法是使用select_for_update配合事务。报名开始时先锁定赛事总名额行判断当前已报名人数是否小于上限再插入报名记录整个操作放在一个transaction.atomic()块里。这样并发请求会被行锁串行化不会出现名额超卖。from django.db import transaction from django.db.models import F transaction.atomic def apply_event(event_id, player_id): event Event.objects.select_for_update().get(pkevent_id) cur_count Registration.objects.filter( eventevent, status__in[pending, approved] ).count() if cur_count event.max_players: raise APIError(名额已满) registration Registration.objects.create( eventevent, playerplayer_id, statuspending ) return registration报名状态流转我设计了三条路径pending待审核、approved已通过、rejected已拒绝。线下赛通常有两种极端一种是运营希望先收钱再确认资格另一种是纯免费赛事直接自动通过。我建议在赛事表上加一个auto_approve布尔字段True时报名成功后状态直接置为approved否则进入待审核列表。这个开关能让运营在赛前轻松切换审核策略。报名编号生成也值得注意。不要用自增ID直接暴露给选手我在生成报名记录时额外生成一个短编号规则是赛事ID 日期 四位随机数比如EV202406130018。这个编号用于现场检录核验选手报出编号运营就可以在后台直接定位记录比翻名字精准得多。3.2 裁判分配自动匹配与回避规则裁判分配不能简单“随机派单”。每个裁判有自己的擅长游戏项目比如有人主修MOBA类有人主修FPS类把FPS裁判派到MOBA比赛上等于让评审看不懂比赛。我的分配策略分两步走先按referees.game_types过滤出具备该赛事项目资质的裁判再检查当前裁判当天已经分配了多少场比赛优先选择场次最少的那位实现简单的负载均衡。回避规则是实现时最容易遗漏的部分。我的冲突检测逻辑包含三层第一层是裁判当前所属战队与某选手所在战队同名直接判冲突第二层是裁判与其选手来自同一个注册单位比如同一所高校第三层是运营手动维护的“黑名单”关系比如某裁判和历史上有纠纷的战队不能同场。三者只要命中任意一个系统就不会把这位裁判分配进该场次。def is_conflict(referee, player): if referee.current_team and referee.current_team player.team_name: return True if referee.unit_name and referee.unit_name player.unit_name: return True if ConflictBlacklist.objects.filter( refereereferee, playerplayer ).exists(): return True return False比例冲突检查也可以在保存前做但真正稳的做法是把它放进数据完整性层面。我在matches表里没有用数据库约束因为这种情况无法用简单的唯一键约束所以我在分配接口的业务逻辑里调用is_conflict同时在赛前跑一个批量扫描任务把历史场次的冲突情况拉出来人工复查一遍。这算是“系统自动检查人工兜底”的双保险。3.3 赛程编排与状态流转赛程编排我从简处理小组赛阶段采用随机分组选手报名时可以勾选“种子选手”运营在后台标记几位往届四强作为种子编排时种子选手优先分散到不同小组避免强队第一轮就碰头。淘汰赛阶段根据小组赛排名生成对阵规则就是标准的“A组第一对B组第二”。每场比赛的状态我用matches.status字段表达not_started、ongoing、finished、cancelled。运营可以手动把not_started改为ongoing比赛结束时由裁判提交成绩系统再把状态置为finished。这个字段表面简单实际上它驱动了前端的展示逻辑——只有ongoing状态的场次才允许裁判录入分数只有finished的场次才进入成绩计算。编排接口我用Django实现逻辑本身不复杂关键在于手动调整的兜底。比赛日经常出现选手迟到、弃权等突发情况我加了一个reschedule接口运营可以指定某个场次的对阵双方、开赛时间、比赛场地系统清除场次原有的评分记录并把后续场次依赖关系重新计算一遍。这个接口的权限必须严格限制在超级管理员级别否则容易出现误操作。3.4 评分录入与成绩计算评分流程是这套系统的高频操作。每场比赛有三位裁判评分每位裁判进入比赛详情页看到参选双方各三位选手以5V5团队赛为例按照选手ID录入个人评分也可以直接给战队整体打分。评分提交后系统会校验几个条件场次状态必须是ongoing、裁判必须属于本场次的referee_id、当前时间不能晚于赛事设定的最晚录入时间。三个条件任何一个不满足接口直接拒绝。成绩计算我放在裁判全部提交后进行不采用边录边算。因为三人评分可能存在某位裁判尚未提交的情况中途计算结果会误导大屏展示。当三位裁判都提交后系统自动触发计算队伍最终得分是每位裁判给该队打分的总和去掉一个最高分去掉一个最低分取中间分之和。如果只有两位裁判就直接求和。计算完成后生成一条match_results记录同时把队伍排名写入赛事排行榜。这里有一个经验分享评分提交接口一定要做幂等。裁判可能因为网络原因点了两次提交如果接口不做判断就会生成两条重复评分。我在score_tmp_表里加了match_id referee_id player_id的唯一索引第二次提交时触发IntegrityError捕获后直接返回“已提交”而不是报错既保证数据唯一又不影响用户体验。4. 关键实现细节权限、上传与联调4.1 三角色权限体系实现我使用Django自带的User模型在Profile表里扩展一个role字段取值包括player选手、referee裁判、admin运营管理员。选手端登录可以看到自己的报名记录和赛程裁判端只能看到分配给自己的场次管理端拥有一切权限。整个系统的权限控制围绕role字段做装饰器分发。Django视图里我写了一个role_required装饰器接受一个或多个角色名在请求进入视图前判断当前用户的role。Flask侧的接口因为主要给裁判现场使用我额外签发了短期token裁判在Flask接口请求头里带这个tokenFlask侧校验token有效后直接从Redis里读取用户ID和角色不再重复查数据库。这个token的有效期我设置成8小时刚好覆盖一个比赛日的完整时段。这里要特别提醒Django和Flask两个服务的session是完全独立的。Django的session存在Django默认的表里Flask的session默认是客户端cookie两边用同一套登录态必然失败。我最终没有强行桥接两个session体系而是让前端在请求Flask接口时单独携带token这套设计虽增加了前端一点工作量但两个服务的职责边界非常清晰排查问题也容易。4.2 头像与证件上传的处理报名阶段选手需要上传身份证照片或学生证照片裁判需要上传资格证书。直接存在media目录下固然简单但如果文件名用原始上传名高并发时容易出现重名覆盖。我采用重新命名策略uuid4().hex加上原文件后缀存储路径按日期分目录比如media/uploads/2024/06/13/xxxx.jpg。文件类型校验不能只检查后缀我用Pillow库打开图片并检查image.verify()能通过说明确实是图片数据。大小限制用request.FILES的size属性判断超过2MB直接拒绝。Django侧配置MEDIA_ROOT和MEDIA_URL生产环境由Nginx托管/media/路径否则静态图片请求会全部打到Python进程上比赛日并发一上来就卡。from PIL import Image from django.core.exceptions import ValidationError def validate_image(upload): try: img Image.open(upload) img.verify() except Exception: raise ValidationError(不是有效的图片文件) if upload.size 2 * 1024 * 1024: raise ValidationError(文件大小不能超过2MB)图片上传后如果需要生成缩略图比如选手列表头像我在保存时用Pillow再生成一张160×160的缩略图存到thumbnails/目录列表页只读取缩略图避免列表加载原始大图导致页面缓慢。4.3 Django与Flask联调的接口约定两个服务之间不直接通过HTTP调用而是通过前端浏览器作为中转。比如裁判检录的页面前端先调用Django接口拿到赛场信息和报名列表然后调用Flask接口提交检录状态最后再刷新Django接口获取最新状态。这样做的好处是不需要在服务端维护复杂的内部调用链坏处是前端需要处理两次请求的时序。但如果确实需要服务端之间同步数据比如Flask侧把裁判评分写入后要通知Django侧刷新比赛状态我采用数据库事件监听方案。Flask在score_tmp_表插入记录后通过消息队列发送一个事件到Django侧的消费者Django收到事件后再计算比赛结果并更新状态。消息队列用Redis的Stream即可不需要引入重型MQ中间件部署成本低故障排查也简单。接口返回格式必须前后端共同约定好。我的统一格式是{code: 0, msg: ok, data: {...}}异常时code非0msg给出人类可读的错误信息。这样前端只需要写一个统一的请求封装层不需要在每个接口里单独做错误处理。联调阶段最耗时的不是逻辑错误而是字段命名不一致所以接口文档要在开发前定好后补文档基本都会被吐槽。5. 部署与日常运行从本机到比赛日5.1 本地环境搭建步骤克隆代码后先用虚拟环境安装依赖。requirements.txt里核心依赖有Django、Flask、mysqlclient、djangorestframework、pillow、requests、gunicorn、redis。Django侧先改settings.py的数据库连接然后依次执行python manage.py makemigrations python manage.py migrate python manage.py createsuperuserFlask侧不需要迁移主表但如果用它管理检录流水和评分暂存需要单独运行它自己的建表逻辑。我习惯在Flask服务的启动函数里用Base.metadata.create_all()自动建表见下表开发环境方便生产环境改为脚本执行避免服务启动时因为权限问题卡住。启动分两个终端一个跑Django开发服务一个跑Flask服务python manage.py runserver 0.0.0.0:8000 flask --app flask_app run --port 5200开发环境不要在同一端口跑两个服务前端联调时在本地配置代理即可。我建议用django-cors-headers开启跨域许可让前端开发服务器可以直接请求两个后端端口。5.2 生产环境部署Nginx Gunicorn双服务生产环境我用两台Gunicorn分别托管Django应用和Flask应用。Django侧跑在8000端口命令类似gunicorn config.wsgi:application -b 0.0.0.0:8000 -w 4Flask侧跑在5200端口命令是gunicorn -w 2 -b 0.0.0.0:5200 flask_app:app。Nginx配置的关键是按照路径分发location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /api/ { proxy_pass http://127.0.0.1:5200; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /media/ { alias /data/project/media/; }这里有个细节容易被忽略如果是按前缀/api/转发给FlaskFlask路由定义时需要关闭默认的静态目录逻辑同时考虑前缀剥离。我的Flask接口统一不带版本号前缀直接/api/checkin就在前端配置路径里约定/api对应Flask服务Nginx的location匹配后会把完整URI传给Flask所以Flask侧路由要写/api/checkin而不是/checkin否则404。比赛日之前我建议做一次模拟压测。用locust或者简单的脚本并发请求报名接口观察数据库连接数是否被打满。MySQL默认最大连接数往往不够我调整到200同时把Gunicorn的worker数设为CPU核数的2倍再往上加只会增加上下文切换不会有性能提升。5.3 赛前备份与赛后数据归档赛事数据是一个时间敏感型资产赛前一天所有报名记录、裁判分配、赛程编排都必须备份。我用mysqldump定时任务每天凌晨备份一次赛前2小时再手动执行一次。备份文件通过脚本压缩后传到独立的备份磁盘或者对象存储本地留一份异地留一份。赛后数据不能立即删除要保留至少一个赛季。成绩数据可能会被选手投诉复查也可能会被赛事主办方用于下一季的种子排名。我设计了一个归档命令把赛季结束超过30天的赛事数据导出为JSON并压缩存储数据库里只保留赛事元信息报名明细和评分详情从主表清走这样能让常用查询表的体积一直保持在一个可控范围内。注意线上环境的DEBUG必须改为FalseALLOWED_HOSTS必须配置实际域名否则Django会拒绝所有请求。这两项看似基础我见过不下三个团队在部署时因为漏掉它们而排查半天。6. 常见问题与避坑实录6.1 并发报名导致“名额超卖”这个坑几乎每个做报名系统的人都会踩。我最早版代码是先count()再create()没有加事务锁赛前开放报名的那一秒钟后端同时收到几十个请求最终报名人数比设定上限多了7个。修复方案就是前面提到的select_for_update。它的问题是要提前知道锁哪一行如果赛事主表恰好不存在锁就无从谈起所以我会在Event表里保证所有赛事记录都存在关闭报名的赛事也要留着一行只是status改为closed。6.2 两套框架的Session与登录态冲突刚开始联调时前端登录Django后跳转到Flask接口结果Flask认为用户未登录整个流程直接卡死。这是因为两边session机制不互通。我的最终解法是让Flask侧完全独立校验通过一个短期token识别裁判身份。前端登录接口从Django拿到token存到localStorage之后每次请求/api/路径都带上Authorization头Flask侧解析token对应的用户ID和角色。这个方案的问题是最坏情况下token泄露后8小时内都能被冒用所以我在签发token时绑定来源IP前端IP变化频繁的场地网络环境会出现误杀。我的取舍是放宽IP绑定只校验签名有效性和有效期因为这是内部赛事系统风险可控。如果做面向公网的大规模系统建议启用严格IP绑定。6.3 图片上传成功但页面打不开上传接口返回200但浏览器里访问图片地址总是404。原因是开发环境直接访问Django的媒体服务没问题生产环境Nginx配置/media/路径的alias写错了导致真实目录和URL对不上。排查方式很简单直接在服务器上curl -I访问一张已知图片的URL看返回的是404还是200200就说明是路径映射问题404则要检查文件是否真的上传成功。部署完后一定要主动测试一次完整的图片上传链路不然比赛日选手传不了证件会让现场非常被动。6.4 比赛时间显示错乱赛事表里存储的start_time是全0时区的时间但系统设置用了本地时区结果管理员后台看到的比赛时间比实际晚了8小时。Django默认USE_TZ True数据库里存UTC时间模板渲染时如果没做时区转换就会显示UTC时间。解决办法是保持USE_TZ True把TIME_ZONE设置为赛事所在时区模板渲染时Django会自动转换。Flask侧读取时间字段时必须先转换为本地时间再返回前端否则同样会差8小时。这个坑特别隐蔽因为小规模测试数据通常都是当天创建差8小时可能刚好落在同一天只有安排跨零点比赛时才暴露。6.5 评分提交接口重复请求比赛现场网络很不稳定裁判提交评分时经常点一次没反应又点一次重复数据随之而来。除了在数据库加唯一约束外前端也要做提交锁提交按钮在请求返回前禁用。两个层级的防重叠加数据才真正安全。我的经验是数据库唯一约束是兜底不能单纯依赖前端。问题现象根因处理方案报名人数超过上限缺少行级锁使用select_for_update 事务裁判无法提交评分场次状态不是ongoing检查前端是否先调用开赛接口图片404Nginx媒体路径映射错误检查alias与MEDIA_ROOT路径比赛时间差8小时时区转换未生效设置TIME_ZONEFlask侧手动转换评分重复提交缺少幂等处理数据库唯一索引 前端提交锁7. 这类系统后续还能怎么延伸报名和裁判管理跑通之后我发现一个很有意思的趋势赛事的数字化价值远不止于“省掉一张Excel表”。同一套数据往后端延伸可以自动生成本赛季的积分排名体系往前端延伸可以做一个选手专属的“我的赛事档案”记录每次比赛战绩、晋级截图、历史评分。如果再接入直播平台的比赛结果推送系统的覆盖面可以从赛前一直延伸到赛后传播。我个人最推荐的下一个模块是电子证书自动生成。赛事结束后系统根据match_results表和报名信息自动生成获奖证书PDF选手在个人中心直接下载。这个功能实现成本不高但对赛事主办方而言它的宣传价值和选手留存价值远超预期。技术方案也不复杂用Python的ReportLab库渲染模板队列生成后存到对象存储前端展示下载按钮即可。最后分享一个我赛后才想明白的体会一套赛事系统的成功不在于功能做了多少而在于比赛日当天运营团队能不能在30秒之内处理掉一个意外。所以开发的时候与其花大量时间做花哨的可视化大屏不如把“运营手动改状态”的接口做得顺手一点。把简单可靠的流程管理好比堆积功能重要得多。这套系统上线后经历了两轮比赛日实战整体稳定但每次赛后复盘都还会发现新的小需求。技术选型始终服务于业务逻辑Django和Flask的共存也只是服务于这个目标的一种手段而已。