C# + SQL Server选课系统源码剖析:从DataSet缓存到事务并发控制
简介一套基于C#与SQL Server数据库的学生选课及成绩查询管理系统是典型的课程设计/毕业设计级桌面应用源码面向计算机专业学生、初入职开发者及教学实训场景。系统覆盖学生信息管理、课程设置、选课冲突处理、成绩录入与多条件查询、权限控制、报表生成等模块通过ADO.NET完成前端与数据库交互可帮助理解C# WinForms开发以及SQL Server表结构设计的完整思路。压缩包共123个文件约4.15MB核心为44个.cs源码文件、20个.resx界面资源及Designer文件并附带SQL脚本、数据库文件.mdf/.ldf、可运行的.exe程序和项目配置文件打开.sln即可直接编译调试。已有366人学习下载。内含完整Visual Studio解决方案与数据库附加说明适合参考其分层结构、界面事件编写和SQL语句组织也可作为课程设计答辩、实训报告撰写或小型信息管理系统的开发模板。1. 从“能跑”到“能改”这套C# SQL Server选课源码值不值得拆每次接到这类带数据库的选课系统最先翻车的多半不是SQL而是界面和数据源之间的缓存不同步。这套基于C#与SQL Server的学生选课及成绩查询管理系统外观是标准WinForms风格真正值得下手的是它的数据组织方式通过DataSet和DataAdapter把学生表、课程表、选课表、成绩表拆成可独立刷新的对象再配合app.config里的连接串完成增删改查。它解决的是一类很实际的诉求管理员要维护学生和课程学生要选课和查分教师要录入和修改成绩三者共用一套数据库但操作边界不同。适合两类人一类是正在做Windows课设、需要完整业务闭环的学生另一类是刚接手别人代码、想在短时间里把业务逻辑和数据访问摸清的开发者。别被那几个Designer.cs和cache文件吓住它们看着像杂物其实把界面绑定关系都藏在里面。2. 源码骨架DataSet缓存链路、连接配置与三层事务边界2.1 先看清文件清单Designer.cs、cache 与可执行配置拿到压缩包后大部分人习惯直接找“数据库脚本.sql”但这份资源里没有一个独立的建表脚本文件建表逻辑是通过两个DataSet设计器文件studentDataSet.Designer.cs、studentDataSet1.Designer.cs和窗体设计器文件addstuForm.Designer.cs、modifystuForm.Designer.cs共同定义的。这是一种看起来绕、实际很常见的做法把表结构、约束和查询适配器全部声明在强类型DataSet里运行时会自动生成对应的SQL语句去SQL Server里执行。ResolveAssemblyReference.cache、DesignTimeResolveAssemblyReferences.cache这类文件是VS在编译时留下的中间产物它们不属于业务代码但通过它们能判断项目当前的目标框架版本和引用状态。初次打开项目时如果VS提示“需要重定向”或“引用丢失”优先怀疑这些cache文件在传输过程中被压缩工具改动了时间戳导致VS重新解析引用。项目根目录下的配置文件主要有两个app.config是设计期的配置test1.vshost.exe.config和test1.exe.config是编译后实际读取的配置。很多人只改了app.config运行程序却发现连接串没生效原因就在这里程序启动时读取的是bin目录下的.exe.config而不是源码目录里的app.config。Visual Studio会在每次生成时把app.config复制成输出文件夹里的config文件但如果输出目录里已经存在同名文件且内容不一致可能不会覆盖。改完配置后建议手动删除bin目录下所有config文件再重新生成。注意连接串尽量只维护app.config这一处不要同时改多个config文件否则排查连接失败时会分不清程序到底读了哪个。2.2 ADO.NET断线模型DataAdapter为什么比“连一次查一次”更适合这套系统使用的数据访问模型属于典型的ADO.NET断线数据集模式。它的工作流程是先用SqlConnection打开连接用SqlDataAdapter执行Select语句把结果填充到内存里的DataTable然后立刻关闭连接。界面上的Grid、下拉框、文本框都绑定到内存表用户增删改只是在内存里操作最后通过DataAdapter的Update方法把差异写回数据库。这种“先取后改、集中提交”的设计对选课场景是合理的。选课列表、成绩列表都是只读为主的数据没必要在用户滚动时反复往返数据库断线缓存能大幅减少连接占用也让界面操作不卡顿。它在低并发的教务系统里是非常成熟的选择缺点是多人同时选课时会出现“脏数据”——两个客户端都读到剩余容量为1同时提交选课数据库里可能就超员了。这一点后面会专门讲怎么兜底。源码里对应的核心类是studentDataSet和studentDataSet1这两个名字看起来像重复实际是两套不同的表结构。studentDataSet更可能承载学生基本信息相关的查询studentDataSet1承载选课记录与成绩关联查询。新增学生和修改学生分别放在了addstuForm和modifystuForm两个窗体里说明开发者在界面层刻意做了操作隔离避免在同一页面上混淆“新增”和“编辑”两种状态。2.3 连接串怎么改才不会崩app.config与SQL Server认证参数连接串是这个项目里第一个会踩坑的地方。常见的写法是Windows身份验证和SQL Server身份验证混用导致在两台机器上表现不一致。以下是这个场景下最稳的连接串模板connectionStrings add nameConnectionString connectionStringData Source127.0.0.1,1433;Initial CatalogStudentDB;User IDsa;PasswordYourStrongPassword;Persist Security InfoTrue;Connect Timeout15;PoolingTrue;Min Pool Size1;Max Pool Size100;Application NameStudentSelectSystem; providerNameSystem.Data.SqlClient / /connectionStrings参数说明Data Source里如果写成“.”或“localhost”且不开SQL Browser服务在局域网其他机器上会连不上建议直接用IP加逗号端口。Initial Catalog是数据库名要与SQL Server里实际建立的那个库保持一致。Persist Security InfoTrue表示在连接字符串被获取时仍保留密码信息如果不需要这套源码里“从连接串取密码做再校验”的逻辑改成False更安全。Connect Timeout给15秒比较合适太短时数据库恢复慢会误报超时太长时排查网络故障要干等半天。Pooling可以保持开启选课系统这类低并发场景的连接复用收益明显。如果运行时报“用户登录失败”先检查SQL Server是否启用了混合认证模式。这个选项不在代码里在SQL Server实例属性的“安全性”页里必须改成“SQL Server和Windows身份验证模式”并重启服务。之后在代码里调用一个最简单的测试查询using (SqlConnection conn new SqlConnection(ConfigurationManager.ConnectionStrings[ConnectionString].ConnectionString)) { conn.Open(); using (SqlCommand cmd new SqlCommand(SELECT 1, conn)) { int result (int)cmd.ExecuteScalar(); if (result 1) { MessageBox.Show(数据库连接正常); } } }这段代码用“SELECT 1”做连通性测试不依赖任何业务表是排查连接问题时第一件该做的事。如果这段能通但你业务页面报错问题就在SQL语句或表结构如果这段都通不了问题就在连接串、防火墙或SQL Server认证配置。2.4 三层事务边界业务层不该把SQL堆在按钮事件里这套源码里很多增删改查操作直接写在了窗体的按钮事件方法中这是课设项目的普遍写法优点是能跑缺点是业务规则没法复用。比如“新增学生”的校验逻辑如果只在addstuForm里写了一遍以后改从Excel导入学生时就得重写一份。更稳妥的组织方式是至少拆出数据访问层和业务层。数据访问层负责SQL语句执行业务层负责参数校验和事务控制。一个典型的“新增学生并创建初始选课账号”的操作按事务边界拆应该是这样public bool AddStudentWithAccount(string studentNo, string studentName, string className) { SqlConnection conn new SqlConnection(_connectionString); SqlTransaction tx null; try { conn.Open(); tx conn.BeginTransaction(AddStudentTx); string insertStudentSql INSERT INTO Student (StudentNo, StudentName, ClassName) VALUES (StudentNo, StudentName, ClassName); using (SqlCommand cmd new SqlCommand(insertStudentSql, conn, tx)) { cmd.Parameters.AddWithValue(StudentNo, studentNo); cmd.Parameters.AddWithValue(StudentName, studentName); cmd.Parameters.AddWithValue(ClassName, className); cmd.ExecuteNonQuery(); } string insertAccountSql INSERT INTO UserAccount (StudentNo, UserName, UserPassword, RoleId) VALUES (StudentNo, StudentNo, 123456, 3); using (SqlCommand cmd new SqlCommand(insertAccountSql, conn, tx)) { cmd.Parameters.AddWithValue(StudentNo, studentNo); cmd.ExecuteNonQuery(); } tx.Commit(); return true; } catch { tx.Rollback(); throw; } finally { conn.Close(); } }这段代码把“学生建档”和“登录账号初始化”放在一个事务里避免出现学生信息插好了、账号没建成的情况。AddWithValue的写法简洁但要注意它推断出来的SqlDbType可能和数据库列类型不匹配遇到日期和金额字段时显式指定DbType更稳。事务的边界设在整个业务动作上而不是每个SQL语句上这是三层拆分的关键。3. 选课与成绩查询以学生表为主线的改课、加课与权限边界3.1 学生管理增删改查的参数化写法学生管理模块包含学生基本信息的录入、修改和删除以及选课状态的跟踪。源码里的addstuForm和modifystuForm分别承担新增与编辑这本身就是一种状态分离设计。处理“编辑学生信息”时最容易出现的问题是把学号主键一起更新导致选课记录的外键跟着错乱。学号一旦生成应该作为不可变字段处理修改界面只允许改姓名、班级、性别这类属性。string updateSql UPDATE Student SET StudentName StudentName, Gender Gender, ClassName ClassName WHERE StudentNo StudentNo; using (SqlCommand cmd new SqlCommand(updateSql, conn)) { cmd.Parameters.AddWithValue(StudentName, txtName.Text.Trim()); cmd.Parameters.AddWithValue(Gender, cmbGender.SelectedItem.ToString()); cmd.Parameters.AddWithValue(ClassName, txtClass.Text.Trim()); cmd.Parameters.AddWithValue(StudentNo, originalStudentNo); cmd.ExecuteNonQuery(); }重点看WHERE条件里的originalStudentNo它来自列表窗体中选中行保存的原始学号而不是文本框里可能被修改的值。很多翻车现场就是把文本框内容当主键条件结果学号被误改后数据库里出现“查不到”或“更新了多行”的怪问题。Update语句成功执行后要把受影响行数反馈给用户当ExecuteNonQuery返回0时意味着界面显示的数据和数据库已经不一致此时应该重新查询刷新列表。删除学生的场景要更谨慎。如果学生已经产生选课记录直接执行DELETE FROM Student会因外键约束失败或者强行级联删除导致成绩记录丢失。常见的解决策略是逻辑删除给Student表增加IsDeleted字段删除时执行UPDATE而不是DELETE。-- 推荐做法逻辑删除保留历史选课数据 UPDATE Student SET IsDeleted 1 WHERE StudentNo StudentNo; -- 不推荐物理删除除非确认没有关联数据 -- DELETE FROM Student WHERE StudentNo StudentNo;逻辑删除的好处是成绩查询功能还能看到历史学生信息坏处是所有查询列表都要额外加一个“WHERE IsDeleted 0”过滤条件。如果源码里没有这个字段可以在所有涉及学生列表的Select语句中补齐。物理删除虽然能节省存储但会让往届学生的成绩查询变成空白在教学管理场景里属于不可接受的数据丢失。3.2 选课冲突与容量控制先查后插仍需唯一约束兜底选课功能是这个系统里最需要强调事务和约束的地方。学生查看课程列表点击选课按钮代码通常先检查该课程是否还有剩余容量再检查是否重复选课通过后执行插入。这套“先查后插”的逻辑在单机演示时完美运行但在多用户同时提交时存在明显漏洞——两次查询剩余容量都看到1双方同时插入课程就超员了。解决思路分两层。第一层是在业务代码里保证事务一致性第二层是在数据库层面加上硬约束。第一层的选课插入操作应该包裹在事务中并且在查询剩余容量时使用UPDLOCK行锁提示string checkAndInsertSql BEGIN TRANSACTION; DECLARE SelectedCount INT; SELECT SelectedCount COUNT(*) FROM SelectionRecord WITH (UPDLOCK, HOLDLOCK) WHERE CourseId CourseId; IF SelectedCount Capacity BEGIN INSERT INTO SelectionRecord (StudentNo, CourseId, SelectTime) VALUES (StudentNo, CourseId, GETDATE()); COMMIT TRANSACTION; SELECT SUCCESS AS Result; END ELSE BEGIN ROLLBACK TRANSACTION; SELECT FAILED AS Result; END;这段SQL把查询和插入放在同一个事务里UPDLOCK加上HOLDLOCK让并发选课变成串行处理当一个事务还没结束时另一个事务的COUNT会阻塞等待。这是应对选课高峰最直接的方案。但依赖锁提示只能解决运行时竞争还要考虑数据的天然防重。第二层是在SelectionRecord表上建立联合唯一约束确保同一个学生不能重复选同一门课ALTER TABLE SelectionRecord ADD CONSTRAINT UQ_StudentCourse UNIQUE (StudentNo, CourseId);有了这个约束即使业务代码里的重复选课检查因并发被绕过数据库也会拒绝第二条记录。此时程序会抛出SqlException错误码2601表示唯一索引冲突。需要在catch块里先判断错误编号再向用户提示“你已经选过这门课”而不是直接弹出“数据库错误”。3.3 成绩查询按学期、课程、学号三种查询的组合条件拼法成绩查询模块要求支持按学期、课程、学生三个维度组合查询。源码里常见的实现方式是三个文本框配一个查询按钮SQL语句在C#里拼接。规范做法是使用参数化条件拼接不让用户输入直接插入SQL字符串。string sql SELECT s.StudentNo, s.StudentName, c.CourseName, sc.Score FROM SelectionRecord sc INNER JOIN Student s ON sc.StudentNo s.StudentNo INNER JOIN Course c ON sc.CourseId c.CourseId WHERE 11; ListSqlParameter parameters new ListSqlParameter(); if (!string.IsNullOrEmpty(txtSemester.Text.Trim())) { sql AND sc.Semester Semester; parameters.Add(new SqlParameter(Semester, txtSemester.Text.Trim())); } if (!string.IsNullOrEmpty(txtCourseName.Text.Trim())) { sql AND c.CourseName LIKE CourseName; parameters.Add(new SqlParameter(CourseName, % txtCourseName.Text.Trim() %)); } if (!string.IsNullOrEmpty(txtStudentNo.Text.Trim())) { sql AND s.StudentNo StudentNo; parameters.Add(new SqlParameter(StudentNo, txtStudentNo.Text.Trim())); }WHERE 11是典型的动态条件拼法它本身没有性能代价目的是让后续每个AND都可以直接追加不用判断要不要加WHERE关键字。LIKE查询只对课程名称这类短文本使用不要在学号上使用LIKE否则会把S0012、S00123这种相近学号混淆。另外注意SqlParameter的变量名和SQL里的变量名必须严格一致包括大小写这一点最玄学经常有人SQL里写StudentNo代码里New SqlParameter(StudentNo)少了符号运行直接报“必须声明标量变量”。3.4 权限边界学生只能看自己教师能改全班系统安全模块要求教师可以查看和修改所有成绩学生只能查看自己的成绩。这类权限控制要避免把判断逻辑只写在按钮事件里。比如学生窗体的“查询成绩”按钮代码是SELECT成绩如果用户绕过界面直接调用某个公开方法一样能拿到别人成绩。一个更可靠的方案是在数据访问层强制加入身份过滤。查询成绩的方法统一接收一个userRole参数和currentUserId参数在SQL内部根据角色决定是否拼接过滤条件string sql SELECT s.StudentNo, c.CourseName, sc.Score FROM SelectionRecord sc INNER JOIN Student s ON sc.StudentNo s.StudentNo INNER JOIN Course c ON sc.CourseId c.CourseId WHERE 11; if (userRole Student) { sql AND s.StudentNo CurrentStudentNo; parameters.Add(new SqlParameter(CurrentStudentNo, currentUserId)); }这样做的好处是权限边界在数据库查询层就生效而不是依赖窗体控件的Enabled属性。界面上隐藏按钮只能防住老实人防不了直接调用业务类的人。源码里如果现成的权限逻辑比较弱接手后第一优先级就是这个改造点。登录用户的具体身份标志建议存在当前会话的公共静态类里不要每次使用都去数据库查权限表否则每个操作都多一次往返。4. 避坑DataSet缓存与连接串相关的五个高频翻车点4.1 改完数据不刷新表没Fill就直接往界面里绑定现象程序启动后DataGridView能显示数据。用SQL Server Management Studio往数据库里插了一条新记录回到程序点“刷新”列表还是旧数据。关闭程序重新启动新数据才出现。更诡异的是有时候明明在程序里新增了一条学生记录列表里显示成功但数据库明细里查不到。原因这个项目的DataGrid绑定的是内存DataSet里的DataTable。刷新按钮的执行逻辑可能只调用了Clear再重新绑定但没有重新执行DataAdapter.Fill。调用Fill之后DataAdapter只把查询结果作为新行追加到现有表而不是先清空原有行所以界面看起来“没变”。新增记录查不到是因为数据在内存表里还没执行Update程序直接关掉了等于主动把修改丢弃。解决刷新前先清除内存表行再重新Fill。关键步骤如下dataTable.Rows.Clear(); sqlDataAdapter.Fill(dataTable);如果项目用的是强类型DataSet学生管理这类系统大概率是刷新函数名称一般是tableAdapter.Fill(数据集.表名)。在刷新时会话里务必确认是先Clear再Fill而不是反着来。另外新增和修改操作执行完后要手动调用tableAdapter.Update(数据集.表名)把内存改动写回数据库这一步不会自动发生很多人就是漏了它才出现“明明显示成功、数据库里空无一物”的现象。注意每次做INSERT后如果界面想要立即显示自动生成的编号执行完Update后必须再Fill一次不能直接把内存表的某列当自增值用。4.2 唯一约束报错但程序没捕获SqlException被吞了现象同一学生重复选同一门课程序直接崩溃报错信息显示“违反UNIQUE KEY约束”但代码里明明写了try/catch却没有任何友好提示。原因很多课设项目把try/catch写在了按钮事件里但catch块只捕获了Exception类型或者干脆catch后什么都没写直接return。SqlException是Exception的子类理论上能接住问题通常出在catch块里又访问了已经释放的窗体控件或者在finally里把数据表清空了导致真实的异常原因还没展示就被第二个异常覆盖。解决捕获异常后先判断错误编号再给用户明确提示。唯一索引冲突的错误编号是2601主键冲突是2627。示例写法catch (SqlException ex) { if (ex.Number 2601) { MessageBox.Show(选课失败你已经选过这门课程, 提示); } else if (ex.Number 2627) { MessageBox.Show(数据冲突存在相同编号的记录, 提示); } else { MessageBox.Show(ex.Message, 数据库错误); } }显示错误编号还有一个好处当程序弹出一个莫名其妙的错误信息时拿编号去检索能快速定位是哪一类问题不用看整段英文提示。对选课系统来说2601和2627是最可能出现的两个编号提前处理能让用户感知到的故障明显减少。4.3 SQL Server端口不是默认的1433时连不上现象本地测试一切正常部署到另一台机器上程序启动时报“在建立与服务器的连接时出错。在连接到SQL Server时在默认的设置下SQL Server不允许进行远程连接”。原因连接串里只写了“Data Source服务器IP;Initial CatalogStudentDB;User IDsa;Passwordxxx”没有写明端口。SQL Server默认监听1433端口但很多部署环境的数据库为了安全被改成了其他端口或者开启了多个实例非默认实例的动态端口映射没有被SQL Browser服务转发客户端无法定位。解决在IP后加逗号和端口号查看SQL Server配置管理器里TCP/IP协议的端口设置。如果端口是14333连接串应该写成add nameConnectionString connectionStringData Source192.168.1.100,14333;Initial CatalogStudentDB;User IDsa;Passwordxxx;Connect Timeout15; providerNameSystem.Data.SqlClient /这一处踩坑的坑点在于打开SQL Server Management Studio时用的是默认端口所以本地测试永远正常发布到服务器后SSMS已经预配好连接也不容易发现端口变化。真正发现问题的是终端用户的客户端程序。接手的同学最容易在这里卡上一整天用SSMS能连、程序连不上非常玄学。先查端口再查防火墙规则挨个排。4.4 vshost.exe.config与项目配置不同步导致连接串改了白改现象改完app.config里的连接串重新生成项目F5运行程序依然连旧数据库。原因Visual Studio宿主进程vshost在调试模式下会缓存上一次编译运行时的配置bin目录里生成的.config文件没有随着app.config更新或者ProjectName.vshost.exe.config残留了旧连接串。官方机制是app.config在编译后应复制为exe.config但存在缓存时程序可能从vshost文件里读取了老内容。解决在项目中手动重新生成所有.config文件。# 在解决方案目录下执行清理命令 dotnet clean # 删除输出目录里所有config文件 del bin\Debug\*.config # 重新生成解决方案 dotnet build -c Debug如果不习惯命令行直接在VS的“生成”菜单里选择“清理解决方案”再“重新生成解决方案”。确认bin目录下的exe.config内容和app.config一致后再按F5运行。新建一个Windows窗体测试项目验证系统配置更新是刚需操作调一次能省掉后续大量排查时间。4.5 新增和编辑共用窗体导致的绑定错乱现象点“新增”弹出窗体是空白点“修改”弹出窗体正常或者先点修改再点新增新增窗体的文本框里残留着上一条数据的值。原因这是典型的新增/编辑没有做状态清理。窗体加载时Load事件里检查的是数据集里的当前行点修改时当前行被选中了窗体能填充数据点新增时数据集里的当前行还停留在上一次选中的行直接把它的字段绑定到了临时修改窗体的控件上看起来就像残留数据。解决新增模式必须显式清空当前编辑状态。在addstuForm打开时不加载任何现有数据行的内容构造方法里传入一个标识参数区分模式public partial class StudentEditForm : Form { private readonly string _editMode; private readonly string _studentNo; public StudentEditForm(string mode, string studentNo null) { InitializeComponent(); _editMode mode; _studentNo studentNo; if (mode Add) { txtStudentNo.ReadOnly false; txtStudentNo.Text ; txtName.Text ; cmbGender.SelectedIndex -1; } else if (mode Edit) { txtStudentNo.ReadOnly true; LoadStudentData(_studentNo); } } }新增模式里把主键文本框设置为可写编辑模式里设置为只读这是最简单有效的防错区分。如果在同一个窗体里做两种操作每次关闭窗体或者切换模式时所有控件的DataBindings必须全部清空重建。BindingSource的Filter属性和DataSource的切换顺序也很容易翻车建议优先使用分离窗体方案而不是共用一个窗体强行区分。5. 进阶把断线缓存换掉亲手把登录和选课改成可复核版本这套源码的架构底子是“快、能用、不折腾”真要让它在模拟部署环境里留下可信记录最简单的进阶改造是把登录校验改成显式执行不再靠DataSet的隐式表查找。登录功能的可复核性直接关系到成绩查询权限边界值得单独拿出来打磨。第一步统一SQL执行入口。写一个静态方法处理所有参数化SqlCommand让每次数据库操作都生成日志记录这样登录时间、选课时间、成绩修改时间都能被回溯。按以下思路封装基础方法public static int ExecuteNonQueryWithLog(string sql, string connectionString, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(connectionString)) using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } conn.Open(); int affectedRows cmd.ExecuteNonQuery(); LogOperation(sql, parameters, affectedRows); return affectedRows; } }参数说明这套封装把SQL和参数都传进日志函数要求日志里不能记录密码值所以对参数做输出时要过滤包含Password、Pwd字样的参数。登录语句使用标准的输密码校验cmd.CommandType CommandType.StoredProcedure; // 使用存储过程的好处是数据库端可以统一记录登录日志 // 而业务代码不需要再单独拼接字符串第二步选课逻辑里引入“先锁后查”的硬事务。即使源码已经用DataSet做了断线缓存选课提交也必须走即时连接事务否则两个客户端同时选课时一切缓存模型都会放大竞态窗口。把选课写入操作整体迁到一个存储过程中让SQL Server承担并发控制比任何C#代码级别的加锁都可靠。CREATE PROCEDURE sp_EnrollCourse StudentNo NVARCHAR(20), CourseId INT, Capacity INT, Result INT OUTPUT AS BEGIN SET NOCOUNT ON; BEGIN TRANSACTION; DECLARE CurrentCount INT; SELECT CurrentCount COUNT(*) FROM SelectionRecord WITH (UPDLOCK, HOLDLOCK) WHERE CourseId CourseId; IF CurrentCount Capacity BEGIN SET Result -1; -- 已满 ROLLBACK TRANSACTION; RETURN; END IF EXISTS (SELECT 1 FROM SelectionRecord WHERE StudentNo StudentNo AND CourseId CourseId) BEGIN SET Result -2; -- 重复选课 ROLLBACK TRANSACTION; RETURN; END INSERT INTO SelectionRecord (StudentNo, CourseId, SelectTime) VALUES (StudentNo, CourseId, GETDATE()); COMMIT TRANSACTION; SET Result 0; -- 成功 END存储过程把业务约束全部放到了数据库端C#端只需要调用并解析返回值。参数化调用时OUTPUT参数需要设置Direction属性SqlParameter resultParam new SqlParameter(Result, SqlDbType.Int); resultParam.Direction ParameterDirection.Output;这种改造完成后代码里的一切选课操作都走同一个审核流程数据库日志里也能看到每次提交的学生学号、课程ID和时间。从那以后我每次接手这类带选课和成绩的系统都会强制自己走一遍这个流程先改登录校验再封装SQL执行入口最后把选课逻辑迁进存储过程。这三个动作做完不管原来代码多乱至少运行链路是黑的变白的中间任何一个环节出错日志都能直接指到具体语句。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

MATLAB实现多模型集成BMA:EM算法与偏差校正详解

MATLAB实现多模型集成BMA:EM算法与偏差校正详解

简介:这是一套基于MATLAB实现的多模型集成(BMA)方法资源,面向气象、环境、水文等领域的科研人员与中高级学习者,旨在解决单一模型预测不确定性高、精度不稳定等问题。资源以贝叶斯模型平均为理论内核,在备选…

2026/10/18 11:08:57 阅读更多 →
全网最新Claude Code 使用教程:对接DeepSeek-R1和Kimi K2,TaoToken统一Key配置实战

全网最新Claude Code 使用教程:对接DeepSeek-R1和Kimi K2,TaoToken统一Key配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 11:08:57 阅读更多 →
微信小程序+Java会议室管理系统毕业设计实战

微信小程序+Java会议室管理系统毕业设计实战

简介:本资源是一套完整的Java毕业设计项目——基于微信小程序的软件学院会议室管理系统,面向计算机专业本科生及Java全栈初学者,解决企业或高校中会议室预约冲突、资源浪费与管理低效等实际问题。系统采用前后端分离架构,后端以Sp…

2026/10/18 11:08:57 阅读更多 →

最新新闻

快鹭办公、通达OA、奥哲、360织语怎么选:关注上手成本与日常易用性的对比横评

快鹭办公、通达OA、奥哲、360织语怎么选:关注上手成本与日常易用性的对比横评

从上手成本和日常易用性出发,一站式 AI 智能办公平台选哪个,关键看搭建入口是否降低门槛、日常操作是否集中、以及上线到习惯的迁移代价。本文围绕这条主线,比较快鹭办公、蓝信、360织语、奥哲、通达OA 五个同产品线候选:快鹭办公…

2026/10/18 13:44:05 阅读更多 →
Rust泛型算法库实战:契约设计、IntoIterator抽象与const泛型

Rust泛型算法库实战:契约设计、IntoIterator抽象与const泛型

简介:本资源是一份面向Rust中高级开发者与系统编程学习者的实战型技术文档,聚焦泛型编程核心能力培养,解决通用算法库设计中类型安全、复用性与性能兼顾的典型难题。文档完整覆盖泛型函数/结构体、trait约束、关联类型、生命周期注解、错误处…

2026/10/18 13:44:05 阅读更多 →
MySQL 主从复制报错 1133:从库缺少 repl 用户导致 SQL 线程中断的排查与修复

MySQL 主从复制报错 1133:从库缺少 repl 用户导致 SQL 线程中断的排查与修复

一、问题现象当前这个从库的 SQL 线程已经断了,原因是回放主库传来的这条语句时报错GRANT REPLICATION SLAVE ON *.* TO repl%报错Last_Errno: 1133Last_Error: Cant find any matching row in the user table说明从库的 mysql.user 里没有 repl% 这行用户记录&…

2026/10/18 13:44:05 阅读更多 →
变量化提示词:用占位符实现提示词的灵活复用

变量化提示词:用占位符实现提示词的灵活复用

在上一篇文章中,我们讨论了提示词模板引擎的整体架构。今天,我们要深入其中最基础也最实用的核心技术——变量化提示词。掌握它,你的提示词将从"一次性用品"变成"可复用的资产"。这就像编程中从硬编码到使用变量的飞跃—…

2026/10/18 13:44:05 阅读更多 →
电信BOSS系统接口契约规范:字段语义、同步机制与错误降级

电信BOSS系统接口契约规范:字段语义、同步机制与错误降级

简介:本资源是中国移动通信集团公司于2004年9月发布的《经营分析系统与BOSS系统互动技术规范》(企业标准QB-╳╳-╳╳╳-╳╳╳╳,V1.0.0)正式文档,面向电信行业系统架构师、BOSS/经营分析系统开发与集成工程师&#x…

2026/10/18 13:44:05 阅读更多 →
Python属性拦截四钩子:__setattr__、__getattr__、__delattr__与__call__实战

Python属性拦截四钩子:__setattr__、__getattr__、__delattr__与__call__实战

简介:这份PDF资料面向已掌握Python基础、希望深入理解面向对象机制的开发者,系统讲解__setattr__、 getattr 、__delattr__与__call__四个魔法方法的用法。内容从属性设置、属性读取、属性删除到实例可调用四个维度展开,每个方法均配有可运…

2026/10/18 13:43:05 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →