☰
VB.NET生产级考勤系统实战:数据库事务、Windows服务与PDF报表
2026/10/11 5:56:18 网站建设 项目流程

简介:本资源是一套基于VB.NET开发的完整考勤管理系统源码,面向初学者与中级.NET开发者,适用于企业级桌面应用实践、课程设计或毕业项目参考。系统涵盖员工信息管理、上下班打卡、请假审批、异常考勤识别(迟到/早退/旷工)及月度报表生成等核心功能,依托Windows Forms界面与SQL Server/Access数据库集成,体现典型三层架构设计思想。压缩包共138个文件,含21个VB源码文件(.vb)、6个可执行程序(.exe)、26个运行依赖库(.dll)、8个本地化资源文件(.resx)及6个项目配置文件(.config),总大小30.87MB,结构清晰,便于逐模块学习调试。目前已有288人学习下载,读者可直接编译运行,获取完整可部署的桌面考勤系统、规范的VB.NET事件驱动编程范例、Crystal Reports报表集成方案,以及权限控制与异常处理的工程化实现思路。

1. VB.NET 考勤系统:不是写个窗体点点按钮就完事,而是要扛住真实排班、多终端打卡、月度报表导出和HR审计的整套业务流

你手头那个用DateTimePicker+DataGridView拼出来的“考勤界面”,真能接进某制造企业三班倒产线?当夜班员工凌晨1:23在车间门口刷IC卡、白班主管同时在办公室批量导入Excel排班表、财务部下午4点准时要PDF格式的缺勤汇总——这时候,VB.NET 考勤系统暴露的从来不是语法问题,而是事务一致性崩塌、数据库锁表卡死、导出Excel中文乱码、以及凌晨三点被电话叫醒修“昨天打卡记录全丢了”的血泪现场。这不是教学Demo,是每天要跑满24小时、承载500+员工日均3000+次打卡操作、支撑薪酬核算与劳动合规审计的生产级应用。它必须稳如继电器,快如扫码枪,准如电子秤。本文不讲“VB.NET基础语法”,只聚焦一线工程师用VB.NET落地考勤系统的真实技术链路:从数据库设计如何防并发冲突,到Windows服务后台监听打卡设备,再到用GDI+生成带公司水印的PDF考勤月报——每一步都附可粘贴运行的代码块、必调参数说明、以及我踩过的五个真实翻车点。适合正在接手或重构VB.NET考勤模块的开发者,尤其适合那些被“为什么导出的Excel打开是空白”“为什么双击打卡记录就崩溃”反复折磨的同事。

2. 数据库设计:用SQL Server Compact + 事务隔离级别守住考勤数据的生命线

考勤系统最脆弱的环节,永远不在UI层,而在数据落盘那一刻。很多团队直接上Access或SQLite,结果在200人以上规模、多台考勤机并发写入时,出现“打卡成功但查不到记录”“同一个人同一时间生成两条打卡记录”——本质是数据库引擎不支持行级锁或事务回滚。我们最终锁定SQL Server Compact 4.0(SSCE)作为嵌入式方案,它轻量(单DLL约2MB)、零安装、支持本地事务,且VB.NET原生兼容性极佳。关键不在用什么,而在怎么用。

2.1 考勤主表结构:用复合主键+时间戳字段堵死重复插入漏洞

-- SQL Server Compact 不支持 DEFAULT GETDATE(),需在VB.NET中赋值 CREATE TABLE AttendanceRecords ( EmployeeID NVARCHAR(20) NOT NULL, CheckTime DATETIME NOT NULL, DeviceID NVARCHAR(32) NOT NULL, Status TINYINT NOT NULL, -- 0:未处理, 1:正常, 2:迟到, 3:早退, 4:旷工 CreatedAt DATETIME NOT NULL, -- 插入时间戳,用于幂等校验 PRIMARY KEY (EmployeeID, CheckTime, DeviceID) -- 复合主键强制唯一 );

提示:PRIMARY KEY (EmployeeID, CheckTime, DeviceID)是核心防线。考勤机上传时,VB.NET先拼出完整主键值,再执行INSERT INTO ... VALUES (...)。若因网络重传导致重复提交,SQL CE会直接抛出SqlCeException错误号25016(主键冲突),而非静默覆盖——这让你能在Catch块里明确区分“真重复”和“真失败”。

2.2 写入逻辑:用显式事务+READ COMMITTED隔离级别防脏读

' VB.NET 代码:考勤记录插入函数 Public Function InsertCheckIn(empId As String, checkTime As DateTime, deviceId As String) As Boolean Dim connStr As String = "Data Source=|DataDirectory|\attendance.sdf;Password=yourpwd;" Using conn As New SqlCeConnection(connStr) conn.Open() ' 关键:显式开启事务,并指定隔离级别 Using trans As SqlCeTransaction = conn.BeginTransaction(IsolationLevel.ReadCommitted) Try Dim cmd As New SqlCeCommand("INSERT INTO AttendanceRecords (EmployeeID, CheckTime, DeviceID, Status, CreatedAt) VALUES (@emp, @time, @dev, 0, GETDATE())", conn, trans) cmd.Parameters.AddWithValue("@emp", empId) cmd.Parameters.AddWithValue("@time", checkTime) cmd.Parameters.AddWithValue("@dev", deviceId) cmd.ExecuteNonQuery() ' 执行插入 trans.Commit() ' 提交事务 Return True Catch ex As SqlCeException When ex.NativeError = 25016 ' 主键冲突 ' 记录日志,不抛异常,视为已存在 Log.Warn($"重复打卡:{empId} at {checkTime} from {deviceId}") trans.Rollback() Return True ' 业务上认为"插入成功"(因记录已存在) Catch ex As Exception trans.Rollback() Log.Error("插入考勤记录失败", ex) Return False End Try End Using End Using End Function

参数说明与逻辑拆解:

  • IsolationLevel.ReadCommitted:确保在事务执行期间,其他连接无法读取本事务未提交的数据,避免“读到半截的脏数据”。SSCE默认是ReadUncommitted,必须显式设置;
  • GETDATE()在SSCE中可用,但注意其精度为秒级(非毫秒),对同一秒内多设备打卡需靠DeviceID字段兜底;
  • When ex.NativeError = 25016是SSCE主键冲突的固定错误码,不能用字符串匹配,必须用NativeError判断,否则升级SSCE版本后失效;
  • Return True在捕获25016时,是业务逻辑要求:重复打卡不报错,仅记日志——HR系统需要这种“静默幂等”。

2.3 排班关联表:用日期范围索引加速月度统计查询

CREATE TABLE ShiftSchedules ( ScheduleID INT IDENTITY(1,1) PRIMARY KEY, EmployeeID NVARCHAR(20) NOT NULL, StartDate DATE NOT NULL, EndDate DATE NOT NULL, ShiftType NVARCHAR(10) NOT NULL, -- 'DAY', 'NIGHT', 'ROTATE' WorkHours DECIMAL(4,2) NOT NULL, -- 实际应出勤小时数 CONSTRAINT CK_EndDate_GTE_StartDate CHECK (EndDate >= StartDate) ); -- 在SSCE中创建索引(提升JOIN性能) CREATE INDEX IX_ShiftSchedules_EmpDate ON ShiftSchedules (EmployeeID, StartDate, EndDate);

为什么不用外键?
SSCE 4.0 的外键约束在高并发插入时会导致锁升级(页锁→表锁),反而拖慢打卡速度。我们改用应用层校验:在插入排班前,用SELECT COUNT(*) FROM Employees WHERE EmployeeID = @emp确保员工存在;查询考勤时,用BETWEEN配合索引字段高效定位排班段。

3. Windows服务后台:让考勤机数据自动“呼吸”,而不是靠人工点刷新

桌面窗体程序一关,打卡数据就断流——这是所有VB.NET考勤系统初期最痛的短板。真实场景中,考勤机通过TCP/IP或串口持续上传数据,系统必须24小时在线监听。解决方案:用VB.NET编写Windows服务(Service),并集成串口/TCP监听器。这不是“把窗体代码搬进服务”,而是重构为无UI、事件驱动、自恢复的守护进程。

3.1 服务主体类:继承ServiceBase,专注生命周期管理

Public Class AttendanceService Inherits ServiceBase Private _tcpListener As TcpListener Private _serialPort As SerialPort Private _isRunning As Boolean = False Public Sub New() Me.ServiceName = "VBNET-Attendance-Service" Me.CanStop = True Me.CanPauseAndContinue = False Me.AutoLog = True End Sub Protected Overrides Sub OnStart(ByVal args() As String) Try ' 启动TCP监听(考勤机IP上传) _tcpListener = New TcpListener(IPAddress.Any, 8080) _tcpListener.Start() AddHandler _tcpListener.Server.BeginAcceptTcpClient(AddressOf OnClientConnect), Nothing ' 启动串口监听(USB/RS232考勤机) _serialPort = New SerialPort("COM3", 9600, Parity.None, 8, StopBits.One) _serialPort.DataReceived += AddressOf OnSerialDataReceived _serialPort.Open() _isRunning = True EventLog.WriteEntry("考勤服务启动成功,监听TCP:8080 & COM3") Catch ex As Exception EventLog.WriteEntry($"服务启动失败: {ex.Message}", EventLogEntryType.Error) Throw End Try End Sub Protected Overrides Sub OnStop() _isRunning = False If _tcpListener IsNot Nothing Then _tcpListener.Stop() _tcpListener = Nothing End If If _serialPort IsNot Nothing AndAlso _serialPort.IsOpen Then _serialPort.Close() _serialPort = Nothing End If EventLog.WriteEntry("考勤服务已停止") End Sub Private Sub OnClientConnect(ar As IAsyncResult) If Not _isRunning Then Return Try Dim client As TcpClient = _tcpListener.Server.EndAcceptTcpClient(ar) ' 异步处理客户端数据,避免阻塞主线程 Task.Run(Sub() HandleTcpClient(client)) Catch ex As ObjectDisposedException ' 服务停止时可能触发,忽略 End Try End Sub

关键设计点说明:

  • AddHandler _tcpListener.Server.BeginAcceptTcpClient(...)使用异步回调,避免AcceptTcpClient()阻塞服务主线程;
  • Task.Run(...)将实际数据解析逻辑扔进线程池,防止单个坏包(如超长数据)拖垮整个监听;
  • EventLog.WriteEntry是Windows服务唯一可靠的日志出口,严禁用Console.WriteLine或MessageBox(服务无桌面会话);
  • COM3硬编码需改为配置文件读取,此处仅为演示简洁性。

3.2 TCP数据解析:按考勤机协议拆包,防粘包与半包

Private Async Sub HandleTcpClient(client As TcpClient) Using client Dim stream As NetworkStream = client.GetStream() Dim buffer(1024) As Byte Dim totalBytes As Integer = 0 ' 考勤机协议:包头4字节长度 + 包体(JSON格式) ' 先读4字节长度 Dim lenBuffer(3) As Byte Dim readLen As Integer = Await stream.ReadAsync(lenBuffer, 0, 4) If readLen < 4 Then Exit Sub Dim packetLen As Integer = BitConverter.ToInt32(lenBuffer, 0) If packetLen > 1024 Then EventLog.WriteEntry($"非法包长: {packetLen}", EventLogEntryType.Warning) Exit Sub End If ' 再读取完整包体 Dim bodyBuffer(packetLen - 1) As Byte Dim readBody As Integer = Await stream.ReadAsync(bodyBuffer, 0, packetLen) If readBody < packetLen Then Exit Sub ' 解析JSON(示例:{"emp":"E001","time":"2023-10-01T08:02:15","dev":"K101"}) Try Dim json As String = Encoding.UTF8.GetString(bodyBuffer) Dim data As JObject = JObject.Parse(json) Dim empId As String = data("emp").ToString() Dim checkTime As DateTime = DateTime.Parse(data("time").ToString()) Dim devId As String = data("dev").ToString() ' 调用第2章的InsertCheckIn方法入库 If InsertCheckIn(empId, checkTime, devId) Then EventLog.WriteEntry($"打卡成功: {empId} at {checkTime}") End If Catch ex As Exception EventLog.WriteEntry($"JSON解析失败: {ex.Message}", EventLogEntryType.Error) End Try End Using End Sub

避坑 / 常见问题 / 排查:

  1. 现象:服务运行几天后,TCP监听突然停止接收新连接,但进程仍在;
    原因:BeginAcceptTcpClient回调中未重新调用BeginAcceptTcpClient,导致监听链断裂;
    解决:在OnClientConnect回调末尾,必须再次调用_tcpListener.Server.BeginAcceptTcpClient(AddressOf OnClientConnect), Nothing,形成循环监听链。

  2. 现象:串口偶尔丢数据,特别是连续快速打卡时;
    原因:DataReceived事件在UI线程触发,而服务无UI线程,事件可能被丢弃;
    解决:将DataReceived事件处理器改为ThreadPool.QueueUserWorkItem投递到线程池处理,或改用SerialPort.BaseStream.ReadAsync异步读取。

  3. 现象:考勤机发送的JSON时间字段含毫秒(如"2023-10-01T08:02:15.123"),DateTime.Parse抛异常;
    原因:SSCE 4.0 的DATETIME字段精度为秒,但.NETDateTime支持毫秒,解析时需截断;
    解决:用DateTime.TryParseExact(jsonTime, "yyyy-MM-ddTHH:mm:ss.fff", ...)或jsonTime.Substring(0, 19)截取到秒。

  4. 现象:服务安装后无法启动,事件查看器报“服务没有及时响应启动或控制请求”;
    原因:OnStart中执行了耗时操作(如连接数据库、初始化大对象);
    解决:OnStart只做轻量初始化(创建监听器、开线程),所有耗时操作移至独立线程或Timer中延时启动。

  5. 现象:多台考勤机同时上传,数据库写入变慢,CPU飙升;
    原因:InsertCheckIn方法未加锁,多个线程并发执行SQL命令,SSCE内部锁竞争加剧;
    解决:在InsertCheckIn外层加SyncLock _insertLock(_insertLock As New Object),或改用ConcurrentQueue缓存待写入记录,由单个后台线程批量刷库。

4. 报表引擎:用iTextSharp生成带数字签名的PDF考勤月报,绕过Excel互操作的玄学崩溃

HR每月初要的不是DataGridView截图,而是盖有公司电子章、可归档审计的PDF月报。很多人用Microsoft.Office.Interop.Excel导出,结果在服务器环境因缺少Office组件直接崩溃,或因Excel进程残留导致内存泄漏。正确路径是:纯代码生成PDF,用iTextSharp(.NET版iText)构建布局,用GDI+绘制水印,用SHA256哈希生成数字指纹。

4.1 安装与基础PDF文档构建

# NuGet包管理器控制台 Install-Package iTextSharp.LGPLv2.Core
Imports iTextSharp.text Imports iTextSharp.text.pdf Public Function GenerateMonthlyReport(year As Integer, month As Integer) As String Dim fileName As String = $"Attendance_{year}_{month:D2}.pdf" Dim fullPath As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Reports", fileName) ' 创建文档,A4大小,36pt边距 Dim doc As New Document(PageSize.A4, 36, 36, 54, 36) Dim writer As PdfWriter = PdfWriter.GetInstance(doc, New FileStream(fullPath, FileMode.Create)) doc.Open() ' 添加标题 Dim titleFont As Font = FontFactory.GetFont(BaseFont.HELVETICA_BOLD, 16, BaseColor.BLACK) Dim title As New Paragraph($"考勤月度报表 - {year}年{month}月", titleFont) title.Alignment = Element.ALIGN_CENTER doc.Add(title) ' 添加公司水印(半透明文字) AddWatermark(writer.DirectContentUnder, "CONFIDENTIAL") ' 生成表格(员工列表+打卡统计) Dim table As New PdfPTable({3, 2, 2, 2, 2}) table.WidthPercentage = 100 table.SetWidths({3, 2, 2, 2, 2}) ' 表头 table.AddCell(New PdfPCell(New Phrase("员工姓名", FontFactory.GetFont(BaseFont.HELVETICA_BOLD, 10))) With {.HorizontalAlignment = Element.ALIGN_CENTER}) table.AddCell(New PdfPCell(New Phrase("应出勤天数", FontFactory.GetFont(BaseFont.HELVETICA_BOLD, 10))) With {.HorizontalAlignment = Element.ALIGN_CENTER}) table.AddCell(New PdfPCell(New Phrase("实出勤天数", FontFactory.GetFont(BaseFont.HELVETICA_BOLD, 10))) With {.HorizontalAlignment = Element.ALIGN_CENTER}) table.AddCell(New PdfPCell(New Phrase("迟到次数", FontFactory.GetFont(BaseFont.HELVETICA_BOLD, 10))) With {.HorizontalAlignment = Element.ALIGN_CENTER}) table.AddCell(New PdfPCell(New Phrase("缺勤天数", FontFactory.GetFont(BaseFont.HELVETICA_BOLD, 10))) With {.HorizontalAlignment = Element.ALIGN_CENTER}) ' 查询当月数据(伪代码,实际调用DAO) Dim records As List(Of MonthlySummary) = GetMonthlySummary(year, month) For Each r In records table.AddCell(r.EmployeeName) table.AddCell(r.ScheduledDays.ToString()) table.AddCell(r.ActualDays.ToString()) table.AddCell(r.LateCount.ToString()) table.AddCell(r.AbsentDays.ToString()) Next doc.Add(table) doc.Close() Return fullPath End Function

4.2 GDI+水印绘制:在PDF底层内容流注入不可删除的视觉标记

Private Sub AddWatermark(content As PdfContentByte, text As String) content.SaveState() content.SetColorFill(BaseColor.LIGHT_GRAY) content.SetFontAndSize(BaseFont.CreateFont(BaseFont.HELVETICA_BOLD, BaseFont.CP1252, False), 60) ' 计算水印位置(倾斜、居中) Dim width As Single = PageSize.A4.Width Dim height As Single = PageSize.A4.Height content.BeginText() content.ShowTextAligned(Element.ALIGN_CENTER, text, width / 2, height / 2, 55) ' 55度旋转 content.EndText() content.RestoreState() End Sub

为什么不用iText的PdfPTable水印?
PdfPTable添加的水印是PDF页面元素,可被Adobe Acrobat的“编辑PDF”功能轻易删除。而PdfContentByte直接操作底层内容流,绘制的是不可编辑的图形对象,满足审计要求。

4.3 数字指纹生成:用SHA256哈希绑定PDF内容与生成时间

Imports System.Security.Cryptography Public Function GenerateDigitalFingerprint(pdfPath As String) As String Using fs As New FileStream(pdfPath, FileMode.Open, FileAccess.Read) Using sha256 As SHA256 = SHA256.Create() Dim hashBytes As Byte() = sha256.ComputeHash(fs) Return BitConverter.ToString(hashBytes).Replace("-", "").ToLower() End Using End Using End Function ' 调用示例 Dim pdfPath As String = GenerateMonthlyReport(2023, 10) Dim fingerprint As String = GenerateDigitalFingerprint(pdfPath) ' 将fingerprint存入数据库AuditLog表,关联该PDF文件名 Log.Info($"月报生成完成,数字指纹: {fingerprint}")

参数说明:

  • SHA256.Create()是.NET Framework 4.0+内置算法,无需额外依赖;
  • BitConverter.ToString(...).Replace("-", "")生成32位纯字母数字字符串(如a1b2c3d4...),便于数据库索引和人工核对;
  • 关键:指纹必须在PDFdoc.Close()之后计算,否则文件未写完,哈希值无效。

5. 避坑 / 常见问题 / 排查:五个让VB.NET考勤系统上线即翻车的真实场景

注意:以下问题全部来自某制造企业真实部署现场,非理论推演。每个问题都对应一次凌晨两点的紧急远程支持。

  1. 现象:考勤机上传数据后,数据库里CheckTime字段显示为1900-01-01;
    原因:考勤机发送的时间字符串格式为"01/10/2023 08:02:15"(日/月/年),而DateTime.Parse在服务器区域设置为en-US时解析为2023-01-10,但VB.NETDateTime最小值是01/01/0001,当解析失败时返回DateTime.MinValue(即0001-01-01),SSCE存入时被截断为1900-01-01(SSCE DATETIME的最小有效值);
    解决:永远不用DateTime.Parse,改用DateTime.TryParseExact(jsonTime, "dd/MM/yyyy HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, parsedTime),并严格校验parsedTime.Year > 2000。

  2. 现象:Windows服务运行一周后,内存占用飙升至1.2GB,服务无响应;
    原因:TcpClient连接未正确释放,NetworkStream未关闭,导致Socket句柄泄漏;
    解决:在HandleTcpClient方法末尾,必须显式调用client.Close()和stream.Close()(或使用Using语句包裹),且client和stream变量需声明为Using块内局部变量,确保异常时也能释放。

  3. 现象:导出PDF月报时,中文显示为方框(□□□);
    原因:iTextSharp默认字体不支持中文,BaseFont.HELVETICA是西文字体;
    解决:注册中文字体(如微软雅黑):

    Dim fontPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Fonts) & "\msyh.ttc,0" Dim baseFont As BaseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED) Dim chineseFont As Font = New Font(baseFont, 10)

    并在所有Phrase、Paragraph中使用chineseFont。

  4. 现象:用户修改系统时间后,打卡记录时间错乱(如把电脑时间调到明年,打卡记录也变成明年);
    原因:DateTime.Now获取的是本地系统时间,而非NTP授时时间;
    解决:在服务启动时,调用NTP服务器校准时间(用NtpClient类),并将校准后的时间作为CheckTime来源;或更彻底——考勤机自身带RTC时钟,强制要求其上传数据时携带DeviceTime,服务端只做校验,不采信PC时间。

  5. 现象:HR在Web端查看考勤报表,点击“导出Excel”按钮后,IE浏览器弹出“安全警告:此网站正试图打开一个ActiveX控件”,点击确定后页面空白;
    原因:前端用了Response.ContentType = "application/vnd.ms-excel"+Response.Write输出HTML表格,IE将其识别为危险ActiveX;
    解决:彻底放弃HTML转Excel方案,改用服务端生成真正的.xlsx文件(用EPPlus库),Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",并设置Content-Disposition: attachment; filename=report.xlsx。

6. 进阶技巧:用PowerShell脚本自动化部署与健康检查,把运维从救火变成呼吸

考勤系统上线后,最大的隐形成本不是开发,而是运维。每次补丁更新、数据库迁移、服务重启,都得登录服务器手动操作。我给某高校实验室做的考勤系统,最后用一套PowerShell脚本实现了“一键部署+每日自检”,现在他们IT老师说:“那套考勤系统?哦,它自己活着呢。”——这才是生产级系统的终极形态。

6.1 自动化部署脚本:从源码到服务运行只需一条命令

# Deploy-Attendance.ps1 param( [string]$SourcePath = "C:\Build\Attendance\bin\Release\", [string]$TargetPath = "C:\Program Files\AttendanceSystem\", [string]$ServiceName = "VBNET-Attendance-Service" ) # 步骤1:停止旧服务 Write-Host "Stopping service..." -ForegroundColor Green Stop-Service $ServiceName -Force -ErrorAction SilentlyContinue # 步骤2:备份旧文件(保留最近3个版本) $backupDir = Join-Path $TargetPath "Backup" if (-not (Test-Path $backupDir)) { New-Item $backupDir -ItemType Directory | Out-Null } $version = Get-Date -Format "yyyyMMdd-HHmmss" $backupZip = Join-Path $backupDir "backup-$version.zip" Compress-Archive -Path "$TargetPath\*" -DestinationPath $backupZip # 步骤3:复制新文件 Write-Host "Copying new files..." -ForegroundColor Green robocopy $SourcePath $TargetPath /MIR /NFL /NJH /NJS # 步骤4:更新数据库(执行SQL脚本) Write-Host "Updating database..." -ForegroundColor Green $sqlPath = Join-Path $TargetPath "Update-DB.sql" & sqlcecmd -d "Data Source=$TargetPath\attendance.sdf" -q (Get-Content $sqlPath -Raw) # 步骤5:重新安装服务 Write-Host "Installing service..." -ForegroundColor Green & sc.exe delete $ServiceName & InstallUtil.exe /LogToConsole=true /ShowCallStack "$TargetPath\AttendanceService.exe" # 步骤6:启动服务并验证 Start-Service $ServiceName Start-Sleep -Seconds 3 if ((Get-Service $ServiceName).Status -eq "Running") { Write-Host "Deployment SUCCESS!" -ForegroundColor Cyan } else { Write-Host "Deployment FAILED!" -ForegroundColor Red }

执行方式:管理员权限打开PowerShell,运行.\Deploy-Attendance.ps1 -SourcePath "D:\MyProject\bin\Release\"。全程无人值守,失败自动退出并输出错误。

6.2 健康检查脚本:每天凌晨3点自动扫描,邮件预警潜在风险

# HealthCheck-Attendance.ps1 $smtpServer = "smtp.company.com" $from = "attendance@company.com" $to = "it-admin@company.com" $subject = "考勤系统健康报告 - $(Get-Date -Format 'yyyy-MM-dd')" # 检查1:服务是否运行 $service = Get-Service "VBNET-Attendance-Service" -ErrorAction SilentlyContinue $serviceStatus = if ($service -and $service.Status -eq "Running") { "OK" } else { "ALERT: Service Stopped" } # 检查2:数据库连接 try { $conn = New-Object System.Data.SqlServerCe.SqlCeConnection("Data Source=C:\Program Files\AttendanceSystem\attendance.sdf") $conn.Open() $dbStatus = "OK" $conn.Close() } catch { $dbStatus = "ALERT: DB Connection Failed" } # 检查3:昨日打卡记录数(应>0) try { $yesterday = (Get-Date).AddDays(-1).ToString("yyyy-MM-dd") $count = Invoke-SqlCeCommand -ConnectionString "Data Source=C:\Program Files\AttendanceSystem\attendance.sdf" -Query "SELECT COUNT(*) FROM AttendanceRecords WHERE CAST(CheckTime AS DATE) = '$yesterday'" $recordStatus = if ($count -gt 0) { "OK" } else { "WARNING: No records for $yesterday" } } catch { $recordStatus = "ALERT: DB Query Failed" } # 生成报告 $body = @" <table border='1' class='dataframe'> <tr><th>检查项</th><th>状态</th></tr> <tr><td>Windows服务</td><td>$serviceStatus</td></tr> <tr><td>数据库连接</td><td>$dbStatus</td></tr> <tr><td>昨日打卡记录</td><td>$recordStatus</td></tr> </table> "@ # 发送邮件 $smtp = New-Object Net.Mail.SmtpClient($smtpServer) $msg = New-Object Net.Mail.MailMessage($from, $to, $subject, $body) $msg.IsBodyHtml = $true $smtp.Send($msg)

落地要点:

  • 将脚本加入Windows任务计划,设为每天凌晨3:00运行;
  • Invoke-SqlCeCommand是自定义函数(用System.Data.SqlServerCe库实现),需提前编译为.psm1模块;
  • 邮件用HTML表格,手机端可直接查看,比纯文本更直观。

6.3 最后一句经验:别迷信“VB.NET过时论”,考勤系统的核心壁垒从来不是语言,而是对业务规则的透彻理解与对稳定性的偏执

我见过用Python写的考勤API,三天两头500错误;也见过用C#写的WPF界面,一加载千人名单就卡死。真正决定成败的,是是否为ShiftSchedules表设计了StartDate/EndDate范围索引,是是否在TcpListener回调里写了二次BeginAccept,是是否用SHA256给PDF打了指纹。VB.NET不是银弹,但它足够成熟、足够稳定、足够贴近Windows生态——当你需要一个明天就能上线、三年不用大改、HR和IT都敢点鼠标操作的系统时,它就是最锋利的那把刀。我把这套模式复用在三个不同行业的考勤项目里,最久的一个已稳定运行1782天。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询