Option Explicit' 排班与登录匹配校验系统' 作者:数据分析部' 版本:2.0' 最后更新:2023-10-15' 常量定义Const SCHEDULE_SHEET As String = "排班表"Const LOGIN_SHEET As String = "登录记录"Const RESULT_SHEET As String = "核对结果"Const OUTPUT_PATH As String = "C:\考勤报表\"' 员工状态枚举Enum EmployeeStatus STATUS_ON_TIME = 1 STATUS_LATE = 2 STATUS_ABSENT = 3 STATUS_LEAVE = 4 STATUS_OVERTIME = 5End Enum' 主程序Sub MatchScheduleAndLogin() Dim startTime As Double startTime = Timer Application.ScreenUpdating = False Application.DisplayAlerts = False Application.Calculation = xlCalculationManual Application.EnableEvents = False On Error GoTo ErrorHandler ' 记录日志 LogMessage "开始执行排班登录匹配校验", "INFO" ' 1. 初始化 If Not InitializeWorkbook() Then LogMessage "工作簿初始化失败", "ERROR" Exit Sub End If ' 2. 加载数据 Dim scheduleData As Variant Dim loginData As Variant scheduleData = LoadScheduleData() If IsEmpty(scheduleData) Then LogMessage "排班数据加载失败", "ERROR" Exit Sub End If loginData = LoadLoginData() If IsEmpty(loginData) Then LogMessage "登录数据加载失败", "ERROR" Exit Sub End If LogMessage "数据加载完成,排班记录:" & UBound(scheduleData) - 1 & "条,登录记录:" & UBound(loginData) - 1 & "条", "INFO" ' 3. 执行匹配 Dim matchResults As Collection Set matchResults = MatchData(scheduleData, loginData) ' 4. 输出结果 If Not ExportResults(matchResults) Then LogMessage "结果导出失败", "ERROR" Exit Sub End If ' 5. 生成报告 If Not GenerateReport(matchResults) Then LogMessage "报告生成失败", "ERROR" Exit Sub End If ' 6. 发送通知 If Not SendNotifications(matchResults) Then LogMessage "通知发送失败", "WARNING" End If Dim endTime As Double endTime = Timer LogMessage "执行完成,耗时:" & Format(endTime - startTime, "0.00") & "秒", "INFO" ' 显示汇总信息 ShowSummary matchResults Exit SubErrorHandler: LogMessage "错误 " & Err.Number & ": " & Err.Description & " (行号:" & Erl & ")", "ERROR" Application.ScreenUpdating = True Application.DisplayAlerts = True Application.Calculation = xlCalculationAutomatic Application.EnableEvents = True MsgBox "程序执行出错,请查看日志!", vbCritical, "错误"End Sub' 初始化工作簿Function InitializeWorkbook() As Boolean On Error GoTo ErrorHandler ' 检查必要的工作表 Dim wsSchedule As Worksheet, wsLogin As Worksheet, wsResult As Worksheet ' 排班表 On Error Resume Next Set wsSchedule = ThisWorkbook.Worksheets(SCHEDULE_SHEET) On Error GoTo ErrorHandler If wsSchedule Is Nothing Then LogMessage "找不到排班表:" & SCHEDULE_SHEET, "ERROR" InitializeWorkbook = False Exit Function End If ' 登录记录表 On Error Resume Next Set wsLogin = ThisWorkbook.Worksheets(LOGIN_SHEET) On Error GoTo ErrorHandler If wsLogin Is Nothing Then LogMessage "找不到登录记录表:" & LOGIN_SHEET, "ERROR" InitializeWorkbook = False Exit Function End If ' 删除旧的核对结果表 On Error Resume Next Application.DisplayAlerts = False ThisWorkbook.Worksheets(RESULT_SHEET).Delete Application.DisplayAlerts = True On Error GoTo ErrorHandler ' 创建新的核对结果表 Set wsResult = ThisWorkbook.Worksheets.Add wsResult.Name = RESULT_SHEET InitializeWorkbook = True Exit FunctionErrorHandler: InitializeWorkbook = FalseEnd Function' 加载排班数据Function LoadScheduleData() As Variant Dim wsSchedule As Worksheet Dim lastRow As Long, lastCol As Long Dim dataRange As Range Dim dataArray As Variant Set wsSchedule = ThisWorkbook.Worksheets(SCHEDULE_SHEET) ' 查找数据范围 lastRow = wsSchedule.Cells(wsSchedule.Rows.Count, 1).End(xlUp).Row lastCol = wsSchedule.Cells(1, wsSchedule.Columns.Count).End(xlToLeft).Column ' 检查数据有效性 If lastRow <= 1 Then LogMessage "排班表无数据", "ERROR" LoadScheduleData = Array() Exit Function End If ' 检查必要的列 Dim empIdCol As Long, nameCol As Long, scheduleTimeCol As Long, deptCol As Long, groupCol As Long empIdCol = FindColumn(wsSchedule, "工号", 1, lastCol) nameCol = FindColumn(wsSchedule, "姓名", 1, lastCol) scheduleTimeCol = FindColumn(wsSchedule, "应到时间", 1, lastCol) deptCol = FindColumn(wsSchedule, "部门", 1, lastCol) groupCol = FindColumn(wsSchedule, "组别", 1, lastCol) If empIdCol = 0 Or nameCol = 0 Or scheduleTimeCol = 0 Then LogMessage "排班表缺少必要列:工号、姓名、应到时间", "ERROR" LoadScheduleData = Array() Exit Function End If ' 读取数据 Set dataRange = wsSchedule.Range(wsSchedule.Cells(1, 1), wsSchedule.Cells(lastRow, lastCol)) dataArray = dataRange.Value ' 验证数据格式 Dim i As Long, errorCount As Long errorCount = 0 For i = 2 To UBound(dataArray, 1) ' 检查工号 If Len(Trim(dataArray(i, empIdCol))) = 0 Then LogMessage "第" & i & "行工号为空", "WARNING" errorCount = errorCount + 1 End If ' 检查应到时间 If Not IsDate(dataArray(i, scheduleTimeCol)) Then LogMessage "第" & i & "行应到时间格式错误:" & dataArray(i, scheduleTimeCol), "WARNING" errorCount = errorCount + 1 Else ' 标准化时间格式 dataArray(i, scheduleTimeCol) = CDate(dataArray(i, scheduleTimeCol)) End If Next i If errorCount > 0 Then LogMessage "排班表发现" & errorCount & "个数据问题,已尝试自动修复", "WARNING" End If LoadScheduleData = dataArray Exit FunctionEnd Function' 加载登录数据Function LoadLoginData() As Variant Dim wsLogin As Worksheet Dim lastRow As Long, lastCol As Long Dim dataRange As Range Dim dataArray As Variant Set wsLogin = ThisWorkbook.Worksheets(LOGIN_SHEET) ' 查找数据范围 lastRow = wsLogin.Cells(wsLogin.Rows.Count, 1).End(xlUp).Row lastCol = wsLogin.Cells(1, wsLogin.Columns.Count).End(xlToLeft).Column ' 检查数据有效性 If lastRow <= 1 Then LogMessage "登录表无数据", "ERROR" LoadLoginData = Array() Exit Function End If ' 检查必要的列 Dim empIdCol As Long, loginTimeCol As Long, logoutTimeCol As Long, systemCol As Long empIdCol = FindColumn(wsLogin, "工号", 1, lastCol) loginTimeCol = FindColumn(wsLogin, "登录时间", 1, lastCol) logoutTimeCol = FindColumn(wsLogin, "退出时间", 1, lastCol) systemCol = FindColumn(wsLogin, "登录系统", 1, lastCol) If empIdCol = 0 Or loginTimeCol = 0 Then LogMessage "登录表缺少必要列:工号、登录时间", "ERROR" LoadLoginData = Array() Exit Function End If ' 读取数据 Set dataRange = wsLogin.Range(wsLogin.Cells(1, 1), wsLogin.Cells(lastRow, lastCol)) dataArray = dataRange.Value ' 验证数据格式 Dim i As Long, errorCount As Long errorCount = 0 For i = 2 To UBound(dataArray, 1) ' 检查工号 If Len(Trim(dataArray(i, empIdCol))) = 0 Then LogMessage "登录表第" & i & "行工号为空", "WARNING" errorCount = errorCount + 1 End If ' 检查登录时间 If Len(Trim(dataArray(i, loginTimeCol))) > 0 Then If Not IsDate(dataArray(i, loginTimeCol)) Then LogMessage "登录表第" & i & "行登录时间格式错误:" & dataArray(i, loginTimeCol), "WARNING" errorCount = errorCount + 1 Else ' 标准化时间格式 dataArray(i, loginTimeCol) = CDate(dataArray(i, loginTimeCol)) End If End If Next i If errorCount > 0 Then LogMessage "登录表发现" & errorCount & "个数据问题,已尝试自动修复", "WARNING" End If LoadLoginData = dataArray Exit FunctionEnd Function' 查找列Function FindColumn(ws As Worksheet, headerName As String, startCol As Long, endCol As Long) As Long Dim col As Long FindColumn = 0 For col = startCol To endCol If Trim(ws.Cells(1, col).Value) = headerName Then FindColumn = col Exit Function End If Next col ' 如果没找到,尝试模糊匹配 For col = startCol To endCol If InStr(1, ws.Cells(1, col).Value, headerName, vbTextCompare) > 0 Then FindColumn = col LogMessage "模糊匹配到列头:'" & ws.Cells(1, col).Value & "' -> '" & headerName & "'", "INFO" Exit Function End If Next col ' 如果还是没找到,记录警告 LogMessage "未找到列头:" & headerName, "WARNING"End Function' 匹配数据Function MatchData(scheduleData As Variant, loginData As Variant) As Collection Dim results As New Collection Dim i As Long, j As Long Dim scheduleRow As Long, loginRow As Long Dim empIdCol As Long, nameCol As Long, scheduleTimeCol As Long, deptCol As Long, groupCol As Long Dim loginEmpIdCol As Long, loginTimeCol As Long, logoutTimeCol As Long, systemCol As Long ' 获取排班表列索引 Dim wsSchedule As Worksheet Set wsSchedule = ThisWorkbook.Worksheets(SCHEDULE_SHEET) empIdCol = FindColumn(wsSchedule, "工号", 1, UBound(scheduleData, 2)) nameCol = FindColumn(wsSchedule, "姓名", 1, UBound(scheduleData, 2)) scheduleTimeCol = FindColumn(wsSchedule, "应到时间", 1, UBound(scheduleData, 2)) deptCol = FindColumn(wsSchedule, "部门", 1, UBound(scheduleData, 2)) groupCol = FindColumn(wsSchedule, "组别", 1, UBound(scheduleData, 2)) ' 获取登录表列索引 Dim wsLogin As Worksheet Set wsLogin = ThisWorkbook.Worksheets(LOGIN_SHEET) loginEmpIdCol = FindColumn(wsLogin, "工号", 1, UBound(loginData, 2)) loginTimeCol = FindColumn(wsLogin, "登录时间", 1, UBound(loginData, 2)) logoutTimeCol = FindColumn(wsLogin, "退出时间", 1, UBound(loginData, 2)) systemCol = FindColumn(wsLogin, "登录系统", 1, UBound(loginData, 2)) ' 创建字典以提高查找效率 Dim loginDict As Object Set loginDict = CreateObject("Scripting.Dictionary") ' 将登录数据加载到字典 For i = 2 To UBound(loginData, 1) Dim empId As String empId = Trim(loginData(i, loginEmpIdCol)) If Len(empId) > 0 Then If Not loginDict.Exists(empId) Then Dim loginInfo As Object Set loginInfo = CreateObject("Scripting.Dictionary") loginInfo("登录时间") = loginData(i, loginTimeCol) loginInfo("退出时间") = IIf(logoutTimeCol > 0, loginData(i, logoutTimeCol), "") loginInfo("登录系统") = IIf(systemCol > 0, loginData(i, systemCol), "") loginDict.Add empId, loginInfo End If End If Next i LogMessage "登录字典加载完成,共" & loginDict.Count & "条记录", "INFO" ' 遍历排班表进行匹配 For i = 2 To UBound(scheduleData, 1) Dim result As Object Set result = CreateObject("Scripting.Dictionary") ' 基础信息 result("序号") = i - 1 result("工号") = Trim(scheduleData(i, empIdCol)) result("姓名") = IIf(nameCol > 0, scheduleData(i, nameCol), "") result("应到时间") = scheduleData(i, scheduleTimeCol) result("部门") = IIf(deptCol > 0, scheduleData(i, deptCol), "") result("组别") = IIf(groupCol > 0, scheduleData(i, groupCol), "") ' 查找登录记录 Dim empIdKey As String empIdKey = Trim(scheduleData(i, empIdCol)) If loginDict.Exists(empIdKey) Then Dim empLoginInfo As Object Set empLoginInfo = loginDict(empIdKey) result("登录时间") = empLoginInfo("登录时间") result("退出时间") = empLoginInfo("退出时间") result("登录系统") = empLoginInfo("登录系统") ' 判断状态 Dim scheduleTime As Date, loginTime As Date scheduleTime = CDate(scheduleData(i, scheduleTimeCol)) loginTime = CDate(empLoginInfo("登录时间")) ' 计算迟到分钟数 Dim lateMinutes As Long lateMinutes = DateDiff("n", scheduleTime, loginTime) If lateMinutes <= 0 Then ' 按时或早到 result("状态") = "按时到岗" result("状态代码") = STATUS_ON_TIME result("迟到分钟") = 0 result("备注") = "按时登录" ElseIf lateMinutes <= 30 Then ' 迟到30分钟内 result("状态") = "迟到(" & lateMinutes & "分钟)" result("状态代码") = STATUS_LATE result("迟到分钟") = lateMinutes result("备注") = "迟到" & lateMinutes & "分钟" Else ' 迟到超过30分钟 result("状态") = "严重迟到(" & lateMinutes & "分钟)" result("状态代码") = STATUS_LATE result("迟到分钟") = lateMinutes result("备注") = "严重迟到" & lateMinutes & "分钟" End If Else ' 未登录 result("登录时间") = "" result("退出时间") = "" result("登录系统") = "" result("状态") = "未登录" result("状态代码") = STATUS_ABSENT result("迟到分钟") = 0 result("备注") = "未登录系统" End If ' 添加到结果集合 results.Add result Next i LogMessage "数据匹配完成,共处理" & results.Count & "条排班记录", "INFO" Set MatchData = resultsEnd Function' 导出结果Function ExportResults(results As Collection) As Boolean Dim wsResult As Worksheet Dim i As Long, col As Long Dim headers() As Variant Dim dataArray() As Variant Set wsResult = ThisWorkbook.Worksheets(RESULT_SHEET) ' 定义表头 headers = Array("序号", "工号", "姓名", "部门", "组别", "应到时间", "登录时间", _ "退出时间", "登录系统", "状态", "迟到分钟", "备注") ' 准备数据数组 ReDim dataArray(1 To results.Count + 1, 1 To UBound(headers) + 1) ' 写入表头 For col = 1 To UBound(headers) + 1 dataArray(1, col) = headers(col - 1) Next col ' 写入数据 For i = 1 To results.Count Dim result As Object Set result = results(i) dataArray(i + 1, 1) = result("序号") dataArray(i + 1, 2) = result("工号") dataArray(i + 1, 3) = result("姓名") dataArray(i + 1, 4) = result("部门") dataArray(i + 1, 5) = result("组别") dataArray(i + 1, 6) = result("应到时间") dataArray(i + 1, 7) = result("登录时间") dataArray(i + 1, 8) = result("退出时间") dataArray(i + 1, 9) = result("登录系统") dataArray(i + 1, 10) = result("状态") dataArray(i + 1, 11) = result("迟到分钟") dataArray(i + 1, 12) = result("备注") Next i ' 写入工作表 wsResult.Range("A1").Resize(UBound(dataArray, 1), UBound(dataArray, 2)).Value = dataArray ' 格式化工作表 Call FormatResultSheet(wsResult, results.Count) ExportResults = TrueEnd Function' 格式化结果表Sub FormatResultSheet(ws As Worksheet, dataCount As Long) Dim lastRow As Long, lastCol As Long Dim i As Long lastRow = dataCount + 1 lastCol = 12 ' 列数 ' 设置列宽 ws.Columns("A:A").ColumnWidth = 6 ws.Columns("B:B").ColumnWidth = 10 ws.Columns("C:C").ColumnWidth = 8 ws.Columns("D:E").ColumnWidth = 12 ws.Columns("F:F").ColumnWidth = 12 ws.Columns("G:H").ColumnWidth = 18 ws.Columns("I:I").ColumnWidth = 10 ws.Columns("J:J").ColumnWidth = 20 ws.Columns("K:K").ColumnWidth = 10 ws.Columns("L:L").ColumnWidth = 20 ' 格式化表头 With ws.Range("A1:L1") .Font.Bold = True .HorizontalAlignment = xlCenter .VerticalAlignment = xlCenter .Interior.Color = RGB(91, 155, 213) .Font.Color = RGB(255, 255, 255) .Borders(xlEdgeBottom).LineStyle = xlContinuous .Borders(xlEdgeBottom).Weight = xlThick End With ' 设置时间格式 With ws.Range("F2:G" & lastRow) .NumberFormat = "yyyy-mm-dd hh:mm" .HorizontalAlignment = xlCenter End With With ws.Range("H2:H" & lastRow) .NumberFormat = "hh:mm" .HorizontalAlignment = xlCenter End With ' 设置数值格式 With ws.Range("K2:K" & lastRow) .NumberFormat = "0" .HorizontalAlignment = xlCenter End With ' 应用条件格式 Call ApplyConditionalFormatting(ws, lastRow) ' 添加筛选 ws.Range("A1:L1").AutoFilter ' 冻结窗格 ws.Range("A2").Select ActiveWindow.FreezePanes = True ' 添加标题 ws.Range("N1").Value = "排班登录匹配结果" ws.Range("N1").Font.Bold = True ws.Range("N1").Font.Size = 16 ws.Range("N1").Font.Color = RGB(31, 78, 120) ws.Range("N2").Value = "生成时间:" & Now() ws.Range("N2").Font.Size = 10 ws.Range("N2").Font.Color = RGB(100, 100, 100) ' 自动调整列宽 ws.Columns("A:L").AutoFitEnd Sub' 应用条件格式Sub ApplyConditionalFormatting(ws As Worksheet, lastRow As Long) ' 清除现有条件格式 ws.Cells.FormatConditions.Delete ' 1. 未登录 - 红色背景 With ws.Range("J2:J" & lastRow) .FormatConditions.Add Type:=xlTextString, String:="未登录", TextOperator:=xlContains .FormatConditions(1).Interior.Color = RGB(255, 199, 206) ' 浅红色 .FormatConditions(1).Font.Color = RGB(156, 0, 6) ' 深红色 End With ' 2. 迟到 - 黄色背景 With ws.Range("J2:J" & lastRow) .FormatConditions.Add Type:=xlTextString, String:="迟到", TextOperator:=xlBeginsWith .FormatConditions(2).Interior.Color = RGB(255, 235, 156) ' 浅黄色 .FormatConditions(2).Font.Color = RGB(153, 102, 0) ' 深黄色 End With ' 3. 严重迟到 - 橙色背景 With ws.Range("J2:J" & lastRow) .FormatConditions.Add Type:=xlTextString, String:="严重迟到", TextOperator:=xlBeginsWith .FormatConditions(3).Interior.Color = RGB(255, 217, 102) ' 橙色 .FormatConditions(3).Font.Color = RGB(153, 76, 0) ' 深橙色 End With ' 4. 按时到岗 - 绿色背景 With ws.Range("J2:J" & lastRow) .FormatConditions.Add Type:=xlTextString, String:="按时到岗", TextOperator:=xlBeginsWith .FormatConditions(4).Interior.Color = RGB(198, 239, 206) ' 浅绿色 .FormatConditions(4).Font.Color = RGB(0, 97, 0) ' 深绿色 End With ' 5. 数据条:迟到分钟 With ws.Range("K2:K" & lastRow) .FormatConditions.AddDatabar .FormatConditions(1).BarColor.Color = RGB(99, 190, 123) .FormatConditions(1).MinPoint.Modify newtype:=xlConditionValueLowestValue .FormatConditions(1).MaxPoint.Modify newtype:=xlConditionValueHighestValue End With ' 6. 图标集:状态评级 With ws.Range("J2:J" & lastRow) .FormatConditions.AddIconSetCondition .FormatConditions(5).SetFirstPriority .FormatConditions(5).IconSet = ActiveWorkbook.IconSets(xl3TrafficLights1) With .FormatConditions(5).IconCriteria(2) .Operator = xlGreaterEqual .Type = xlConditionValueFormula .Value = "=""按时到岗""" End With With .FormatConditions(5).IconCriteria(3) .Operator = xlGreaterEqual .Type = xlConditionValueFormula .Value = "=""迟到""" End With End WithEnd Sub' 生成报告Function GenerateReport(results As Collection) As Boolean Dim wsReport As Worksheet Dim wsResult As Worksheet Dim summaryData As Object Dim i As Long Set wsResult = ThisWorkbook.Worksheets(RESULT_SHEET) Set summaryData = CreateObject("Scripting.Dictionary") ' 统计汇总 Dim totalCount As Long, onTimeCount As Long, lateCount As Long, seriousLateCount As Long, absentCount As Long totalCount = results.Count onTimeCount = 0 lateCount = 0 seriousLateCount = 0 absentCount = 0 ' 按部门统计 Dim deptStats As Object Set deptStats = CreateObject("Scripting.Dictionary") ' 按组统计 Dim groupStats As Object Set groupStats = CreateObject("Scripting.Dictionary") For i = 1 To results.Count Dim result As Object Set result = results(i) Dim status As String, dept As String, group As String status = result("状态") dept = result("部门") group = result("组别") ' 总体统计 If InStr(status, "按时到岗") > 0 Then onTimeCount = onTimeCount + 1 ElseIf InStr(status, "严重迟到") > 0 Then seriousLateCount = seriousLateCount + 1 lateCount = lateCount + 1 ElseIf InStr(status, "迟到") > 0 Then lateCount = lateCount + 1 ElseIf status = "未登录" Then absentCount = absentCount + 1 End If ' 部门统计 If Len(dept) > 0 Then If Not deptStats.Exists(dept) Then Dim deptInfo As Object Set deptInfo = CreateObject("Scripting.Dictionary") deptInfo("total") = 0 deptInfo("onTime") = 0 deptInfo("late") = 0 deptInfo("absent") = 0 deptStats.Add dept, deptInfo End If deptStats(dept)("total") = deptStats(dept)("total") + 1 If InStr(status, "按时到岗") > 0 Then deptStats(dept)("onTime") = deptStats(dept)("onTime") + 1 ElseIf InStr(status, "迟到") > 0 Then deptStats(dept)("late") = deptStats(dept)("late") + 1 ElseIf status = "未登录" Then deptStats(dept)("absent") = deptStats(dept)("absent") + 1 End If End If ' 组统计 If Len(group) > 0 Then If Not groupStats.Exists(group) Then Dim groupInfo As Object Set groupInfo = CreateObject("Scripting.Dictionary") groupInfo("total") = 0 groupInfo("onTime") = 0 groupInfo("late") = 0 groupInfo("absent") = 0 groupStats.Add group, groupInfo End If groupStats(group)("total") = groupStats(group)("total") + 1 If InStr(status, "按时到岗") > 0 Then groupStats(group)("onTime") = groupStats(group)("onTime") + 1 ElseIf InStr(status, "迟到") > 0 Then groupStats(group)("late") = groupStats(group)("late") + 1 ElseIf status = "未登录" Then groupStats(group)("absent") = groupStats(group)("absent") + 1 End If End If Next i ' 创建报告表 On Error Resume Next Application.DisplayAlerts = False ThisWorkbook.Worksheets("统计报告").Delete Application.DisplayAlerts = True On Error GoTo 0 Set wsReport = ThisWorkbook.Worksheets.Add wsReport.Name = "统计报告" ' 写入汇总数据 With wsReport ' 标题 .Range("A1").Value = "排班登录匹配统计报告" .Range("A1").Font.Bold = True .Range("A1").Font.Size = 16 .Range("A1").Font.Color = RGB(31, 78, 120) .Range("A1:B1").Merge .Range("A2").Value = "生成时间:" .Range("B2").Value = Now() .Range("B2").NumberFormat = "yyyy-mm-dd hh:mm:ss" ' 总体统计 .Range("A4").Value = "总体统计" .Range("A4").Font.Bold = True .Range("A4").Font.Size = 12 .Range("A5").Value = "总人数" .Range("B5").Value = totalCount .Range("B5").Font.Bold = True .Range("A6").Value = "按时到岗" .Range("B6").Value = onTimeCount .Range("C6").Value = Format(onTimeCount / totalCount, "0.00%") .Range("B6:C6").Interior.Color = RGB(198, 239, 206) .Range("A7").Value = "迟到(≤30分钟)" .Range("B7").Value = lateCount - seriousLateCount .Range("C7").Value = Format((lateCount - seriousLateCount) / totalCount, "0.00%") .Range("B7:C7").Interior.Color = RGB(255, 235, 156) .Range("A8").Value = "严重迟到(>30分钟)" .Range("B8").Value = seriousLateCount .Range("C8").Value = Format(seriousLateCount / totalCount, "0.00%") .Range("B8:C8").Interior.Color = RGB(255, 217, 102) .Range("A9").Value = "未登录" .Range("B9").Value = absentCount .Range("C9").Value = Format(absentCount / totalCount, "0.00%") .Range("B9:C9").Interior.Color = RGB(255, 199, 206) .Range("A10").Value = "出勤率" .Range("B10").Value = Format((totalCount - absentCount) / totalCount, "0.00%") .Range("B10").Font.Bold = True .Range("B10").Font.Color = RGB(0, 97, 0) ' 部门统计 Dim rowOffset As Long rowOffset = 12 .Range("A" & rowOffset).Value = "部门统计" .Range("A" & rowOffset).Font.Bold = True .Range("A" & rowOffset).Font.Size = 12 .Range("A" & rowOffset + 1).Value = "部门" .Range("B" & rowOffset + 1).Value = "总人数" .Range("C" & rowOffset + 1).Value = "按时到岗" .Range("D" & rowOffset + 1).Value = "迟到" .Range("E" & rowOffset + 1).Value = "未登录" .Range("F" & rowOffset + 1).Value = "出勤率" Dim deptKeys As Variant, deptKey As Variant deptKeys = deptStats.Keys For i = 0 To deptStats.Count - 1 deptKey = deptKeys(i) Dim deptData As Object Set deptData = deptStats(deptKey) .Range("A" & rowOffset + 2 + i).Value = deptKey .Range("B" & rowOffset + 2 + i).Value = deptData("total") .Range("C" & rowOffset + 2 + i).Value = deptData("onTime") .Range("D" & rowOffset + 2 + i).Value = deptData("late") .Range("E" & rowOffset + 2 + i).Value = deptData("absent") Dim attendanceRate As Double attendanceRate = (deptData("total") - deptData("absent")) / deptData("total") .Range("F" & rowOffset + 2 + i).Value = Format(attendanceRate, "0.00%") ' 条件格式 If attendanceRate >= 0.95 Then .Range("F" & rowOffset + 2 + i).Interior.Color = RGB(198, 239, 206) ElseIf attendanceRate >= 0.9 Then .Range("F" & rowOffset + 2 + i).Interior.Color = RGB(255, 235, 156) Else .Range("F" & rowOffset + 2 + i).Interior.Color = RGB(255, 199, 206) End If Next i ' 设置表格格式 Dim lastRow As Long lastRow = rowOffset + 2 + deptStats.Count With .Range("A" & rowOffset + 1 & ":F" & lastRow) .Borders.LineStyle = xlContinuous .Borders.Weight = xlThin End With With .Range("A" & rowOffset + 1 & ":F" & rowOffset + 1) .Font.Bold = True .HorizontalAlignment = xlCenter .Interior.Color = RGB(221, 235, 247) End With ' 自动调整列宽 .Columns("A:F").AutoFit End With GenerateReport = TrueEnd Function' 发送通知Function SendNotifications(results As Collection) As Boolean Dim outlookApp As Object Dim outlookMail As Object Dim i As Long, absentCount As Long, lateCount As Long Dim subject As String, body As String Dim recipients As String ' 检查是否有需要通知的情况 absentCount = 0 lateCount = 0 For i = 1 To results.Count Dim result As Object Set result = results(i) If result("状态") = "未登录" Then absentCount = absentCount + 1 ElseIf InStr(result("状态"), "迟到") > 0 Then lateCount = lateCount + 1 End If Next i If absentCount = 0 And lateCount = 0 Then LogMessage "无异常情况,无需发送通知", "INFO" SendNotifications = True Exit Function End If ' 创建Outlook应用 On Error Resume Next Set outlookApp = CreateObject("Outlook.Application") If outlookApp Is Nothing Then LogMessage "无法创建Outlook应用,请确保已安装Outlook", "ERROR" SendNotifications = False Exit Function End If ' 创建邮件 Set outlookMail = outlookApp.CreateItem(0) ' 设置邮件属性 subject = "【考勤异常通知】" & Format(Date, "yyyy年mm月dd日") & "排班匹配结果" recipients = "manager@company.com;hr@company.com" ' 修改为实际收件人 body = "<html><body>" body = body & "<h2>考勤异常通知</h2>" body = body & "<p><strong>报告时间:</strong>" & Now() & "</p>" body = body & "<p><strong>总排班人数:</strong>" & results.Count & "人</p>" body = body & "<p><strong>异常情况:</strong></p>" body = body & "<ul>" body = body & "<li>未登录:" & absentCount & "人</li>" body = body & "<li>迟到:" & lateCount & "人</li>" body = body & "</ul>" If absentCount > 0 Then body = body & "<h3>未登录人员名单:</h3>" body = body & "<table border='1' cellpadding='5' style='border-collapse: collapse;'>" body = body & "<tr style='background-color: #f2f2f2;'>" body = body & "<th>工号</th><th>姓名</th><th>部门</th><th>组别</th><th>应到时间</th>" body = body & "</tr>" For i = 1 To results.Count Set result = results(i) If result("状态") = "未登录" Then body = body & "<tr style='background-color: #ffe6e6;'>" body = body & "<td>" & result("工号") & "</td>" body = body & "<td>" & result("姓名") & "</td>" body = body & "<td>" & result("部门") & "</td>" body = body & "<td>" & result("组别") & "</td>" body = body & "<td>" & Format(result("应到时间"), "hh:mm") & "</td>" body = body & "</tr>" End If Next i body = body & "</table>" End If If lateCount > 0 Then body = body & "<h3>迟到人员名单(≥5分钟):</h3>" body = body & "<table border='1' cellpadding='5' style='border-collapse: collapse;'>" body = body & "<tr style='background-color: #f2f2f2;'>" body = body & "<th>工号</th><th>姓名</th><th>部门</th><th>组别</th><th>应到时间</th><th>登录时间</th><th>迟到分钟</th>" body = body & "</tr>" For i = 1 To results.Count Set result = results(i) If InStr(result("状态"), "迟到") > 0 And result("迟到分钟") >= 5 Then Dim rowColor As String If result("迟到分钟") > 30 Then rowColor = "#fff0b3" Else rowColor = "#ffffcc" End If body = body & "<tr style='background-color: " & rowColor & ";'>" body = body & "<td>" & result("工号") & "</td>" body = body & "<td>" & result("姓名") & "</td>" body = body & "<td>" & result("部门") & "</td>" body = body & "<td>" & result("组别") & "</td>" body = body & "<td>" & Format(result("应到时间"), "hh:mm") & "</td>" body = body & "<td>" & Format(result("登录时间"), "hh:mm") & "</td>" body = body & "<td>" & result("迟到分钟") & "</td>" body = body & "</tr>" End If Next i body = body & "</table>" End If body = body & "<p>详细报告请查看附件。</p>" body = body & "<p><em>本邮件由考勤系统自动发送,请勿回复。</em></p>" body = body & "</body></html>" With outlookMail .Subject = subject .To = recipients .HTMLBody = body .Importance = 2 ' 高重要性 ' 添加附件 Dim filePath As String filePath = ThisWorkbook.Path & "\考勤报告_" & Format(Date, "yyyymmdd") & ".xlsx" ThisWorkbook.SaveCopyAs filePath .Attachments.Add filePath ' 发送邮件 .Send End With ' 清理临时文件 Kill filePath LogMessage "通知邮件已发送至:" & recipients, "INFO" Set outlookMail = Nothing Set outlookApp = Nothing SendNotifications = TrueEnd Function' 显示汇总信息Sub ShowSummary(results As Collection) Dim summary As String Dim totalCount As Long, onTimeCount As Long, lateCount As Long, seriousLateCount As Long, absentCount As Long Dim i As Long totalCount = results.Count onTimeCount = 0 lateCount = 0 seriousLateCount = 0 absentCount = 0 For i = 1 To results.Count Dim result As Object Set result = results(i) If InStr(result("状态"), "按时到岗") > 0 Then onTimeCount = onTimeCount + 1 ElseIf InStr(result("状态"), "严重迟到") > 0 Then seriousLateCount = seriousLateCount + 1 lateCount = lateCount + 1 ElseIf InStr(result("状态"), "迟到") > 0 Then lateCount = lateCount + 1 ElseIf result("状态") = "未登录" Then absentCount = absentCount + 1 End If Next i summary = "排班登录匹配完成!" & vbCrLf & vbCrLf summary = summary & "统计结果:" & vbCrLf summary = summary & "=" & String(30, "=") & vbCrLf summary = summary & "总排班人数:" & totalCount & "人" & vbCrLf summary = summary & "按时到岗:" & onTimeCount & "人 (" & Format(onTimeCount / totalCount, "0.0%") & ")" & vbCrLf summary = summary & "迟到(≤30分钟):" & (lateCount - seriousLateCount) & "人 (" & Format((lateCount - seriousLateCount) / totalCount, "0.0%") & ")" & vbCrLf summary = summary & "严重迟到(>30分钟):" & seriousLateCount & "人 (" & Format(seriousLateCount / totalCount, "0.0%") & ")" & vbCrLf summary = summary & "未登录:" & absentCount & "人 (" & Format(absentCount / totalCount, "0.0%") & ")" & vbCrLf summary = summary & "出勤率:" & Format((totalCount - absentCount) / totalCount, "0.0%") & vbCrLf summary = summary & "=" & String(30, "=") & vbCrLf & vbCrLf If absentCount > 0 Then summary = summary & "未登录人员:" & vbCrLf For i = 1 To results.Count Set result = results(i) If result("状态") = "未登录" Then summary = summary & " " & result("姓名") & " (" & result("工号") & ") - " & result("部门") & "/" & result("组别") & vbCrLf End If Next i summary = summary & vbCrLf End If If lateCount > 0 Then summary = summary & "迟到人员(≥5分钟):" & vbCrLf For i = 1 To results.Count Set result = results(i) If InStr(result("状态"), "迟到") > 0 And result("迟到分钟") >= 5 Then summary = summary & " " & result("姓名") & " (" & result("工号") & ") - 迟到" & result("迟到分钟") & "分钟" & vbCrLf End If Next i End If MsgBox summary, vbInformation, "匹配完成"End Sub' 记录日志Sub LogMessage(message As String, msgType As String) Dim logSheet As Worksheet Dim logRow As Long On Error Resume Next Set logSheet = ThisWorkbook.Worksheets("操作日志") If logSheet Is Nothing Then Set logSheet = ThisWorkbook.Worksheets.Add logSheet.Name = "操作日志" ' 设置日志表头 logSheet.Range("A1").Value = "时间" logSheet.Range("B1").Value = "类型" logSheet.Range("C1").Value = "消息" ' 格式化表头 With logSheet.Range("A1:C1") .Font.Bold = True .HorizontalAlignment = xlCenter .Interior.Color = RGB(221, 235, 247) End With End If ' 查找最后一行 logRow = logSheet.Cells(logSheet.Rows.Count, 1).End(xlUp).Row + 1 ' 写入日志 logSheet.Cells(logRow, 1).Value = Now() logSheet.Cells(logRow, 1).NumberFormat = "yyyy-mm-dd hh:mm:ss" logSheet.Cells(logRow, 2).Value = msgType logSheet.Cells(logRow, 3).Value = message ' 根据类型设置颜色 Select Case msgType Case "ERROR" logSheet.Cells(logRow, 2).Font.Color = RGB(192, 0, 0) logSheet.Cells(logRow, 3).Font.Color = RGB(192, 0, 0) Case "WARNING" logSheet.Cells(logRow, 2).Font.Color = RGB(255, 128, 0) logSheet.Cells(logRow, 3).Font.Color = RGB(255, 128, 0) Case "INFO" logSheet.Cells(logRow, 2).Font.Color = RGB(0, 112, 192) logSheet.Cells(logRow, 3).Font.Color = RGB(0, 112, 192) End Select ' 自动调整列宽 logSheet.Columns("A:C").AutoFit ' 保存日志 ThisWorkbook.Save ' 输出到立即窗口(调试用) Debug.Print Format(Now(), "hh:mm:ss") & " [" & msgType & "] " & messageEnd Sub
#!/usr/bin/env python3"""排班与登录匹配校验系统 - Python版本功能:核对排班表与系统登录数据,识别考勤异常作者:数据分析团队版本:2.0"""import pandas as pdimport numpy as npfrom datetime import datetime, timedeltaimport warningsfrom typing import Dict, List, Tuple, Optional, Anyimport osimport loggingfrom dataclasses import dataclassfrom enum import Enumimport jsonimport smtplibfrom email.mime.text import MIMETextfrom email.mime.multipart import MIMEMultipartfrom email.mime.application import MIMEApplicationimport matplotlib.pyplot as pltimport seaborn as snsfrom openpyxl import load_workbookfrom openpyxl.styles import PatternFill, Font, Border, Side, Alignmentfrom openpyxl.utils import get_column_letterimport sys# 配置日志logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('attendance_check.log', encoding='utf-8'), logging.StreamHandler(sys.stdout) ])logger = logging.getLogger(__name__)warnings.filterwarnings('ignore')# 枚举定义class AttendanceStatus(Enum): """考勤状态枚举""" ON_TIME = "按时到岗" LATE = "迟到" SERIOUS_LATE = "严重迟到" ABSENT = "未登录" LEAVE = "请假" OVERTIME = "加班" ERROR = "数据错误"@dataclassclass EmployeeSchedule: """员工排班信息""" employee_id: str name: str department: str group: str schedule_time: datetime shift_type: str remarks: str = ""@dataclassclass LoginRecord: """登录记录""" employee_id: str login_time: datetime logout_time: Optional[datetime] = None system: str = "" ip_address: str = "" device: str = ""@dataclassclass AttendanceResult: """考勤结果""" employee_id: str name: str department: str group: str schedule_time: datetime login_time: Optional[datetime] status: AttendanceStatus late_minutes: int = 0 login_system: str = "" logout_time: Optional[datetime] = None work_duration: Optional[timedelta] = None remarks: str = ""class AttendanceAnalyzer: """考勤分析器""" def __init__(self, config_path: Optional[str] = None): """初始化分析器""" self.config = self._load_config(config_path) self.schedule_data = None self.login_data = None self.results = [] self.summary = {} # 设置显示选项 pd.set_option('display.max_columns', None) pd.set_option('display.width', None) pd.set_option('display.max_colwidth', 50) logger.info("考勤分析器初始化完成") def _load_config(self, config_path: Optional[str]) -> Dict: """加载配置文件""" default_config = { 'data': { 'schedule_columns': { 'employee_id': ['工号', '员工编号', '员工ID', 'EmployeeID', 'StaffID'], 'name': ['姓名', '员工姓名', 'Name', 'EmployeeName'], 'department': ['部门', 'Department', 'Dept'], 'group': ['组别', '班组', 'Group', 'Team'], 'schedule_time': ['应到时间', '排班时间', '上班时间', 'ScheduleTime', 'WorkTime'], 'shift_type': ['班次', '班型', 'Shift', 'ShiftType'] }, 'login_columns': { 'employee_id': ['工号', '员工编号', '员工ID', 'EmployeeID'], 'login_time': ['登录时间', '上线时间', 'LoginTime', 'StartTime'], 'logout_time': ['退出时间', '下线时间', 'LogoutTime', 'EndTime'], 'system': ['登录系统', '系统名称', 'System', 'AppName'], 'ip_address': ['IP地址', '登录IP', 'IP'], 'device': ['设备', '终端', 'Device', 'Terminal'] }, 'encoding': 'utf-8', 'timezone': 'Asia/Shanghai' }, 'rules': { 'late_threshold': 5, # 迟到阈值(分钟) 'serious_late_threshold': 30, # 严重迟到阈值(分钟) 'work_duration_threshold': 4, # 工作时长阈值(小时) 'ignore_early_minutes': 60, # 忽略提前登录的分钟数 'grace_period': 3 # 宽限期(分钟) }, 'output': { 'format': 'excel', 'excel_template': None, 'report_title': '考勤匹配报告', 'save_path': './output', 'send_email': False, 'email_recipients': ['manager@company.com'] }, 'email': { 'smtp_server': 'smtp.office365.com', 'smtp_port': 587, 'sender': 'attendance_system@company.com', 'password': '', 'use_ssl': True } } if config_path and os.path.exists(config_path): try: with open(config_path, 'r', encoding='utf-8') as f: user_config = json.load(f) # 深度合并配置 import copy for key in user_config: if key in default_config and isinstance(default_config[key], dict): default_config[key].update(user_config[key]) else: default_config[key] = user_config[key] logger.info(f"配置文件加载成功: {config_path}") except Exception as e: logger.warning(f"配置文件加载失败: {str(e)},使用默认配置") else: logger.info("使用默认配置") return default_config def load_schedule_data(self, file_path: str, sheet_name: str = 0) -> pd.DataFrame: """加载排班数据""" logger.info(f"加载排班数据: {file_path}") try: # 支持多种文件格式 if file_path.endswith('.csv'): schedule_df = pd.read_csv(file_path, encoding=self.config['data']['encoding']) elif file_path.endswith('.xlsx') or file_path.endswith('.xls'): schedule_df = pd.read_excel(file_path, sheet_name=sheet_name) elif file_path.endswith('.parquet'): schedule_df = pd.read_parquet(file_path) else: raise ValueError(f"不支持的文件格式: {file_path}") logger.info(f"排班数据加载成功: {len(schedule_df)} 行 × {len(schedule_df.columns)} 列") # 标准化列名 schedule_df = self._standardize_columns( schedule_df, self.config['data']['schedule_columns'] ) # 数据预处理 schedule_df = self._preprocess_schedule_data(schedule_df) self.schedule_data = schedule_df return schedule_df except Exception as e: logger.error(f"排班数据加载失败: {str(e)}") raise def load_login_data(self, file_path: str, sheet_name: str = 0) -> pd.DataFrame: """加载登录数据""" logger.info(f"加载登录数据: {file_path}") try: # 支持多种文件格式 if file_path.endswith('.csv'): login_df = pd.read_csv(file_path, encoding=self.config['data']['encoding']) elif file_path.endswith('.xlsx') or file_path.endswith('.xls'): login_df = pd.read_excel(file_path, sheet_name=sheet_name) elif file_path.endswith('.json'): login_df = pd.read_json(file_path) else: raise ValueError(f"不支持的文件格式: {file_path}") logger.info(f"登录数据加载成功: {len(login_df)} 行 × {len(login_df.columns)} 列") # 标准化列名 login_df = self._standardize_columns( login_df, self.config['data']['login_columns'] ) # 数据预处理 login_df = self._preprocess_login_data(login_df) self.login_data = login_df return login_df except Exception as e: logger.error(f"登录数据加载失败: {str(e)}") raise def _standardize_columns(self, df: pd.DataFrame, column_mapping: Dict) -> pd.DataFrame: """标准化列名""" df_clean = df.copy() # 创建反向映射:标准列名 -> 可能列名 standard_to_possible = {} for standard_name, possible_names in column_mapping.items(): for name in possible_names: standard_to_possible[name] = standard_name # 重命名列 rename_dict = {} for col in df_clean.columns: col_str = str(col).strip() if col_str in standard_to_possible: rename_dict[col] = standard_to_possible[col_str] if rename_dict: df_clean = df_clean.rename(columns=rename_dict) logger.info(f"标准化列名: {rename_dict}") return df_clean def _preprocess_schedule_data(self, df: pd.DataFrame) -> pd.DataFrame: """预处理排班数据""" df_clean = df.copy() logger.info("预处理排班数据...") # 1. 处理工号 if 'employee_id' in df_clean.columns: df_clean['employee_id'] = df_clean['employee_id'].astype(str).str.strip() empty_ids = df_clean['employee_id'].isna().sum() if empty_ids > 0: logger.warning(f"发现 {empty_ids} 个空工号,已填充为未知") df_clean['employee_id'] = df_clean['employee_id'].fillna('未知') # 2. 处理应到时间 if 'schedule_time' in df_clean.columns: # 转换为datetime df_clean['schedule_time'] = pd.to_datetime( df_clean['schedule_time'], errors='coerce', format='mixed' ) invalid_times = df_clean['schedule_time'].isna().sum() if invalid_times > 0: logger.warning(f"发现 {invalid_times} 个无效时间,已删除这些记录") df_clean = df_clean.dropna(subset=['schedule_time']) # 3. 处理部门组别 for col in ['department', 'group']: if col in df_clean.columns: df_clean[col] = df_clean[col].fillna('未分配') # 4. 处理班次 if 'shift_type' in df_clean.columns: df_clean['shift_type'] = df_clean['shift_type'].fillna('默认班次') # 标准化班次名称 shift_mapping = { '早班': ['早班', '早', 'A班', 'A'], '中班': ['中班', '中', 'B班', 'B'], '晚班': ['晚班', '晚', 'C班', 'C'], '夜班': ['夜班', '夜', 'D班', 'D'] } def standardize_shift(shift): if pd.isna(shift): return '默认班次' shift_str = str(shift).strip() for standard, variations in shift_mapping.items(): if any(var in shift_str for var in variations): return standard return shift_str df_clean['shift_type'] = df_clean['shift_type'].apply(standardize_shift) # 5. 添加日期列 if 'schedule_time' in df_clean.columns: df_clean['schedule_date'] = df_clean['schedule_time'].dt.date df_clean['schedule_hour'] = df_clean['schedule_time'].dt.hour df_clean['schedule_minute'] = df_clean['schedule_time'].dt.minute df_clean['schedule_weekday'] = df_clean['schedule_time'].dt.day_name() logger.info(f"排班数据处理完成,有效记录: {len(df_clean)} 行") return df_clean def _preprocess_login_data(self, df: pd.DataFrame) -> pd.DataFrame: """预处理登录数据""" df_clean = df.copy() logger.info("预处理登录数据...") # 1. 处理工号 if 'employee_id' in df_clean.columns: df_clean['employee_id'] = df_clean['employee_id'].astype(str).str.strip() empty_ids = df_clean['employee_id'].isna().sum() if empty_ids > 0: logger.warning(f"发现 {empty_ids} 个空工号,已删除这些记录") df_clean = df_clean.dropna(subset=['employee_id']) # 2. 处理登录时间 if 'login_time' in df_clean.columns: df_clean['login_time'] = pd.to_datetime( df_clean['login_time'], errors='coerce', format='mixed' ) invalid_logins = df_clean['login_time'].isna().sum() if invalid_logins > 0: logger.warning(f"发现 {invalid_logins} 个无效登录时间,已删除这些记录") df_clean = df_clean.dropna(subset=['login_time']) # 3. 处理退出时间 if 'logout_time' in df_clean.columns: df_clean['logout_time'] = pd.to_datetime( df_clean['logout_time'], errors='coerce', format='mixed' ) # 计算工作时长 df_clean['work_duration'] = df_clean['logout_time'] - df_clean['login_time'] df_clean['work_hours'] = df_clean['work_duration'].dt.total_seconds() / 3600 # 4. 处理登录系统 if 'system' in df_clean.columns: df_clean['system'] = df_clean['system'].fillna('未知系统') # 5. 去重:保留每人最早的登录记录 df_clean = df_clean.sort_values(['employee_id', 'login_time']) df_clean = df_clean.drop_duplicates(subset=['employee_id'], keep='first') logger.info(f"登录数据处理完成,有效记录: {len(df_clean)} 行") return df_clean def match_attendance(self) -> List[AttendanceResult]: """匹配排班和登录数据""" logger.info("开始匹配考勤数据...") if self.schedule_data is None or self.login_data is None: raise ValueError("请先加载排班和登录数据") results = [] # 创建登录数据字典以提高查找效率 login_dict = {} for _, row in self.login_data.iterrows(): emp_id = row['employee_id'] login_record = LoginRecord( employee_id=emp_id, login_time=row.get('login_time'), logout_time=row.get('logout_time'), system=row.get('system', ''), ip_address=row.get('ip_address', ''), device=row.get('device', '') ) login_dict[emp_id] = login_record logger.info(f"登录字典构建完成: {len(login_dict)} 条记录") # 遍历排班数据进行匹配 for idx, schedule_row in self.schedule_data.iterrows(): try: # 提取排班信息 emp_id = schedule_row.get('employee_id', '') if not emp_id or pd.isna(emp_id): logger.warning(f"第 {idx+1} 行: 工号为空,跳过") continue # 创建排班记录 schedule = EmployeeSchedule( employee_id=emp_id, name=schedule_row.get('name', ''), department=schedule_row.get('department', ''), group=schedule_row.get('group', ''), schedule_time=schedule_row.get('schedule_time'), shift_type=schedule_row.get('shift_type', ''), remarks=schedule_row.get('remarks', '') ) # 查找登录记录 login_record = login_dict.get(emp_id) # 判断考勤状态 attendance_result = self._determine_attendance_status(schedule, login_record) results.append(attendance_result) except Exception as e: logger.error(f"处理第 {idx+1} 行时出错: {str(e)}") # 创建错误记录 error_result = AttendanceResult( employee_id=schedule_row.get('employee_id', ''), name=schedule_row.get('name', ''), department=schedule_row.get('department', ''), group=schedule_row.get('group', ''), schedule_time=schedule_row.get('schedule_time'), login_time=None, status=AttendanceStatus.ERROR, remarks=f"处理错误: {str(e)}" ) results.append(error_result) self.results = results # 生成统计摘要 self._generate_summary() logger.info(f"考勤匹配完成: 共处理 {len(results)} 条记录") return results def _determine_attendance_status(self, schedule: EmployeeSchedule, login_record: Optional[LoginRecord]) -> AttendanceResult: """判断考勤状态""" # 默认值 status = AttendanceStatus.ABSENT late_minutes = 0 login_time = None logout_time = None work_duration = None login_system = "" remarks = "" if login_record is not None: login_time = login_record.login_time logout_time = login_record.logout_time login_system = login_record.system if login_time and schedule.schedule_time: # 计算迟到分钟数 time_diff = (login_time - schedule.schedule_time).total_seconds() / 60 # 考虑宽限期 grace_period = self.config['rules']['grace_period'] late_threshold = self.config['rules']['late_threshold'] serious_late_threshold = self.config['rules']['serious_late_threshold'] if time_diff <= grace_period: # 按时到岗(考虑宽限期) status = AttendanceStatus.ON_TIMEelif time_diff <= late_threshold: # 轻微迟到,但不超过迟到阈值 status = AttendanceStatus.ON_TIME late_minutes = 0 remarks = f"迟到{time_diff:.0f}分钟,但在宽限期内不计为迟到" elif time_diff <= serious_late_threshold: # 迟到 status = AttendanceStatus.LATE late_minutes = int(time_diff) remarks = f"迟到{late_minutes}分钟" else: # 严重迟到 status = AttendanceStatus.SERIOUS_LATE late_minutes = int(time_diff) remarks = f"严重迟到{late_minutes}分钟" # 计算工作时长 if logout_time and login_time: work_duration = logout_time - login_time work_hours = work_duration.total_seconds() / 3600 if work_hours < self.config['rules']['work_duration_threshold']: remarks += f",工作时长不足{self.config['rules']['work_duration_threshold']}小时" else: # 有登录记录但无法计算时间差 status = AttendanceStatus.ON_TIME remarks = "有登录记录,但时间信息不完整" else: # 无登录记录 status = AttendanceStatus.ABSENT remarks = "无登录记录" # 创建考勤结果对象 attendance_result = AttendanceResult( employee_id=schedule.employee_id, name=schedule.name, department=schedule.department, group=schedule.group, schedule_time=schedule.schedule_time, login_time=login_time, status=status, late_minutes=late_minutes, login_system=login_system, logout_time=logout_time, work_duration=work_duration, remarks=remarks ) return attendance_result def _generate_summary(self): """生成统计摘要""" if not self.results: return total = len(self.results) status_counts = {} for result in self.results: status = result.status.value status_counts[status] = status_counts.get(status, 0) + 1 # 按部门统计 dept_stats = {} for result in self.results: dept = result.department if dept not in dept_stats: dept_stats[dept] = { 'total': 0, 'on_time': 0, 'late': 0, 'absent': 0, 'other': 0 } dept_stats[dept]['total'] += 1 if result.status == AttendanceStatus.ON_TIME: dept_stats[dept]['on_time'] += 1 elif result.status in [AttendanceStatus.LATE, AttendanceStatus.SERIOUS_LATE]: dept_stats[dept]['late'] += 1 elif result.status == AttendanceStatus.ABSENT: dept_stats[dept]['absent'] += 1 else: dept_stats[dept]['other'] += 1 self.summary = { 'total': total, 'status_counts': status_counts, 'dept_stats': dept_stats, 'attendance_rate': (total - status_counts.get(AttendanceStatus.ABSENT.value, 0)) / total if total > 0 else 0 } logger.info(f"统计摘要生成完成: 总计{total}人,出勤率{self.summary['attendance_rate']:.2%}") def export_results(self, output_path: Optional[str] = None) -> str: """导出结果到Excel""" if not self.results: raise ValueError("没有可导出的结果,请先执行匹配") if output_path is None: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = os.path.join( self.config['output']['save_path'], f"考勤匹配结果_{timestamp}.xlsx" ) # 确保输出目录存在 os.makedirs(os.path.dirname(output_path), exist_ok=True) # 将结果转换为DataFrame results_data = [] for result in self.results: results_data.append({ '工号': result.employee_id, '姓名': result.name, '部门': result.department, '组别': result.group, '应到时间': result.schedule_time, '登录时间': result.login_time, '退出时间': result.logout_time, '工作时长': result.work_duration, '登录系统': result.login_system, '考勤状态': result.status.value, '迟到分钟': result.late_minutes, '备注': result.remarks }) df_results = pd.DataFrame(results_data) # 创建Excel写入器 with pd.ExcelWriter(output_path, engine='openpyxl') as writer: # 写入详细结果 df_results.to_excel(writer, sheet_name='详细结果', index=False) # 写入统计摘要 summary_data = [] for status, count in self.summary['status_counts'].items(): summary_data.append({ '考勤状态': status, '人数': count, '占比': f"{count/self.summary['total']:.2%}" }) df_summary = pd.DataFrame(summary_data) df_summary.to_excel(writer, sheet_name='状态统计', index=False) # 写入部门统计 dept_data = [] for dept, stats in self.summary['dept_stats'].items(): dept_data.append({ '部门': dept, '总人数': stats['total'], '按时到岗': stats['on_time'], '迟到': stats['late'], '未登录': stats['absent'], '其他': stats['other'], '出勤率': f"{(stats['total'] - stats['absent']) / stats['total']:.2%}" if stats['total'] > 0 else '0.00%' }) df_dept = pd.DataFrame(dept_data) df_dept.to_excel(writer, sheet_name='部门统计', index=False) # 应用格式 self._apply_excel_formatting(output_path) logger.info(f"结果已导出到: {output_path}") return output_path def _apply_excel_formatting(self, file_path: str): """应用Excel格式""" try: from openpyxl import load_workbook from openpyxl.styles import PatternFill, Font, Border, Side, Alignment wb = load_workbook(file_path) # 定义颜色 colors = { 'on_time': 'C6EFCE', # 浅绿 'late': 'FFEB9C', # 浅黄 'serious_late': 'FFC7CE', # 浅红 'absent': 'FFC7CE', # 浅红 'header': 'D9E1F2' # 浅蓝 } # 格式化每个工作表 for sheet_name in wb.sheetnames: ws = wb[sheet_name] # 设置列宽 for column in ws.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = min(max_length + 2, 50) ws.column_dimensions[column_letter].width = adjusted_width # 设置表头样式 header_fill = PatternFill(start_color=colors['header'], end_color=colors['header'], fill_type="solid") header_font = Font(bold=True) header_alignment = Alignment(horizontal="center", vertical="center") for cell in ws[1]: cell.fill = header_fill cell.font = header_font cell.alignment = header_alignment # 设置边框 thin_border = Border( left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin') ) for row in ws.iter_rows(min_row=1, max_row=ws.max_row, max_col=ws.max_column): for cell in row: cell.border = thin_border # 在详细结果表中应用条件格式 if sheet_name == '详细结果': for row in ws.iter_rows(min_row=2, max_row=ws.max_row): status_cell = row[9] # 考勤状态列 status = status_cell.value if status == '按时到岗': fill_color = colors['on_time'] elif status == '迟到': fill_color = colors['late'] elif status == '严重迟到': fill_color = colors['serious_late'] elif status == '未登录': fill_color = colors['absent'] else: fill_color = None if fill_color: for cell in row: cell.fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type="solid") wb.save(file_path) logger.info("Excel格式应用完成") except Exception as e: logger.warning(f"应用Excel格式时出错: {str(e)}") def send_report_email(self, report_path: str): """发送报告邮件""" if not self.config['output']['send_email']: logger.info("邮件发送功能已禁用") return try: # 创建邮件 msg = MIMEMultipart() msg['From'] = self.config['email']['sender'] msg['To'] = ', '.join(self.config['output']['email_recipients']) msg['Subject'] = f"{self.config['output']['report_title']} - {datetime.now().strftime('%Y-%m-%d')}" # 邮件正文 body = f""" <html> <body> <h2>{self.config['output']['report_title']}</h2> <p>生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p> <p>总排班人数:{self.summary['total']}</p> <p>出勤率:{self.summary['attendance_rate']:.2%}</p> <h3>状态统计:</h3> <ul> """ for status, count in self.summary['status_counts'].items(): body += f"<li>{status}: {count}人 ({count/self.summary['total']:.2%})</li>" body += """ </ul> <p>详细报告请查看附件。</p> </body> </html> """ msg.attach(MIMEText(body, 'html')) # 添加附件 with open(report_path, 'rb') as f: attachment = MIMEApplication(f.read(), _subtype='xlsx') attachment.add_header('Content-Disposition', 'attachment', filename=os.path.basename(report_path)) msg.attach(attachment) # 发送邮件 with smtplib.SMTP(self.config['email']['smtp_server'], self.config['email']['smtp_port']) as server: if self.config['email']['use_ssl']: server.starttls() server.login(self.config['email']['sender'], self.config['email']['password']) server.send_message(msg) logger.info(f"报告邮件已发送至: {self.config['output']['email_recipients']}") except Exception as e: logger.error(f"发送邮件失败: {str(e)}") def run_analysis(self, schedule_file: str, login_file: str, schedule_sheet: str = 0, login_sheet: str = 0) -> str: """运行完整分析流程""" logger.info("开始考勤匹配分析流程") # 1. 加载数据 self.load_schedule_data(schedule_file, schedule_sheet) self.load_login_data(login_file, login_sheet) # 2. 执行匹配 self.match_attendance() # 3. 导出结果 report_path = self.export_results() # 4. 发送邮件 if self.config['output']['send_email']: self.send_report_email(report_path) logger.info("考勤匹配分析流程完成") return report_path # 使用示例 def main(): """主函数示例""" # 创建分析器 analyzer = AttendanceAnalyzer() # 运行分析 report_path = analyzer.run_analysis( schedule_file='排班表.xlsx', login_file='登录记录.csv', schedule_sheet=0, login_sheet=0 ) print(f"报告已生成: {report_path}") # 打印摘要 print("\n考勤匹配结果摘要:") print(f"总人数: {analyzer.summary['total']}") print(f"出勤率: {analyzer.summary['attendance_rate']:.2%}") for status, count in analyzer.summary['status_counts'].items(): print(f"{status}: {count}人 ({count/analyzer.summary['total']:.2%})") if __name__ == "__main__": main()