☰
平行志愿模拟录取系统:数据库课设中的存储过程与事务实战
2026/9/26 1:40:48 网站建设 项目流程

简介:数据库课程设计项目——平行志愿模拟录取系统的完整源码包,适用于计算机相关专业学生在课程设计、毕业设计或数据库综合实训中参考。项目基于MySQL或SQL Server设计数据库,结合Vue、JavaScript、SCSS等前端技术构建交互界面,以Java作为后端支撑,覆盖考生志愿填报、投档排序、录取结果查询等核心业务场景。资源共969个文件,压缩包大小3.87MB,除前后端源代码外,还包含SQL建库脚本、项目配置文件、说明文档以及Excel数据样例,便于快速理解表结构和业务逻辑。目前已有104人学习下载,对于需要快速上手数据库设计并完成同类课题的同学具有较高参考价值,可直接借鉴其设计思路与实现方法。

1. 平行志愿模拟录取系统:课程设计里最练数据库真功夫的题目

数据库课程设计选题时,很多人第一反应都是“学生、课程、成绩”这老三样,但平行志愿模拟录取系统不一样——它逼着你在一个系统里同时处理好规则、表关系、事务和存储过程,任何一步偷懒,录取结果都会错得离谱。这份资源是一套跑得通的平行志愿模拟录取系统,业务规则就是真实的高考“分数优先、遵循志愿”:分数高的考生先挑,每人按志愿顺序一次投档,而不是简单地按分数排个序就完事。它覆盖了数据库课程设计最核心的考点:ER设计、完整性约束、事务隔离、存储过程、并发控制。适合正在做数据库课设、准备数据库方向面试,以及想搞清楚录取规则怎么落库的从业者。你拿到的不只是代码,而是一套能讲明白逻辑的完整方案。

2. 先搞懂业务规则再建表:平行志愿的录取流程与ER设计

2.1 “分数优先、遵循志愿”是怎么变成可执行逻辑的

平行志愿的核心规则一句话:按分数从高到低排序,依次检索每个考生的志愿序列,从第一志愿开始,只要某个志愿对应的专业还“有名额”,就投档结束,后面志愿不再看;如果所有志愿都满了,就滑档。关键点在“分数优先”和“志愿顺序”两个维度上的优先级不冲突:分数高的考生永远先于分数低的考生去挑学校,哪怕低分考生把某校填成第一志愿,也无法跟高分考生的第二志愿抢名额。

把这条规则翻译成数据库逻辑,就是两步:先对所有考生按总分降序排列,形成一个检索队列;然后逐个处理队列里的考生,按他的志愿优先级从1到N遍历,判断每个志愿专业是否还能接收。这个判断不能靠“初始招生计划数”蒙混过关,因为录取是一个动态占坑的过程——前面考生投进某个专业后,该专业剩余名额要立刻减少,下一个考生看到的是更新后的状态。否则你算出来的结果会超出招生计划,这是课设里最典型的翻车点。

所以在设计表结构之前,你得先意识到:这个系统本质上是一个“带状态变化的逐行处理过程”,不只是几条SELECT能解决的。也正是这一点,决定了表要拆成哪几张,字段要留哪些冗余,以及为什么必须用存储过程而不是用程序代码去硬算。

2.2 核心表设计:从考生到志愿需要的五张表

我拆这套系统时的做法是分成五张表:考生表、学校表、专业表、志愿表、录取结果表。为什么志愿表和录取结果表分开?因为考生填报的志愿是“预期”,录取结果是“事实”。一个考生可以填多个志愿,但最终只能被一个专业录取,也可能一个都录不上。如果混在一张表里,撤销和重录时状态会很混乱。

-- 考生表 CREATE TABLE student ( sid INT PRIMARY KEY AUTO_INCREMENT COMMENT '考生号', name VARCHAR(50) NOT NULL COMMENT '姓名', score DECIMAL(5,1) NOT NULL COMMENT '高考总分', province VARCHAR(20) DEFAULT NULL COMMENT '省份' ); -- 学校表 CREATE TABLE school ( sch_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '学校ID', sch_name VARCHAR(100) NOT NULL COMMENT '学校名称', location VARCHAR(50) COMMENT '所在省份' ); -- 专业表 CREATE TABLE major ( maj_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '专业ID', maj_name VARCHAR(100) NOT NULL COMMENT '专业名称', sch_id INT NOT NULL COMMENT '所属学校', quota INT NOT NULL COMMENT '招生计划人数', FOREIGN KEY (sch_id) REFERENCES school(sch_id) ); -- 志愿表 CREATE TABLE application ( app_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '志愿ID', sid INT NOT NULL COMMENT '考生ID', maj_id INT NOT NULL COMMENT '专业ID', priority TINYINT NOT NULL COMMENT '志愿顺序,1为第一志愿', FOREIGN KEY (sid) REFERENCES student(sid), FOREIGN KEY (maj_id) REFERENCES major(maj_id) ); -- 录取结果表 CREATE TABLE admission ( ad_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '录取ID', sid INT NOT NULL COMMENT '考生ID', maj_id INT NOT NULL COMMENT '专业ID', sch_id INT NOT NULL COMMENT '学校ID', ad_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '录取时间', FOREIGN KEY (sid) REFERENCES student(sid), FOREIGN KEY (maj_id) REFERENCES major(maj_id) );

这里几个字段值得注意:score用的是DECIMAL(5,1),能存三位数和一位小数,足够容纳常规高考总分;priority是TINYINT,因为志愿数量一般不超过10个,没必要用INT;admission表里的sch_id是从专业反查出来的,虽然可以通过major表JOIN得到,但冗余存一个可以让后续对账SQL少一次JOIN。这只是课程设计层面的取舍,别在正式业务里盲目做冗余。

2.3 外键、唯一索引和级联:关系完整性怎么设才不会翻车

表建好只是第一步,关系约束设置不合理,后面会一个接一个踩坑。先说外键:application表的sid和maj_id必须外键到考生和专业,这样插入志愿时数据库会帮你检查考生和专业是否存在,而不是靠应用程序去判断。admission表的sid同样外键到考生,但建议不要级联删除考生——如果删考生时级联删掉录取记录,历史数据就全没了。课程设计里通常要求“候选人库不可追溯删除”,所以外键用ON DELETE RESTRICT更合理,强制你先处理录取结果再删考生。

再说唯一索引,有两个地方必须加。第一,application(sid, priority)要唯一,保证同一个考生不能有两个同样的志愿顺序;第二,application(sid, maj_id)要唯一,防止重复填报同一专业。如果这两个约束漏了,存储过程跑出来的录取结果会出现一个考生录两次,或者志愿顺序重复导致按优先级检索时逻辑混乱。

我还建议在admission(sid)上加唯一索引,因为一个考生只能有一条录取记录。这个约束能直接在数据库层挡住“重复录取”的bug。你可能会问:那撤销后重新录取怎么办?先删掉旧记录再插新的,旧记录可以另外开一个admission_log表去存历史,这样既保证唯一性,又保留了回溯的余地。级联删除方面,我只在major.sch_id上用了外键,删除学校时如果直接级联删专业,会导致学生的志愿变成孤儿数据。所以我的习惯是:除了school到major可以设计成ON DELETE CASCADE(删除学校时清掉它下面的专业是合理的),其余外键一律用默认限制。

3. 建库脚本与初始数据:把ER落到MySQL/SQLserver

3.1 从MySQL到SQLserver:建库脚本的差异和统一写法

现在很多课程设计允许MySQL和SQLserver二选一,但这两个数据库的方言差异不小。最明显的就是自增列:MySQL用AUTO_INCREMENT,SQLserver是IDENTITY(1,1)。字符串拼接符号也不一样。如果只写一套脚本,答辩时换数据库环境直接报错。我在拆这套资源时,把核心建表语句做成了尽量方言无关的版本,只在自增字段上区分。

-- MySQL版本 CREATE TABLE student ( sid INT PRIMARY KEY AUTO_INCREMENT, ... ); -- SQLserver版本 CREATE TABLE student ( sid INT PRIMARY KEY IDENTITY(1,1), ... );

除了自增列,DECIMAL和VARCHAR在两个数据库里都能用。外键约束语法相同。所以建议你把表结构定义做成同一个SQL文件,只把AUTO_INCREMENT改成IDENTITY。存储过程的语法差异才是大头,这个放到第4章讲。

3.2 模拟数据批量生成:用存储过程插入足够支撑算法演示的数据

没有数据,存储过程跑两分钟就结束了,看不出效果。课设演示至少要准备三四十个考生,其中要有几个高分低就、几个同分、一个滑档,才能体现“平行志愿”的规则特性。常见做法是:先手工插入几个边界案例,然后用循环随机生成一批常规数据。

-- 手工插入边界数据 INSERT INTO student(name, score) VALUES ('张高分', 680.0), ('李中分', 600.0), ('王低分', 550.0), ('赵同分1', 620.0), ('赵同分2', 620.0); INSERT INTO major(maj_name, sch_id, quota) VALUES ('计算机', 1, 2), ('数学', 1, 1), ('机械', 2, 3); INSERT INTO application(sid, maj_id, priority) VALUES (1, 1, 1), (1, 2, 2), -- 高分考生第一志愿计算机 (2, 1, 1), (2, 3, 2), -- 中分考生冲计算机,第二志愿机械 (3, 3, 1), (3, 2, 2), (4, 2, 1), (4, 1, 2), -- 同分不同志愿顺序 (5, 1, 1), (5, 2, 2);

这里赵同分1和赵同分2同分,但志愿顺序不同,就是要故意制造同分场景,后面你会发现排序不稳定到底怎么解决。随机生成考生时,可以用存储过程循环插入:

DROP PROCEDURE IF EXISTS gen_students; DELIMITER $$ CREATE PROCEDURE gen_students(IN count INT) BEGIN DECLARE i INT DEFAULT 1; WHILE i <= count DO INSERT INTO student(name, score) VALUES (CONCAT('考生_', i), 400 + RAND() * 280); SET i = i + 1; END WHILE; END$$ DELIMITER ; CALL gen_students(30);

RAND()生成0到1之间的小数,乘280后加400,得到400到680之间的分数。注意:随机生成的数据可能没有层次感,所以我还是建议手工插入的边界数据一定要保留,它们才是检验算法正确性的关键。生成志愿表时同理,可以按每个考生随机填2到3个不重复专业。

3.3 视图和索引:先别急着优化,但这三个索引必须有

课程设计不需要上复杂优化,但最基本的索引缺失会导致存储过程慢到超时。我的习惯是先建三个索引:student(score DESC)给排序用,application(sid, priority)给检索志愿序列用,major(quota)给更新配额用。视图方面,做一个“专业已录人数”的统计视图很有用,调试时一眼看清每个专业还剩下多少名额。

CREATE INDEX idx_score ON student(score DESC); CREATE INDEX idx_app_sid_pri ON application(sid, priority); CREATE INDEX idx_maj_quota ON major(quota); CREATE OR REPLACE VIEW v_major_remaining AS SELECT m.maj_id, m.maj_name, m.quota, m.quota - COUNT(a.ad_id) AS remaining FROM major m LEFT JOIN admission a ON m.maj_id = a.maj_id GROUP BY m.maj_id, m.maj_name, m.quota;

这个视图的remaining字段是负的,说明超录了。别小看这个负数,很多时候算法跑完,你直接查这个视图,就能第一眼看出超录在哪。索引别加太多,这是OLTP模拟环境,不是分析平台,索引多了反而让插入和更新变慢。

4. 录取核心算法实现:逐分检索的存储过程

4.1 为什么不能直接ORDER BY然后挨个分配:同分和状态更新的坑

有人以为录取就是按照score DESC排序,然后遍历考生,把第一个未满的专业塞给他。这思路没错,但直接用一条SELECT做不了,因为每录一个人,专业名额就变了,后面的人能不能投档取决于前面的更新结果。这天然是个过程化逻辑,必须写存储过程或脚本循环。另一个坑是同分排序——如果ORDER BY score DESC碰到同分,系统返回顺序就可能不稳定。比如刚才手工插入的赵同分1和赵同分2,同一分数谁先处理?规则上是按单科成绩或随机排序,但数据库不会自动给你一个稳定顺序,你需要自己指定二级排序字段。

我的建议是:存储过程不做二次排序,而是在排序时加上一个确定性规则。常见做法是加“语文数学总分”或直接按考生ID,但为了演示原理,用sid ASC作为同分排序,保证每次跑结果一致。

4.2 存储过程主逻辑:逐志愿尝试投档的完整实现

下面这段是MySQL的存储过程,核心思想是:先把考生按分数降序插入临时表,然后逐行处理;对每个考生,按志愿优先级从小到大查志愿,检查对应专业是否还有名额,有名额就插入录取表并减少剩余名额。

DELIMITER $$ DROP PROCEDURE IF EXISTS sp_simulate_admission $$ CREATE PROCEDURE sp_simulate_admission() BEGIN DECLARE done INT DEFAULT 0; DECLARE v_sid INT; DECLARE v_score DECIMAL(5,1); DECLARE v_maj_id INT; DECLARE v_priority TINYINT; DECLARE v_remaining INT; -- 按分数降序、同分按sid升序生成检索队列 DECLARE cur_student CURSOR FOR SELECT sid, score FROM student ORDER BY score DESC, sid ASC; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; -- 清空上次录取结果,保证可重复运行 DELETE FROM admission; OPEN cur_student; student_loop: LOOP FETCH cur_student INTO v_sid, v_score; IF done = 1 THEN LEAVE student_loop; END IF; -- 遍历当前考生的所有志愿,按priority升序 SET v_priority = 1; search_major: WHILE v_priority <= 10 DO -- 查出该优先级对应的专业ID SELECT maj_id INTO v_maj_id FROM application WHERE sid = v_sid AND priority = v_priority LIMIT 1; -- 没有这个志愿,说明志愿序列结束,直接滑档 IF v_maj_id IS NULL THEN LEAVE search_major; END IF; -- 查询该专业剩余名额 SELECT quota - ( SELECT COUNT(*) FROM admission WHERE maj_id = v_maj_id ) INTO v_remaining FROM major WHERE maj_id = v_maj_id; IF v_remaining > 0 THEN -- 投档成功 INSERT INTO admission(sid, maj_id, sch_id) SELECT v_sid, v_maj_id, sch_id FROM major WHERE maj_id = v_maj_id; -- 找到学校ID并写入 UPDATE admission a JOIN major m ON a.maj_id = m.maj_id SET a.sch_id = m.sch_id WHERE a.sid = v_sid AND a.maj_id = v_maj_id; LEAVE search_major; ELSE -- 该专业已满,继续看下一个志愿 SET v_priority = v_priority + 1; SET v_maj_id = NULL; END IF; END WHILE; -- 重置游标状态,注意不要重置done END LOOP; CLOSE cur_student; END$$ DELIMITER ;

这段存储过程有两个设计细节容易被人忽略。第一,每次处理一个考生前都要删除全部admission记录,保证重复执行时结果可对比。第二,v_priority初始是1,当专业满时v_priority+1,如果该考生所有志愿都没有可投的名额,最后会因为查不到志愿而LEAVE。如果你在SQLserver上写,逻辑一样,但游标语法有差异,DECLARE CURSOR和FETCH写法略有不同,变量赋值也要用SET @v_sid = ...。课程设计里我建议MySQL跑通一个版本,然后说明另一个版本的差异即可,没必要两个都调试到完美。

4.3 事务和并发控制:模拟录取时如何避免超录

上面的存储过程直接跑是单用户模式,肯定能跑通,但面试官可能会问你:如果多个录取进程并发执行,会不会超录?答案会。因为检查剩余名额和插入录取记录之间不是原子操作。两个进程同时查到remaining=1,然后各自插入一条,专业名额就变成负数了。解决思路有几种,最简单的是在处理每个考生时对整个表加锁,但演示场景里不现实。另一种是给major表加上remaining字段,在更新时使用UPDATE major SET remaining = remaining - 1 WHERE maj_id = ... AND remaining > 0,利用原子更新来防止超录。这个技巧可以在答辩时展示一下,会是很加分的点。

不过课程设计本身不需要真的做高并发,只要你在文档里说明白这个风险和解决方案就行。我当时把整个录取过程包在一个START TRANSACTION ... COMMIT里,保证要么全部成功要么全部回滚,这样撤销和重跑都干净。

5. 避坑与常见问题:课设答辩和自测最容易翻车的五个点

5.1 现象:录取结果超出招生计划

某专业计划5人,录取结果里出现了6条记录。原因几乎都是“用初始quota做判断”,而没有扣除已经占用的名额。我见过很多版本的代码,在判断quota > 0时直接用了major.quota,而这个字段从头到尾没变过。解决:要么在每次投档成功时执行UPDATE major SET quota = quota - 1,但要用剩余名额,不要用计划人数;要么像第4章那样实时计算quota - COUNT(admission)。无论哪种,都要记住:major.quota是静态计划值,永远不要用它直接做动态剩余判断。

5.2 现象:同分考生排序混乱,录取结果每次跑都不一样

同分时ORDER BY score DESC不稳定,尤其是随机生成的分数,很多人同时同分。原因就是没有二级排序字段。解决:在游标查询里加ORDER BY score DESC, sid ASC,如果有偏科规则,可以加“语文+数学总分”这种业务字段。我习惯用sid ASC,简单而且确定。答辩时一定要主动说这个点,因为老师就爱问同分怎么处理。

5.3 现象:MySQL跑得好好的,换到SQLserver就报错

常见原因有三个:自增列写法不同,字符串拼接用CONCAT在SQLserver里其实也支持但部分版本只能+,还有存储过程里的LIMIT 1在SQLserver里要改成TOP 1。我当初就是没注意LIMIT,导致导师的SQLserver环境里存储过程直接编译失败。解决:写资源包时单独提供SQLserver版本的存储过程文件,主文件用标准SQL,并在注释里标注方言差异。如果你只有MySQL环境,建议至少在文档里写清楚差异,答辩时能说出来就加分。

5.4 现象:存储过程执行特别慢或者直接卡死

慢主要因为游标循环里逐行查COUNT(admission),导致大量子查询。数据量300个考生、50个专业,理论上不至于卡死,但如果没索引就会全表扫描。卡死的常见原因是游标的NOT FOUND处理有问题:没有设置CONTINUE HANDLER FOR NOT FOUND,游标到末尾就抛异常退出,但循环条件又没及时更新done标志,变成死循环。解决:在循环体里每处理完一个考生就SET done = 0?不,应该检查done在LEAVE之后是否被重置,正确做法是游标Fetch不到时立刻LEAVE,然后CLOSE。另外,如果发现用了WHILE但内部没有更新v_priority,也会死循环。调试时可以在每个分支加SELECT打印临时值,但性能低,不推荐留到正式版。

5.5 现象:撤销录取后专业名额没有恢复

如果实现了“撤销某个考生的录取”功能,很可能只在admission表删除记录,而major表里维护的剩余数字没有回加。我见到的版本里,因为用了quota字段当剩余量,撤销时忘了把quota加回来,导致后续考生误以为专业有空余名额。解决:删除录取记录和更新名额必须放在同一个事务里。而且如果发录取结果不依赖major.quota,建议就别在major表里维护剩余量,老老实实每次算COUNT(admission),虽然慢一点,但不容易出现不一致。这个坑自己主动跳进去一次就记住了。

6. 数据可视化与验证:用SQL核对录取结果的正确性

6.1 用对账SQL验证录取逻辑的四个检查点

无论存储过程写得多漂亮,最后都要拿数据说话。我习惯把对账SQL直接写进说明文档,作为“验证方法”一节。检查点有四个:每个专业录取人数不超过计划数;每个考生最多一条录取记录;被录取考生的录取专业确实存在于他的志愿表里;同分考生按既定规则排序。

-- 检查1:统计专业录取人数是否超计划 SELECT m.maj_id, m.quota, COUNT(a.ad_id) AS admitted FROM major m LEFT JOIN admission a ON m.maj_id = a.maj_id GROUP BY m.maj_id, m.quota HAVING admitted > m.quota; -- 检查2:同一考生是否出现多条录取记录 SELECT sid, COUNT(*) FROM admission GROUP BY sid HAVING COUNT(*) > 1; -- 检查3:录取专业是否属于该考生的志愿表 SELECT a.sid, a.maj_id, ap.maj_id AS real_maj FROM admission a LEFT JOIN application ap ON a.sid = ap.sid AND a.maj_id = ap.maj_id WHERE ap.maj_id IS NULL;

这三个查询的结果集如果都是空,说明核心规则基本没问题。第四个检查同分顺序,需要对照输入数据人工核验。把对账SQL存成一个check.sql文件,答辩演示时先跑存储过程,再跑一遍对账,这是最直观的“我有验证流程”的证明。

6.2 把录取结果导出成Excel,边看边讲

数据库课设很少要求做前端界面,但为了答辩效果,我一般会把最终结果导出成Excel或CSV。MySQL里可以用SELECT ... INTO OUTFILE,但权限限制比较多;更省事的是用Navicat等工具把admission表导出成Excel,再拿学校、专业名称做一次VLOOKUP。如果你会点Python,也可以用pandas直接连库导出。注意导出前先把录取结果表按学校和专业排序,让老师一眼看到每个专业的录取名单。

SELECT s.name AS 考生, s.score AS 分数, sc.sch_name AS 学校, m.maj_name AS 专业 FROM admission a JOIN student s ON a.sid = s.sid JOIN major m ON a.maj_id = m.maj_id JOIN school sc ON a.sch_id = sc.sch_id ORDER BY sc.sch_id, m.maj_id, s.score DESC;

这个排序结果导出成Excel后,可以做成一个简单的柱状图,展示各专业录取分数线。别小看这步,答辩时能有可视化截图,比纯代码表更有说服力。

我从那次课设之后,每次提交这类系统前都会强制自己走一遍“删除结果 → 跑存储过程 → 跑对账SQL → 导出核对”的全流程,尤其是对账SQL里那三条必须空出来。哪怕代码再熟,只要有一次没跑,改天就可能带着超录或重复录取去答辩。希望这套思路能帮你在数据库课程设计里少踩几个坑,把平行志愿这个题目做出真正的亮点。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询