# -*- coding: utf-8 -*- """导出 device_liveness 表里所有 device_id 及其对应的推送 regId,按 push_vendor 分组(本地开发工具)。 用法: 双击 scripts/show_device_regids.bat,或命令行: python scripts/show_device_regids.py [filter] [filter] 可选:大小写不敏感的子串,匹配 device_id / push_vendor / push_token / registration_id 任一列;不传则列出全部。 show_device_regids.py xiaomi # 只看小米 show_device_regids.py device_Pixel # 按 device_id 片段找 说明: - 直接以**只读**方式读 SQLite(server 在跑也不会抢写锁),所以开不开服务器都能用。 - push_token = 各厂商的 regId/pushToken(新链路,当前在用);registration_id = 旧极光 regId(历史兼容)。两列都打出来,方便跟 logcat 现役值逐字比对。 - 输出刻意保持纯 ASCII 排版并竖排展示**完整值**:双击弹出的 cmd 走 GBK 码页,竖排纯 ASCII 不会乱码;完整值不截断,才能直接复制去比对。 - 若 DATABASE_URL 不是 sqlite(生产 postgres),这里只提示改用 psql。 """ from __future__ import annotations import os import sqlite3 import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent def _resolve_sqlite_path() -> Path | None: """按 env > .env > 默认 的顺序拿 DATABASE_URL,解析出 sqlite 文件路径。""" url = os.environ.get("DATABASE_URL", "").strip() if not url: env = REPO_ROOT / ".env" if env.exists(): for line in env.read_text(encoding="utf-8", errors="ignore").splitlines(): s = line.strip() if s.startswith("DATABASE_URL="): url = s.split("=", 1)[1].strip().strip('"').strip("'") break if not url: url = "sqlite:///./data/app.db" if not url.startswith("sqlite"): print(f"[!] DATABASE_URL not sqlite: {url}") print(" This tool only reads a local SQLite dev DB. For prod use psql.") return None raw = url.split("///", 1)[1] if "///" in url else "./data/app.db" p = Path(raw) if not p.is_absolute(): p = (REPO_ROOT / raw).resolve() return p def main() -> int: needle = sys.argv[1].lower() if len(sys.argv) > 1 else None db = _resolve_sqlite_path() if db is None: return 2 if not db.exists(): print(f"[X] DB file not found: {db}") return 2 # 只读打开,避免和正在运行的 server 抢写锁。URI 路径用正斜杠(as_posix)规避 # Windows 反斜杠/盘符在 file: URI 里的歧义;万一 URI 形式打不开再回退普通连接。 try: con = sqlite3.connect(f"file:{db.as_posix()}?mode=ro", uri=True) except sqlite3.OperationalError: con = sqlite3.connect(str(db)) con.row_factory = sqlite3.Row rows = con.execute( """ SELECT id, user_id, device_id, push_vendor, push_token, registration_id, platform, app_version, liveness_state, last_heartbeat_at, updated_at FROM device_liveness ORDER BY updated_at DESC """ ).fetchall() con.close() if needle: def hit(r: sqlite3.Row) -> bool: for k in ("device_id", "push_vendor", "push_token", "registration_id"): v = r[k] if v and needle in str(v).lower(): return True return False rows = [r for r in rows if hit(r)] with_token = sum(1 for r in rows if (r["push_token"] or "").strip()) with_reg = sum(1 for r in rows if (r["registration_id"] or "").strip()) # 按 push_vendor 分组;None/空归入 "(none)"。组顺序:设备数多的在前,(none) 垫底; # 组内沿用 updated_at DESC(rows 查询时已如此排序,dict 保序即可)。 groups: dict[str, list] = {} for r in rows: groups.setdefault(r["push_vendor"] or "(none)", []).append(r) ordered = sorted( groups.items(), key=lambda kv: (kv[0] == "(none)", -len(kv[1]), kv[0]) ) tally = " ".join(f"{v}={len(items)}" for v, items in ordered) or "-" print(f"DB : {db}") line = f"device_liveness : {len(rows)} row(s)" if needle: line += f' filter="{sys.argv[1]}"' print(line) print(f"has push_token(vendor regId) : {with_token}" f" has registration_id(jiguang) : {with_reg}") print(f"vendors : {tally}") print("=" * 72) if not rows: print("(no rows)") return 0 for vendor, items in ordered: print() print(f"===== vendor={vendor} : {len(items)} device(s) =====") for r in items: print(f" [#{r['id']}] user_id={r['user_id']} platform={r['platform']}" f" state={r['liveness_state']} app={r['app_version'] or '-'}") print(f" device_id : {r['device_id']}") print(f" push_token(regId): {r['push_token'] or '(empty)'}") print(f" registration_id : {r['registration_id'] or '(empty)'}") print(f" last_heartbeat : {r['last_heartbeat_at'] or '-'}" f" updated : {r['updated_at']}") print(" " + "-" * 68) return 0 if __name__ == "__main__": raise SystemExit(main())