简介:这份资源是一套基于 VB.NET 开发的考勤管理系统源码,面向具备一定 VB.NET 与 .NET Framework 基础、希望学习企业级桌面应用开发的开发者。系统围绕员工信息管理、考勤记录、迟到早退旷工等异常处理、报表生成与权限管理展开,采用 Windows Forms 构建界面,并可连接 SQL Server、Access 等数据库完成数据存储与查询,适合作为课程设计、毕业设计或企业考勤工具的参考实现。压缩包共 138 个文件,约 30.87MB,包含 21 个 vb 源码文件、26 个 dll 依赖库、25 个 xml 配置、8 个 resx 与 7 个 resources 资源文件,以及 exe、config、sln、vbproj 等工程与运行文件,完整保留了可编译的项目结构。目前已有 289 人学习下载。读者可从中获取考勤系统的模块划分思路、事件驱动与异步编程的实践方式、异常判定逻辑及报表输出方案,便于快速理解并二次开发。
1. 一个考勤系统从打卡到报表,VB.NET 到底能扛多少事
很多做企业内训或者校园信息化的朋友,一提到考勤系统,第一反应就是“这玩意儿不是随便找个 Web 框架就搞定了吗”。但真落到一个中小型单位、几十号到几百号人、每天四次打卡、月底还要拉出迟到早退统计表的场景里,你会发现用 VB.NET 写一个桌面端考勤系统,反而是最省事、最稳的一条路。这份 VB.NET 考勤系统源码,就是围绕“人员管理 + 打卡记录 + 排班规则 + 统计报表”这条主线搭起来的完整工程,适合刚接触 VB.NET 想找一个能跑通的项目练手的人,也适合手里有类似需求、想直接拿一套骨架去改的开发者。它不追求高并发,也不搞微服务那一套,就是把考勤这件事用最直接的方式做扎实。
2. 项目定位与技术选型:为什么是 WinForms + ADO.NET 这套组合
2.1 这套源码解决的是什么问题
从项目结构来看,这套 VB.NET 考勤系统是一个典型的 C/S 架构桌面应用。它的核心业务闭环是:管理员在后台维护部门、人员和排班规则,员工在客户端进行打卡操作,系统把每次打卡写入数据库,最后按月份、部门、个人三个维度生成考勤统计。它不涉及复杂的审批流,也没有移动端打卡,定位非常清晰——就是给内网环境下的单位用,部署简单,不需要额外配服务器环境,一台装了 .NET Framework 的 Windows 机器就能跑起来。
这种定位决定了它的技术选型不会太花哨。整个项目基于 VB.NET 语言,界面层用 WinForms,数据访问层用 ADO.NET 直接操作数据库,没有引入 ORM 框架,也没有用 WPF 做界面。对于刚上手 VB.NET 的开发者来说,这套组合的好处是“所见即所得”:每个按钮点下去,代码怎么走的、SQL 怎么拼的、数据怎么绑到表格上的,全都能在代码里一眼看到,不存在框架帮你藏了一层又一层的情况。
2.2 技术栈拆解与依赖说明
项目用到的技术栈可以拆成四块来看。第一块是语言和运行时,VB.NET 配合 .NET Framework 4.x,这是 Visual Studio 里创建 WinForms 项目时的默认搭配,兼容性好,XP 之后的 Windows 系统基本都能跑。第二块是界面层,用的是 WinForms 的常用控件——DataGridView 做数据展示、TextBox 和 ComboBox 做输入、DateTimePicker 做日期选择,没有用第三方 UI 库,所以不需要额外装 NuGet 包。第三块是数据访问,用 ADO.NET 的 SqlConnection、SqlCommand、SqlDataAdapter 这一套,数据库选的是 SQL Server,也可以换成 SQL Server Express 本地版,改一下连接字符串就行。第四块是报表输出,常见做法是用 Microsoft Report Viewer 或者直接导出 Excel,源码里如果带了导出功能,一般是引用 Microsoft.Office.Interop.Excel 或者用 StreamWriter 写 CSV。
注意:如果你机器上装的是较新版本的 Visual Studio,打开项目时可能会提示 .NET Framework 版本不匹配,这时候在项目属性里把目标框架改成你本机已有的版本即可,不需要重装环境。
2.3 目录结构长什么样
一个典型的 VB.NET WinForms 考勤系统,目录结构大致是这样的:
AttendanceSystem/ ├── AttendanceSystem.sln ├── AttendanceSystem/ │ ├── My Project/ │ │ ├── AssemblyInfo.vb │ │ ├── Resources.resx │ │ └── Settings.settings │ ├── Forms/ │ │ ├── FrmLogin.vb │ │ ├── FrmMain.vb │ │ ├── FrmEmployee.vb │ │ ├── FrmAttendance.vb │ │ └── FrmReport.vb │ ├── Modules/ │ │ ├── DbHelper.vb │ │ ├── EmployeeModule.vb │ │ └── AttendanceModule.vb │ ├── Models/ │ │ ├── Employee.vb │ │ └── AttendanceRecord.vb │ └── App.config └── Database/ └── AttendanceDB.sqlForms 文件夹放的是各个窗体,Modules 里是业务逻辑和数据访问的封装,Models 里是实体类,Database 文件夹里是建库建表的 SQL 脚本。这种分层方式在 VB.NET 项目里很常见,虽然没有严格按三层架构去拆,但至少把界面、逻辑、数据分开写了,改起来不会牵一发动全身。
3. 核心模块拆解:从登录到打卡的完整链路
3.1 数据库连接与 DbHelper 封装
不管什么业务系统,数据库连接都是第一道坎。这套源码里通常会有一个 DbHelper 模块,把连接字符串、打开连接、执行命令这些重复操作封装起来。下面是一个典型的实现:
Module DbHelper ' 从 App.config 读取连接字符串,避免硬编码 Private ReadOnly connStr As String = ConfigurationManager.ConnectionStrings("AttendanceDB").ConnectionString ' 执行增删改,返回受影响行数 Public Function ExecuteNonQuery(sql As String, params As SqlParameter()) As Integer Using conn As New SqlConnection(connStr) Using cmd As New SqlCommand(sql, conn) If params IsNot Nothing Then cmd.Parameters.AddRange(params) conn.Open() Return cmd.ExecuteNonQuery() End Using End Using End Function ' 执行查询,返回 DataTable,方便直接绑到 DataGridView Public Function ExecuteQuery(sql As String, params As SqlParameter()) As DataTable Using conn As New SqlConnection(connStr) Using cmd As New SqlCommand(sql, conn) If params IsNot Nothing Then cmd.Parameters.AddRange(params) Dim adapter As New SqlDataAdapter(cmd) Dim dt As New DataTable() adapter.Fill(dt) Return dt End Using End Using End Function End Module这段代码的关键点有两个。一是连接字符串从 App.config 读,不写死在代码里,换数据库的时候只改配置文件。二是用 Using 语句包住 SqlConnection 和 SqlCommand,保证用完就释放,不会因为忘记 Close 导致连接池被占满。参数化查询通过 SqlParameter 传入,避免拼接 SQL 字符串带来的注入风险。参数说明上,sql 是带占位符的语句,比如"SELECT * FROM Employee WHERE DeptId = @DeptId",params 是对应的参数数组。
3.2 登录验证与权限区分
登录窗体 FrmLogin 的逻辑不复杂,但它是整个系统的入口,权限区分也在这里做。用户输入工号和密码后,系统去 Employee 表里查匹配记录,如果查到并且密码正确,就把员工角色写进一个全局变量或者 Session 类似的模块里,然后打开主窗体。
Private Sub btnLogin_Click(sender As Object, e As EventArgs) Handles btnLogin.Click Dim sql As String = "SELECT EmpId, EmpName, Role FROM Employee WHERE EmpNo = @EmpNo AND Password = @Pwd" Dim params As SqlParameter() = { New SqlParameter("@EmpNo", txtEmpNo.Text.Trim()), New SqlParameter("@Pwd", txtPwd.Text.Trim()) } Dim dt As DataTable = DbHelper.ExecuteQuery(sql, params) If dt.Rows.Count = 0 Then MessageBox.Show("工号或密码错误,请重新输入。") Return End If ' 把登录信息存到全局模块,后续窗体直接读 CurrentUser.EmpId = CInt(dt.Rows(0)("EmpId")) CurrentUser.EmpName = dt.Rows(0)("EmpName").ToString() CurrentUser.Role = dt.Rows(0)("Role").ToString() Me.Hide() Dim main As New FrmMain() main.ShowDialog() Me.Close() End Sub这里有个细节值得注意:密码在数据库里是明文存储的。对于内网小系统来说,这样写能跑,但如果要稍微正规一点,常见做法是在存密码的时候做一次 MD5 或者 SHA256 哈希,登录时把输入的密码同样哈希后再比对。改起来不难,在 EmployeeModule 里加一个哈希函数就行。
3.3 打卡记录写入与重复打卡判断
打卡是考勤系统的核心动作。员工在主界面点“上班打卡”或“下班打卡”,系统要做三件事:确认当前时间、判断今天这个时段是否已经打过卡、把记录写进 AttendanceRecord 表。
Public Function AddAttendance(empId As Integer, punchType As String) As Boolean Dim today As Date = Date.Today ' 先查今天该时段是否已有记录,避免重复打卡 Dim checkSql As String = "SELECT COUNT(*) FROM AttendanceRecord WHERE EmpId = @EmpId AND PunchDate = @Date AND PunchType = @Type" Dim checkParams As SqlParameter() = { New SqlParameter("@EmpId", empId), New SqlParameter("@Date", today), New SqlParameter("@Type", punchType) } Dim count As Integer = Convert.ToInt32(DbHelper.ExecuteScalar(checkSql, checkParams)) If count > 0 Then Return False ' 写入打卡记录,PunchTime 精确到秒 Dim insertSql As String = "INSERT INTO AttendanceRecord (EmpId, PunchDate, PunchTime, PunchType) VALUES (@EmpId, @Date, @Time, @Type)" Dim insertParams As SqlParameter() = { New SqlParameter("@EmpId", empId), New SqlParameter("@Date", today), New SqlParameter("@Time", Date.Now), New SqlParameter("@Type", punchType) } Return DbHelper.ExecuteNonQuery(insertSql, insertParams) > 0 End Function这段逻辑里,PunchType 一般用“上班”“下班”两个值区分。重复打卡判断是先查后插,严格来说在高并发下会有竞态问题,但内网几十个人的场景完全够用。如果想让数据库层面兜底,可以在 AttendanceRecord 表上给 EmpId + PunchDate + PunchType 建一个唯一索引,插入重复时直接报错,代码里捕获异常提示“今天已经打过卡了”。
3.4 考勤统计与报表生成
统计模块是整个系统里 SQL 写得最重的地方。以月度统计为例,需要把每个员工当月的打卡记录按日期分组,算出每天的最早打卡时间和最晚打卡时间,再和排班表里的标准上下班时间比对,得出迟到、早退、缺卡的次数。
Public Function GetMonthlyReport(deptId As Integer, year As Integer, month As Integer) As DataTable Dim sql As String = " SELECT e.EmpNo, e.EmpName, COUNT(DISTINCT a.PunchDate) AS AttendDays, SUM(CASE WHEN a.PunchType = '上班' AND CONVERT(TIME, a.PunchTime) > '09:00:00' THEN 1 ELSE 0 END) AS LateCount, SUM(CASE WHEN a.PunchType = '下班' AND CONVERT(TIME, a.PunchTime) < '18:00:00' THEN 1 ELSE 0 END) AS EarlyCount FROM Employee e LEFT JOIN AttendanceRecord a ON e.EmpId = a.EmpId AND YEAR(a.PunchDate) = @Year AND MONTH(a.PunchDate) = @Month WHERE e.DeptId = @DeptId GROUP BY e.EmpNo, e.EmpName ORDER BY e.EmpNo" Dim params As SqlParameter() = { New SqlParameter("@Year", year), New SqlParameter("@Month", month), New SqlParameter("@DeptId", deptId) } Return DbHelper.ExecuteQuery(sql, params) End Function这个查询用 LEFT JOIN 保证即使某个员工当月一条打卡记录都没有,也会出现在报表里,显示出勤天数为 0。LateCount 和 EarlyCount 用 CASE WHEN 做条件计数,比在 VB.NET 里循环 DataTable 逐行判断要快得多。查出来的 DataTable 直接绑到 FrmReport 的 DataGridView 上,再加一个导出按钮,用 StreamWriter 写成 CSV 文件,Excel 打开就能看。
4. 避坑与常见问题:那些第一次跑容易翻车的地方
4.1 连接字符串写错导致连不上数据库
现象是程序一启动就弹异常,提示“未找到连接字符串”或者“登录失败”。原因通常是 App.config 里的 name 属性和代码里 ConfigurationManager.ConnectionStrings 取的键名不一致,或者 SQL Server 实例名写成了默认的.\SQLEXPRESS但本机装的是完整版。解决办法是打开 App.config 确认 connectionString 的 name 和代码里完全一致,然后去 SQL Server 配置管理器里确认实例名和 TCP/IP 协议是否启用。
4.2 DataGridView 绑定后不刷新
打卡之后回到记录列表,发现新数据没出来,手动关掉窗体再打开才有。原因是 DataGridView 绑定的 DataTable 是一次性查询结果,不会自动感知数据库变化。解决方式是在打卡成功后重新调用一次查询方法,把新的 DataTable 重新赋给 DataSource,或者用 BindingSource 做一层中转,调用 ResetBindings(False) 强制刷新。
4.3 日期时间格式在不同区域设置下解析失败
现象是打卡记录写入时抛“字符串转换为 DateTime 失败”。原因是代码里用了CDate("2024/01/15")这种依赖区域设置的转换,在某些系统上会按月/日/年解析。解决办法是统一用DateTime.ParseExact指定格式,或者在 SQL 参数里直接传 DateTime 类型的值,让 ADO.NET 去处理格式转换,不要自己拼字符串。
4.4 导出 Excel 时提示“未安装 Office”
如果报表导出用的是 Microsoft.Office.Interop.Excel,那目标机器上没装 Office 就会直接报错。这是很多内网机器的通病。解决方式有两种:一是改成导出 CSV,用 StreamWriter 写逗号分隔文件,Excel 照样能打开;二是换成 NPOI 这类不需要装 Office 的库,但需要额外引入 DLL。
4.5 多用户同时打卡时记录丢失
现象是两个人几乎同时点打卡,结果只有一条记录写进去了。原因是先查后插的逻辑在并发下存在窗口期。解决办法是在 AttendanceRecord 表的 EmpId + PunchDate + PunchType 上建唯一约束,插入冲突时捕获 SqlException,提示“请勿重复打卡”,这样即使并发也不会丢数据。
5. 二次开发与改造:从能跑到好用还差哪几步
5.1 把密码明文改成哈希存储
这是最值得先做的一步改造。在 EmployeeModule 里加一个哈希函数,用 SHA256 对密码加盐后存库,登录时同样处理再比对。改动量很小,但安全性提升明显。具体做法是在 Employee 表加一个 Salt 字段,每个用户注册时生成随机盐值,存Hash(password + salt),验证时用同样的盐重新计算。
5.2 增加排班规则表,支持弹性上下班
现在的统计逻辑把上班时间硬编码成 09:00,下班硬编码成 18:00。如果单位有多个班次,就需要加一张 Schedule 表,字段包括 ScheduleId、ScheduleName、WorkStart、WorkEnd,Employee 表加一个 ScheduleId 外键。统计查询里把硬编码的时间换成 JOIN Schedule 表取对应班次的时间,这样不同部门可以套不同班次,改起来也不影响已有数据。
5.3 用参数化配置替换硬编码
项目里凡是出现“09:00”“18:00”“迟到阈值”这类数字的地方,都建议抽到一个 Config 表或者 App.config 的 appSettings 节点里。比如加一个LateThresholdMinutes配置项,统计迟到时用WorkStart + LateThresholdMinutes去比对,这样调整宽限时间不用改代码重新编译。
5.4 报表导出换成 NPOI 或直接写 CSV
如果目标部署环境不确定有没有 Office,建议把导出功能改成写 CSV。下面是一个简单的 CSV 导出方法:
Public Sub ExportToCsv(dt As DataTable, filePath As String) Dim sb As New StringBuilder() ' 写表头 Dim headers As String() = dt.Columns.Cast(Of DataColumn)().Select(Function(c) c.ColumnName).ToArray() sb.AppendLine(String.Join(",", headers)) ' 写数据行,字段里如果有逗号就加引号包起来 For Each row As DataRow In dt.Rows Dim fields As String() = row.ItemArray.Select( Function(f) If(f.ToString().Contains(","), """" & f.ToString() & """", f.ToString()) ).ToArray() sb.AppendLine(String.Join(",", fields)) Next File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8) End Sub这个方法不依赖任何外部库,生成的 CSV 用 Excel 打开中文也不会乱码,因为指定了 UTF8 编码。参数 dt 是统计查询返回的 DataTable,filePath 是保存路径,调用前用 SaveFileDialog 让用户选位置就行。
5.5 验证改造是否成功的几个检查点
改完之后怎么确认没改坏?我一般会走一遍这几个检查点:第一,用两个不同角色的账号登录,确认权限区分仍然生效;第二,手动改系统时间到上下班边界,测试迟到早退判断是否按新规则走;第三,连续打两次同类型卡,确认重复打卡拦截还在;第四,导出报表后用 Excel 打开,检查中文列名和数据行数是否和界面一致;第五,把 App.config 里的连接字符串指向一个新库,跑一遍建表脚本,确认部署流程没有遗漏。
从那以后我每次改完考勤统计相关的 SQL,都会先把系统时间调到月初和月末各跑一次报表,确认跨月边界不会漏记录或者多算天数。这个习惯帮我省了好几次被业务方追着问“为什么上个月的数据对不上”的麻烦。希望这份拆解能帮到你,源码拿下去先跑通登录和打卡,再慢慢往统计和报表上改,节奏会顺很多。
本文还有配套的精品资源,点击获取