"""客户模型:客户档案与归属变更留痕。 - 「公司名称 + 负责人」是业务唯一标识(需规 2.1.3);负责人是客户公司的 负责人**人名**(字符串),不是系统内的营销人员 —— 系统内归属另用 owner_personnel_id 外键表达,两者语义不同 - 归属变更只改主表当前值 + 插一条留痕,**不回溯历史**(对照表 #19): 历史核算结果在 M3 台账已固化,无需 effective_date 时点化设计 - 留痕表(OwnershipChangeLog)对客户/业务/人员 ID 均为**无外键的冗余固化**: 留痕是历史记录,被引用方被删除后记录仍须完整可读;RESTRICT 外键反而会 把「删除客户」逼成 500。业务实体删除保护由服务层检查承担 """ from datetime import datetime from sqlalchemy import DateTime, ForeignKey, Index, String, UniqueConstraint, text from sqlalchemy.orm import Mapped, mapped_column from app.models.base import ( BIGINT_UNSIGNED, TINYINT_TYPE, Base, TimestampMixin, TPlusSyncMixin, mysql_table_args, ) # 归属类型(ownership_change_logs.owner_type) OWNER_TYPE_CUSTOMER = 1 OWNER_TYPE_BUSINESS = 2 class Customer(Base, TimestampMixin, TPlusSyncMixin): """客户表(需求 2.1.3)。customer_type 是计提规则匹配锚点(M3 消费)。""" __tablename__ = "customers" __table_args__ = mysql_table_args( "客户表(公司名称+负责人为业务唯一标识;T+同步不覆盖客户分类)", UniqueConstraint("company_name", "owner_name", name="uk_customers_company_owner"), ) id: Mapped[int] = mapped_column( BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID" ) company_name: Mapped[str] = mapped_column(String(128), nullable=False, comment="公司名称") owner_name: Mapped[str] = mapped_column( String(64), nullable=False, comment="客户负责人姓名(客户公司负责人的人名,非系统人员)" ) contact_person: Mapped[str | None] = mapped_column(String(64), comment="联系人") contact_phone: Mapped[str | None] = mapped_column(String(20), comment="联系电话") address: Mapped[str | None] = mapped_column(String(255), comment="地址") industry: Mapped[str | None] = mapped_column(String(64), comment="所属行业(自由文本)") customer_type: Mapped[str] = mapped_column( String(32), nullable=False, server_default="personal", index=True, comment="客户分类:personal个人户 manager_personal经理个人户 enterprise企业户(计提规则匹配锚点;T+同步不覆盖)", ) owner_personnel_id: Mapped[int | None] = mapped_column( BIGINT_UNSIGNED, ForeignKey("personnel.id", ondelete="RESTRICT"), index=True, comment="归属负责营销人员ID(手动修改不回溯历史,变更留痕见 ownership_change_logs)", ) remark: Mapped[str | None] = mapped_column(String(255), comment="备注") class OwnershipChangeLog(Base): """归属变更记录表(append-only,需求 2.1.3 + 对照表 #19)。 owner_type 区分客户归属/业务归属:1 → customer_id 非空,2 → business_id 非空,由应用层校验(无外键,见模块 docstring 的固化论证)。 前后值冗余固化(无外键):人员被删除后留痕仍可读。 """ __tablename__ = "ownership_change_logs" __table_args__ = mysql_table_args( "归属变更记录表(append-only;客户/业务归属变更留痕,不回溯历史)", Index("idx_ownership_change_logs_customer_id", "customer_id"), Index("idx_ownership_change_logs_business_id", "business_id"), Index("idx_ownership_change_logs_create_time", "create_time"), ) id: Mapped[int] = mapped_column( BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID" ) owner_type: Mapped[int] = mapped_column( TINYINT_TYPE, nullable=False, comment="归属类型:1客户归属 2业务归属" ) customer_id: Mapped[int | None] = mapped_column( BIGINT_UNSIGNED, comment="客户ID(固化,owner_type=1 时非空)", ) business_id: Mapped[int | None] = mapped_column( BIGINT_UNSIGNED, comment="业务ID(固化,owner_type=2 时非空)", ) old_owner_personnel_id: Mapped[int | None] = mapped_column( BIGINT_UNSIGNED, comment="变更前负责营销人员ID(固化)" ) new_owner_personnel_id: Mapped[int | None] = mapped_column( BIGINT_UNSIGNED, comment="变更后负责营销人员ID(固化)" ) old_assistant_personnel_id: Mapped[int | None] = mapped_column( BIGINT_UNSIGNED, comment="变更前协助营销人员ID(固化,仅业务归属使用)" ) new_assistant_personnel_id: Mapped[int | None] = mapped_column( BIGINT_UNSIGNED, comment="变更后协助营销人员ID(固化,仅业务归属使用)" ) change_reason: Mapped[str | None] = mapped_column(String(255), comment="变更原因") operator_id: Mapped[int | None] = mapped_column( BIGINT_UNSIGNED, ForeignKey("users.id", ondelete="SET NULL"), comment="操作人ID(账号删除后置空)", ) operator_name: Mapped[str] = mapped_column( String(64), nullable=False, comment="操作人用户名(冗余固化)" ) create_time: Mapped[datetime] = mapped_column( DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP"), comment="创建时间" )