# -*- coding: utf-8 -*- """每日 00:00 调度: 遍历所有绑定主账号的区服, 用其 token 调 getTodayOpen, 写入 zones 表。 调度方式 (任选其一): - Windows 任务计划程序: 每天 00:00:30 触发 schtasks /Create /SC DAILY /TN "996cq_zone_sync" /TR "python D:\\传奇\\tools\\daily_zone_sync.py" /ST 00:00:30 - cron: 30 0 * * * python /path/to/daily_zone_sync.py - 手动: python tools/daily_zone_sync.py 依赖: - B1 已跑过 (servers 在库) - server_users 已有 is_main=1 的绑定 - 主账号 token 在 users.token (新跑 A1 后不超过 30 天) """ import argparse import asyncio import sys from datetime import datetime from pathlib import Path _HERE = Path(__file__).resolve().parent sys.path.insert(0, str(_HERE.parent / "cq2_sdk" / "src")) sys.path.insert(0, str(_HERE / "已经开发的部分代码" / "chuanqi-main" / "code" / "GeekedTest")) # 加载 cq2_sdk/.env (CQ2_DB_* 等) —— 必须先 import env_loader from cq2_sdk.env_loader import load_env # noqa: E402 load_env(_HERE.parent / "cq2_sdk" / ".env") from cq2_sdk.db_models import ServerUser, Zone from cq2_sdk.login import Box996Login, ManualGeetestSolver from cq2_sdk.login.box_996 import TodayOpenServer from cq2_sdk.store import ( ServerStore, ServerUserStore, UserStore, ZoneStore, connection, ) DEFAULT_DEVICE_ID = "00:15:5d:0a:04:00" def api_zone_to_db_zone(api: TodayOpenServer) -> Zone: """996box API → Zone, 拆分 date + time-of-day, 按 state 区分 status。""" open_time = datetime.fromtimestamp(api.open_time) if api.open_time else None if api.state == 1: status = "open" elif api.state == 5: status = "scheduled" else: status = "closed" return Zone( zone_id=str(api.serverid), zone_name=(f"{api.server_name} / {api.section.strip()}" if api.section else api.server_name), host=None, port=None, open_time=open_time, open_date=open_time.date() if open_time else None, open_time_of_day=open_time.time() if open_time else None, api_state=api.state, api_section=(api.section or "").strip() or None, status=status, ) async def sync_one(su: ServerUser) -> dict: """用主账号 token 调 getTodayOpen 并入库。返回结果摘要。""" with connection() as conn: user = UserStore(conn).get_by_username(su.username) if not user or not user.token: return {"server": su.server_name, "user": su.username, "status": "FAIL", "msg": "no token in MySQL"} server = ServerStore(conn).get_by_name(su.server_name) if not server: return {"server": su.server_name, "user": su.username, "status": "FAIL", "msg": "server not in MySQL"} # 调 API try: box = Box996Login(geetest=ManualGeetestSolver(), device_id=DEFAULT_DEVICE_ID) box.token = user.token api_zones = await box.get_today_open(game_id=server.game_id) except Exception as e: return {"server": su.server_name, "user": su.username, "status": "FAIL", "msg": f"API error: {type(e).__name__}: {e}"} # 入库 db_zones = [api_zone_to_db_zone(z) for z in api_zones] zones = ZoneStore(conn) n_upserted = zones.bulk_upsert(server.id, db_zones) fresh_ids = {z.zone_id for z in db_zones} n_closed = zones.mark_missing_closed(server.id, fresh_ids) return { "server": su.server_name, "user": su.username, "status": "OK", "api_total": len(api_zones), "upserted": n_upserted, "closed": n_closed, "open_count": sum(1 for z in db_zones if z.status == "open"), "scheduled_count": sum(1 for z in db_zones if z.status == "scheduled"), } async def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--server", help="只同步指定区服名 (默认: 所有绑定主账号的区服)") ap.add_argument("--dry-run", action="store_true", help="列出将同步的区服, 不实际调 API") args = ap.parse_args() with connection() as conn: su_store = ServerUserStore(conn) all_sus = su_store.list_servers_with_main() if args.server: all_sus = [su for su in all_sus if su.server_name == args.server] if not all_sus: print(f"FAIL: 没有为 '{args.server}' 绑定的主账号。先跑 bind_server_main.py") return 1 print(f"将同步 {len(all_sus)} 个区服:") for su in all_sus: print(f" - {su.server_name} (主账号: {su.username})") if args.dry_run: print("\n[DRY-RUN] 不实际执行。") return 0 results = [] for su in all_sus: print(f"\n>>> {su.server_name} (主账号 {su.username})") r = await sync_one(su) results.append(r) for k, v in r.items(): print(f" {k}: {v}") print("\n========== 汇总 ==========") for r in results: print(f" {r.get('server','?'):<24} {r.get('status','?'):<6} " f"api={r.get('api_total','-')} upsert={r.get('upserted','-')} " f"open={r.get('open_count','-')} sched={r.get('scheduled_count','-')}") return 0 if all(r["status"] == "OK" for r in results) else 1 if __name__ == "__main__": sys.exit(asyncio.run(main()))