"""绩效核算台账模型(M3 ④:每笔回款仅核算一次的固化点)。 **append-only 版本行**:重算 = 插入 calc_version+1 的新行,旧行仅把 is_active 翻 0 —— 这是全表唯一的受控 UPDATE(模型注释与 test_models 均已声明例外),行内容永不改写;历史版本可回溯(重算授权留痕的需求口径)。 防重三层:服务层 CAS(UPDATE ... WHERE settlement_status=0,rowcount=0 即 400)+ FOR UPDATE 行锁 + 库级 uk(receipt_id, calc_version, personnel_id) 兜底。uk 含 personnel_id:二次分配分摊(澄清 #1 答复 2026-10-19,分配 绩效——按方案比例拆给参与人、余额归合同负责人)后同一回款同版本按人 各落一行,拆分仍是「一次核算」的组成部分;聚合行 receipt_id 为 NULL, MySQL 唯一索引对 NULL 不去重,天然不约束。 经理聚合行(calc_type=2,receipt_id 为 NULL):月度批处理时按「本团队 业务员绩效 ×12.5% + 全公司 ×5%」落一行;其唯一性由月结生成的 settlement_month 串行化 + 服务层保证(MySQL 无部分唯一索引,docstring 声明即可)。 #12 答复(2026-10-19):经理个人户业绩是否计入聚合基数改为参数 ``manager.personal.included``(默认 0=不计入)——消费点在 settlement_service 的聚合函数。 """ from datetime import date from decimal import Decimal from sqlalchemy import ( JSON, Date, ForeignKey, Index, Numeric, String, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column from app.models.base import BIGINT_UNSIGNED, TINYINT_TYPE, Base, TimestampMixin, mysql_table_args # 核算类型(performance_ledgers.calc_type) CALC_TYPE_RECEIPT = 1 # 单笔回款核算 CALC_TYPE_MANAGER_AGGREGATE = 2 # 经理团队聚合(月度批处理) class PerformanceLedger(Base, TimestampMixin): """绩效核算台账表:核算事实的固化点,报表 2.4.1 的数据源。""" __tablename__ = "performance_ledgers" __table_args__ = mysql_table_args( "绩效核算台账表(append-only 版本行;重算插新版本旧行仅翻 is_active)", # 库级防重兜底:同一回款同版本按人仅一行(分摊拆行后负责人+参与人 # 各占一行;服务层 CAS+行锁之外的第二道闸)。聚合行 receipt_id 为 # NULL,MySQL 唯一索引对 NULL 不去重,天然不约束聚合行 UniqueConstraint( "receipt_id", "calc_version", "personnel_id", "allocation_key", name="uk_performance_ledgers_receipt_id_calc_version_personnel", ), Index("idx_performance_ledgers_month_active", "settlement_month", "is_active"), Index( "idx_performance_ledgers_month_personnel_type", "settlement_month", "personnel_id", "calc_type", ), Index("idx_performance_ledgers_personnel_id", "personnel_id"), Index("idx_performance_ledgers_receipt_id", "receipt_id"), Index("idx_performance_ledgers_business_id", "business_id"), ) id: Mapped[int] = mapped_column( BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID" ) receipt_id: Mapped[int | None] = mapped_column( BIGINT_UNSIGNED, ForeignKey("receipts.id", ondelete="RESTRICT"), comment="回款ID(receipts.id;聚合行可为 NULL)", ) calc_version: Mapped[int] = mapped_column( TINYINT_TYPE, nullable=False, server_default="1", comment="核算版本(重算+1,旧行 is_active=0)", ) allocation_key: Mapped[str] = mapped_column( String(64), nullable=False, server_default="''", comment="正式FIFO事件键;历史单笔核算为空串", ) is_active: Mapped[int] = mapped_column( TINYINT_TYPE, nullable=False, server_default="1", comment="生效标记:1当前生效 0已被重算替代(重算受控 UPDATE 例外)", ) calc_type: Mapped[int] = mapped_column( TINYINT_TYPE, nullable=False, server_default="1", comment="核算类型:1单笔回款 2经理团队聚合(月度)", ) business_id: Mapped[int] = mapped_column( BIGINT_UNSIGNED, comment="业务ID(固化无外键;聚合行为该经理名下首行业务ID,报表下钻锚点)", ) business_no: Mapped[str] = mapped_column( String(32), nullable=False, comment="业务编号(冗余固化,列表免联查)" ) personnel_id: Mapped[int] = mapped_column( BIGINT_UNSIGNED, ForeignKey("personnel.id", ondelete="RESTRICT"), nullable=False, comment="归属营销人员ID(personnel.id;核算归属锚点)", ) personnel_name: Mapped[str] = mapped_column( String(64), nullable=False, comment="归属营销人员姓名(冗余固化)" ) role_code: Mapped[str] = mapped_column( String(32), nullable=False, comment="岗位编码(business_roles.role_code,计提公式锚点)" ) customer_type: Mapped[str] = mapped_column( String(32), nullable=False, comment="客户分类:personal/manager_personal/enterprise" ) product_category: Mapped[str] = mapped_column( String(32), nullable=False, comment="产品类别:municipal_water/rail_transit/mixed(混合业务,行级明细入 snapshot_json)", ) ship_date: Mapped[date | None] = mapped_column(Date, comment="发货日期(回款天数基准)") receipt_date: Mapped[date] = mapped_column( Date, nullable=False, comment="回款日期(参数版本匹配日,裁决 #11)" ) payment_days: Mapped[int | None] = mapped_column( comment="回款天数(发货日首日计第 1 天;未指定批次为 NULL;先款后货照实负值)" ) rate_text: Mapped[str] = mapped_column( String(32), nullable=False, comment="计提比例原文(如 4/360,防 Fraction 截断;聚合行存 12.5%/5% 结构说明)", ) converted_profit: Mapped[Decimal | None] = mapped_column( Numeric(14, 2), comment="折算毛利润额(可负;混合业务为各行汇总)" ) performance_amount: Mapped[Decimal | None] = mapped_column( Numeric(14, 2), comment="绩效金额(可负,裁决:负绩效照算不归零)" ) barter_applied: Mapped[int] = mapped_column( TINYINT_TYPE, nullable=False, server_default="0", comment="以物抵顶折算:1已按 80% 折算 0否", ) weight_r: Mapped[int | None] = mapped_column(comment="支付权重 R(回款占销售额百分比向下取整)") payment_weight: Mapped[Decimal | None] = mapped_column( Numeric(7, 6), comment="支付权重系数 R×R(展示用)" ) payable_amount: Mapped[Decimal | None] = mapped_column( Numeric(14, 2), comment="当期应付绩效(绩效 × 支付权重;聚合行为当月汇总)" ) settlement_month: Mapped[str] = mapped_column( String(7), nullable=False, comment="归属月(回款日期所在月,YYYY-MM;月结与报表维度)" ) is_probation: Mapped[int] = mapped_column( TINYINT_TYPE, nullable=False, server_default="0", comment="试用期:1照算不发放 0否", ) missing_cost: Mapped[int] = mapped_column( TINYINT_TYPE, nullable=False, server_default="0", comment="成本缺失:1有明细行缺成本(该行毛利按 0 计)0否", ) snapshot_json: Mapped[dict | None] = mapped_column( JSON, comment="计算快照(所用参数版本与行级折算明细,重算可回溯)" ) settlement_id: Mapped[int | None] = mapped_column( BIGINT_UNSIGNED, ForeignKey("monthly_settlements.id", ondelete="RESTRICT"), comment="所属月结ID(月结生成时回填;NULL=未入结)", ) calc_reason: Mapped[str | None] = mapped_column( String(255), comment="核算/重算原因(重算必填留痕)" ) remark: Mapped[str | None] = mapped_column(String(255), comment="备注")