3月15日,原本是全球消费者共同关注的日子,可对于常年在键盘上敲代码的我们来说,更多意味着一次别开生面的编程挑战。今年年初,我们团队用短短两周时间,搭建了一个名为“3.15 OJ”的在线判题系统,在消费者权益日当天组织了一场“诚信打假”编程竞赛。这篇文章我想从策划、开发、出题到运维,完整复盘一下这次项目的经验。无论你是第一次接触在线判题系统,还是想尝试做一场有主题的技术活动,这篇内容都能给你一些可以直接抄走的思路。
先说背景。市面上主流的在线判题平台多如牛毛,算法训练、竞赛备赛都有成熟方案。但纯技术导向的平台往往缺一点“场景感”,参与者刷题只是机械地求AC,缺少代入感。我们想做一个不一样的:以消费者权益保护为内核,把数据核验、信用评分、反欺诈检测这些真实业务场景封装成题目,再放到一个自研的OJ平台上跑起来。这样一来,玩的人既练了算法,又理解了消费维权背后的技术逻辑。整个项目分为四大块:题目设计、评测系统、用户体系、赛事运营,下面我逐一拆开讲。
1. 内容整体设计与思路拆解
1.1 从一句口号到功能清单
最开始只有一句话:“做一个3.15主题的算法比赛”。这句话落到纸上,就要拆成可执行的功能点:
- 用户注册、登录与个人信息管理;
- 题目列表、题目详情、代码提交;
- 判题服务:编译、运行、比对输出;
- 排行榜:按正确题目数和罚时排序;
- 管理员后台:管理题目、用户、公告。
我见过很多项目一开场就想着加论坛、加私信、加成就系统,结果两周时间根本做不完。我们的原则很直接:砍掉一切不影响核心闭环的功能。核心闭环就是“用户看到题 -> 写代码 -> 提交 -> 得到结果”。
但光有闭环还不够。既然叫“3.15 OJ”,题目一定要紧扣消费者权益。回想一下每年的315晚会,曝光最多的不外乎数据造假、虚假宣传、产品质量缺陷。对应到编程任务,就是数据校验、异常检测、分类识别。我们把这三类场景做成了五道题,难度从入门到进阶逐级抬升,既照顾了新手,也给老手留了发挥空间。
1.2 平台定位与差异化
市面上的OJ通常面向竞赛用户,界面冷淡、题目偏难,新手望而生畏。我们的定位恰恰相反:面向大众开发者、高校学生和刚入门算法的人。所以界面要友好,题目要带业务背景,判题反馈要人性化。
怎么体现“人性化”?普通的OJ只告诉你“Wrong Answer”或“Time Limit Exceeded”,我们用了另一种策略:每个错误类型附带一句业务化提示。比如题目是“检测商品标签是否合规”,当提交的代码漏掉了某条规则,判题结果返回的不是冷冰冰的WA,而是“检测到第3条规则未被覆盖,请检查生产日期格式”。这个设计让参与者更容易定位问题,也更有沉浸感。
差异化还有一个点:我们提供了“参考解法”和“赛后报告”。所有比赛的题解在赛后公开,每道题附上解题思路、算法复杂度分析、代码示例和常见误区。平台本身不只是竞赛工具,还是一个学习资源库。
1.3 风险预案与边界控制
因为项目周期只有两周,必须提前想清楚风险点。
- 判题服务不能因为某段恶意代码而崩溃,所以必须沙箱隔离;
- 并发用户数可能瞬时暴增,所以评测任务要异步排队;
- 数据要每天备份,防止比赛当天出乱子。
我们没有一步到位做复杂的微服务架构,而是单机部署加进程隔离,先用最简单稳定方案跑通业务。如果数据量真的大到扛不住,再考虑横向扩展也不迟。
2. 核心细节解析与实操要点
2.1 题目到底怎么设计
这是整个项目最花心思的地方。每道题都需要有真实业务感,又要保证算法考点清晰。
举个例子,我们设计了一道“商品标签校验”题。业务描述是:某电商平台上有很多商品,每个商品有一个标签字符串,包含名称、产地、生产日期、保质期。要求编写程序,校验给定标签是否符合国家相关格式规范。这个题目看似是字符串处理,实际考察的知识点包括正则表达式、边界条件处理和状态机。
再比如“投诉文本分类”那道题,给出一段用户投诉内容,要求判断投诉属于“质量问题”“物流问题”还是“服务态度问题”。这本来是个文本分类任务,但为了降低门槛,我们限定用户只能用规则方法或简单的词频统计,而不需要训练深度学习模型。这样新手也能用字典、列表这些基础结构做出来。
每道题我们都准备了10到20组测试数据,包含正常情况、边界情况和极特殊情况。比如商品标签里生产日期写的是“2024-02-30”,这种明显不存在的日期必须有测试用例覆盖;投诉文本里如果夹杂表情符号,也不能影响分类结果。
2.2 评测规则:为什么采用ACM式罚时
排行榜规则直接借用国际大学生程序设计竞赛(ACM-ICPC)的常见玩法:正确一题得一个气球(计一分),名次先看正确题数,再看总罚时。每次错误提交罚时20分钟,这个规则大家很熟悉,不用额外解释。
但我们加了一条自己的规则:连续提交相同代码三次以上,直接判为恶意提交,封禁账号24小时。主要目的是杜绝有些人用自动脚本刷评测次数,既浪费判题资源,也对认真做题的人不公平。
判题立足点是“一次提交定胜负”,但也允许在比赛期间重复提交,只要最终通过即可。这种宽容度更适合新手,不会因为一个编译错误就彻底丢掉一道题。
2.3 反作弊与诚信体系
3.15的主题本身就是“诚信”,所以反作弊不能走过场。我们在平台里加了三道防线:
- 代码相似度检查:提交代码两两比对,计算相似度,超过阈值则标记;
- 提交时间和排名变化监控:如果某用户排行榜名次在最后几分钟波动异常,人工复核;
- 赛后答辩环节:获得前列名次的选手,要求用视频会议现场讲解自己的代码逻辑,讲解不通的一律取消资格。
这三道防线层层递进,既防止了简单的拷贝行为,也筛掉了代打选手。诚信数据我们保存了整整一年,后续还开放给合作单位做信用参考。
3. 实操过程与核心环节实现
3.1 技术选型:稳定压倒一切
两周开发周期,容不得试错。技术栈全部选我们团队最熟悉的一套:
- 后端:Java Spring Boot,因为团队成员对它最熟,生态也完善;
- 前端:Vue3 + Element Plus,搭建后台管理界面效率极高;
- 数据库:MySQL 8.0,存储用户和题目数据;
- 缓存:Redis,用于排行榜和会话管理;
- 判题模块:Python脚本 + Docker沙箱,独立服务,通过消息队列通信。
为什么判题要用独立服务?因为判题任务会消耗大量CPU和内存,如果和Web服务放一起,用户并发一高,整个网站就卡死了。独立服务的好处是资源隔离,即使判题进程偶发崩溃,也不影响用户浏览题目。
3.2 核心表结构设计与代码展示
用户表、题目表、提交记录表这三张表是整个系统的心脏。下面是一张简化版提交记录表:
CREATE TABLE submission ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, problem_id INT NOT NULL, code TEXT NOT NULL, language VARCHAR(20) NOT NULL, status TINYINT NOT NULL DEFAULT 0, used_time INT DEFAULT NULL, used_memory INT DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );状态字段我用了数字表示,0是排队中、1是编译中、2是运行中、3是通过、4是答案错误、5是超时、6是内存超限、7是运行时错误。这个设计简单直白,查问题的时候一眼就能看懂。
前端提交代码后,请求先进入Web服务,然后把任务扔进Redis消息队列,判题服务监听队列取任务,执行编译和运行,最后把结果写回数据库。整个流程是异步的,即使判题慢,网页也不会卡死。
3.3 沙箱隔离:把安全风险挡在门外
在线判题最怕什么?怕有人提交恶意代码,比如读取服务端文件、删除数据库、或者无限循环把机器拖垮。我们的方案是Docker容器隔离。
每个提交在独立容器里运行,容器内没有网络访问权限,挂载目录只读,CPU和内存都做了限制。具体实现时,用Python的docker模块启动容器,执行编译和运行命令,读取输出。
import docker client = docker.from_env() container = client.containers.run( "gcc:latest", command="bash -c 'gcc /submission/main.c -o /submission/main && /submission/main'", network_disabled=True, mem_limit="128m", cpu_period=100000, cpu_quota=50000, detach=True, remove=True ) log = container.logs(stdout=True, stderr=True, timestamps=False)这里有一个细节容易踩坑:容器启动时如果不加network_disabled=True,默认是能联网的,那就可能被恶意脚本利用,去访问外部资源。加了之后就直接断网,安全系数大幅提升。
内存限制我设置为128MB,CPU额度限制为0.5核,正常题目的代码怎么跑都够用,恶意程序也折腾不出水花。
4. 常见问题与排查技巧实录
4.1 判题队列堆积,用户提交后长时间无响应
上线首日就遇到了。一开始有几十个用户同时提交,前端页面一直转圈,后台日志显示Redis队列长度飙升,但判题服务消费速度很慢。
排查过程三步走:
- 第一步,看CPU和内存,发现判题服务所在服务器的CPU已经打满;
- 第二步,看Docker容器数量,发现还有一批僵尸容器没有销毁,占着内存;
- 第三步,看代码逻辑,发现容器销毁动作写在了日志读取之后,万一日志读取失败,容器就永远停在那里。
根因是异常处理不完善。修改方式:用try-finally结构确保容器最终被清理,并给容器操作加超时时间。这样即使某个测试数据异常卡住了,也能及时杀掉容器,释放资源。
4.2 测试用例覆盖不全,导致误判
我们出一道“价格欺诈检测”题,要求识别一段时间内价格波动超过阈值的商品。最初测试数据里只给了线性波动的情况,结果不少选手用简单比较也能过。赛后我们复盘,又补了一组数据:价格是先涨后跌再急涨,峰值时间不在末尾,单纯从头到尾比较的解法就会出错。
测试数据质量直接决定题目含金量。我们的经验是:在设计测试用例时,先想清楚正解应该怎么写,再用正解生成答案,再思考哪些“投机取巧”的解法能通过原有数据,然后特意设计反例削弱它们。
4.3 在线IDE卡顿,影响用户体验
我们做了一个简易的网页代码编辑器,基于开源的文本编辑组件。但比赛时发现,如果用户代码超过两百行,编辑器会明显卡顿。
原因是代码高亮和语法检查每次按键都在重新执行,开销很大。修复方式是加入防抖函数,只有停止输入500毫秒才做语法高亮,同时把编辑器的智能提示关掉一半。改完之后卡顿基本消失。
5. 赛事运营与体验优化
5.1 比赛前预热:让参与者提前了解平台
第一次在3.15当天办比赛,最大的担忧是没人来。我们提前一周在技术社区发布了比赛预告,附上平台地址和两道上手题。这两道题难度很低,只要会基础语法就能做出,目的是让大家提前熟悉提交和判题流程,避免比赛当天因为不会操作而误事。
同时,在报名时要求填写一段自己与消费者权益保护相关的经历,比如买过什么假货、如何维权等。这个设置看似与编程无关,却让用户提前进入“3.15”这个主题语境,也增加了参与者的归属感。
5.2 实时榜单与观众互动
比赛期间,排行榜每5分钟刷新一次,数据从Redis里读取,性能很好。我们还开了弹幕墙,观众和选手都能发文字,气氛瞬间拉满。不过弹幕墙后来被很多人刷梗,导致和比赛无关内容过多,下次可以考虑设置关键词过滤。
5.3 赛后复盘:最有价值的沉淀
比赛结束不是活动结束。我们做了一场直播题解,每道题不只讲正解,还把错误解法拿出来公开处刑。比如有人题目都没看清就写了一百行代码,这类“反面教材”比正确写法更有教育意义。
所有提交代码都匿名化后公开,参赛者可以查看别人的解法,学习不同思路。这样做的一个好处是,即使没有获奖的人也能在赛后学到东西,不会觉得自己白来一趟。
最后再分享一个小技巧:在线判题系统的成绩统计不要只看最终排名。把每道题的首次通过时间、错误提交次数、平均解题时间拉出来,做成图表,能非常直观地看出参与者的水平和题目难度是否匹配。下次办活动,这些数据就是调整题目的依据。
做这个项目的过程中,我最大的体会是:技术难度其实不是核心,最难的是把业务场景和算法题巧妙地融合在一起。如果在出题时只顾着考算法,就会变成普通的OJ,丢了“3.15”的魂;如果只顾着讲故事,又会让题目变得华而不实。我们反复打磨了好几轮,才让每道题既讲得清业务,又算得清复杂度。
如果你也打算做一个类似的主题OJ,我的建议是:先把题目设计好,再动工开发平台。题目是整个活动的灵魂,平台只是承载它的工具。不要一上来就写代码,先拿出一张纸,画出题目的业务场景和算法考点,再投入开发,你会少走很多弯路。