"""新增岗位角色与计提参数四表 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")