简介:本资源是一个基于Django框架构建的轻量级在线编程竞赛平台源码包,面向Python Web开发初学者与算法竞赛系统学习者,解决从零搭建可运行OJ(Online Judge)系统的实践难题。压缩包共193个文件,含163个核心Python模块(涵盖用户、题目、竞赛、提交、API、缓存及管理命令等完整业务逻辑)、7个配置文件(如supervisord.conf、nginx.conf、Dockerfile等,支撑生产环境部署与服务编排)、以及HTML模板、Markdown文档、SSL证书和Shell脚本等辅助文件,整体仅282KB,结构紧凑、模块职责清晰。目前已有31人下载学习,适合希望深入理解Django项目工程化组织、RESTful API设计、竞赛流程闭环(报名→答题→判题→排名→统计)及前后端协同逻辑的开发者。读者可直接部署调试,快速掌握用户权限控制、代码沙箱集成思路、UV统计实现及多环境配置分离等实战要点。
1. 这不是另一个 Django 博客模板:它是一套可直接投产的 OJ 系统骨架,含完整容器化部署链路
你手头这个.zip包里没有requirements.txt的模糊提示,也没有manage.py runserver就能跑通的幻觉。它是一套真实生产环境打磨过的在线编程竞赛(OJ)平台源码——从用户提交代码、沙箱判题、实时排名到 HTTPS 反向代理、进程守护、静态资源分发,全链路配置文件已就位。Dockerfile 不是玩具示例,而是明确指定python:3.11-slim基础镜像并预装gcc、g++、make等编译工具;supervisord.conf里同时管理gunicorn(Django 应用)、celery(异步任务如判题)、redis-server(缓存与消息队列)三个关键进程;nginx.conf中locations.conf已定义/api/、/static/、/media/的精确路由规则,连/judge/submit/这类判题专用路径都做了proxy_pass转发。它面向的是需要快速上线校内 ACM 训练平台、企业内部编程考核系统或开源 OJ 社区的运维工程师与后端开发者——你得懂 Django 模型设计,也得会调supervisorctl status查进程状态,更得在rsyncd.conf里配好判题机结果同步目录。这不是教学 Demo,是删掉注释就能进 CI/CD 流水线的工程实体。
2. Django 核心模块解析:从用户权限模型到判题状态机的设计逻辑
2.1 用户与权限模型:超越AbstractUser的细粒度控制
项目未直接继承django.contrib.auth.models.AbstractUser,而是定义了自研User模型,位于users/models.py。该模型显式声明username、email、is_active、is_staff字段,并额外扩展nickname(用于前端显示)、school(机构归属)、avatar(头像存储路径)及rating(Elo 排名积分)。关键设计在于权限字段的拆分:is_judge_admin控制判题后台访问权,is_contest_manager控制竞赛创建权,而非依赖is_staff全局开关。这种设计避免了staff权限泛滥导致的安全风险——例如普通管理员无法误操作沙箱配置。
# users/models.py class User(AbstractBaseUser, PermissionsMixin): username = models.CharField(max_length=150, unique=True) email = models.EmailField(unique=True) nickname = models.CharField(max_length=100, blank=True) school = models.CharField(max_length=200, blank=True) avatar = models.ImageField(upload_to='avatars/', blank=True) rating = models.IntegerField(default=1500) is_judge_admin = models.BooleanField(default=False) is_contest_manager = models.BooleanField(default=False) # ... 其他字段与方法提示:迁移时需执行
python manage.py makemigrations users而非auth,因自定义用户模型要求AUTH_USER_MODEL = 'users.User'在settings.py中强制指定,否则migrate会报错RelatedObjectDoesNotExist。
2.2 判题状态机:Submission模型的状态流转与原子性保障
submissions/models.py中Submission模型定义了完整的判题生命周期状态:PENDING(等待判题)、JUDGING(正在判题)、ACCEPTED(通过)、WRONG_ANSWER(答案错误)、TIME_LIMIT_EXCEEDED(超时)、MEMORY_LIMIT_EXCEEDED(内存超限)、COMPILE_ERROR(编译失败)、RUNTIME_ERROR(运行时错误)、SYSTEM_ERROR(判题机异常)。状态变更不通过简单save()实现,而是封装为update_status()方法,内部使用select_for_update()锁定当前记录,防止并发提交导致状态覆盖:
# submissions/models.py from django.db import transaction class Submission(models.Model): STATUS_CHOICES = [ ('PENDING', 'Pending'), ('JUDGING', 'Judging'), ('ACCEPTED', 'Accepted'), # ... 其他状态 ] status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING') def update_status(self, new_status): with transaction.atomic(): # 锁定当前记录,确保状态更新原子性 submission = Submission.objects.select_for_update().get(pk=self.pk) if submission.status != 'PENDING' and submission.status != 'JUDGING': raise ValueError("Cannot update status from non-pending/judging state") submission.status = new_status submission.save(update_fields=['status'])此设计保证了当多个判题进程同时处理同一提交时,状态不会被错误覆盖。例如,若两个进程同时读取到PENDING状态,其中一个将其设为JUDGING后,另一个再尝试更新将因select_for_update()阻塞直至前一个事务结束,从而避免竞态条件。
2.3 竞赛与题目关联:多对多关系的反向查询优化
contests/models.py中Contest模型通过problems = models.ManyToManyField('problems.Problem', through='ContestProblem')关联题目,但未使用默认中间表,而是自定义ContestProblem模型,额外存储order(题目顺序)、score(分值)、is_public(是否公开)字段。这使得同一题目可出现在不同竞赛中且拥有独立属性。查询某竞赛所有题目时,Django ORM 自动生成高效 JOIN:
# 获取竞赛ID为1的所有题目,按order排序 contest = Contest.objects.get(id=1) problems = contest.problems.all().order_by('contestproblem__order')生成的 SQL 会自动 JOINcontestproblem表,避免 N+1 查询。若需获取题目对应分值,直接访问contestproblem.score属性即可,无需额外查询。
3. 容器化部署实战:从 Docker 构建到 Nginx 反向代理的全链路配置
3.1 Dockerfile 解析:精简镜像与判题环境预置
Dockerfile采用多阶段构建,基础阶段使用python:3.11-slim减少攻击面,构建阶段安装gcc、g++、make、python3-dev等编译依赖,并编译psutil(进程监控)与pyzmq(Celery 消息传输)。最终镜像仅保留运行时所需二进制与 Python 包,体积控制在 350MB 以内:
# Dockerfile FROM python:3.11-slim AS builder RUN apt-get update && apt-get install -y \ gcc g++ make python3-dev \ && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt FROM python:3.11-slim RUN apt-get update && apt-get install -y \ gcc g++ make \ && rm -rf /var/lib/apt/lists/* COPY --from=builder /wheels /wheels RUN pip install --no-cache-dir --find-links /wheels --no-index * COPY . /app WORKDIR /app CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]注意:
requirements.txt中django==4.2.12与celery==5.3.6版本锁定,避免因依赖升级导致判题逻辑异常。若需升级 Django,必须同步验证django.contrib.postgres(若使用 PostgreSQL)与celery的兼容性,否则celery -A oj worker启动会失败。
3.2 supervisord.conf:三进程协同与故障自愈策略
supervisord.conf定义了gunicorn、celery、redis-server三个程序组,每个均配置autostart=true、autorestart=unexpected及startretries=3。关键参数在于gunicorn的numprocs=2与celery的numprocs=4,适配常见 4 核服务器。gunicorn启动命令明确指定--bind unix:/tmp/gunicorn.sock,使 Nginx 可通过 Unix socket 高效通信:
# supervisord.conf [program:gunicorn] command=/usr/local/bin/gunicorn --bind unix:/tmp/gunicorn.sock --workers 2 --timeout 120 oj.wsgi:application directory=/app user=www-data autostart=true autorestart=unexpected startretries=3 redirect_stderr=true stdout_logfile=/var/log/gunicorn.log [program:celery] command=/usr/local/bin/celery -A oj worker --loglevel=info --concurrency=4 directory=/app user=www-data autostart=true autorestart=unexpected startretries=3 redirect_stderr=true stdout_logfile=/var/log/celery.log [program:redis] command=redis-server /etc/redis/redis.conf autostart=true autorestart=unexpected startretries=3启动后执行supervisorctl status可验证三进程均处于RUNNING状态。若celery显示FATAL,需检查redis-server是否先于celery启动(supervisord默认按配置顺序启动,redis在celery前定义,故无问题)。
3.3 nginx.conf 与 locations.conf:动静分离与 API 路由精准匹配
nginx.conf主配置引入locations.conf,后者定义了核心路由规则。/api/路径全部代理至 Gunicorn 的 Unix socket,/static/与/media/直接由 Nginx 服务,避免请求经 Django 处理造成性能损耗:
# locations.conf location /api/ { proxy_pass http://unix:/tmp/gunicorn.sock; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /static/ { alias /app/staticfiles/; expires 1h; add_header Cache-Control "public, immutable"; } location /media/ { alias /app/media/; expires 1h; }提示:
/judge/submit/这类高并发路径在locations.conf中被单独配置proxy_buffering off;,禁用 Nginx 缓冲,确保判题结果流式返回给前端,避免延迟。
4. 判题服务对接:本地沙箱配置与 rsync 结果同步机制
4.1 判题机沙箱环境:Docker-in-Docker 与资源限制
判题服务不直接在主容器内执行用户代码,而是通过docker run --rm --memory=128m --cpus=0.5 --pids-limit=32启动隔离沙箱。rsyncd.conf配置了专用模块[judge_results],允许判题机将结果文件同步至主应用服务器的/app/judge_results/目录:
# rsyncd.conf [judge_results] path = /app/judge_results/ comment = Judge result files uid = www-data gid = www-data read only = false write only = true list = false auth users = judge secrets file = /etc/rsyncd.secrets判题脚本执行rsync -avz --delete /tmp/judge_result/ judge@main-server::judge_results/,其中judge@main-server的密码明文存于/etc/rsyncd.secrets(格式judge:password123)。主应用通过FileSystemStorage(location=settings.JUDGE_RESULT_ROOT)读取结果文件,触发Submission.update_status()更新数据库。
4.2 UV 统计实现:Redis HyperLogLog 与每日去重计数
UV(独立访客)统计未使用数据库 COUNT DISTINCT,而是基于 Redis 的HyperLogLog数据结构,内存占用恒定约 12KB,误差率 0.81%。stats/views.py中uv_count()视图每日凌晨执行redis_client.pfcount('uv:20240601'),并将结果写入DailyUV模型:
# stats/views.py from django_redis import get_redis_connection def uv_count(request): redis_client = get_redis_connection("default") today = datetime.date.today().strftime("%Y%m%d") key = f"uv:{today}" # 用户登录后,前端调用 /api/stats/track-uv/ 记录 if request.user.is_authenticated: redis_client.pfadd(key, str(request.user.id)) count = redis_client.pfcount(key) return JsonResponse({"uv": count})注意:
pfadd操作幂等,同一用户 ID 多次调用只计 1 次。若需跨日统计,需遍历日期键并pfmerge,但本项目仅提供单日查询,符合 OJ 场景下实时监控需求。
5. 生产环境排错:五类高频故障定位与修复指令
5.1 Gunicorn 启动失败:Socket 权限与进程冲突排查
当supervisorctl status显示gunicorn为FATAL,首先检查/tmp/gunicorn.sock文件权限:
# 进入容器 docker exec -it oj_app bash # 检查 socket 文件是否存在及权限 ls -l /tmp/gunicorn.sock # 正确输出应为:srw-rw---- 1 www-data www-data 0 Jun 10 10:00 /tmp/gunicorn.sock # 若不存在,手动创建并授权 mkdir -p /tmp touch /tmp/gunicorn.sock chown www-data:www-data /tmp/gunicorn.sock chmod 660 /tmp/gunicorn.sock若权限正确仍失败,检查是否有残留进程占用端口:
# 查看监听端口 netstat -tuln | grep ':8000' # 强制终止残留进程 pkill -f "gunicorn.*oj.wsgi"5.2 Celery 任务积压:Redis 连接与队列监控
celery -A oj inspect stats返回空结果,表明 Worker 未连接 Redis。检查celery日志:
supervisorctl tail celery # 若出现 "Connection refused",确认 redis-server 进程状态 supervisorctl status redis # 若为 STOPPED,手动启动 supervisorctl start redis监控队列长度:
# 进入 Redis CLI redis-cli # 查看 celery 队列长度 llen celery # 查看队列中任务详情(谨慎使用,大数据量会阻塞) lrange celery 0 95.3 静态文件 404:collectstatic 与 Nginx 路径映射验证
访问/static/css/base.css返回 404,需验证两处配置:
- Django 执行
python manage.py collectstatic --noinput,确认/app/staticfiles/目录下存在文件; - Nginx 配置中
alias路径是否匹配:
# 容器内检查 ls -l /app/staticfiles/css/base.css # 应输出类似:-rw-r--r-- 1 root root 1234 Jun 10 09:00 /app/staticfiles/css/base.css # Nginx 配置检查 grep "alias" /etc/nginx/conf.d/locations.conf # 必须为:alias /app/staticfiles/;5.4 判题结果不同步:rsync 认证与目录权限
判题机执行rsync报错@ERROR: auth failed on module judge_results,检查/etc/rsyncd.secrets权限:
# 主服务器上 ls -l /etc/rsyncd.secrets # 正确权限:-rw------- 1 root root 25 Jun 10 08:00 /etc/rsyncd.secrets # 若权限过宽,修复 chmod 600 /etc/rsyncd.secrets同时验证判题机rsync命令中的密码文件路径是否指向正确的 secrets 文件。
5.5 HTTPS 重定向失效:https_redirect.conf 与 Nginx 模块加载
访问 HTTP 端口未自动跳转 HTTPS,检查https_redirect.conf是否被正确 include:
# 查看 nginx 主配置 grep "include" /etc/nginx/nginx.conf # 应包含:include /etc/nginx/conf.d/https_redirect.conf; # 验证 https_redirect.conf 内容 cat /etc/nginx/conf.d/https_redirect.conf # 正确内容: server { listen 80; server_name _; return 301 https://$host$request_uri; }若return 301未生效,确认 Nginx 已加载http_ssl_module:
nginx -V 2>&1 | grep -o "http_ssl_module" # 输出 "http_ssl_module" 表示已启用| 故障现象 | 核心命令 | 关键参数说明 |
|---|---|---|
| Gunicorn socket 权限错误 | chown www-data:www-data /tmp/gunicorn.sock | www-data是 supervisor 中定义的运行用户,必须匹配 |
| Celery 无法连接 Redis | supervisorctl restart redis | redis是 supervisord.conf 中定义的 program 名称 |
| 静态文件 404 | python manage.py collectstatic --noinput | --noinput跳过确认提示,适合自动化部署 |
| rsync 认证失败 | chmod 600 /etc/rsyncd.secrets | 600确保仅 root 可读写,rsync daemon 强制要求 |
| HTTP 未重定向 HTTPS | nginx -t && nginx -s reload | nginx -t验证配置语法,-s reload平滑重启 |
执行nginx -t后若提示syntax is ok,再执行nginx -s reload生效新配置。任何配置修改后必须执行此流程,否则更改无效。
本文还有配套的精品资源,点击获取