- TPlusSyncMixin 统一 T+ 同步预留四列(可空+普通索引,唯一键待 IT 定键后升级) - 留痕表实体 ID 无外键冗余固化,删除保护由服务层承担 - init-dev.sql 同步六表 DDL,alembic_version 标记更新 Co-Authored-By: Claude Code <noreply@anthropic.com>
122 lines
5.5 KiB
Python
122 lines
5.5 KiB
Python
"""客户模型:客户档案与归属变更留痕。
|
||
|
||
- 「公司名称 + 负责人」是业务唯一标识(需规 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="创建时间"
|
||
)
|