↓ 跳过正文

Excel Audit Vb

·1771 字·4 分钟
Suntower Lee
作者
Suntower Lee
记录工作中的技术笔记和日常观察,也发布自己开发的小软件。

Excel 2019 下“永久保护 + 隐藏公式 + 用户只能改部分单元格 + 强制记录每一次修改 + 用户永远无法解保护和看到代码”这一经典企业级难题的完整演进路径

最终结论(2025年11月最新版,最可靠方案)
#

在 Excel 2019(以及所有不想依赖新式线程评论的版本)下,唯一100%永不失灵的记录方式是: 放弃任何形式的批注(Legacy Comment / .CommentThreaded 都会在保护状态下静默失败或编译错误) 改用一个 xlSheetVeryHidden 的隐藏工作表来永久存储修改日志

下面按时间顺序列出每一次的主要版本和踩坑点:

阶段0:你的原始需求(完美复述)
#

  • Excel 2019 xlsm
  • 部分单元格锁定+隐藏公式(用户永远看不到公式)
  • 部分单元格解锁(用户可输入)
  • 工作表必须永久保护,且普通用户永远无法解保护
  • 每一次用户修改解锁单元格的内容,必须永久记录(修改人+时间+旧值→新值)
  • 普通用户永远不能看到VBA代码
  • 保存关闭后重新打开,一切功能依然完美工作

阶段1:最初的批注方案(能跑但重新打开后不记录)
#

使用了 Legacy Comment (.AddComment) 踩坑:保存关闭重新打开后 UserInterfaceOnly:=True 被 Excel 自动丢弃 → SheetChange 里无法再添加批注 → 表面无错,但不记录

阶段2:强行重新加 UserInterfaceOnly 保护
#

加入 Workbook_Open + Application.OnTime 延迟调用 ReApplyProtection 踩坑:OnTime 写完整路径宏名 → 另存为后找不到宏 → 打开即弹“无法运行宏”

阶段3:把重新保护宏放到标准模块,宏名不带路径
#

解决了弹窗问题 踩坑:保护状态下 .AddComment 仍然静默失败(Excel 2019 已知限制)

阶段4:改用新式线程评论 .CommentThreaded / .AddCommentThreaded
#

理论上在保护下能用 踩坑:在 Excel 2019 直接编译错误“隐藏的模块中的编译错误”,因为这些属性2019根本不存在

阶段5:最终彻底可靠方案(当前推荐的工业级终极版)
#

放弃一切批注,改用 xlSheetVeryHidden 的隐藏工作表记录日志 这是唯一在 Excel 2007~2019 全版本、永久保护状态下100%一定记录的方案

最终完整代码(已验证无数次,2025年11月最新版)
#

ThisWorkbook 模块完整代码(全部替换)
#

vba

Option Explicit

Private OldVal As Variant

'========== 请务必改成你自己的超强密码 ==========
Private Const SHEET_PASSWORD As String = "x9#kP2@mZqW8!vN4L&"   ' 工作表保护密码
Private Const VBA_PASSWORD As String = "A1b#9xY7@zQ2wE5rT8uI"       ' VBA工程锁密码(可选)

Private Sub Workbook_Open()
    Application.OnTime Now + TimeValue("00:00:01"), "ReApplyProtection"
End Sub

Private Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
    Call ReApplyProtection
End Sub

Private Sub Workbook_BeforeClose(Cancel As Boolean)
    Call ReApplyProtection
    ThisWorkbook.Save
End Sub

Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
    If Target.CountLarge = 1 Then
        If Not Target.Locked Then
            OldVal = Target.Value
        Else
            OldVal = vbNullString
        End If
    Else
        OldVal = vbNullString
    End If
End Sub

Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
    Dim c As Range
    Dim LogWs As Worksheet
    Dim NextRow As Long
    Dim sUser As String, sTime As String, OldStr As String, NewStr As String
    
    On Error GoTo CleanUp
    Application.EnableEvents = False
    
    Set LogWs = ThisWorkbook.Worksheets("ChangeLogHidden")
    sUser = Environ("USERNAME") & " (" & Application.UserName & ")"
    sTime = Format(Now, "yyyy-mm-dd HH:mm:ss")
    
    For Each c In Target.Cells
        If c.Locked Then GoTo NextC
        
        OldStr = Nz(OldVal)
        NewStr = Nz(c.Value)
        
        If OldStr = NewStr And OldVal <> vbNullString Then GoTo NextC
        
        NextRow = LogWs.Cells(LogWs.Rows.Count, 1).End(xlUp).Row + 1
        
        With LogWs
            .Cells(NextRow, 1) = Sh.Name & "!" & c.Address(False, False)
            .Cells(NextRow, 2) = sUser
            With .Cells(NextRow, 3)
                .ClearFormats
                .NumberFormat = "@"
                .Value = sTime
                .Font.Size = 10
            End With
            .Cells(NextRow, 4) = OldStr & " → " & NewStr
        End With
        
NextC:
        OldVal = vbNullString
    Next c
    
CleanUp:
    Application.EnableEvents = True
End Sub

Private Function Nz(v As Variant) As String
    If IsEmpty(v) Or IsNull(v) Or IsError(v) Then
        Nz = ""
    Else
        Nz = CStr(v)
    End If
End Function

标准模块 Module1 完整代码
#

vba

Public Sub ReApplyProtection()
    Dim ws As Worksheet
    Dim pwd As String: pwd = "x9#kP2@mZqW8!vN4L&"   ' 必须和 ThisWorkbook 里的一致
    
    Application.EnableEvents = False
    Application.ScreenUpdating = False
    
    On Error Resume Next
    For Each ws In ThisWorkbook.Worksheets
        ws.Unprotect pwd
        ws.Unprotect
    ' 双保险
    Next ws
    On Error GoTo 0
    
    For Each ws In ThisWorkbook.Worksheets
        If ws.Name <> "ChangeLogHidden" Then
            ws.Protect Password:=pwd, _
                       DrawingObjects:=True, Contents:=True, Scenarios:=True, _
                       UserInterfaceOnly:=True, _
                       AllowFormattingCells:=True, AllowFormattingColumns:=True, AllowFormattingRows:=True
        Else
            ws.Visible = xlSheetVeryHidden   ' 超级隐藏
        End If
    Next ws
    
    Application.EnableEvents = True
    Application.ScreenUpdating = True
End Sub

'========== 管理员后门:查看日志 ==========
Public Sub 查看修改日志()
    Dim pwd As String
    pwd = InputBox("请输入管理员密码查看日志", "管理员权限")
    If pwd = "8888" Then   ' ← 改成你喜欢的密码
        With ThisWorkbook.Worksheets("ChangeLogHidden")
            .Visible = xlSheetVisible
            .Activate
        End With
        MsgBox "查看完毕后关闭文件会自动重新隐藏", vbInformation
    Else
        MsgBox "密码错误", vbCritical
    End If
End Sub

'========== 一键导出日志到桌面 ==========
Public Sub 导出修改日志()
    Dim pwd As String
    pwd = InputBox("请输入管理员密码导出日志", "管理员权限")
    If pwd <> "8888" Then
        MsgBox "密码错误", vbCritical: Exit Sub
    End If
    
    Dim LogWs As Worksheet, wbNew As Workbook, SavePath As String
    Set LogWs = ThisWorkbook.Worksheets("ChangeLogHidden")
    
    SavePath = CreateObject("WScript.Shell").SpecialFolders("Desktop") & "\修改日志_" & Format(Now, "yyyymmdd_HHMMSS") & ".xlsx"
    
    LogWs.Copy
    Set wbNew = ActiveWorkbook
    With wbNew.Worksheets(1)
        .Range("A1").CurrentRegion.Columns.AutoFit
        .Rows(1).Font.Bold = True
    End With
    ActiveWindow.FreezePanes = False
    wbNew.Worksheets(1).Rows("2:2").Select
    ActiveWindow.FreezePanes = True
    
    wbNew.SaveAs SavePath, 51 '51 = xlsx
    wbNew.Close False
    MsgBox "已导出到桌面:" & SavePath, vbInformation
End Sub
Public Sub 修复时间格式()
    Dim LogWs As Worksheet, LastRow As Long, i As Long
    Set LogWs = ThisWorkbook.Worksheets("ChangeLogHidden")
    LastRow = LogWs.Cells(LogWs.Rows.Count, 1).End(xlUp).Row
    
    With LogWs
        .Range("C2:C" & LastRow).NumberFormat = "@"
        For i = 2 To LastRow
            If IsNumeric(.Cells(i, 3).Value) Then
                .Cells(i, 3).Value = Format(.Cells(i, 3).Value, "yyyy-mm-dd HH:MM:SS")
            End If
        Next i
        .Columns("A:D").AutoFit
    End With
    MsgBox "所有时间已永久修复为文字格式!", vbInformation
End Sub

总结:为什么这个方案是终极答案
#

  • 兼容 Excel 2007–2019–365
  • 保护状态下100%记录(不依赖批注)
  • 日志永不丢失,超级隐藏,用户永远看不到
  • 时间格式严格保持hh:mm:ss,字体设定为10号字体
  • 支持一键导出、密码查看、清空(可选)
  • 公式永久隐藏,用户永远无法解保护和看到代码

这就是整整折腾了十几轮后,真正“一次修改,终身无忧”的工业级最终方案。 以后谁再遇到同样需求,直接把这套代码甩过去就行了,永远不用再踩坑了。