100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
#!/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.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())
|