"""客户评级模型(需求 2.2.5)。 评级从「纯计算器不落库」升级为**每客户一行落库**(2026-10-19 确认): 客户管理页要罗列评级列并按其排序,评分必须可回溯。设计要点: - ``customer_id`` 唯一键 + RESTRICT 外键:一客户一份当前评级(快照语义, 非流水),删除客户由服务层先删评级行再删主行(RESTRICT 兜底并发窗口) - ``grade_name`` 为**快照固化、无外键**:等级映射表仍允许自由增删清空 (rating_service 的既有前提),删等级不能伤及已评级客户的历史结论 —— 与归属留痕「ID/人名固化、不回溯」同一论证口径;等级未命中时为 NULL - ``scorer_id`` 外键 SET NULL + ``scorer_name`` 冗余固化:账号删除后 记录仍可读(同 ownership_change_logs 的操作人口径) - 分值列 DECIMAL(5,1):UI 整数 0-20,DECIMAL 留十进制余地;总分由 **服务端重算**,不采信前端回传 """ from decimal import Decimal from sqlalchemy import ForeignKey, Numeric, String, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from app.models.base import BIGINT_UNSIGNED, Base, TimestampMixin, mysql_table_args class CustomerRating(Base, TimestampMixin): """客户评级表:每客户一行(uk_customer_ratings_customer_id)。 五项各 0-20 分、总分 = 五项之和(0-100),判定规则同等级映射表 (按 sort_order 升序首个 min_score ≤ 总分,见 rating_service.resolve_grade)。 """ __tablename__ = "customer_ratings" __table_args__ = mysql_table_args( "客户评级表(每客户一行;等级名为快照固化无外键,删等级不伤已评级记录)", UniqueConstraint("customer_id", name="uk_customer_ratings_customer_id"), ) id: Mapped[int] = mapped_column( BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID" ) customer_id: Mapped[int] = mapped_column( BIGINT_UNSIGNED, ForeignKey("customers.id", ondelete="RESTRICT"), nullable=False, comment="客户ID(唯一:一客户一份当前评级)", ) score_ship_amount: Mapped[Decimal] = mapped_column( Numeric(5, 1), nullable=False, server_default="0", comment="发货额评分(0-20)" ) score_receipt_rate: Mapped[Decimal] = mapped_column( Numeric(5, 1), nullable=False, server_default="0", comment="回款率评分(0-20)" ) score_profit_rate: Mapped[Decimal] = mapped_column( Numeric(5, 1), nullable=False, server_default="0", comment="毛利率评分(0-20)" ) score_contract_cooperation: Mapped[Decimal] = mapped_column( Numeric(5, 1), nullable=False, server_default="0", comment="合同履约配合能力评分(0-20)" ) score_site_construction: Mapped[Decimal] = mapped_column( Numeric(5, 1), nullable=False, server_default="0", comment="现场规范施工能力评分(0-20)" ) total_score: Mapped[Decimal] = mapped_column( Numeric(5, 1), nullable=False, comment="总分(服务端重算,五项之和 0-100)" ) grade_name: Mapped[str | None] = mapped_column( String(32), comment="评级等级(保存时按等级映射快照固化,无外键;未命中/等级表空为 NULL)", ) remark: Mapped[str | None] = mapped_column(String(255), comment="备注") scorer_id: Mapped[int | None] = mapped_column( BIGINT_UNSIGNED, ForeignKey("users.id", ondelete="SET NULL"), comment="评级操作人ID(账号删除后置空)", ) scorer_name: Mapped[str] = mapped_column( String(64), nullable=False, comment="评级操作人用户名(冗余固化)" )