
简介这是一份基于 SQL Server 的学生选课系统数据库设计课程设计资源面向计算机相关专业学生、教师及需要快速完成课设或项目演示的开发者。资源围绕学生选课场景提供完整的数据表结构、关系设计、查询与视图等源码并配套详细文档说明适合从课程设计到答辩展示的全流程参考代码已在 mac 和 Windows 10/11 上验证运行具备较好的可移植性也适合小白对照学习进阶。包内共 6 个文件以 sql 脚本、docx 文档、md 说明、zbak 备份及 png 截图为主可分别用于还原数据库、查阅设计思路和查看运行界面整体约 139KB轻量且便于快速部署。目前已有 50 人学习下载可作为课设选题、SQL Server 练习或二次开发的基础项目。资源从建表、插入数据到查询功能均有完整体现并保留了数据库备份文件便于直接附加到 SQL Server 中运行95 分的答辩成绩也印证了项目完整度与规范性可在此基础上继续扩展选课、退课等业务功能。1. SQL Server学生选课系统课程设计为什么总在表结构上翻车SQL Server学生选课系统数据库设计源码及详细文档这类课程设计题目几乎每个学数据库的人都碰过。我见过不少同学把界面做得挺完整增删改查全跑得通结果评审老师一句话就问住了为什么选课表要用两个字段做主键你的表结构在第几范式同一门课在两个学期都开课你的主键能表达吗说白了这个题目考的不是写SQL的熟练度而是从业务规则到表结构、从约束到事务的完整设计能力。这门功课真正值钱的地方在于一张选课表能把E-R建模、范式、约束、事务、存储过程、触发器全串起来而且规模刚好适合课程设计不会大到失控。本文按“建模→建表→业务逻辑→避坑→自查”的顺序走一遍目的是让你交出去的源码和详细文档经得起追问也让你自己在答辩前心里有底。2. 从业务规则到ER图选课系统的实体划分与范式取舍2.1 实体与属性先把学生、课程、教师、选课记录四张表定下来拿到需求先别急着开写脚本先把名词圈出来。常见的学生选课系统核心实体有四类学生、课程、教师、选课记录。前三类是基础数据选课记录是联系实体同时也带成绩、选课时间这些属性。我的习惯是先列一张实体属性表写清楚每个字段的业务含义再动手建表这样后续写数据字典也方便。主键选择上学号和课程编号都用业务主键因为现实中它们已经稳定且唯一不需要再造一个自增列。选课记录是个例外它没有天然的唯一业务键常见做法是用联合主键(SID, CID)让“一个学生不能重复选同一门课”这个规则直接落在主键上。也有课程设计给选课记录加一个自增的EnrollID代理键这并不错但要注意加了代理键之后仍然要给(SID, CID)建UNIQUE约束否则重复选课问题还是会出现。我的建议是课程设计阶段用联合主键直观、好讲、好验证。属性原子化这里是个容易踩的地方。比如学生联系方式不要在一个字段里写“13800000000,QQ:12345”要拆成Phone和Email两列地址如果需要按楼栋统计就拆成宿舍楼号和房间号而不是一列写“X区X号楼X室”。出生日期用DATE类型而不是字符串入学年份用SMALLINT而不是VARCHAR。这些细节在详细文档的数据字典里都要能对应上评审按字段逐个查时经不起含糊。2.2 联系与基数学生和课程的多对多决定了选课表必须单独建实体之间的关系用基数来描述一个学生可以选多门课程一门课程可以被多个学生选这是典型的多对多。多对多不能直接在学生表里加一个“课程列表”字段那样要么违反第一范式要么数据冗余到没法维护。正确的拆法是引入选课记录表把一个多对多拆成两个一对多学生1——N选课记录M——1课程。这是整个设计的骨架文档里的E-R图必须把这个关系画清楚。教师和课程之间是简化后的一对多一名教师可以教多门课程一门课程由一名教师负责。这里常见的错误是把教师姓名直接写成课程表里的一个文本字段比如CName旁边放一个TeacherName。一旦教师改名所有历史课程数据都要跟着改而且同名教师会混在一起。正确做法是用TeacherID外键引用教师表查询时再去关联姓名。课程设计阶段这个细节很能体现对“外键引用完整性”的理解。选课记录本身还带属性选课时间、成绩。选课时间用DEFAULT约束自动生成即可成绩允许为空空值表示“未录入”这和0分有本质区别。把属性放在联系实体上而不是放在学生或课程表里也是第二范式的要求下一节具体看。2.3 范式检查选课表的主键设计与适度冗余的代价第二范式要求非主属性完全依赖整个主键。选课表主键是(SID, CID)成绩依赖的是“这个学生选了这门课”这个完整事实符合2NF但如果把课程名也塞进选课表它就只依赖CID不依赖SID这就是部分依赖会出现“改一门课的名字要更新几百行选课记录”的尴尬。这也是为什么选课表里永远只放SID、CID、Score、EnrollTime这几个字段。第三范式消除的是传递依赖。比如学生表里有“系主任姓名”这个字段它依赖的是Dept而不是SID同系每个学生都重复存一份系主任姓名改一次系主任要更新整个系的学生行。课程设计里常见做法是学生表直接存Dept名称而不拆Dept表这其实是一种适度的反范式。我的建议是可以这样设计但详细文档里必须写明“这里有意冗余以简化查询代价是院系改名时需要批量更新”。还有一个值得写进文档的边界问题如果同一门课程在多个学期重复开设课程表只存“课程基础信息”选课表联合主键(SID, CID)就无法区分“这学期选过、下学期还能不能选”。真正的生产方案需要引入开课计划表CourseOffering把学期、任课教师、容量挂在开课ID上选课表再引用开课ID。课程设计受篇幅限制可以简化但建议至少在文档的“扩展设计”一节提一句答辩时很容易变成加分项。3. 建库建表CREATE TABLE字句里藏着评审爱问的细节3.1 建库参数排序规则、文件增长与后续脚本的关系建库这一步很多人直接复制默认库就完事但排序规则会直接影响后面所有字符串比较和中文存储。我通常先建库再显式设置排序规则为简体中文常用配置。文件名和路径按本机实际数据目录改不要照抄。CREATE DATABASE [SelectCourseDB] ON PRIMARY ( NAME NSelectCourseDB_Data, FILENAME ND:\SQLData\SelectCourseDB.mdf, SIZE 8MB, FILEGROWTH 64MB ) LOG ON ( NAME NSelectCourseDB_Log, FILENAME ND:\SQLData\SelectCourseDB_log.ldf, SIZE 4MB, FILEGROWTH 32MB ); GO ALTER DATABASE [SelectCourseDB] COLLATE Chinese_PRC_CI_AS; GO这段脚本里FILENAME必须是SQL Server服务账号有读写权限的目录否则CREATE DATABASE直接报“拒绝访问”这是很多人拿到别人脚本后第一个翻车点。SIZE是初始文件大小FILEGROWTH是自动增长步长课程设计数据量小8MB和64MB足够把自动增长步长调大一点能避免频繁触发文件扩展导致性能抖动。Chinese_PRC_CI_AS的含义是简体中文、不区分大小写、区分重音。这意味着查询时SID20240001和20240001完全匹配同时也不会出现两个仅大小写不同的学号同时存在。排序规则在库创建后还能改但已经建好的表和索引要重建所以最好建库时就定下来。关于版本兼容性近几个SQL Server版本的默认兼容级别都能直接跑本文的脚本只要别用低版本不支持的新函数就行后面涉及的内存优化等特性我会避开。3.2 学生、课程、教师三张表的建表SQL建表顺序有讲究先建被引用的表再建引用它的表。教师表没有外键依赖先建课程表引用教师第二个建学生表独立第三个选课表引用前两者最后建。顺序错了会直接报“对象名无效”。下面是前三张表的脚本。CREATE TABLE dbo.Teacher ( TID CHAR(6) NOT NULL, TName NVARCHAR(20) NOT NULL, Title NVARCHAR(10) NULL, Dept NVARCHAR(30) NULL, CONSTRAINT PK_Teacher PRIMARY KEY (TID) ); GO CREATE TABLE dbo.Course ( CID CHAR(8) NOT NULL, CName NVARCHAR(50) NOT NULL, Credit DECIMAL(3,1) NOT NULL, TeacherID CHAR(6) NULL, Capacity INT NOT NULL DEFAULT 0, SelectedCount INT NOT NULL DEFAULT 0, Location NVARCHAR(50) NULL, TimeDesc NVARCHAR(50) NULL, CONSTRAINT PK_Course PRIMARY KEY (CID), CONSTRAINT FK_Course_Teacher FOREIGN KEY (TeacherID) REFERENCES dbo.Teacher (TID) ON DELETE SET NULL, CONSTRAINT CK_Course_Capacity CHECK (Capacity 0), CONSTRAINT CK_Course_SelectedCount CHECK (SelectedCount 0) ); GO CREATE TABLE dbo.Student ( SID CHAR(12) NOT NULL, SName NVARCHAR(20) NOT NULL, Gender CHAR(1) NOT NULL, Birth DATE NULL, Dept NVARCHAR(30) NOT NULL, Class NVARCHAR(20) NULL, EnrollYear SMALLINT NOT NULL, CONSTRAINT PK_Student PRIMARY KEY (SID), CONSTRAINT CK_Student_Gender CHECK (Gender IN (N男, N女)) ); GO注意几个参数选择。TID和CID用CHAR定长类型因为学号和课程编号长度固定定长字符类型在等值查询和索引比较上比VARCHAR更高效姓名和院系用NVARCHAR是为了让中文字符串字面量在排序规则下不产生隐式转换问题。Credit用DECIMAL(3,1)而不是FLOAT学分只保留一位小数DECIMAL是精确数值类型不会出0.10.2这类浮点误差。TeacherID允许为空表示某门课可能暂时没有分配教师。关于SelectedCount这个字段我需要特意说明它是为了统计已选人数而加的冗余列不在第三范式的要求内。加它的目的是避免每次统计选课人数都要COUNT一遍选课表但代价是必须由存储过程或触发器维护一致性。这一点在详细文档里要作为“有意冗余”写明否则评审会认为你不懂范式。同时注意我没有写CHECK(SelectedCount Capacity)这种约束——听上去合理但实际操作中“先增后校验”的顺序会让单条语句临时违反约束反而造成更新失败容量控制应当放在存储过程的事务里做而不是靠约束硬顶。3.3 选课表与数据类型NVARCHAR、DECIMAL、DATETIME2怎么选选课表是整个系统的核心字段不多但每个都有讲究。脚本如下。CREATE TABLE dbo.Enrollment ( SID CHAR(12) NOT NULL, CID CHAR(8) NOT NULL, EnrollTime DATETIME2(0) NOT NULL CONSTRAINT DF_Enrollment_EnrollTime DEFAULT (SYSDATETIME()), Score DECIMAL(4,1) NULL, CONSTRAINT PK_Enrollment PRIMARY KEY (SID, CID), CONSTRAINT FK_Enrollment_Student FOREIGN KEY (SID) REFERENCES dbo.Student (SID) ON DELETE CASCADE, CONSTRAINT FK_Enrollment_Course FOREIGN KEY (CID) REFERENCES dbo.Course (CID), CONSTRAINT CK_Enrollment_Score CHECK (Score IS NULL OR (Score 0 AND Score 100)) ); GO CREATE INDEX IX_Enrollment_CID ON dbo.Enrollment(CID); GOScore用DECIMAL(4,1)最大能存999.9成绩范围100分以内完全够用。有人用INT存成绩一旦要求保留一位小数就得改表结构有人用FLOAT浮点误差在显示时容易出幺蛾子。DECIMAL(4,1)是稳妥选择。关键点是CHECK约束里显式写了Score IS NULL OR ...因为SQL的CHECK约束对NULL值默认是通过的但写得明确一点看文档的人一眼就能理解“NULL表示成绩未录入”。EnrollTime用DATETIME2(0)精确到秒。DATETIME老类型也能用但DATETIME2范围更大、精度可控现在是推荐方向。DEFAULT(SYSDATETIME())让应用层不用管这个字段。联合主键(SID, CID)自动在SID和CID上建立复合索引但这个索引对“查某门课有哪些学生”这种以CID为起点的查询帮助不大所以我额外加了一个IX_Enrollment_CID单列索引。加不加这个索引课程设计阶段看不出来但文档里写一句“针对CID查询频率高而补充”评审会认为你考虑过索引成本。3.4 外键策略CASCADE还是RESTRICT删除课程会影响成绩历史外键的ON DELETE策略不是随便选的。Course.TeacherID引用Teacher时用了SET NULL教师离职后课程记录保留TeacherID置空成绩历史不受影响。Enrollment引用Student用了CASCADE学生注销学籍后他的选课记录跟着删除符合业务直觉。但Enrollment引用Course我故意没有写级联删除保持默认的NO ACTION。原因很简单一门课程可能有几百条选课记录里面还包含成绩直接DELETE课程会抛外键冲突逼着开发者先处理选课记录或者先退课避免误删成绩历史。CASCADE虽然方便但它会绕过业务逻辑。如果退课有“成绩已录入不能退”的规则而DELETE Student又级联删了选课记录那这条规则就被绕过了。课程设计阶段可以在文档里讨论一下这个取舍什么时候该CASCADE、什么时候该NO ACTION。我这里给出的是一个能自圆其说的方案你如果选择全部CASCADE就要在文档里解释为什么成绩历史可以接受级联删除。4. 选课与退课业务逻辑用存储过程和事务把规则锁进数据库4.1 选课存储过程UPDATE条件判断是防超选的稳妥写法选课逻辑看着简单插入一条选课记录。但实际上有三个规则要同时满足课程必须存在、容量不能满、学生不能重复选。最忌讳的写法是先SELECT检查再INSERT因为并发环境下两个会话可能同时通过检查最后选课人数超出容量。我推荐用一条UPDATE语句同时完成“锁行、判断容量、递增人数”三步再根据影响行数决定是否继续。CREATE PROCEDURE dbo.usp_EnrollCourse SID CHAR(12), CID CHAR(8) AS BEGIN SET NOCOUNT ON; DECLARE RC INT 0; BEGIN TRY BEGIN TRAN; UPDATE dbo.Course WITH (UPDLOCK, HOLDLOCK) SET SelectedCount SelectedCount 1 WHERE CID CID AND Capacity SelectedCount; SET RC ROWCOUNT; IF RC 0 BEGIN IF NOT EXISTS (SELECT 1 FROM dbo.Course WHERE CID CID) BEGIN THROW 50001, N课程不存在, 1; END ELSE BEGIN THROW 50002, N课程容量已满, 1; END END IF EXISTS (SELECT 1 FROM dbo.Enrollment WHERE SID SID AND CID CID) BEGIN THROW 50003, N该学生已选过这门课程, 1; END INSERT INTO dbo.Enrollment (SID, CID) VALUES (SID, CID); COMMIT TRAN; END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK TRAN; THROW; END CATCH END; GO这段逻辑的关键在UPDATE语句的WHERE条件Capacity SelectedCount如果当前人数已满这句更新影响0行随后THROW出对应错误。同时WITH (UPDLOCK, HOLDLOCK)给课程行加了更新锁和范围锁另一个会话想同时选这最后一门课时必须等当前事务结束这样就避免了并发超选。THROW抛出的错误会被CATCH捕获整个事务回滚SelectedCount也会回退到之前的值数据保持一致。参数说明SID和CID的类型与表字段完全一致都是CHAR定长避免隐式转换SET NOCOUNT ON防止“影响N行”的消息干扰客户端调用。错误码50001到50003是自定义区间避开SQL Server系统错误号THROW消息里的中文要加N前缀这是排序规则相关的坑后面还会再提。这套写法里重复选课检查放在UPDATE之后即便触发了THROW事务回滚也会撤销人数增加所以顺序不用担心。4.2 退课存储过程成绩已录入时的业务回滚退课比选课多一层规则成绩一旦录入就不允许退课。这里要区分“成绩为0”和“成绩未录入”0是合法成绩NULL才是未录入判断条件必须是Score IS NULL而不是Score 0。退课逻辑同时要用更新锁锁定选课记录防止退课瞬间成绩正在被录入。CREATE PROCEDURE dbo.usp_DropCourse SID CHAR(12), CID CHAR(8) AS BEGIN SET NOCOUNT ON; DECLARE Score DECIMAL(4,1); BEGIN TRY BEGIN TRAN; SELECT Score Score FROM dbo.Enrollment WITH (UPDLOCK, HOLDLOCK) WHERE SID SID AND CID CID; IF Score IS NULL BEGIN DELETE FROM dbo.Enrollment WHERE SID SID AND CID CID; UPDATE dbo.Course SET SelectedCount SelectedCount - 1 WHERE CID CID; COMMIT TRAN; END ELSE BEGIN ROLLBACK TRAN; THROW 50005, N成绩已录入不能退课, 1; END END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK TRAN; THROW; END CATCH END; GO这里用SELECT Score把Score读到变量里同时用UPDLOCK锁住这行。如果课程不存在SELECT影响0行Score保持NULL会进入DELETE分支而DELETE本身影响0行不会报错但Course表的人数却减了1这是这个脚本的一个边界情况。补救办法是在SELECT后加一个判断也可以用ROWCOUNT检查是否找到了选课记录。实际使用时我更倾向先检查是否存在选课记录再继续后续操作结构更清楚。退课操作要放在事务里和Course表的UPDATE一起提交是因为选课人数与选课记录必须同步。如果先删记录再更新人数中途出错就会留下一个计数对不上的课程。成绩已经录入时回滚事务并抛错这个处理方式比“先删再恢复”要干净得多。4.3 触发器与视图审计日志和统计查询的分工存储过程负责业务规则触发器适合做审计这类“旁路动作”。给Enrollment表加一个AFTER UPDATE触发器只有Score列被修改时往审计表里写一条记录这个设计能在课程设计答辩中很好地展示触发器能力同时避免触发器去做重复的判断逻辑。CREATE TABLE dbo.ScoreAudit ( AuditID INT IDENTITY(1,1) PRIMARY KEY, SID CHAR(12), CID CHAR(8), OldScore DECIMAL(4,1), NewScore DECIMAL(4,1), UpdateTime DATETIME2(0) DEFAULT SYSDATETIME(), Operator NVARCHAR(50) NULL ); GO CREATE TRIGGER dbo.TR_Enrollment_ScoreAudit ON dbo.Enrollment AFTER UPDATE AS BEGIN SET NOCOUNT ON; IF UPDATE(Score) BEGIN INSERT INTO dbo.ScoreAudit (SID, CID, OldScore, NewScore) SELECT i.SID, i.CID, d.Score, i.Score FROM inserted i INNER JOIN deleted d ON i.SID d.SID AND i.CID d.CID WHERE i.Score IS DISTINCT FROM d.Score; END END; GO这段触发器用到SQL Server的INSERTED和DELETED虚拟表UPDATE操作下deleted里是旧行inserted里是新行通过联合主键关联两条虚拟表就能拿到新旧成绩。UPDATE(Score)函数用于判断这次更新是否涉及Score列避免改别的字段也触发审计写入。IS DISTINCT FROM比较能正确处理“旧值NULL、新值0”这种变化这是成绩从“未录入”变成“0分”时的关键点。触发器里不允许显式COMMIT或ROLLBACK也不应该返回结果集这是SQL Server的一条重要规则后续避坑章节会展开。视图方面我一般提供两个一个用于成绩查询关联学生、课程、教师另一个统计每门课的选课人数。统计视图用LEFT JOIN保留没人选的课程配合COUNT_BIG避免GROUP BY后丢行这个细节比较实用。5. 课程设计避坑指南外键顺序、中文乱码与分离备份5.1 建表顺序和外键对象名无效的报错怎么解决现象按“先建选课表、再建学生表”的顺序执行脚本报错信息类似“对象名dbo.Student无效”。原因SQL Server在创建外键时会校验被引用表是否存在子表先建、父表后建引用关系自然无法建立。这不是语法错误而是对象依赖顺序问题。解决先建Teacher再建Course和Student最后建Enrollment。如果脚本已经写成长文件不方便调整顺序可以先把所有表不带外键建出来再统一用ALTER TABLE ADD CONSTRAINT补充外键约束。ALTER TABLE的写法完全不依赖建表顺序也更方便排查哪个外键没加上。课程设计文档的源码注释里建议把完整的执行顺序用注释标在文件头评审照着跑时第一遍就能成功。5.2 中文乱码VARCHAR与NVARCHAR的隐式转换现象往表里插入“张三”查询结果显示“??”或者字符串被自动截断。这个问题在不同人的机器上表现不一样换一台电脑可能又正常了所以很多人叫它“玄学”。原因本质是字符类型和排序规则不匹配。列类型是VARCHAR时只能存非Unicode字符中文字符在GBK或UTF-8环境下会依赖数据库排序规则的代码页插入时字符串字面量没有加N前缀SQL Server会把它当作数据库当前代码页下的非Unicode字符串处理代码页不对就变成问号。解决姓名、院系、课程名这类可能包含中文的列全部用NVARCHAR或NCHAR类型同时所有中文字符串字面量统一加N前缀比如N信息工程学院这样无论服务器排序规则是简体中文还是拉丁文Unicode字符串都不会乱码。这个规则也适用于CHECK约束里的中文判断前面建表脚本里CHECK(Gender IN (N男, N女))已经在用。还有一个隐蔽点存储过程参数类型也要写成NVARCHAR对应否则参数传入时隐式转换照样发生。5.3 触发器里的COMMIT嵌套事务的报错与半提交数据现象在触发器里直接写了COMMIT或ROLLBACK调用存储过程时出现“COMMIT TRANSACTION请求没有对应的BEGIN TRANSACTION”的报错或者数据出现半提交状态一部分操作生效、一部分没生效。原因触发器是在“触发该触发器的语句”的事务上下文中执行的它本身不是独立事务。如果显式执行COMMIT相当于提前结束了外层事务之后外层事务再COMMIT就没有对应的BEGIN会直接报错。如果只写ROLLBACK而不做任何处理会把外层事务整个回滚调用方拿到一个错误却不清楚发生了什么非常难排查。解决触发器里不要写COMMIT和ROLLBACK也不要写SELECT返回结果集。如果要在触发器中阻止某个操作正确做法是用THROW抛出一个明确错误让外层事务介入并统一回滚。审计触发器这类场景只需要做INSERT写入完全不需要控制事务。这条算是血泪经验当年在日志触发器里加了SELECT客户端程序直接被“触发器返回了结果集”搞崩找了一下午。5.4 交资料前的备份分离数据库与附加失败的那些坑现象把正在使用的.mdf文件从数据目录直接复制到U盘拿到另一台机器上执行附加操作报“无法打开物理文件”或“日志文件不一致”。原因数据库在线运行时数据文件被服务进程独占直接复制得到的是一个不完整或被缓存覆盖的文件副本。另外把.mdf单独拷走、不带对应.ldf附加时又会因为日志链断裂而失败。解决正规做法是使用备份而不是分离或复制文件。在源库上执行BACKUP DATABASE生成.bak文件再把.bak交给接收方做RESTORE这是对数据文件最安全的打包方式。如果确实需要分离数据库先确保没有活动会话再执行sp_detach_db拷完文件后交给对方附加附加失败时八成是目录权限问题SQL Server服务账号必须对目标目录有读写权限。课程设计交作业优先交.bak备份文件比交.mdf靠谱得多也显得专业。6. 把源码和文档对起来一个自查技巧让答辩少挨三句问交稿前我会跑一段系统视图查询把库里的对象清单拉出来和文档里的数据字典逐项比对。外键、约束有没有漏建一查就知道。下面这个查询把当前库的所有外键列出来按关联表排序几分钟就能核完一遍。SELECT OBJECT_NAME(fk.parent_object_id) AS ChildTable, OBJECT_NAME(fk.referenced_object_id) AS ParentTable, fk.name AS FKName FROM sys.foreign_keys fk ORDER BY ChildTable;数据字典的写法我都建议用表格字段名、类型、允许空、默认值、约束、说明。比如Student表的SID可以写成“CHAR(12)否无主键学号”Enrollment表的Score写成“DECIMAL(4,1)是无0~100NULL表示未录入”。文档和脚本一起改别先写脚本再补文档这是保证不脱节的唯一办法。造测试数据也有技巧用系统表生成序列几行就能插200条学生记录比手工一条条INSERT效率高得多。测试数据要覆盖边界成绩为0、成绩为NULL、选课容量已满、重复选课这四类场景在测试文档里都要有对应的执行结果。做完这些再把前面提到的主键策略、冗余字段理由、外键级联选择分别写成一句话放在文档的设计说明里答辩被问到时就有话可答。我早期交课程设计只丢一个脚本文件数据字典是用Word现写的结果评审照着表结构一翻字段对不上。从那以后我养成了习惯脚本文件头部注释里先写明实体清单和执行顺序表结构定稿后再写数据字典文档和源码永远一起改。这个习惯后来帮我省了很多返工。希望帮到你。本文还有配套的精品资源点击获取