ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C# WinForms+SQL Server外聘教师管理系统:档案、课时、结算全流程

C# WinForms+SQL Server外聘教师管理系统:档案、课时、结算全流程

简介:这是一套基于C#与SQL Server开发的外聘教师管理系统完整源码,面向需要完成课程设计或毕业设计的计算机专业学生,以及需要快速搭建人事管理原型的中小院校。系统覆盖外聘教师信息维护、多条件查询、按代课数与级别核算工资、系部课程等基础数据管理,并包含登录权限与密码修改等系统管理功能,业务模块划分清晰。压缩包内共97个文件,核心包括26个C#窗体与逻辑代码文件、10个资源文件、10个resx界面布局文件,以及可直接附加使用的SQL2012数据库(MDF/LDF);同时附带exe、dll、config等运行与配置文件,大小约35.47MB,解压后修改数据库连接字符串即可运行。已有437人学习下载,适合用于参考外聘教师场景下的增删改查、统计查询与工资计算实现。资源内置多张界面截图和完整项目结构,便于对照运行效果与二次开发。

1. 外聘教师管理系统:C#+SQL组合的完整业务闭环

外聘教师管理系统这个项目,第一眼看上去就是常规的C#管理软件,但真正部署过的人都知道它卡在哪:外聘教师的档案散在Excel里,课时费月底靠人肉核对,合同什么时候到期全凭记忆。这套系统用C# WinForms做客户端界面和业务控制,用SQL Server做数据持久化,把教师档案、任课课程、课时记录、课时费结算和合同到期提醒拧到一条业务链上。登录进去,管理员能看到哪些教师近期合同到期,教务员能按月份录课时、发起结算,外聘教师的数据不再是几张孤立的表,而是一整套从录档案到算钱的流水线。

适合两类人:一类是刚学完C#和SQL、手头缺完整工程练手的开发者,能一次看全登录权限、分页查询、存储过程计算这些常用功能;另一类是负责教务信息化的实施人员,拿这套源码改改就能给内部快速落一套外聘教师管理流程。工程规模不大,代码量适中,胜在结构清楚,没有绕弯的框架,每一层干的事都很直白,这也是它适合拿来读源码和二次改造的原因。

2. 技术选型与工程结构:为什么是C# WinForms搭配SQL Server

2.1 项目定位:外聘教师管理到底在管什么

先想清楚这个系统的业务边界。外聘教师不是正式职工,管理方式和在职员工差别很大:他们没有固定坐班时间,报酬按课时费结算;合同有明确期限,到期前要决定续聘还是解聘;授课课程分散在不同专业、不同学期,课时统计非常容易漏。如果靠Excel管理,最大的问题不是录入慢,而是不同的人拿着不同版本的表格,行政拿着一版课表,财务拿着一版课时汇总,月底核对时数据对不上,只能反复打电话确认,低效且容易翻车。

这个系统的数据模型就是围绕这类真实场景设计的。用户表区分管理员和教务员两种权限,教师档案表记录合同期和职称,课程表把教师和学期绑定在一起,课时记录表存放每一节实际上课的时长,结算模块把这些课时汇总成课时费。整个设计是典型的“主数据—业务单据—汇总计算”三段式,外聘教师、课程、课时记录分别充当主数据和单据,结算结果只是这些数据的投影。对于C#学习者,这个项目最有价值的地方在于,你能清楚看到数据表之间的一对多关系是怎么支撑界面操作的,而不是只看一堆孤立的概念图。

2.2 技术栈拆解:每个组件在项目里实际做什么

项目用C#做客户端,数据访问走ADO.NET的SqlClient,后端数据库用SQL Server,连接配置统一放在App.config里。这套组合在中小型管理系统里很常见,原因很直接:C#开发效率高,WinForms天生适合做连数据库的表格类界面;SQL Server的T-SQL在写存储过程、做汇总计算时,比在内存里写一堆循环更直观,也更容易维护。对外聘教师管理系统这种查询多、计算多、并发量小的场景,传统技术栈反而是最稳妥的选择,框架比功能多反而会增加上手成本。

工程按“界面层—业务层—数据访问层”的方式组织。界面层只做控件绑定与事件响应,不直接碰SQL;数据访问层用DbHelper封装SqlConnection和SqlCommand,所有取数请求都通过参数化SQL或存储过程执行;业务层夹在中间,负责校验输入、组装参数、处理返回值。分层带来的好处是:改界面不会影响取数逻辑,换数据库只动App.config即可,业务代码一行都不需要改。我一般会建议初次接触这类源码的人先找DbHelper类来看,它是整个项目的数据库门户,读懂了它,其他模块的取数逻辑基本就通了一半。

2.3 工程结构:解决方案里每一层放什么代码

外聘教师管理系统/ ├── 外聘教师管理系统.sln ├── database/ │ ├── create_db.sql -- 建库建表脚本 │ └── init_data.sql -- 初始化数据与默认账号 └── 外聘教师管理系统/ ├── App.config -- 连接字符串与运行参数 ├── Program.cs -- 程序入口,启动登录窗体 ├── Forms/ │ ├── FormLogin.cs -- 登录窗口 │ ├── FormMain.cs -- 主窗体,菜单与导航 │ ├── FormTeacher.cs -- 外聘教师档案管理 │ ├── FormCourse.cs -- 课程安排管理 │ ├── FormHour.cs -- 课时记录录入 │ └── FormSettlement.cs -- 课时费结算 ├── Models/ │ ├── Teacher.cs -- 教师实体 │ ├── Course.cs -- 课程实体 │ └── Settlement.cs -- 结算实体 └── DataAccess/ ├── DbHelper.cs -- 数据库操作封装 └── TeacherDAL.cs -- 教师模块数据访问

看目录结构就能感受到“一窗体一模块”的组织方式:FormTeacher管档案,FormCourse管课程,FormHour管课时,FormSettlement管结算。这种安排特别适合中小型系统,定位问题非常快:结算金额不对,直接查FormSettlement和对应的存储过程;课时录不进去,去FormHour里看事件绑定的数据访问方法。相比引入一堆重量级框架,这个工程不依赖注入、不搞ORM映射,实体就是普通C#类,打开就能逐行调试,这也是我推荐它当练手工程的原因,排错链路短,适合做二次开发的基础骨架。

换成ASP.NET WebForms或者WPF能不能做?当然能,但外聘教师管理系统这类内部使用的工具,业务场景是“少量人员、高频操作、低并发”,WinForms的响应速度和本地部署优势比B/S架构更明显。开发人员不必处理IIS配置、跨域、浏览器兼容这些问题,把精力全部集中在业务正确性上。选技术栈不是追新,而是看场景合不合适,这个项目把这一点体现得很实在,也算是一线的通用判断逻辑。

3. 核心模块拆解:从教师录入到课时费结算的完整链路

3.1 数据库设计:六张核心表的职责边界

数据库是这个系统的地基,核心表一共五张,外加一张用户表,表之间的逻辑可以用一句话串起来:外聘教师被聘到课程上,课程产生课时记录,课时记录按月度汇总生成结算单。建表脚本在database目录下,我挑重点拆开讲。

教师档案表:

CREATE TABLE Teacher( TeacherId INT IDENTITY(1,1) PRIMARY KEY, TeacherNo NVARCHAR(20) NOT NULL UNIQUE, Name NVARCHAR(50) NOT NULL, Phone NVARCHAR(20), Title NVARCHAR(20) DEFAULT '无', HireDate DATE, ExpireDate DATE, Status TINYINT DEFAULT 1 );

TeacherNo是外聘教师编号,加了唯一约束,保证同一个编号不会重复建档;Title保存职称,后续课时费分段计费就是以此为依据;Status为1表示在聘,0表示已解聘,列表页默认只查Status=1的数据,让离岗教师自动从日常列表里消失。Phone没设NOT NULL,因为外聘教师联系方式经常变,档案可以先建再补电话,不能因为缺一个手机号就卡住建档流程。ExpireDate单独存而不是用HireDate加时长计算,是因为续聘时只改到期日,原始聘用日期要保持不变,否则审计时说不清最初签订的时间。

课程表把教师和学期绑在一起:

CREATE TABLE Course( CourseId INT IDENTITY(1,1) PRIMARY KEY, CourseName NVARCHAR(100) NOT NULL, TeacherId INT NOT NULL, Semester NVARCHAR(20), PlanHours INT DEFAULT 0, FOREIGN KEY(TeacherId) REFERENCES Teacher(TeacherId) );

PlanHours是计划课时,只作为参考值,实际结算不直接用这个字段,真正的依据是课时记录表。这么设计的原因很实际:计划赶不上变化,调课、停课、补课都会让实际上课课时偏离计划,如果结算硬扣计划课时,必然和数据事实冲突。

课时记录表和结算表:

CREATE TABLE HourRecord( RecordId INT IDENTITY(1,1) PRIMARY KEY, CourseId INT, TeacherId INT, RecordDate DATE, ActualHours DECIMAL(4,1), AuditStatus TINYINT DEFAULT 0 ); CREATE TABLE Settlement( SettlementId INT IDENTITY(1,1) PRIMARY KEY, TeacherId INT, Month VARCHAR(7), TotalHours DECIMAL(6,1), Rate DECIMAL(6,2), Amount DECIMAL(8,2), CreateTime DATETIME DEFAULT GETDATE() );

ActualHours用DECIMAL(4,1)而不是整数,是因为排课经常出现1.5小时、2.5小时这种数据,用整数会丢精度;DECIMAL(4,1)表示总长4位、小数1位,最大能存999.9,一个月的课时量完全够用。AuditStatus默认0表示待审核,结算时只统计已审核的记录,这是防止财务把临时补登但尚未确认的课时算进去的关键环节。

3.2 登录与会话:怎么区分管理员和教务员

登录窗口是进入系统的第一道门,FormLogin.cs里最核心的就是btnLogin_Click方法:

private void btnLogin_Click(object sender, EventArgs e) { string user = txtUser.Text.Trim(); string pwd = txtPwd.Text; // 密码不Trim,首尾空格也是密码的一部分 if (user == "" || pwd == "") { MessageBox.Show("账号和密码都不能为空"); return; } DataTable dt = DbHelper.Query( "SELECT UserId, UserName, Role FROM SysUser WHERE UserName=@u AND Password=@p", new SqlParameter("@u", user), new SqlParameter("@p", pwd)); if (dt.Rows.Count == 1) { Session.CurrentUser = new UserInfo { UserId = Convert.ToInt32(dt.Rows[0]["UserId"]), UserName = dt.Rows[0]["UserName"].ToString(), Role = dt.Rows[0]["Role"].ToString() }; this.DialogResult = DialogResult.OK; } else { MessageBox.Show("用户名或密码错误"); } }

这段代码有三个地方值得注意。第一,密码框不做Trim,因为用户在输密码时手滑多加一个空格是常事,如果自动Trim掉,会掩盖“密码其实带空格”的真相,反而让排查变复杂;第二,查询走了参数化SQL,没有拼接字符串,这是C#连SQL项目必须守住的底线,既防注入也避免引号转义的幺蛾子;第三,登录成功后只把UserId、UserName、Role放进静态的Session类,后续主窗体就根据Role决定哪些菜单可见,管理员能看到结算审核入口,教务员只能做录入操作。

登录之后主窗体FormMain通过MenuStrip控制导航,每个菜单项点击后实例化对应窗体。这里没有用单例模式,而是每次点击都new一个窗体实例,关闭时直接释放,内存占用不高,代码还简单。业务系统里不需要为了模式而模式,简单可控比花哨重要。

3.3 教师档案管理:分页查询与编辑回填

档案列表是使用频率最高的界面,数据量大了之后不可能一次性全拉出来,项目里用的是SQL Server的ROW_NUMBER分页:

public DataTable LoadTeachers(int pageIndex, int pageSize) { int offset = (pageIndex - 1) * pageSize; string sql = @" SELECT * FROM ( SELECT ROW_NUMBER() OVER(ORDER BY TeacherId DESC) AS RowNo, * FROM Teacher WITH(NOLOCK) WHERE Status=1 ) AS T WHERE RowNo BETWEEN @start AND @end"; return DbHelper.Query(sql, new SqlParameter("@start", offset + 1), new SqlParameter("@end", offset + pageSize)); }

分页为什么用ROW_NUMBER而不是TOP加子查询?因为后者做跳页时逻辑很容易写错,尤其页码从3跳到15,偏移量计算稍不留神就会翻车;ROW_NUMBER先按TeacherId倒序编号,再截取行号区间,是目前在SQL Server里最稳的分页写法。pageIndex是页码,从1开始,pageSize是每页条数,比如每页20条;参数@start和@end是行号区间的起止值。WITH(NOLOCK)是查询层面的乐观锁优化,读操作不申请共享锁,避免和写操作互相阻塞,在外聘教师这种读多写少的场景里收益明显。

“编辑”按钮触发事件后,界面把选中行的TeacherId传给DAL层,DAL根据主键查单条记录,再逐个控件赋值回填。日期赋值是这里的高频报错点,如果直接把数据库的DateTime赋值给DateTimePicker,会因格式不匹配抛异常;正确做法是先用Convert.ToDateTime转换成DateTime,再用ToString("yyyy-MM-dd")格式化后赋值。回填完成后还要在下拉框里选中该教师原来的职称,做法是先遍历DropDownList的Items找匹配项,找不到就默认第一项。

3.4 课时费计算:把课时记录变成钱的存储过程

结算这个动作,项目用存储过程在数据库端完成。调用方只需传教师编号和月份,数据库算完直接写结算表:

CREATE PROCEDURE CalcSettlement @TeacherId INT, @Month VARCHAR(7) AS BEGIN DECLARE @TotalHours DECIMAL(6,1); DECLARE @Rate DECIMAL(6,2); DECLARE @Amount DECIMAL(8,2); SELECT @TotalHours = SUM(ActualHours) FROM HourRecord WHERE TeacherId = @TeacherId AND AuditStatus = 1 AND CONVERT(VARCHAR(7), RecordDate, 120) = @Month; SELECT @Rate = Rate FROM Teacher WHERE TeacherId = @TeacherId; SET @Amount = ISNULL(@TotalHours, 0) * ISNULL(@Rate, 0); INSERT INTO Settlement(TeacherId, Month, TotalHours, Rate, Amount) VALUES(@TeacherId, @Month, @TotalHours, @Rate, @Amount); END

关键点都在WHERE条件和ISNULL上。CONVERT(VARCHAR(7), RecordDate, 120)把日期转成“2025-06”这种年月格式,120是ODBC标准格式;@TeacherId限定只看这个教师的课时;AuditStatus=1确保只有已审核课时参与结算,避免把待确认的数据算进去。最后用ISNULL兜底:如果某月没有课时记录,SUM返回NULL,乘以单价就变成NULL,ISNULL把它转成0;同样,如果教师表里没设置单价,Rate为NULL时也转成0,这样结算表里不会出现空值,后面报表统计时才不会被NULL干扰。

这套设计把计算压力放在数据库端,客户端只负责传参和显示结果。好处是计算规则只在存储过程里维护,以后改课时费标准,不需要重新发布客户端;坏处是存储过程写错了排查路径更长,所以我在实际改造中会在存储过程开头加一句SET NOCOUNT ON,减少不必要的行数消息,同时用PRINT输出中间变量,方便调试。

4. 运行部署三步走:还原数据库、改连接串、启动看流程

4.1 环境准备:Visual Studio、SQL Server与注意事项

跑这个项目需要的环境很标准:安装Visual Studio时勾选“.NET桌面开发”工作负载,SQL Server装开发版或Express版都可以,版本选2008以上基本就能兼容。工程文件是.sln格式,双击打开后Visual Studio会自动识别目标框架并还原所需组件,一般不需要手动装额外的NuGet包,这一点对新人很友好,省去了配置第三方依赖的步骤。

SQL Server安装时有个容易忽略的坑:身份验证模式。建议在安装时选择“混合模式”,并给sa账号设置一个明确的密码。如果已经装完了才发现,可以通过SQL Server Management Studio的服务器属性改,不需要重装。外聘教师管理系统用sa账号连接最省事,但如果你所在环境不允许开sa,也可以建一个普通登录账号,给它映射到对应数据库的db_owner角色,效果一样。

安装完请先验证SQL Server服务是否真的在跑。按Win+R输入services.msc,找到SQL Server服务,确认状态是“正在运行”。这一步花不了30秒,但能省掉后续一半的“连不上数据库”排查时间。很多同学一打开程序就报连接错误,结果检查半天发现是服务根本没启动,属于典型的低级翻车。

4.2 执行数据库脚本:建库、建表、初始化一把过

打开SQL Server Management Studio,用管理员账号连接实例,然后依次执行database目录下的两个脚本。第一个create_db.sql负责建库和建表,第二个init_data.sql负责插入初始化数据,包括默认管理员账号admin和一条测试教师记录。执行顺序不能反,先有库和表,才能灌数据。

如果脚本第一次执行失败,大概率是库里已经有同名对象了。SQL Server不像有些数据库会自动覆盖,必须手动清理。建议执行前右键数据库检查是否已存在同名库,如果存在,先执行DROP DATABASE清理掉,或者手动删掉库里已有的表再重新执行。我一般会直接在SSMS里把执行结果全部选中,看是否有红色报错信息,有就按报错提示逐步处理,通常是“对象已存在”这类幂等问题。

初始化数据里的默认账号我在脚本注释里已经写明,通常是admin,密码用一个强密码,如果你要改密码,直接改init_data.sql里的INSERT语句再执行即可。不要直接在管理工具里把账号删掉重建,用户表外键关系可能还引用着其他数据,删完会影响后续模块。

4.3 连接字符串配置:App.config是唯一需要动的地方

所有数据库连接信息都集中在App.config文件里,这是分层带来的福利,改完重新生成一次即可生效:

<configuration> <connectionStrings> <add name="MSSQL" connectionString="Server=.;Database=ExternalTeacherDB;User ID=sa;Password=123456;" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration>

连接字符串里的Server=.代表本机默认实例,如果你的SQL Server是具名实例,就要写成“主机名\实例名”,比如“localhost\SQLEXPRESS”;Database对应真实库名,不能写错;User ID和Password填SQL Server的登录账号,不是Windows登录账号。如果你完全不确定这块参数,我一般会先用SSMS拿这组账号密码实际连一次实例,能连上说明连接串没写错,再放到App.config里就万无一失。

改完连接字符串后,右键解决方案点击重新生成,让项目把最新配置编译进输出目录。这一点经常被新手忽略:改了App.config不重新生成,运行的是bin目录下旧版本的配置文件,连接串看起来改了实际还是旧的,白白浪费几分钟排查。记住,改完配置务必重新生成。

4.4 启动验证:从登录到走通一条完整业务流

按F5启动程序,先弹出登录窗口,输入admin账号和初始化密码,登录成功后进入主窗体。验证流程建议按业务顺序走一遍:先在外聘教师管理里新增一条教师记录,填写姓名、编号、职称、聘用日期和到期时间;然后进入课程管理,给这位教师分配一门课程,设置学期;接着在课时录入里添加几条上课记录,注意日期要选当月,课时数填实际值;最后到结算模块,选择教师和月份,点击生成结算,弹出结果里应能看到汇总课时和金额。

这条流程走通,说明数据库表、连接、界面和存储过程都正常。验证时最好对照数据库里的数据看,在SSMS里执行查询:

SELECT * FROM HourRecord; SELECT * FROM Settlement;

核对HourRecord里新增了几行,Settlement里有没有生成对应记录,金额是否等于课时数乘以单价。前端界面是数据的投影,如果界面显示正常但库里查不到,说明事务没提交,这时要回头检查DAL层有没有关闭SqlTransaction;如果库里有数据但界面不显示,则要检查查询SQL的WHERE条件里是不是带了额外过滤。

5. 避坑记录:数据库连不上、登录失败、乱码的排查顺序

5.1 登录窗口报“用户名或密码错误”,但数据库能正常连接

这是最容易让人疑惑的一个现象:用SSMS连同一个数据库能查能改,程序登录却说账号密码错误。我第一次遇到时也以为是代码逻辑写错了,后来才发现问题在密码处理方式上。项目里登录查询用的是参数化SQL,按用户名查库里的密码再比对,而初始化脚本里插入的密码是明文,如果你新建账号时手工往库里插数据,密码字段忘了写,比对自然失败。

排查顺序应该这么走:先在SSMS里执行SELECT UserName, Password, Role FROM SysUser,确认这条记录存在且密码字段有值;然后看程序登录代码里拿到的password变量是否带有多余空格;最后检查是不是走错了库——App.config里连接的是A库,但你在B库插的账号。三者逐一排除后,登录通常就通了。

另一个可能被忽略的点是:如果初始化脚本里密码是用某种Hash算法生成的,而登录SQL拿明文去比,也会永远失败。本项目默认是明文存储,如果你要增强安全性改成MD5,必须同时改登录SQL和初始化脚本,两处一致才能跑通。

5.2 能连本机数据库,却连不上远程服务器

开发机连本机SQL Server一切正常,部署到服务器或连同事的数据库时,程序一直报超时或网络错误。这种现象十有八九不是代码问题,而是网络或服务配置问题。服务器上要先确认SQL Server服务在跑,然后检查TCP/IP协议有没有启用:打开SQL Server配置管理器,找到SQL Server网络配置,把TCP/IP的状态改为“已启用”,然后重启服务。

端口也要确认。SQL Server默认监听1433端口,如果服务器开了防火墙,必须在入站规则里放行1433;云服务器还要在安全组里加对应规则。判断端口通不通,可以用一行命令在命令行执行:telnet 服务器IP 1433,能连上会直接进入空白界面,连不上会提示无法连接。这些检查都做完还不行,再看是否有登录名权限、密码是否与数据库账号一致的问题。

有一点值得单独说明:连接串里的Server字段如果写的是localhost,程序只会连接本机,不会去找远程服务器。要连远程,必须写对方IP或主机名,写成localhost永远只能在本地跑通,换台机器就翻车。

5.3 中文乱码:程序里显示正常,数据库查出来却是问号

中文乱码出现的位置不同,根因完全不同。如果程序界面里录入的中文,到SSMS里看成了“???”,通常是连接字符串缺少字符集指定,或者数据库排序规则不支持中文;如果SSMS里中文正常,程序界面显示乱码,则是程序读取时编码转换出了问题。

最省心的做法是一开始就把数据库排序规则设成Chinese_PRC_CI_AS,连接字符串保持兼容默认。如果数据库已经建出来了,可以执行ALTER DATABASE ... COLLATE Chinese_PRC_CI_AS更改排序规则来转换,但要注意表里如果已经有数据,转换可能存在风险,最好先备份再操作。另一点是代码文件的编码,C#源文件默认就是UTF-8,一般不涉及乱码;真正频繁出问题的是SQL脚本文件,如果脚本本身用记事本改过并保存成了ANSI编码,执行时中文字符可能直接损坏,导致初始化数据就是坏的。建议用Visual Studio或SSMS打开脚本检查编码,避免用记事本编辑SQL脚本。

5.4 结算金额算出来是0,但课时记录明明存在

这个坑我印象最深,直接看表象:课时记录表里有数据,审核状态也改成1了,但结算存储过程算出来的金额是0。打开存储过程逐行排查,发现SUM(ActualHours)算出来竟然是NULL,再一查是ActualHours字段为NULL,插入课时记录时没给默认值,程序录入界面也没强制校验。

这段血泪经验让我养成了一个习惯:对参与计算的数值字段,规范要求必须加NOT NULL约束,录入界面加必填校验,存储过程里再用ISNULL兜底。三条防线缺一条,数据总会在某个角落偷偷变成NULL。结算里出现的任何0值或空值,都先查源头单据有没有录完整,而不是先怀疑计算逻辑。

还有一个相关场景:结算金额正常,但不是整数,格式显示成一长串小数。这是Rate字段精度不够或没做四舍五入导致的,DECIMAL(6,2)里的“2”就是小数位,计算时用ROUND(@Amount, 2)保留两位,能避免财务单据上出现多余小数位。财务的数据最讲究格式一致,这一点看似小,实际很影响使用体验。

6. 二次开发:把课时费规则改成按职称分段计费

6.1 改存储过程还是改C#?各管一头

系统原本的课时费单价是每个教师固定一个Rate,适合小规模场景,但实际管理中外聘教师按职称区分课时费更常见:教授一小时300,副教授一小时200,讲师一小时150。要实现这个规则,我建议把计算规则收敛到存储过程里,不动C#代码。

新增一个费用档位表,把职称和单价的关系独立出来,不要硬编码到存储过程里,因为单价会随年度调整,放表里改数据就能维护:

CREATE TABLE FeeRate( Title NVARCHAR(20) PRIMARY KEY, Rate DECIMAL(6,2) ); INSERT INTO FeeRate(Title, Rate) VALUES('教授', 300); INSERT INTO FeeRate(Title, Rate) VALUES('副教授', 200); INSERT INTO FeeRate(Title, Rate) VALUES('讲师', 150);

然后修改CalcSettlement存储过程,把从Teacher表取单价的逻辑改成先从FeeRate表按职称查单价,查不到再从Teacher表的Rate兜底:

SELECT @Rate = ISNULL(f.Rate, t.Rate) FROM Teacher t LEFT JOIN FeeRate f ON t.Title = f.Title WHERE t.TeacherId = @TeacherId;

先按职称查档,查不到就退回教师档案里的固定单价,这样既能支撑阶梯价,又能兼容存量数据。前端界面不需要改动,存储过程对客户端是透明的,改完重新生成项目再执行一次结算即可。这类改造的价值在于:业务规则集中管理,维护成本最低,这也是我坚持把课时费计算放在存储过程里而不是C#代码里的原因。

6.2 改造后怎么验证规则正确

改完代码不能直接上线,至少要走一遍验证流程。第一,新建三个不同职称的测试教师,分别录入同样的课时量,执行结算,核对金额是否符合对应档位单价;第二,改其中一个教师的职称,重新结算,确认金额跟着职称联动变化;第三,构造一个职称不在FeeRate表里的教师,确认兜底单价生效;第四,跑一次回归,确认原有固定单价的教师结算结果不被破坏。

从那以后我每次改存储过程,都强制自己先在测试库里造一套边界数据,再走结算验证,最后才更新正式环境。数据库计算逻辑改起来快,出问题也隐蔽,不上线前测一遍,上线后面对一堆对不上的账,那才是真的煎熬。希望这些踩过的坑和改造思路能帮到你,尤其是刚接触这种C#+SQL经典架构的开发者,把这条业务链路走通一次,后面做其他管理系统也能少绕不少弯。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进