简介:这份资源是面向计算机及相关专业学生的数据库课程设计实战案例,以C#与Windows Forms构建功能完整的人事管理系统,适合正在做课程设计、需要参考数据库应用开发全流程的学习者。系统覆盖员工档案、部门管理、考勤记录、薪资计算与登录注册、修改密码等模块,桌面界面逻辑与关系型数据库紧密集成,可直接借助随包数据库文件导入运行,省去从零建库的繁琐。压缩包共91个文件、约4.1MB,以cs源代码、resx与resources界面资源、config配置文件为主,另含sln与csproj工程文件、mdf与ldf数据库文件、exe可执行程序,以及数据库课程设计报告和系统运行说明书两份docx文档,分别对应设计思路与上手操作。已有5724人学习下载,读者可据此理解需求分析、数据库建模、SQL查询与事务处理如何落地到真实程序,并对照源码掌握界面与数据交互的实现方法,完成课程设计或作为小型管理系统的开发模板。
1. 一次答辩追问引出的设计起点:人事管理系统的数据库课程设计先解决数据一致性
人事管理系统的数据库课程设计,答辩现场常出现这样的追问:员工从技术部调到市场部之后,三个月前那条薪资记录里的部门名称,到底要不要跟着改?答"要"的人,历史报表会整体错位,去年的人力成本分析全变成新部门的账;答"不要"的人,得先解释清楚为什么同一个部门信息要在两张表里存两遍。这类问题几乎不会出现在功能演示环节,却直接决定这套 C# 加关系型数据库的完整数据库方案是"能跑起来"还是"站得住"。课程设计真正被打分的地方,从来不是窗体有几个按钮,而是表结构、约束、视图、存储过程、事务这几层经不经得起追问。
2. 需求与库表设计:从六类业务单据倒推出十张核心表
2.1 谁在用这个系统:四类角色和他们的真实动作
做人事管理系统最容易犯的错,是打开 IDE 先拖控件。我一般会先拿一张纸,把使用者的动作按频率列出来,因为动作决定表结构,表结构决定后面所有工作量。这套系统里的角色大致分四类:人事专员负责员工入职、调岗、离职、合同续签;部门主管看本部门考勤和加班申请;普通员工查自己的薪资条和历史考勤;系统管理员维护账号、角色和字典数据。
把这四类人的动作翻译成数据语言,就是四组能力:员工的增删改查与状态流转、组织和岗位的层级维护、按月汇总的考勤数据、按月封账的薪资数据。关键点在于后两组——考勤和薪资都是"按月快照"性质的数据,一旦封账就不该再被组织架构的变动影响。这个判断直接决定了后面 SalaryRecord 表要冗余部门 ID 和岗位 ID,而不是简单靠外键关联过去。
角色权限在数据库课设里不必做成完整的 RBAC,但至少要有 User、Role、UserRole 三张表,并在存储过程或应用层校验。答辩时被问到"怎么防止普通员工改自己工资",能说清权限表和校验位置就够了,不需要上框架。
2.2 ER 图到关系模式:实体、联系和主外键的落地
概念设计阶段画 ER 图,实体大约六到八个:部门、岗位、员工、合同、考勤、薪资、系统用户。联系上要特别标注清楚三个容易画错的地方。第一,员工与部门是多对一,但要允许部门有上级部门,所以 Department 表自带自引用外键 ParentID,这是一棵不限层级的树,不是简单的一层列表。第二,员工与岗位不是纯多对一——员工调岗后历史薪资要保留原岗位,所以岗位信息在薪资表里以快照字段出现。第三,员工与合同是一对多,一个员工可能有多份历史合同,只有一份处于生效状态。
把 ER 图转成关系模式时,按下面的方式落表,字段名前缀统一,避免同名不同义:
| 表名 | 主键 | 关键外键 / 约束 | 说明 |
|---|---|---|---|
| Department | DeptID | ParentID → Department | 部门树,含撤销状态 |
| Post | PostID | DeptID → Department | 岗位挂在部门下 |
| Employee | EmpID | DeptID、PostID | EmpNo、IDCard 唯一 |
| Contract | ContractID | EmpID → Employee | 一人多合同,一条生效 |
| Attendance | AttendID | EmpID → Employee | (EmpID, AttendDate) 唯一 |
| SalaryRecord | SalaryID | EmpID → Employee | (EmpID, SalaryMonth) 唯一 |
| SysUser / SysRole / UserRole | — | 多对多关系表 | 权限控制 |
注意 Employee 表里的 DeptID 是"当前部门",只服务于当前组织查询;历史归属要靠 SalaryRecord 的快照字段去还原。如果只保留一个 DeptID 又想查历史,就只能靠日志表勉强凑,查询会写成三层嵌套,答辩时很难讲圆。
另外把字典类数据单独拆表,比如学历、婚姻状况、合同类型,统一放在 SysDict 里,用 DictType 区分。这类字段用中文直接存进业务表看着省事,后期要统计"本科及以上占比"就得写一堆 LIKE,得不偿失。
2.3 范式与反范式的取舍:什么时候该故意留冗余
课设报告里几乎都会写"本设计满足第三范式",但真按 3NF 做到极致,薪资查询会变成五表连接。我的做法是先规范化再局部反规范化,并且每处冗余都写清理由,这恰恰是答辩加分的点。
规范化部分:员工表里不放部门名称、岗位名称,只放 ID;考勤表里不放员工姓名;薪资表里不放银行卡号(放员工表)。这些字段一旦冗余,改名和调岗就会产生更新异常,属于必须消掉的。
反规范化部分有两个:一是 SalaryRecord.DeptIDSnap 和 PostIDSnap,理由前面已经说过,是为了保留发放时点的组织归属,属于"有意为之的历史快照",不是设计缺陷;二是 SalaryRecord 里存 NetPay 计算列,用PERSISTED关键字让数据库物化存储,查询工资条时不用每次重算。第二处要小心,基础字段一改,持久化计算列会自动重算,但如果参与计算的字段来自其他表就不行了,所以只能基于本表字段。
第三范式被破坏的第三个常见位置是考勤月度汇总。如果每次查月报都从 Attendance 明细表做 GROUP BY,数据量一大就慢。通常的做法是建一张 AttendanceMonth 汇总表,由存储过程在月末跑批写入,明细表只做流水。这张汇总表是派生数据,允许重复,但必须在文档里说明它的刷新时机和口径。
3. 数据库物理实现:约束、视图、存储过程和触发器的分工
3.1 建表脚本:约束该放在数据库层就别放到代码里
很多人把校验全写在 C# 里,结果换个客户端或者直接连库改数据就全乱了。我的原则是:能在数据库层表达的规则,一定在数据库层再表达一遍。非空、唯一、外键、取值范围、默认值,这五类都写进建表脚本。下面这段是部门和员工两张核心表的骨架:
-- 部门表:自引用实现多级组织架构,Status 支持软删除 CREATE TABLE Department ( DeptID INT IDENTITY(1,1) PRIMARY KEY, DeptName NVARCHAR(50) NOT NULL, ParentID INT NULL, -- 顶级部门为 NULL Status TINYINT NOT NULL DEFAULT 1,-- 1 在用 0 已撤销 CreatedAt DATETIME NOT NULL DEFAULT GETDATE(), CONSTRAINT UQ_Dept_Name UNIQUE (DeptName), CONSTRAINT FK_Dept_Parent FOREIGN KEY (ParentID) REFERENCES Department(DeptID) ); -- 员工表:EmpNo 是业务主键,EmpID 是物理主键 CREATE TABLE Employee ( EmpID INT IDENTITY(1,1) PRIMARY KEY, EmpNo CHAR(8) NOT NULL, EmpName NVARCHAR(30) NOT NULL, Gender CHAR(1) NOT NULL, IDCard CHAR(18) NULL, Phone VARCHAR(20) NULL, DeptID INT NOT NULL, PostID INT NOT NULL, HireDate DATE NOT NULL, Status TINYINT NOT NULL DEFAULT 1, -- 1在职 2试用 3离职 CONSTRAINT UQ_Emp_No UNIQUE (EmpNo), CONSTRAINT UQ_Emp_IDCard UNIQUE (IDCard), CONSTRAINT CK_Emp_Gender CHECK (Gender IN ('M','F')), CONSTRAINT FK_Emp_Dept FOREIGN KEY (DeptID) REFERENCES Department(DeptID), CONSTRAINT FK_Emp_Post FOREIGN KEY (PostID) REFERENCES Post(PostID) );几个参数值得说明。EmpNo 用 CHAR(8) 而不是 VARCHAR,是因为工号长度固定,定长在索引比较时更省事。IDCard 允许 NULL 但加唯一约束,SQL Server 的唯一约束对 NULL 的处理是只允许出现一个 NULL,如果业务上会批量导入无身份证的实习员工,就得改成带条件的唯一索引。Gender 用 CHAR(1) 存 M/F,比存"男/女"更稳,展示层再翻译。
Department 的 ManagerID 我没有写进建表语句,因为它指向 Employee,而 Employee 又依赖 Department,形成循环依赖。正确做法是两张表建完之后用ALTER TABLE Department ADD CONSTRAINT FK_Dept_Manager FOREIGN KEY (ManagerID) REFERENCES Employee(EmpID);补上。这个细节在答辩里被问到"外键循环怎么处理"时很管用。
薪资表是整套设计里最需要斟酌的一张:
CREATE TABLE SalaryRecord ( SalaryID BIGINT IDENTITY(1,1) PRIMARY KEY, EmpID INT NOT NULL, SalaryMonth CHAR(7) NOT NULL, -- 形如 2025-03 DeptIDSnap INT NOT NULL, -- 发放时部门快照 PostIDSnap INT NOT NULL, -- 发放时岗位快照 BaseSalary DECIMAL(10,2) NOT NULL, Bonus DECIMAL(10,2) NOT NULL DEFAULT 0, Deduction DECIMAL(10,2) NOT NULL DEFAULT 0, SocialFee DECIMAL(10,2) NOT NULL DEFAULT 0, NetPay AS (BaseSalary + Bonus - Deduction - SocialFee) PERSISTED, PayStatus TINYINT NOT NULL DEFAULT 0, -- 0待发 1已发 CONSTRAINT UQ_Salary_Emp_Month UNIQUE (EmpID, SalaryMonth), CONSTRAINT FK_Salary_Emp FOREIGN KEY (EmpID) REFERENCES Employee(EmpID) );金额一律用 DECIMAL(10,2),绝不用 FLOAT——浮点误差在工资条上会变成 0.01 元的差额,被人截图发群里就很难解释。SalaryMonth 用 CHAR(7) 存年月,比存 DATETIME 再格式化更方便按月分组,也避免了"某月第一天几点几分"这种无意义的时间部分。NetPay 是持久化计算列,好处是查询和排序直接走存储值,坏处是它不能被直接 UPDATE,插入时必须提供基础字段。
3.2 视图层:把多表连接封成一层薄接口
UI 层直接写五表 JOIN 的 SQL,是课设里最常见的结构问题。改一个字段名,要翻遍所有窗体。我的习惯是给每类高频查询建一个视图,应用层只跟视图打交道。
-- 员工综合信息视图:界面列表页、导出、详情页共用 CREATE VIEW vw_EmployeeDetail AS SELECT e.EmpID, e.EmpNo, e.EmpName, CASE e.Gender WHEN 'M' THEN N'男' ELSE N'女' END AS GenderText, d.DeptName, p.PostName, e.HireDate, e.Status, c.ContractNo, c.EndDate AS ContractEndDate FROM Employee e JOIN Department d ON d.DeptID = e.DeptID JOIN Post p ON p.PostID = e.PostID LEFT JOIN Contract c ON c.EmpID = e.EmpID AND c.Status = 1; -- 只关联生效合同注意 Contract 用的是 LEFT JOIN 加状态过滤,因为新入职员工可能还没签合同,用 INNER JOIN 会导致这些人从列表里凭空消失,这种问题在演示时最容易被发现。CASE 表达式把 M/F 翻译成中文,让 WinForms 的 DataGridView 直接绑定就能显示,省掉一列格式化代码。
视图的另一个用途是做权限隔离。给部门主管用的视图带WHERE d.DeptID = @DeptID是不行的,视图不能带参数,正确做法是建带参数的内联表值函数,或者干脆在存储过程里控制。课设报告里把这一点写清楚,比堆十个视图更有说服力。
3.3 存储过程与触发器:把月结逻辑下沉到数据库
薪资核算是这套系统里唯一真正需要事务的地方。批量生成某个月的工资记录,要么全成功要么全失败,中间断掉会留下一半数据,重跑又撞唯一约束。用存储过程包住,配合 TRY/CATCH 和显式事务:
CREATE PROCEDURE usp_GenerateSalary @SalaryMonth CHAR(7) -- 目标月份,如 '2025-03' AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; -- 出错自动回滚,避免事务悬挂 BEGIN TRY BEGIN TRANSACTION; INSERT INTO SalaryRecord (EmpID, SalaryMonth, DeptIDSnap, PostIDSnap, BaseSalary, Bonus, Deduction, SocialFee) SELECT e.EmpID, @SalaryMonth, e.DeptID, e.PostID, p.BaseSalary, ISNULL(SUM(CASE WHEN a.AttendType = 3 THEN a.Hours ELSE 0 END), 0) * 50, -- 加班补贴 ISNULL(SUM(CASE WHEN a.AttendType = 2 THEN 1 ELSE 0 END), 0) * 200, -- 缺勤扣款 p.BaseSalary * 0.105 -- 社保个人部分 FROM Employee e JOIN Post p ON p.PostID = e.PostID LEFT JOIN Attendance a ON a.EmpID = e.EmpID AND CONVERT(CHAR(7), a.AttendDate, 120) = @SalaryMonth WHERE e.Status IN (1, 2) -- 在职和试用 AND NOT EXISTS (SELECT 1 FROM SalaryRecord s -- 幂等:已生成则跳过 WHERE s.EmpID = e.EmpID AND s.SalaryMonth = @SalaryMonth) GROUP BY e.EmpID, e.DeptID, e.PostID, p.BaseSalary; COMMIT TRANSACTION; END TRY BEGIN CATCH IF XACT_STATE() <> 0 ROLLBACK TRANSACTION; THROW; -- 把原始错误抛给调用方,不要吞掉 END CATCH END这段里有三个细节值得在报告里单独讲。SET XACT_ABORT ON让运行时错误自动回滚整个事务,否则某些错误只会终止当前语句,事务还挂着,连接池里就留下一个僵尸事务。NOT EXISTS保证了幂等性,同一个月重复执行不会插入重复行,也不会因为撞唯一约束整批失败。THROW而不是RAISERROR,是因为 THROW 会保留原始错误号和行号,排查时省事。
触发器我一般只用在一处:关键表的审计。薪资发放状态一变,就写一条日志,方便事后追溯是谁在什么时候点的"发放":
CREATE TRIGGER trg_Salary_Audit ON SalaryRecord AFTER UPDATE AS BEGIN SET NOCOUNT ON; -- 只记录发放状态的变更,避免普通改金额也刷日志 INSERT INTO SalaryAudit (SalaryID, OldStatus, NewStatus, ChangedAt, ChangedBy) SELECT i.SalaryID, d.PayStatus, i.PayStatus, GETDATE(), SUSER_SNAME() FROM inserted i JOIN deleted d ON d.SalaryID = i.SalaryID WHERE i.PayStatus <> d.PayStatus; END触发器里必须用 inserted 和 deleted 两个伪表做集合操作,绝不能假设"只影响一行"。批量发放一百条工资时,如果触发器里写SELECT @id = SalaryID FROM inserted,只会记录最后一条,这个坑非常经典。另外触发器默认是递归和嵌套都可能开启的,涉及同表更新的触发器要特别小心,必要时用ALTER DATABASE ... SET RECURSIVE_TRIGGERS OFF关掉。
3.4 索引与执行计划:该建的建,不该建的别乱建
课设里的数据量通常只有几百上千行,索引效果看不出来,但报告里必须写清楚设计依据。我的判断标准是看查询条件的选择性和使用频率:
| 表 | 索引字段 | 类型 | 理由 |
|---|---|---|---|
| Employee | EmpNo | 唯一索引(约束自带) | 按工号精确查询是最高频操作 |
| Employee | DeptID, Status | 非聚集复合索引 | 部门列表按状态过滤 |
| Attendance | EmpID, AttendDate | 复合非聚集索引 | 月度考勤按人按日期范围扫描 |
| SalaryRecord | SalaryMonth, PayStatus | 非聚集索引 | 按月封账统计 |
| SalaryRecord | (EmpID, SalaryMonth) | 唯一索引(约束自带) | 幂等判断依赖它 |
反过来,不要给 Gender、Status 这种只有两三个取值的字段单独建索引。选择性太低,优化器多半直接忽略,白白增加插入时的维护成本。姓名也不要建普通索引,中文姓名重复率高,真要做模糊查询得考虑全文索引,课设阶段说明"数据量小暂不引入"即可。
验证方式是打开 SQL Server Management Studio 的"显示实际执行计划",跑一遍月度薪资统计,看有没有出现 Key Lookup 或 Table Scan。如果某个查询回表次数很高,可以考虑把常用列加到索引的 INCLUDE 里做覆盖索引。这套流程写进报告,比单纯罗列"我建了五个索引"专业得多。
4. C# 持久层落地:三层结构、参数化查询和事务边界
4.1 三层架构与目录划分
WinForms 项目最容易写成一坨:窗体里直接拼 SQL 字符串,按钮事件里开连接。后面加一个字段就要改十个地方。我一般按下面这个目录分:
HrSystem.sln ├── HrSystem.Model/ // 实体类,与表一一对应,无逻辑 │ ├── Employee.cs │ └── SalaryRecord.cs ├── HrSystem.DAL/ // 数据访问:DbHelper + 各表 Dao │ ├── DbHelper.cs │ ├── EmployeeDao.cs │ └── SalaryDao.cs ├── HrSystem.BLL/ // 业务规则:校验、事务编排 │ └── SalaryService.cs └── HrSystem.UI/ // WinForms 窗体 ├── FrmEmployeeList.cs └── FrmSalaryGenerate.cs分层的关键不是文件夹好看,而是依赖方向单一:UI 只引用 BLL,BLL 只引用 DAL 和 Model,DAL 不引用 UI。这样窗体里不出现SqlConnection,Dao 里不出现MessageBox。答辩老师翻代码时,看到 UI 层没有 SQL 字符串,基本就认可结构是清晰的。
Model 层保持纯净,只放属性。有人喜欢在实体类里加Save()方法,那等于把持久化逻辑塞进模型,实体就不 pure 了,跨层复用时会很难受。
4.2 连接管理、参数化查询与事务
所有数据访问收敛到一个 DbHelper,连接字符串从配置文件读,绝不硬编码。核心是两个方法:一个查 DataTable,一个执行非查询:
public static class DbHelper { // 连接字符串放在 App.config 的 connectionStrings 节点 private static readonly string ConnStr = ConfigurationManager.ConnectionStrings["HrDb"].ConnectionString; public static DataTable Query(string sql, params SqlParameter[] ps) { using (var conn = new SqlConnection(ConnStr)) using (var cmd = new SqlCommand(sql, conn)) using (var da = new SqlDataAdapter(cmd)) { if (ps?.Length > 0) cmd.Parameters.AddRange(ps); // 参数化,杜绝拼接 var dt = new DataTable(); da.Fill(dt); return dt; } // using 保证连接归还连接池,异常也不会漏 } public static int Execute(string sql, params SqlParameter[] ps) { using (var conn = new SqlConnection(ConnStr)) using (var cmd = new SqlCommand(sql, conn)) { if (ps?.Length > 0) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteNonQuery(); } } }参数化查询不只是防注入,它还让 SQL Server 能复用执行计划。拼接字符串时每个不同的工号都会生成一份新的执行计划,缓存很快被撑爆。写报告时把这两点都写上,比单说"防止 SQL 注入"更完整。
需要跨多条语句保证一致性的场景,用下面这个事务封装,把连接和事务对象交给调用方,避免在业务层再开一次连接:
public static void InTransaction(Action<SqlConnection, SqlTransaction> work) { using (var conn = new SqlConnection(ConnStr)) { conn.Open(); using (var tran = conn.BeginTransaction()) { try { work(conn, tran); // 业务代码在这里执行多条命令 tran.Commit(); } catch { tran.Rollback(); // 任何异常整批回退 throw; // 保留堆栈,别写成 throw ex } } } }调用侧长这样:入职登记要同时写 Employee、Contract、SysUser 三张表,任何一张失败都必须整体回滚,否则会出现"有员工没账号"的脏数据。这种场景下throw;和throw ex;的区别值得强调一次——后者会重置堆栈,日志里只剩最后一行,排查等于摸黑。
4.3 界面绑定与几个实用的交互细节
WinForms 里绑数据首选 BindingSource 加 DataGridView,中间加一层 DataTable 做缓冲:
var dt = DbHelper.Query( "SELECT * FROM vw_EmployeeDetail WHERE DeptName LIKE @kw ORDER BY EmpNo", new SqlParameter("@kw", "%" + txtKeyword.Text.Trim() + "%")); var bs = new BindingSource { DataSource = dt }; dgvEmployee.AutoGenerateColumns = true; dgvEmployee.DataSource = bs; lblCount.Text = $"共 {dt.Rows.Count} 条";分页不要用TOP 20硬切,翻到第二页就对不上了。SQL Server 2012 之后用ORDER BY EmpNo OFFSET @skip ROWS FETCH NEXT @size ROWS ONLY,配合一个SELECT COUNT(*)拿总数,翻页逻辑就稳了。参数说明:@skip 是已经跳过的行数,等于(页码减一)乘每页条数;@size 是每页条数,界面上做个下拉框让用户选 20/50/100。
日期输入用 DateTimePicker,别让用户手打字符串,这是最省事也最有效的一处体验优化。删除操作一律做软删除,Employee.Status 置 3 而不是 DELETE,因为薪资、考勤都挂着外键,物理删除必然报错,硬删还会毁掉历史数据。
5. 避坑与排查:课设里最容易翻车的五个点
5.1 中文姓名查出来是问号
现象:窗体里输入"张三"能查到,但导出的 CSV 打开全是乱码,或者插入中文变成??。
原因一般有两层。数据库字段用了 VARCHAR 而不是 NVARCHAR,非 Unicode 类型在特定排序规则下存不了中文;C# 端拼接连接字符串时没指定字符集,或者导出文件没写 BOM 头,Excel 默认按 GBK 解析。
解决:所有文本字段统一 NVARCHAR;连接字符串保持默认的 Unicode 传输;导出 CSV 时用new UTF8Encoding(true)写入 BOM。三处任一遗漏都会复现,所以最好在项目初期就定死规范,别等到最后导数据才发现。
5.2 批量导入考勤时部分成功部分失败
现象:导入 300 条考勤,界面提示成功,但数据库里只有 287 条,剩下 13 条不知去向,而且没有任何报错。
原因:把每条 INSERT 放在独立的 try-catch 里,异常被吞掉了;或者用了SqlBulkCopy但没开事务,中途某行违反唯一约束(同一员工同一天重复打卡),整批中断,前面已提交的部分留在库里。
解决:整批导入包在一个事务里,任何一行失败就整体回滚,并把失败的行号和原因返回给界面。同时给 Attendance 表建 (EmpID, AttendDate) 唯一约束,让数据库兜底,而不是靠应用层判断——并发导入时应用层判断会失效。
5.3 删除部门时提示外键冲突
现象:人事专员想撤销一个已合并的部门,点删除弹出"DELETE 语句与 REFERENCE 约束冲突"。
原因:Department 被 Employee 和 Post 两张表引用,物理删除必然失败。这不是 bug,是外键在正常工作。
解决:改成软删除,把 Status 置 0,列表页默认只查 Status=1 的部门。如果业务上确实需要物理清理,得先检查有没有在职员工挂在该部门下,有就拦截并提示"该部门下还有 3 名在职员工,请先调岗"。把这条校验写在 BLL 层,顺便在报告里作为"业务完整性约束"的例子,比单纯删掉外键更有说服力。
5.4 触发器导致批量更新直接报错
现象:单条发放工资正常,勾选 50 条批量发放时抛出"事务在触发器中结束,批处理已中止"。
原因:触发器里写了ROLLBACK TRANSACTION,而外层存储过程或 C# 事务还没结束,回滚把外层事务一起干掉,或者触发器里用了游标逐行处理,性能骤降触发超时。
解决:触发器里不要写 ROLLBACK,改为 RAISERROR 或 THROW 抛错让外层决定;触发器逻辑必须基于 inserted/deleted 做集合操作,杜绝逐行游标。另外注意SET NOCOUNT ON一定要加,否则 ADO.NET 会把受影响行数当成结果集读取,偶发报"结果集无列"。
5.5 演示时连接超时,重开就好
现象:连续点几次查询按钮后,界面卡住十几秒然后报超时,重启程序恢复正常。
原因:SqlConnection 或 SqlDataReader 没有释放,连接池被占满。常见写法是var conn = new SqlConnection(...); conn.Open();之后忘了 using 或 Dispose,异常路径下更是彻底漏掉。
解决:所有连接、命令、读取器一律 using;连接字符串里加Max Pool Size=100;Connect Timeout=15,让问题早点暴露而不是无声挂起。上线前用sp_who2看一眼连接数,能看到明显的泄漏痕迹。
6. 验收与答辩:把设计讲成一次有取舍的过程
验收其实在写代码之前就开始了。我一般会先列一张交付清单,对着清单逐项检查,缺一项就在答辩时被动一次:
| 交付物 | 关键要求 | 自检方式 |
|---|---|---|
| 建库脚本 | 一次执行即可建库、建表、建约束、灌初始数据 | 换一台机器从零跑一遍 |
| ER 图与关系模式 | 实体、联系、基数标注完整 | 能指着图解释每根线 |
| 数据字典 | 字段类型、长度、约束、含义 | 与建表脚本逐字段对齐 |
| 视图/存储过程/触发器 | 各有至少一个真实业务用途 | 演示时现场执行一次 |
| 功能演示流程 | 从入职到发薪走通一条完整链路 | 提前彩排两遍,掐时间 |
| 测试说明 | 正常、边界、异常三类用例 | 每个用例附截图或输出 |
演示脚本只有一条主线最有效:新建一个员工,签一份合同,导入几天考勤,生成当月工资,再把他调到另一个部门,最后回到薪资列表指出旧记录的部门快照没变。这条链路一次性覆盖了主外键、事务、存储过程、快照字段四个核心设计点,比逐个功能点开窗体强得多。
答辩提问绕不开三件事:为什么这么设计、不这么设计会怎样、如果数据量涨一百倍怎么办。第一个问题用快照字段和范式取舍回答;第二个问题用"直接删部门会外键冲突"这类具体后果回答;第三个问题老实说课设数据量小、索引和分页已做基础预留,再往上要考虑分区和读写分离,但不在本次范围内。承认边界比硬答更稳。
最后留一个自己的习惯:每次交作业前,我会把数据库完整备份一份,删掉整个库,再从脚本重建一次,用生成的空库跑完整演示流程。有一次就是这一步发现建表脚本里漏了一条 ALTER TABLE,补外键的那句只在我本机执行过,换机器直接报错,要是答辩现场才炸,血泪经验就白攒了。从那以后我每次交付前都强制走一遍"删库重建"的流程,这一步比多写十个功能都让人踏实。希望帮到你。
本文还有配套的精品资源,点击获取