#!/usr/bin/env python # -*- coding: utf-8 -*- """ 学生操行分历史数据迁移 - 脚本3:数据关联处理 功能说明: - 根据学号关联xg_student_msg表,获取学生ID - 根据班级ID获取班主任信息 - 更新temp_conduct_score_parsed表中的关联字段 执行方式: python 03_link_student_data.py 注意事项: - 依赖脚本2已执行完成 - 学号不存在的学生记录会标记为错误状态 """ from __future__ import annotations import argparse import logging import sys import time from datetime import datetime from typing import Any, Dict, List, Optional, Tuple import pymysql from pymysql.cursors import DictCursor 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/03_link_data.log", mode='a', encoding='utf-8') ] ) logger = logging.getLogger("link_data") class DataLinker: """数据关联处理器""" def __init__(self): self.db_config = DB_CONFIG self.temp_parsed_table = TABLE_CONFIG["temp_parsed"] self.students_table = TABLE_CONFIG["students"] self.classes_table = TABLE_CONFIG["classes"] self.batch_size = CONSTANTS["BATCH_SIZE"] self.connection = None 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 get_pending_data(self) -> List[Dict[str, Any]]: """获取待关联的数据""" try: with self.connection.cursor(DictCursor) as cursor: sql = f""" SELECT id, temp_id, student_no, academic_year, semester, month, score FROM {self.temp_parsed_table} WHERE processing_status = 'parsed' ORDER BY id LIMIT {self.batch_size} """ cursor.execute(sql) return cursor.fetchall() except Exception as e: logger.error(f"获取待关联数据失败: {e}") return [] def link_student_info(self, student_nos: List[str]) -> Dict[str, Dict[str, Any]]: """批量关联学生信息""" if not student_nos: return {} try: with self.connection.cursor(DictCursor) as cursor: # 构建IN查询 placeholders = ','.join(['%s'] * len(student_nos)) sql = f""" SELECT s.id as student_id, s.student_no, s.class_id, c.class_teacher_id FROM {self.students_table} s LEFT JOIN {self.classes_table} c ON s.class_id = c.id WHERE s.student_no IN ({placeholders}) AND s.deleted = 0 AND (c.id IS NULL OR c.deleted = 0) """ cursor.execute(sql, student_nos) results = cursor.fetchall() # 转换为字典格式 student_info = {} for row in results: student_info[row['student_no']] = { 'student_id': row['student_id'], 'class_id': row['class_id'], 'class_teacher_id': row['class_teacher_id'] } return student_info except Exception as e: logger.error(f"关联学生信息失败: {e}") return {} def update_parsed_data(self, update_data: List[Dict[str, Any]]) -> tuple[int, int]: """更新解析表中的关联信息""" if not update_data: return 0, 0 try: with self.connection.cursor() as cursor: # 分别处理成功和失败的数据 success_updates = [] error_updates = [] for item in update_data: if item.get('student_id'): success_updates.append(item) else: error_updates.append(item) # 更新成功的记录 success_count = 0 if success_updates: sql = f""" UPDATE {self.temp_parsed_table} SET student_id = %s, class_id = %s, class_teacher_id = %s, processing_status = 'linked', update_time = NOW() WHERE id = %s """ values = [(item['student_id'], item['class_id'], item['class_teacher_id'], item['id']) for item in success_updates] cursor.executemany(sql, values) success_count = len(success_updates) # 更新失败的记录 error_count = 0 if error_updates: sql = f""" UPDATE {self.temp_parsed_table} SET processing_status = 'error', error_msg = %s, update_time = NOW() WHERE id = %s """ values = [("学号不存在或学生已被删除", item['id']) for item in error_updates] cursor.executemany(sql, values) error_count = len(error_updates) self.connection.commit() return success_count, error_count except Exception as e: logger.error(f"更新关联数据失败: {e}") self.connection.rollback() return 0, len(update_data) def process_batch(self, batch_data: List[Dict[str, Any]]) -> Dict[str, Any]: """处理一批数据""" if not batch_data: return {'processed': 0, 'linked': 0, 'not_found': 0} # 提取学号列表 student_nos = list(set(row['student_no'] for row in batch_data)) # 关联学生信息 student_info = self.link_student_info(student_nos) # 构建更新数据 update_data = [] not_found_nos = set() for row in batch_data: student_no = row['student_no'] info = student_info.get(student_no) if info: update_data.append({ 'id': row['id'], 'student_id': info['student_id'], 'class_id': info['class_id'], 'class_teacher_id': info['class_teacher_id'] }) else: update_data.append({ 'id': row['id'], 'student_id': None, 'class_id': None, 'class_teacher_id': None }) not_found_nos.add(student_no) # 输出未找到的学号 if not_found_nos: logger.warning(f"未找到以下学号的学生信息 ({len(not_found_nos)}个):") for student_no in sorted(list(not_found_nos)[:10]): # 只显示前10个 logger.warning(f" {student_no}") if len(not_found_nos) > 10: logger.warning(f" ... 还有{len(not_found_nos) - 10}个") # 更新数据库 linked, not_linked = self.update_parsed_data(update_data) return { 'processed': len(batch_data), 'linked': linked, 'not_found': not_linked, 'unique_students': len(student_nos), 'found_students': len(student_info) } def get_statistics(self) -> Dict[str, Any]: """获取处理统计""" try: with self.connection.cursor(DictCursor) as cursor: sql = f""" SELECT COUNT(*) as total, SUM(CASE WHEN processing_status = 'parsed' THEN 1 ELSE 0 END) as pending, SUM(CASE WHEN processing_status = 'linked' THEN 1 ELSE 0 END) as linked, SUM(CASE WHEN processing_status = 'error' THEN 1 ELSE 0 END) as error FROM {self.temp_parsed_table} """ cursor.execute(sql) result = cursor.fetchone() return { 'total_records': result['total'] or 0, 'pending': result['pending'] or 0, 'linked': result['linked'] or 0, 'error': result['error'] or 0 } except Exception as e: logger.error(f"获取统计信息失败: {e}") return {'total_records': 0, 'pending': 0, 'linked': 0, 'error': 0} def link_student_data_efficient(self) -> Tuple[int, int]: """高效的关联学生数据 - 使用单次UPDATE JOIN操作""" try: with self.connection.cursor() as cursor: # 使用UPDATE JOIN一次性关联所有数据 sql = f""" UPDATE {self.temp_parsed_table} t LEFT JOIN {self.students_table} s ON t.student_no = s.student_no AND s.deleted = 0 LEFT JOIN {self.classes_table} c ON s.class_id = c.id AND c.deleted = 0 SET t.student_id = s.id, t.class_id = s.class_id, t.class_teacher_id = c.class_teacher_id, t.processing_status = CASE WHEN s.id IS NOT NULL THEN 'linked' ELSE 'error' END, t.error_msg = CASE WHEN s.id IS NULL THEN '学号不存在或学生已被删除' ELSE NULL END, t.update_time = NOW() WHERE t.processing_status = 'parsed' """ affected_rows = cursor.execute(sql) self.connection.commit() # 统计成功和失败的数量 success_count = 0 error_count = 0 if affected_rows > 0: # 获取更新后的统计 with self.connection.cursor() as count_cursor: count_sql = f""" SELECT SUM(CASE WHEN processing_status = 'linked' THEN 1 ELSE 0 END) as linked, SUM(CASE WHEN processing_status = 'error' THEN 1 ELSE 0 END) as error FROM {self.temp_parsed_table} WHERE processing_status IN ('linked', 'error') """ count_cursor.execute(count_sql) result = count_cursor.fetchone() success_count = result[0] or 0 error_count = result[1] or 0 logger.info(f"UPDATE JOIN操作完成,影响行数: {affected_rows}") return success_count, error_count except Exception as e: logger.error(f"高效关联操作失败: {e}") self.connection.rollback() return 0, 0 def run(self) -> bool: """执行关联流程""" start_time = time.time() try: self.connect_db() # 显示初始统计 initial_stats = self.get_statistics() logger.info("数据关联处理开始") logger.info(f"当前状态: 总计{initial_stats['total_records']}条记录," f"待关联{initial_stats['pending']}条,已关联{initial_stats['linked']}条," f"错误{initial_stats['error']}条") # 使用高效的UPDATE JOIN方式处理所有数据 logger.info("开始执行高效关联操作...") linked, error = self.link_student_data_efficient() # 显示最终统计 final_stats = self.get_statistics() logger.info("="*50) logger.info("数据关联统计结果:") logger.info(f" 关联成功: {linked}") logger.info(f" 学号未找到: {error}") logger.info(f" 最终状态:") logger.info(f" - 已关联: {final_stats['linked']}") logger.info(f" - 关联失败: {final_stats['error']}") logger.info("="*50) # 如果有未找到的学号,输出详细信息 if error > 0: logger.warning(f"共有{error}条记录的学号未找到对应学生信息") logger.warning("这些记录将无法导入到正式表,请检查学生数据") return error == 0 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='学生操行分历史数据 - 数据关联处理') logger.info("="*60) logger.info("学生操行分历史数据迁移 - 脚本3:数据关联处理") logger.info("="*60) # 创建logs目录 import os os.makedirs("logs", exist_ok=True) # 执行关联 linker = DataLinker() success = linker.run() if success: logger.info("✅ 数据关联处理完成") sys.exit(0) else: logger.warning("⚠️ 数据关联处理完成,但存在未关联的记录") sys.exit(0) # 不算错误,只是警告 if __name__ == "__main__": main()