当前位置:首页>python>第208讲:排班匹配自动化:VBA与Python的校验对决

第208讲:排班匹配自动化:VBA与Python的校验对决

  • 2026-03-27 03:42:30
第208讲:排班匹配自动化:VBA与Python的校验对决

某电商公司客服中心每天有300名客服排班,但总有5-8人“神秘失踪”。传统核对需要2小时,自动化后只需5分钟。

一、业务痛点:排班执行的“黑洞效应”

1.1 背景:客服中心的调度噩梦

每天早上8:30,某电商公司客服中心的主管张经理都会面临同样的焦虑:

排班表显示

  • 早班(8:00-16:00):120人

  • 中班(12:00-20:00):100人

  • 晚班(16:00-24:00):80人

  • 总计:300人

系统登录显示

  • 8:00准时登录:112人

  • 8:00-8:30迟到登录:6人

  • 9:00仍未登录:2人

  • 总计:118人

问题来了

  • 还有2人未登录,是旷工还是请假未报备?

  • 迟到的6人中,哪些是偶尔迟到,哪些是惯犯?

  • 如何快速通知未到岗人员的组长?

1.2 传统人工核对的困境

张经理原来需要做以下工作:

# 手动操作步骤(耗时2小时)1. 打开排班表Excel文件2. 打开系统导出登录表CSV文件3. 在排班表旁边新增两列:"是否登录"、"登录时间"4. 在"是否登录"列输入公式:   =IFERROR(VLOOKUP(A2, 登录表!$A:$B, 2, FALSE), "未登录")5. 在"登录时间"列输入公式:   =IFERROR(VLOOKUP(A2, 登录表!$A:$C, 3, FALSE), "N/A")6. 筛选"是否登录"为"未登录"的行7. 手动高亮这些行8. 复制未登录人员名单,发送邮件给各组长9. 记录迟到人员(登录时间晚于应到时间30分钟以上)10. 统计考勤异常报表

痛点分析

  • 易错:VLOOKUP公式容易拖错范围

  • 低效:每天重复相同操作

  • 不及时:9点才能完成核对

  • 无追溯:历史数据难以分析

二、VBA解决方案:办公室自动化的经典

2.1 完整VBA实现代码

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

2.2 VBA方案的优势与不足

优势

  1. 零部署成本:内置于Excel,无需额外安装

  2. 界面友好:结果直接在Excel中呈现

  3. 自动格式化:条件格式、颜色标注

  4. 集成邮件:自动发送异常通知

  5. 完整日志:操作可追溯

不足

  1. 性能瓶颈:处理超过5万行数据时速度下降

  2. 稳定性问题:Excel崩溃风险

  3. 扩展性差:难以对接其他系统

  4. 维护困难:代码复杂,调试困难

三、Python解决方案:现代数据处理的力量

3.1 完整Python实现代码

#!/usr/bin/env python3"""排班与登录匹配校验系统 - Python版本功能:核对排班表与系统登录数据,识别考勤异常作者:数据分析团队版本:2.0"""import pandas as pdimport numpy as npfrom datetime import datetime, timedeltaimport warningsfrom typing import DictListTupleOptionalAnyimport 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()

四、VBA vs Python 对比分析

4.1 性能对比

测试环境

  • 排班表:1000行记录

  • 登录表:800行记录

  • 硬件:Intel i7-1165G7, 16GB RAM

  • 操作系统:Windows 10

测试结果

指标

VBA方案

Python方案

差异

执行时间

5-8秒

1-2秒

快4-6倍

内存占用

150-200MB

50-80MB

减少60%

代码行数

600+行

300+行

减少50%

可维护性

较差

优秀

结构清晰

4.2 功能对比

功能

VBA实现

Python实现

评价

数据加载

依赖Excel文件

支持CSV/Excel/JSON/Parquet

Python胜

列名匹配

硬编码或简单匹配

智能模糊匹配

Python胜

错误处理

基本错误处理

完善的异常捕获和日志

Python胜

结果导出

只能输出Excel

可扩展多种格式

Python胜

邮件发送

依赖Outlook

支持SMTP协议,不依赖客户端

Python胜

部署难度

无需部署

需要Python环境

VBA胜

用户体验

Excel内直接运行

需要命令行或脚本

VBA胜

4.3 扩展性对比

VBA的局限性

  1. 无法轻松对接数据库

  2. 难以实现Web服务

  3. 不支持多线程

  4. 无法进行机器学习分析

Python的优势

  1. 可轻松对接MySQL/PostgreSQL/MongoDB

  2. 可构建RESTful API服务

  3. 支持异步处理和并发

  4. 可集成机器学习模型进行预测分析

五、实际应用建议

5.1 选择建议

选择VBA的情况

  • 用户不懂编程,需要在Excel中直接操作

  • 数据量较小(<5000行)

  • 没有IT支持,需要零部署解决方案

  • 只需要基本匹配功能

选择Python的情况

  • 数据量较大(>10000行)

  • 需要对接其他系统(数据库、API等)

  • 需要复杂的业务逻辑和数据分析

  • 希望建立自动化工作流

  • 有IT支持,可以部署Python环境

5.2 混合方案

对于大型企业,可以考虑混合方案:

  1. 前端用Excel:用户通过Excel上传数据和查看结果

  2. 后端用Python:处理复杂的匹配逻辑和数据分析

  3. 数据库存储:将历史数据存储到数据库

  4. 定时任务:每天自动运行匹配任务

  5. Web报表:通过Web界面查看统计报表

六、选择题测试

  1. 在VBA方案中,用于高效查找登录记录的数据结构是?

    A) 数组

    B) 集合(Collection)

    C) 字典(Dictionary)

    D) 记录集(Recordset)

  2. Python方案中使用pandas.merge进行表连接时,哪种连接方式最适合排班表和登录表的匹配?

    A) 内连接(inner)

    B) 左连接(left)

    C) 右连接(right)

    D) 外连接(outer)

  3. 在判断迟到状态时,如果设置了3分钟宽限期,员工应到时间8:00,登录时间8:02,应该判断为什么状态?

    A) 按时到岗

    B) 迟到

    C) 严重迟到

    D) 未登录

  4. Python方案中,将考勤结果导出为Excel文件时,使用的引擎(engine)是什么?

    A) xlwt

    B) xlsxwriter

    C) openpyxl

    D) xlrd

  5. 在VBA方案中,为结果表应用条件格式,将未登录的行标记为红色,应该使用什么方法?

    A) FormatConditions.Add

    B) ConditionalFormatting.Add

    C) Style.Add

    D) Format.Add


答案

  1. C - 字典(Dictionary)提供O(1)的查找效率

  2. B - 左连接(left)保留所有排班记录,匹配登录记录

  3. A - 在3分钟宽限期内,不计为迟到

  4. C - openpyxl用于读写Excel 2010 xlsx/xlsm文件

  5. A - FormatConditions.Add方法添加条件格式

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-03-27 11:39:56 HTTP/2.0 GET : https://f.mffb.com.cn/a/481503.html
  2. 运行时间 : 0.082771s [ 吞吐率:12.08req/s ] 内存消耗:4,842.99kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=7af305a06050ab2fb739425fe78dc8d1
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000543s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000718s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000295s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000303s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000459s ]
  6. SELECT * FROM `set` [ RunTime:0.000188s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000492s ]
  8. SELECT * FROM `article` WHERE `id` = 481503 LIMIT 1 [ RunTime:0.000572s ]
  9. UPDATE `article` SET `lasttime` = 1774582796 WHERE `id` = 481503 [ RunTime:0.004724s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 66 LIMIT 1 [ RunTime:0.000235s ]
  11. SELECT * FROM `article` WHERE `id` < 481503 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000429s ]
  12. SELECT * FROM `article` WHERE `id` > 481503 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000442s ]
  13. SELECT * FROM `article` WHERE `id` < 481503 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.000760s ]
  14. SELECT * FROM `article` WHERE `id` < 481503 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.000672s ]
  15. SELECT * FROM `article` WHERE `id` < 481503 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.001514s ]
0.084407s