feat(system): 新增岗位角色与计提参数四表及 V3 迁移

- business_roles/param_definitions/param_change_batches/param_values 四表 + 种子(3 内置岗位/16 参数定义/19 版本条目/初始批次 P20260101-00)
- 双轨迁移:Alembic bfbda9c3191d + db/migration/V3 导出 + init-dev.sql 同步(schema_sync 校验通过)
- 参数值以 Fraction 可解析字符串存储(4/360 禁 0.011111);企业户 2‰→3‰ 双种子支持按到账日回溯

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
liuwenxinandClaude Code committed 2026-10-12 16:13:13 +08:00
1 parent c7212cd93f
commit bc019cadf5
8 files changed
+1375 -1

No files matched your search

@@ -0,0 +1,810 @@
"""新增岗位角色与计提参数四表
Revision ID: bfbda9c3191d
Revises: a45a8087831b
Create Date: 2026-10-12 16:01:48.760620
"""
from collections.abc import Sequence
from datetime import date
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "bfbda9c3191d"
down_revision: str | Sequence[str] | None = "a45a8087831b"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""应用迁移。"""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"business_roles",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column(
"role_code",
sa.String(length=32),
nullable=False,
comment="岗位编码:salesperson营销员 sales_manager营销经理 info_officer信息员,创建后不可修改",
),
sa.Column("role_name", sa.String(length=64), nullable=False, comment="岗位名称"),
sa.Column(
"calc_template",
sa.String(length=32),
nullable=False,
comment="计提公式模板编码(以代码内模板注册表为准)",
),
sa.Column("description", sa.String(length=255), nullable=True, comment="岗位说明"),
sa.Column(
"sort_order", sa.Integer(), server_default="0", nullable=False, comment="显示排序"
),
sa.Column(
"status",
mysql.TINYINT(),
server_default="1",
nullable=False,
comment="状态:1启用 0禁用",
),
sa.Column(
"is_builtin",
mysql.TINYINT(),
server_default="0",
nullable=False,
comment="是否内置岗位:1内置(禁删禁停) 0自定义",
),
sa.Column(
"create_time",
sa.DateTime(),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=False,
comment="创建时间",
),
sa.Column(
"update_time",
sa.DateTime(),
server_default=sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"),
nullable=False,
comment="更新时间",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_business_roles")),
sa.UniqueConstraint("role_code", name=op.f("uk_business_roles_role_code")),
comment="业务岗位角色表(与登录角色roles区分,决定计提公式与参数)",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_table(
"param_definitions",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column(
"param_code",
sa.String(length=64),
nullable=False,
comment="参数编码,如 salesperson.rate.slope",
),
sa.Column("param_name", sa.String(length=64), nullable=False, comment="参数名称"),
sa.Column(
"param_group",
sa.String(length=32),
nullable=False,
comment="参数分组:业务员个人户/经理分成/企业户/信息员/支付环节/产品阈值/月工资口径",
),
sa.Column(
"value_type",
sa.String(length=16),
nullable=False,
comment="值类型:DECIMAL/INT/BOOL/ENUM/TEXT",
),
sa.Column(
"unit", sa.String(length=16), nullable=True, comment="展示单位:‰/%/元/天,无单位为空"
),
sa.Column(
"dimension_type",
sa.String(length=32),
server_default="NONE",
nullable=False,
comment="维度类型:NONE/PRODUCT_CATEGORY/BUSINESS_ROLE",
),
sa.Column(
"options_text",
sa.String(length=255),
nullable=True,
comment="枚举选项(竖线分隔),仅 ENUM 使用",
),
sa.Column(
"default_value",
sa.String(length=64),
nullable=True,
comment="系统默认值(Fraction 可解析字符串)",
),
sa.Column(
"value_min", sa.String(length=64), nullable=True, comment="取值下限(含),校验用"
),
sa.Column(
"value_max", sa.String(length=64), nullable=True, comment="取值上限(含),校验用"
),
sa.Column(
"description", sa.String(length=255), nullable=True, comment="口径说明(配置页展示)"
),
sa.Column(
"sort_order", sa.Integer(), server_default="0", nullable=False, comment="显示排序"
),
sa.Column(
"status",
mysql.TINYINT(),
server_default="1",
nullable=False,
comment="状态:1启用 0停用",
),
sa.Column(
"create_time",
sa.DateTime(),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=False,
comment="创建时间",
),
sa.Column(
"update_time",
sa.DateTime(),
server_default=sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"),
nullable=False,
comment="更新时间",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_param_definitions")),
sa.UniqueConstraint("param_code", name=op.f("uk_param_definitions_param_code")),
comment="计提参数定义表",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_table(
"param_change_batches",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column(
"version_no", sa.String(length=32), nullable=False, comment="版本号,如 P20261012-01"
),
sa.Column("effective_date", sa.Date(), nullable=False, comment="生效日期(业务日期口径)"),
sa.Column(
"change_type",
mysql.TINYINT(),
nullable=False,
comment="生效方式:1立即生效 2指定日期生效",
),
sa.Column("remark", sa.String(length=255), nullable=True, comment="变更说明"),
sa.Column(
"operator_id",
mysql.BIGINT(unsigned=True),
nullable=True,
comment="操作人ID(账号删除后置空)",
),
sa.Column(
"operator_name",
sa.String(length=64),
nullable=False,
comment="操作人用户名(冗余固化)",
),
sa.Column(
"item_count", sa.Integer(), server_default="0", nullable=False, comment="变更参数项数"
),
sa.Column(
"create_time",
sa.DateTime(),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=False,
comment="创建时间",
),
sa.ForeignKeyConstraint(
["operator_id"],
["users.id"],
name=op.f("fk_param_change_batches_operator_id"),
ondelete="SET NULL",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_param_change_batches")),
sa.UniqueConstraint("version_no", name=op.f("uk_param_change_batches_version_no")),
comment="参数变更批次表",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_table(
"param_values",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column(
"param_id",
mysql.BIGINT(unsigned=True),
nullable=False,
comment="参数定义ID(param_definitions.id)",
),
sa.Column(
"dimension_value",
sa.String(length=32),
server_default=sa.text("''"),
nullable=False,
comment="维度取值:NONE维度为空串;产品类别/岗位编码为具体值",
),
sa.Column(
"effective_date", sa.Date(), nullable=False, comment="生效日期(按业务日期时点化取值)"
),
sa.Column(
"value_text",
sa.String(length=64),
nullable=False,
comment="参数值(Fraction 可解析字符串/枚举码/开关)",
),
sa.Column(
"old_value_text", sa.String(length=64), nullable=True, comment="变更前值(留痕)"
),
sa.Column(
"batch_id",
mysql.BIGINT(unsigned=True),
nullable=False,
comment="变更批次ID(param_change_batches.id)",
),
sa.Column(
"create_time",
sa.DateTime(),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=False,
comment="创建时间",
),
sa.ForeignKeyConstraint(
["batch_id"],
["param_change_batches.id"],
name=op.f("fk_param_values_batch_id"),
ondelete="RESTRICT",
),
sa.ForeignKeyConstraint(
["param_id"],
["param_definitions.id"],
name=op.f("fk_param_values_param_id"),
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_param_values")),
comment="参数版本条目表(append-only,按生效日期时点化取值)",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_index("idx_param_values_batch_id", "param_values", ["batch_id"], unique=False)
op.create_index(
"idx_param_values_effective_date", "param_values", ["effective_date"], unique=False
)
op.create_index(
"idx_param_values_param_id_dimension_value_effective_date",
"param_values",
["param_id", "dimension_value", "effective_date"],
unique=False,
)
# ### end Alembic commands ###
# ---- 种子数据(生产与开发同源;口径依据 2026-10-12 业务裁决对照表)----
# 显式指定 id:四张表均为本迁移新建、应用时必为空表,id 稳定可控;
# param_values 按 param_id 引用定义,无法依赖自增(bulk_insert 取不回自增值)。
op.bulk_insert(
sa.table(
"business_roles",
sa.column("id", sa.BigInteger),
sa.column("role_code", sa.String),
sa.column("role_name", sa.String),
sa.column("calc_template", sa.String),
sa.column("description", sa.String),
sa.column("sort_order", sa.Integer),
sa.column("is_builtin", sa.Integer),
),
[
{
"id": 1,
"role_code": "salesperson",
"role_name": "营销员",
"calc_template": "TPL_SALESPERSON_DECAY",
"description": "业务员个人户:回款天数线性递减(10‰ − 天数×4/360‰,361 天起固定 4‰)",
"sort_order": 1,
"is_builtin": 1,
},
{
"id": 2,
"role_code": "sales_manager",
"role_name": "营销经理",
"calc_template": "TPL_MANAGER_SHARE",
"description": "经理绩效:∑本团队业务员绩效×12.5% + ∑全公司业务员绩效×5%",
"sort_order": 2,
"is_builtin": 1,
},
{
"id": 3,
"role_code": "info_officer",
"role_name": "信息员",
"calc_template": "TPL_INFO_SALES_FIXED",
"description": "信息员绩效:本人销售额×1‰",
"sort_order": 3,
"is_builtin": 1,
},
],
)
op.bulk_insert(
sa.table(
"param_change_batches",
sa.column("id", sa.BigInteger),
sa.column("version_no", sa.String),
sa.column("effective_date", sa.Date),
sa.column("change_type", sa.Integer),
sa.column("remark", sa.String),
sa.column("operator_name", sa.String),
sa.column("item_count", sa.Integer),
),
[
{
"id": 1,
"version_no": "P20260101-00",
"effective_date": date(2026, 1, 1),
"change_type": 1,
"remark": "初始版本(系统初始化种子,口径依据 2026-10-12 业务裁决)",
"operator_name": "system",
"item_count": 19,
}
],
)
op.bulk_insert(
sa.table(
"param_definitions",
sa.column("id", sa.BigInteger),
sa.column("param_code", sa.String),
sa.column("param_name", sa.String),
sa.column("param_group", sa.String),
sa.column("value_type", sa.String),
sa.column("unit", sa.String),
sa.column("dimension_type", sa.String),
sa.column("options_text", sa.String),
sa.column("default_value", sa.String),
sa.column("value_min", sa.String),
sa.column("value_max", sa.String),
sa.column("description", sa.String),
sa.column("sort_order", sa.Integer),
),
[
{
"id": 1,
"param_code": "salesperson.rate.start",
"param_name": "业务员起始计提比例",
"param_group": "业务员个人户",
"value_type": "DECIMAL",
"unit": "‰",
"dimension_type": "BUSINESS_ROLE",
"options_text": None,
"default_value": "10",
"value_min": "0",
"value_max": None,
"description": "线性递减段起始值:比例 = 起始 − 回款天数 × 斜率(≤分界天数)",
"sort_order": 1,
},
{
"id": 2,
"param_code": "salesperson.rate.slope",
"param_name": "业务员日递减斜率",
"param_group": "业务员个人户",
"value_type": "DECIMAL",
"unit": "‰/天",
"dimension_type": "BUSINESS_ROLE",
"options_text": None,
"default_value": "4/360",
"value_min": "0",
"value_max": None,
"description": "精确分数 4/360‰/天(=1/90),禁止按 0.011111 截断存储",
"sort_order": 2,
},
{
"id": 3,
"param_code": "salesperson.rate.floor",
"param_name": "业务员计提下限",
"param_group": "业务员个人户",
"value_type": "DECIMAL",
"unit": "‰",
"dimension_type": "BUSINESS_ROLE",
"options_text": None,
"default_value": "4",
"value_min": "0",
"value_max": None,
"description": "回款天数超过分界天数后固定取此比例(≥361 天 = 4‰)",
"sort_order": 3,
},
{
"id": 4,
"param_code": "salesperson.days.boundary",
"param_name": "线性递减分界天数",
"param_group": "业务员个人户",
"value_type": "INT",
"unit": "天",
"dimension_type": "BUSINESS_ROLE",
"options_text": None,
"default_value": "360",
"value_min": "0",
"value_max": None,
"description": "≤分界天数按公式线性递减(360 天=6‰);超过则取下限",
"sort_order": 4,
},
{
"id": 5,
"param_code": "manager.share.team",
"param_name": "经理本团队分成比例",
"param_group": "经理分成",
"value_type": "DECIMAL",
"unit": "%",
"dimension_type": "BUSINESS_ROLE",
"options_text": None,
"default_value": "12.5",
"value_min": "0",
"value_max": "100",
"description": "∑本人带领业务员绩效 × 此比例(限本团队)",
"sort_order": 5,
},
{
"id": 6,
"param_code": "manager.share.company",
"param_name": "经理全公司分成比例",
"param_group": "经理分成",
"value_type": "DECIMAL",
"unit": "%",
"dimension_type": "BUSINESS_ROLE",
"options_text": None,
"default_value": "5",
"value_min": "0",
"value_max": "100",
"description": "∑全部业务员绩效 × 此比例(全公司)",
"sort_order": 6,
},
{
"id": 7,
"param_code": "enterprise.rate",
"param_name": "企业户计提比例",
"param_group": "企业户",
"value_type": "DECIMAL",
"unit": "‰",
"dimension_type": "PRODUCT_CATEGORY",
"options_text": None,
"default_value": "3",
"value_min": "0",
"value_max": None,
"description": "所有企业户适用;2026-08-13 起由 2‰ 提高至 3‰",
"sort_order": 7,
},
{
"id": 8,
"param_code": "info.rate",
"param_name": "信息员计提比例",
"param_group": "信息员",
"value_type": "DECIMAL",
"unit": "‰",
"dimension_type": "BUSINESS_ROLE",
"options_text": None,
"default_value": "1",
"value_min": "0",
"value_max": None,
"description": "信息员绩效 = 本人销售额 × 此比例",
"sort_order": 8,
},
{
"id": 9,
"param_code": "prepay.rate",
"param_name": "预付款计提比例",
"param_group": "支付环节",
"value_type": "DECIMAL",
"unit": "‰",
"dimension_type": "NONE",
"options_text": None,
"default_value": "10",
"value_min": "0",
"value_max": None,
"description": "预付款统一按此比例计提",
"sort_order": 9,
},
{
"id": 10,
"param_code": "barter.discount",
"param_name": "以物抵顶折算比例",
"param_group": "支付环节",
"value_type": "DECIMAL",
"unit": "%",
"dimension_type": "NONE",
"options_text": None,
"default_value": "80",
"value_min": "0",
"value_max": "100",
"description": "以物抵顶回款绩效按应发绩效的此比例折算",
"sort_order": 10,
},
{
"id": 11,
"param_code": "payment.start.ratio",
"param_name": "支付起付比例",
"param_group": "支付环节",
"value_type": "DECIMAL",
"unit": "%",
"dimension_type": "NONE",
"options_text": None,
"default_value": "60",
"value_min": "0",
"value_max": "100",
"description": "单项业务回款额达销售额(不含运费)此比例后起付;达标前累计",
"sort_order": 11,
},
{
"id": 12,
"param_code": "payment.weight.mode",
"param_name": "支付权重应用方式",
"param_group": "支付环节",
"value_type": "ENUM",
"unit": None,
"dimension_type": "NONE",
"options_text": "FULL|BALANCE",
"default_value": "FULL",
"value_min": None,
"value_max": None,
"description": "FULL=按 R×R 乘当月全额应发绩效;BALANCE=乘余额(语义待 M3 前确认)",
"sort_order": 12,
},
{
"id": 13,
"param_code": "product.threshold",
"param_name": "折算毛利润额阈值",
"param_group": "产品阈值",
"value_type": "DECIMAL",
"unit": "%",
"dimension_type": "PRODUCT_CATEGORY",
"options_text": None,
"default_value": "30",
"value_min": "0",
"value_max": "100",
"description": "折算毛利润额 = 销售额 ×(毛利润率 − 阈值);市政水利 30%、轨交 20%",
"sort_order": 13,
},
{
"id": 14,
"param_code": "salary.basic.mode",
"param_name": "月工资基本工资口径",
"param_group": "月工资口径",
"value_type": "ENUM",
"unit": None,
"dimension_type": "BUSINESS_ROLE",
"options_text": "INCLUDE|DEDUCT|EXCLUDE",
"default_value": "INCLUDE",
"value_min": None,
"value_max": None,
"description": "INCLUDE含基本工资 / DEDUCT扣除基本工资 / EXCLUDE不计",
"sort_order": 14,
},
{
"id": 15,
"param_code": "probation.annual.threshold",
"param_name": "试用期年累计结算阈值",
"param_group": "月工资口径",
"value_type": "DECIMAL",
"unit": "元",
"dimension_type": "NONE",
"options_text": None,
"default_value": "18000",
"value_min": "0",
"value_max": None,
"description": "试用期年累计绩效超过此值部分年底一次性结算",
"sort_order": 15,
},
{
"id": 16,
"param_code": "probation.salary.basic",
"param_name": "试用期基本工资标准",
"param_group": "月工资口径",
"value_type": "DECIMAL",
"unit": "元",
"dimension_type": "NONE",
"options_text": None,
"default_value": "3000",
"value_min": "0",
"value_max": None,
"description": "试用期仅发基本工资、不发放绩效(归属待 M2 复核)",
"sort_order": 16,
},
],
)
op.bulk_insert(
sa.table(
"param_values",
sa.column("id", sa.BigInteger),
sa.column("param_id", sa.BigInteger),
sa.column("dimension_value", sa.String),
sa.column("effective_date", sa.Date),
sa.column("value_text", sa.String),
sa.column("batch_id", sa.BigInteger),
),
[
# 业务员个人户(2026-02-17 折算公式执行日起)
{
"id": 1,
"param_id": 1,
"dimension_value": "salesperson",
"effective_date": date(2026, 2, 17),
"value_text": "10",
"batch_id": 1,
},
{
"id": 2,
"param_id": 2,
"dimension_value": "salesperson",
"effective_date": date(2026, 2, 17),
"value_text": "4/360",
"batch_id": 1,
},
{
"id": 3,
"param_id": 3,
"dimension_value": "salesperson",
"effective_date": date(2026, 2, 17),
"value_text": "4",
"batch_id": 1,
},
{
"id": 4,
"param_id": 4,
"dimension_value": "salesperson",
"effective_date": date(2026, 2, 17),
"value_text": "360",
"batch_id": 1,
},
# 经理与信息员(2026-08-13 执行日起)
{
"id": 5,
"param_id": 5,
"dimension_value": "sales_manager",
"effective_date": date(2026, 8, 13),
"value_text": "12.5",
"batch_id": 1,
},
{
"id": 6,
"param_id": 6,
"dimension_value": "sales_manager",
"effective_date": date(2026, 8, 13),
"value_text": "5",
"batch_id": 1,
},
{
"id": 7,
"param_id": 7,
"dimension_value": "",
"effective_date": date(2026, 2, 17),
"value_text": "2",
"batch_id": 1,
},
{
"id": 8,
"param_id": 7,
"dimension_value": "",
"effective_date": date(2026, 8, 13),
"value_text": "3",
"batch_id": 1,
},
{
"id": 9,
"param_id": 8,
"dimension_value": "info_officer",
"effective_date": date(2026, 8, 13),
"value_text": "1",
"batch_id": 1,
},
# 支付环节与通用口径(需规默认值,无历史冲突)
{
"id": 10,
"param_id": 9,
"dimension_value": "",
"effective_date": date(2026, 1, 1),
"value_text": "10",
"batch_id": 1,
},
{
"id": 11,
"param_id": 10,
"dimension_value": "",
"effective_date": date(2026, 1, 1),
"value_text": "80",
"batch_id": 1,
},
{
"id": 12,
"param_id": 11,
"dimension_value": "",
"effective_date": date(2026, 1, 1),
"value_text": "60",
"batch_id": 1,
},
{
"id": 13,
"param_id": 12,
"dimension_value": "",
"effective_date": date(2026, 1, 1),
"value_text": "FULL",
"batch_id": 1,
},
# 产品阈值(按产品类别维度)
{
"id": 14,
"param_id": 13,
"dimension_value": "municipal_water",
"effective_date": date(2026, 2, 17),
"value_text": "30",
"batch_id": 1,
},
{
"id": 15,
"param_id": 13,
"dimension_value": "rail_transit",
"effective_date": date(2026, 2, 17),
"value_text": "20",
"batch_id": 1,
},
# 月工资口径(按岗位维度)
{
"id": 16,
"param_id": 14,
"dimension_value": "salesperson",
"effective_date": date(2026, 1, 1),
"value_text": "INCLUDE",
"batch_id": 1,
},
{
"id": 17,
"param_id": 14,
"dimension_value": "sales_manager",
"effective_date": date(2026, 1, 1),
"value_text": "DEDUCT",
"batch_id": 1,
},
{
"id": 18,
"param_id": 15,
"dimension_value": "",
"effective_date": date(2026, 1, 1),
"value_text": "18000",
"batch_id": 1,
},
{
"id": 19,
"param_id": 16,
"dimension_value": "",
"effective_date": date(2026, 1, 1),
"value_text": "3000",
"batch_id": 1,
},
],
)
def downgrade() -> None:
"""回滚迁移。
**注意(autogenerate 不会替你处理)**:生成的 `op.drop_index` 与
`op.drop_table` 顺序需人工检查 —— 若某索引是外键的唯一支撑索引,
单独 drop_index 会报 MySQL 1553(needed in a foreign key constraint)。
**整表回滚应只保留 `op.drop_table`**,DROP TABLE 会连带删除索引与外键。
"""
# 整表回滚只保留 drop_table —— DROP TABLE 会连带删除索引与外键。
# 原 autogenerate 输出的逐个 drop_index 会失败:组合索引最左列 param_id 与
# idx_param_values_batch_id 分别是两个外键的唯一支撑索引(MySQL errno 1553)。
# 删除顺序:先删引用方 param_values,再删被引用方。
op.drop_table("param_values")
op.drop_table("param_change_batches")
op.drop_table("param_definitions")
op.drop_table("business_roles")
+6
View File
@@ -10,15 +10,21 @@
""" """
from app.models.base import Base, TimestampMixin from app.models.base import Base, TimestampMixin
from app.models.business_role import BusinessRole
from app.models.login_log import LoginLog from app.models.login_log import LoginLog
from app.models.operation_log import OperationLog from app.models.operation_log import OperationLog
from app.models.param import ParamChangeBatch, ParamDefinition, ParamValue
from app.models.role import Permission, Role, RolePermission from app.models.role import Permission, Role, RolePermission
from app.models.user import User, UserRole from app.models.user import User, UserRole
__all__ = [ __all__ = [
"Base", "Base",
"BusinessRole",
"LoginLog", "LoginLog",
"OperationLog", "OperationLog",
"ParamChangeBatch",
"ParamDefinition",
"ParamValue",
"Permission", "Permission",
"Role", "Role",
"RolePermission", "RolePermission",
+46
View File
@@ -0,0 +1,46 @@
"""业务岗位角色模型。
与系统登录角色(`roles`,权限维度)严格区分:本表是**业务岗位**维度,
决定人员适用的计提公式模板与参数。需求 #28 要求岗位角色可增删、
新增岗位时在系统设置中绑定公式与参数(无需改代码),故模板编码
存于本表、公式实现留在代码内注册表(一期不引入规则引擎)。
"""
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import BIGINT_UNSIGNED, TINYINT_TYPE, Base, TimestampMixin, mysql_table_args
class BusinessRole(Base, TimestampMixin):
"""业务岗位角色表。内置三个岗位(营销员/营销经理/信息员)随迁移初始化。"""
__tablename__ = "business_roles"
__table_args__ = mysql_table_args("业务岗位角色表(与登录角色roles区分,决定计提公式与参数)")
id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
)
role_code: Mapped[str] = mapped_column(
String(32),
nullable=False,
unique=True,
comment="岗位编码:salesperson营销员 sales_manager营销经理 info_officer信息员,创建后不可修改",
)
role_name: Mapped[str] = mapped_column(String(64), nullable=False, comment="岗位名称")
calc_template: Mapped[str] = mapped_column(
String(32), nullable=False, comment="计提公式模板编码(以代码内模板注册表为准)"
)
description: Mapped[str | None] = mapped_column(String(255), comment="岗位说明")
sort_order: Mapped[int] = mapped_column(
Integer, nullable=False, server_default="0", comment="显示排序"
)
status: Mapped[int] = mapped_column(
TINYINT_TYPE, nullable=False, server_default="1", comment="状态:1启用 0禁用"
)
is_builtin: Mapped[int] = mapped_column(
TINYINT_TYPE,
nullable=False,
server_default="0",
comment="是否内置岗位:1内置(禁删禁停) 0自定义",
)
+159
View File
@@ -0,0 +1,159 @@
"""计提参数模型:定义、变更批次与版本条目。
存储设计(M1 评审定稿):
- 参数值按**生效日期时点化**存储(逐项版本条目,append-only),核算按业务
日期回溯取值(对照表 #11:按回款到账日匹配参数版本),禁读「当前值」算历史
- 「版本」由变更批次承载:一次保存 = 一个批次 = 一条变更记录;条目逐项留痕
(旧值/新值),满足 #28 版本管理与留痕(需规 2.3.1 四)
- 值一律为 Fraction 可解析字符串(如 "4/360",禁 0.011111),解析不经 float
- **不设 (param_id, dimension_value, effective_date) 唯一键**:同日多次修正
是合法留痕,解析取 (effective_date, id) 最新一条;唯一键会把同日二次变更
逼成覆盖写、丢失中间值
- 批次与条目均为 append-only(仅 create_time、无 update_time,同 login_logs
先例):带 ON UPDATE 的列会让「未改也被刷新」造成留痕假象
"""
from datetime import date, datetime
from sqlalchemy import Date, DateTime, ForeignKey, Index, Integer, String, text
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import BIGINT_UNSIGNED, TINYINT_TYPE, Base, TimestampMixin, mysql_table_args
class ParamDefinition(Base, TimestampMixin):
"""计提参数定义表。目录元数据(类型/单位/维度/取值域),随迁移种子初始化。"""
__tablename__ = "param_definitions"
__table_args__ = mysql_table_args("计提参数定义表")
id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
)
param_code: Mapped[str] = mapped_column(
String(64), nullable=False, unique=True, comment="参数编码,如 salesperson.rate.slope"
)
param_name: Mapped[str] = mapped_column(String(64), nullable=False, comment="参数名称")
param_group: Mapped[str] = mapped_column(
String(32),
nullable=False,
comment="参数分组:业务员个人户/经理分成/企业户/信息员/支付环节/产品阈值/月工资口径",
)
value_type: Mapped[str] = mapped_column(
String(16), nullable=False, comment="值类型:DECIMAL/INT/BOOL/ENUM/TEXT"
)
unit: Mapped[str | None] = mapped_column(String(16), comment="展示单位:‰/%/元/天,无单位为空")
dimension_type: Mapped[str] = mapped_column(
String(32),
nullable=False,
server_default="NONE",
comment="维度类型:NONE/PRODUCT_CATEGORY/BUSINESS_ROLE",
)
options_text: Mapped[str | None] = mapped_column(
String(255), comment="枚举选项(竖线分隔),仅 ENUM 使用"
)
default_value: Mapped[str | None] = mapped_column(
String(64), comment="系统默认值(Fraction 可解析字符串)"
)
value_min: Mapped[str | None] = mapped_column(String(64), comment="取值下限(含),校验用")
value_max: Mapped[str | None] = mapped_column(String(64), comment="取值上限(含),校验用")
description: Mapped[str | None] = mapped_column(String(255), comment="口径说明(配置页展示)")
sort_order: Mapped[int] = mapped_column(
Integer, nullable=False, server_default="0", comment="显示排序"
)
status: Mapped[int] = mapped_column(
TINYINT_TYPE, nullable=False, server_default="1", comment="状态:1启用 0停用"
)
class ParamChangeBatch(Base):
"""参数变更批次表(append-only)。
一次提交 = 一个批次;版本号(如 P20261012-01)供对比与追溯引用。
未生效批次可整批撤回(删批次与条目);已生效条目禁改禁删。
"""
__tablename__ = "param_change_batches"
__table_args__ = mysql_table_args("参数变更批次表")
id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
)
version_no: Mapped[str] = mapped_column(
String(32), nullable=False, unique=True, comment="版本号,如 P20261012-01"
)
effective_date: Mapped[date] = mapped_column(
Date, nullable=False, comment="生效日期(业务日期口径)"
)
change_type: Mapped[int] = mapped_column(
TINYINT_TYPE, nullable=False, comment="生效方式:1立即生效 2指定日期生效"
)
remark: 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="操作人用户名(冗余固化)"
)
item_count: Mapped[int] = mapped_column(
Integer, nullable=False, server_default="0", comment="变更参数项数"
)
create_time: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP"), comment="创建时间"
)
class ParamValue(Base):
"""参数版本条目表(append-only)。
解析规则:按 (param_id, dimension_value) 取 effective_date <= 业务日期 的
ORDER BY effective_date DESC, id DESC 首条(同日多次变更取最新)。
dimension_value 用空串而非 NULL:MySQL 组合索引对 NULL 不去重,空串语义更稳。
"""
__tablename__ = "param_values"
__table_args__ = mysql_table_args(
"参数版本条目表(append-only,按生效日期时点化取值)",
Index(
"idx_param_values_param_id_dimension_value_effective_date",
"param_id",
"dimension_value",
"effective_date",
),
Index("idx_param_values_effective_date", "effective_date"),
Index("idx_param_values_batch_id", "batch_id"),
)
id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
)
param_id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED,
ForeignKey("param_definitions.id", ondelete="RESTRICT"),
nullable=False,
comment="参数定义ID(param_definitions.id)",
)
dimension_value: Mapped[str] = mapped_column(
String(32),
nullable=False,
server_default=text("''"),
comment="维度取值:NONE维度为空串;产品类别/岗位编码为具体值",
)
effective_date: Mapped[date] = mapped_column(
Date, nullable=False, comment="生效日期(按业务日期时点化取值)"
)
value_text: Mapped[str] = mapped_column(
String(64), nullable=False, comment="参数值(Fraction 可解析字符串/枚举码/开关)"
)
old_value_text: Mapped[str | None] = mapped_column(String(64), comment="变更前值(留痕)")
batch_id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED,
ForeignKey("param_change_batches.id", ondelete="RESTRICT"),
nullable=False,
comment="变更批次ID(param_change_batches.id)",
)
create_time: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP"), comment="创建时间"
)
+4
View File
@@ -19,6 +19,10 @@ EXPECTED_TABLES = {
"role_permissions", "role_permissions",
"login_logs", "login_logs",
"operation_logs", "operation_logs",
"business_roles",
"param_definitions",
"param_change_batches",
"param_values",
} }
+139 -1
View File
@@ -16,6 +16,10 @@
SET NAMES utf8mb4; SET NAMES utf8mb4;
-- 删除顺序遵循外键依赖:先删子表 -- 删除顺序遵循外键依赖:先删子表
DROP TABLE IF EXISTS `param_values`;
DROP TABLE IF EXISTS `param_change_batches`;
DROP TABLE IF EXISTS `param_definitions`;
DROP TABLE IF EXISTS `business_roles`;
DROP TABLE IF EXISTS `operation_logs`; DROP TABLE IF EXISTS `operation_logs`;
DROP TABLE IF EXISTS `role_permissions`; DROP TABLE IF EXISTS `role_permissions`;
DROP TABLE IF EXISTS `user_roles`; DROP TABLE IF EXISTS `user_roles`;
@@ -167,6 +171,89 @@ CREATE INDEX idx_operation_logs_user_id ON operation_logs (user_id);
CREATE INDEX idx_operation_logs_username ON operation_logs (username); CREATE INDEX idx_operation_logs_username ON operation_logs (username);
-- ---------------------------------------------------------------------------
-- 业务岗位角色表(V3 迁移)
-- ---------------------------------------------------------------------------
CREATE TABLE business_roles (
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
role_code VARCHAR(32) NOT NULL COMMENT '岗位编码:salesperson营销员 sales_manager营销经理 info_officer信息员,创建后不可修改',
role_name VARCHAR(64) NOT NULL COMMENT '岗位名称',
calc_template VARCHAR(32) NOT NULL COMMENT '计提公式模板编码(以代码内模板注册表为准)',
description VARCHAR(255) COMMENT '岗位说明',
sort_order INTEGER NOT NULL COMMENT '显示排序' DEFAULT '0',
status TINYINT NOT NULL COMMENT '状态:1启用 0禁用' DEFAULT '1',
is_builtin TINYINT NOT NULL COMMENT '是否内置岗位:1内置(禁删禁停) 0自定义' DEFAULT '0',
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT pk_business_roles PRIMARY KEY (id),
CONSTRAINT uk_business_roles_role_code UNIQUE (role_code)
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='业务岗位角色表(与登录角色roles区分,决定计提公式与参数)' COLLATE utf8mb4_general_ci;
-- ---------------------------------------------------------------------------
-- 计提参数定义表(V3 迁移)
-- ---------------------------------------------------------------------------
CREATE TABLE param_definitions (
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
param_code VARCHAR(64) NOT NULL COMMENT '参数编码,如 salesperson.rate.slope',
param_name VARCHAR(64) NOT NULL COMMENT '参数名称',
param_group VARCHAR(32) NOT NULL COMMENT '参数分组:业务员个人户/经理分成/企业户/信息员/支付环节/产品阈值/月工资口径',
value_type VARCHAR(16) NOT NULL COMMENT '值类型:DECIMAL/INT/BOOL/ENUM/TEXT',
unit VARCHAR(16) COMMENT '展示单位:‰/%/元/天,无单位为空',
dimension_type VARCHAR(32) NOT NULL COMMENT '维度类型:NONE/PRODUCT_CATEGORY/BUSINESS_ROLE' DEFAULT 'NONE',
options_text VARCHAR(255) COMMENT '枚举选项(竖线分隔),仅 ENUM 使用',
default_value VARCHAR(64) COMMENT '系统默认值(Fraction 可解析字符串)',
value_min VARCHAR(64) COMMENT '取值下限(含),校验用',
value_max VARCHAR(64) COMMENT '取值上限(含),校验用',
description VARCHAR(255) COMMENT '口径说明(配置页展示)',
sort_order INTEGER NOT NULL COMMENT '显示排序' DEFAULT '0',
status TINYINT NOT NULL COMMENT '状态:1启用 0停用' DEFAULT '1',
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT pk_param_definitions PRIMARY KEY (id),
CONSTRAINT uk_param_definitions_param_code UNIQUE (param_code)
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='计提参数定义表' COLLATE utf8mb4_general_ci;
-- ---------------------------------------------------------------------------
-- 参数变更批次表(V3 迁移)
-- ---------------------------------------------------------------------------
CREATE TABLE param_change_batches (
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
version_no VARCHAR(32) NOT NULL COMMENT '版本号,如 P20261012-01',
effective_date DATE NOT NULL COMMENT '生效日期(业务日期口径)',
change_type TINYINT NOT NULL COMMENT '生效方式:1立即生效 2指定日期生效',
remark VARCHAR(255) COMMENT '变更说明',
operator_id BIGINT UNSIGNED COMMENT '操作人ID(账号删除后置空)',
operator_name VARCHAR(64) NOT NULL COMMENT '操作人用户名(冗余固化)',
item_count INTEGER NOT NULL COMMENT '变更参数项数' DEFAULT '0',
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_param_change_batches PRIMARY KEY (id),
CONSTRAINT fk_param_change_batches_operator_id FOREIGN KEY(operator_id) REFERENCES users (id) ON DELETE SET NULL,
CONSTRAINT uk_param_change_batches_version_no UNIQUE (version_no)
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='参数变更批次表' COLLATE utf8mb4_general_ci;
-- ---------------------------------------------------------------------------
-- 参数版本条目表(V3 迁移,append-only)
-- ---------------------------------------------------------------------------
CREATE TABLE param_values (
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
param_id BIGINT UNSIGNED NOT NULL COMMENT '参数定义ID(param_definitions.id)',
dimension_value VARCHAR(32) NOT NULL COMMENT '维度取值:NONE维度为空串;产品类别/岗位编码为具体值' DEFAULT '',
effective_date DATE NOT NULL COMMENT '生效日期(按业务日期时点化取值)',
value_text VARCHAR(64) NOT NULL COMMENT '参数值(Fraction 可解析字符串/枚举码/开关)',
old_value_text VARCHAR(64) COMMENT '变更前值(留痕)',
batch_id BIGINT UNSIGNED NOT NULL COMMENT '变更批次ID(param_change_batches.id)',
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_param_values PRIMARY KEY (id),
CONSTRAINT fk_param_values_batch_id FOREIGN KEY(batch_id) REFERENCES param_change_batches (id) ON DELETE RESTRICT,
CONSTRAINT fk_param_values_param_id FOREIGN KEY(param_id) REFERENCES param_definitions (id) ON DELETE RESTRICT
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='参数版本条目表(append-only,按生效日期时点化取值)' COLLATE utf8mb4_general_ci;
CREATE INDEX idx_param_values_batch_id ON param_values (batch_id);
CREATE INDEX idx_param_values_effective_date ON param_values (effective_date);
CREATE INDEX idx_param_values_param_id_dimension_value_effective_date ON param_values (param_id, dimension_value, effective_date);
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
-- 字典数据:角色(与 V1 迁移中的初始化数据一致) -- 字典数据:角色(与 V1 迁移中的初始化数据一致)
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
@@ -174,6 +261,57 @@ INSERT INTO roles (role_code, role_name, description, sort_order) VALUES ('admin
INSERT INTO roles (role_code, role_name, description, sort_order) VALUES ('user', '普通用户', '业务操作', 2); INSERT INTO roles (role_code, role_name, description, sort_order) VALUES ('user', '普通用户', '业务操作', 2);
-- ---------------------------------------------------------------------------
-- 字典数据:业务岗位与计提参数(与 V3 迁移种子一致;口径依据 2026-10-12 业务裁决)
-- 显式指定 id:param_values 按 param_id 引用定义,与迁移种子保持同源一致
-- ---------------------------------------------------------------------------
INSERT INTO business_roles (id, role_code, role_name, calc_template, description, sort_order, is_builtin) VALUES
(1, 'salesperson', '营销员', 'TPL_SALESPERSON_DECAY', '业务员个人户:回款天数线性递减(10‰ − 天数×4/360‰,361 天起固定 4‰)', 1, 1),
(2, 'sales_manager', '营销经理', 'TPL_MANAGER_SHARE', '经理绩效:∑本团队业务员绩效×12.5% + ∑全公司业务员绩效×5%', 2, 1),
(3, 'info_officer', '信息员', 'TPL_INFO_SALES_FIXED', '信息员绩效:本人销售额×1‰', 3, 1);
INSERT INTO param_change_batches (id, version_no, effective_date, change_type, remark, operator_name, item_count) VALUES
(1, 'P20260101-00', '2026-01-01', 1, '初始版本(系统初始化种子,口径依据 2026-10-12 业务裁决)', 'system', 19);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES
(1, 'salesperson.rate.start', '业务员起始计提比例', '业务员个人户', 'DECIMAL', '‰', 'BUSINESS_ROLE', NULL, '10', '0', NULL, '线性递减段起始值:比例 = 起始 − 回款天数 × 斜率(≤分界天数)', 1),
(2, 'salesperson.rate.slope', '业务员日递减斜率', '业务员个人户', 'DECIMAL', '‰/天', 'BUSINESS_ROLE', NULL, '4/360', '0', NULL, '精确分数 4/360‰/天(=1/90),禁止按 0.011111 截断存储', 2),
(3, 'salesperson.rate.floor', '业务员计提下限', '业务员个人户', 'DECIMAL', '‰', 'BUSINESS_ROLE', NULL, '4', '0', NULL, '回款天数超过分界天数后固定取此比例(≥361 天 = 4‰)', 3),
(4, 'salesperson.days.boundary', '线性递减分界天数', '业务员个人户', 'INT', '天', 'BUSINESS_ROLE', NULL, '360', '0', NULL, '≤分界天数按公式线性递减(360 天=6‰);超过则取下限', 4),
(5, 'manager.share.team', '经理本团队分成比例', '经理分成', 'DECIMAL', '%', 'BUSINESS_ROLE', NULL, '12.5', '0', '100', '∑本人带领业务员绩效 × 此比例(限本团队)', 5),
(6, 'manager.share.company', '经理全公司分成比例', '经理分成', 'DECIMAL', '%', 'BUSINESS_ROLE', NULL, '5', '0', '100', '∑全部业务员绩效 × 此比例(全公司)', 6),
(7, 'enterprise.rate', '企业户计提比例', '企业户', 'DECIMAL', '‰', 'PRODUCT_CATEGORY', NULL, '3', '0', NULL, '所有企业户适用;2026-08-13 起由 2‰ 提高至 3‰', 7),
(8, 'info.rate', '信息员计提比例', '信息员', 'DECIMAL', '‰', 'BUSINESS_ROLE', NULL, '1', '0', NULL, '信息员绩效 = 本人销售额 × 此比例', 8),
(9, 'prepay.rate', '预付款计提比例', '支付环节', 'DECIMAL', '‰', 'NONE', NULL, '10', '0', NULL, '预付款统一按此比例计提', 9),
(10, 'barter.discount', '以物抵顶折算比例', '支付环节', 'DECIMAL', '%', 'NONE', NULL, '80', '0', '100', '以物抵顶回款绩效按应发绩效的此比例折算', 10),
(11, 'payment.start.ratio', '支付起付比例', '支付环节', 'DECIMAL', '%', 'NONE', NULL, '60', '0', '100', '单项业务回款额达销售额(不含运费)此比例后起付;达标前累计', 11),
(12, 'payment.weight.mode', '支付权重应用方式', '支付环节', 'ENUM', NULL, 'NONE', 'FULL|BALANCE', 'FULL', NULL, NULL, 'FULL=按 R×R 乘当月全额应发绩效;BALANCE=乘余额(语义待 M3 前确认)', 12),
(13, 'product.threshold', '折算毛利润额阈值', '产品阈值', 'DECIMAL', '%', 'PRODUCT_CATEGORY', NULL, '30', '0', '100', '折算毛利润额 = 销售额 ×(毛利润率 − 阈值);市政水利 30%、轨交 20%', 13),
(14, 'salary.basic.mode', '月工资基本工资口径', '月工资口径', 'ENUM', NULL, 'BUSINESS_ROLE', 'INCLUDE|DEDUCT|EXCLUDE', 'INCLUDE', NULL, NULL, 'INCLUDE含基本工资 / DEDUCT扣除基本工资 / EXCLUDE不计', 14),
(15, 'probation.annual.threshold', '试用期年累计结算阈值', '月工资口径', 'DECIMAL', '元', 'NONE', NULL, '18000', '0', NULL, '试用期年累计绩效超过此值部分年底一次性结算', 15),
(16, 'probation.salary.basic', '试用期基本工资标准', '月工资口径', 'DECIMAL', '元', 'NONE', NULL, '3000', '0', NULL, '试用期仅发基本工资、不发放绩效(归属待 M2 复核)', 16);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES
(1, 1, 'salesperson', '2026-02-17', '10', 1),
(2, 2, 'salesperson', '2026-02-17', '4/360', 1),
(3, 3, 'salesperson', '2026-02-17', '4', 1),
(4, 4, 'salesperson', '2026-02-17', '360', 1),
(5, 5, 'sales_manager', '2026-08-13', '12.5', 1),
(6, 6, 'sales_manager', '2026-08-13', '5', 1),
(7, 7, '', '2026-02-17', '2', 1),
(8, 7, '', '2026-08-13', '3', 1),
(9, 8, 'info_officer', '2026-08-13', '1', 1),
(10, 9, '', '2026-01-01', '10', 1),
(11, 10, '', '2026-01-01', '80', 1),
(12, 11, '', '2026-01-01', '60', 1),
(13, 12, '', '2026-01-01', 'FULL', 1),
(14, 13, 'municipal_water', '2026-02-17', '30', 1),
(15, 13, 'rail_transit', '2026-02-17', '20', 1),
(16, 14, 'salesperson', '2026-01-01', 'INCLUDE', 1),
(17, 14, 'sales_manager', '2026-01-01', 'DEDUCT', 1),
(18, 15, '', '2026-01-01', '18000', 1),
(19, 16, '', '2026-01-01', '3000', 1);
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
-- 迁移版本记录:须等于当前 head,使 alembic upgrade head 成为空操作 -- 迁移版本记录:须等于当前 head,使 alembic upgrade head 成为空操作
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
@@ -182,4 +320,4 @@ CREATE TABLE alembic_version (
CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num) CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)
)CHARSET=utf8mb4 ENGINE=InnoDB COLLATE utf8mb4_general_ci; )CHARSET=utf8mb4 ENGINE=InnoDB COLLATE utf8mb4_general_ci;
INSERT INTO alembic_version (version_num) VALUES ('a45a8087831b'); INSERT INTO alembic_version (version_num) VALUES ('bfbda9c3191d');
@@ -0,0 +1,153 @@
-- Running upgrade a45a8087831b -> bfbda9c3191d
CREATE TABLE business_roles (
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
role_code VARCHAR(32) NOT NULL COMMENT '岗位编码:salesperson营销员 sales_manager营销经理 info_officer信息员,创建后不可修改',
role_name VARCHAR(64) NOT NULL COMMENT '岗位名称',
calc_template VARCHAR(32) NOT NULL COMMENT '计提公式模板编码(以代码内模板注册表为准)',
description VARCHAR(255) COMMENT '岗位说明',
sort_order INTEGER NOT NULL COMMENT '显示排序' DEFAULT '0',
status TINYINT NOT NULL COMMENT '状态:1启用 0禁用' DEFAULT '1',
is_builtin TINYINT NOT NULL COMMENT '是否内置岗位:1内置(禁删禁停) 0自定义' DEFAULT '0',
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT pk_business_roles PRIMARY KEY (id),
CONSTRAINT uk_business_roles_role_code UNIQUE (role_code)
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='业务岗位角色表(与登录角色roles区分,决定计提公式与参数)' COLLATE utf8mb4_general_ci;
CREATE TABLE param_definitions (
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
param_code VARCHAR(64) NOT NULL COMMENT '参数编码,如 salesperson.rate.slope',
param_name VARCHAR(64) NOT NULL COMMENT '参数名称',
param_group VARCHAR(32) NOT NULL COMMENT '参数分组:业务员个人户/经理分成/企业户/信息员/支付环节/产品阈值/月工资口径',
value_type VARCHAR(16) NOT NULL COMMENT '值类型:DECIMAL/INT/BOOL/ENUM/TEXT',
unit VARCHAR(16) COMMENT '展示单位:‰/%/元/天,无单位为空',
dimension_type VARCHAR(32) NOT NULL COMMENT '维度类型:NONE/PRODUCT_CATEGORY/BUSINESS_ROLE' DEFAULT 'NONE',
options_text VARCHAR(255) COMMENT '枚举选项(竖线分隔),仅 ENUM 使用',
default_value VARCHAR(64) COMMENT '系统默认值(Fraction 可解析字符串)',
value_min VARCHAR(64) COMMENT '取值下限(含),校验用',
value_max VARCHAR(64) COMMENT '取值上限(含),校验用',
description VARCHAR(255) COMMENT '口径说明(配置页展示)',
sort_order INTEGER NOT NULL COMMENT '显示排序' DEFAULT '0',
status TINYINT NOT NULL COMMENT '状态:1启用 0停用' DEFAULT '1',
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT pk_param_definitions PRIMARY KEY (id),
CONSTRAINT uk_param_definitions_param_code UNIQUE (param_code)
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='计提参数定义表' COLLATE utf8mb4_general_ci;
CREATE TABLE param_change_batches (
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
version_no VARCHAR(32) NOT NULL COMMENT '版本号,如 P20261012-01',
effective_date DATE NOT NULL COMMENT '生效日期(业务日期口径)',
change_type TINYINT NOT NULL COMMENT '生效方式:1立即生效 2指定日期生效',
remark VARCHAR(255) COMMENT '变更说明',
operator_id BIGINT UNSIGNED COMMENT '操作人ID(账号删除后置空)',
operator_name VARCHAR(64) NOT NULL COMMENT '操作人用户名(冗余固化)',
item_count INTEGER NOT NULL COMMENT '变更参数项数' DEFAULT '0',
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_param_change_batches PRIMARY KEY (id),
CONSTRAINT fk_param_change_batches_operator_id FOREIGN KEY(operator_id) REFERENCES users (id) ON DELETE SET NULL,
CONSTRAINT uk_param_change_batches_version_no UNIQUE (version_no)
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='参数变更批次表' COLLATE utf8mb4_general_ci;
CREATE TABLE param_values (
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
param_id BIGINT UNSIGNED NOT NULL COMMENT '参数定义ID(param_definitions.id)',
dimension_value VARCHAR(32) NOT NULL COMMENT '维度取值:NONE维度为空串;产品类别/岗位编码为具体值' DEFAULT '',
effective_date DATE NOT NULL COMMENT '生效日期(按业务日期时点化取值)',
value_text VARCHAR(64) NOT NULL COMMENT '参数值(Fraction 可解析字符串/枚举码/开关)',
old_value_text VARCHAR(64) COMMENT '变更前值(留痕)',
batch_id BIGINT UNSIGNED NOT NULL COMMENT '变更批次ID(param_change_batches.id)',
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_param_values PRIMARY KEY (id),
CONSTRAINT fk_param_values_batch_id FOREIGN KEY(batch_id) REFERENCES param_change_batches (id) ON DELETE RESTRICT,
CONSTRAINT fk_param_values_param_id FOREIGN KEY(param_id) REFERENCES param_definitions (id) ON DELETE RESTRICT
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='参数版本条目表(append-only,按生效日期时点化取值)' COLLATE utf8mb4_general_ci;
CREATE INDEX idx_param_values_batch_id ON param_values (batch_id);
CREATE INDEX idx_param_values_effective_date ON param_values (effective_date);
CREATE INDEX idx_param_values_param_id_dimension_value_effective_date ON param_values (param_id, dimension_value, effective_date);
INSERT INTO business_roles (id, role_code, role_name, calc_template, description, sort_order, is_builtin) VALUES (1, 'salesperson', '营销员', 'TPL_SALESPERSON_DECAY', '业务员个人户:回款天数线性递减(10‰ − 天数×4/360‰,361 天起固定 4‰)', 1, 1);
INSERT INTO business_roles (id, role_code, role_name, calc_template, description, sort_order, is_builtin) VALUES (2, 'sales_manager', '营销经理', 'TPL_MANAGER_SHARE', '经理绩效:∑本团队业务员绩效×12.5% + ∑全公司业务员绩效×5%', 2, 1);
INSERT INTO business_roles (id, role_code, role_name, calc_template, description, sort_order, is_builtin) VALUES (3, 'info_officer', '信息员', 'TPL_INFO_SALES_FIXED', '信息员绩效:本人销售额×1‰', 3, 1);
INSERT INTO param_change_batches (id, version_no, effective_date, change_type, remark, operator_name, item_count) VALUES (1, 'P20260101-00', '2026-01-01', 1, '初始版本(系统初始化种子,口径依据 2026-10-12 业务裁决)', 'system', 19);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (1, 'salesperson.rate.start', '业务员起始计提比例', '业务员个人户', 'DECIMAL', '‰', 'BUSINESS_ROLE', NULL, '10', '0', NULL, '线性递减段起始值:比例 = 起始 − 回款天数 × 斜率(≤分界天数)', 1);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (2, 'salesperson.rate.slope', '业务员日递减斜率', '业务员个人户', 'DECIMAL', '‰/天', 'BUSINESS_ROLE', NULL, '4/360', '0', NULL, '精确分数 4/360‰/天(=1/90),禁止按 0.011111 截断存储', 2);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (3, 'salesperson.rate.floor', '业务员计提下限', '业务员个人户', 'DECIMAL', '‰', 'BUSINESS_ROLE', NULL, '4', '0', NULL, '回款天数超过分界天数后固定取此比例(≥361 天 = 4‰)', 3);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (4, 'salesperson.days.boundary', '线性递减分界天数', '业务员个人户', 'INT', '天', 'BUSINESS_ROLE', NULL, '360', '0', NULL, '≤分界天数按公式线性递减(360 天=6‰);超过则取下限', 4);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (5, 'manager.share.team', '经理本团队分成比例', '经理分成', 'DECIMAL', '%', 'BUSINESS_ROLE', NULL, '12.5', '0', '100', '∑本人带领业务员绩效 × 此比例(限本团队)', 5);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (6, 'manager.share.company', '经理全公司分成比例', '经理分成', 'DECIMAL', '%', 'BUSINESS_ROLE', NULL, '5', '0', '100', '∑全部业务员绩效 × 此比例(全公司)', 6);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (7, 'enterprise.rate', '企业户计提比例', '企业户', 'DECIMAL', '‰', 'PRODUCT_CATEGORY', NULL, '3', '0', NULL, '所有企业户适用;2026-08-13 起由 2‰ 提高至 3‰', 7);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (8, 'info.rate', '信息员计提比例', '信息员', 'DECIMAL', '‰', 'BUSINESS_ROLE', NULL, '1', '0', NULL, '信息员绩效 = 本人销售额 × 此比例', 8);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (9, 'prepay.rate', '预付款计提比例', '支付环节', 'DECIMAL', '‰', 'NONE', NULL, '10', '0', NULL, '预付款统一按此比例计提', 9);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (10, 'barter.discount', '以物抵顶折算比例', '支付环节', 'DECIMAL', '%', 'NONE', NULL, '80', '0', '100', '以物抵顶回款绩效按应发绩效的此比例折算', 10);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (11, 'payment.start.ratio', '支付起付比例', '支付环节', 'DECIMAL', '%', 'NONE', NULL, '60', '0', '100', '单项业务回款额达销售额(不含运费)此比例后起付;达标前累计', 11);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (12, 'payment.weight.mode', '支付权重应用方式', '支付环节', 'ENUM', NULL, 'NONE', 'FULL|BALANCE', 'FULL', NULL, NULL, 'FULL=按 R×R 乘当月全额应发绩效;BALANCE=乘余额(语义待 M3 前确认)', 12);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (13, 'product.threshold', '折算毛利润额阈值', '产品阈值', 'DECIMAL', '%', 'PRODUCT_CATEGORY', NULL, '30', '0', '100', '折算毛利润额 = 销售额 ×(毛利润率 − 阈值);市政水利 30%、轨交 20%', 13);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (14, 'salary.basic.mode', '月工资基本工资口径', '月工资口径', 'ENUM', NULL, 'BUSINESS_ROLE', 'INCLUDE|DEDUCT|EXCLUDE', 'INCLUDE', NULL, NULL, 'INCLUDE含基本工资 / DEDUCT扣除基本工资 / EXCLUDE不计', 14);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (15, 'probation.annual.threshold', '试用期年累计结算阈值', '月工资口径', 'DECIMAL', '元', 'NONE', NULL, '18000', '0', NULL, '试用期年累计绩效超过此值部分年底一次性结算', 15);
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES (16, 'probation.salary.basic', '试用期基本工资标准', '月工资口径', 'DECIMAL', '元', 'NONE', NULL, '3000', '0', NULL, '试用期仅发基本工资、不发放绩效(归属待 M2 复核)', 16);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (1, 1, 'salesperson', '2026-02-17', '10', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (2, 2, 'salesperson', '2026-02-17', '4/360', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (3, 3, 'salesperson', '2026-02-17', '4', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (4, 4, 'salesperson', '2026-02-17', '360', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (5, 5, 'sales_manager', '2026-08-13', '12.5', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (6, 6, 'sales_manager', '2026-08-13', '5', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (7, 7, '', '2026-02-17', '2', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (8, 7, '', '2026-08-13', '3', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (9, 8, 'info_officer', '2026-08-13', '1', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (10, 9, '', '2026-01-01', '10', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (11, 10, '', '2026-01-01', '80', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (12, 11, '', '2026-01-01', '60', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (13, 12, '', '2026-01-01', 'FULL', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (14, 13, 'municipal_water', '2026-02-17', '30', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (15, 13, 'rail_transit', '2026-02-17', '20', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (16, 14, 'salesperson', '2026-01-01', 'INCLUDE', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (17, 14, 'sales_manager', '2026-01-01', 'DEDUCT', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (18, 15, '', '2026-01-01', '18000', 1);
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (19, 16, '', '2026-01-01', '3000', 1);
UPDATE alembic_version SET version_num='bfbda9c3191d' WHERE alembic_version.version_num = 'a45a8087831b';
+58
View File
@@ -0,0 +1,58 @@
-- ============================================================================
-- 计提参数查询留档(M1 参数体系)
--
-- 时点化取值核心规则:按 (param_id, dimension_value) 分组,取
-- effective_date <= :biz_date 的 ORDER BY effective_date DESC, id DESC 首条
-- (同日多次变更取最新一条,不设唯一键以保证完整留痕)。
-- 参数总量约百行级,全量快照一次取回后在应用层按覆盖链
-- (角色维度 > 产品类别维度 > 默认行)选择,无需在 SQL 层做维度回退。
-- ============================================================================
-- 1. 全量快照:某业务日期下每个(参数, 维度值)的最新生效值
-- 窗口函数版(MySQL 8.0+),供 param_repository.resolve_values 使用
SELECT t.param_code, t.dimension_value, t.value_text, t.effective_date
FROM (
SELECT pd.param_code,
pv.dimension_value,
pv.value_text,
pv.effective_date,
ROW_NUMBER() OVER (
PARTITION BY pv.param_id, pv.dimension_value
ORDER BY pv.effective_date DESC, pv.id DESC
) AS rn
FROM param_values pv
JOIN param_definitions pd ON pd.id = pv.param_id
WHERE pv.effective_date <= :biz_date
AND pd.status = 1
) t
WHERE t.rn = 1;
-- 2. 单参数版本历史(变更留痕,供参数历史接口与导出)
SELECT pv.effective_date,
pv.dimension_value,
pv.old_value_text,
pv.value_text,
b.version_no,
b.operator_name,
b.create_time
FROM param_values pv
JOIN param_change_batches b ON b.id = pv.batch_id
WHERE pv.param_id = :param_id
ORDER BY pv.effective_date DESC, pv.id DESC;
-- 3. 区间变更条目(对比视图辅助:应用层对两日期各执行查询 1 后逐项 diff,
-- 本查询列出区间内实际发生过的变更,供变更记录分页与导出)
SELECT pd.param_code,
pd.param_name,
pv.dimension_value,
pv.old_value_text,
pv.value_text,
pv.effective_date,
b.version_no,
b.operator_name
FROM param_values pv
JOIN param_definitions pd ON pd.id = pv.param_id
JOIN param_change_batches b ON b.id = pv.batch_id
WHERE pv.effective_date > :from_date
AND pv.effective_date <= :to_date
ORDER BY pv.effective_date, pv.id;