ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#学生选课系统开发:从表结构到并发控制的完整实践

C#学生选课系统开发:从表结构到并发控制的完整实践 简介面向高校教务场景开发的C#与SQL SERVER学生选课及成绩查询管理系统源码包适合.NET方向初学者、课程设计及毕业设计参考可作为理解桌面数据库应用开发全流程的完整范例。系统全面覆盖学生信息管理、课程管理、选课申请、成绩查询与统计报表生成等核心模块前端基于C#与ADO.NET设计WinForms窗体界面并实现业务逻辑后端依赖SQL SERVER完成数据存储、关联查询与权限管控所附mdf/ldf数据库文件与sql脚本可直接用于环境搭建及数据测试。资源共123个文件包含44个C#源码文件、20个资源文件及对应resx配置、窗体设计器布局、可运行的exe与dll、项目解决方案sln与使用说明文档等整体大小4.15MB代码结构完整清晰打开解决方案即可编译运行。已有366人学习下载适合深入理解WinForms窗体程序设计、数据表结构设计与增删改查实现也可作为课程设计或毕业设计的基础框架与排错参考。1. 学生选课及成绩查询管理系统为什么多数课设都卡在“能用”到“能交”之间做C#课程设计或毕业设计的人十个里有八个都撞过“学生选课及成绩查询管理系统”这个题目。网上源码一大把下载下来能跑但被问一句“两个学生同时选最后一门课怎么办”就哑火了。这个题目的本质不是数据库增删改查而是选课冲突、成绩统计和权限边界。我前后带过三届实习生发现真正能把选课事务、成绩联查、打包发布一条龙讲清楚的人不多。这篇笔记就按我实际交付这套系统的顺序来讲表怎么建、C#怎么连、选课怎么不翻车、发布怎么不丢库最后给一套能自己验证的收尾方法。适合在Visual Studio SQL Server环境下做课设、毕设或者想把这套流程复用到其他管理系统的人。2. 先把表设计对选课不冲突、成绩能追溯的数据库结构很多人的系统翻车不是代码写得差而是表结构从第一天就埋了雷。学生选课及成绩查询管理系统听起来只要“学生、课程、成绩”三张表实际做起来会发现成绩得单独拆选课关系必须带学期字段课程表还得有容量控制。这一章把表结构讲透再给一套兼容SQL Server 2008 R2的建库脚本。2.1 先拆需求四张基础表把选课关系与成绩历史分开我先按“一个学生可以选多门课一门课可以被多个学生选”的多对多关系来拆。网上不少课设直接用一张Student_Course表存学生和课程ID外加一个成绩字段图省事但这样设计会带来三个问题学生重修同一门课成绩只有一条记录覆盖了历史退课和改成绩没有操作痕迹统计平均分时老成绩混在一起算不出有效数据。所以我的做法是拆四张表Student学生表、Course课程表、SelectCourse选课表、Score成绩表。SelectCourse只负责“谁在什么学期选了哪门课”Score负责“这次选课最终考了多少分”。这样补考、重修都能追加记录查询时用选课ID关联中间表不冗余成绩。字段设计也要提前想清楚。学号不要用int用NVARCHAR(20)因为很多学校的学号是“2023001”这种带前导零的字符串int会把零丢掉。课程号同理。姓名用NVARCHAR(50)成绩用SMALLINT比INT省空间而且能配合CHECK约束限定0到100。创建时间字段统一叫CreateTime后面排查数据问题时你会感谢这个字段。2.2 建库脚本兼容SQL Server 2008 R2的T-SQL写法下面是实际交付项目里我会直接使用的建库脚本。目标版本是SQL Server 2008 R2所以在SQL Server 2012/2016/2019上运行也没有兼容问题。注意几个细节数据库排序规则指定Chinese_PRC_CI_AS字段类型用NVARCHAR选课表加唯一约束防止同一学期重复选课课程表加Capacity和SelectedCount两个字段用于选课容量控制。CREATE DATABASE StudentCourseDB; GO USE StudentCourseDB; GO CREATE TABLE Student ( StudentNo NVARCHAR(20) PRIMARY KEY, -- 学号字符串类型兼容带前导零的学号 StudentName NVARCHAR(50) NOT NULL, -- 姓名 Gender NCHAR(1) NOT NULL DEFAULT N男, -- 性别默认男 ClassName NVARCHAR(50) NULL, -- 班级允许为空 CreateTime DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE Course ( CourseNo NVARCHAR(10) PRIMARY KEY, -- 课程编号 CourseName NVARCHAR(50) NOT NULL, -- 课程名称 Credit NUMERIC(3,1) NOT NULL DEFAULT 0, -- 学分如3.0 Capacity INT NOT NULL DEFAULT 60, -- 课程容量 SelectedCount INT NOT NULL DEFAULT 0, -- 已选人数 CheckTime NVARCHAR(10) NULL, -- 上课时间如“周一第3-4节” CreateTime DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE SelectCourse ( SelectID INT IDENTITY(1,1) PRIMARY KEY, -- 选课记录ID自增 StudentNo NVARCHAR(20) NOT NULL, CourseNo NVARCHAR(10) NOT NULL, Semester NVARCHAR(20) NOT NULL, -- 学期如“2024-2025-1” SelectTime DATETIME NOT NULL DEFAULT GETDATE(), CONSTRAINT FK_Select_Student FOREIGN KEY (StudentNo) REFERENCES Student(StudentNo), CONSTRAINT FK_Select_Course FOREIGN KEY (CourseNo) REFERENCES Course(CourseNo), CONSTRAINT UQ_Select UNIQUE (StudentNo, CourseNo, Semester) -- 同一学期同一门课只允许选一次 ); CREATE TABLE Score ( ScoreID INT IDENTITY(1,1) PRIMARY KEY, SelectID INT NOT NULL, -- 关联选课记录 ScoreValue SMALLINT NULL, -- 成绩NULL代表未录入 ExamType NVARCHAR(20) NOT NULL DEFAULT N期末, -- 考试类型如期末/补考/重修 RecordTime DATETIME NOT NULL DEFAULT GETDATE(), CONSTRAINT FK_Score_Select FOREIGN KEY (SelectID) REFERENCES SelectCourse(SelectID), CONSTRAINT CK_Score_Value CHECK (ScoreValue BETWEEN 0 AND 100) -- 成绩范围限定 );这段脚本的逻辑说明Student和Course是基础数据表各自的主键都用业务编号而不是自增ID这样在C#代码里可以用学号和课程号直接拼接业务逻辑不用先“查ID再插入”多走一步。SelectCourse是连接表SelectID作为成绩表的外键保证一条成绩一定对应一条真实的选课记录不会出现“成绩在但选课记录被删”的孤儿数据。Score表带ExamType字段补考和重修的分数能分开存不会覆盖期末成绩。参数说明里有两个容易被忽略的点。一是Semester字段用NVARCHAR(20)类似“2024-2025-1”这种格式而不是用日期类型因为学期不是自然时间点。二是UNIQUE约束UQ_Select它的作用原理是在插入时由数据库检查同一个人同一学期是否已经选过这门课如果没有这条约束你得在C#里先SELECT再INSERT并发时就会重复选课。让数据库来保证比让应用程序保证可靠得多。2.3 容量控制与防超选为什么“先查后插”一定会翻车选课系统最典型的业务规则是“课程容量60人满了不能选”。新手最常见的写法是先查询已选人数再判断是否小于容量然后插入选课记录。这在单用户测试时一切正常但设想一个场景第59个学生选了课第60个和第61个学生同时点“选课”两个连接都查询到已选人数是59都判断可以选于是都插入了最终变成61人。这个问题的根源是“先查后插”不是原子操作两个事务之间存在时间窗口。我的做法是把“判断容量”和“增加人数”合并成一条UPDATE语句利用SQL Server的行锁来做原子判断DECLARE StudentNo NVARCHAR(20) N2023001; DECLARE CourseNo NVARCHAR(10) NCS101; DECLARE Semester NVARCHAR(20) N2024-2025-1; BEGIN TRANSACTION; UPDATE Course WITH (UPDLOCK, HOLDLOCK) SET SelectedCount SelectedCount 1 WHERE CourseNo CourseNo AND SelectedCount Capacity; IF ROWCOUNT 1 BEGIN INSERT INTO SelectCourse (StudentNo, CourseNo, Semester) VALUES (StudentNo, CourseNo, Semester); PRINT N选课成功; END ELSE BEGIN PRINT N课程已满; END COMMIT TRANSACTION;这段脚本的逻辑说明UPDATE语句里的SelectedCount Capacity条件会在更新时由数据库锁定这一行直到事务结束。如果容量没满SelectedCount加1ROWCOUNT返回1然后插入选课记录如果满了UPDATE影响0行直接跳过插入。整个过程在同一个事务里两个并发请求同时执行时第二个请求会被第一个请求的行锁阻塞等第一个提交后再执行此时SelectedCount已经被加过条件不成立自然选不上。参数说明里的WITH (UPDLOCK, HOLDLOCK)是这个方案的关键。UPDLOCK告诉SQL Server在更新时加更新锁防止两个读取者同时读到同一个值HOLDLOCK让锁保持到事务结束防止在UPDATE和INSERT之间被其他事务插进来。实际测试中我用20个线程同时抢最后1个名额只有1个成功其余全部返回“课程已满”。这比在C#里用lock锁对象靠谱因为Web应用是多进程部署时C#的锁只能锁住当前进程。3. C#连接SQL Server的落地姿势连接串、SqlHelper与配置管理表结构设计好之后接下来是在Visual Studio里写C#代码连接SQL Server。这一章不讲花哨的ORM框架就讲最稳的SqlConnectionSqlCommand这套原生方式因为课设和多数企业内部小系统用这一套已经足够而且面试时被问底层原理也不会心虚。3.1 连接串的五个参数Server、Initial Catalog、Integrated Security、User ID、Password连接字符串是整个系统最先被检查的部分。我用的是一个带连接池和超时控制的标准写法待会儿逐字段解释为什么这么配connectionStrings add nameStudentDB connectionStringServerlocalhost\SQLEXPRESS;Initial CatalogStudentCourseDB;Integrated SecurityTrue;PoolingTrue;Connect Timeout15;EncryptFalse; providerNameSystem.Data.SqlClient / /connectionStrings各参数含义如下表参数示例值说明常见坑Serverlocalhost\SQLEXPRESS数据库实例名本机默认实例写localhost命名实例写“主机名\实例名”写成.或127.0.0.1连接默认实例时可能找不到命名实例Initial CatalogStudentCourseDB要连接的数据库名写错名称运行时不报错但查询时提示“对象名无效”Integrated SecurityTrue用Windows身份验证不需要用户名密码打包到别的机器后目标机器SQL Server必须开着Windows身份验证User ID / Passwordsa / 123456SQL Server身份验证时使用本地开着Windows验证时用这个会报“用户登录失败”PoolingTrue启用连接池避免频繁建连与数据库版本无关但连接串变更时旧池连接不会被释放参数这块最容易踩的坑是Server名。如果你的开发机装的是Visual Studio自带的LocalDB连接串应该写(localdb)\MSSQLLocalDB如果装的是SQL Server Express默认实例名是.\SQLEXPRESS。我推荐直接用localhost\SQLEXPRESS并在开发前先用Navicat for SQL Server试用版连一次确认端口和实例名再来填连接串。这样可以过滤掉一半的“连接不了数据库”问题。3.2 一个够用的SqlHelper把连接、命令、参数封装在一起不引入第三方ORM我就会写一个精简的SqlHelper类把打开连接、执行命令、关闭连接封装成三个方法。这个类放在项目根目录下所有窗体共用一份。下面是实际用过的版本去掉了花哨的泛型保证新手也能看懂。using System; using System.Configuration; using System.Data; using System.Data.SqlClient; public class SqlHelper { private static readonly string connStr ConfigurationManager.ConnectionStrings[StudentDB].ConnectionString; // 执行增删改返回受影响行数 public static int ExecuteNonQuery(string sql, SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(connStr)) { using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } } // 执行查询返回SqlDataReader调用方负责关闭 public static SqlDataReader ExecuteReader(string sql, SqlParameter[] parameters) { SqlConnection conn new SqlConnection(connStr); SqlCommand cmd new SqlCommand(sql, conn); if (parameters ! null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteReader(CommandBehavior.CloseConnection); } // 执行查询返回第一行第一列的值常用于统计 public static object ExecuteScalar(string sql, SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(connStr)) { using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteScalar(); } } } }这段代码的逻辑说明using块保证连接对象用完后自动释放不需要手动Close()。ExecuteReader方法故意没有用using包连接而是返回SqlDataReader并加上CommandBehavior.CloseConnection这样调用方在关闭Reader时连接也会自动关闭避免“连接被占用”的经典报错。三个方法覆盖了查询、增删改、聚合统计三类操作整个学生选课及成绩查询系统的数据访问层用这三个方法就够了。参数说明里我要强调一点所有SQL语句里的变量必须通过SqlParameter传入不要用字符串拼接。比如查询学号时拼接写法是SELECT * FROM Student WHERE StudentNo txtStudentNo.Text 这个写法在文本框里输入 OR 11时会返回整张表的数据属于SQL注入。用SqlParameter后参数值被数据库视为纯数据不会参与SQL语法解析。这也是课设答辩时老师必问的点答上来很加分。3.3 把连接串放进App.config并加密为什么不能硬编码我看到太多课设代码里直接写Serverlocalhost;DatabaseStudentCourseDB;User IDsa;Password123写死在每个窗体里。这样改一次连接串要翻遍所有代码文件而且密码泄露在源码里。正确做法是把连接串统一放在App.config文件的connectionStrings节然后在代码里通过ConfigurationManager.ConnectionStrings[StudentDB]读取。连接串统一管理之后还有一个容易被忽略的点课设交源码时连接串里的服务器名和密码是开发机的评委老师在自己的机器上跑不起来。我习惯在App.config里留两个连接串一个叫StudentDB指向开发机一个叫StudentDB_Deploy指向发布环境发布前用#if DEBUG指令切换。这个习惯帮我免去了无数次“老师跑不起来”的尴尬。4. 核心业务实现选课事务、成绩联查与统计表结构和数据访问层就绪后开始写业务窗体。这一章落到选课按钮、成绩查询、统计排名三块代码。选课按钮的代码是整套系统的重头戏后台隐藏着事务边界和并发控制成绩查询则要体现多表关联的写法统计部分牵扯到“在SQL里算还是在C#里算”的选型判断。4.1 选课按钮背后的C#代码事务里先锁定、再判断、再插入界面设计不展开讲简单提一句控件命名。网上很多教程把按钮叫button1、文本框叫textbox1写到后面自己都分不清。C#控件命名简称建议统一按类型前缀走选课按钮叫btnSelectCourse学号文本框叫txtStudentNo班级下拉框叫cmbClass数据表格叫dgvScore。命名清晰的项目代码评审时观感会好很多。选课按钮的点击事件我通常会把上一章那条原子SQL封装成存储过程然后在C#里调用。下面是纯C#的实现版本方便看事务逻辑private void btnSelectCourse_Click(object sender, EventArgs e) { string studentNo txtStudentNo.Text.Trim(); string courseNo cmbCourse.SelectedValue.ToString(); string semester 2024-2025-1; string sql UPDATE Course WITH (UPDLOCK, HOLDLOCK) SET SelectedCount SelectedCount 1 WHERE CourseNo CourseNo AND SelectedCount Capacity; IF ROWCOUNT 1 BEGIN INSERT INTO SelectCourse (StudentNo, CourseNo, Semester) VALUES (StudentNo, CourseNo, Semester); SELECT N选课成功; END ELSE BEGIN SELECT N课程已满; END; SqlParameter[] paras { new SqlParameter(StudentNo, studentNo), new SqlParameter(CourseNo, courseNo), new SqlParameter(Semester, semester) }; string result SqlHelper.ExecuteScalar(sql, paras).ToString(); MessageBox.Show(result); }这段代码的逻辑说明SQL脚本里先执行可能成功的UPDATE再根据ROWCOUNT决定是否插入选课记录。ExecuteScalar拿到的是最后的SELECT结果也就是“选课成功”或“课程已满”的中文提示。这里没有显式地写BEGIN TRANSACTION因为UPDATE语句本身的锁会一直保持到整个批处理结束INSERT和UPDATE在同一个批次里数据库默认隐式事务足够保证一致性。参数说明Trim()方法去掉文本框首尾空格是必须的否则用户复制粘贴时带来一个空格学号就匹配不上。cmbCourse.SelectedValue依赖下拉框的ValueMember属性我在窗体加载时把课程表绑定到下拉框DisplayMember绑定CourseNameValueMember绑定CourseNo。如果运行时SelectedValue为null先检查是否在Form_Load里执行了绑定这个错误很常见。4.2 成绩查询三表JOIN把学号、姓名、课程、成绩一次取回成绩查询界面通常按学号或姓名搜索显示该学生所有课程的成绩。这个功能用JOIN查询一次取回不要在C#里先查选课表再循环查课程表那样会产生N1次数据库往返数据量大时界面会明显卡顿。标准查询如下SELECT sc.Semester, c.CourseNo, c.CourseName, sc.SelectTime, CASE WHEN s.ScoreValue IS NULL THEN N未录入 ELSE CAST(s.ScoreValue AS NVARCHAR(10)) END AS ScoreValue, CASE WHEN s.ScoreValue 60 THEN N通过 WHEN s.ScoreValue IS NULL THEN N待考试 ELSE N不及格 END AS Status FROM Student stu INNER JOIN SelectCourse sc ON stu.StudentNo sc.StudentNo INNER JOIN Course c ON sc.CourseNo c.CourseNo LEFT JOIN Score s ON sc.SelectID s.SelectID AND s.ExamType N期末 WHERE stu.StudentNo StudentNo ORDER BY sc.Semester DESC, c.CourseNo;这段SQL的逻辑说明Student和SelectCourse、Course是内连接保证只显示真实存在的选课记录Score用LEFT JOIN而不是INNER JOIN因为很多学生选完课还没考试成绩表里没有对应记录LEFT JOIN能让这些行也显示出来成绩列显示“未录入”。s.ExamType N期末这个条件放在JOIN的ON子句里而不是WHERE子句这样过滤只作用于成绩表不会把没有期末成绩的选课记录整行过滤掉。这是新手最容易写错的地方放到WHERE里结果会少很多行。参数说明StudentNo仍是SqlParameter传入。查询结果绑定到DataGridView时Status列是计算出来的不要存进数据库避免数据冗余。我这里只查了期末成绩如果要显示补考成绩可以在界面加一个“考试类型”下拉框把ExamType作为参数传进来。成绩查询功能是答辩时最容易演示的亮点多一个补考历史展示会让评委觉得系统考虑到了真实学业场景。4.3 统计、排名与补考标记在SQL里算还是在C#里算很多人在C#里用DataTable循环算平均分、排名用C#写排序算法。这个做法在数据量几千行时没问题但成绩统计通常伴随“按课程汇总平均分”“按班级排名”这类聚合操作直接在SQL里用窗口函数执行既省流量又省内存。下面这段SQL同时统计了每门课的平均分、最高分、不及格人数和每个学生在其课程内的排名SELECT c.CourseNo, c.CourseName, AVG(s.ScoreValue) AS AvgScore, MAX(s.ScoreValue) AS MaxScore, SUM(CASE WHEN s.ScoreValue 60 THEN 1 ELSE 0 END) AS FailCount, COUNT(s.ScoreID) AS TotalCount FROM Course c LEFT JOIN SelectCourse sc ON c.CourseNo sc.CourseNo LEFT JOIN Score s ON sc.SelectID s.SelectID AND s.ExamType N期末 GROUP BY c.CourseNo, c.CourseName ORDER BY AvgScore DESC;这段代码的逻辑说明LEFT JOIN保证没人选过的课程也出现在统计里平均分显示NULL而不是被直接忽略。SUM(CASE WHEN ...)是条件聚合的标准写法不用先把数据拉到C#再过滤。GROUP BY按课程分组统计维度是课程维度的。如果要看单个学生的班级排名可以在查询里加一个ROW_NUMBER() OVER (PARTITION BY ClassName ORDER BY ScoreValue DESC)窗口函数比在C#里排序要稳得多。参数说明里值得注意AVG函数会自动忽略NULL值所以没参加考试的学生成绩为NULL不会拉低平均分。如果业务上需要把缺考按0分计要在AVG里用ISNULL(s.ScoreValue, 0)这个逻辑要想清楚取决于学校是“缺考不纳入统计”还是“缺考计0分”。代码里我特意写了ExamType N期末避免把补考成绩混进平均分。5. 从开发到打包的避坑清单五个高频翻车点这一章是从真实交付经验里整理的踩坑记录每一条都对应一个具体现象。在整个系统的开发与源码打包过程中这些问题出现的频率最高有的甚至要在答辩现场才暴露。5.1 SQL Server版本混用附加数据库提示“版本不兼容”现象开发机用的SQL Server 2019生成数据库文件后发给同学对方用SQL Server 2008 R2附加弹出错误“数据库 StudentCourseDB 的版本为 904无法打开。此服务器支持 655 及更低版本”。原因是SQL Server数据库文件向后兼容、不向前兼容高版本建的库文件在低版本实例上打不开。解决确认目标运行环境后再决定用哪个版本开发。如果是课设交作业建议统一用SQL Server 2008 R2或2012建库脚本用第二章那套基础T-SQL不要用2019才支持的新语法。另一个办法是生成CREATE DATABASE脚本文件让对方在自己机器上执行脚本重建数据库而不是直接拷贝.mdf文件。发布时我也习惯同时准备“脚本建库”和“附加数据库”两种方式后者适合评委现场演示前者适合对方自己安装环境。5.2 连接串Server名写错明明装了SQL Server却连不上现象代码里写Serverlocalhost本机也装了SQL Server但运行时一直报“建立到服务器的连接时发生错误”。原因多半是装的是SQL Server Express默认实例名是.\SQLEXPRESS而不是默认实例MSSQLSERVER。写成localhost时客户端去连接默认实例的1433端口而Express默认实例的监听可能没开或者实例名压根不是默认的。解决打开“SQL Server配置管理器”查看“SQL Server服务”里的实例名。如果是SQL Server (SQLEXPRESS)连接串写localhost\SQLEXPRESS。配置好后用Navicat for SQL Server试连一次能连上再回Visual Studio里改连接串。开发阶段我还会在App.config旁边放一个connection_test.txt记录当前机器的实例名和验证方式方便换机器时快速定位。5.3 选课并发“超选”已选人数字段在并发下被覆盖现象课程容量60人数据库里SelectedCount变成了62但SelectCourse表里真的有62条记录。原因是“先查后插”逻辑被多个事务同时执行读出SelectedCount59后都执行插入没有锁保护。这与操作系统的“竞态条件”是同一类问题只是规模小。解决把容量判断改成第二章的原子UPDATE方案。注意UPDATE Course WITH (UPDLOCK, HOLDLOCK)这行代码不能省两个锁提示缺一不可。UPDLOCK防止两个读操作同时拿到共享锁HOLDLOCK防止行锁提前释放。我自己的测试方法是开一个C#控制台项目用Parallel.For模拟50个线程同时选同一门课验证最终选课记录数和SelectedCount一致。5.4 中文乱码插入姓名变成“??”或“鏉庢槑”现象界面上文本框输入中文点保存后数据表里显示“??”或者看起来像乱码。原因有两个数据库排序规则不是中文相关或者SQL语句里中文字符串没有加N前缀。前者导致数据库层面无法正确存储中文后者导致SQL语句里的中文字面量被按非Unicode代码页解释。解决建库时排序规则指定Chinese_PRC_CI_AS字段类型统一用NVARCHAR/NCHAR而不是VARCHAR/CHAR。SQL脚本里所有中文都写成N中文格式。C#中传参时SqlParameter不受这个限制因为参数本身是NVarChar类型。排查时先查数据库排序规则再查字段类型最后查SQL脚本按这个顺序能快速定位。5.5 源码打包后连不上库换了机器数据库附属文件找不到现象项目在本机运行正常把整个解决方案文件拷到另一台电脑打开程序后连接数据库失败。原因多数是连接串里的Serverlocalhost\SQLEXPRESS是指向开发机的实例对方电脑上没有这个实例名或者.mdf文件没有放到对方机器上。解决交付源码包时我会在“发布说明.txt”里写清三件事要求对方安装SQL Server Express或者提供安装教程链接提供CreateDatabase.sql脚本说明连接串修改位置。如果对方电脑已经装了完整的SQL Server装了Navicat等工具可以附加数据库。如果环境不乐观把数据库文件放到项目App_Data目录下使用AttachDbFilename方式连接这是唯一的“免安装”方案但性能略差只推荐演示用。6. 进阶收尾用验证数据说服自己也说服答辩老师系统做完之后很多人直接拿几条测试数据就交了结果答辩时老师输入一个不存在的学号、选一门已经满员的课程序直接报错。我的习惯是写一套验证脚本用批量数据把系统的边界都测一遍再把测试过程截图放进设计文档。这个动作能让系统从“看起来能用”变成“确实经得起问”。首先生成一套仿真数据50名学生、10门课选课记录分散在不同学期成绩覆盖0到100分特意包含几条不及格和缺考记录。然后用三段验证SQL确认数据完整性检查有没有重复选课检查成绩有没有越界检查有没有选课记录但课程容量溢出。-- 验证1同一学期同一门课是否重复选课 SELECT StudentNo, CourseNo, Semester, COUNT(*) FROM SelectCourse GROUP BY StudentNo, CourseNo, Semester HAVING COUNT(*) 1; -- 验证2成绩是否超出合法范围 SELECT ScoreValue, COUNT(*) FROM Score WHERE ScoreValue 0 OR ScoreValue 100 GROUP BY ScoreValue; -- 验证3已选人数大于容量的课程 SELECT CourseNo, CourseName, Capacity, SelectedCount FROM Course WHERE SelectedCount Capacity;三段脚本全部返回0行说明系统的基础约束生效。最后我把选课按钮的并发测试结果截图附在“系统测试”一节里答辩时老师一看到50个并发线程抢1个名额的结果就知道这不是临时拼接的演示项目。这段经历也是我的个人教训最开始我也只写数据访问层和界面不管验证后来一份源码被学弟拿去演示正好遇到“课程容量55人但选了57人”的问题当场翻车。从那以后凡是要交出去的源码我都先跑验证脚本再打包。希望这套流程对你有用多做一次验证少一次现场尴尬。本文还有配套的精品资源点击获取
返回列表