179 lines
7.0 KiB
Python
179 lines
7.0 KiB
Python
"""Core 模拟库只读访问(jinrong_core · 无 HTTP API)。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import date, datetime, time, timedelta
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
from sqlalchemy import text
|
||
from sqlalchemy.engine import Engine
|
||
|
||
from app.config.settings import settings
|
||
from app.utils.db import get_engine
|
||
|
||
|
||
class CoreReadOnlyRepository:
|
||
"""仅 SELECT jinrong_core;禁止写操作。"""
|
||
|
||
def __init__(self, engine: Engine | None = None) -> None:
|
||
self._engine = engine or get_engine(settings.mysql_core_database)
|
||
|
||
def get_customer_l0(self, customer_id: str) -> dict[str, Any] | None:
|
||
sql = text(
|
||
"""
|
||
SELECT c.customer_id, c.display_name, c.age, c.occupation, c.open_date,
|
||
r.risk_code, r.evaluated_at AS risk_evaluated_at
|
||
FROM core_customer c
|
||
LEFT JOIN core_customer_risk r ON r.customer_id = c.customer_id
|
||
WHERE c.customer_id = :cid AND c.is_active = 1
|
||
"""
|
||
)
|
||
with self._engine.connect() as conn:
|
||
row = conn.execute(sql, {"cid": customer_id}).mappings().first()
|
||
return dict(row) if row else None
|
||
|
||
def list_holdings(self, customer_id: str, limit: int = 500) -> list[dict[str, Any]]:
|
||
"""持仓明细(市值降序);limit 为 SQL 层保护上限(T-04 评审 P2)。
|
||
|
||
注意:调用方若要精确合计,须自行判断 len(rows) 是否触及 limit
|
||
(core_tools.query_holdings 以 truncated 字段对外暴露)。
|
||
"""
|
||
sql = text(
|
||
"""
|
||
SELECT h.*, p.product_name, p.min_risk_code, p.product_type
|
||
FROM core_holding h
|
||
JOIN core_product p ON p.product_id = h.product_id
|
||
WHERE h.customer_id = :cid
|
||
ORDER BY h.market_value DESC
|
||
LIMIT :lim
|
||
"""
|
||
)
|
||
with self._engine.connect() as conn:
|
||
return [
|
||
dict(r)
|
||
for r in conn.execute(sql, {"cid": customer_id, "lim": limit}).mappings()
|
||
]
|
||
|
||
def list_trades_range(
|
||
self, customer_id: str, start: datetime, end: datetime, limit: int = 10000
|
||
) -> list[dict[str, Any]]:
|
||
"""[start, end) confirmed 申赎流水,**时间升序**,不 JOIN(规则统计专用)。
|
||
|
||
升序保证 RISK-005「先小后大」的最早小额铺垫不被截断(B4 评审 P2-5);
|
||
去掉 product_name JOIN 防脏产品数据丢行。规则演示规模远低于 limit。
|
||
"""
|
||
sql = text(
|
||
"""
|
||
SELECT * FROM core_trade
|
||
WHERE customer_id = :cid
|
||
AND trade_type IN ('subscribe', 'redeem')
|
||
AND trade_status = 'confirmed'
|
||
AND traded_at >= :start AND traded_at < :end
|
||
ORDER BY traded_at ASC
|
||
LIMIT :lim
|
||
"""
|
||
)
|
||
with self._engine.connect() as conn:
|
||
return [
|
||
dict(r)
|
||
for r in conn.execute(
|
||
sql, {"cid": customer_id, "start": start, "end": end, "lim": limit}
|
||
).mappings()
|
||
]
|
||
|
||
def get_trade_by_id(self, trade_id: str) -> dict[str, Any] | None:
|
||
"""按 trade_id 查单笔 confirmed 交易流水(rebuild_alerts 补偿重放用,仅 SELECT)。
|
||
|
||
与引擎统计口径一致只取 confirmed——非 confirmed 流水不经引擎,重放无意义。
|
||
"""
|
||
sql = text(
|
||
"""
|
||
SELECT * FROM core_trade
|
||
WHERE trade_id = :tid AND trade_status = 'confirmed'
|
||
"""
|
||
)
|
||
with self._engine.connect() as conn:
|
||
row = conn.execute(sql, {"tid": trade_id}).mappings().first()
|
||
return dict(row) if row else None
|
||
|
||
def sum_trades_on_date(self, customer_id: str, day: date) -> Decimal:
|
||
"""当日申赎合计金额(RISK-002 累计口径:仅 confirmed 的 subscribe/redeem)。
|
||
|
||
当日 = 服务器本地时区自然日,以 traded_at 落在 [day 00:00, day+1 00:00) 为准。
|
||
"""
|
||
day_start = datetime.combine(day, time.min)
|
||
day_end = day_start + timedelta(days=1)
|
||
sql = text(
|
||
"""
|
||
SELECT COALESCE(SUM(amount), 0)
|
||
FROM core_trade
|
||
WHERE customer_id = :cid
|
||
AND trade_type IN ('subscribe', 'redeem')
|
||
AND trade_status = 'confirmed'
|
||
AND traded_at >= :day_start
|
||
AND traded_at < :day_end
|
||
"""
|
||
)
|
||
with self._engine.connect() as conn:
|
||
total = conn.execute(
|
||
sql, {"cid": customer_id, "day_start": day_start, "day_end": day_end}
|
||
).scalar_one()
|
||
return Decimal(total)
|
||
|
||
def get_product(self, product_id: str) -> dict[str, Any] | None:
|
||
sql = text("SELECT * FROM core_product WHERE product_id = :pid")
|
||
with self._engine.connect() as conn:
|
||
row = conn.execute(sql, {"pid": product_id}).mappings().first()
|
||
return dict(row) if row else None
|
||
|
||
def get_latest_nav(self, product_id: str) -> dict[str, Any] | None:
|
||
sql = text(
|
||
"""
|
||
SELECT * FROM core_product_nav
|
||
WHERE product_id = :pid
|
||
ORDER BY nav_date DESC LIMIT 1
|
||
"""
|
||
)
|
||
with self._engine.connect() as conn:
|
||
row = conn.execute(sql, {"pid": product_id}).mappings().first()
|
||
return dict(row) if row else None
|
||
|
||
def list_active_customers(self) -> list[dict[str, Any]]:
|
||
"""全量在册客户(id + display_name;AML scan_all 全量扫描用,仅 SELECT)。"""
|
||
sql = text(
|
||
"SELECT customer_id, display_name FROM core_customer WHERE is_active = 1"
|
||
)
|
||
with self._engine.connect() as conn:
|
||
return [dict(r) for r in conn.execute(sql).mappings()]
|
||
|
||
def list_customers_by_advisor(self, advisor_id: str) -> list[str]:
|
||
sql = text(
|
||
"""
|
||
SELECT customer_id FROM core_customer_advisor
|
||
WHERE advisor_id = :aid AND rel_status = 'active'
|
||
"""
|
||
)
|
||
with self._engine.connect() as conn:
|
||
return [r[0] for r in conn.execute(sql, {"aid": advisor_id})]
|
||
|
||
def get_staff(self, staff_id: str) -> dict[str, Any] | None:
|
||
"""RBAC 联调:查员工角色种子。"""
|
||
sql = text(
|
||
"SELECT staff_id, display_name, staff_type, roles FROM core_staff WHERE staff_id = :sid AND is_active = 1"
|
||
)
|
||
with self._engine.connect() as conn:
|
||
row = conn.execute(sql, {"sid": staff_id}).mappings().first()
|
||
return dict(row) if row else None
|
||
|
||
def is_advisor_assigned(self, advisor_id: str, customer_id: str) -> bool:
|
||
sql = text(
|
||
"""
|
||
SELECT 1 FROM core_customer_advisor
|
||
WHERE advisor_id = :aid AND customer_id = :cid AND rel_status = 'active'
|
||
LIMIT 1
|
||
"""
|
||
)
|
||
with self._engine.connect() as conn:
|
||
return conn.execute(sql, {"aid": advisor_id, "cid": customer_id}).first() is not None
|