根据审查结果统一以物抵顶的线下核实口径,移除当前模型、初始化脚本和前端接口中的自动折算误导文案。\n补充业务接入必须持有共享 T+ 命名锁的执行契约,明确来源快照稳定性依赖。\nWindows 部署说明区分新 supervisor 与旧任务迁移检查,避免现场并行启动旧 worker。\n\n影响范围:T+ 托管文档、业务接入契约、回款/台账模型注释和参数试算说明。\n\nCo-Authored-By: Claude Code <noreply@anthropic.com>
187 lines
8.2 KiB
Python
187 lines
8.2 KiB
Python
"""绩效核算台账模型(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表示由线下规则处理 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="备注")
|