Files
2026-01-04 10:00:08 +08:00

372 lines
14 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
学生操行分历史数据迁移 - 脚本4:正式导入处理(简化版本)
功能说明:
- 使用高效的INSERT INTO SELECT语句直接导入数据
- 自动生成雪花ID作为主键
- 支持重新处理模式:重新导入所有已关联的数据
- 简洁高效,避免复杂的重复检查和批量处理逻辑
执行方式:
python 04_import_to_production.py # 正常模式:仅处理待导入数据
python 04_import_to_production.py --reprocess # 重新处理模式:重新处理所有已关联数据
注意事项:
- 依赖脚本3已执行完成
- 使用数据库的INSERT IGNORE处理重复数据
- 性能大幅提升,代码大幅简化
"""
from __future__ import annotations
import argparse
import logging
import sys
import time
from datetime import datetime
from typing import Any, Dict, List
import pymysql
from db_config import DB_CONFIG, TABLE_CONFIG, CONSTANTS
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler("logs/04_import_production.log", mode='a', encoding='utf-8')
]
)
logger = logging.getLogger("import_production")
class ProductionImporter:
"""正式表导入处理器(简化版本)"""
def __init__(self):
self.db_config = DB_CONFIG
self.temp_parsed_table = TABLE_CONFIG["temp_parsed"]
self.production_table = TABLE_CONFIG["production"]
self.tenant_id = CONSTANTS["DEFAULT_TENANT_ID"]
self.create_by = CONSTANTS["DEFAULT_CREATE_BY"]
self.update_by = CONSTANTS["DEFAULT_UPDATE_BY"]
self.connection = None
self.sequence_counter = 0 # 雪花ID序列号计数器
self.last_timestamp = -1 # 上次时间戳
def connect_db(self) -> None:
"""连接数据库"""
try:
self.connection = pymysql.connect(**self.db_config)
logger.info("数据库连接成功")
except Exception as e:
logger.error(f"数据库连接失败: {e}")
raise
def disconnect_db(self) -> None:
"""断开数据库连接"""
if self.connection:
self.connection.close()
logger.info("数据库连接已断开")
def generate_snowflake_id(self) -> int:
"""生成雪花ID"""
epoch = CONSTANTS["SNOWFLAKE_EPOCH"]
worker_id = CONSTANTS["WORKER_ID"]
datacenter_id = CONSTANTS["DATACENTER_ID"]
timestamp = int(time.time() * 1000)
if timestamp == self.last_timestamp:
self.sequence_counter = (self.sequence_counter + 1) % 4096
else:
self.sequence_counter = 0
self.last_timestamp = timestamp
timestamp_part = (timestamp - epoch) << 22
datacenter_part = datacenter_id << 17
worker_part = worker_id << 12
sequence_part = self.sequence_counter
return timestamp_part | datacenter_part | worker_part | sequence_part
def get_pending_ids(self, reprocess: bool = False) -> List[int]:
"""获取待处理的记录ID"""
try:
with self.connection.cursor() as cursor:
if reprocess:
# 重新处理模式:获取所有已关联的数据
sql = f"""
SELECT id FROM {self.temp_parsed_table}
WHERE processing_status IN ('linked', 'imported', 'duplicate')
AND student_id IS NOT NULL
"""
else:
# 正常模式:只获取待导入的数据
sql = f"""
SELECT id FROM {self.temp_parsed_table}
WHERE processing_status = 'linked'
AND student_id IS NOT NULL
"""
cursor.execute(sql)
return [row[0] for row in cursor.fetchall()]
except Exception as e:
logger.error(f"获取待处理记录ID失败: {e}")
return []
def import_data_directly(self, record_ids: List[int]) -> Dict[str, int]:
"""使用INSERT INTO SELECT直接导入数据"""
if not record_ids:
return {'imported': 0, 'duplicates': 0, 'errors': 0}
try:
with self.connection.cursor() as cursor:
# 先预生成一批雪花ID
snowflake_ids = []
for _ in range(len(record_ids)):
snowflake_ids.append(self.generate_snowflake_id())
current_time = datetime.now()
# 创建临时表存储ID映射
temp_id_table = "temp_snowflake_ids"
cursor.execute(f"DROP TEMPORARY TABLE IF EXISTS {temp_id_table}")
cursor.execute(f"""
CREATE TEMPORARY TABLE {temp_id_table} (
record_id BIGINT PRIMARY KEY,
snowflake_id BIGINT UNIQUE
)
""")
# 批量插入ID映射
id_mapping = list(zip(record_ids, snowflake_ids))
cursor.executemany(f"""
INSERT INTO {temp_id_table} (record_id, snowflake_id) VALUES (%s, %s)
""", id_mapping)
# 使用INSERT IGNORE直接导入数据
insert_sql = f"""
INSERT IGNORE INTO {self.production_table} (
id, tenant_id, create_dept, create_by, create_time,
update_by, update_time, deleted,
academic_year, semester, month,
student_id, class_id, class_teacher_id,
score, remark
)
SELECT
t.snowflake_id,
'{self.tenant_id}',
NULL,
{self.create_by},
'{current_time.strftime('%Y-%m-%d %H:%M:%S')}',
{self.update_by},
'{current_time.strftime('%Y-%m-%d %H:%M:%S')}',
0,
p.academic_year, p.semester, p.month,
p.student_id, p.class_id, p.class_teacher_id,
p.score, COALESCE(p.remark, '')
FROM {self.temp_parsed_table} p
INNER JOIN {temp_id_table} t ON p.id = t.record_id
WHERE p.class_teacher_id IS NOT NULL
"""
cursor.execute(insert_sql)
imported_count = cursor.rowcount
# 计算重复和错误数量
total_processed = len(record_ids)
duplicates = total_processed - imported_count
errors = 0 # INSERT IGNORE不会产生错误,只有重复
self.connection.commit()
# 清理临时表
cursor.execute(f"DROP TEMPORARY TABLE {temp_id_table}")
return {
'imported': imported_count,
'duplicates': duplicates,
'errors': errors
}
except Exception as e:
logger.error(f"直接导入数据失败: {e}")
self.connection.rollback()
return {'imported': 0, 'duplicates': 0, 'errors': len(record_ids)}
def update_status_after_import(self, record_ids: List[int], imported_count: int, duplicate_count: int) -> None:
"""根据导入结果更新状态"""
try:
with self.connection.cursor() as cursor:
# 更新已导入的记录状态
if imported_count > 0:
imported_ids = record_ids[:imported_count]
placeholders = ','.join(['%s'] * len(imported_ids))
cursor.execute(f"""
UPDATE {self.temp_parsed_table}
SET processing_status = 'imported', update_time = NOW()
WHERE id IN ({placeholders})
""", imported_ids)
# 更新重复的记录状态
if duplicate_count > 0:
duplicate_ids = record_ids[imported_count:imported_count + duplicate_count]
placeholders = ','.join(['%s'] * len(duplicate_ids))
cursor.execute(f"""
UPDATE {self.temp_parsed_table}
SET processing_status = 'duplicate', error_msg = '数据已存在', update_time = NOW()
WHERE id IN ({placeholders})
""", duplicate_ids)
self.connection.commit()
except Exception as e:
logger.error(f"更新状态失败: {e}")
self.connection.rollback()
def get_statistics(self) -> Dict[str, Any]:
"""获取处理统计"""
try:
with self.connection.cursor() as cursor:
# 中间表统计
sql_parsed = f"""
SELECT
COUNT(*) as total,
SUM(CASE WHEN processing_status = 'linked' THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN processing_status = 'imported' THEN 1 ELSE 0 END) as imported,
SUM(CASE WHEN processing_status = 'duplicate' THEN 1 ELSE 0 END) as duplicate,
SUM(CASE WHEN processing_status = 'import_error' THEN 1 ELSE 0 END) as error
FROM {self.temp_parsed_table}
"""
cursor.execute(sql_parsed)
parsed_stats = cursor.fetchone()
# 正式表统计
sql_production = f"""
SELECT COUNT(*) as total_production
FROM {self.production_table}
WHERE deleted = 0
"""
cursor.execute(sql_production)
production_stats = cursor.fetchone()
return {
'parsed_total': parsed_stats[0] or 0,
'parsed_pending': parsed_stats[1] or 0,
'parsed_imported': parsed_stats[2] or 0,
'parsed_duplicate': parsed_stats[3] or 0,
'parsed_error': parsed_stats[4] or 0,
'production_total': production_stats[0] or 0
}
except Exception as e:
logger.error(f"获取统计信息失败: {e}")
return {}
def run(self, reprocess: bool = False) -> bool:
"""执行导入流程(简化版本)"""
start_time = time.time()
try:
self.connect_db()
# 显示初始统计
initial_stats = self.get_statistics()
if reprocess:
logger.info("重新处理模式:将重新处理所有已关联的数据")
else:
logger.info("正常处理模式:仅处理待导入的数据")
logger.info("正式表导入处理开始")
logger.info(f"当前状态: 中间表总计{initial_stats.get('parsed_total', 0)}条记录,")
logger.info(f" 待导入: {initial_stats.get('parsed_pending', 0)}")
logger.info(f" 已导入: {initial_stats.get('parsed_imported', 0)}")
logger.info(f" 重复数据: {initial_stats.get('parsed_duplicate', 0)}")
logger.info(f" 导入错误: {initial_stats.get('parsed_error', 0)}")
logger.info(f"正式表当前总记录数: {initial_stats.get('production_total', 0)}")
# 获取所有待处理的记录ID
record_ids = self.get_pending_ids(reprocess)
if not record_ids:
logger.info("没有待处理的记录")
return True
logger.info(f"开始直接导入 {len(record_ids)} 条记录...")
# 直接导入所有数据
result = self.import_data_directly(record_ids)
# 更新状态
self.update_status_after_import(record_ids, result['imported'], result['duplicates'])
# 显示最终统计
final_stats = self.get_statistics()
logger.info("="*50)
logger.info("正式表导入统计结果:")
logger.info(f" 处理总记录: {len(record_ids)}")
logger.info(f" 成功导入: {result['imported']}")
logger.info(f" 重复跳过: {result['duplicates']}")
logger.info(f" 导入失败: {result['errors']}")
logger.info(f" 最终状态:")
logger.info(f" - 中间表已导入: {final_stats.get('parsed_imported', 0)}")
logger.info(f" - 中间表重复: {final_stats.get('parsed_duplicate', 0)}")
logger.info(f" - 中间表错误: {final_stats.get('parsed_error', 0)}")
logger.info(f" - 正式表总记录: {final_stats.get('production_total', 0)}")
logger.info(f"导入总耗时: {elapsed_time:.1f}秒")
logger.info("="*50)
success = result['errors'] == 0
if not success:
logger.warning(f"有{result['errors']}条记录导入失败,请检查日志")
return success
except Exception as e:
logger.error(f"导入过程出现错误: {e}")
return False
finally:
self.disconnect_db()
elapsed_time = time.time() - start_time
logger.info(f"导入总耗时: {elapsed_time:.1f}秒")
def main():
"""主函数"""
parser = argparse.ArgumentParser(description='学生操行分历史数据 - 正式导入处理')
parser.add_argument('--reprocess', action='store_true',
help='重新处理已导入的数据(忽略processing_status状态)')
args = parser.parse_args()
logger.info("="*60)
logger.info("学生操行分历史数据迁移 - 脚本4:正式导入处理")
if args.reprocess:
logger.info("模式: 重新处理所有已关联的数据")
else:
logger.info("模式: 仅处理待导入的数据")
logger.info("="*60)
# 创建logs目录
import os
os.makedirs("logs", exist_ok=True)
# 执行导入
importer = ProductionImporter()
success = importer.run(reprocess=args.reprocess)
if success:
logger.info("✅ 正式表导入处理完成")
sys.exit(0)
else:
logger.error("❌ 正式表导入处理失败")
sys.exit(1)
if __name__ == "__main__":
main()