| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- #!/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()
|