372 lines
14 KiB
Python
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()
|