04_import_to_production.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. 学生操行分历史数据迁移 - 脚本4:正式导入处理(简化版本)
  5. 功能说明:
  6. - 使用高效的INSERT INTO SELECT语句直接导入数据
  7. - 自动生成雪花ID作为主键
  8. - 支持重新处理模式:重新导入所有已关联的数据
  9. - 简洁高效,避免复杂的重复检查和批量处理逻辑
  10. 执行方式:
  11. python 04_import_to_production.py # 正常模式:仅处理待导入数据
  12. python 04_import_to_production.py --reprocess # 重新处理模式:重新处理所有已关联数据
  13. 注意事项:
  14. - 依赖脚本3已执行完成
  15. - 使用数据库的INSERT IGNORE处理重复数据
  16. - 性能大幅提升,代码大幅简化
  17. """
  18. from __future__ import annotations
  19. import argparse
  20. import logging
  21. import sys
  22. import time
  23. from datetime import datetime
  24. from typing import Any, Dict, List
  25. import pymysql
  26. from db_config import DB_CONFIG, TABLE_CONFIG, CONSTANTS
  27. # 配置日志
  28. logging.basicConfig(
  29. level=logging.INFO,
  30. format="%(asctime)s %(levelname)s %(message)s",
  31. handlers=[
  32. logging.StreamHandler(sys.stdout),
  33. logging.FileHandler("logs/04_import_production.log", mode='a', encoding='utf-8')
  34. ]
  35. )
  36. logger = logging.getLogger("import_production")
  37. class ProductionImporter:
  38. """正式表导入处理器(简化版本)"""
  39. def __init__(self):
  40. self.db_config = DB_CONFIG
  41. self.temp_parsed_table = TABLE_CONFIG["temp_parsed"]
  42. self.production_table = TABLE_CONFIG["production"]
  43. self.tenant_id = CONSTANTS["DEFAULT_TENANT_ID"]
  44. self.create_by = CONSTANTS["DEFAULT_CREATE_BY"]
  45. self.update_by = CONSTANTS["DEFAULT_UPDATE_BY"]
  46. self.connection = None
  47. self.sequence_counter = 0 # 雪花ID序列号计数器
  48. self.last_timestamp = -1 # 上次时间戳
  49. def connect_db(self) -> None:
  50. """连接数据库"""
  51. try:
  52. self.connection = pymysql.connect(**self.db_config)
  53. logger.info("数据库连接成功")
  54. except Exception as e:
  55. logger.error(f"数据库连接失败: {e}")
  56. raise
  57. def disconnect_db(self) -> None:
  58. """断开数据库连接"""
  59. if self.connection:
  60. self.connection.close()
  61. logger.info("数据库连接已断开")
  62. def generate_snowflake_id(self) -> int:
  63. """生成雪花ID"""
  64. epoch = CONSTANTS["SNOWFLAKE_EPOCH"]
  65. worker_id = CONSTANTS["WORKER_ID"]
  66. datacenter_id = CONSTANTS["DATACENTER_ID"]
  67. timestamp = int(time.time() * 1000)
  68. if timestamp == self.last_timestamp:
  69. self.sequence_counter = (self.sequence_counter + 1) % 4096
  70. else:
  71. self.sequence_counter = 0
  72. self.last_timestamp = timestamp
  73. timestamp_part = (timestamp - epoch) << 22
  74. datacenter_part = datacenter_id << 17
  75. worker_part = worker_id << 12
  76. sequence_part = self.sequence_counter
  77. return timestamp_part | datacenter_part | worker_part | sequence_part
  78. def get_pending_ids(self, reprocess: bool = False) -> List[int]:
  79. """获取待处理的记录ID"""
  80. try:
  81. with self.connection.cursor() as cursor:
  82. if reprocess:
  83. # 重新处理模式:获取所有已关联的数据
  84. sql = f"""
  85. SELECT id FROM {self.temp_parsed_table}
  86. WHERE processing_status IN ('linked', 'imported', 'duplicate')
  87. AND student_id IS NOT NULL
  88. """
  89. else:
  90. # 正常模式:只获取待导入的数据
  91. sql = f"""
  92. SELECT id FROM {self.temp_parsed_table}
  93. WHERE processing_status = 'linked'
  94. AND student_id IS NOT NULL
  95. """
  96. cursor.execute(sql)
  97. return [row[0] for row in cursor.fetchall()]
  98. except Exception as e:
  99. logger.error(f"获取待处理记录ID失败: {e}")
  100. return []
  101. def import_data_directly(self, record_ids: List[int]) -> Dict[str, int]:
  102. """使用INSERT INTO SELECT直接导入数据"""
  103. if not record_ids:
  104. return {'imported': 0, 'duplicates': 0, 'errors': 0}
  105. try:
  106. with self.connection.cursor() as cursor:
  107. # 先预生成一批雪花ID
  108. snowflake_ids = []
  109. for _ in range(len(record_ids)):
  110. snowflake_ids.append(self.generate_snowflake_id())
  111. current_time = datetime.now()
  112. # 创建临时表存储ID映射
  113. temp_id_table = "temp_snowflake_ids"
  114. cursor.execute(f"DROP TEMPORARY TABLE IF EXISTS {temp_id_table}")
  115. cursor.execute(f"""
  116. CREATE TEMPORARY TABLE {temp_id_table} (
  117. record_id BIGINT PRIMARY KEY,
  118. snowflake_id BIGINT UNIQUE
  119. )
  120. """)
  121. # 批量插入ID映射
  122. id_mapping = list(zip(record_ids, snowflake_ids))
  123. cursor.executemany(f"""
  124. INSERT INTO {temp_id_table} (record_id, snowflake_id) VALUES (%s, %s)
  125. """, id_mapping)
  126. # 使用INSERT IGNORE直接导入数据
  127. insert_sql = f"""
  128. INSERT IGNORE INTO {self.production_table} (
  129. id, tenant_id, create_dept, create_by, create_time,
  130. update_by, update_time, deleted,
  131. academic_year, semester, month,
  132. student_id, class_id, class_teacher_id,
  133. score, remark
  134. )
  135. SELECT
  136. t.snowflake_id,
  137. '{self.tenant_id}',
  138. NULL,
  139. {self.create_by},
  140. '{current_time.strftime('%Y-%m-%d %H:%M:%S')}',
  141. {self.update_by},
  142. '{current_time.strftime('%Y-%m-%d %H:%M:%S')}',
  143. 0,
  144. p.academic_year, p.semester, p.month,
  145. p.student_id, p.class_id, p.class_teacher_id,
  146. p.score, COALESCE(p.remark, '')
  147. FROM {self.temp_parsed_table} p
  148. INNER JOIN {temp_id_table} t ON p.id = t.record_id
  149. WHERE p.class_teacher_id IS NOT NULL
  150. """
  151. cursor.execute(insert_sql)
  152. imported_count = cursor.rowcount
  153. # 计算重复和错误数量
  154. total_processed = len(record_ids)
  155. duplicates = total_processed - imported_count
  156. errors = 0 # INSERT IGNORE不会产生错误,只有重复
  157. self.connection.commit()
  158. # 清理临时表
  159. cursor.execute(f"DROP TEMPORARY TABLE {temp_id_table}")
  160. return {
  161. 'imported': imported_count,
  162. 'duplicates': duplicates,
  163. 'errors': errors
  164. }
  165. except Exception as e:
  166. logger.error(f"直接导入数据失败: {e}")
  167. self.connection.rollback()
  168. return {'imported': 0, 'duplicates': 0, 'errors': len(record_ids)}
  169. def update_status_after_import(self, record_ids: List[int], imported_count: int, duplicate_count: int) -> None:
  170. """根据导入结果更新状态"""
  171. try:
  172. with self.connection.cursor() as cursor:
  173. # 更新已导入的记录状态
  174. if imported_count > 0:
  175. imported_ids = record_ids[:imported_count]
  176. placeholders = ','.join(['%s'] * len(imported_ids))
  177. cursor.execute(f"""
  178. UPDATE {self.temp_parsed_table}
  179. SET processing_status = 'imported', update_time = NOW()
  180. WHERE id IN ({placeholders})
  181. """, imported_ids)
  182. # 更新重复的记录状态
  183. if duplicate_count > 0:
  184. duplicate_ids = record_ids[imported_count:imported_count + duplicate_count]
  185. placeholders = ','.join(['%s'] * len(duplicate_ids))
  186. cursor.execute(f"""
  187. UPDATE {self.temp_parsed_table}
  188. SET processing_status = 'duplicate', error_msg = '数据已存在', update_time = NOW()
  189. WHERE id IN ({placeholders})
  190. """, duplicate_ids)
  191. self.connection.commit()
  192. except Exception as e:
  193. logger.error(f"更新状态失败: {e}")
  194. self.connection.rollback()
  195. def get_statistics(self) -> Dict[str, Any]:
  196. """获取处理统计"""
  197. try:
  198. with self.connection.cursor() as cursor:
  199. # 中间表统计
  200. sql_parsed = f"""
  201. SELECT
  202. COUNT(*) as total,
  203. SUM(CASE WHEN processing_status = 'linked' THEN 1 ELSE 0 END) as pending,
  204. SUM(CASE WHEN processing_status = 'imported' THEN 1 ELSE 0 END) as imported,
  205. SUM(CASE WHEN processing_status = 'duplicate' THEN 1 ELSE 0 END) as duplicate,
  206. SUM(CASE WHEN processing_status = 'import_error' THEN 1 ELSE 0 END) as error
  207. FROM {self.temp_parsed_table}
  208. """
  209. cursor.execute(sql_parsed)
  210. parsed_stats = cursor.fetchone()
  211. # 正式表统计
  212. sql_production = f"""
  213. SELECT COUNT(*) as total_production
  214. FROM {self.production_table}
  215. WHERE deleted = 0
  216. """
  217. cursor.execute(sql_production)
  218. production_stats = cursor.fetchone()
  219. return {
  220. 'parsed_total': parsed_stats[0] or 0,
  221. 'parsed_pending': parsed_stats[1] or 0,
  222. 'parsed_imported': parsed_stats[2] or 0,
  223. 'parsed_duplicate': parsed_stats[3] or 0,
  224. 'parsed_error': parsed_stats[4] or 0,
  225. 'production_total': production_stats[0] or 0
  226. }
  227. except Exception as e:
  228. logger.error(f"获取统计信息失败: {e}")
  229. return {}
  230. def run(self, reprocess: bool = False) -> bool:
  231. """执行导入流程(简化版本)"""
  232. start_time = time.time()
  233. try:
  234. self.connect_db()
  235. # 显示初始统计
  236. initial_stats = self.get_statistics()
  237. if reprocess:
  238. logger.info("重新处理模式:将重新处理所有已关联的数据")
  239. else:
  240. logger.info("正常处理模式:仅处理待导入的数据")
  241. logger.info("正式表导入处理开始")
  242. logger.info(f"当前状态: 中间表总计{initial_stats.get('parsed_total', 0)}条记录,")
  243. logger.info(f" 待导入: {initial_stats.get('parsed_pending', 0)}")
  244. logger.info(f" 已导入: {initial_stats.get('parsed_imported', 0)}")
  245. logger.info(f" 重复数据: {initial_stats.get('parsed_duplicate', 0)}")
  246. logger.info(f" 导入错误: {initial_stats.get('parsed_error', 0)}")
  247. logger.info(f"正式表当前总记录数: {initial_stats.get('production_total', 0)}")
  248. # 获取所有待处理的记录ID
  249. record_ids = self.get_pending_ids(reprocess)
  250. if not record_ids:
  251. logger.info("没有待处理的记录")
  252. return True
  253. logger.info(f"开始直接导入 {len(record_ids)} 条记录...")
  254. # 直接导入所有数据
  255. result = self.import_data_directly(record_ids)
  256. # 更新状态
  257. self.update_status_after_import(record_ids, result['imported'], result['duplicates'])
  258. # 显示最终统计
  259. final_stats = self.get_statistics()
  260. logger.info("="*50)
  261. logger.info("正式表导入统计结果:")
  262. logger.info(f" 处理总记录: {len(record_ids)}")
  263. logger.info(f" 成功导入: {result['imported']}")
  264. logger.info(f" 重复跳过: {result['duplicates']}")
  265. logger.info(f" 导入失败: {result['errors']}")
  266. logger.info(f" 最终状态:")
  267. logger.info(f" - 中间表已导入: {final_stats.get('parsed_imported', 0)}")
  268. logger.info(f" - 中间表重复: {final_stats.get('parsed_duplicate', 0)}")
  269. logger.info(f" - 中间表错误: {final_stats.get('parsed_error', 0)}")
  270. logger.info(f" - 正式表总记录: {final_stats.get('production_total', 0)}")
  271. logger.info(f"导入总耗时: {elapsed_time:.1f}秒")
  272. logger.info("="*50)
  273. success = result['errors'] == 0
  274. if not success:
  275. logger.warning(f"有{result['errors']}条记录导入失败,请检查日志")
  276. return success
  277. except Exception as e:
  278. logger.error(f"导入过程出现错误: {e}")
  279. return False
  280. finally:
  281. self.disconnect_db()
  282. elapsed_time = time.time() - start_time
  283. logger.info(f"导入总耗时: {elapsed_time:.1f}秒")
  284. def main():
  285. """主函数"""
  286. parser = argparse.ArgumentParser(description='学生操行分历史数据 - 正式导入处理')
  287. parser.add_argument('--reprocess', action='store_true',
  288. help='重新处理已导入的数据(忽略processing_status状态)')
  289. args = parser.parse_args()
  290. logger.info("="*60)
  291. logger.info("学生操行分历史数据迁移 - 脚本4:正式导入处理")
  292. if args.reprocess:
  293. logger.info("模式: 重新处理所有已关联的数据")
  294. else:
  295. logger.info("模式: 仅处理待导入的数据")
  296. logger.info("="*60)
  297. # 创建logs目录
  298. import os
  299. os.makedirs("logs", exist_ok=True)
  300. # 执行导入
  301. importer = ProductionImporter()
  302. success = importer.run(reprocess=args.reprocess)
  303. if success:
  304. logger.info("✅ 正式表导入处理完成")
  305. sys.exit(0)
  306. else:
  307. logger.error("❌ 正式表导入处理失败")
  308. sys.exit(1)
  309. if __name__ == "__main__":
  310. main()