Files
group_fqcd_jr/tools/reconcile_graph.py
张胜宇 e239eb778b docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富
   统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」;
   同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。
2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本),
   新增《文档规整方案与开发前待决事项-2026-09-17》。
3) 客服agent 四份交付文档首次纳入本分支。
2026-09-17 15:15:22 +08:00

99 lines
3.8 KiB
Python

"""图投影对账:检查图里的关系与权威画像是否一致,可选自动修复。
图是投影(MySQL 才是唯一真相),所以它可能因为投影漏投、记忆失效后未清理、或图库故障
而与画像不一致。本工具用**数据层对账**回答"图里现在的内容对不对",与
`ProjectionReconciliationService` 的**事件层对账**(哪些投影事件还没投递)互补。
用法:
```powershell
# 只检查,不改动
python tools/reconcile_graph.py 9001
# 检查并修复(缺失的补写、多余的删除)
python tools/reconcile_graph.py 9001 --repair
# 检查所有有记忆数据的客户
python tools/reconcile_graph.py --all
```
退出码:0 = 全部一致(或已修复),1 = 存在不一致(未修复)或对账降级。
"""
import argparse
import asyncio
import sys
from sqlalchemy import select
from app.infrastructure.db import SessionFactory
from app.infrastructure.graph import build_graph_driver
from app.model.memory import MemoryUnit
from app.service.profile_graph_projection_service import ProfileGraphProjectionService
from app.service.relationship_service import RelationshipService
# GBK 控制台下图里的中文标签可能含不可编码字符,替换而不是让脚本自身崩掉
sys.stdout.reconfigure(errors="replace")
async def reconcile_one(customer_id: int, *, repair: bool) -> bool:
driver = build_graph_driver()
if driver is None:
print("图驱动不可用:检查 .env 中的 NEO4J_URI / NEO4J_PASSWORD")
return False
graph = RelationshipService(driver)
try:
async with SessionFactory() as session:
service = ProfileGraphProjectionService(session, graph)
outcome = await service.reconcile_customer(customer_id, repair=repair)
finally:
await driver.close()
print(f"\n== 客户 {customer_id} ==")
if outcome.degraded:
print(f" 对账降级:{outcome.reason}")
return False
print(f" 应有 {len(outcome.expected)} 条 / 图里 {len(outcome.actual)} 条")
if outcome.missing:
print(f" 缺失(画像有、图里没有){len(outcome.missing)} 条:")
for relation, target_type, target_id in outcome.missing:
print(f" + {relation} -> {target_type}:{target_id}")
if outcome.orphaned:
print(f" 多余(图里有、画像已无){len(outcome.orphaned)} 条:")
for relation, target_type, target_id in outcome.orphaned:
print(f" - {relation} -> {target_type}:{target_id}")
if outcome.consistent:
print(" 一致")
elif repair and outcome.repaired:
print(" 已按画像修复(缺失已补写、多余已删除)")
else:
print(" 不一致:加 --repair 可按画像修复")
return outcome.consistent
async def main() -> int:
parser = argparse.ArgumentParser(description="图投影对账")
parser.add_argument("customer_id", nargs="?", type=int, help="客户 id")
parser.add_argument("--all", action="store_true", help="对账所有有记忆数据的客户")
parser.add_argument("--repair", action="store_true", help="按画像修复差异")
args = parser.parse_args()
if args.all:
async with SessionFactory() as session:
rows = list(await session.scalars(select(MemoryUnit.customer_id).distinct()))
targets = sorted({int(row) for row in rows})
if not targets:
print("没有任何客户有记忆数据,无需对账")
return 0
print(f"将对账 {len(targets)} 个客户:{targets}")
results = [await reconcile_one(cid, repair=args.repair) for cid in targets]
return 0 if all(results) else 1
if args.customer_id is None:
parser.print_help()
return 2
return 0 if await reconcile_one(args.customer_id, repair=args.repair) else 1
sys.exit(asyncio.run(main()))