cleanup_selection_records.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 清理选品记录表 buff_selection_request 的历史数据
  5. - 每1小时执行一次
  6. - 对每个用户,保留最新的一条选品记录,删除其他历史记录
  7. - 如果用户只有1条选品记录,则不删除
  8. - 记录清理结果到日志
  9. """
  10. import os
  11. import sys
  12. import time
  13. import configparser
  14. from datetime import datetime, timezone, timedelta
  15. from pathlib import Path
  16. import pymysql
  17. # 北京时间 UTC+8,与 backend-client 一致
  18. BEIJING_TZ = timezone(timedelta(hours=8))
  19. def beijing_now() -> datetime:
  20. """当前北京时间(naive)"""
  21. return datetime.now(BEIJING_TZ).replace(tzinfo=None)
  22. def load_config():
  23. """从 config.ini 加载配置,返回 config 对象"""
  24. conf_path = Path(__file__).resolve().parent / "config.ini"
  25. if not conf_path.exists():
  26. print(f"错误: 未找到配置文件 {conf_path},请复制 config.ini.example 为 config.ini 并填写", file=sys.stderr)
  27. sys.exit(1)
  28. config = configparser.ConfigParser()
  29. config.read(conf_path, encoding="utf-8")
  30. return config
  31. def get_db_connection(config):
  32. """创建数据库连接"""
  33. return pymysql.connect(
  34. host=config.get("database", "host"),
  35. port=config.getint("database", "port"),
  36. user=config.get("database", "user"),
  37. password=config.get("database", "password"),
  38. database=config.get("database", "database"),
  39. charset=config.get("database", "charset"),
  40. cursorclass=pymysql.cursors.DictCursor,
  41. )
  42. def run_once(config) -> int:
  43. """
  44. 执行一次清理:删除每个用户的历史选品记录
  45. 每个用户保留最新的一条记录
  46. 返回: 删除的记录数量
  47. """
  48. now_dt = beijing_now()
  49. now_str = now_dt.strftime("%Y-%m-%d %H:%M:%S")
  50. conn = get_db_connection(config)
  51. try:
  52. with conn.cursor() as cursor:
  53. # 先查出每个用户的最新记录 ID
  54. cursor.execute("""
  55. SELECT user_id, MAX(id) as max_id
  56. FROM buff_selection_request
  57. GROUP BY user_id
  58. """)
  59. latest_records = cursor.fetchall()
  60. if not latest_records:
  61. print(f"[{now_str}] 无选品记录需要清理")
  62. return 0
  63. # 删除不在保留列表中的 request_id(即要删除的请求单 ID)
  64. cursor.execute("""
  65. DELETE FROM buff_selection_request
  66. WHERE id NOT IN (
  67. SELECT max_id FROM (
  68. SELECT MAX(id) as max_id
  69. FROM buff_selection_request
  70. GROUP BY user_id
  71. ) AS latest_ids
  72. )
  73. """)
  74. deleted_request_count = cursor.rowcount
  75. # 删除关联的子表数据(item 和 log)
  76. # 注意:MySQL 不允许在 DELETE 中直接引用同一张表做子查询,需要用派生表
  77. cursor.execute("""
  78. DELETE FROM buff_selection_request_item
  79. WHERE request_id NOT IN (
  80. SELECT max_id FROM (
  81. SELECT MAX(id) as max_id
  82. FROM buff_selection_request
  83. GROUP BY user_id
  84. ) AS latest_ids
  85. )
  86. """)
  87. deleted_item_count = cursor.rowcount
  88. cursor.execute("""
  89. DELETE FROM buff_selection_request_log
  90. WHERE request_id NOT IN (
  91. SELECT max_id FROM (
  92. SELECT MAX(id) as max_id
  93. FROM buff_selection_request
  94. GROUP BY user_id
  95. ) AS latest_ids
  96. )
  97. """)
  98. deleted_log_count = cursor.rowcount
  99. conn.commit()
  100. # 查询剩余记录数
  101. cursor.execute("SELECT COUNT(*) as remaining_count FROM buff_selection_request")
  102. remaining_count = cursor.fetchone()["remaining_count"]
  103. if deleted_request_count == 0 and deleted_item_count == 0 and deleted_log_count == 0:
  104. print(f"[{now_str}] 无历史选品记录需要清理(每个用户仅保留最新记录)")
  105. return 0
  106. print(f"[{now_str}] 清理完成:")
  107. print(f" - 删除 {deleted_request_count} 条选品请求单")
  108. print(f" - 删除 {deleted_item_count} 条选中饰品明细")
  109. print(f" - 删除 {deleted_log_count} 条选品日志")
  110. print(f" - 剩余 {remaining_count} 条选品请求单")
  111. return deleted_request_count
  112. finally:
  113. conn.close()
  114. def main():
  115. config = load_config()
  116. interval_seconds = config.getint("script", "interval_seconds", fallback=3600)
  117. print(f"=" * 50)
  118. print(f"选品记录清理脚本已启动")
  119. print(f"清理周期: 每 {interval_seconds} 秒执行一次 ({interval_seconds // 60} 分钟)")
  120. print(f"清理规则: 每个用户保留最新的一条选品记录")
  121. print(f"启动时间: {beijing_now().strftime('%Y-%m-%d %H:%M:%S')}")
  122. print(f"=" * 50)
  123. # 启动时先执行一次清理
  124. try:
  125. run_once(config)
  126. except Exception as e:
  127. print(f"[{beijing_now()}] 启动时清理失败: {e}", file=sys.stderr)
  128. print(f"\n开始定时清理,每 {interval_seconds} 秒执行一次...\n")
  129. while True:
  130. time.sleep(interval_seconds)
  131. try:
  132. run_once(config)
  133. except Exception as e:
  134. print(f"[{beijing_now()}] 清理异常: {e}", file=sys.stderr)
  135. if __name__ == "__main__":
  136. main()