import_conduct_score_history.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. 学生操行分历史数据迁移 - 主控制脚本
  5. 功能说明:
  6. - 协调整个迁移流程的执行
  7. - 按顺序执行所有迁移脚本
  8. - 记录整体迁移过程和结果
  9. - 支持断点续传(可从指定步骤开始)
  10. 执行方式:
  11. python import_conduct_score_history.py [--start-from STEP] [--validate-only]
  12. 参数说明:
  13. - --start-from: 从指定步骤开始执行 (1-5)
  14. - --validate-only: 仅执行验证,不执行迁移
  15. 迁移步骤:
  16. 1. Excel导入临时表 (01_import_excel_to_temp.py)
  17. 2. 数据解析处理 (02_parse_score_data.py)
  18. 3. 数据关联处理 (03_link_student_data.py)
  19. 4. 正式导入处理 (04_import_to_production.py)
  20. 5. 验证与清理 (05_validate_and_cleanup.py)
  21. """
  22. from __future__ import annotations
  23. import argparse
  24. import logging
  25. import subprocess
  26. import sys
  27. import time
  28. from datetime import datetime
  29. from pathlib import Path
  30. from typing import Any, Dict, List, Optional
  31. # 配置日志
  32. logging.basicConfig(
  33. level=logging.INFO,
  34. format="%(asctime)s %(levelname)s %(message)s",
  35. handlers=[
  36. logging.StreamHandler(sys.stdout),
  37. logging.FileHandler("logs/migration_main.log", mode='a', encoding='utf-8')
  38. ]
  39. )
  40. logger = logging.getLogger("migration_main")
  41. class MigrationController:
  42. """迁移控制器"""
  43. def __init__(self, start_from: int = 1, validate_only: bool = False):
  44. self.start_from = start_from
  45. self.validate_only = validate_only
  46. self.steps = [
  47. {
  48. 'name': 'Excel导入临时表',
  49. 'script': '01_import_excel_to_temp.py',
  50. 'description': '读取操行.xls文件,导入到temp_conduct_score_import表'
  51. },
  52. {
  53. 'name': '数据解析处理',
  54. 'script': '02_parse_score_data.py',
  55. 'description': '解析学年、学期、月度分数,生成月度记录'
  56. },
  57. {
  58. 'name': '数据关联处理',
  59. 'script': '03_link_student_data.py',
  60. 'description': '通过学号关联学生、班级、班主任信息'
  61. },
  62. {
  63. 'name': '正式导入处理',
  64. 'script': '04_import_to_production.py',
  65. 'description': '将验证后的数据导入到xg_conduct_score表'
  66. },
  67. {
  68. 'name': '验证与清理',
  69. 'script': '05_validate_and_cleanup.py',
  70. 'description': '验证迁移结果,生成报告'
  71. }
  72. ]
  73. def check_prerequisites(self) -> bool:
  74. """检查前置条件"""
  75. logger.info("检查前置条件...")
  76. # 检查Python脚本是否存在
  77. for step in self.steps:
  78. script_path = step['script']
  79. if not Path(script_path).exists():
  80. logger.error(f"脚本文件不存在: {script_path}")
  81. return False
  82. # 检查Excel文件是否存在(仅在执行第一步时检查)
  83. if self.start_from == 1 and not self.validate_only:
  84. excel_file = "操行.xls"
  85. if not Path(excel_file).exists():
  86. logger.error(f"Excel文件不存在: {excel_file}")
  87. return False
  88. # 检查logs目录
  89. Path("logs").mkdir(exist_ok=True)
  90. logger.info("前置条件检查通过")
  91. return True
  92. def run_step(self, step_info: Dict[str, Any]) -> tuple[bool, str]:
  93. """执行单个步骤"""
  94. step_name = step_info['name']
  95. script = step_info['script']
  96. description = step_info['description']
  97. logger.info(f"开始执行步骤: {step_name}")
  98. logger.info(f"脚本: {script}")
  99. logger.info(f"说明: {description}")
  100. start_time = time.time()
  101. try:
  102. # 构建命令
  103. cmd = [sys.executable, script]
  104. # 如果是验证步骤,添加--cleanup参数(由用户决定是否清理)
  105. # 这里默认不清理,让用户手动决定
  106. # 执行脚本
  107. result = subprocess.run(
  108. cmd,
  109. capture_output=True,
  110. text=True,
  111. encoding='utf-8',
  112. timeout=3600 # 1小时超时
  113. )
  114. elapsed_time = time.time() - start_time
  115. # 记录执行结果
  116. if result.returncode == 0:
  117. logger.info(f"步骤耗时: {step_elapsed:.1f}秒")
  118. return True, result.stdout + result.stderr
  119. else:
  120. logger.error(f"步骤耗时: {step_elapsed:.1f}秒")
  121. logger.error("标准输出:")
  122. logger.error(result.stdout)
  123. logger.error("标准错误:")
  124. logger.error(result.stderr)
  125. return False, result.stdout + result.stderr
  126. except subprocess.TimeoutExpired:
  127. elapsed_time = time.time() - start_time
  128. logger.error(f"步骤执行超时: {elapsed_time:.1f}秒")
  129. return False, "执行超时"
  130. except Exception as e:
  131. elapsed_time = time.time() - start_time
  132. logger.error(f"步骤执行出错: {elapsed_time:.1f}秒")
  133. return False, str(e)
  134. def run_migration(self) -> bool:
  135. """执行迁移流程"""
  136. logger.info("="*80)
  137. logger.info("学生操行分历史数据迁移开始")
  138. logger.info("="*80)
  139. logger.info(f"开始步骤: {self.start_from}")
  140. logger.info(f"验证模式: {self.validate_only}")
  141. logger.info(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  142. logger.info("")
  143. # 显示执行计划
  144. logger.info("执行计划:")
  145. for i, step in enumerate(self.steps, 1):
  146. status = "⏭️ 跳过" if i < self.start_from else "▶️ 执行"
  147. if self.validate_only and i < 5:
  148. status = "⏭️ 跳过"
  149. logger.info(f" {i}. {status} {step['name']}")
  150. logger.info("")
  151. total_start_time = time.time()
  152. results = []
  153. # 执行步骤
  154. for i, step in enumerate(self.steps, 1):
  155. step_start_time = time.time()
  156. # 检查是否需要执行此步骤
  157. if i < self.start_from:
  158. logger.info(f"跳过步骤 {i}: {step['name']}")
  159. continue
  160. if self.validate_only and i < 5:
  161. logger.info(f"验证模式跳过步骤 {i}: {step['name']}")
  162. continue
  163. # 执行步骤
  164. success, output = self.run_step(step)
  165. step_elapsed = time.time() - step_start_time
  166. results.append({
  167. 'step': i,
  168. 'name': step['name'],
  169. 'success': success,
  170. 'duration': step_elapsed,
  171. 'output': output
  172. })
  173. # 如果步骤失败,停止执行
  174. if not success:
  175. logger.error(f"步骤 {i} 执行失败,停止迁移流程")
  176. break
  177. logger.info("")
  178. # 输出总结报告
  179. total_elapsed = time.time() - total_start_time
  180. self.print_summary_report(results, total_elapsed)
  181. # 检查整体结果
  182. all_success = all(result['success'] for result in results)
  183. return all_success
  184. def print_summary_report(self, results: List[Dict[str, Any]], total_time: float) -> None:
  185. """打印总结报告"""
  186. logger.info("="*80)
  187. logger.info("迁移执行总结报告")
  188. logger.info("="*80)
  189. logger.info("各步骤执行结果:")
  190. for result in results:
  191. status = "✅ 成功" if result['success'] else "❌ 失败"
  192. duration = ".1f"
  193. logger.info("6")
  194. logger.info("")
  195. logger.info(f"迁移总耗时: {total_time:.1f}秒")
  196. # 统计信息
  197. successful_steps = sum(1 for r in results if r['success'])
  198. total_steps = len(results)
  199. logger.info(f"成功步骤: {successful_steps}/{total_steps}")
  200. if successful_steps == total_steps:
  201. logger.info("🎉 迁移流程完全成功!")
  202. else:
  203. logger.info("⚠️ 迁移流程部分失败,请检查上述错误信息")
  204. logger.info("="*80)
  205. def main():
  206. """主函数"""
  207. parser = argparse.ArgumentParser(description='学生操行分历史数据迁移 - 主控制脚本')
  208. parser.add_argument('--start-from', type=int, choices=range(1, 6), default=1,
  209. help='从指定步骤开始执行 (1-5)')
  210. parser.add_argument('--validate-only', action='store_true',
  211. help='仅执行验证步骤(步骤5),跳过前面的迁移步骤')
  212. args = parser.parse_args()
  213. # 如果是验证模式,从步骤5开始
  214. if args.validate_only:
  215. args.start_from = 5
  216. # 创建控制器并执行
  217. controller = MigrationController(
  218. start_from=args.start_from,
  219. validate_only=args.validate_only
  220. )
  221. # 检查前置条件
  222. if not controller.check_prerequisites():
  223. logger.error("前置条件检查失败,请解决上述问题后重试")
  224. sys.exit(1)
  225. # 执行迁移
  226. success = controller.run_migration()
  227. if success:
  228. logger.info("✅ 学生操行分历史数据迁移完成")
  229. sys.exit(0)
  230. else:
  231. logger.error("❌ 学生操行分历史数据迁移失败")
  232. sys.exit(1)
  233. if __name__ == "__main__":
  234. main()