#!/usr/bin/env python3 """回填 signals.matched_project_id 对应的 project_signal_links。""" from __future__ import annotations import argparse import asyncio from sqlalchemy import and_, select from app.core.database import AsyncSessionLocal from app.models import ProjectSignalLink, Signal async def _query_orphans(*, tenant_id: int, signal_id: int | None) -> list[tuple[int, int, int, str]]: async with AsyncSessionLocal() as db: stmt = ( select( Signal.id, Signal.tenant_id, Signal.matched_project_id, Signal.title, ) .outerjoin( ProjectSignalLink, and_( ProjectSignalLink.tenant_id == Signal.tenant_id, ProjectSignalLink.signal_id == Signal.id, ), ) .where( and_( Signal.tenant_id == int(tenant_id), Signal.matched_project_id.is_not(None), ProjectSignalLink.id.is_(None), ) ) .order_by(Signal.id.asc()) ) if signal_id is not None: stmt = stmt.where(Signal.id == int(signal_id)) rows = (await db.execute(stmt)).all() return [ ( int(row[0]), int(row[1]), int(row[2]), str(row[3] or ""), ) for row in rows if row[0] is not None and row[1] is not None and row[2] is not None ] async def _apply_backfill(*, tenant_id: int, signal_id: int | None) -> int: rows = await _query_orphans(tenant_id=tenant_id, signal_id=signal_id) if not rows: return 0 async with AsyncSessionLocal() as db: for sid, tid, pid, _ in rows: db.add( ProjectSignalLink( tenant_id=tid, signal_id=sid, project_id=pid, link_type="matched_project", relevance_score=1.0, ) ) await db.commit() return len(rows) async def _main() -> None: parser = argparse.ArgumentParser(description="Backfill orphan project_signal_links from signals.matched_project_id") parser.add_argument("--tenant-id", type=int, default=1, help="目标租户 ID,默认 1") parser.add_argument("--signal-id", type=int, default=None, help="仅修复指定 signal_id") parser.add_argument("--apply", action="store_true", help="执行写入;不传时仅预览") args = parser.parse_args() rows = await _query_orphans(tenant_id=args.tenant_id, signal_id=args.signal_id) print(f"[preview] tenant_id={args.tenant_id} orphan_count={len(rows)}") for sid, _, pid, title in rows[:20]: print(f"- signal_id={sid} -> project_id={pid} title={title[:80]}") if len(rows) > 20: print(f"... ({len(rows) - 20} more)") if not args.apply: print("[dry-run] 未执行写入。加 --apply 以回填。") return inserted = await _apply_backfill(tenant_id=args.tenant_id, signal_id=args.signal_id) remaining = await _query_orphans(tenant_id=args.tenant_id, signal_id=args.signal_id) print(f"[apply] inserted={inserted} remaining_orphans={len(remaining)}") if __name__ == "__main__": asyncio.run(_main())