wip: 客户评级弹窗化/公司信息精简/参数中文化/品牌站点同步(基于 927592b)

This commit is contained in:
郑博文 committed 2026-10-20 11:06:01 +08:00
1 parent 927592b472
commit 27533eb465
43 files changed
+2959 -434

No files matched your search

+39 -10
View File
@@ -6,7 +6,7 @@
from sqlalchemy import ColumnElement, Row, Select, func, or_, select
from sqlalchemy.orm import Session
from app.models import Customer, OwnershipChangeLog, Personnel
from app.models import Customer, CustomerRating, OwnershipChangeLog, Personnel
from app.models.customer import OWNER_TYPE_CUSTOMER
from app.repositories import ownership_log_repository
from app.repositories.query_utils import fuzzy
@@ -31,7 +31,7 @@ def count_business_refs(session: Session, customer_id: int) -> int:
def get_with_owner_name(session: Session, customer_id: int) -> Row | None:
"""查单个客户并带出归属人员姓名(详情、编辑回显与归属变更共用)。"""
"""查单个客户并带出归属人员姓名与评级快照(详情/编辑回显/归属变更共用)。"""
stmt = _base_select().where(Customer.id == customer_id)
return session.execute(stmt).first()
@@ -53,12 +53,14 @@ def list_customers(
keyword: str | None = None,
customer_type: str | None = None,
owner_personnel_id: int | None = None,
order_field: str | None = None,
order_type: str | None = None,
) -> list[Row]:
"""分页查询客户,行内容为 (客户, 归属人员姓名)。
"""分页查询客户,行内容为 (客户, 归属人员姓名, 评级总分, 评级等级)。
归属人员外键非空但统一用 outerjoin:人员行不可考(被物理删除的脏数据)
不应让客户行整个查不出来(同 personnel_repository 的韧性口径)。
排序按 id 倒序:客户无排序字段,新增的靠前符合列表页习惯。
归属人员/评级行均用 outerjoin:关联行不可考(人员被物理删除、客户
未评级)不应让客户行整个查不出来(同 personnel_repository 韧性口径)。
默认排序按 id 倒序:新增的靠前符合列表页习惯;评级排序见 ``_apply_order``。
"""
stmt = _apply_filters(
_base_select(),
@@ -66,7 +68,8 @@ def list_customers(
customer_type=customer_type,
owner_personnel_id=owner_personnel_id,
)
stmt = stmt.order_by(Customer.id.desc()).offset(offset).limit(limit)
stmt = _apply_order(stmt, order_field=order_field, order_type=order_type)
stmt = stmt.offset(offset).limit(limit)
return list(session.execute(stmt).all())
@@ -120,12 +123,38 @@ def count_ownership_logs(session: Session, *, customer_id: int) -> int:
def _base_select() -> Select:
"""客户主表外联归属人员姓名。"""
return select(Customer, Personnel.personnel_name).outerjoin(
Personnel, Customer.owner_personnel_id == Personnel.id
"""客户主表外联归属人员姓名与评级快照(等级名/总分,列表评级列免 N+1)。"""
return (
select(
Customer,
Personnel.personnel_name,
CustomerRating.total_score,
CustomerRating.grade_name,
)
.outerjoin(Personnel, Customer.owner_personnel_id == Personnel.id)
.outerjoin(CustomerRating, CustomerRating.customer_id == Customer.id)
)
def _apply_order(stmt: Select, *, order_field: str | None, order_type: str | None) -> Select:
"""应用列表排序。默认 id 倒序;order_field=rating 按评级总分排序。
等级由总分判定、与总分同向,故按总分即等价按等级排序。MySQL 没有
``NULLS LAST`` 语法,用布尔键 ``total_score IS NULL`` 升序模拟 ——
未评级行恒在末尾,正序/倒序一致;同分以 id 倒序兜底保证次序稳定。
非法字段/方向不落到 SQL(schema Literal 已在入口拦 400),此处静默
回退默认排序作为纵深防御。
"""
if order_field == "rating" and order_type in ("asc", "desc"):
score_order = (
CustomerRating.total_score.asc()
if order_type == "asc"
else CustomerRating.total_score.desc()
)
return stmt.order_by(CustomerRating.total_score.is_(None), score_order, Customer.id.desc())
return stmt.order_by(Customer.id.desc())
def _apply_filters(
stmt: Select,
*,