Files

182 lines
6.9 KiB
Python
Raw Permalink 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.
"""customer_relation 客户-投顾关系仓储。
同时服务于:
- 投顾工作台(advisor):数据权限根(仅本人名下客户)+ 客户列表 + AUM 聚合;
- 记忆/client_agent 模块:按客户 ID 读取有效关系。
"""
from __future__ import annotations
from decimal import Decimal
from sqlalchemy import func, or_, select
from common.common_const import CUSTOMER_REL_STATUS_SIGNED, CUSTOMER_REL_STATUS_UNSIGNED
from model.customer_relation import CustomerRelation
from model.fin_customer_profile import FinCustomerProfile
from model.fin_holdings import FinHoldings
from model.sys_user import SysUser
from repositories.base import BaseRepository
# 持仓中状态:仅统计「持有中」市值,与 service/holdings.py 口径一致(DDL 注释,未入 common_const)
_HOLDING_STATUS = "持有中"
class CustomerRelationRepo(BaseRepository):
model = CustomerRelation
async def get_active_relation(
self, *, customer_id: int, advisor_id: int
) -> CustomerRelation | None:
"""读取投顾可访问的当前关系,排除已结束关系。"""
return await self.db.scalar(
select(CustomerRelation)
.where(
CustomerRelation.customer_id == customer_id,
CustomerRelation.advisor_id == advisor_id,
CustomerRelation.status.in_(
[CUSTOMER_REL_STATUS_UNSIGNED, CUSTOMER_REL_STATUS_SIGNED]
),
)
.order_by(CustomerRelation.id.desc())
.limit(1)
)
async def get_by_customer_advisor(
self, customer_id: int, advisor_id: int
) -> CustomerRelation | None:
"""取指定客户与投顾的关系(数据权限判断用)。"""
return await self.db.scalar(
select(CustomerRelation).where(
CustomerRelation.customer_id == customer_id,
CustomerRelation.advisor_id == advisor_id,
)
)
async def get_current_by_customer(self, customer_id: int) -> CustomerRelation | None:
"""取客户当前有效关系(历史重分配可能多行,取最近一条,按 id 倒序)。"""
return await self.db.scalar(
select(CustomerRelation)
.where(CustomerRelation.customer_id == customer_id)
.order_by(CustomerRelation.id.desc())
.limit(1)
)
async def list_by_customer(self, customer_id: int) -> list[CustomerRelation]:
"""返回指定客户尚未结束的关系(记忆/client_agent 模块用)。"""
statement = (
select(CustomerRelation)
.where(
CustomerRelation.customer_id == customer_id,
CustomerRelation.status != "已结束",
)
.order_by(CustomerRelation.assign_time.desc(), CustomerRelation.id.desc())
)
return list((await self.db.scalars(statement)).all())
async def list_by_status(self, status: str) -> list[CustomerRelation]:
"""按状态取全部关系(定时调度遍历 signed 客户用,跨投顾)。"""
return list(
(
await self.db.scalars(
select(CustomerRelation).where(CustomerRelation.status == status)
)
).all()
)
async def list_by_advisor(
self, advisor_id: int, status: str | None = None
) -> list[CustomerRelation]:
"""名下客户关系列表(status=None 不过滤)。"""
stmt = select(CustomerRelation).where(CustomerRelation.advisor_id == advisor_id)
if status:
stmt = stmt.where(CustomerRelation.status == status)
return list((await self.db.scalars(stmt)).all())
async def list_customer_rows(
self,
*,
advisor_id: int,
status: str | None = None,
keyword: str | None = None,
limit: int = 100,
offset: int = 0,
) -> list[tuple[CustomerRelation, SysUser, FinCustomerProfile | None]]:
"""客户列表:关系 + 客户账号 + 画像(左连),返回三元组供 service 拼装响应。"""
conds = [CustomerRelation.advisor_id == advisor_id]
if status:
conds.append(CustomerRelation.status == status)
if keyword:
like = f"%{keyword}%"
conds.append(or_(SysUser.real_name.like(like), SysUser.phone.like(like)))
stmt = (
select(CustomerRelation, SysUser, FinCustomerProfile)
.join(SysUser, SysUser.id == CustomerRelation.customer_id)
.outerjoin(
FinCustomerProfile,
FinCustomerProfile.customer_id == CustomerRelation.customer_id,
)
.where(*conds)
.order_by(CustomerRelation.id.desc())
.limit(limit)
.offset(offset)
)
return list((await self.db.execute(stmt)).all())
async def count_customer_rows(
self,
*,
advisor_id: int,
status: str | None = None,
keyword: str | None = None,
) -> int:
conds = [CustomerRelation.advisor_id == advisor_id]
if status:
conds.append(CustomerRelation.status == status)
if keyword:
like = f"%{keyword}%"
conds.append(or_(SysUser.real_name.like(like), SysUser.phone.like(like)))
stmt = (
select(func.count())
.select_from(CustomerRelation)
.join(SysUser, SysUser.id == CustomerRelation.customer_id)
.where(*conds)
)
return (await self.db.scalar(stmt)) or 0
async def risk_level_distribution(
self, advisor_id: int
) -> list[tuple[str | None, int]]:
"""名下客户风险等级分布(C1-C5 各档人数),供驾驶舱客户分层。"""
stmt = (
select(FinCustomerProfile.risk_level, func.count())
.select_from(CustomerRelation)
.join(
FinCustomerProfile,
FinCustomerProfile.customer_id == CustomerRelation.customer_id,
)
.where(CustomerRelation.advisor_id == advisor_id)
.group_by(FinCustomerProfile.risk_level)
)
return list((await self.db.execute(stmt)).all())
async def sum_holdings_value(
self, advisor_id: int, relation_status: str | None = None
) -> Decimal:
"""AUM:名下客户「持有中」持仓当前市值之和(relation_status 过滤,如 signed)。"""
stmt = (
select(func.coalesce(func.sum(FinHoldings.current_value), 0))
.select_from(CustomerRelation)
.join(FinHoldings, FinHoldings.customer_id == CustomerRelation.customer_id)
.where(
CustomerRelation.advisor_id == advisor_id,
FinHoldings.status == _HOLDING_STATUS,
)
)
if relation_status:
stmt = stmt.where(CustomerRelation.status == relation_status)
return await self.db.scalar(stmt)
__all__ = ["CustomerRelationRepo"]