Files
BLD_sync/migration.py
Misaka_Company 83c90f0161 Refactor configuration management and remove deprecated files
- Consolidated database, file source, and field mapping configurations into dedicated modules under the `config` directory.
- Removed hardcoded database connection details from `migration.py` and replaced them with imports from the new configuration structure.
- Updated `ntfy_utils.py` and `run_incremental_sync.py` to utilize the new configuration imports for cleaner code and better maintainability.
- Deleted `update_config.py` as its contents have been integrated into the new configuration files.
- Added a new `settings.local.json` for managing permissions related to script execution.
- Enhanced the structure of the migration tasks and Excel configurations for better organization and clarity.
2026-01-12 12:49:51 +08:00

173 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import pandas as pd
import os
import shutil
import urllib
from sqlalchemy import create_engine, text
from config import DB_CONFIG, MIGRATION_TASKS, TEMP_DIR
import ntfy_utils # 确保该文件在同一目录下
# ==========================================
# 1. 脚本配置 (Configuration)
# ==========================================
# 目标表配置
TARGET_DB_SCHEMA = "warehouseOutbound"
TARGET_TABLE_NAME = "customerProductType"
SQL_SOURCE_FILE_COL = "SourceFile" # 你在SQL中新增的字段名
# 字段映射常量
SQL_COL_YEAR = "合同年份"
SQL_COL_WORKSHOP = "车间号"
SQL_COL_ORDER = "工令号"
SQL_COL_MODEL = "客户型号"
# 运行参数
FORCE_UPDATE = False # 如果设为 True则无视时间对比强制更新所有文件
# ==========================================
# 2. 核心辅助函数
# ==========================================
def get_db_engine():
params = urllib.parse.quote_plus(
f"DRIVER={{{DB_CONFIG['driver']}}};"
f"SERVER={DB_CONFIG['server']};"
f"DATABASE={DB_CONFIG['database']};"
f"UID={DB_CONFIG['username']};"
f"PWD={DB_CONFIG['password']};"
f"TrustServerCertificate=yes;"
)
# fast_executemany 极大提高写入速度
return create_engine(f"mssql+pyodbc:///?odbc_connect={params}", fast_executemany=True)
def get_file_mtime(path):
"""获取文件最后修改时间戳"""
try:
return os.path.getmtime(path)
except OSError:
return 0
def delete_old_data(engine, filename):
"""根据 SourceFile 字段精确删除旧数据"""
full_table = f"[{TARGET_DB_SCHEMA}].[{TARGET_TABLE_NAME}]"
sql = text(f"DELETE FROM {full_table} WHERE [{SQL_SOURCE_FILE_COL}] = :fname")
with engine.begin() as conn:
conn.execute(sql, {"fname": filename})
# ==========================================
# 3. 迁移主逻辑
# ==========================================
def run_migration():
# 初始化环境
if not os.path.exists(TEMP_DIR):
os.makedirs(TEMP_DIR)
engine = get_db_engine()
sync_count = 0
error_count = 0
print(f"🚀 开始增量同步任务 (强制更新={FORCE_UPDATE})")
for task in MIGRATION_TASKS:
remote_path = task['file_path']
filename = os.path.basename(remote_path)
local_path = os.path.join(TEMP_DIR, filename)
# 1. 检查源文件
if not os.path.exists(remote_path):
msg = f"远程文件未找到: {remote_path}"
print(f"{msg}")
ntfy_utils.send_error(msg)
continue
# 2. 增量判定
remote_mtime = get_file_mtime(remote_path)
local_mtime = get_file_mtime(local_path)
if not FORCE_UPDATE and os.path.exists(local_path) and remote_mtime <= local_mtime:
print(f"⏭️ 跳过: {filename} (文件未变更)")
continue
print(f"🔄 正在处理: {filename} ...")
try:
# 3. 复制文件到本地 temp
shutil.copy2(remote_path, local_path)
# 4. 读取 Excel
xls_dict = pd.read_excel(local_path, sheet_name=task['sheet_names'])
if not isinstance(xls_dict, dict):
xls_dict = {task['sheet_names'][0]: xls_dict}
# 准备存放该文件所有 Sheet 的合并数据
df_all_sheets = []
for sheet_name, df in xls_dict.items():
if df.empty: continue
# 清洗与过滤
df.columns = df.columns.astype(str).str.strip()
source_cols = list(task['mapping'].keys())
missing = [c for c in source_cols if c not in df.columns]
if missing:
print(f" ⚠️ Sheet[{sheet_name}] 缺失列: {missing}")
continue
# 提取并重命名
df_subset = df[source_cols].copy()
df_subset.rename(columns=task['mapping'], inplace=True)
# 注入年份和来源文件名
df_subset[SQL_COL_YEAR] = task['year']
df_subset[SQL_SOURCE_FILE_COL] = filename # 存入文件名,用于下次精准删除
# 数据清洗
subset_keys = [SQL_COL_YEAR, SQL_COL_WORKSHOP, SQL_COL_ORDER]
df_subset.dropna(subset=subset_keys, inplace=True)
df_subset.drop_duplicates(subset=subset_keys, keep='first', inplace=True)
if not df_subset.empty:
df_all_sheets.append(df_subset)
# 5. 写入数据库
if df_all_sheets:
final_df = pd.concat(df_all_sheets, ignore_index=True)
# 执行删除并插入 (事务)
with engine.begin() as conn:
# A. 删除旧记录
delete_sql = text(f"DELETE FROM [{TARGET_DB_SCHEMA}].[{TARGET_TABLE_NAME}] WHERE [{SQL_SOURCE_FILE_COL}] = :fname")
conn.execute(delete_sql, {"fname": filename})
# B. 插入新记录
final_df.to_sql(
name=TARGET_TABLE_NAME,
schema=TARGET_DB_SCHEMA,
con=conn,
if_exists='append',
index=False,
chunksize=1000
)
print(f" ✅ 成功同步: {len(final_df)} 行记录")
sync_count += 1
else:
print(f" ⚠️ 警告: 文件内容为空或格式不符")
except Exception as e:
error_msg = f"文件 [{filename}] 处理失败: {str(e)}"
print(f"{error_msg}")
ntfy_utils.send_error(error_msg)
error_count += 1
# 结束汇总
summary = f"同步完成: 成功 {sync_count} 个文件, 失败 {error_count} 个文件。"
print(f"\n🏁 {summary}")
if sync_count > 0:
# 只有在有实际更新时才发送成功通知
ntfy_utils.send_ntfy(summary, title="📊 数据迁移报告", tags=["package"])
if __name__ == "__main__":
run_migration()