Files
XingHuo/app/repository/core_ro.py
T

179 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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