VB.NET生产级考勤系统实战:数据库事务、Windows服务与PDF报表

发布时间:2026/10/11 5:56:59
VB.NET生产级考勤系统实战:数据库事务、Windows服务与PDF报表 简介本资源是一套基于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审计的整套业务流你手头那个用DateTimePickerDataGridView拼出来的“考勤界面”真能接进某制造企业三班倒产线当夜班员工凌晨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.0SSCE作为嵌入式方案它轻量单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;Passwordyourpwd; 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避坑 / 常见问题 / 排查现象服务运行几天后TCP监听突然停止接收新连接但进程仍在原因BeginAcceptTcpClient回调中未重新调用BeginAcceptTcpClient导致监听链断裂解决在OnClientConnect回调末尾必须再次调用_tcpListener.Server.BeginAcceptTcpClient(AddressOf OnClientConnect), Nothing形成循环监听链。现象串口偶尔丢数据特别是连续快速打卡时原因DataReceived事件在UI线程触发而服务无UI线程事件可能被丢弃解决将DataReceived事件处理器改为ThreadPool.QueueUserWorkItem投递到线程池处理或改用SerialPort.BaseStream.ReadAsync异步读取。现象考勤机发送的JSON时间字段含毫秒如2023-10-01T08:02:15.123DateTime.Parse抛异常原因SSCE 4.0 的DATETIME字段精度为秒但.NETDateTime支持毫秒解析时需截断解决用DateTime.TryParseExact(jsonTime, yyyy-MM-ddTHH:mm:ss.fff, ...)或jsonTime.Substring(0, 19)截取到秒。现象服务安装后无法启动事件查看器报“服务没有及时响应启动或控制请求”原因OnStart中执行了耗时操作如连接数据库、初始化大对象解决OnStart只做轻量初始化创建监听器、开线程所有耗时操作移至独立线程或Timer中延时启动。现象多台考勤机同时上传数据库写入变慢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.CoreImports 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 Function4.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考勤系统上线即翻车的真实场景注意以下问题全部来自某制造企业真实部署现场非理论推演。每个问题都对应一次凌晨两点的紧急远程支持。现象考勤机上传数据后数据库里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-01SSCE存入时被截断为1900-01-01SSCE DATETIME的最小有效值解决永远不用DateTime.Parse改用DateTime.TryParseExact(jsonTime, dd/MM/yyyy HH:mm:ss, CultureInfo.InvariantCulture, DateTimeStyles.None, parsedTime)并严格校验parsedTime.Year 2000。现象Windows服务运行一周后内存占用飙升至1.2GB服务无响应原因TcpClient连接未正确释放NetworkStream未关闭导致Socket句柄泄漏解决在HandleTcpClient方法末尾必须显式调用client.Close()和stream.Close()或使用Using语句包裹且client和stream变量需声明为Using块内局部变量确保异常时也能释放。现象导出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。现象用户修改系统时间后打卡记录时间错乱如把电脑时间调到明年打卡记录也变成明年原因DateTime.Now获取的是本地系统时间而非NTP授时时间解决在服务启动时调用NTP服务器校准时间用NtpClient类并将校准后的时间作为CheckTime来源或更彻底——考勤机自身带RTC时钟强制要求其上传数据时携带DeviceTime服务端只做校验不采信PC时间。现象HR在Web端查看考勤报表点击“导出Excel”按钮后IE浏览器弹出“安全警告此网站正试图打开一个ActiveX控件”点击确定后页面空白原因前端用了Response.ContentType application/vnd.ms-excelResponse.Write输出HTML表格IE将其识别为危险ActiveX解决彻底放弃HTML转Excel方案改用服务端生成真正的.xlsx文件用EPPlus库Response.ContentType application/vnd.openxmlformats-officedocument.spreadsheetml.sheet并设置Content-Disposition: attachment; filenamereport.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 /LogToConsoletrue /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 attendancecompany.com $to it-admincompany.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 SourceC:\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 SourceC:\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 border1 classdataframe trth检查项/thth状态/th/tr trtdWindows服务/tdtd$serviceStatus/td/tr trtd数据库连接/tdtd$dbStatus/td/tr trtd昨日打卡记录/tdtd$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天。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询