""" 任务二调度器模块 负责定时执行TAPD状态同步任务 功能: 1. 按配置频率定时执行 2. 优雅退出(Ctrl+C) 3. 运行统计 """ import sys import time import signal import argparse from pathlib import Path from datetime import datetime # 将项目根目录添加到 Python 路径 project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root)) # 导入schedule库 try: import schedule except ImportError: print("错误: 缺少schedule库") print("请运行: pip install schedule") sys.exit(1) from src.token_manager import TokenManager from src2.config import Task2ConfigManager from src2.sync_service import run_once class Task2Scheduler: """任务二调度器""" def __init__(self, config_path=None, verbose=False): """ 初始化调度器 Args: config_path: 配置文件路径 verbose: 是否显示详细信息 """ self.config_path = config_path self.verbose = verbose self.running = True # 统计信息 self.stats = { 'start_time': None, 'total_runs': 0, 'success_runs': 0, 'failed_runs': 0, 'total_records_synced': 0, 'total_records_updated': 0, 'last_run_time': None } # 初始化配置管理器 self._init_config() # 初始化TokenManager self._init_token_manager() # 获取调度配置 self.sync_interval = self.config['schedule']['sync_interval'] def _init_config(self): """初始化配置""" try: print("正在加载配置文件...") self.config_manager = Task2ConfigManager(config_path=self.config_path) self.config = self.config_manager.get_all_config() print("✓ 配置文件加载成功") except Exception as e: print(f"✗ 配置文件加载失败: {e}") raise def _init_token_manager(self): """初始化TokenManager""" try: print("正在初始化TokenManager...") self.token_manager = TokenManager() print("✓ TokenManager初始化成功") except Exception as e: print(f"✗ TokenManager初始化失败: {e}") raise def job(self): """执行一次同步任务""" print("\n" + "=" * 80) print(f"开始执行同步任务 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print("=" * 80) try: # 获取access_token access_token = self.token_manager.get_token() # 执行一次同步流程 result = run_once( config_manager=self.config_manager, access_token=access_token, test_mode=self.verbose ) # 更新统计信息 self.stats['total_runs'] += 1 self.stats['last_run_time'] = datetime.now() if result['success']: self.stats['success_runs'] += 1 self.stats['total_records_synced'] += result['records_synced'] self.stats['total_records_updated'] += result['records_updated'] print("\n" + "-" * 80) print("本次执行统计:") print(f" 处理子表: {result['sheets_processed']} 个") print(f" 跳过子表: {result['sheets_skipped']} 个") print(f" 包含链接: {result['records_with_link']} 条") print(f" 同步成功: {result['records_synced']} 条") print(f" 需要更新: {result['records_updated']} 条") print(f" 同步失败: {result['records_failed']} 条") print("-" * 80) else: self.stats['failed_runs'] += 1 print("\n" + "-" * 80) print("本次执行失败:") print(f" 错误信息: {result.get('error_message', '未知错误')}") print("-" * 80) except Exception as e: self.stats['total_runs'] += 1 self.stats['failed_runs'] += 1 self.stats['last_run_time'] = datetime.now() print("\n" + "-" * 80) print("本次执行异常:") print(f" 错误类型: {type(e).__name__}") print(f" 错误信息: {e}") print("-" * 80) if self.verbose: import traceback print("\n详细错误信息:") traceback.print_exc() # 显示下次执行时间 self._show_next_run_time() def _show_next_run_time(self): """显示下次执行时间""" next_run = schedule.idle_seconds() if next_run is not None: next_run_time = datetime.now().timestamp() + next_run next_run_str = datetime.fromtimestamp(next_run_time).strftime('%Y-%m-%d %H:%M:%S') print(f"\n下次执行时间: {next_run_str} (约 {int(next_run / 60)} 分钟后)") def _startup_check(self): """启动检查""" print("\n" + "=" * 80) print("启动检查") print("=" * 80) checks_passed = True # 1. 检查配置文件 print("\n[1/3] 检查配置文件...") try: print(f" ✓ 配置文件已加载: {self.config_path or '默认路径'}") print(f" ✓ TAPD workspace_id: {self.config['tapd']['workspace_id']}") print(f" ✓ SmartSheet docid: {self.config['smartsheet']['docid'][:20]}...") print(f" ✓ 同步间隔: {self.sync_interval} 分钟") except Exception as e: print(f" ✗ 配置检查失败: {e}") checks_passed = False # 2. 检查环境变量 print("\n[2/3] 检查环境变量...") import os required_env_vars = { 'CORPID': '企业微信CorpID', 'CORPSECRET': '企业微信CorpSecret', 'TAPD_API_USER': 'TAPD API用户名', 'TAPD_API_PASSWORD': 'TAPD API密码' } for env_var, description in required_env_vars.items(): if os.environ.get(env_var): print(f" ✓ {description} ({env_var}): 已设置") else: print(f" ✗ {description} ({env_var}): 未设置") checks_passed = False # 3. 测试access_token获取 print("\n[3/3] 测试access_token获取...") try: access_token = self.token_manager.get_token() print(f" ✓ access_token获取成功: {access_token[:10]}...(已隐藏)") except Exception as e: print(f" ✗ access_token获取失败: {e}") checks_passed = False print("\n" + "=" * 80) if checks_passed: print("启动检查通过 ✓") else: print("启动检查失败 ✗") print("\n请检查上述错误并修复后重试") return False print("=" * 80) return True def start(self): """启动调度器""" print("\n" + "=" * 80) print("TAPD状态同步调度器 (任务二)") print("版本: 1.0.0 (第四阶段)") print("=" * 80) # 执行启动检查 if not self._startup_check(): print("\n调度器启动失败") sys.exit(1) # 记录启动时间 self.stats['start_time'] = datetime.now() # 注册信号处理 signal.signal(signal.SIGINT, self._signal_handler) signal.signal(signal.SIGTERM, self._signal_handler) # 配置定时任务 schedule.every(self.sync_interval).minutes.do(self.job) print(f"\n调度器已启动:") print(f" - 同步任务: 立即执行一次,然后每 {self.sync_interval} 分钟执行一次") print("按 Ctrl+C 停止调度器") print() # 立即执行一次同步任务 self.job() # 进入调度循环 while self.running: schedule.run_pending() time.sleep(1) def _signal_handler(self, signum, frame): """信号处理函数""" print("\n\n" + "=" * 80) print("收到停止信号,正在优雅退出...") print("=" * 80) self.running = False self._print_final_stats() def _print_final_stats(self): """打印最终统计信息""" print("\n" + "=" * 80) print("运行统计") print("=" * 80) if self.stats['start_time']: start_time_str = self.stats['start_time'].strftime('%Y-%m-%d %H:%M:%S') print(f"启动时间: {start_time_str}") if self.stats['last_run_time']: last_run_str = self.stats['last_run_time'].strftime('%Y-%m-%d %H:%M:%S') print(f"最后执行: {last_run_str}") if self.stats['start_time']: running_time = datetime.now() - self.stats['start_time'] hours = int(running_time.total_seconds() // 3600) minutes = int((running_time.total_seconds() % 3600) // 60) print(f"运行时长: {hours} 小时 {minutes} 分钟") print(f"\n同步任务统计:") print(f" 总执行次数: {self.stats['total_runs']}") print(f" 成功次数: {self.stats['success_runs']}") print(f" 失败次数: {self.stats['failed_runs']}") print(f" 总同步记录: {self.stats['total_records_synced']}") print(f" 总更新记录: {self.stats['total_records_updated']}") if self.stats['total_runs'] > 0: success_rate = (self.stats['success_runs'] / self.stats['total_runs']) * 100 print(f" 成功率: {success_rate:.1f}%") print("=" * 80) print("调度器已停止") print("=" * 80) def parse_arguments(): """解析命令行参数""" parser = argparse.ArgumentParser( description='TAPD状态同步调度器 - 定时执行同步任务', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 示例用法: # 使用默认配置启动调度器 python src2/scheduler.py # 指定配置文件路径 python src2/scheduler.py --config /path/to/config.ini # 显示详细信息 python src2/scheduler.py --verbose """ ) parser.add_argument( '-c', '--config', default=None, help='配置文件路径(默认: config/config_task2.ini)' ) parser.add_argument( '-v', '--verbose', action='store_true', help='显示详细输出信息' ) return parser.parse_args() def main(): """主函数""" try: # 解析命令行参数 args = parse_arguments() # 创建并启动调度器 scheduler = Task2Scheduler( config_path=args.config, verbose=args.verbose ) scheduler.start() return 0 except KeyboardInterrupt: return 0 except Exception as e: print(f"\n✗ 调度器启动失败: {e}") import traceback traceback.print_exc() return 1 if __name__ == "__main__": sys.exit(main())