
简介C#员工考勤管理系统源码是一套基于Visual Studio 2010与SQL Server 2005开发的完整考勤管理项目适合正在学习C#桌面应用或希望接触企业信息化系统的初中级开发者。系统实现了员工信息录入与维护、上下班时间记录、迟到早退与请假自动判别、按日/周/月汇总出勤报表以及多级管理员权限分配代码中涉及ADO.NET数据库操作、三层架构搭建和常用设计模式能够帮助读者串联起界面、业务规则与数据存储的协作过程。压缩包为RAR格式大小约14.56MB上游页面未提供具体文件清单因此文件总数与类型暂不列出。已有313人浏览学习可作为C#与SQL Server项目实战的参考从中获得源码结构划分、数据库表设计思路和考勤统计逻辑的实现细节。1. 时间判断与统计口径先看清这套C#考勤系统源码的边界考勤系统表面上是增删改查真正的难点是某天某员工算不算迟到这类时间判断以及这个月迟到几次这类统计口径。打卡记录缺失、请假冲抵、上下班卡顺序错乱一旦混进来报表数字就会失真。这套C#员工考勤管理系统源码在VS2010里用WinForms做界面通过ADO.NET直连SQL Server 2005把员工信息管理、上下班打卡、出勤统计、异常审核和权限控制串成了完整闭环。它没有ORM包装数据从数据库到界面怎么流动一眼能看清适合刚学完C#基础、想拆一个完整WinForms项目的人也适合需要快速给公司内部搭桌面考勤系统、又不想上重型框架的开发者。VS2010项目在高版本Visual Studio里也能直接打开迁移成本低这是它至今仍被拿来学习的主要原因。2. 三层架构实践先读懂SQL Server 2005里的数据表与ADO.NET访问层2.1 为什么考勤系统必须分UI、BLL、DAL三层考勤的业务规则比界面操作复杂。员工打卡后要判断今天是否已经打过卡、需不需要补记请假记录要冲抵迟到异常打卡要走审核流程。这些规则如果散落在按钮点击事件里改一处就崩一片。三层架构里DAL数据访问层只负责拼SQL、执行增删改查不关心业务含义BLL业务逻辑层处理迟到、早退、请假冲抵这些判断UI层只做展示和输入校验。这套源码把三层结构拆得比较干净读的时候从BLL层入手能最快看清考勤规则的全貌。DAL层选择ADO.NET直连而不是Entity Framework有现实原因。这个项目要跑在SQL Server 2005上当年的EF对老数据库的兼容性和部署复杂度都不划算。ADO.NET的SqlConnection、SqlCommand足够覆盖全部场景SQL可控、出错时排查直观。更实际的好处是如果以后要把后端换成MySQL或国产数据库只需重写DAL层BLL和UI几乎不动这就是考试勤系统架构时最值得复用的部分。2.2 核心表结构员工表、考勤表与状态位设计考勤管理涉及员工档案、打卡记录、请假申请、系统用户四类数据。员工表和考勤表打底请假表单独存放系统用户和角色表控制权限。先看最核心的两张表。字段类型说明EmpIDINT IDENTITY(1,1)员工主键EmpNoVARCHAR(20)工号建唯一约束EmpNameNVARCHAR(50)员工姓名DeptIDINT所属部门PositionNVARCHAR(50)职位字段类型说明AttendanceIDINT IDENTITY(1,1)打卡记录主键EmpIDINT外键关联EmployeeWorkDateDATETIME工作日期只用日期部分CheckInDATETIME上班打卡时间CheckOutDATETIME下班打卡时间StatusTINYINT0正常 1迟到 2早退 3迟到早退 4异常RemarkNVARCHAR(200)备注或审核说明一个容易踩的兼容性坑SQL Server 2005没有DATE类型日期只能存DATETIME。WorkDate看起来是日期实际是DATETIME查询时必须用范围过滤比如 WorkDate Start AND WorkDate DATEADD(DAY, 1, Start)不能直接写等号。源码如果按2005的语法建表换到2008以上版本也不会出问题但反过来从高版本往2005迁移就会报语法错误。CREATE TABLE dbo.Employee ( EmpID INT IDENTITY(1,1) PRIMARY KEY, EmpNo VARCHAR(20) NOT NULL UNIQUE, EmpName NVARCHAR(50) NOT NULL, DeptID INT NOT NULL, Position NVARCHAR(50) ); CREATE TABLE dbo.Attendance ( AttendanceID INT IDENTITY(1,1) PRIMARY KEY, EmpID INT NOT NULL REFERENCES dbo.Employee(EmpID), WorkDate DATETIME NOT NULL, CheckIn DATETIME NULL, CheckOut DATETIME NULL, Status TINYINT NOT NULL DEFAULT 0, Remark NVARCHAR(200) ); CREATE INDEX IX_Attendance_EmpDate ON dbo.Attendance(EmpID, WorkDate);逻辑说明Status用TINYINT存状态而不是中文文本统计时可以直接做聚合判断存储也省状态码含义写在程序枚举里以后新增缺卡这类状态不用改表结构。最后一行联合索引把员工ID和日期绑在一起打卡写入时先走索引去重报表查询时按日期范围过滤性能比全表扫描高一个量级。参数说明DEPTID在这里只存ID不做外键约束是因为老系统里部门表经常重建外键约束反而成了数据导入的障碍考勤表外键保留保证打卡记录不会指向不存在的员工。2.3 ADO.NET连接串与参数化查询老项目最常见的坑连接串统一放在app.config里集中管理。源码默认连本机SQL Server 2005实例部署时主要改三个值Data Source改成目标服务器实例名数据库名改成实际库名验证方式按SQL Server的认证模式调整。connectionStrings add nameAttendanceDB connectionStringData Source.;Initial CatalogAttendanceDB;User IDsa;Password123456; providerNameSystem.Data.SqlClient/ /connectionStrings参数说明Data Source是服务器名加实例名本机默认实例写.或localhost命名实例要写成localhost\SQLEXPRESSInitial Catalog对应数据库名要与附加时指定的名称一致User ID和Password是SQL Server登录账号若用Windows身份验证把这两项替换为Integrated SecuritySSPI。连接字符串里不建议写Connect Timeout默认15秒对单次查询场景够了。DAL层写入员工信息时推荐用参数化查询而不是字符串拼接。public bool InsertEmployee(Employee emp) { string sql INSERT INTO dbo.Employee(EmpNo, EmpName, DeptID, Position) VALUES(EmpNo, EmpName, DeptID, Position); using (SqlConnection conn new SqlConnection( ConfigurationManager.ConnectionStrings[AttendanceDB].ConnectionString)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(EmpNo, emp.EmpNo); cmd.Parameters.AddWithValue(EmpName, emp.EmpName); cmd.Parameters.AddWithValue(DeptID, emp.DeptID); cmd.Parameters.AddWithValue(Position, (object)emp.Position ?? DBNull.Value); conn.Open(); return cmd.ExecuteNonQuery() 0; } }逻辑说明SQL文本里的EmpNo、EmpName是占位符由Parameters集合绑定值后提交给SQL Server数据库收到的是参数类型不是拼接后的字符串。即使EmpName里带单引号或者SQL语句片段也只会被当成普通数据这是防注入的第一道防线。Position可空赋值时用(object)emp.Position ?? DBNull.Value把null显式转成数据库NULL不然AddWithValue会认为是空参数。参数说明using语句保证连接和命令在方法退出时释放避免连接池被占满。这套写法在源码的DAL方法里是统一模板新增表时照着改表名和字段即可不用重新设计。3. WinForms打卡与出勤报表迟到早退判定和按周月统计的C#实现3.1 打卡写入一次点击背后的去重与事务边界打卡动作在BLL层要先回答一个问题同一员工同一天已经打了上班卡再次点击怎么办。常见做法是当天没有记录就插入一行并把当前时间写入CheckIn已有记录且CheckIn为空则补上班卡CheckIn已存在则把当前时间写入CheckOut。数据库写入必须放在事务里否则插入和更新之间出了异常会出现只有上班卡没有下班卡的数据。public bool SaveAttendance(int empID, DateTime punchTime, out string msg) { string connStr ConfigurationManager.ConnectionStrings[AttendanceDB].ConnectionString; using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); SqlTransaction tran conn.BeginTransaction(); try { string sqlCheck SELECT CheckIn FROM dbo.Attendance WHERE EmpID EmpID AND WorkDate WorkDate; DateTime? checkIn; using (SqlCommand cmd new SqlCommand(sqlCheck, conn, tran)) { cmd.Parameters.AddWithValue(EmpID, empID); cmd.Parameters.AddWithValue(WorkDate, punchTime.Date); object result cmd.ExecuteScalar(); checkIn result DBNull.Value ? (DateTime?)null : (DateTime)result; } if (checkIn null) { string sqlInsert INSERT INTO dbo.Attendance(EmpID, WorkDate, CheckIn) VALUES(EmpID, WorkDate, CheckIn); using (SqlCommand insertCmd new SqlCommand(sqlInsert, conn, tran)) { insertCmd.Parameters.AddWithValue(EmpID, empID); insertCmd.Parameters.AddWithValue(WorkDate, punchTime.Date); insertCmd.Parameters.AddWithValue(CheckIn, punchTime); insertCmd.ExecuteNonQuery(); } msg 上班打卡成功; } else { string sqlUpdate UPDATE dbo.Attendance SET CheckOut CheckOut WHERE EmpID EmpID AND WorkDate WorkDate; using (SqlCommand updateCmd new SqlCommand(sqlUpdate, conn, tran)) { updateCmd.Parameters.AddWithValue(EmpID, empID); updateCmd.Parameters.AddWithValue(WorkDate, punchTime.Date); updateCmd.Parameters.AddWithValue(CheckOut, punchTime); updateCmd.ExecuteNonQuery(); } msg 下班打卡成功; } tran.Commit(); return true; } catch { tran.Rollback(); msg 打卡写入失败请联系管理员; return false; } } }逻辑说明事务把查重、插入、更新包成一个原子操作中途报错会把已写入的数据回滚。这里有个细节ExecuteScalar返回值是object类型可能是null也可能是DBNull.Value直接强转成DateTime?会抛InvalidCastException所以先判断DBNull.Value再转换。WorkDate取punchTime.Date保证同一天的多次查询能命中联合索引。参数说明每条命令都单独创建SqlCommand但共享同一个事务可读性比复用一个命令改CommandText更好。打卡方法返回bool和msgUI层直接拿msg弹窗不用自己拼提示语。这个方法是WinForms按钮点击事件里最常调用的入口后续加班、补卡逻辑也挂在它后面扩展。3.2 迟到、早退与状态标记的计算逻辑迟到早退判断的本质是比较TimeOfDay也就是时刻与日期无关。考勤标准上班时间、下班时间和宽限分钟数建议放在常量类或配置表里不要散落在窗体代码里。下面是核心判断方法。public string JudgeStatus(DateTime checkIn, DateTime checkOut, TimeSpan workStart, TimeSpan workEnd, TimeSpan threshold) { if (checkIn.TimeOfDay workStart.Add(threshold)) { if (checkOut.TimeOfDay workEnd) return 迟到早退; return 迟到; } if (checkOut.TimeOfDay workEnd) return 早退; return 正常; }逻辑说明TimeOfDay取当天零点到当前时刻的跨度专门用来比较几点几分。workStart加threshold的意思是允许迟到5分钟或10分钟比直接写死9点灵活。这里要特别注意跨天班次晚上10点上班、次日6点下班的情况WorkEnd按TimeOfDay比较会直接判定早退需要单独设计时间基准源码没有展开这个场景改造时要自己处理。状态判定完成后把返回的中文状态映射成Status枚举值再更新到Attendance表。状态码对应关系如下。枚举值状态含义0正常上下班打卡齐全且未超时1迟到上班打卡晚于宽限时间2早退下班打卡早于规定时间3迟到早退两者都发生4异常缺卡或需人工审核注意请假的员工不应该出现在迟到统计里。常见的做法是先查请假表把请假覆盖的日期从考勤统计里排除再对剩余日期做状态判断。这个先后顺序如果反过来请假员工会被同时记成迟到月报数字就会失真。3.3 按日、周、月出勤统计的SQL写法日报表按员工当天的打卡记录展开周报和月报本质是聚合查询。核心思路是用GROUP BY按员工分组再用CASE WHEN把各种状态计成数字。下面是按月统计员工出勤的SQL。SELECT e.EmpNo, e.EmpName, COUNT(*) AS TotalDays, SUM(CASE WHEN a.Status IN (1, 3) THEN 1 ELSE 0 END) AS LateDays, COUNT(CASE WHEN a.CheckOut IS NULL THEN 1 END) AS MissCheckOut FROM dbo.Attendance a INNER JOIN dbo.Employee e ON a.EmpID e.EmpID WHERE a.WorkDate MonthStart AND a.WorkDate DATEADD(MONTH, 1, MonthStart) GROUP BY e.EmpNo, e.EmpName;逻辑说明WHERE条件用范围过滤而不是在WorkDate上做CONVERT(VARCHAR(7), WorkDate, 120) 是因为函数包裹字段会让索引失效全表扫描在考勤数据量上来之后会明显变慢。COUNT(CASE WHEN CheckOut IS NULL THEN 1 END)统计的是下班卡缺失的天数用这种方式替代COUNT(DISTINCT ...)可以少做一次去重排序。参数说明MonthStart从C#侧传当月第一天例如2024-06-01 00:00:00时间边界不含糊不会漏掉6月30日23点59分的记录。周报的逻辑一样把MonthStart换成DATEADD(WEEK, DATEDIFF(WEEK, 0, GETDATE()), 0)计算出本周一聚合部分完全复用。4. 权限、防注入和UI刷新卡顿考勤系统里最容易翻车的三处细节4.1 把权限做进登录会话而不是只隐藏按钮这套源码的管理员权限分为系统管理员、人事管理员、普通员工三类。权限不能只靠界面隐藏菜单实现用户登录后角色ID要存进会话对象每次执行敏感操作前在BLL层再校验一次。角色和菜单的对应关系用经典RBAC模型角色表、菜单表、角色菜单关联表三张表完成。角色可访问菜单可执行操作系统管理员全部菜单用户管理、系统参数配置人事管理员员工、考勤、报表异常审核、统计导出普通员工个人考勤查询查看本人打卡记录权限判断在BLL层的写法是这样的。public bool HasPermission(int roleID, string menuCode) { const string sql SELECT COUNT(*) FROM dbo.tbRoleMenu rm INNER JOIN dbo.tbMenu m ON rm.MenuID m.MenuID WHERE rm.RoleID RoleID AND m.MenuCode MenuCode; using (SqlConnection conn new SqlConnection(_connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(RoleID, roleID); cmd.Parameters.AddWithValue(MenuCode, menuCode); conn.Open(); return ((int)cmd.ExecuteScalar()) 0; } }逻辑说明按钮是否可见由界面控制但能否执行由BLL层再校验一次这样绕过前端直接调DAL的人也拿不到数据。ExecuteScalar返回首行首列COUNT(*)结果转成int判断即可。每次操作都查一次权限表对考勤系统这种几十上百人的规模完全够用如果是万人并发登录时把菜单权限加载进内存缓存再按角色做版本控制会更合适。参数说明menuCode是菜单唯一编码比如AttendanceReport写在枚举里比直接传字符串更安全。这个方法只接收roleID和menuCode不依赖任何界面状态单元测试时可以直接调用。4.2 参数化查询之外数据库账号权限也要收紧参数化查询挡住的是拼接注入但连接串里如果还挂着sa账号数据库所有权限都暴露给应用层了。防注入的完整做法是参数化查询加最小权限数据库账号两层叠加。给应用单独建账号的SQL如下。USE AttendanceDB; CREATE LOGIN app_user WITH PASSWORD StrongPwd123; CREATE USER app_user FOR LOGIN app_user; GRANT SELECT, INSERT, UPDATE, DELETE ON dbo.Employee TO app_user; GRANT SELECT, INSERT, UPDATE, DELETE ON dbo.Attendance TO app_user;逻辑说明应用连接串改用app_user后即使某个查询真的被注入攻击者也拿不到ALTER、DROP权限连读sys.objects元数据都可能被拦。xp_cmdshell在SQL Server 2005里默认关闭只要不把app_user加进sysadmin角色从注入到提权的路径基本封死。注意每次加新表要记得补一条GRANT不然应用连接串会报权限不足这个错误比SQL语法错误更难定位因为日志里只显示对象访问被拒绝。4.3 报表统计循环里UI刷新卡顿的处理考勤统计要处理几百上千条记录如果在循环里直接往DataGridView逐行填数据窗口会卡到像假死。原因是UI线程被占满窗口消息循环处理不了重绘请求。C#里解决这个问题老派又可靠的做法是BackgroundWorkerVS2010环境下不用装额外包.NET 2.0自带System.ComponentModel就能干。private void btnBuildReport_Click(object sender, EventArgs e) { btnBuildReport.Enabled false; BackgroundWorker worker new BackgroundWorker(); worker.DoWork (s, args) { args.Result attendanceBLL.GetMonthlyReport(month); }; worker.RunWorkerCompleted (s, args) { dataGridView1.DataSource args.Result; btnBuildReport.Enabled true; }; worker.RunWorkerAsync(); }逻辑说明DoWork事件在后台线程执行拿到的数据放在args.Result里整个过程中不碰任何控件RunWorkerCompleted在UI线程回调这时才把结果赋给DataGridView界面在查询期间保持响应。如果要在页面上显示进度条用ReportProgress回传百分比不要在子线程里直接改控件属性否则会抛跨线程访问异常而且这种异常时好时坏比业务逻辑错误更难排查。注意BackgroundWorker的回调理论上回到UI线程但如果主窗体在这个线程上同步等待某个耗时空转回调一样被卡住。耗时逻辑必须完整放进DoWork不能一半放后台一半放前台。5. 部署与排错VS2010 SQL Server 2005环境下附加数据库和连接串修改技巧5.1 用附加数据库方式还原而不是新建库拿到源码包后最省事的还原方式是把mdf直接附加到SQL Server 2005附加库保留表、索引和存储过程的原始定义避免从脚本重建时漏掉某些对象。常见做法是用系统存储过程。EXEC sp_attach_db dbname NAttendanceDB, filename1 ND:\DB\AttendanceDB.mdf, filename2 ND:\DB\AttendanceDB_log.ldf;逻辑说明sp_attach_db在SQL Server 2005下可用如果是2008以上版本建议用CREATE DATABASE ... FOR ATTACH语法。两个文件名分别对应数据和日志文件路径必须与实际存放位置一致否则报文件找不到。附加完成后检查库的所有者把它设为连接串里使用的登录账号避免权限边界问题。5.2 连接串和运行期的常见错误对照部署现场大部分时间花在数据库连接上按出现频率整理成下面的排查表遇到问题对号入座。错误现象常见原因处理方法无法打开数据库登录被拒绝用户名密码与实例认证模式不符确认开启混合认证或连接串改用Integrated SecuritySSPI用户sa登录失败sa被禁用或密码错误用Windows身份登录后在登录名里重置SqlException: 找不到服务器Data Source实例名错误本机默认实例写 .命名实例写 .\SQLEXPRESS启动窗体提示.vb文件类型csproj的Compile项引用路径错误用记事本打开csproj核对.cs文件路径和Build Action打开项目后启动某个窗体文件提示确保已安装文件类型(.vb)的应用程序这个报错实际是项目文件引用丢失。C#项目的窗体以.cs结尾检查.csproj里对应的Compile Include路径是否存在注意路径带中文或者项目目录被移动过VS2010里经常出现这种解析失败。5.3 会看执行计划比凭感觉加索引靠谱如果报表查询明显变慢先别急着往表上加索引。打开一次慢查询的执行计划看有没有Table Scan表扫描节点。考勤表通常增长最快统计聚合时最容易暴露索引设计缺口。SQL Server 2005自带数据库引擎优化顾问可以输入工作负载并给出索引建议按建议创建的索引比拍脑袋建的更贴合实际查询模式。本文还有配套的精品资源点击获取