数据库课设实战:平行志愿模拟录取系统设计与投档算法实现

发布时间:2026/10/14 7:31:47
数据库课设实战:平行志愿模拟录取系统设计与投档算法实现 简介这份资源是面向高校数据库课程设计场景的「平行志愿模拟录取系统」完整项目包适合正在完成数据库课设、需要参考真实业务建模的本科生与自学者。系统以高考平行志愿投档录取为核心业务涵盖考生信息、院校专业、志愿填报与录取规则等典型数据实体可用于练习需求分析、E-R建模、表结构设计与复杂查询编写。压缩包共969个文件约3.87MB以JavaScript、CSS、SCSS、Vue、TypeScript等前端资源为主辅以Java后端代码、SQL脚本及少量HTML、Markdown、XML与Excel数据文件构成前后端分离的完整工程结构。资源中附带数据库脚本与说明文档便于读者还原表关系、理解投档排序与录取判定逻辑并对照代码梳理接口与数据流转。目前已有104人学习下载适合作为课设选题参考或二次开发基础。1. 平行志愿模拟录取系统数据库课设里最容易被低估的硬骨头每年到了数据库课程设计选题的时候总有一批人盯着学生成绩管理图书借阅系统这类题目觉得稳。但真正做过一轮的人会告诉你平行志愿模拟录取系统才是那个看起来简单、做起来全是坑的选题。它表面上是个增删改查的活儿实际上你要处理的是志愿填报、分数排序、投档检索、录取状态流转这一整套带业务规则的流程。数据库设计一旦没想清楚后面写查询能写到怀疑人生。这个系统的核心价值在于它把数据库课程里最关键的几个能力——关系建模、事务控制、复杂查询、并发一致性——全部塞进了一个真实场景里。适合谁做适合已经学完 SQL 基础、想拿一个能写进简历的课设的人也适合那些做过简单 CRUD 但没碰过一条 SQL 决定谁被录取这种逻辑的人。接下来我按实际落地的顺序把建模、建表、投档算法、并发处理和排错一条条拆开讲。2. 先把数据模型立住五张表撑起整个录取流程2.1 为什么是五张表而不是三张很多人第一反应是建三张表考生表、志愿表、院校专业表。跑起来发现不对——录取结果放哪调剂状态怎么记同一考生在不同批次的投档怎么区分所以最少要五张核心表考生表、院校专业计划表、志愿填报表、投档记录表、录取结果表。这里有个建模上的关键决策志愿填报表和投档记录表必须分开。志愿是考生填的投档是系统算的。如果把投档结果直接写回志愿表一旦需要重新模拟比如改了投档规则原始志愿数据就被污染了没有后悔药可吃。分开之后志愿表是只读输入投档记录表是可重算的输出。另一个容易翻车的地方是分数和位次的关系。平行志愿的核心是分数优先、遵循志愿所以考生表里必须同时存总分和同分排序位次。只存总分遇到同分考生就没法确定谁先投档这是血泪经验。2.2 建表 SQL 与字段说明-- 考生表存储考生基本信息与成绩 CREATE TABLE student ( student_id BIGINT PRIMARY KEY AUTO_INCREMENT, exam_no VARCHAR(20) NOT NULL UNIQUE COMMENT 准考证号, name VARCHAR(50) NOT NULL, total_score INT NOT NULL COMMENT 总分, rank_no INT NOT NULL COMMENT 全省位次同分按规则排好, batch_code VARCHAR(10) NOT NULL COMMENT 批次代码, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_batch_rank (batch_code, rank_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 院校专业计划表每个专业方向的招生名额 CREATE TABLE major_plan ( plan_id BIGINT PRIMARY KEY AUTO_INCREMENT, college_code VARCHAR(10) NOT NULL, major_code VARCHAR(10) NOT NULL, major_name VARCHAR(100) NOT NULL, batch_code VARCHAR(10) NOT NULL, plan_count INT NOT NULL COMMENT 计划招生人数, admitted_count INT NOT NULL DEFAULT 0 COMMENT 已录取人数, UNIQUE KEY uk_college_major (college_code, major_code, batch_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 志愿填报表考生填报的志愿顺序 CREATE TABLE volunteer ( volunteer_id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, seq_no INT NOT NULL COMMENT 志愿顺序从1开始, college_code VARCHAR(10) NOT NULL, major_code VARCHAR(10) NOT NULL, batch_code VARCHAR(10) NOT NULL, UNIQUE KEY uk_student_seq (student_id, seq_no), INDEX idx_student (student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 投档记录表系统模拟投档的结果 CREATE TABLE admission_log ( log_id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, volunteer_id BIGINT NOT NULL, plan_id BIGINT NOT NULL, status VARCHAR(20) NOT NULL COMMENT ADMITTED/REJECTED, reason VARCHAR(200) COMMENT 未录取原因, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_student (student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 录取结果表最终录取名单 CREATE TABLE admission_result ( result_id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL UNIQUE, plan_id BIGINT NOT NULL, admit_score INT NOT NULL, admit_rank INT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计上有几个点值得展开。student表的rank_no是预先算好的不要在投档时现算因为同分排序规则可能涉及单科成绩现算会让 SQL 变得极其复杂。major_plan表的admitted_count是冗余字段但它能避免每次投档都去admission_result里 count 一遍这是典型的用空间换时间。volunteer表的seq_no加了唯一约束防止同一考生填出两个第 1 志愿。提示batch_code这个字段几乎出现在每张表里因为不同批次的投档是独立进行的。如果课设只做一个批次可以简化掉但建议保留答辩时能体现你对业务边界的理解。2.3 索引怎么加才不白加上面建表语句里已经埋了几个索引这里说清楚为什么。student表的idx_batch_rank是投档时排序用的投档算法要按批次取出所有考生按位次排序这个复合索引能让排序走索引而不是 filesort。volunteer表的uk_student_seq既保证数据正确性又让查某考生的所有志愿这个高频操作走索引。不要给admission_log的status单独建索引因为它的区分度太低只有两个值建了反而拖慢写入。这是很多人做课设时容易犯的错——看到 where 条件就加索引结果索引比数据还大。3. 投档算法落地一条 SQL 还是一段程序3.1 两种实现路线的取舍平行志愿投档有两种实现方式纯 SQL 实现和程序循环实现。纯 SQL 的思路是用窗口函数一次性算出每个考生能投到哪个志愿程序循环的思路是按位次逐个考生处理每个考生从第 1 志愿开始检索。课设场景下我一般推荐程序循环 事务的方式。原因很实际纯 SQL 虽然优雅但一旦投档规则有变化比如某个专业有单科要求SQL 会膨胀到没法维护。而且程序循环更容易加日志、更容易调试答辩时也能讲清楚每一步在干什么。但程序循环有个致命问题并发。如果两个线程同时处理两个考生都去读同一个专业的admitted_count就可能超录。所以必须用事务加行锁或者用乐观锁的版本号机制。3.2 核心投档逻辑的 Python 实现import pymysql def simulate_admission(batch_code): 模拟指定批次的平行志愿投档 核心规则分数优先、遵循志愿、一轮投档 conn pymysql.connect(hostlocalhost, userroot, passwordyour_password, databaseadmission, charsetutf8mb4, autocommitFalse) try: with conn.cursor(pymysql.cursors.DictCursor) as cur: # 1. 按位次取出本批次所有考生位次小的优先 cur.execute( SELECT student_id, total_score, rank_no FROM student WHERE batch_code %s ORDER BY rank_no ASC , (batch_code,)) students cur.fetchall() for stu in students: # 2. 取该考生的志愿按顺序排列 cur.execute( SELECT v.volunteer_id, v.seq_no, p.plan_id, p.plan_count, p.admitted_count FROM volunteer v JOIN major_plan p ON v.college_code p.college_code AND v.major_code p.major_code AND v.batch_code p.batch_code WHERE v.student_id %s AND v.batch_code %s ORDER BY v.seq_no ASC , (stu[student_id], batch_code)) volunteers cur.fetchall() admitted False for vol in volunteers: # 3. 加行锁读取计划余额防止并发超录 cur.execute( SELECT plan_count, admitted_count FROM major_plan WHERE plan_id %s FOR UPDATE , (vol[plan_id],)) plan cur.fetchone() if plan[admitted_count] plan[plan_count]: # 4. 有名额录取 cur.execute( UPDATE major_plan SET admitted_count admitted_count 1 WHERE plan_id %s , (vol[plan_id],)) cur.execute( INSERT INTO admission_result (student_id, plan_id, admit_score, admit_rank) VALUES (%s, %s, %s, %s) , (stu[student_id], vol[plan_id], stu[total_score], stu[rank_no])) cur.execute( INSERT INTO admission_log (student_id, volunteer_id, plan_id, status) VALUES (%s, %s, %s, ADMITTED) , (stu[student_id], vol[volunteer_id], vol[plan_id])) admitted True break else: # 5. 没名额记录并继续检索下一志愿 cur.execute( INSERT INTO admission_log (student_id, volunteer_id, plan_id, status, reason) VALUES (%s, %s, %s, REJECTED, 计划已满) , (stu[student_id], vol[volunteer_id], vol[plan_id])) if not admitted: # 所有志愿都投不出去标记为滑档 cur.execute( INSERT INTO admission_log (student_id, volunteer_id, plan_id, status, reason) VALUES (%s, 0, 0, REJECTED, 所有志愿均未投中) , (stu[student_id],)) conn.commit() except Exception as e: conn.rollback() raise e finally: conn.close()这段代码有几个关键点。第一ORDER BY rank_no ASC保证了分数优先位次小的先处理。第二FOR UPDATE是行级锁锁住当前专业的计划行防止两个考生同时读到相同的admitted_count然后都加一导致超录。第三每个考生一旦投中一个志愿就break这对应平行志愿一轮投档的规则——不会因为第 1 志愿没中就把第 2 志愿当第 1 志愿重新排。参数上batch_code是必须传的因为不同批次独立投档。如果你要做多批次外层再套一层循环即可。autocommitFalse配合手动 commit/rollback 是保证原子性的基础千万别用自动提交。3.3 投档结果怎么验证投档跑完之后至少要做三个校验。第一admission_result里每个plan_id的录取人数不能超过major_plan.plan_count。第二每个考生最多只有一条录取记录。第三所有被拒的志愿在admission_log里都有记录且原因合理。-- 校验1是否有专业超录 SELECT p.plan_id, p.plan_count, COUNT(r.result_id) AS admitted FROM major_plan p LEFT JOIN admission_result r ON p.plan_id r.plan_id GROUP BY p.plan_id, p.plan_count HAVING admitted p.plan_count; -- 校验2是否有考生被多次录取 SELECT student_id, COUNT(*) AS cnt FROM admission_result GROUP BY student_id HAVING cnt 1;这两条 SQL 跑出来如果是空结果说明投档的基本正确性没问题。如果第一条有结果八成是FOR UPDATE没加或者事务隔离级别设错了。4. 避坑与排查投档系统最容易翻车的五个地方4.1 同分考生投档顺序错乱现象两个总分相同的考生位次靠后的反而先被投档导致本该录取的考生滑档。原因rank_no在录入时没有按同分排序规则生成或者生成时用了不稳定的排序。比如只按总分排序同分时数据库返回的顺序是不确定的。解决生成位次时必须有明确的 tie-breaker。常见规则是依次比较语文、数学、外语单科成绩。在 SQL 里用ORDER BY total_score DESC, chinese DESC, math DESC, english DESC生成行号再把行号写回rank_no。不要依赖数据库的默认返回顺序。4.2 并发投档导致超录现象模拟多线程投档时某个专业录取人数比计划数多出好几个。原因两个线程同时读到admitted_count 9、plan_count 10都判断还有名额然后都执行加一结果变成 11。解决读取计划行时必须加FOR UPDATE把行锁住。或者改用乐观锁UPDATE major_plan SET admitted_count admitted_count 1 WHERE plan_id ? AND admitted_count plan_count然后检查 affected rows 是否为 1。后者性能更好但需要处理更新失败后的重试逻辑。4.3 志愿表里有不存在的专业现象投档时关联major_plan查不到记录考生该志愿被静默跳过日志里没有任何痕迹。原因志愿填报时没有做外键校验或者院校代码、专业代码录入了错误的值。解决在volunteer表上对(college_code, major_code, batch_code)加外键引用major_plan的唯一键。如果课设不允许加外键至少在投档前跑一次校验 SQL把孤儿志愿找出来。-- 找出没有对应招生计划的志愿 SELECT v.* FROM volunteer v LEFT JOIN major_plan p ON v.college_code p.college_code AND v.major_code p.major_code AND v.batch_code p.batch_code WHERE p.plan_id IS NULL;4.4 重新模拟时旧数据没清干净现象第二次模拟投档后录取人数比第一次还多明显不对。原因admission_result和admission_log里的旧数据没删major_plan.admitted_count也没重置。解决每次模拟前先做一次重置把结果表和日志表清空把admitted_count归零。这个操作要放在同一个事务里避免重置到一半失败导致数据不一致。START TRANSACTION; DELETE FROM admission_result WHERE plan_id IN (SELECT plan_id FROM major_plan WHERE batch_code B001); DELETE FROM admission_log WHERE student_id IN (SELECT student_id FROM student WHERE batch_code B001); UPDATE major_plan SET admitted_count 0 WHERE batch_code B001; COMMIT;4.5 位次字段类型选错导致排序异常现象位次按字符串排序出现 10 排在 2 前面的情况。原因rank_no定义成了VARCHAR或者从 Excel 导入时被当成了文本。解决rank_no必须是INT或BIGINT。如果数据源是 CSV导入时显式转换类型。已经建错表的用ALTER TABLE student MODIFY rank_no INT NOT NULL改回来但改之前要确认现有数据都能转成整数。5. 进阶技巧把模拟系统做成可回放的分析工具课设做到能跑通投档其实只完成了一半。真正让这个系统有价值的是可回放——你能改一个参数重新跑一遍然后对比两次结果的差异。这在真实招生场景里叫模拟投档是正式投档前必须做的压力测试。实现回放的关键是给每次模拟打一个批次号。在admission_result和admission_log里加一个simulation_id字段每次模拟生成一个唯一 ID所有结果都挂在这个 ID 下。这样你就能同时保留多次模拟的结果随时对比。-- 对比两次模拟中某个专业的录取分数线变化 SELECT r1.simulation_id, MIN(r1.admit_score) AS min_score, MAX(r1.admit_score) AS max_score, COUNT(*) AS admit_count FROM admission_result r1 WHERE r1.plan_id 1001 GROUP BY r1.simulation_id;这个查询能直接告诉你改了计划数之后某个专业的录取分数区间怎么变。对于做课设的人来说这就是答辩时最能加分的部分——你不只是实现了一个系统还能用它做分析。另一个进阶方向是滑档考生分析。把所有admission_log里状态为REJECTED且原因是所有志愿均未投中的考生找出来看他们的志愿填报有什么共性。是志愿填得太高还是志愿数量太少这种分析用一条 SQL 就能出结果。-- 滑档考生的志愿数量分布 SELECT vol_cnt, COUNT(*) AS student_count FROM ( SELECT s.student_id, COUNT(v.volunteer_id) AS vol_cnt FROM student s JOIN admission_log l ON s.student_id l.student_id LEFT JOIN volunteer v ON s.student_id v.student_id WHERE l.reason 所有志愿均未投中 GROUP BY s.student_id ) t GROUP BY vol_cnt ORDER BY vol_cnt;如果结果显示滑档考生普遍只填了 2 到 3 个志愿那结论就很明确志愿数量不足是滑档的主要原因。这种从数据里挖结论的能力才是数据库课设真正想训练的东西。最后说一个我自己的习惯每次改完投档逻辑先拿 10 个考生的迷你数据集跑一遍人工核对每一步的admission_log确认逻辑对了再上全量数据。全量跑一次可能要几十秒但迷你数据集一秒出结果调试效率差十倍。这个习惯帮我省下了大量排查时间希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询