first commit

This commit is contained in:
Misaka_Company
2026-01-06 15:40:38 +08:00
commit 77b72392a3
9 changed files with 903 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
__pycache__/
.venv
build
dist
log
*.spec

51
check_drivers.py Normal file
View File

@@ -0,0 +1,51 @@
import pyodbc
def check_drivers():
print("正在扫描系统 ODBC 驱动...\n")
try:
drivers = pyodbc.drivers()
except Exception as e:
print(f"❌ 错误: 无法读取驱动列表。原因: {e}")
return
if not drivers:
print("❌ 未找到任何 ODBC 驱动。")
return
print(f"✅ 共发现 {len(drivers)} 个驱动:")
print("=" * 50)
for d in drivers:
print(f"{d}")
print("=" * 50)
print("\n[配置建议]")
# --- 1. 寻找 SQL Server 驱动 ---
# 优先级ODBC Driver 18 > 17 > 13 > Native Client > SQL Server (旧)
sql_candidates = [d for d in drivers if "SQL Server" in d]
best_sql = None
# 简单的版本优先级判断
if any("ODBC Driver 18" in d for d in sql_candidates):
best_sql = "ODBC Driver 18 for SQL Server"
elif any("ODBC Driver 17" in d for d in sql_candidates):
best_sql = "ODBC Driver 17 for SQL Server"
elif any("ODBC Driver 13" in d for d in sql_candidates):
best_sql = "ODBC Driver 13 for SQL Server"
elif sql_candidates:
best_sql = sql_candidates[0] # 随便取一个其他的
if best_sql:
print(f"👉 SQL Server 驱动 (填入 config.py): {{{best_sql}}}")
else:
print("⚠️ 未找到 SQL Server 驱动,请下载安装 'ODBC Driver 17 for SQL Server'")
# --- 2. 寻找 Access 驱动 ---
acc_candidates = [d for d in drivers if "Access Driver" in d]
if acc_candidates:
print(f"👉 Access 驱动 (填入 config.py): {{{acc_candidates[0]}}}")
else:
print("⚠️ 未找到 Access 驱动,请下载安装 'Access Database Engine'")
if __name__ == "__main__":
check_drivers()

58
config.py Normal file
View File

@@ -0,0 +1,58 @@
# config.py
# ================= 数据库连接配置 =================
SQL_SERVER_CONN = {
'driver': '{ODBC Driver 18 for SQL Server}',
'server': '192.168.110.114',
'database': 'CompanyDB',
'uid': 'peng',
'pwd': 'Cqbld123456.'
}
# Access 驱动配置 (根据你的 Office 版本选择 mdb 或 accdb)
ACCESS_DRIVER = "{Microsoft Access Driver (*.mdb, *.accdb)}"
# ================= 日志表配置 =================
# 根据你提供的表结构配置列名
LOG_TABLE_CONFIG = {
'schema': 'dbo',
'table_name': 'TableChangeLog',
# 下面定义字段名,方便代码引用,防止硬编码
'col_log_id': 'LogID',
'col_table_name': 'TableName',
'col_record_id': 'RecordID',
'col_address': 'TableAddress', # 使用 TableAddress 作为文件路径来源
'col_synced': 'Synced'
}
# ================= 同步映射配置 (核心重构) =================
# 结构: { "Access文件绝对路径": { "Access表名": { SQL目标配置 } } }
# 这样即使不同文件有相同的表名,也能区分开
SYNC_MAPPING = {
# 第一个 Access 文件
r"\\192.168.110.114\生产进度表\2025年数据\生产合同数据.accdb": {
"26年压力表合同数据": {
"target_schema": "productionContractData",
"target_table": "26年压力表合同数据", # 映射到 A 表
"pk_col": "ID"
},
"26年温度计合同数据": {
"target_schema": "productionContractData",
"target_table": "26年温度计合同数据",
"pk_col": "ID"
}
},
# # 第二个 Access 文件 (可能有同名的 UserInfo 表)
# r"D:\Data\ProjectB\backup.mdb": {
# "UserInfo": {
# "target_schema": "dbo",
# "target_table": "ProjB_UserInfo", # 映射到 B 表
# "pk_col": "ID"
# }
# }
}
# ================= 运行参数 =================
POLL_INTERVAL = 60 # 轮询间隔(秒)
BATCH_SIZE = 1000 # 批量处理大小

36
db_utils.py Normal file
View File

@@ -0,0 +1,36 @@
# db_utils.py
import pyodbc
import config
def get_sql_conn():
"""获取 SQL Server 连接"""
c = config.SQL_SERVER_CONN
# 显式添加 TrustServerCertificate=yes 以兼容 ODBC Driver 18+
conn_str = (
f"DRIVER={c['driver']};SERVER={c['server']};"
f"DATABASE={c['database']};UID={c['uid']};PWD={c['pwd']};"
"Encrypt=yes;TrustServerCertificate=yes;"
)
return pyodbc.connect(conn_str)
def get_access_conn(file_path):
"""获取 Access 连接"""
conn_str = f"DRIVER={config.ACCESS_DRIVER};DBQ={file_path};"
return pyodbc.connect(conn_str)
def fmt_table(schema, table):
"""格式化 SQL Server 表名 [schema].[table]"""
return f"[{schema}].[{table}]"
def get_columns(cursor, table_name):
"""获取 Access 表的列名"""
# Access 查询表名加 []
cursor.execute(f"SELECT TOP 1 * FROM [{table_name}]")
return [column[0] for column in cursor.description]
def generate_insert_sql(target_schema, target_table, columns):
"""生成带架构的 INSERT 语句"""
full_table_name = fmt_table(target_schema, target_table)
col_str = ",".join([f"[{col}]" for col in columns])
placeholders = ",".join(["?"] * len(columns))
return f"INSERT INTO {full_table_name} ({col_str}) VALUES ({placeholders})"

83
init_full_sync.py Normal file
View File

@@ -0,0 +1,83 @@
# init_full_sync.py
import pyodbc
import config
import db_utils
import time
import os
def run_full_sync():
print("=== 开始全量初始化同步 ===")
print("注意:将基于 config.SYNC_MAPPING 中的文件路径进行处理")
sql_conn = db_utils.get_sql_conn()
sql_cursor = sql_conn.cursor()
sql_cursor.fast_executemany = True
# 第一层循环:遍历配置文件中的所有文件路径
for acc_path, tables_map in config.SYNC_MAPPING.items():
if not os.path.exists(acc_path):
print(f"⚠️ [跳过] 文件不存在: {acc_path}")
continue
print(f"\n📂 正在处理文件: {acc_path}")
try:
acc_conn = db_utils.get_access_conn(acc_path)
acc_cursor = acc_conn.cursor()
# 第二层循环:遍历该文件下的所有表映射
for acc_table, target_config in tables_map.items():
target_schema = target_config['target_schema']
target_table = target_config['target_table']
full_target_name = db_utils.fmt_table(target_schema, target_table)
print(f" 👉 同步表: [{acc_table}] -> {full_target_name}")
try:
# 1. 检查 Access 表是否存在
acc_cursor.execute(f"SELECT TOP 1 * FROM [{acc_table}]")
# 2. 获取列结构
columns = [col[0] for col in acc_cursor.description]
insert_sql = db_utils.generate_insert_sql(target_schema, target_table, columns)
# 3. TRUNCATE 目标表
sql_cursor.execute(f"TRUNCATE TABLE {full_target_name}")
# 4. 开启 IDENTITY_INSERT (尝试)
try: sql_cursor.execute(f"SET IDENTITY_INSERT {full_target_name} ON")
except: pass
# 5. 传输数据
print(f" 正在读取并写入数据...")
acc_cursor.execute(f"SELECT * FROM [{acc_table}]")
total_rows = 0
while True:
rows = acc_cursor.fetchmany(config.BATCH_SIZE)
if not rows: break
sql_cursor.executemany(insert_sql, rows)
total_rows += len(rows)
print(f" 已写入 {total_rows} 行...", end='\r')
# 6. 关闭 IDENTITY_INSERT
try: sql_cursor.execute(f"SET IDENTITY_INSERT {full_target_name} OFF")
except: pass
sql_conn.commit()
print(f"\n ✅ 完成。")
except Exception as tbl_err:
print(f" ❌ 表级错误: {tbl_err}")
sql_conn.rollback()
acc_conn.close()
except Exception as file_err:
print(f" ❌ 文件级错误: {file_err}")
sql_conn.close()
print("\n=== 全量同步结束 ===")
if __name__ == "__main__":
run_full_sync()

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
pyodbc>=5.0.0

222
run_incremental_sync.py Normal file
View File

@@ -0,0 +1,222 @@
import time
import os
import sys
import pyodbc
import config
import db_utils
import logging
from logging.handlers import TimedRotatingFileHandler
from collections import defaultdict
# ================= 日志系统配置 (新增) =================
def setup_logger():
"""
配置双向日志系统:控制台输出 + 按天轮转的文件日志
"""
# 1. 确保日志目录存在
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'log')
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# 2. 创建 Logger
logger = logging.getLogger("SyncService")
logger.setLevel(logging.INFO)
logger.handlers = [] # 清除旧句柄,防止重复打印
# 3. 文件处理器 (按天轮转)
# filename: 正在写入的日志名
# when='midnight': 每天午夜轮转
# interval=1: 间隔1天
# backupCount=30: 保留最近30个文件
log_file_path = os.path.join(log_dir, 'sync_service.log')
file_handler = TimedRotatingFileHandler(
log_file_path, when='midnight', interval=1, backupCount=30, encoding='utf-8'
)
file_handler.suffix = "%Y-%m-%d" # 轮转后的文件名后缀
# 文件日志格式:[时间] [级别] 消息
file_fmt = logging.Formatter('%(asctime)s - [%(levelname)s] - %(message)s')
file_handler.setFormatter(file_fmt)
# 4. 控制台处理器 (屏幕输出)
console_handler = logging.StreamHandler(sys.stdout)
# 控制台日志格式:[时间] 消息 (保持简洁)
console_fmt = logging.Formatter('%(asctime)s - %(message)s', datefmt='%H:%M:%S')
console_handler.setFormatter(console_fmt)
# 5. 添加处理器
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger
# 初始化全局 Logger
logger = setup_logger()
# ================= 辅助函数 =================
def clean_access_path(raw_addr):
"""清洗 TableAddress 字段"""
if not raw_addr: return ""
s = str(raw_addr).strip()
if s.upper().startswith(";DATABASE="): return s[10:]
if s.upper().startswith("LOCAL:"): return s[6:]
return s
def get_norm_path(path):
"""获取标准化路径"""
if not path: return ""
return os.path.normpath(path).lower()
# ================= 主逻辑 =================
def process_logs():
sql_conn = db_utils.get_sql_conn()
sql_cursor = sql_conn.cursor()
sql_cursor.fast_executemany = True
cols = config.LOG_TABLE_CONFIG
# --- 0. 预处理配置映射 ---
normalized_mapping = {}
for cfg_path, cfg_tables in config.SYNC_MAPPING.items():
norm_key = get_norm_path(cfg_path)
normalized_mapping[norm_key] = {
'original_path': cfg_path,
'tables': cfg_tables
}
try:
log_full_name = db_utils.fmt_table(cols['schema'], cols['table_name'])
# --- 1. 读取日志 ---
query_log = f"""
SELECT TOP 1000
{cols['col_log_id']},
{cols['col_table_name']},
{cols['col_record_id']},
{cols['col_address']}
FROM {log_full_name}
WHERE {cols['col_synced']} = 0
AND TableType = 'LINKED_ACCESS'
ORDER BY {cols['col_log_id']} ASC
"""
sql_cursor.execute(query_log)
logs = sql_cursor.fetchall()
if not logs:
return False
logger.info(f"📥 收到 {len(logs)} 条链接表变更日志...")
# --- 2. 任务分组 ---
tasks = defaultdict(lambda: defaultdict(lambda: {'record_ids': set(), 'log_ids': []}))
for row in logs:
log_id, acc_table, record_id, raw_address = row
clean_path = clean_access_path(raw_address)
norm_lookup_key = get_norm_path(clean_path)
# 校验配置
if norm_lookup_key not in normalized_mapping:
# 记录一条警告日志,但不刷屏
logger.warning(f"忽略未配置的文件路径: {clean_path}")
continue
mapping_node = normalized_mapping[norm_lookup_key]
if acc_table not in mapping_node['tables']:
logger.warning(f"忽略未配置的表: {acc_table} (在文件 {os.path.basename(clean_path)} 中)")
continue
tasks[clean_path][acc_table]['record_ids'].add(record_id)
tasks[clean_path][acc_table]['log_ids'].append(log_id)
# --- 3. 执行同步循环 ---
for file_path, tables_data in tasks.items():
if not os.path.exists(file_path):
logger.error(f"无法访问文件: {file_path}")
continue
try:
acc_conn = db_utils.get_access_conn(file_path)
acc_cursor = acc_conn.cursor()
except Exception as conn_err:
logger.error(f"连接 Access 失败 [{file_path}]: {conn_err}")
continue
norm_key = get_norm_path(file_path)
config_node = normalized_mapping[norm_key]
for acc_table, data in tables_data.items():
record_ids = list(data['record_ids'])
log_ids = data['log_ids']
target_conf = config_node['tables'][acc_table]
target_schema = target_conf['target_schema']
target_table = target_conf['target_table']
pk_col = target_conf['pk_col']
target_full_name = db_utils.fmt_table(target_schema, target_table)
try:
ids_placeholders = ','.join(['?'] * len(record_ids))
# A. Access 读取
acc_sql = f"SELECT * FROM [{acc_table}] WHERE [{pk_col}] IN ({ids_placeholders})"
acc_cursor.execute(acc_sql, record_ids)
new_rows = acc_cursor.fetchall()
acc_cols = [col[0] for col in acc_cursor.description]
# B. SQL Server 写入
del_sql = f"DELETE FROM {target_full_name} WHERE [{pk_col}] IN ({ids_placeholders})"
sql_cursor.execute(del_sql, record_ids)
if new_rows:
insert_sql = db_utils.generate_insert_sql(target_schema, target_table, acc_cols)
try: sql_cursor.execute(f"SET IDENTITY_INSERT {target_full_name} ON")
except: pass
sql_cursor.executemany(insert_sql, new_rows)
try: sql_cursor.execute(f"SET IDENTITY_INSERT {target_full_name} OFF")
except: pass
# C. 更新日志状态
log_placeholders = ','.join(['?'] * len(log_ids))
update_log_sql = f"""
UPDATE {log_full_name}
SET {cols['col_synced']} = 1
WHERE {cols['col_log_id']} IN ({log_placeholders})
"""
sql_cursor.execute(update_log_sql, log_ids)
sql_conn.commit()
logger.info(f"✅ 同步成功: {target_table} 更新 {len(record_ids)} 条 (来源: {os.path.basename(file_path)})")
except Exception as tbl_err:
logger.error(f"处理表 {acc_table} 异常: {tbl_err}")
sql_conn.rollback()
acc_conn.close()
return True
except Exception as e:
logger.critical(f"全局严重异常: {e}")
return False
finally:
try: sql_conn.close()
except: pass
if __name__ == "__main__":
logger.info("=== 增量同步服务已启动 (日志模式: log/sync_service.log) ===")
while True:
try:
has_work = process_logs()
time.sleep(0.5 if has_work else config.POLL_INTERVAL)
except KeyboardInterrupt:
logger.info("收到停止指令,服务正在关闭...")
break
except Exception as e:
logger.critical(f"主循环崩溃: {e}")
time.sleep(5) # 防止死循环刷屏

350
vba.txt Normal file
View File

@@ -0,0 +1,350 @@
Option Compare Database
Option Explicit
' === 模块级变量 ===
Private m_IsNewRecord As Boolean
Private m_DeletedIDs As New Collection
' === 安全的字符串转义 ===
Private Function SafeSQL(txt As String) As String
SafeSQL = Replace(txt, "'", "''") ' SQL标准转义
End Function
' === 获取窗体数据源表名 (修复版) ===
Private Function GetFormTableName() As String
On Error Resume Next
Dim tableName As String
' 方法 1: 【最推荐】直接检查主键字段的来源表
' 既然你的查询里有 "缺件记录.ID",那么 ID 字段的 SourceTable 属性就是 "缺件记录"
' 这比解析字符串准确得多,也不怕查询改名
tableName = Me.Recordset.Fields("ID").SourceTable
' 如果方法1成功直接返回
If Len(tableName) > 0 Then
GetFormTableName = tableName
Exit Function
End If
' 方法 2: 检查窗体的 UniqueTable 属性
' Access 经常在属性表中设置这个值来指定多表查询中哪个表可更新
tableName = Me.UniqueTable
If Len(tableName) > 0 Then
GetFormTableName = tableName
Exit Function
End If
' 方法 3: (保底方案) 解析 RecordSource 字符串
' 只有当上面都失败时,才尝试去解析 SQL
Dim recordSource As String
recordSource = Trim(Me.recordSource & "")
' 如果是保存的查询名不含SELECT需要先获取查询的SQL
If InStr(UCase(recordSource), "SELECT") = 0 Then
' 检查是否为查询对象
Dim qdf As DAO.QueryDef
Set qdf = CurrentDb.QueryDefs(recordSource)
If Not qdf Is Nothing Then
recordSource = qdf.sql ' 获取查询背后的真实 SQL
Else
' 既不是SELECT又不是查询那只能是直接的表名了
GetFormTableName = recordSource
Exit Function
End If
Set qdf = Nothing
End If
' 解析 SQL: 提取 FROM 之后、JOIN 之前的第一个词
Dim fromPos As Long
fromPos = InStr(1, UCase(recordSource), " FROM ", vbTextCompare)
If fromPos > 0 Then
Dim tempStr As String
tempStr = Mid(recordSource, fromPos + 6) ' 跳过 " FROM "
tempStr = Trim(tempStr)
' 截断点:遇到 JOIN, WHERE, ORDER BY, GROUP BY 或逗号时停止
Dim stopChars As Variant
Dim i As Integer, minPos As Long, p As Long
stopChars = Array(" LEFT ", " RIGHT ", " INNER ", " OUTER ", " JOIN ", " WHERE ", " ORDER ", " GROUP ", ",")
minPos = Len(tempStr) + 1
For i = LBound(stopChars) To UBound(stopChars)
p = InStr(1, UCase(tempStr), stopChars(i), vbTextCompare)
If p > 0 And p < minPos Then minPos = p
Next i
tableName = Trim(Left(tempStr, minPos - 1))
' 清理括号
tableName = Replace(tableName, "[", "")
tableName = Replace(tableName, "]", "")
End If
' 默认值
If Len(tableName) = 0 Then tableName = "UnknownTable"
GetFormTableName = tableName
On Error GoTo 0
End Function
' === 获取表地址(连接字符串或本地路径) ===
Private Function GetTableAddress(tableName As String) As String
On Error Resume Next
Dim db As DAO.Database
Dim tdf As DAO.TableDef
Dim address As String
Set db = CurrentDb
Set tdf = db.TableDefs(tableName)
If Not tdf Is Nothing Then
If Len(Trim(tdf.Connect & "")) > 0 Then
' 链接表:返回连接字符串
address = tdf.Connect
Else
' 本地表:返回当前数据库路径
address = "LOCAL:" & CurrentDb.Name
End If
Else
address = "UNKNOWN"
End If
' 清理过长的连接字符串(可选)
If Len(address) > 500 Then
address = Left(address, 500) & "..."
End If
GetTableAddress = address
Set tdf = Nothing
Set db = Nothing
On Error GoTo 0
End Function
' === 获取表类型 ===
Private Function GetTableType(tableName As String) As String
On Error Resume Next
Dim db As DAO.Database
Dim tdf As DAO.TableDef
Dim connectStr As String
Dim tableType As String
Set db = CurrentDb
Set tdf = db.TableDefs(tableName)
If Not tdf Is Nothing Then
connectStr = UCase(Trim(tdf.Connect & ""))
If Len(connectStr) = 0 Then
' 本地表
tableType = "LOCAL"
ElseIf InStr(connectStr, "ODBC;") > 0 Then
' ODBC链接表
If InStr(connectStr, "SQL SERVER") > 0 Then
tableType = "LINKED_SQLSERVER"
ElseIf InStr(connectStr, "MYSQL") > 0 Then
tableType = "LINKED_MYSQL"
ElseIf InStr(connectStr, "ORACLE") > 0 Then
tableType = "LINKED_ORACLE"
Else
tableType = "LINKED_ODBC"
End If
ElseIf InStr(connectStr, "MS ACCESS") > 0 Or InStr(connectStr, ".ACCDB") > 0 Or InStr(connectStr, ".MDB") > 0 Then
' Access链接表
tableType = "LINKED_ACCESS"
ElseIf InStr(connectStr, "EXCEL") > 0 Or InStr(connectStr, ".XLS") > 0 Then
' Excel链接表
tableType = "LINKED_EXCEL"
ElseIf InStr(connectStr, "TEXT;") > 0 Or InStr(connectStr, ".TXT") > 0 Or InStr(connectStr, ".CSV") > 0 Then
' 文本/CSV链接表
tableType = "LINKED_TEXT"
Else
' 其他类型链接表
tableType = "LINKED_OTHER"
End If
Else
tableType = "UNKNOWN"
End If
GetTableType = tableType
Set tdf = Nothing
Set db = Nothing
On Error GoTo 0
End Function
' === 获取本机IP地址(带超时保护) ===
Private Function GetLocalIPAddress() As String
On Error Resume Next
Dim objWMI As Object
Dim colItems As Object
Dim objItem As Object
Dim ip As String
Dim startTime As Double
startTime = Timer
Set objWMI = GetObject("winmgmts:\\.\root\cimv2")
' 超时保护:WMI查询最多等待2秒
If Timer - startTime > 2 Then
GetLocalIPAddress = "127.0.0.1"
Exit Function
End If
Set colItems = objWMI.ExecQuery("SELECT IPAddress FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = True")
For Each objItem In colItems
If Not IsNull(objItem.ipAddress) Then
ip = objItem.ipAddress(0)
If InStr(ip, ":") = 0 Then
GetLocalIPAddress = ip
Exit Function
End If
End If
Next
GetLocalIPAddress = "127.0.0.1"
On Error GoTo 0
End Function
' === 异步日志写入(不阻塞用户操作) ===
Private Sub WriteLog(actionType As String, recordID As String)
Dim sql As String
Dim ipAddr As String
Dim remoteTable As String
Dim localTable As String
Dim tableAddr As String
Dim tableType As String
Dim computerName As String ' 新增:计算机名称变量
' --- 配置区域 ---
remoteTable = "dbo_TableChangeLog"
' ----------------
' 【关键1】立即返回,不等待耗时操作
On Error Resume Next
' 【新增】动态获取窗体数据源表名
localTable = GetFormTableName()
' 【新增】动态获取表地址
tableAddr = GetTableAddress(localTable)
' 【新增】动态获取表类型
tableType = GetTableType(localTable)
' 快速验证:表是否存在
If IsNull(DLookup("Name", "MSysObjects", "Name='" & SafeSQL(remoteTable) & "'")) Then
Debug.Print "Log Warning: 表 " & remoteTable & " 不存在,跳过日志"
Exit Sub
End If
' 【关键2】防止空值和特殊字符
If Len(Trim(recordID & "")) = 0 Then recordID = "NULL"
recordID = SafeSQL(recordID)
' 【关键3】快速获取IP(带缓存)
Static cachedIP As String
If cachedIP = "" Then cachedIP = GetLocalIPAddress()
ipAddr = cachedIP
' 获取当前电脑的【计算机名称】,替代原有的 Windows登录用户名
computerName = Environ$("COMPUTERNAME")
' 【关键4】参数化查询(如果支持)或安全拼接
' 注意:不写 ChangeDate 字段,让 SQL Server 的 DEFAULT GETDATE() 自动填充
sql = "INSERT INTO " & remoteTable & " " & _
"(TableName, TableAddress, TableType, RecordID, ActionType, IPAddress, UserName, FilePath) " & _
"VALUES ('" & SafeSQL(localTable) & "', " & _
"'" & SafeSQL(tableAddr) & "', " & _
"'" & SafeSQL(tableType) & "', " & _
"'" & recordID & "', " & _
"'" & SafeSQL(actionType) & "', " & _
"'" & SafeSQL(ipAddr) & "', " & _
"'" & SafeSQL(computerName) & "', " & _
"'" & SafeSQL(CurrentDb.Name) & "')"
' 【关键5】静默执行,绝不影响用户
CurrentDb.Execute sql ' 移除 dbFailOnError!
If Err.Number <> 0 Then
' 仅记录到立即窗口,不弹窗
Debug.Print Now & " - Log Failed: " & Err.Description & " | SQL: " & sql
' 可选:写入本地备份表
Call WriteLocalBackupLog(localTable, recordID, actionType)
End If
On Error GoTo 0
End Sub
' === 本地备份日志(防止远程失败) ===
Private Sub WriteLocalBackupLog(tblName As String, recID As String, actType As String)
On Error Resume Next
' Access 本地表使用 Now() 函数
CurrentDb.Execute "INSERT INTO LocalLogBackup (TableName, RecordID, ActionType, LogTime) " & _
"VALUES ('" & SafeSQL(tblName) & "', '" & SafeSQL(recID) & "', '" & SafeSQL(actType) & "', Now())"
On Error GoTo 0
End Sub
' ==============================
' 窗体事件逻辑(增强版)
' ==============================
' 1. 保存前状态判断
Private Sub Form_BeforeUpdate(Cancel As Integer)
On Error Resume Next ' 【保护】防止日志逻辑影响保存
m_IsNewRecord = Me.NewRecord
On Error GoTo 0
End Sub
' 2. 修改保存后
Private Sub Form_AfterUpdate()
On Error Resume Next
If Not m_IsNewRecord Then
Call WriteLog("UPDATE", Nz(Me.ID.Value, ""))
End If
On Error GoTo 0
End Sub
' 3. 新增确认后
Private Sub Form_AfterInsert()
On Error Resume Next
Call WriteLog("INSERT", Nz(Me.ID.Value, ""))
m_IsNewRecord = False
On Error GoTo 0
End Sub
' 4. 删除开始(收集ID)
Private Sub Form_Delete(Cancel As Integer)
On Error Resume Next
If Not IsNull(Me.ID.Value) Then
m_DeletedIDs.Add CStr(Me.ID.Value)
End If
On Error GoTo 0
End Sub
' 5. 删除确认后(批量写入)
Private Sub Form_AfterDelConfirm(Status As Integer)
On Error Resume Next
Dim vID As Variant
If Status = acDeleteOK Then
For Each vID In m_DeletedIDs
Call WriteLog("DELETE", CStr(vID))
Next
End If
' 清理集合
Set m_DeletedIDs = Nothing
Set m_DeletedIDs = New Collection
On Error GoTo 0
End Sub
' === 窗体关闭时清理(可选) ===
Private Sub Form_Unload(Cancel As Integer)
On Error Resume Next
Set m_DeletedIDs = Nothing
On Error GoTo 0
End Sub

96
vbareplace.py Normal file
View File

@@ -0,0 +1,96 @@
import os
import sys
import win32com.client
import tkinter as tk
from tkinter import messagebox
# 隐藏 tkinter 主窗口
root = tk.Tk()
root.withdraw()
def main():
# 检查是否通过拖拽传入文件
if len(sys.argv) != 2:
messagebox.showerror("错误", "请将一个 Access 数据库文件拖拽到此 EXE 上运行!")
return
access_file = sys.argv[1]
# 检查文件是否存在且是 Access 文件
if not os.path.exists(access_file):
messagebox.showerror("错误", f"文件不存在:\n{access_file}")
return
ext = os.path.splitext(access_file)[1].lower()
if ext not in ['.accdb', '.mdb']:
messagebox.showwarning("警告", "这不是一个 Access 数据库文件(.accdb 或 .mdb\n将尝试继续,但可能失败。")
# 获取脚本所在目录EXE 同目录)
if getattr(sys, 'frozen', False):
script_dir = os.path.dirname(sys.executable)
else:
script_dir = os.path.dirname(os.path.abspath(__file))
vba_txt_file = os.path.join(script_dir, "vba.txt")
if not os.path.exists(vba_txt_file):
messagebox.showerror("错误", f"未找到 vba.txt 文件!\n请确保 vba.txt 与 EXE 在同一目录。\n路径: {vba_txt_file}")
return
# 读取新 VBA 代码
try:
with open(vba_txt_file, "r", encoding="utf-8") as f:
new_code = f.read()
except Exception as e:
messagebox.showerror("错误", f"读取 vba.txt 失败:\n{e}")
return
try:
# 启动 Access
access = win32com.client.Dispatch("Access.Application")
access.Visible = False # 后台运行,不显示窗口
# 打开数据库
access.OpenCurrentDatabase(access_file)
all_forms = access.CurrentProject.AllForms
form_count = all_forms.Count
if form_count == 0:
messagebox.showinfo("完成", "数据库中没有窗体,无需替换。")
access.CloseCurrentDatabase()
access.Quit()
return
replaced = 0
for i in range(form_count):
form_name = all_forms.Item(i).Name
try:
access.DoCmd.OpenForm(form_name, 1) # acDesign = 1
form = access.Forms(form_name)
module = form.Module
line_count = module.CountOfLines
if line_count > 0:
module.DeleteLines(1, line_count)
if new_code.strip():
module.InsertLines(1, new_code)
access.DoCmd.Close(0, form_name, 1) # acForm=0, acSaveYes=1
replaced += 1
except Exception as e:
access.DoCmd.Close(0, form_name, 0) # 尝试关闭,避免卡住
print(f"处理窗体 {form_name} 时出错: {e}")
access.CloseCurrentDatabase()
access.Quit()
messagebox.showinfo("成功", f"操作完成!\n\n已处理 {form_count} 个窗体,成功替换 {replaced} 个。\n\n文件已保存: {os.path.basename(access_file)}")
except Exception as e:
messagebox.showerror("运行错误", f"操作失败:\n{e}\n\n请检查是否已启用 VBA 项目访问信任。")
if __name__ == "__main__":
main()