简介:这是一套面向知识竞赛组织者与开发者的在线答题系统源码资源,基于VC++6.0与Access数据库实现,适合需要搭建竞赛平台、进行课程设计或二次开发的技术人员参考。系统覆盖试题管理、人员管理、在线答题、自动评分与排名展示等核心模块,支持添加、删除、修改试题,录入参赛者信息,并依据答对数量、答题速度等维度生成动态排名,同时具备计时、通知与数据统计等辅助功能。压缩包共61个文件,约7.68MB,以cpp与h源码文件为主,辅以obj、pdb等编译中间文件,以及mdf、ldf数据库脚本、rc资源文件、ico图标和exe可执行程序,结构完整,便于直接运行或分析实现逻辑。目前已有393人学习下载。通过研读源码与数据库设计,读者可掌握竞赛系统的整体架构、答题流程与排名算法,快速完成环境搭建、功能扩展或维护升级。
1. 知识竞赛系统:从线下抢答器到线上高并发,一套能扛住 500 人同时交卷的实战方案
做过知识竞赛系统的人都知道,这东西最邪门的不是出题,而是交卷那一瞬间。线下抢答器时代,拼的是手速和电路;搬到线上之后,拼的是后端能不能在 3 秒内接住几百号人同时点「提交」的请求。我见过太多团队把知识竞赛系统当成一个「题库 + 表单」的 CRUD 项目来做,结果初赛一开考,数据库连接池直接打满,选手页面转圈转到怀疑人生。知识竞赛系统的本质是一套带计时、带排名、带防作弊的在线答题引擎,它要解决的核心问题有三个:题目怎么发、答案怎么收、排名怎么算。适合谁看?适合正在做企业内部培训考试、校园学科竞赛、党建知识问答、行业技能比武这类场景的后端和全栈工程师。接下来我按「架构怎么搭 → 题目怎么发 → 答案怎么收 → 排名怎么算 → 坑在哪」的顺序,把这套系统拆开讲透。
2. 知识竞赛系统的架构选型:为什么我不建议一上来就上微服务
2.1 单体 + Redis 的组合,能覆盖 90% 的竞赛场景
先泼一盆冷水:绝大多数知识竞赛系统的实际并发量,远没有你想的那么高。一场 500 人的竞赛,如果限时 30 分钟,平均每秒的答题请求可能只有几十次,真正的峰值集中在开考后前 10 秒和交卷前 30 秒。这两个峰值用单体应用 + Redis 缓存完全扛得住,上微服务只会让你在部署和调试上多花两周时间。
我一般推荐的架构是这样的:Nginx 做负载均衡,后面挂 2 到 4 个无状态的应用实例,应用层用 Spring Boot 或 FastAPI 都行,数据层分三块——MySQL 存题库和最终成绩,Redis 存会话状态和实时排名,消息队列(RabbitMQ 或 Redis Stream)削峰处理交卷请求。这套组合的好处是每一层都能单独扩容,而且运维成本低,一个运维工程师就能搞定。
选型上有个关键判断:你的竞赛是「一次性」还是「常态化」。一次性竞赛(比如年度比武)用单体 + 云数据库就够了,考完就下线;常态化竞赛(比如每周一练)才需要考虑读写分离和分库分表。别为了技术而技术,我见过一个 200 人的内部竞赛上了 K8s 集群,结果光是调 Helm Chart 就花了一周,纯属血泪教训。
2.2 用 Docker Compose 在本地跑通最小可运行环境
光说架构没用,先把环境跑起来。下面这份docker-compose.yml是我常用的最小配置,包含 MySQL、Redis 和应用服务三个容器,本地开发直接docker compose up就能跑。
version: "3.8" services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: quiz_root_2024 MYSQL_DATABASE: quiz_db ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql command: --max_connections=500 --innodb_buffer_pool_size=512M redis: image: redis:7-alpine ports: - "6379:6379" command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru app: build: . ports: - "8080:8080" depends_on: - mysql - redis environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/quiz_db SPRING_REDIS_HOST: redis这份配置里有三个参数值得单独说。max_connections=500是 MySQL 的最大连接数,竞赛场景下应用实例多、连接池配置大,默认的 151 根本不够用。innodb_buffer_pool_size=512M是 InnoDB 缓冲池大小,题库和成绩表都靠它加速,本地开发给 512M 足够,生产环境按物理内存的 60% 到 70% 设置。Redis 的maxmemory-policy allkeys-lru是内存淘汰策略,竞赛期间排名数据是热数据,用 LRU 能保证内存不爆。
启动之后验证一下:docker compose ps看到三个容器都是running状态,然后docker compose exec mysql mysql -uroot -p能进 MySQL 命令行,说明环境通了。这一步看着简单,但我踩过的坑是 MySQL 8.0 默认的认证插件是caching_sha2_password,老版本的 JDBC 驱动连不上,要么升级驱动,要么在init.sql里改回mysql_native_password。
2.3 数据库表结构:三张核心表撑起整个系统
表结构设计直接决定了后面排名和防作弊能不能做。核心就三张表:question(题库)、exam_record(答题记录)、exam_session(考试场次)。
CREATE TABLE question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, content TEXT NOT NULL COMMENT '题干', options JSON NOT NULL COMMENT '选项,格式 [{"key":"A","text":"..."}]', answer CHAR(1) NOT NULL COMMENT '正确答案', score INT DEFAULT 1 COMMENT '分值', category VARCHAR(64) COMMENT '分类', difficulty TINYINT DEFAULT 1 COMMENT '难度 1-5' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE exam_session ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(128) NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, duration_minutes INT NOT NULL COMMENT '单场时长', status TINYINT DEFAULT 0 COMMENT '0未开始 1进行中 2已结束' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE exam_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, session_id BIGINT NOT NULL, user_id BIGINT NOT NULL, question_id BIGINT NOT NULL, user_answer CHAR(1), is_correct TINYINT DEFAULT 0, answer_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_session_user_question (session_id, user_id, question_id), KEY idx_session_user (session_id, user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;exam_record上的唯一索引uk_session_user_question是防重复提交的第一道防线,同一个用户对同一道题只能有一条记录,重复提交直接触发唯一键冲突。idx_session_user这个联合索引是给排名查询用的,按场次和用户聚合分数时能走索引。options字段用 JSON 类型而不是拆成单独的选项表,是因为选项数量固定且不需要单独查询,JSON 更省事。注意answer_time默认CURRENT_TIMESTAMP,后面算答题用时和检测异常提交都靠它。
3. 题目下发与答题流程:把「发题」这件事做稳
3.1 题目乱序与选项乱序的实现
知识竞赛系统最容易被忽略的防作弊手段就是乱序。同一场考试,每个选手拿到的题目顺序和选项顺序都应该不一样,这样邻座之间没法对答案。实现思路很简单:考试开始时,后端根据session_id和user_id生成一个随机种子,用这个种子对题目列表做洗牌,把洗牌后的题目 ID 顺序存到 Redis 里。
import random import json import redis r = redis.Redis(host='localhost', port=6379, db=0) def generate_paper(session_id: int, user_id: int, question_ids: list): # 用 session_id + user_id 作为种子,保证同一用户每次拉取顺序一致 seed = hash(f"{session_id}:{user_id}") & 0xFFFFFFFF rng = random.Random(seed) shuffled = question_ids.copy() rng.shuffle(shuffled) # 选项乱序:对每道题的选项单独洗牌 paper = [] for qid in shuffled: options = get_options_from_db(qid) # 返回 [{"key":"A","text":"..."}] rng.shuffle(options) paper.append({"question_id": qid, "options": options}) # 缓存 2 小时,覆盖整场考试时长 r.setex(f"paper:{session_id}:{user_id}", 7200, json.dumps(paper)) return paper这段代码的关键在于种子必须可复现。用hash(session_id:user_id)做种子,好处是同一个用户刷新页面后拿到的题目顺序不变,避免选手因为刷新而看到不同试卷导致混乱。rng.shuffle对选项列表原地洗牌,注意洗牌后选项的key还是原来的 A/B/C/D,但text的顺序变了,前端渲染时按数组顺序显示即可。缓存时间设 7200 秒(2 小时),比一般考试时长多留了缓冲。
有个细节要注意:Python 的hash()函数在不同进程间结果可能不一致(因为 PYTHONHASHSEED 随机化),生产环境要用hashlib.md5替代。
import hashlib seed = int(hashlib.md5(f"{session_id}:{user_id}".encode()).hexdigest()[:8], 16)这样跨进程、跨重启都能保证种子一致。
3.2 答题接口的幂等设计与限流
答题接口是整个系统里调用最频繁的接口,必须做幂等和限流。幂等靠数据库唯一索引兜底,限流靠 Redis 计数器。
@PostMapping("/api/answer") public Result submitAnswer(@RequestBody AnswerDTO dto, HttpServletRequest request) { Long userId = getUserId(request); String rateKey = "rate:answer:" + userId; // 限流:每个用户每秒最多 5 次答题请求 Long count = redisTemplate.opsForValue().increment(rateKey); if (count == 1) { redisTemplate.expire(rateKey, 1, TimeUnit.SECONDS); } if (count > 5) { return Result.fail("操作过于频繁,请稍后再试"); } // 校验考试是否在进行中 ExamSession session = sessionService.getById(dto.getSessionId()); if (session.getStatus() != 1) { return Result.fail("考试未开始或已结束"); } try { examRecordService.saveAnswer(userId, dto); } catch (DuplicateKeyException e) { // 唯一键冲突说明已答过,直接返回成功,保证幂等 return Result.success("已提交"); } return Result.success(); }限流用increment+expire的组合,第一次请求时设置 1 秒过期,后续请求在 1 秒内累加,超过 5 次就拒绝。这个阈值怎么定?一般选手答一道单选题需要 3 到 10 秒,每秒 5 次已经是很宽松的上限,能挡住脚本刷题但不会误伤正常操作。DuplicateKeyException的捕获是幂等的关键——用户网络抖动导致重复提交时,第二次请求会撞唯一索引,这时候返回成功而不是报错,用户体验更好。
考试状态校验不能省。我见过一个翻车案例:考试结束后选手还能提交答案,原因是状态判断写在了前端,后端没校验。所有状态判断必须在后端做,前端只是展示层。
3.3 断线重连与答案暂存
竞赛现场网络抖动是常态,选手答到一半断线,重新进来必须能看到已答的题目。这个靠 Redis 暂存 + 数据库落盘双写实现。
// 前端:每答一题,本地暂存 + 异步上报 const answerCache = new Map(); async function submitAnswer(questionId, answer) { answerCache.set(questionId, answer); localStorage.setItem('quiz_cache', JSON.stringify([...answerCache])); try { await fetch('/api/answer', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({questionId, answer, sessionId}) }); } catch (e) { // 网络失败不阻塞,本地已暂存,重连后批量补交 console.warn('提交失败,已本地暂存', questionId); } } // 重连后批量补交 async function flushCache() { const cached = JSON.parse(localStorage.getItem('quiz_cache') || '[]'); for (const [qid, ans] of cached) { await submitAnswer(qid, ans); } }前端用localStorage做本地暂存,断网时答案不丢。重连后调用flushCache批量补交,因为后端接口是幂等的,重复提交不会出问题。这里有个坑:localStorage存的是明文答案,如果竞赛涉及敏感内容,要考虑加密存储。另外批量补交时要加个节流,别一次性发几百个请求把后端打挂。
后端在 Redis 里也存一份答题进度,key 是progress:{session_id}:{user_id},用 Hash 结构存question_id -> answer的映射。选手重新进入考试时,先读 Redis 进度,再合并数据库记录,取并集返回给前端。这样即使换设备登录,答题进度也能恢复。
4. 交卷与排名计算:高并发下的成绩一致性
4.1 交卷请求的削峰与异步处理
交卷是整场考试压力最大的时刻。500 人同时交卷,如果每个请求都同步算分、写库、更新排名,数据库瞬间就会被打满。我的做法是交卷请求只做入队,算分和排名异步处理。
@PostMapping("/api/submit") public Result submitExam(@RequestBody SubmitDTO dto, HttpServletRequest request) { Long userId = getUserId(request); String submitKey = "submit:" + dto.getSessionId() + ":" + userId; // 防止重复交卷,用 setnx 做分布式锁 Boolean first = redisTemplate.opsForValue() .setIfAbsent(submitKey, "1", 30, TimeUnit.MINUTES); if (Boolean.FALSE.equals(first)) { return Result.fail("请勿重复交卷"); } // 只入队,不阻塞 SubmitMessage msg = new SubmitMessage(dto.getSessionId(), userId, System.currentTimeMillis()); rabbitTemplate.convertAndSend("exam.submit", msg); return Result.success("交卷成功,成绩计算中"); }setIfAbsent是 Redis 的原子操作,只有第一次交卷能设置成功,后续重复请求直接拒绝。锁的过期时间设 30 分钟,覆盖整场考试时长,防止锁提前释放导致重复交卷。消息发到exam.submit队列后立即返回,前端显示「成绩计算中」,用户体验上不会卡顿。
消费者端从队列拉取消息,执行算分逻辑。这里要注意消息的幂等消费——RabbitMQ 可能重复投递,消费者要根据session_id + user_id判断是否已处理过。
@RabbitListener(queues = "exam.submit") public void handleSubmit(SubmitMessage msg) { String doneKey = "done:" + msg.getSessionId() + ":" + msg.getUserId(); Boolean first = redisTemplate.opsForValue().setIfAbsent(doneKey, "1", 1, TimeUnit.HOURS); if (Boolean.FALSE.equals(first)) { return; // 已处理过,直接跳过 } // 从数据库聚合该用户的所有答题记录 int score = examRecordMapper.sumScore(msg.getSessionId(), msg.getUserId()); long costSeconds = (msg.getSubmitTime() - getSessionStartTime(msg.getSessionId())) / 1000; // 写入成绩表 examResultMapper.insert(new ExamResult(msg.getSessionId(), msg.getUserId(), score, costSeconds)); // 更新 Redis 实时排名 redisTemplate.opsForZSet().add( "rank:" + msg.getSessionId(), String.valueOf(msg.getUserId()), score * 1000000 - costSeconds // 分数优先,用时次之 ); }排名分数的计算有个技巧:score * 1000000 - costSeconds。分数是主排序键,用时是次排序键,把用时取负数加到分数上,就能用 Redis 的 ZSet 一次性完成「分数降序、用时升序」的排序。1000000 这个系数要保证大于最大可能用时(秒),一般考试时长不超过 3600 秒,1000000 绰绰有余。
4.2 实时排名的 Redis ZSet 方案
Redis 的 ZSet(有序集合)是实时排名的标准方案,ZADD写入、ZREVRANGE查询 Top N、ZREVRANK查个人排名,都是 O(log N) 复杂度,几千人的排名毫秒级返回。
# 写入排名(分数 = 得分 * 1000000 - 用时秒数) ZADD rank:1001 95000000 "user_101" ZADD rank:1001 95000030 "user_102" # 查 Top 10(降序) ZREVRANGE rank:1001 0 9 WITHSCORES # 查某个用户的排名(从 0 开始,需要 +1 展示) ZREVRANK rank:1001 "user_101" # 查某个用户的分数 ZSCORE rank:1001 "user_101"ZREVRANGE返回的是降序排列,正好对应「高分在前」。ZREVRANK返回的排名是从 0 开始的,展示时要加 1。这里有个坑:如果两个用户分数和用时完全相同,ZSet 会按成员字典序排列,虽然概率极低,但严格来说需要再加一个随机因子或者按提交时间戳做最终区分。
排名数据要设置过期时间,考试结束后保留 24 小时供查询,之后自动清理。用EXPIRE rank:1001 86400即可。如果竞赛需要长期保留排名,消费者处理完后要同步落库到exam_result表。
4.3 成绩一致性校验:对账机制不能少
异步处理最大的风险是消息丢失导致成绩没算。我一般会加一个定时对账任务,每 5 分钟扫描一次「已交卷但无成绩」的记录,重新入队处理。
-- 找出已交卷但成绩表里没有记录的用户 SELECT s.session_id, s.user_id FROM exam_submit_log s LEFT JOIN exam_result r ON s.session_id = r.session_id AND s.user_id = r.user_id WHERE s.session_id = ? AND s.submit_time < DATE_SUB(NOW(), INTERVAL 5 MINUTE) AND r.id IS NULL;exam_submit_log是交卷时同步写入的日志表,记录谁在什么时候交了卷。对账任务用 LEFT JOIN 找出成绩表里缺失的记录,重新投递到消息队列。这个机制是成绩准确性的最后一道保险,宁可多算一次(幂等消费会跳过)也不能漏算。
对账任务的执行频率要权衡:太频繁浪费资源,太稀疏则选手等成绩时间过长。5 分钟是个经验值,竞赛结束后可以手动触发一次全量对账,确保所有成绩都算出来。
5. 知识竞赛系统避坑清单:5 个我真实踩过的坑
5.1 坑一:考试开始瞬间题目接口被打挂
现象:考试时间一到,所有选手同时刷新页面拉题目,题目接口 QPS 瞬间冲到几千,应用实例 CPU 打满,部分选手页面白屏。
原因:题目接口每次都查数据库,没有缓存。500 人同时请求,每个请求查 50 道题,数据库瞬间承受 25000 次查询。
解决:题目在考试开始前预生成并写入 Redis,接口只读 Redis 不读库。预生成时机设在考试开始前 10 分钟,用定时任务批量生成所有参赛选手的试卷。如果参赛名单是动态的(比如允许现场报名),就在选手首次进入考试时生成并缓存,后续请求直接读缓存。
5.2 坑二:交卷后排名不更新,选手看到的是旧排名
现象:选手交卷后刷新排名页,发现自己还是 0 分,但实际已经答对了很多题。
原因:排名更新是异步的,选手交卷后立即查排名,此时消息还在队列里没被消费。
解决:前端交卷后不要立即跳排名页,而是显示「成绩计算中」并轮询查询成绩接口。成绩接口先查exam_result表,如果没查到就返回「计算中」,查到就返回成绩和排名。轮询间隔设 1 秒,最多轮询 10 次,超时提示「成绩计算较慢,请稍后在成绩页查看」。后端消费者处理速度要保证在 3 秒内完成,否则要加消费者实例。
5.3 坑三:选手用浏览器开发者工具篡改答案
现象:有选手在提交前用 F12 修改了请求体里的答案,把错误答案改成了正确答案。
原因:后端信任了前端传来的答案,没有做二次校验。
解决:后端不能信任任何前端传来的答案内容,但可以做答案合法性校验——检查提交的答案是否在选项范围内(A/B/C/D),以及提交时间是否在考试时间内。更严格的做法是前端提交时不传答案内容,只传选项的索引或哈希值,后端根据题目 ID 和索引反查真实答案。但这样会增加复杂度,一般竞赛场景做到「校验答案格式 + 校验时间窗口 + 记录提交 IP 和设备指纹」就够了。如果竞赛涉及重大利益,建议加摄像头监考或屏幕录制。
5.4 坑四:Redis 内存爆了导致排名数据丢失
现象:竞赛进行到一半,排名页显示空白,Redis 报 OOM 错误。
原因:Redis 没有设置内存上限和淘汰策略,排名数据加上试卷缓存把内存撑爆了。
解决:Redis 启动时必须设置maxmemory和maxmemory-policy。排名数据用 ZSet 存储,500 人的排名大概占几十 KB,不是内存大户;真正占内存的是试卷缓存,每份试卷 50 道题带选项,大概 20KB,500 人就是 10MB,也不算大。但如果同时有多场考试,或者试卷缓存没设过期时间,内存就会累积。我的做法是:试卷缓存设 2 小时过期,排名数据设 24 小时过期,maxmemory设为物理内存的 70%,淘汰策略用allkeys-lru。另外要监控 Redis 内存使用率,超过 80% 就告警。
5.5 坑五:MySQL 连接池耗尽导致答题接口超时
现象:答题高峰期,接口响应时间从 50ms 飙升到 5s,日志里大量Connection timeout错误。
原因:应用连接池配置太小,默认的 HikariCP 最大连接数是 10,500 人并发答题根本不够用。
解决:连接池大小要按公式估算——最大连接数 = (CPU 核心数 * 2) + 有效磁盘数。4 核 8G 的机器,连接池设 20 到 30 比较合适。同时要设置连接超时时间(connectionTimeout)为 3 秒,避免请求无限等待。MySQL 服务端的max_connections也要相应调大,至少是应用实例数乘以单实例连接池大小再加缓冲。另外,答题接口的数据库操作要尽量简单,避免长事务,查询走索引,写入用批量提交。
6. 压测与验证:用 JMeter 模拟 500 人同时交卷
6.1 压测脚本的关键配置
系统上线前必须压测,不压测就上线等于裸奔。我用 JMeter 模拟 500 人同时交卷的场景,核心是线程组配置和集合点。
线程组设置:线程数 500,Ramp-up 时间 1 秒(模拟瞬间并发),循环次数 1。集合点(Synchronizing Timer)设置 500 个用户,让所有线程在交卷请求前等待,同时发出。这样能精确模拟「考试结束铃响,所有人同时点交卷」的极端场景。
HTTP 请求配置:POST 到/api/submit,请求体用 CSV 参数化,每个线程用不同的user_id和session_id。CSV 文件准备 500 行用户数据,用__CSVRead函数读取。
# 用 JMeter 命令行模式跑压测,生成结果文件 jmeter -n -t submit_test.jmx -l result.jtl -e -o report/ # 查看聚合报告关键指标 # Average: 平均响应时间,目标 < 500ms # 99% Line: 99% 请求的响应时间,目标 < 2000ms # Error %: 错误率,目标 < 0.1% # Throughput: 吞吐量,目标 > 500/s压测结果重点关注三个指标:99% 响应时间、错误率、吞吐量。99% 响应时间超过 2 秒说明有请求排队严重,要加应用实例或优化数据库查询。错误率超过 0.1% 要查日志定位原因,常见的是连接池耗尽或 Redis 超时。吞吐量上不去通常是数据库瓶颈,考虑加缓存或读写分离。
6.2 压测中发现的典型瓶颈与调优
第一次压测大概率会翻车,这很正常。我总结了几种典型瓶颈和对应的调优手段。
| 瓶颈现象 | 可能原因 | 调优手段 |
|---|---|---|
| 交卷接口 99% 响应 > 5s | 消息队列消费者处理慢 | 增加消费者实例,批量消费 |
| 答题接口错误率 > 1% | 数据库连接池耗尽 | 调大 HikariCP maximumPoolSize |
| Redis 超时频繁 | 单节点 Redis 压力大 | 加 Redis 从节点做读写分离 |
| 应用 CPU 打满 | 序列化/反序列化开销大 | 换用 Protobuf 或减少传输字段 |
| MySQL 慢查询增多 | 缺少索引或索引失效 | 用 EXPLAIN 分析,补索引 |
调优的顺序是:先加缓存,再调连接池,最后才考虑加机器。加机器是最贵的手段,能靠优化解决就别堆硬件。压测通过的标准是:500 并发下 99% 响应时间小于 2 秒,错误率小于 0.1%,且系统在压测后能自动恢复到正常状态(没有内存泄漏或连接泄漏)。
6.3 一个我坚持了三年的习惯
每次竞赛上线前,我都会做一次全链路演练:找 10 个同事,用真实设备、真实网络,完整走一遍从登录到交卷到查排名的流程。压测工具模拟的请求再真实,也比不上真人操作暴露的问题——比如某个型号的手机键盘弹不出来、某个浏览器的时间控件不兼容、某个网络环境下 WebSocket 断连。这些问题压测脚本发现不了,但真实用户一用就炸。
演练时我会盯着监控大屏,重点看四个指标:接口响应时间、错误率、Redis 内存、MySQL 连接数。任何一个指标异常就立即暂停演练,排查清楚再继续。这个习惯帮我挡掉了至少三次线上事故,虽然每次演练要多花半天时间,但比起竞赛当天翻车,这半天花得太值了。
知识竞赛系统这东西,技术难度不算高,但细节特别多,任何一个细节没考虑到都可能在关键时刻掉链子。把上面这些做完,你的系统扛住 500 人同时交卷基本没问题。希望帮到你。
本文还有配套的精品资源,点击获取