Django+Flask混搭架构:电竞赛事管理系统从设计到实践

发布时间:2026/10/10 13:22:57
Django+Flask混搭架构:电竞赛事管理系统从设计到实践
去年接了个挺有意思的活给某高校电竞社和本地几家电竞馆搭一套电竞赛事管理系统核心就两件事——报名和裁判管理。一开始他们用的是在线文档收集报名信息比赛当天靠人工对名单裁判安排全凭负责人一张表。等参赛队伍到四十支、一天要打二十多场的时候这套流程彻底崩了有人重复报名、选手跟裁判是熟人没人发现、赛程撞场次吵成一团、赛后成绩录入漏了好几场。所以就有了这个基于 Python 的 Django Flask 混搭项目。Django 负责报名审核、后台管理、数据建模这些重活Flask 做成轻量服务专门承接成绩上报、赛况查询这类接口比较多的场景。整个系统上线之后报名审核从人工一天变成自动秒过裁判指派冲突率降到了零赛程表生成从手工排两小时变成一键生成。这篇文章把我从需求梳理、技术选型到编码实现、部署上线这整条链路里的关键决策和踩坑记录都写出来。适合正在做类似赛事系统、活动管理系统的开发者参考也适合想了解 Django 和 Flask 怎么配合使用的朋友。不用照着抄但里面这些为什么这么设计的思路大概率能帮你少走弯路。1. 项目定位与整体架构1.1 为什么用 Python 做赛事管理系统选 Python 不是因为它比 Java 快而是因为这类系统的业务形态和 Python 的生态太匹配了。报名管理系统本质上是一个表单 审批流 资源调度的业务系统。这种系统对绝对性能的要求并不高——一个线下电竞赛事报名峰值也就几百上千人同时提交远没到需要高并发框架硬扛的程度。但对开发效率的要求很高赛事的规则变化快这周还是 5v5 团队赛下周可能就加一个 1v1 单挑赛字段和流程经常要改。Python 的 Django 自带 Admin 后台模型改完马上能在后台界面增删改查非常适合这种边办赛边改需求的节奏。中间我也考虑过要不要用 Node.js 或者 Go 重写一套。Node.js 的实时性确实好但后台管理这块得从零搭Go 性能强但 ORM 和后台生态相对没那么顺手。回到需求本身裁判指派、赛程编排这种核心逻辑瓶颈在算法正确性和数据一致性不在语言执行速度。Python 团队招人也容易懂 Django 的开发者一抓一大把后期维护成本低。还有一个私心Django 自带认证系统、ORM、Admin、表单校验这些在赛事系统里全都能直接用省掉的工程量比想象中大得多。1.2 Django 和 Flask 混搭的分工逻辑很多人一听到一个项目里既有 Django 又有 Flask就觉得是过度设计。我当初也是抱着能用一个框架解决就别造两套的原则去评估的后来发现这个项目确实有拆分的必要。Django 侧承担的是数据核心和管理后台选手、战队、裁判、赛事、场次的信息管理报名审批流程赛程编排的复杂业务逻辑所有后台管理页面Flask 侧承担的是轻量接口服务比赛现场的成绩上报接口现场工作人员要用平板快速提交赛况实时查询接口现场大屏轮询后续接入大屏展示、直播信息面板的扩展位为什么要拆现场成绩上报这个场景有它的特殊性接口路径经常要调整返回格式要跟着大屏展示组的需求频繁变有时候今天加个加赛标识明天加个弃权原因。如果全塞进 Django 的大工程里每改一次都要过一遍完整的模型层和权限体系很重。Flask 这边只依赖共享数据库和 Redis改一个路由函数就能上线灵活得多。两个服务之间不是各干各的而是通过同一个 MySQL 数据库 Redis 缓存协作。Django 是数据的生产者和权威源Flask 是数据的消费者和回写通道。权限校验上Flask 接口不自己做登录体系而是靠 Django 签发的一次性 Token 验身份后面细说。这个架构的底线是核心数据流必须经过 Django 的模型层Flask 不允许直接改表结构只能通过接口写业务数据。这样一来混搭的灵活性和 Django 的数据一致性都保住了。2. 核心模块拆解与数据模型设计2.1 报名模块从选手提交到资格审核报名是整个系统最核心的入口。最初的文档收集方式最大的问题是数据没有结构化有人填队伍名随便起有人拉了一串微信名当队员名单审核的人得人工去比对是不是重复报名。数据模型上我没有单独建一个选手表而是分成三层战队表Team、选手表Player、报名记录表Registration。# teams/models.py class Team(models.Model): name models.CharField(max_length50, uniqueTrue) # 战队名唯一防止重复报名 captain models.OneToOneField(Player, on_deletemodels.PROTECT, related_namecaptain_team) created_at models.DateTimeField(auto_now_addTrue) class Player(models.Model): real_name models.CharField(max_length30) game_id models.CharField(max_length50) # 游戏内 ID phone models.CharField(max_length20) id_card_hash models.CharField(max_length64) # 身份证号只存哈希不存明文 team models.ForeignKey(Team, nullTrue, on_deletemodels.SET_NULL, related_namemembers) created_at models.DateTimeField(auto_now_addTrue) class Registration(models.Model): STATUS_CHOICES [ (pending, 待审核), (approved, 已通过), (rejected, 已驳回), (cancelled, 已取消), ] team models.ForeignKey(Team, on_deletemodels.CASCADE, related_nameregistrations) event models.ForeignKey(Event, on_deletemodels.CASCADE, related_nameregistrations) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending) remark models.TextField(blankTrue) # 审核备注 reviewed_by models.ForeignKey(User, nullTrue, on_deletemodels.SET_NULL) submitted_at models.DateTimeField(auto_now_addTrue) class Meta: constraints [ models.UniqueConstraint(fields[team, event], nameunique_team_event) ]三个关键设计点第一报名记录和战队、赛事都建立关联队伍和赛事之间加了唯一约束。这一条在数据库层面就堵死了同一个队伍重复报名同一个赛事的可能。以前用文档收集时同样一支队伍换个队长名字就能提交两次现在不可能了。第二身份证号不存明文只存哈希。赛事报名涉及未成年人隐私合规是底线。我用 SHA-256 加盐哈希平时比对只看哈希值是否一致这样既能在跨赛事时识别出同一个选手是否重复参赛比如某些赛事规定一名选手只能代表一支队伍又不需要在数据库里留敏感信息。第三报名状态机从提交到审核是显式流转的。待审核 - 已通过 / 已驳回 - 已取消。驳回必须填备注审核人必须记录这样选手被拒之后能知道原因主办方事后也能追溯。2.2 裁判管理档案、指派与执裁记录裁判管理是这套系统里逻辑最复杂的部分。需求听起来简单——记录裁判信息给每场比赛安排裁判——但落到细节就发现坑很多裁判有等级之分国家级、一级、二级有擅长的游戏项目同一时间段只能执裁一场比赛而且裁判不能执裁自己所在战队的比赛。我的模型设计是这样的# referees/models.py class Referee(models.Model): LEVEL_CHOICES [(national, 国家级), (first, 一级), (second, 二级)] name models.CharField(max_length30) phone models.CharField(max_length20) level models.CharField(max_length20, choicesLEVEL_CHOICES) game_specialties models.ManyToManyField(GameItem) # 擅长的游戏项目 belong_team models.ForeignKey(Team, nullTrue, blankTrue, on_deletemodels.SET_NULL) # 所属战队用于避嫌 is_active models.BooleanField(defaultTrue) # 是否处于可用状态 created_at models.DateTimeField(auto_now_addTrue) class Match(models.Model): event models.ForeignKey(Event, on_deletemodels.CASCADE, related_namematches) round_name models.CharField(max_length30) # 如16进8、半决赛、决赛 team_a models.ForeignKey(Team, on_deletemodels.PROTECT, related_namehome_matches) team_b models.ForeignKey(Team, on_deletemodels.PROTECT, related_nameaway_matches) referee models.ForeignKey(Referee, nullTrue, on_deletemodels.SET_NULL, related_namematches) scheduled_time models.DateTimeField() status models.CharField(max_length20, choicesMatchStatus.choices, defaultscheduled) result models.JSONField(defaultdict, blankTrue) # 存放比分、最佳选手等referee字段一开始我建成了必选后来被现实教育了赛程生成的时候如果某个时段的裁判池不够比赛根本排不出来。后来改成可空赛程先生成裁判指派在第二步做。belong_team这个字段是避嫌逻辑的关键。现实中很多裁判本身也是某个战队的成员或者关联人员如果不用字段把它显式记下来指派时就不可能自动避免自己人裁自己人。2.3 赛程与成绩状态机设计赛程编排的核心不是生成表格而是保证约束条件全部满足。在我这个系统里约束有三条同一时间同一场地只能有一场比赛同一裁判同一时间段只能执裁一场比赛裁判不能执裁本方战队参与的比赛赛程生成的算法我放在 Django 的 management command 里没有做成可视化拖拽因为线下赛的场地和时间是固定的决赛日就那几个小时算法排完微调一下就行。核心流程是先确定比赛轮次再按淘汰关系把对阵双方配对最后通过贪心算法给每场比赛找空闲裁判。成绩这块我用了一个统一的状态机。MatchStatus定义如下class MatchStatus(models.TextChoices): SCHEDULED scheduled, 待开始 ONGOING ongoing, 进行中 SCORE_SUBMITTED score_submitted, 成绩已提交 CONFIRMED confirmed, 成绩已确认 DISPUTED disputed, 仲裁中 FINISHED finished, 已结束为什么成绩要分已提交和已确认两个状态因为现场上报成绩的是工作人员不是裁判本人。工作人员提交后裁判要在后台确认确认之前如果选手有异议可以进入仲裁中状态。这个设计在赛事现场救了我很多次——有场比赛双方对击杀数有争议工作人员已经提交了如果系统直接判定结果就麻烦了。有了仲裁状态承办方可以先把争议挂起核对录像后再确认。3. 关键功能实现与代码落地3.1 报名审核怎么做到秒过又能人工介入报名审核这块我并没有一开始就做全自动。审核这件事分两层硬性规则机器判断软性规则人工判断。硬性规则包括队伍人数是否达到下限、队员是否满员、是否重复报名、是否在报名截止时间之前。这些我用 Django 的 ModelForm 加表单校验就能处理提交时直接校验不通过就当场拦截。软性规则包括战队名是否符合规范、队员 ID 是否有明显乱填的、未成年人参赛有没有监护人同意书。这些没法用规则完全覆盖所以保留人工审核通道。我的做法是系统自动先过一遍硬性规则通过后默认置为已通过同时后台把可能存在问题的记录标黄提醒审核人。这样做的好处是绝大多数正常报名的队伍能秒过审核人只需要盯着系统标出来的可疑记录看。实际运行下来四十多支队伍里系统标黄的只有七八支人工复核量大幅下降。审核模块的核心逻辑# registrations/services.py def review_registration(registration_id): reg Registration.objects.select_for_update().get(idregistration_id) if reg.status ! pending: raise ValidationError(当前状态不可审核) # 硬性校验 team reg.team if team.members.count() 5: reg.status rejected reg.remark 队伍人数不足5人 reg.save() return # 自动通过 标记风险 risk_flags check_risk_flags(team, reg.event) reg.status approved if risk_flags: reg.remark .join(risk_flags) reg.save()这里我用select_for_update()做了行级锁防止审核人员和管理员同时在后台操作同一条报名记录导致状态覆盖。赛事进行中后台同时开着的账号不少这个锁必须加。3.2 Flask 侧的成绩上报接口Flask 服务这边只做一件事接收现场工作人员从平板提交的成绩数据写入 Redis 做实时展示再异步写回 MySQL。接口设计尽量轻# score_api/app.py from flask import Flask, request, jsonify from datetime import datetime import redis, pymysql app Flask(__name__) r redis.Redis(host127.0.0.1, port6379, db1) app.route(/api/matches/int:match_id/score, methods[POST]) def submit_score(match_id): # 先校验一次性 TokenToken 由 Django 后台签发 token request.headers.get(X-Score-Token) if not verify_token(token): return jsonify({code: 403, msg: 无效凭证}), 403 data request.get_json() score_a data.get(score_a) score_b data.get(score_b) if score_a is None or score_b is None: return jsonify({code: 400, msg: 缺少比分}), 400 # 写 Redis供大屏实时拉取 r.hset(fmatch:{match_id}, mapping{ score_a: score_a, score_b: score_b, reported_at: datetime.now().isoformat(), }) # 异步回写 MySQL write_score_to_mysql.delay(match_id, score_a, score_b) return jsonify({code: 0, msg: ok})这里有个细节为什么成绩先写 Redis 再异步写 MySQL因为大屏展示是高频轮询如果每次轮询都查 MySQL数据库压力很大。而 Redis 的哈希结构天然适合存这种快照型数据。MySQL 那边通过 Celery 异步落库保证最终一致。Token 校验的逻辑也值得说。Flask 服务虽然独立跑但它不能脱离 Django 的权限体系。我的做法是Django 后台给每个工作人员生成一个有效期两小时的 Token存在 Redis 里Flask 校验时直接查 Redis。工作人员 Token 过期了就找赛事管理员重新签发避免了在 Flask 侧再做一套用户体系。3.3 权限控制与操作日志电竞赛事管理系统里的权限不是管理员和普通用户两级就够的。实际运营中有好几类角色超级管理员能看到一切、赛事管理员只管某个赛事、审核员只管报名审核、裁判只能看自己执裁的比赛、工作人员只能提交成绩。Django 自带的 Group 权限系统刚好够用。我把权限粒度控制在模型级别 少量对象级别。模型级别用 Django 自带的has_perm对象级别用自定义的判断函数。比如审核员只能审核Registration.status pending的记录裁判登录后只能看到Match.referee 当前用户关联的裁判的比赛。操作日志这块一开始我没放在心上结果出了个纠纷有场半决赛某个选手说他的队伍被错误地标记为已驳回后台一查发现根本没人操作过。后来排查是报名截止时间自动触发的定时任务把超时未补材料的队伍全部置为驳回但当时没有任何日志记录说不清楚。从那以后我加了统一的OperationLog模型。任何状态的变更、任何审核操作都通过一个装饰器写日志# common/decorators.py def log_operation(model_name, action): def decorator(func): wraps(func) def wrapper(request, *args, **kwargs): result func(request, *args, **kwargs) OperationLog.objects.create( operatorrequest.user.username, model_namemodel_name, actionaction, detailf{model_name} #{kwargs.get(pk)} - {action}, iprequest.META.get(REMOTE_ADDR, ), ) return result return wrapper return decorator日志写的字段不多但谁、什么时间、从哪个 IP、对哪条记录做了什么操作这些必须有。这不仅是事后追责的依据更重要的是能定位系统自身有没有 bug 导致错误。赛事结束后复盘时这些日志就是最诚实的第一手资料。4. 从开发到上线的完整流程4.1 环境搭建与项目初始化这个项目虽然是 Django Flask 两个服务但我用了一个 git 仓库加两个子目录来管理依赖分开装。项目根目录结构大致是这样esports_system/ ├── web/ # Django 主服务 │ ├── manage.py │ ├── config/ │ ├── teams/ │ ├── referees/ │ ├── events/ │ └── requirements.txt ├── score_api/ # Flask 轻量服务 │ ├── app.py │ ├── tasks.py │ └── requirements-api.txt ├── deploy/ │ ├── docker-compose.yml │ ├── nginx.conf │ └── .env.example └── README.md环境准备阶段我踩了个小坑Django 和 Flask 的依赖有很多重叠但版本要求不一样。比如 WerkzeugFlask 2.x 要求 Werkzeug 2.xDjango 本身不依赖 Werkzeug但是通过某些第三方包间接引用了。当时我把两个项目的需求塞进同一个 requirements.txt结果装完 Flask 再装 Django 的依赖把 Werkzeug 降级了Flask 直接起不来。所以两个服务一定要用独立的虚拟环境。我的部署方案是 Docker Compose 启动三个容器# deploy/docker-compose.yml version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_DATABASE: esports MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} volumes: - mysql_data:/var/lib/mysql ports: - 3306:3306 django: build: ../web command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 4 depends_on: - mysql - redis env_file: - .env flask: build: ../score_api command: gunicorn app:app --bind 0.0.0.0:5000 --workers 2 --worker-class gevent depends_on: - redis - mysql env_file: - .env redis: image: redis:7-alpine ports: - 6379:6379中间还有个插曲Flask 的 Gunicorn worker 类型不能和 Django 用一样的默认的 sync worker 在处理长连接时很浪费成绩上报接口是短请求sync 也够用但我后面加了轮询大屏的 SSE 接口所以干脆用 gevent worker。这个选型等到后面接实时赛况时省了很多事。4.2 上线前的检查清单与性能优化上线前的自检清单是我每次做这类系统都会固化的流程。这次也没例外我照着下面这张表逐项过了一遍检查项具体内容数据库约束唯一约束、外键保护、必要索引是否齐全状态机合法性所有非法状态流转是否有防护权限边界每个角色能访问的接口和数据是否验证过并发场景报名提交、审核操作是否有锁保护敏感信息数据库里有没有不该存的明文信息日志完整性关键操作是否都有日志备份方案MySQL 定时备份脚本是否生效性能优化方面我做了三件事第一列表页查询加 select_related 和 prefetch_related。报名列表要展示队伍名、赛事名、审核人如果不用预取从几十条记录开始就有明显的 N1 查询问题。加了之后一次页面请求的 SQL 数量从几十条降到几条。第二赛程列表和大屏数据走 Redis 缓存。赛程表生成之后不会频繁变更我把它序列化后缓存到 Redis设置 60 秒过期。大屏端轮询直接走 Flask 接口查缓存MySQL 的读压力基本为零。第三报名提交接口加简单限流。虽然报名的人不多但为了防止恶意脚本刷报名之前真有人写脚本反复提交制造假数据我在 Django 侧用一个 Redis 计数器限制同一个 IP 每分钟最多提交 5 次。上线之后又做了个小优化MySQL 加了一些组合索引最典型的是match(event_id, status, scheduled_time)。这个索引在赛程查询场景下效果很明显因为查询条件永远是某个赛事下的某个状态按时间排序组合索引直接覆盖避免文件排序。5. 实战中踩过的坑与排查记录5.1 报名超卖问题一次并发把报名搞崩了系统上线后第一次赛事报名开启后十分钟内涌入了大量请求。虽然我们的提交量不至于把服务器打崩但出现了一个更隐蔽的问题同一个战队在极短时间里提交了两次报名居然都成功了。原因很典型我在服务层做了查重检查这个队伍是否已报名但两个并发请求同时通过了检查然后各自插入了一条记录。数据库层面的唯一约束unique_team_event在第二次插入时确实会报错但报错被我上层代码吞掉了返回给用户的是系统繁忙请重试——而实际上数据已经进去了只是以异常的形式。排查思路是这样的先看 MySQL 慢查询日志发现有两笔插入在几十毫秒内先后执行。再看应用日志发现第二个请求触发了IntegrityError异常。问题定位很简单就是检查然后插入这个经典竞态。修复方案有两个层面第一层代码层面改成语义化的原子操作。Django 的get_or_create在底层会处理唯一约束冲突比先查再插安全得多。第二层对于复杂的校验比如检查队伍人数是否达标再提交用事务加行锁from django.db import transaction transaction.atomic def submit_registration(team_id, event_id): team Team.objects.select_for_update().get(idteam_id) if Registration.objects.filter(teamteam, event_idevent_id).exists(): raise ValidationError(该队伍已报名) # 后续插入操作 Registration.objects.create(teamteam, event_idevent_id, statuspending)select_for_update会把战队这行锁住第二个请求必须等第一个请求事务提交后才能继续此时exists()就能查到已存在的数据从而正确拦截。这个坑的深层教训是在并发场景下永远不要用先查再写的模式处理唯一性校验。数据库的唯一约束才是最后的防线应用层的查重只是提升用户体验的手段。5.2 裁判时间冲突一个容易漏掉的边界情况裁判指派模块开发的时候我设计了一个函数来检查某个裁判在某场比赛时间段是否空闲。最初的逻辑很简单def is_referee_available(referee, start_time, end_time): conflict Match.objects.filter( refereereferee, scheduled_time__ltend_time, scheduled_time__gtstart_time, status__in[scheduled, ongoing], ).exists() return not conflict单看这个查询好像没问题但实际跑起来发现有漏洞如果该裁判的某场比赛正好在这个时间段结束那这条比赛记录的scheduled_time是开始时间而结束时间并没有存字段。我的模型里每场比赛只存了scheduled_time意味着只要一场比赛没结束下一场就永远无法安排同一个裁判。问题是一场电竞比赛的实际耗时是不确定的强队碾压可能 15 分钟结束实力接近可能要打满五局五十分钟。用固定的开始时间做冲突判断要么把裁判的利用率压得很低要么产生真实冲突。最终方案是给Match增加estimated_duration预计耗时字段冲突判断时用开始时间加上预计耗时作为粗粒度判断。实际执裁过程中裁判可以在后台点击本场结束系统重新标记该裁判的空闲状态。这个方案不是精确的但结合现场人工微调实用性远超完全靠算法。这个案例说明业务系统里很多时间冲突问题本质是数据模型没有表达清楚时间段。如果一开始就存开始时间和预计结束时间后面会少很多麻烦。5.3 列表查询越来越慢的排查记录赛事进行到第三天后台赛程列表页面开始明显变卡有时候加载要三秒以上。当时第一反应是索引问题但检查之后发现表里的索引都建了数据量也没多大——一个赛事几十场比赛而已。后来通过 Django Debug Toolbar 看 SQL发现查询时间本身很快慢的是页面渲染赛程列表页每行要显示左右两队的信息、裁判信息、当前状态这些关联数据在模板里通过属性访问产生了大量额外查询。虽然我用select_related预取了队伍和裁判但问题出在team_a和team_b都是外键到同一个Team表Django 对同一张表的多次关联预取处理不够聪明仍然重复查询。解决方式很直接列表页的字段提前在视图里组装成扁平字典不走 ORM 对象属性链。代码丑是丑一点但页面加载时间从三秒降到了两百毫秒以内。def match_list(request, event_id): matches Match.objects.filter(event_idevent_id).select_related(referee) data [ { id: m.id, team_a_name: m.team_a.name, team_b_name: m.team_b.name, referee_name: m.referee.name if m.referee else 待指派, round: m.round_name, status: m.get_status_display(), scheduled_time: m.scheduled_time.strftime(%m-%d %H:%M), } for m in matches ] return render(request, match_list.html, {matches: data})顺带说一句Django ORM 对于同一个模型被多次外键引用的场景预取效果一直一般。如果你也遇到这种页面与其在 ORM 层面纠结不如直接扁平化数据结构简单且可控。5.4 时区问题赛程时间凭空差了一小时这个坑很小但特别典型必须写下来。赛事前夜我做了次模拟测试发现后台录入的比赛时间到前台显示时总是差一小时。排查下来是时区设置问题。Django 默认的USE_TZ True数据库里存的是 UTC 时间模板渲染时如果TIME_ZONE没设成Asia/Shanghai显示的就是 UTC 时间比北京时间慢八小时但我们差了七小时——因为当时正好是夏令时季节不对中国没有夏令时。真实原因更简单服务器操作系统时区是 UTCMySQL 连接的时区也是 UTC但我的配置里TIME_ZONE Asia/Shanghai只设置了 Django 层Django 写入 MySQL 时把上海时间转成了 UTC 存储读取时又按上海时间解析。中间某个环节多了一次转换导致最终显示错乱。最终修复就是三处统一Django 的TIME_ZONE Asia/Shanghai、USE_TZ True、MySQL 连接参数加上init_command: SET time_zone 08:00然后让所有业务代码统一使用 Django 的时区工具处理时间不在代码里手动datetime.now()。做这类系统的兄弟姐妹们时间戳的处理一定要在框架层面统一任何手工干预都会成为隐患。5.5 现场突发大屏数据不更新的排查比赛当天现场大屏的赛况数据突然停在上一场不更新了。第一时间查 Redis发现match:{id}这个 key 确实有新数据。再查大屏端轮询接口发现 Flask 服务返回的还是一小时前的数据。原因很隐蔽Flask 接口读取 Redis 时用了r.hgetall(match:{id})而写侧是r.hset(match:{id}, mapping{...})。单独看都没问题问题出在我把 Redis 的数据写和读放到了不同的 Python 进程而 Redis 客户端库默认会做连接池复用。Flask 某个 worker 进程的连接因为长时间空闲被 Redis 服务端断开了但客户端没有感知读请求一直走这个死连接拿到的自然是旧数据。解决方式也不复杂Redis 客户端加socket_keepaliveTrue和socket_connect_timeout参数同时读取时增加异常重试逻辑连接断开时重建。这个坑的通用教训是任何长连接组件Redis、数据库连接池在长时间运行后都可能出现假死一定要配置合理的保活参数和重试机制。最后再分享一个我实操中的体会整个系统从需求到上线花了大概三周中间最花时间的不是写代码而是把赛事规则翻译成数据结构和状态机。如果你也在做类似的项目我的建议是先把业务流程画清楚再动手建表。我当时跟承办方开了两次需求会一开始他们说很简单就是报名和安排裁判等到画流程图的时候才发现光报名就有提交、审核、补材料、驳回申诉、改队员、截止锁定六个环节安排裁判又牵扯避嫌、等级匹配、时间冲突这些约束。如果一开始急着写代码后面大概率要大改。技术上 Django Flask 的混搭架构在这个项目里验证是成功的但我不建议所有项目都这么搞。只有当你的接口服务确实有独立的变更节奏、独立的部署需求、独立的技术栈诉求时拆出来才有价值。否则一个 Django 应用扛到底维护成本更低。这套系统后续我又扩展了邮件通知、赛果自动生成战报的功能都是在 Flask 服务上加个路由就搞定Django 侧完全没动。这种重核心 轻接口的组合让我在应对赛事运营方那句能不能再加个功能时底气足了不少。