maintenance.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. # -*- coding: utf-8 -*-
  2. # Author : Charley
  3. # Python : 3.12.10
  4. # Date : 2026/09/09
  5. """得卡运维常驻:账号池 cooling 巡检恢复 + 定时清理堆积的报告 Excel。
  6. 两个后台定时任务(单进程 schedule 循环):
  7. 1) 账号池 cooling 巡检:每 10 分钟把冷却(cooling)到期的账号恢复 healthy(补账号池状态机闭环,
  8. 否则软失败转 cooling 的号永不自动恢复,见审查问题4)。
  9. 2) 报告 Excel 清理:每天 03:00 删除 EXCEL_DIRS 下超过 EXCEL_KEEP_DAYS 天的 .xlsx(报告每天生成、
  10. 堆积占盘),排除 EXCEL_KEEP_FILES 里的固定产物。
  11. ⚠️ 待主公确认(2026/09/09):EXCEL_KEEP_DAYS 保留天数、EXCEL_DIRS 清理目录、EXCEL_KEEP_FILES 白名单。
  12. """
  13. import os
  14. import sys
  15. import time
  16. import glob
  17. _ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  18. sys.path.insert(0, os.path.join(_ROOT, "common"))
  19. os.chdir(_ROOT)
  20. import schedule
  21. from loguru import logger
  22. from mysql_pool import MySQLConnectionPool
  23. from account_pool import AccountPool
  24. logger.remove()
  25. logger.add(sys.stderr, format="[{time:HH:mm:ss}] {level} {message}", level="INFO")
  26. logger.add("./logs/maintenance_{time:YYYYMMDD}.log", encoding="utf-8", rotation="00:00",
  27. format="[{time:YYYY-MM-DD HH:mm:ss}] {level} {message}", level="INFO", retention="7 day")
  28. # ---- 配置(待主公确认)----
  29. EXCEL_KEEP_DAYS = 30 # Excel 保留天数:删除早于此的报告 xlsx
  30. EXCEL_DIRS = [ # 要清理的报告输出目录
  31. os.path.join(_ROOT, "output", "onsale"), # 在售报告
  32. os.path.join(_ROOT, "reports", "sold_report"), # 已售日/周/月报等
  33. ]
  34. EXCEL_KEEP_FILES = {"球队.xlsx"} # 固定产物白名单:不按天清理(非报告,勿删)
  35. COOLING_REVIVE_MINUTES = 10 # cooling 巡检间隔(分钟)
  36. KEEPALIVE_SCAN_HOURS = 1 # token 保活巡检间隔(小时)
  37. KEEPALIVE_IDLE_HOURS = 6 # 闲置超过该小时数的 healthy 号才主动刷新(远小于 refresh_token 过期时长)
  38. EXCEL_CLEAN_AT = "03:00" # 每天几点清理 Excel
  39. def keepalive_pool(log) -> None:
  40. """巡检:对闲置超过 KEEPALIVE_IDLE_HOURS 的 healthy 号主动刷新 token,避免 refresh_token 闲到过期被判死。
  41. Args:
  42. log: 日志对象。
  43. """
  44. try:
  45. pool = MySQLConnectionPool(log=log)
  46. AccountPool(pool, log=log, task_tag="maintenance").keepalive_idle(KEEPALIVE_IDLE_HOURS, log=log)
  47. except Exception as e:
  48. log.warning(f"[保活] token 保活异常: {e}")
  49. def revive_pool(log) -> None:
  50. """巡检:把账号池里 cooling 冷却到期的账号恢复 healthy。
  51. Args:
  52. log: 日志对象。
  53. """
  54. try:
  55. pool = MySQLConnectionPool(log=log)
  56. AccountPool(pool, log=log, task_tag="maintenance").revive_cooling()
  57. log.info("[巡检] cooling 冷却到期账号已恢复检查完成")
  58. except Exception as e:
  59. log.warning(f"[巡检] cooling 恢复异常: {e}")
  60. def clean_old_excel(log) -> None:
  61. """清理各报告目录下超过 EXCEL_KEEP_DAYS 天的 .xlsx(排除白名单固定产物)。
  62. Args:
  63. log: 日志对象。
  64. """
  65. cutoff = time.time() - EXCEL_KEEP_DAYS * 86400
  66. removed = 0
  67. for d in EXCEL_DIRS:
  68. if not os.path.isdir(d):
  69. continue
  70. for f in glob.glob(os.path.join(d, "*.xlsx")):
  71. if os.path.basename(f) in EXCEL_KEEP_FILES: # 固定产物不清
  72. continue
  73. try:
  74. if os.path.getmtime(f) < cutoff:
  75. os.remove(f)
  76. removed += 1
  77. log.info(f"[清理] 删除旧报告 {f}")
  78. except Exception as e:
  79. log.warning(f"[清理] 删除失败 {f}: {e}")
  80. log.info(f"[清理] Excel 清理完成,删除 {removed} 个(保留最近 {EXCEL_KEEP_DAYS} 天)")
  81. def main() -> None:
  82. """注册两个维护定时任务并进入单循环调度。"""
  83. schedule.every(COOLING_REVIVE_MINUTES).minutes.do(revive_pool, logger) # cooling 巡检
  84. schedule.every(KEEPALIVE_SCAN_HOURS).hours.do(keepalive_pool, logger) # token 保活巡检
  85. schedule.every().day.at(EXCEL_CLEAN_AT).do(clean_old_excel, logger) # Excel 清理
  86. logger.info(f"运维守护启动:cooling 巡检每 {COOLING_REVIVE_MINUTES} 分钟 / "
  87. f"token 保活每 {KEEPALIVE_SCAN_HOURS} 小时(闲置>{KEEPALIVE_IDLE_HOURS}h 刷新) / "
  88. f"Excel 清理每天 {EXCEL_CLEAN_AT}(保留 {EXCEL_KEEP_DAYS} 天)")
  89. revive_pool(logger) # 启动即巡检一次
  90. keepalive_pool(logger) # 启动即保活一次
  91. while True:
  92. schedule.run_pending()
  93. time.sleep(1)
  94. if __name__ == "__main__":
  95. main()