172 lines
6.4 KiB
Python
172 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
"""Core → Neo4j 关系图同步(P0 节点与关系)。"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parents[2]
|
||
|
|
sys.path.insert(0, str(ROOT))
|
||
|
|
|
||
|
|
from neo4j import GraphDatabase
|
||
|
|
from sqlalchemy import create_engine, text
|
||
|
|
|
||
|
|
from app.config.settings import settings
|
||
|
|
|
||
|
|
|
||
|
|
def db_url(database: str) -> str:
|
||
|
|
pwd = settings.mysql_password
|
||
|
|
auth = f"{settings.mysql_user}:{pwd}" if pwd else settings.mysql_user
|
||
|
|
return (
|
||
|
|
f"mysql+pymysql://{auth}@{settings.mysql_host}:{settings.mysql_port}"
|
||
|
|
f"/{database}?charset=utf8mb4"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
if not settings.neo4j_password:
|
||
|
|
print("WARN: NEO4J_PASSWORD empty, skip neo4j sync")
|
||
|
|
return
|
||
|
|
|
||
|
|
engine = create_engine(db_url(settings.mysql_core_database), pool_pre_ping=True)
|
||
|
|
driver = GraphDatabase.driver(
|
||
|
|
settings.neo4j_uri,
|
||
|
|
auth=(settings.neo4j_user, settings.neo4j_password),
|
||
|
|
)
|
||
|
|
|
||
|
|
with engine.connect() as conn:
|
||
|
|
customers = conn.execute(
|
||
|
|
text("SELECT customer_id, display_name FROM core_customer WHERE is_active=1")
|
||
|
|
).mappings().all()
|
||
|
|
advisors = conn.execute(
|
||
|
|
text(
|
||
|
|
"SELECT staff_id, display_name FROM core_staff WHERE staff_type='advisor' AND is_active=1"
|
||
|
|
)
|
||
|
|
).mappings().all()
|
||
|
|
products = conn.execute(
|
||
|
|
text("SELECT product_id, product_name, min_risk_code, industry_code FROM core_product")
|
||
|
|
).mappings().all()
|
||
|
|
grades = conn.execute(
|
||
|
|
text("SELECT code, display_name, grade_type FROM core_risk_grade")
|
||
|
|
).mappings().all()
|
||
|
|
industries = conn.execute(
|
||
|
|
text("SELECT industry_code, industry_name FROM core_industry")
|
||
|
|
).mappings().all()
|
||
|
|
assignments = conn.execute(
|
||
|
|
text(
|
||
|
|
"""
|
||
|
|
SELECT customer_id, advisor_id, effective_from, rel_status
|
||
|
|
FROM core_customer_advisor WHERE rel_status='active'
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
).mappings().all()
|
||
|
|
cust_risks = conn.execute(
|
||
|
|
text("SELECT customer_id, risk_code, evaluated_at FROM core_customer_risk")
|
||
|
|
).mappings().all()
|
||
|
|
holdings = conn.execute(
|
||
|
|
text(
|
||
|
|
"""
|
||
|
|
SELECT customer_id, product_id, qty, market_value, cost_amount, pnl_pct, as_of
|
||
|
|
FROM core_holding
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
).mappings().all()
|
||
|
|
|
||
|
|
with driver.session() as session:
|
||
|
|
session.run("CREATE CONSTRAINT customer_id IF NOT EXISTS FOR (c:Customer) REQUIRE c.customer_id IS UNIQUE")
|
||
|
|
session.run("CREATE CONSTRAINT advisor_id IF NOT EXISTS FOR (a:Advisor) REQUIRE a.advisor_id IS UNIQUE")
|
||
|
|
session.run("CREATE CONSTRAINT product_id IF NOT EXISTS FOR (p:Product) REQUIRE p.product_id IS UNIQUE")
|
||
|
|
session.run("CREATE CONSTRAINT risk_code IF NOT EXISTS FOR (r:RiskGrade) REQUIRE r.code IS UNIQUE")
|
||
|
|
session.run("CREATE CONSTRAINT industry_code IF NOT EXISTS FOR (i:Industry) REQUIRE i.industry_code IS UNIQUE")
|
||
|
|
|
||
|
|
for g in grades:
|
||
|
|
session.run(
|
||
|
|
"MERGE (r:RiskGrade {code: $code}) SET r.display_name=$display_name, r.grade_type=$grade_type",
|
||
|
|
**g,
|
||
|
|
)
|
||
|
|
for ind in industries:
|
||
|
|
session.run(
|
||
|
|
"MERGE (i:Industry {industry_code: $industry_code}) SET i.industry_name=$industry_name",
|
||
|
|
**ind,
|
||
|
|
)
|
||
|
|
for c in customers:
|
||
|
|
session.run(
|
||
|
|
"MERGE (c:Customer {customer_id: $customer_id}) SET c.display_name=$display_name",
|
||
|
|
**c,
|
||
|
|
)
|
||
|
|
for a in advisors:
|
||
|
|
session.run(
|
||
|
|
"MERGE (a:Advisor {advisor_id: $staff_id}) SET a.display_name=$display_name",
|
||
|
|
staff_id=a["staff_id"],
|
||
|
|
display_name=a["display_name"],
|
||
|
|
)
|
||
|
|
for p in products:
|
||
|
|
session.run(
|
||
|
|
"""
|
||
|
|
MERGE (p:Product {product_id: $product_id})
|
||
|
|
SET p.product_name=$product_name, p.min_risk_code=$min_risk_code
|
||
|
|
WITH p
|
||
|
|
OPTIONAL MATCH (i:Industry {industry_code: $industry_code})
|
||
|
|
FOREACH (_ IN CASE WHEN i IS NOT NULL THEN [1] ELSE [] END |
|
||
|
|
MERGE (p)-[:BELONGS_TO]->(i)
|
||
|
|
)
|
||
|
|
""",
|
||
|
|
**p,
|
||
|
|
)
|
||
|
|
session.run(
|
||
|
|
"""
|
||
|
|
MATCH (p:Product {product_id: $product_id}), (r:RiskGrade {code: $min_risk_code})
|
||
|
|
MERGE (p)-[:REQUIRES_MIN_RISK {rule_id: 'R02-PROD-MIN'}]->(r)
|
||
|
|
""",
|
||
|
|
product_id=p["product_id"],
|
||
|
|
min_risk_code=p["min_risk_code"],
|
||
|
|
)
|
||
|
|
for row in assignments:
|
||
|
|
session.run(
|
||
|
|
"""
|
||
|
|
MATCH (c:Customer {customer_id: $customer_id}), (a:Advisor {advisor_id: $advisor_id})
|
||
|
|
MERGE (c)-[r:ASSIGNED_TO]->(a)
|
||
|
|
SET r.since=$effective_from, r.status=$rel_status
|
||
|
|
""",
|
||
|
|
**row,
|
||
|
|
)
|
||
|
|
for row in cust_risks:
|
||
|
|
session.run(
|
||
|
|
"""
|
||
|
|
MATCH (c:Customer {customer_id: $customer_id}), (r:RiskGrade {code: $risk_code})
|
||
|
|
MERGE (c)-[hr:HAS_RISK_LEVEL]->(r)
|
||
|
|
SET hr.source='l0', hr.evaluated_at=$evaluated_at
|
||
|
|
""",
|
||
|
|
customer_id=row["customer_id"],
|
||
|
|
risk_code=row["risk_code"],
|
||
|
|
evaluated_at=str(row["evaluated_at"]),
|
||
|
|
)
|
||
|
|
session.run("MATCH ()-[h:HOLDS]->() DELETE h")
|
||
|
|
for h in holdings:
|
||
|
|
session.run(
|
||
|
|
"""
|
||
|
|
MATCH (c:Customer {customer_id: $customer_id}), (p:Product {product_id: $product_id})
|
||
|
|
MERGE (c)-[h:HOLDS]->(p)
|
||
|
|
SET h.qty=$qty, h.market_value=$market_value, h.cost=$cost_amount,
|
||
|
|
h.pnl_pct=$pnl_pct, h.as_of=$as_of
|
||
|
|
""",
|
||
|
|
customer_id=h["customer_id"],
|
||
|
|
product_id=h["product_id"],
|
||
|
|
qty=float(h["qty"]),
|
||
|
|
market_value=float(h["market_value"]),
|
||
|
|
cost_amount=float(h["cost_amount"]),
|
||
|
|
pnl_pct=float(h["pnl_pct"]),
|
||
|
|
as_of=str(h["as_of"]),
|
||
|
|
)
|
||
|
|
|
||
|
|
driver.close()
|
||
|
|
print(
|
||
|
|
f"sync_neo4j: customers={len(customers)} advisors={len(advisors)} "
|
||
|
|
f"products={len(products)} holdings={len(holdings)}"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|