简介:这份《学生学籍管理系统.doc》面向教育机构信息化建设者、数据库课程设计学生及需要梳理学籍管理业务的技术人员,围绕学生基本信息、成绩、课程、班级与教师资料的管理需求,给出较完整的设计文档。压缩包内仅含 1 个 doc 文件,大小约 295KB,便于直接查阅与二次整理。文档从需求分析、开发计划切入,覆盖学院、专业、年级、班级、学生、课程、教师等实体的 E-R 关系设计,并延伸到关系模式、数据表字段与物理存储结构,同时说明教师、学生、课程、班级、成绩等管理模块及管理员和学生两类用户的认证权限。已有 12954 人学习,适合用作学籍管理系统课程设计、需求说明或数据库建模的参考底稿,读者可据此快速搭起功能框架、表结构与文档骨架。
1. 从一份 .doc 讲起:学生学籍管理系统到底能跑出什么
拿到这份资源的时候,它只是一个 Word 文档,封面写着学生学籍管理系统,里面从需求分析、E-R 图、关系模式一路写到建库语句、视图、存储过程和触发器。多数人翻两页就判定「课程设计报告而已」然后关掉,可真正把里面的 SQL 从头敲一遍会发现,这套数据库设计把「学院—专业—年级—班级—学生—课程—教师」整条业务链跑通了:谁属于哪个班、谁选了哪门课、哪门课由谁上、成绩挂在哪条记录上,全都压进表结构和外键里。它适合两类人:一类是要交数据库课设、需要一份既能建起来又能讲清楚的设计;另一类是练手 SQL、想搞明白多对多关系到底怎么落地的开发者。下面按「模型怎么来 → 表怎么建 → 过程和触发器怎么写 → 哪里会翻车 → 怎么改成真能用的系统」拆。
2. 架构与数据模型:十一张表撑起来的学籍数据库
2.1 实体关系梳理:七个实体和它们的基数
把文档里那张手绘 E-R 图拆开看,实体一共七类:学院、专业、年级、班级、学生、课程、教师。关系有七条,文档给出的基数判断是:学院与专业一对多,专业与年级多对多,专业与班级一对多,班级与学生一对多,学生与课程多对多(成绩是这条联系上的属性),学院与教师一对多,教师与课程多对多。
| 实体对 | 基数 | 落地方式 |
|---|---|---|
| 学院 — 专业 | 1:N | major 表带 collegename 外键 |
| 专业 — 年级 | M:N | 独立建 grades 表,同时挂 collegename、majorname |
| 专业 — 班级 | 1:N | classes 表带 majorname 外键 |
| 班级 — 学生 | 1:N | student 表带 class 外键 |
| 学生 — 课程 | M:N | selectcourse 中间表,主键 (sno, cno),score 是联系属性 |
| 学院 — 教师 | 1:N | teachers 表带 collegename 外键 |
| 教师 — 课程 | M:N | teachercourse 中间表,主键 (cno, teacherID) |
这里面最值得琢磨的是「专业 — 年级」这条。严格按 E-R 语义,年级只是一个整型值,不该单独成表;一旦成表,就得回答「同一个年级在不同专业下是不是同一条记录」。文档的处理是把它物化成 grades 表,主键只有 grade,同时带 collegename 和 majorname 外键——这实际上把「年级」降格成了「专业+年级」的组合标签,主键却还是单个 grade,语义上自相矛盾。我一般会把 grades 表砍掉,直接在 classes 表里放一个 grade 整型字段,班级表天然承载了「某专业某年级某班」三重信息,少一次 join,也不会出现两个专业抢同一个年级主键的尴尬。
第二个点是成绩的位置。成绩不是独立实体,而是学生与课程这条多对多联系上的属性,所以它必须落在 selectcourse 中间表里,而不是塞进 student 或 course。这一点文档做对了,而且额外挂了 teacherID 外键,让「谁教的这门课、给了这个分」也能追溯——这是不少同类设计漏掉的一环。
2.2 逻辑模式落地:文档说九张表,实际建了十一张
逻辑结构设计一节列了九个关系模式,但翻到实施阶段,CREATE TABLE 一共出现了十一次:college、major、grades、classes、student、course、depart、teachers、selectcourse、teachercourse,外加一张 system 账号表。多出来的两张是 grades 和 system,前者是上面说的年级物化表,后者是用户认证用的账号表——文档在关系模式集里压根没提它,只在需求分析的「用户认证」里一笔带过。这种前后不一致在课程设计里非常常见,真正建库时要自己补齐。
核心表的关键字段如下,直接决定了后面所有 SQL 能不能跑通:
| 表名 | 主键 | 关键外键 | 说明 |
|---|---|---|---|
| college | collegename | 无 | 学院名做主键,collegeID 只是普通列 |
| major | majorname | collegename | 专业名做主键,同名前缀问题在这埋雷 |
| grades | grade | collegename、majorname | 年级单列主键,语义有歧义 |
| classes | class | grade、collegename、majorname | 班级名做主键,跨专业会撞 |
| student | sno | collegename、majorname、grade、class | 学号 char(10) |
| course | cno | collegename | 课程号 char(10) |
| teachers | teacherID | collegename、department | 工号 varchar(10) |
| depart | department | collegename | 教研室名做主键 |
| selectcourse | (sno, cno) | sno、cno、teacherID | 成绩 score 为 int |
| teachercourse | (cno, teacherID) | cno、teacherID | 任课关系 |
| system | zhanghao | 无 | 账号表,只有账号和操作两列 |
用「名称」当主键是这套设计最典型的教学痕迹:collegename、majorname、class、department 全是 varchar,一旦学院改名,级联更新会波及一大片;而且 varchar 做主键在聚簇索引里占空间,页分裂也更频繁。真实项目里我会统一改成自增 ID 或业务编码做主键,名称只做唯一约束。不过话说回来,教学场景下用名称做主键的好处是查询语句极好读,where collegename='计算机学院'一眼就懂,作为理解关系模型的练手素材,这个取舍可以接受。
2.3 类型选择上的三个明显短板
看字段类型,问题集中在三处。学生姓名 studentname 用的是 varchar(10),教师姓名 teachername 也是 varchar(10)——十个字符对三个字的姓名够用,但遇上复姓加少数民族姓名就直接截断,实际项目里给到 varchar(50) 更稳。性别 sex 用 char(2),比用 char(1) 存 M/F 或者 tinyint 存枚举要费一倍空间,好处是能直接存「男」「女」两个汉字,展示层不用做映射。
成绩 score 定义成 int,没有任何 CHECK 约束,意味着 150 分、-20 分都能插进去,文档把这类校验全部押在触发器上——后面会看到那些触发器写得并不牢靠。学号 sno 用 char(10) 定长,好处是索引比较快、不会因为前后空格产生重复记录,坏处是将来学号升到 12 位就得改表,而 char 改 varchar 在有数据的情况下并不是无痛操作。
还有一处容易被忽略:整张表没有创建时间、更新时间,也没有任何软删除标记。学生毕业离校,文档的做法是「将学生信息删除」,物理删除一执行,selectcourse 里这个学生的所有成绩记录要么靠触发器级联删掉,要么直接变成孤儿数据。真实教务系统里学生是绝对不删的,只会改状态字段——这条差异在二次开发时必须补上。
3. 建库建表实操:把物理设计敲进 SQL Server
3.1 建库语句:文件路径和增长参数别照抄
文档给的建库语句是标准的三段式:主数据文件、日志文件、增长策略。原样抄下来大概率会在第一步就失败,因为D:\DATA这个目录在你机器上多半不存在。
-- 建库:主文件 + 日志文件,注意先确认目录已存在 CREATE DATABASE studentmanagesystem ON PRIMARY ( name = 'studentmanagement', -- 逻辑名,后续备份还原都用它 filename = 'D:\DATA\studentmanagesystem.mdf', -- 物理路径,目录必须提前建好 size = 3, -- 初始 3MB maxsize = UNLIMITED, -- 不设上限 filegrowth = 1 -- 每次增长 1MB ) LOG ON ( name = 'studentmanagesystem_log', filename = 'D:\DATA\studentmanagesystem_log.ldf', size = 1, maxsize = 2, -- 日志封顶 2MB,这里明显不合理 filegrowth = 10PERCENT );逻辑说明:ON PRIMARY指定主文件组,教学环境只有一个文件组,写不写都行,但写上能让后面加辅助文件时有地方挂。filegrowth = 1不写单位时默认按 MB 增长,而日志文件写的是10%百分比增长——两种增长方式混用是 DBA 的大忌,百分比增长会随着文件变大导致单次扩容越来越慢,还可能卡住写入。
参数怎么改:把filename换成你自己机器上已存在的目录,比如C:\db\;maxsize = 2对日志来说几乎必然不够,一门课的选课数据加上触发器日志,很快就能撑爆,我一般直接给maxsize = UNLIMITED配filegrowth = 64MB。如果只是想跑通看效果,把日志的 size 和 maxsize 都放大到 50MB 更省事。
还有一个坑:SQL Server 服务账号对D:\DATA未必有写权限,报错通常是「操作系统错误 5(拒绝访问)」。解决办法不是去改服务账号,而是把目录建在C:\ProgramData之类的默认位置,或者手动给目录加 Everyone 写权限——后者只在本地练手时用。
3.2 建表顺序与外键依赖:十一条 DDL 的执行序列
外键是建表顺序的硬约束:被引用的表必须先存在。文档给的顺序基本正确,从 college 开始一路往下,但中间有几处依赖是乱的,比如 student 表引用了 classes 和 grades,就必须排在它们之后。
-- 第一层:无外键依赖 CREATE TABLE college ( collegename varchar(20) PRIMARY KEY NOT NULL, collegeID int NOT NULL ); -- 第二层:只依赖 college CREATE TABLE major ( majorname varchar(20) PRIMARY KEY NOT NULL, majorID int NOT NULL, collegename varchar(20) NOT NULL, FOREIGN KEY (collegename) REFERENCES college(collegename) ); CREATE TABLE depart ( department varchar(20) NOT NULL PRIMARY KEY, departID int NOT NULL, collegename varchar(20), FOREIGN KEY (collegename) REFERENCES college(collegename) ); -- 第三层:依赖 college + major CREATE TABLE grades ( grade int NOT NULL PRIMARY KEY, collegename varchar(20) NOT NULL, majorname varchar(20) NOT NULL, FOREIGN KEY (collegename) REFERENCES college(collegename), FOREIGN KEY (majorname) REFERENCES major(majorname) ); -- 第四层:依赖 grades CREATE TABLE classes ( class char(10) NOT NULL PRIMARY KEY, grade int NOT NULL, collegename varchar(20) NOT NULL, majorname varchar(20) NOT NULL, FOREIGN KEY (collegename) REFERENCES college(collegename), FOREIGN KEY (majorname) REFERENCES major(majorname), FOREIGN KEY (grade) REFERENCES grades(grade) );逻辑说明:这一段的关键不是语法,而是把「引用图」画出来。college 是根,major、depart 挂在它下面,grades 同时挂 college 和 major,classes 再挂 grades——class这个字段名在 SQL Server 里不是保留字,但和 C#、Java 的关键字冲突,写应用层时会碰到@class这种别扭的变量名,建议改成 classname。
参数说明:varchar(20) 存中文在 UTF-8 排序规则下实际只能放 6 到 7 个汉字,学院名稍微长一点就截断,我给到 varchar(50)。char(10) 的学号和班级名是定长,插入不足十位会自动补空格,where class='软件2201'照样能匹配,但拿去做字符串拼接时尾随空格会阴人一把,前端展示前记得 TRIM。
后续几张表的建表顺序是:student(依赖 college、major、grades、classes)→ course(依赖 college)→ teachers(依赖 college、depart)→ selectcourse(依赖 student、course、teachers)→ teachercourse(依赖 course、teachers)→ system(无依赖)。只要保证被引用的表在前,整套 DDL 就能一次跑完。
3.3 索引与视图:哪些该留,哪些必须改
文档建了三个非聚簇索引,全部是降序:
-- 成绩降序索引,配合「按分数排名」类查询 CREATE NONCLUSTERED INDEX score_index ON selectcourse(score DESC); -- 年龄降序索引 CREATE NONCLUSTERED INDEX student_sage_index ON student(sage DESC); -- 教师年龄降序索引 CREATE NONCLUSTERED INDEX teachers_sage_index ON teachers(sage DESC);逻辑说明:score_index 有一定价值,成绩排名、分段统计能用上;但年龄上的两个索引基本是无效索引——学生年龄集中在 18 到 22 岁,选择性极低,查询优化器大概率直接忽略它,白白增加每次 INSERT 的维护成本。真正该建索引的是student(class)、selectcourse(cno)、teachers(collegename)这类外键列,因为按班级查学生、按课程查选课记录是最高频的操作,而外键列恰恰是 SQL Server 不会自动建索引的地方。
视图部分文档给了十个,思路是用视图做权限隔离和常用查询封装,方向没问题,但有几个定义会直接报错或结果错位。最典型的是 c_major_view:
-- 有问题的写法:列别名顺序与 SELECT 顺序不一致 CREATE VIEW c_major_view(collegename, collegeID, majorname, majorID) AS SELECT collegeID, college.collegename, majorID, majorname FROM college, major WHERE major.collegename = college.collegename;视图声明的四列顺序是「学院名、学院号、专业名、专业号」,而 SELECT 输出的顺序是「学院号、学院名、专业号、专业名」,结果就是collegename这一列里躺着的是整数 ID。这种错误不会报错,只会安安静静地把数据搞错,查出来一看全是数字,排查起来非常费劲。正确写法是把两边的顺序对齐,或者干脆不写列别名列表,让视图自动继承列名。
同类的还有 avg_s 视图里的avg(grade)——selectcourse 表根本没有 grade 字段,成绩列叫 score,这句一执行就是「列名无效」。以及各视图里大量使用GROUP BY全字段来「去重」,其实语义上就是DISTINCT,写 GROUP BY 除了让执行计划多一步聚合,没有任何好处。
4. 存储过程与触发器:业务规则写进数据库层
4.1 五个存储过程:把高频查询封成接口
文档给了五个存储过程,覆盖的正是教务场景里最高频的五类查询:查学生各科成绩、查课程平均分和选课人次、查学院下专业列表、查班级学生名单、查教师任课情况。设计意图很清楚——把多表 join 藏进数据库,应用层只传参数拿结果。先看写得最完整的一个:
-- 输入学号,返回该生所有课程的成绩与学分 CREATE PROC scoreproc @sno char(10) AS BEGIN SELECT s.sno, s.studentname, c.coursename, sc.score, c.Credit FROM student s JOIN selectcourse sc ON s.sno = sc.sno JOIN course c ON c.cno = sc.cno WHERE s.sno = @sno; END; GO -- 调用方式 EXEC scoreproc '2022010101';逻辑说明:原文档用的是逗号连接加 WHERE 过滤的老写法,我改成显式 JOIN,纯粹为了可读性,执行计划是一样的。三个表都命中主键索引,性能没问题。参数@sno是 char(10),调用时传不足十位会自动补空格,字符串比较仍然成立,但如果从应用层传入的是 varchar,隐式转换会让索引失效,这一点在数据量上来后会很明显。
带输出参数的是 avgscoreproc 和 majorproc,用来同时返回结果集和统计值:
-- 输入课程名,输出平均分和选课人次 CREATE PROC avgscoreproc @cname varchar(50), @avg int OUTPUT, @count smallint OUTPUT AS BEGIN SELECT @avg = AVG(sc.score), -- 原文写的是 avg(grade),字段名错了 @count = COUNT(*) FROM selectcourse sc JOIN course c ON c.cno = sc.cno WHERE c.coursename = @cname; END; GO DECLARE @a int, @b smallint; EXEC avgscoreproc '数据库原理', @a OUTPUT, @b OUTPUT; SELECT @a AS 平均成绩, @b AS 选课人次;逻辑说明:OUTPUT 参数适合「一条明细 + 一个汇总」的场景,比如既列出班级学生名单,又返回总人数。参数类型上@avg int会把平均分截断成整数,89.6 变成 89,成绩统计里这点误差不能忍,我一般改成 decimal(5,1)。另外没有匹配课程时 AVG 返回 NULL,直接赋值给 int 变量不会报错,但下游拿到的是空值,应用层必须判空。
原文里还有两处明显的笔误:teacherproc 的参数写成@college char(10) outchar,outchar不是合法关键字,应为 OUTPUT;classproc 创建时叫 classproc,调用时写成exec classprco,名字对不上直接报「找不到存储过程」。这类错误在课程设计文档里成片出现,照抄必翻车。
4.2 触发器的真实意图:十三个里有几个是硬凑的
触发器一共写了十三个,粗略分类:四个是「删除课程时联动删除选课记录」「修改学号时联动更新选课表」「修改工号时联动更新任课表」「学号或课程号不存在则拒绝插入」,属于真正的一致性维护;剩下九个 insert_xxx 全部是「往 A 表插一条,就往 A 表再插一条一模一样的」,逻辑上等于自己触发自己,纯属凑数。
先看有实际意义的那几个,以级联删除为例:
-- 课程被删除时,删掉所有引用它的选课记录 CREATE TRIGGER ctrig ON course AFTER DELETE AS BEGIN DELETE sc FROM selectcourse sc JOIN deleted d ON sc.cno = d.cno; -- 原文写成 delete SC,SC 表根本不存在 END;逻辑说明:deleted是 SQL Server 在 DML 触发器中提供的伪表,里面是本次被删除的行,多行删除时它也是多行,所以必须按集合操作,不能假设只有一条。原文写的是DELETE SC WHERE cno IN (SELECT cno FROM deleted),其中 SC 是另一套教材里的表名,在这套库里不存在,导致触发器能创建成功、一执行就报「对象名无效」。
同样的毛病出现在 check_trig 里,用了不存在的s表;以及两个触发器都叫ctrig、两个都叫ttrig,第二个创建时直接撞名报错。至于 update 学号的那个:
-- 原写法:SET 后面跟 IN,语法非法 -- UPDATE selectcourse SET sno IN (SELECT sno FROM inserted) WHERE ... -- 正确写法:通过 inserted/deleted 关联更新 CREATE TRIGGER strigger ON student AFTER UPDATE AS BEGIN IF UPDATE(sno) BEGIN UPDATE sc SET sc.sno = i.sno FROM selectcourse sc JOIN deleted d ON sc.sno = d.sno JOIN inserted i ON i.studentname = d.studentname; END; END;逻辑说明:SET 列 IN (子查询)不是 T-SQL 语法,只能写成SET 列 = 标量子查询或者用UPDATE ... FROM的形式。另外原来的关联条件缺失——inserted 和 deleted 之间没有明确的对应关系,多行更新时会张冠李戴。真实项目里我从不靠姓名去 join,而是用主键或一个稳定的业务键。
4.3 用级联约束和事务替代一半触发器
上面那三类「联动更新/删除」的触发器,其实一行外键约束就能解决,而且性能和可靠性都好得多:
-- 建表时直接声明级联,删课程自动删选课记录 ALTER TABLE selectcourse ADD CONSTRAINT fk_sc_course FOREIGN KEY (cno) REFERENCES course(cno) ON DELETE CASCADE ON UPDATE CASCADE;逻辑说明:ON DELETE CASCADE让父表删除时自动清理子表,效果和 ctrig 完全一致,但由存储引擎在同一个事务里完成,不会出现触发器执行失败导致父表删除成功、子表残留的半吊子状态。ON UPDATE CASCADE对应「改学号联动改选课表」,同样是一行搞定。
参数说明:级联删除在多级外键链上要慎用,比如 college 删了会连带删 major、grades、classes、student、selectcourse,一条 DELETE 干掉半库数据,所以生产库通常只在最底层(选课记录这种纯关系表)开级联,上层的删除改成软删除加人工确认。
至于「学号或课程号不存在就拒绝插入」这类校验,更合适的做法是 CHECK 约束加外键本身——外键存在就意味着插不进不存在的学号,触发器里那段IF EXISTS ... ROLLBACK属于重复劳动,而且 ROLLBACK 会把整个外层事务一起回滚,如果外层还有别的操作,那些操作也白做了,排查时非常难定位。真正需要触发器的是「成绩必须在 0 到 100 之间」这种跨列或跨表的业务规则,可惜文档里恰恰没写。
5. 避坑与排查:这份文档跑起来最容易翻车的五处
5.1 建库阶段:路径不存在与日志封顶
现象:执行 CREATE DATABASE 报「操作系统错误 5(拒绝访问)」或「找不到路径」,或者建库成功但插入几百条数据后就报日志已满。
原因:一是filename里写的D:\DATA目录在多数机器上不存在,SQL Server 服务账号也不会自动创建目录;二是日志文件的maxsize = 2加filegrowth = 10%,2MB 的日志上限在教学演示里够用,一旦开始批量插入学生数据或者跑触发器,日志迅速写满,数据库直接进入只读状态。
解决:先手动建好目录,或者把路径改到默认实例目录;把日志改成maxsize = UNLIMITED, filegrowth = 64MB。已经建好的库可以用ALTER DATABASE ... MODIFY FILE改增长参数,不用重建。
5.2 建表阶段:外键顺序与名称主键撞车
现象:执行CREATE TABLE student时报「引用的对象 classes 不存在」,或者插入第二个专业的「2201 班」时报主键冲突。
原因:前者是建表顺序错了,student 依赖 classes 和 grades,必须先建;后者是 classes 表用 class 单列做主键,而class里存的只是「2201」这样的班级号,两个专业各有一个 2201 班,主键直接撞车。
解决:建表严格按 college → major / depart → grades → classes → student / course / teachers → selectcourse / teachercourse 的顺序执行;classes 的主键改成(collegename, majorname, grade, class)复合主键,或者加一个自增 ID 做主键、四列做唯一约束。后者更好改,因为 student 表里引用的外键也得跟着改。
5.3 视图阶段:列别名错位与不存在的字段
现象:查询 c_major_view 时,学院名那一列显示的是数字;查询 avg_s 视图时报「列名 grade 无效」。
原因:前者是视图声明的列名列表和 SELECT 的输出顺序不一致,数据没错位但列名贴错了标签;后者是 selectcourse 表的成绩字段叫 score,视图定义里却写了avg(grade),数据库里根本没有 grade 这个列。这两个错误都不会在创建视图时报出来,只有查询时才炸。
解决:视图定义里的列名列表能省就省,让视图自动继承 SELECT 的列名;真要重命名就逐个对齐。字段名先sp_help selectcourse看一眼实际列名再写,别凭记忆。
5.4 存储过程阶段:参数关键字与过程名拼写
现象:创建 teacherproc 报语法错误;调用exec classprco报「找不到存储过程 classprco」。
原因:outchar写成这样是因为手滑漏了output,编译器把它当成参数名而不是关键字;classproc和classprco是同一份文档里两处拼写不一致,创建和调用对不上。
解决:OUTPUT 关键字务必写全;命名统一用一套规则,比如proc_前缀加业务名,创建完立刻SELECT name FROM sys.procedures核对一遍名字。这类低级错误在照抄文档时出现频率极高,敲完一段就执行一次,别攒到最后一起跑。
5.5 触发器阶段:伪表名、重名与递归
现象:删了一条课程记录,触发器报「对象名 SC 无效」;创建第五个触发器时报「数据库中已存在名为 ctrig 的对象」;往 student 插一条数据,触发器里的 INSERT 似乎执行了,但表里数据没变化。
原因:SC 和 s 是别的教材里的表名,这套库里叫 selectcourse 和 student;两个触发器都叫ctrig,SQL Server 在同一 schema 下不允许同名;insert_student 触发器在 student 的 AFTER INSERT 里又往 student 插数据,属于递归触发,而 SQL Server 默认关闭 RECURSIVE_TRIGGERS,所以它既不报错也不生效,静默失败最坑人。
解决:全局搜一遍表名,统一成实际存在的表名;触发器命名带业务前缀,如trg_course_del;把所有 insert_xxx 这类「自插自」的触发器直接删掉,它们没有任何业务价值。真要验证递归行为,可以临时ALTER DATABASE ... SET RECURSIVE_TRIGGERS ON看效果,但别留在生产库上。
6. 二次开发改造:从课设文档到能上手的系统
6.1 补一层应用服务:把 SQL 从界面里剥出来
这套设计只到数据库层,没有界面也没有服务端。要让它变成真能用的系统,最小改造是加一层薄薄的服务端,按 Controller — Service — DAO 三层拆开,登录鉴权放在最前面。账号表 system 只有账号和一列「操作」,得补上密码哈希、角色(admin / student)、状态三个字段,密码绝不能明文存。
# 登录校验:先查账号,再按角色分发不同的首页数据权限 def login(account: str, raw_password: str) -> dict: row = db.query_one( "SELECT zhanghao, pwd_hash, role FROM system WHERE zhanghao = ?", (account,) ) if not row or not verify_hash(raw_password, row["pwd_hash"]): raise AuthError("账号或密码错误") # 学生只能查自己的数据,管理员走全量接口 scope = "self" if row["role"] == "student" else "all" return {"account": row["zhanghao"], "role": row["role"], "scope": scope}逻辑说明:role决定后面每个接口的数据范围——学生调/score/list时服务端强制把 sno 换成自己,不给前端传参的机会,否则改个 URL 就能查别人成绩。verify_hash用 bcrypt 之类的慢哈希,别用 MD5。这段代码没有直接调用存储过程,因为分页、条件组合这类动态查询用 ORM 拼更灵活;而 scoreproc 那种固定报表查询可以保留,走EXEC调用,省得在应用层重复写三表 join。
6.2 迁移到 MySQL 或 PostgreSQL 的改造清单
SQL Server 2005 现在很难装,多数人会把库迁到 MySQL 8 或 PostgreSQL。语法差异主要集中在类型和级联上,下面这张表是我迁移时踩过的对照:
| 项目 | SQL Server 写法 | MySQL 8 | PostgreSQL |
|---|---|---|---|
| 自增主键 | IDENTITY(1,1) | AUTO_INCREMENT | GENERATED ALWAYS AS IDENTITY |
| 字符串 | varchar(n) 按字节 | varchar(n) 按字符 | varchar(n) 按字符 |
| 级联更新 | ON UPDATE CASCADE | 支持 | 不支持,需触发器 |
| 触发器伪表 | inserted / deleted | NEW / OLD | NEW / OLD |
| 存储过程 | CREATE PROC | CREATE PROCEDURE | CREATE FUNCTION / PROCEDURE |
最需要注意的是 varchar 的长度语义:SQL Server 里 varchar(20) 存中文大约只能放六七个字,迁到 MySQL 之后按字符算,同样的 20 能存二十个汉字,看起来是好事,但如果有前端按字节截断的逻辑,反而会出现显示不一致。级联更新在 PostgreSQL 里不支持,只能用ON UPDATE触发器补,这也是唯一必须保留触发器的场景。
6.3 改造后的三个验证检查点
改完别急着交付,按这三个点验一遍。第一,插一条学院记录,再插一个属于它的专业,然后把学院名改掉,看专业表的 collegename 有没有跟着变——这一步验证级联更新是否生效,或者触发器是否写对。第二,删一门已经有学生选了的课程,看 selectcourse 里的对应记录是否同步消失,同时确认其他课程的选课记录没被误删,这是级联范围有没有写宽的直接证据。第三,用学生账号登录,手动构造一个带别人学号的请求,看服务端是返回 403 还是乖乖把数据吐出来。
我第一次把这套库迁到 MySQL 时,栽在最不起眼的地方:classes 表用 char(10) 存班级名,迁过去之后 MySQL 默认不补空格,而 student 表里存的 class 值还带着尾部空格,join 一条都匹配不上,查出来的班级学生名单全是空的。从那以后我每次迁移定长字符字段,都强制先跑一遍SELECT class, LENGTH(class) FROM classes和SELECT DISTINCT LENGTH(class) FROM student对一遍长度,确认没有尾随空格再往下做。希望帮到你。
本文还有配套的精品资源,点击获取