学生选课系统数据库课设:从E-R模型到SQL Server可运行脚本

发布时间:2026/10/27 19:26:09
学生选课系统数据库课设:从E-R模型到SQL Server可运行脚本

简介:面向高校计算机及相关专业学生与数据库课程设计指导教师的课程设计报告范文,围绕学生选课管理信息系统给出从需求分析到应用实现的完整撰写框架。压缩包内为1个docx文档,约3.96MB,正文按章节组织,包含系统需求概述、组织结构与功能模块划分、业务流与数据流分析、数据字典,以及概念结构中的实体属性联系与CDM图、逻辑结构PDM图等内容。物理实现部分涉及SQL Server表设计、完整性与约束代码,以及视图、触发器、存储过程和索引的创建,并配有功能调试与登录、成绩管理等应用模块示例。已有852人学习,适合需要参考报告结构、图表组织与数据库设计表述的读者用于课程设计撰写和实验复盘。

1. 学生选课管理信息系统:一份数据库课设里真正能跑的部分是什么

带过几届课程设计之后我发现一个规律:同样是《数据库系统概论》的课设,有人交上去的是几十页 Word 目录,有人交上去的是一份能直接连 SQL Server 跑通的库。这份《学生选课管理信息系统》属于后者——需求分析、数据字典、CDM 与 PDM 图一路写下来,后面还跟了建表 DDL、视图、触发器、存储过程、索引,以及学生信息管理、课程信息管理的调试记录。不管你是想找一份结构完整的课设模板当素材改,还是想照着把教务管理员、开课教师、选课学生这三类角色的选课业务真正建起来,这份报告都能当骨架用。它最值钱的地方不在排版,而在第四、五章那些能直接粘进查询分析器执行的脚本。

2. 需求与数据字典:三类角色怎么落成五张表和一份字段清单

很多人拿到课设题目就直接画 E-R 图,画完发现表建不出来,回头再改需求,来回折腾。这份报告的顺序是对的:先把角色和业务流程讲清楚,再定字段,最后才画图。照着这个顺序走,后面几乎不用返工。

2.1 三类角色与五个功能模块的划分

报告里把系统使用者分成教务管理员、开课教师、选课学生,三者的权限是层层收窄的。管理员权限最大,负责初始化学生和教师账号、设置选课时段和选课规则、数据备份还原;教师负责申请开课、维护自己课程的基本信息、录入成绩;学生只能浏览课程、选课退课、查自己的成绩。这个划分直接决定了表结构里必须有独立的manage、teacher、student三张账号表,而不是一张user表加角色字段。

对应的功能模块被拆成课程管理、学生管理、教师管理、查询管理、选课管理五块,每一块背后都挂着具体的表操作。这里有个容易被忽略的点:报告提到「选课时段内学生可登录,选课结束后学生只能查看不能修改」。这条规则本身不是表结构能表达的,它需要一个配置项。常见的做法是加一张sys_config表,存enroll_start、enroll_end这类键值对,应用层每次选课动作前先查一次。我在课程设计里吃过亏——把时段写死在代码的if里,答辩时老师问「换学期怎么办」,当场卡住。

业务流从管理员初始化数据开始,到教师申请开课,再到学生选课、教师录成绩、管理员归档,形成闭环。数据流图上能看到三条主线:学生线(查询个人信息、修改资料、选课)、教师线(查询个人信息、开设课程、录入成绩)、管理员线(基本管理、审批课程),三条线最终都汇到同一个选课数据库。画数据流图的时候建议按这个分层来,第 0 层一张总图,第 1 层按角色拆三张,第 2 层再拆到具体加工,报告里就是这么组织的。

2.2 数据字典逐字段过一遍:类型、长度、可空性

数据字典是整份报告里最该较真的部分。原报告的字段定义基本可用,但有几处按现在的标准看是埋雷的,我把它整理成一张对照表,你改课设的时候可以直接照着调。

数据项报告中的类型建议类型调整理由
sno 学号char(10)char(10)学号定长且无业务含义,定长合适
spassword 密码char(6)varchar(64)6 位定长,用户设 8 位密码直接报截断错误
sname 姓名char(10)nvarchar(20)中文必须带 n 前缀,复姓和少数民族姓名容易超长
age 年龄intsmallint + CHECK年龄是派生数据,更该存出生日期,课设里保留但要卡值域
ccredit 学分inttinyint1 到 10 的整数,tinyint 够用还省一个字节
grade 成绩intsmallint + CHECK(0~100)允许 NULL 表示未录入,但录入后必须落在合法区间
cpno 先修课号放在选课表里移到课程表先修课只依赖课程号,放选课表违反第二范式

表里最后一行是设计层面的问题,后面第 3 章会展开。前面几行是纯工程问题:char是定长类型,插入短字符串会自动补空格,插入超长字符串在ANSI_WARNINGS打开时直接抛「字符串或二进制数据将被截断」。课设演示时密码一般就 6 位看不出来,换成真实学号加生日做密码,8 位起步,第一个报错就来了。中文姓名用char而不带n前缀,在非中文排序规则的库里会变成问号,这个坑我在两台不同配置的机器上各踩过一次。

2.3 业务流与数据流:从选课单到成绩归档的四个阶段

把业务流程按时间轴摊开,其实是四个阶段,每个阶段对应不同的写操作。第一阶段管理员初始化,批量插入学生和教师记录,这批数据来自教务系统导出,通常用BULK INSERT或者一段循环INSERT,要注意的是主键冲突处理;第二阶段教师申请开课,往course表插记录,同时往teachingplan联系表插授课关系;第三阶段学生选课,高频写sc表,是整个系统并发压力最大的地方;第四阶段教师录成绩,更新sc.grade,然后管理员归档、出报表。

四个阶段的读写特征完全不同,这决定了后面索引该怎么建。选课阶段是典型的「按学生查已选课程」加「按课程查已选人数」,成绩归档阶段是「按学期、按课程聚合统计」。如果只在主键上建聚集索引,第二阶段和第四阶段的查询会全部退化成全表扫描。我在一个两千人规模的模拟数据上试过,sc表十万行,不加索引统计一门课的平均分要两秒多,加了覆盖索引之后降到几十毫秒。这个量级在课设里看不出来,但答辩时把执行计划调出来给老师看,说服力完全不一样。

3. 概念到逻辑:CDM 里的 n:m 关系怎么变成 PDM 里的主外键

概念模型和逻辑模型这两章,很多课设写得最虚,画个图就过去了。实际上从 E-R 图到建表脚本之间那一步转换规则,才是数据库设计课真正要考的东西。这份报告把转换规则明确写了出来:一对一、一对多、多对多分别怎么处理,标注主键外键,这段是能直接抄进自己课设的。

3.1 实体、属性、联系:三个实体两张联系表

报告里识别出三个核心实体:学生、课程、教师,外加选课记录和管理员两个辅助实体。属性部分列得很细,学生有学号、密码、姓名、系名、性别、年龄、邮箱、电话八项,课程有课程号、课程名、开课学期、授课教师号、学分五项,教师有工号、密码、姓名、系名、电话、职称、参考书七项。这里注意「参考书」这个属性,它是教师实体上的多值属性,严格来说该单独拆一张表,课设里用一个book char(20)字段凑合了,答辩时可能被问到。

联系有两个关键的:学生和课程之间是选课关系,一个学生选多门课,一门课被多个学生选,n:m;教师和课程之间是授课关系,一个教师教多门课,一门课也可能由多位教师合上,也是 n:m。还有一个自反的一对多:课程和它的先修课,一门课最多有一个直接先修课(报告里的设定),但一门课可以是多门课的先修课。

n:m 联系在逻辑模型里必须拆成独立的联系表,这就是sc(选课表)和teachingplan(授课计划表)的由来。一对多的自反联系不用拆表,直接在course表里加一个指向自身主键的cpno字段,配一个自引用外键。这个区别是 PDM 图能不能画对的关键——很多同学把先修课也拆成一张表,画出来反而错了。

3.2 多对多拆解与主外键落地

把联系表拆出来之后,主外键的分配有明确规则:联系表的主键由两端实体的主键组合而成,同时这两个字段各自是外键。选课表主键(sno, cno),授课计划表主键(cno, tno)。注意授课计划表的主键组合顺序,把cno放前面是因为最常用的查询是「这门课有哪些老师上」,聚集索引按这个顺序组织,范围扫描更顺。

如果联系本身带属性,属性就落在联系表上。选课联系的属性是成绩,所以grade在sc表里,这个没问题。但报告的数据字典把「先修课号」也放进了选课记录,这就出事了——先修课号只跟课程有关,跟哪个学生选的没关系,放在(sno, cno)做主键的表里,构成对主键的部分函数依赖,直接违反第二范式。

3.3 范式检查:SC 表里那个 cpno 该不该留

把cpno留在sc表会带来什么后果,具体想一下就明白。假设「数据库原理」的先修课是「数据结构」,这门课有一百个学生选,那sc表里就有一百行记录的cpno都写着「数据结构」的课程号。哪天教务调整先修课,要把先修课改成「程序设计基础」,就得同时更新这一百行,漏掉一行就出现同一门课有两个先修课号的脏数据。这就是典型的更新异常。

正确的做法是把cpno移到course表,作为指向cno的自引用外键。一门课的先修课只存一份,改一次全库生效。这个改动在 PDM 图上就是把那条自反的连线从「课程—选课」挪到「课程—课程」,图会变得干净很多。

顺带提一句第三范式。student表里同时存了sdept(系名),如果再加一个「系主任」字段,就构成传递依赖,得拆系表。报告里没加,所以三张主表都在 3NF,这是它做得比较克制的地方。课设里最容易画蛇添足的就是给每张表都加一堆冗余字段「方便查询」,结果范式检查一塌糊涂。

4. 物理实现:从建表 DDL 到触发器、存储过程、索引的完整脚本

这一章是整份报告最硬的部分,也是你真正能抄走直接跑的内容。下面按建表、视图、触发器、存储过程加索引的顺序走一遍,每段脚本后面说清楚它在解决什么问题,参数怎么调。

4.1 五张核心表与完整性约束的建表脚本

建表顺序很重要,被引用的表要先建。course引用了teacher,sc引用了student和course,所以顺序是 teacher → student → course → sc → teachingplan。下面这段是可以整段执行的,我在 SQL Server 2019 上验证过。

-- 教师表:课程表要引用它,必须最先建 CREATE TABLE teacher ( tno CHAR(5) NOT NULL PRIMARY KEY, tpassword CHAR(6) NOT NULL, tname CHAR(10) NOT NULL, tdept CHAR(20) NULL, duty CHAR(10) NULL, telephone CHAR(12) NULL, book CHAR(20) NULL ); GO CREATE TABLE student ( sno CHAR(10) NOT NULL PRIMARY KEY, spassword CHAR(6) NOT NULL, sname CHAR(10) NOT NULL, sdept CHAR(20) NULL, ssex CHAR(2) NOT NULL, age SMALLINT NULL, telephone CHAR(12) NULL, email VARCHAR(30) NULL, CONSTRAINT ck_stu_sex CHECK (ssex IN (N'男', N'女')), CONSTRAINT ck_stu_age CHECK (age BETWEEN 15 AND 60) ); GO CREATE TABLE course ( cno CHAR(7) NOT NULL PRIMARY KEY, cname CHAR(20) NOT NULL, term INT NOT NULL, -- 开课学期,形如 202401 tno CHAR(5) NULL, ccredit TINYINT NOT NULL, cpno CHAR(7) NULL, -- 先修课,指向本表 cno capacity INT NOT NULL CONSTRAINT df_course_cap DEFAULT 60, CONSTRAINT ck_course_credit CHECK (ccredit BETWEEN 1 AND 10), CONSTRAINT fk_course_teacher FOREIGN KEY (tno) REFERENCES teacher(tno), CONSTRAINT fk_course_cpno FOREIGN KEY (cpno) REFERENCES course(cno) ); GO CREATE TABLE sc ( sno CHAR(10) NOT NULL, cno CHAR(7) NOT NULL, grade SMALLINT NULL, -- NULL 表示未录入 CONSTRAINT pk_sc PRIMARY KEY (sno, cno), CONSTRAINT fk_sc_student FOREIGN KEY (sno) REFERENCES student(sno), CONSTRAINT fk_sc_course FOREIGN KEY (cno) REFERENCES course(cno), CONSTRAINT ck_sc_grade CHECK (grade IS NULL OR grade BETWEEN 0 AND 100) ); GO CREATE TABLE teachingplan ( cno CHAR(7) NOT NULL, tno CHAR(5) NOT NULL, CONSTRAINT pk_teachingplan PRIMARY KEY (cno, tno), CONSTRAINT fk_tp_course FOREIGN KEY (cno) REFERENCES course(cno), CONSTRAINT fk_tp_teacher FOREIGN KEY (tno) REFERENCES teacher(tno) ); GO

先说capacity这个字段,报告原文里没有,是我加的。加它的理由很实际:选课系统最核心的业务规则就是名额,没有容量字段,选课就变成了无限制入库,演示时完全体现不出业务逻辑。默认值 60 按一般公选课设定,实际按课程调。

再说约束。ck_stu_sex用CHECK把性别卡死成两个值,看着很死板,但课设演示时如果需要录入「其他」选项就得改约束,我做的时候一般改成允许三个值。ck_sc_grade里grade IS NULL OR ...这个写法必须带上,否则未录入成绩的行会被约束判为违反,插入直接失败——这是CHECK约束里最常见的翻车点,因为NULL参与比较的结果是UNKNOWN,不是TRUE。

course.cpno的自引用外键有个副作用:插入第一门没有先修课的课程时,cpno必须是NULL才行;如果误填了空字符串'',外键会去找主键为空的课程,直接报冲突。这也是我建议把cpno设成可空而不是给默认值的原因。

4.2 视图:把三表连接压成一条查询

报告里建了学生基本信息视图、学生参考书视图、教师基本信息视图三个。前两个是单表视图,作用主要是列裁剪和权限隔离,比如学生登录后只允许查v_student_info,不给他看密码字段。真正有价值的是跨表视图,我在它的基础上补了两个。

-- 学生课表视图:一次查出学号、姓名、课程、教师、学分、成绩 CREATE VIEW v_student_schedule AS SELECT sc.sno, s.sname, c.cno, c.cname, c.term, t.tname, c.ccredit, sc.grade FROM sc JOIN student s ON s.sno = sc.sno JOIN course c ON c.cno = sc.cno LEFT JOIN teacher t ON t.tno = c.tno; GO -- 教师工作量视图:按课程统计选课人数和平均分 CREATE VIEW v_teacher_load AS SELECT t.tno, t.tname, c.cno, c.cname, c.term, COUNT(sc.sno) AS student_cnt, AVG(CAST(sc.grade AS DECIMAL(5,2))) AS avg_grade FROM teacher t JOIN course c ON c.tno = t.tno LEFT JOIN sc ON sc.cno = c.cno GROUP BY t.tno, t.tname, c.cno, c.cname, c.term; GO

第一个视图里teacher用的是LEFT JOIN,因为课程可能还没分配教师(tno为空),用内连接会把这类课程直接过滤掉,学生就查不到自己选的课了。这个细节在联合查询里特别容易漏,而且漏了不会报错,只会少数据,排查起来很费劲。

第二个视图里AVG外面套了一层CAST,是为了避免整数平均值被截断。sc.grade是SMALLINT,SQL Server 里整数相除结果还是整数,不转类型的话平均分会变成 78 而不是 78.5。

要注意的是,多表连接的视图在 SQL Server 里默认不可更新。想改学生姓名,得直接UPDATE student,对着v_student_schedule执行更新会报「视图或函数不可更新,因为修改会影响多个基表」。这个我在第 5 章会再展开。

4.3 触发器:选课门数上限与成绩范围校验

报告里提到创建触发器,但没写具体业务。按选课系统的实际需求,最该用触发器的场景是两个:一是限制同一学期选课门数,二是记录成绩变更日志。

-- 限制同一学生同一学期最多选 6 门课 CREATE TRIGGER tr_sc_term_limit ON sc AFTER INSERT AS BEGIN SET NOCOUNT ON; -- 触发器是语句级触发的,inserted 里可能有多行,必须集合化判断 IF EXISTS ( SELECT 1 FROM inserted i JOIN course ci ON ci.cno = i.cno JOIN sc s ON s.sno = i.sno -- 含本次新插入的行 JOIN course cs ON cs.cno = s.cno WHERE cs.term = ci.term GROUP BY i.sno, ci.term HAVING COUNT(*) > 6 ) BEGIN ROLLBACK TRANSACTION; THROW 50001, N'同一学期选课不得超过 6 门', 1; RETURN; -- 回滚后必须返回,否则后续语句还会执行 END END; GO

这段的关键在两个地方。inserted是伪表,一次批量插入 500 行时它就有 500 行,如果按游标逐行判断,性能差还容易只拦住第一条。用EXISTS加GROUP BY ... HAVING是集合化写法,一次就能把所有超限的学生找出来。另一个是ROLLBACK之后必须RETURN,否则事务已经回滚了,控制流还往下走,后面的语句会报「当前事务无法提交,且无法支持写入日志文件的操作」,事务计数不匹配的 3609 错误就是这么来的。

第二个触发器做成绩审计,需要先建一张日志表。

CREATE TABLE grade_log ( id BIGINT IDENTITY(1,1) PRIMARY KEY, sno CHAR(10) NOT NULL, cno CHAR(7) NOT NULL, old_grade SMALLINT NULL, new_grade SMALLINT NULL, changed_at DATETIME2 NOT NULL DEFAULT SYSDATETIME() ); GO CREATE TRIGGER tr_sc_grade_audit ON sc AFTER UPDATE AS BEGIN SET NOCOUNT ON; -- 用 ISNULL 兜底,否则 NULL 参与 <> 比较结果是 UNKNOWN,记录会被漏掉 INSERT INTO grade_log(sno, cno, old_grade, new_grade) SELECT i.sno, i.cno, d.grade, i.grade FROM inserted i JOIN deleted d ON d.sno = i.sno AND d.cno = i.cno WHERE ISNULL(i.grade, -1) <> ISNULL(d.grade, -1); END; GO

deleted伪表在UPDATE触发器里存的是更新前的旧值,这是做审计的标准套路。WHERE条件里用ISNULL包一层是血泪经验——成绩从NULL改成 90,如果不包,90 <> NULL的结果是UNKNOWN,这条变更就进不了日志表,审计等于白做。包成ISNULL(grade, -1)之后,90 <> -1成立,记录正常落库。用-1而不是0做哨兵值,因为 0 是合法成绩。

4.4 存储过程与索引:选课、录成绩和查询加速

选课这个动作在应用层要做三步:查学号是否存在、查是否重复选课、插入记录。三步分开写,中间任何一步在网络传输上断了,就会出现半完成状态。封进存储过程,让它在服务端一次跑完。

CREATE PROCEDURE usp_enroll @sno CHAR(10), @cno CHAR(7) AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; -- 出错自动回滚整个事务 IF NOT EXISTS (SELECT 1 FROM student WHERE sno = @sno) THROW 50010, N'学号不存在', 1; IF NOT EXISTS (SELECT 1 FROM course WHERE cno = @cno) THROW 50011, N'课程不存在', 1; IF EXISTS (SELECT 1 FROM sc WHERE sno = @sno AND cno = @cno) THROW 50012, N'该课程已选,请勿重复提交', 1; BEGIN TRY BEGIN TRANSACTION; INSERT INTO sc(sno, cno, grade) VALUES (@sno, @cno, NULL); COMMIT TRANSACTION; END TRY BEGIN CATCH IF XACT_STATE() <> 0 ROLLBACK TRANSACTION; THROW; -- 重新抛出原始错误,保留错误号和消息 END CATCH END; GO

XACT_ABORT ON这个开关建议默认打开,它让运行时错误自动回滚事务,不用每处都手写ROLLBACK。XACT_STATE()判断是为了区分「事务还活着」和「事务已经不可提交」两种情况,直接ROLLBACK在不可提交状态下会报错。

索引这块,报告里提到了创建索引的过程但没给具体列。按第 2 章分析的四个业务阶段,我一般建这三个:

-- 按课程查选课名单和人数:聚集索引是 (sno,cno),按 cno 查走不到,必须单独建 CREATE NONCLUSTERED INDEX ix_sc_cno ON sc(cno) INCLUDE (sno, grade); -- 按学期筛课程:选课首页就是列本学期可选课程 CREATE NONCLUSTERED INDEX ix_course_term ON course(term) INCLUDE (cno, cname, tno, capacity); -- 按教师查所授课程 CREATE NONCLUSTERED INDEX ix_course_tno ON course(tno) INCLUDE (cname, term, ccredit); GO

INCLUDE里放的列不会成为索引键,但会随索引一起存储。上面第一个索引,查询「这门课有哪些学生选、成绩多少」时只扫这个索引就够了,不用回表,这就是覆盖索引的价值。ix_course_term同理,选课页面列课程列表需要的字段全在INCLUDE里。

不要给ssex(性别)、ccredit(学分)这种只有两三个取值的列建索引。选择度太低,优化器根本不会用,白白增加写入开销。我在一个课设项目里见过给性别建索引的,sys.dm_db_index_usage_stats里user_seeks一直是 0。

应用层的登录模块,报告里只有截图没有代码。按现在的标准,账号密码验证必须走参数化查询,拼接字符串是自找麻烦。

// 登录验证:账号密码一律走参数,禁止字符串拼接 public string Login(string account, string password, string role) { // 按角色选不同的表和字段,避免三表 UNION 带来的歧义 string sql = role switch { "student" => "SELECT sname FROM student WHERE sno = @acc AND spassword = @pwd", "teacher" => "SELECT tname FROM teacher WHERE tno = @acc AND tpassword = @pwd", "admin" => "SELECT gname FROM manage WHERE gno = @acc AND gpassword = @pwd", _ => throw new ArgumentException("未知角色") }; using var conn = new SqlConnection(_connStr); using var cmd = new SqlCommand(sql, conn); cmd.Parameters.Add("@acc", SqlDbType.Char, 10).Value = account.Trim(); cmd.Parameters.Add("@pwd", SqlDbType.VarChar, 64).Value = password; conn.Open(); return cmd.ExecuteScalar() as string; // 返回 null 表示验证失败 }

SqlDbType.Char, 10里的 10 要跟表定义一致,长度不匹配时 SQL Server 会做隐式转换,索引可能失效。account.Trim()是防用户复制粘贴时带空格。这段代码配上前面把spassword改成varchar(64)的调整,才算闭环。

5. 联调排查:选课系统上线前最容易翻车的五个地方

课设做完能不能跑起来,跟设计好不好是两回事。下面这五条是我在类似的选课系统上真实遇到过的,每条按现象、原因、解决写,你照着对照一遍能省不少时间。

5.1 表结构与约束上的三个坑

坑一:字符串截断,插入直接报错。现象是执行插入语句时提示「将截断字符串或二进制数据」,事务回滚。原因是数据字典里把spassword定成char(6)、sname定成char(10),测试数据里只要有一个 8 位密码或者四个字以上的名字就炸。解决方法是把密码字段改成varchar(64),姓名字段改成nvarchar(20),中文列一律加N前缀。改完之后要检查已有的INSERT语句,字符串字面量前面补上N,否则存进去还是乱码。

坑二:先修课号放错表,出现同课多值。现象是同一个课程号在选课表里对应了两个不同的先修课号,查先修关系时结果不唯一。原因是cpno放在了sc表,只依赖cno不依赖sno,违反第二范式。解决办法是把cpno移到course表做自引用外键,然后写一条清洗语句,把sc表里的cpno按课程号回填到course,再删掉冗余列:

-- 回填:每门课取一个先修课号即可 UPDATE c SET c.cpno = x.cpno FROM course c JOIN (SELECT cno, MIN(cpno) AS cpno FROM sc WHERE cpno IS NOT NULL GROUP BY cno) x ON x.cno = c.cno; ALTER TABLE sc DROP COLUMN cpno;

坑三:平均分虚高,统计口径对不上。现象是教师工作量视图里平均分 85,实际按名单手算只有 72。原因是AVG(grade)会忽略NULL,分母是已录入人数而不是选课人数,没录成绩的学生不参与计算。解决办法是把口径写明确,要么用SUM(grade) / COUNT(*)算「按选课人数摊」,要么保留AVG但在视图里同时输出COUNT(grade)和COUNT(*)两个计数,让看报表的人自己判断。我倾向于后者,因为两种口径都有业务意义。

5.2 对象与联调阶段的两个坑

坑四:触发器拦不住批量插入,或者报事务计数错误。现象有两种,一种是批量导入选课数据时,超限的学生只被拦下一部分;另一种是报「EXECUTE 后的事务计数指示 BEGIN 和 COMMIT 语句数目不匹配,上一计数 = 1,当前计数 = 0」。前者的原因是触发器里用了游标逐行判断,或者只判断了inserted的第一行;后者的原因是ROLLBACK之后没有RETURN,控制流继续往下执行到了COMMIT。解决办法是把判断写成集合化的EXISTS + GROUP BY ... HAVING,ROLLBACK后面紧跟RETURN,或者干脆用THROW抛出异常替代回滚。改完记得测一次批量插入 100 行的场景。

坑五:对着视图执行更新,报「不可更新」。现象是学生修改个人信息时报错「视图或函数 'v_student_schedule' 不可更新,因为修改会影响多个基表」。原因是多表连接的视图只有在满足特定条件(比如只更新其中一张基表的列且不涉及计算列)时才可更新,我那个视图带LEFT JOIN和聚合,完全不可更新。解决办法是权限上就分开:视图只读,所有写操作走基表,应用层的更新方法直接拼UPDATE student SET ...;如果确实需要让视图可写,就给视图挂一个INSTEAD OF UPDATE触发器,把更新动作拆到各基表去。

6. 二次开发:把课设报告改成一个能演示、能答辩的选课系统

报告给的是骨架,想拿去答辩或者当毕业设计的底稿,还得往里填肉。下面三个改造点是我觉得性价比最高的,按工作量从小到大排。

改造点涉及对象大致工作量演示效果
选课容量与满员提示course.capacity、usp_enroll半天名额满了能拦住,并给出提示
成绩变更审计grade_log、tr_sc_grade_audit两小时改成绩留痕,可查历史
选课时段开关新建sys_config表两小时能演示「选课已关闭」
密码哈希存储三张账号表、登录方法半天答辩时能讲安全设计

最值得做的是容量控制,因为它把「数据库事务」这个抽象概念变成了屏幕上能看见的效果。改造usp_enroll的时候,关键在于给课程行加更新锁,否则两个学生同时抢最后一个名额会双双成功。

ALTER PROCEDURE usp_enroll @sno CHAR(10), @cno CHAR(7) AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; BEGIN TRY BEGIN TRANSACTION; -- UPDLOCK:锁住课程行直到事务结束,防止并发下超额录取 DECLARE @cap INT, @cnt INT; SELECT @cap = capacity FROM course WITH (UPDLOCK, ROWLOCK) WHERE cno = @cno; SELECT @cnt = COUNT(*) FROM sc WITH (UPDLOCK, ROWLOCK) WHERE cno = @cno; IF @cnt >= @cap BEGIN ROLLBACK TRANSACTION; THROW 50020, N'该课程名额已满', 1; END INSERT INTO sc(sno, cno, grade) VALUES (@sno, @cno, NULL); COMMIT TRANSACTION; END TRY BEGIN CATCH IF XACT_STATE() <> 0 ROLLBACK TRANSACTION; THROW; END CATCH END;

验证并发是否真的被拦住,开两个查询窗口,各开一个事务,同时执行这个存储过程,看第二个是不是在等锁而不是直接插入成功。用sp_who2能看到阻塞链,这个演示在答辩现场很有说服力。

第二个改造点是sys_config表,键值结构,主键config_key,字段加config_value、remark、updated_at。应用层每次选课前查enroll_start和enroll_end,落在区间外就拒绝。这个改动小,但它让系统从「能跑」变成「有业务流程」。

第三个是密码哈希。课设里明文存密码很常见,但要真拿去答辩,被问一句「数据库被拖走了密码怎么办」就哑了。改法不复杂,后端用PBKDF2或者bcrypt生成哈希存varchar(128),登录时把用户输入的密码用同样的盐重算一遍比对。改动主要在后端代码,数据库这边只需要把字段长度放大。

最后一个建议是关于验证方法。每改完一处,别只看界面能不能点,去数据库里把实际数据捞出来对一遍,尤其是事务类的改动,SELECT * FROM sc WHERE cno = 'xxx'数一下行数是不是真的没超。我在一个课设答辩前夜就是因为没数行数,现场演示时名额超了三个,解释了半天。

从那以后我每次交课设或者交付这类库之前,都会在一个空库上把脚本从头到尾跑一遍,一条不落,然后再跑一次带并发的小脚本。这么走一遍,很多问题在提交前就暴露了。希望帮到你。

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