182 lines
6.9 KiB
Python
182 lines
6.9 KiB
Python
"""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"]
|