daily_zone_sync.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. # -*- coding: utf-8 -*-
  2. """每日 00:00 调度: 遍历所有绑定主账号的区服, 用其 token 调 getTodayOpen, 写入 zones 表。
  3. 调度方式 (任选其一):
  4. - Windows 任务计划程序: 每天 00:00:30 触发
  5. schtasks /Create /SC DAILY /TN "996cq_zone_sync" /TR "python D:\\传奇\\tools\\daily_zone_sync.py" /ST 00:00:30
  6. - cron: 30 0 * * * python /path/to/daily_zone_sync.py
  7. - 手动: python tools/daily_zone_sync.py
  8. 依赖:
  9. - B1 已跑过 (servers 在库)
  10. - server_users 已有 is_main=1 的绑定
  11. - 主账号 token 在 users.token (新跑 A1 后不超过 30 天)
  12. """
  13. import argparse
  14. import asyncio
  15. import sys
  16. from datetime import datetime
  17. from pathlib import Path
  18. _HERE = Path(__file__).resolve().parent
  19. sys.path.insert(0, str(_HERE.parent / "cq2_sdk" / "src"))
  20. sys.path.insert(0, str(_HERE / "已经开发的部分代码" / "chuanqi-main" / "code" / "GeekedTest"))
  21. # 加载 cq2_sdk/.env (CQ2_DB_* 等) —— 必须先 import env_loader
  22. from cq2_sdk.env_loader import load_env # noqa: E402
  23. load_env(_HERE.parent / "cq2_sdk" / ".env")
  24. from cq2_sdk.db_models import ServerUser, Zone
  25. from cq2_sdk.login import Box996Login, ManualGeetestSolver
  26. from cq2_sdk.login.box_996 import TodayOpenServer
  27. from cq2_sdk.store import (
  28. ServerStore, ServerUserStore, UserStore, ZoneStore, connection,
  29. )
  30. DEFAULT_DEVICE_ID = "00:15:5d:0a:04:00"
  31. def api_zone_to_db_zone(api: TodayOpenServer) -> Zone:
  32. """996box API → Zone, 拆分 date + time-of-day, 按 state 区分 status。"""
  33. open_time = datetime.fromtimestamp(api.open_time) if api.open_time else None
  34. if api.state == 1:
  35. status = "open"
  36. elif api.state == 5:
  37. status = "scheduled"
  38. else:
  39. status = "closed"
  40. return Zone(
  41. zone_id=str(api.serverid),
  42. zone_name=(f"{api.server_name} / {api.section.strip()}"
  43. if api.section else api.server_name),
  44. host=None,
  45. port=None,
  46. open_time=open_time,
  47. open_date=open_time.date() if open_time else None,
  48. open_time_of_day=open_time.time() if open_time else None,
  49. api_state=api.state,
  50. api_section=(api.section or "").strip() or None,
  51. status=status,
  52. )
  53. async def sync_one(su: ServerUser) -> dict:
  54. """用主账号 token 调 getTodayOpen 并入库。返回结果摘要。"""
  55. with connection() as conn:
  56. user = UserStore(conn).get_by_username(su.username)
  57. if not user or not user.token:
  58. return {"server": su.server_name, "user": su.username,
  59. "status": "FAIL", "msg": "no token in MySQL"}
  60. server = ServerStore(conn).get_by_name(su.server_name)
  61. if not server:
  62. return {"server": su.server_name, "user": su.username,
  63. "status": "FAIL", "msg": "server not in MySQL"}
  64. # 调 API
  65. try:
  66. box = Box996Login(geetest=ManualGeetestSolver(),
  67. device_id=DEFAULT_DEVICE_ID)
  68. box.token = user.token
  69. api_zones = await box.get_today_open(game_id=server.game_id)
  70. except Exception as e:
  71. return {"server": su.server_name, "user": su.username,
  72. "status": "FAIL", "msg": f"API error: {type(e).__name__}: {e}"}
  73. # 入库
  74. db_zones = [api_zone_to_db_zone(z) for z in api_zones]
  75. zones = ZoneStore(conn)
  76. n_upserted = zones.bulk_upsert(server.id, db_zones)
  77. fresh_ids = {z.zone_id for z in db_zones}
  78. n_closed = zones.mark_missing_closed(server.id, fresh_ids)
  79. return {
  80. "server": su.server_name, "user": su.username,
  81. "status": "OK",
  82. "api_total": len(api_zones),
  83. "upserted": n_upserted,
  84. "closed": n_closed,
  85. "open_count": sum(1 for z in db_zones if z.status == "open"),
  86. "scheduled_count": sum(1 for z in db_zones if z.status == "scheduled"),
  87. }
  88. async def main() -> int:
  89. ap = argparse.ArgumentParser()
  90. ap.add_argument("--server", help="只同步指定区服名 (默认: 所有绑定主账号的区服)")
  91. ap.add_argument("--dry-run", action="store_true",
  92. help="列出将同步的区服, 不实际调 API")
  93. args = ap.parse_args()
  94. with connection() as conn:
  95. su_store = ServerUserStore(conn)
  96. all_sus = su_store.list_servers_with_main()
  97. if args.server:
  98. all_sus = [su for su in all_sus if su.server_name == args.server]
  99. if not all_sus:
  100. print(f"FAIL: 没有为 '{args.server}' 绑定的主账号。先跑 bind_server_main.py")
  101. return 1
  102. print(f"将同步 {len(all_sus)} 个区服:")
  103. for su in all_sus:
  104. print(f" - {su.server_name} (主账号: {su.username})")
  105. if args.dry_run:
  106. print("\n[DRY-RUN] 不实际执行。")
  107. return 0
  108. results = []
  109. for su in all_sus:
  110. print(f"\n>>> {su.server_name} (主账号 {su.username})")
  111. r = await sync_one(su)
  112. results.append(r)
  113. for k, v in r.items():
  114. print(f" {k}: {v}")
  115. print("\n========== 汇总 ==========")
  116. for r in results:
  117. print(f" {r.get('server','?'):<24} {r.get('status','?'):<6} "
  118. f"api={r.get('api_total','-')} upsert={r.get('upserted','-')} "
  119. f"open={r.get('open_count','-')} sched={r.get('scheduled_count','-')}")
  120. return 0 if all(r["status"] == "OK" for r in results) else 1
  121. if __name__ == "__main__":
  122. sys.exit(asyncio.run(main()))