feat(migration): V6 系统设置/二次分配/审批表与角色参数种子
- 新增 system_settings(公司信息单行)/ business_secondary_allocations(二次 分配明细,比例不强制合计满额,分摊计算待澄清另批)/ approval_orders+ approval_steps(三级审批状态机,开关关闭自动通过留痕) - roles 种子新增营销后勤/财务/领导三职能角色;内置保护改 BUILTIN_ROLE_CODES 白名单(roles 表无 is_builtin 列,不加列) - PARAM_REGISTRY 首例 BOOL:两个审批开关(16→18 项,种子双写) - 双轨同步:V6 导出 SQL + init-dev.sql(表/索引/种子/版本号 79bcc5aa98d4) - 测试同步:EXPECTED_TABLES 四新表、角色种子断言 5 角色、目录断言 18 项 八分组、BOOL 校验边界;迁移 downgrade 修 1553(只保留 drop_table)并补 行级种子反向清理 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
94c49138bf
commit
acf87fdfdc
14 files changed
+918
-25
No files matched your search
@@ -0,0 +1,436 @@
|
||||
"""V6 系统设置/二次分配/审批表与角色参数种子
|
||||
|
||||
Revision ID: 79bcc5aa98d4
|
||||
Revises: 8efc189b131f
|
||||
Create Date: 2026-10-15 15:42:13.467447
|
||||
|
||||
"""
|
||||
|
||||
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 = "79bcc5aa98d4"
|
||||
down_revision: str | Sequence[str] | None = "8efc189b131f"
|
||||
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(
|
||||
"system_settings",
|
||||
sa.Column(
|
||||
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
|
||||
),
|
||||
sa.Column(
|
||||
"company_name",
|
||||
sa.String(length=64),
|
||||
nullable=False,
|
||||
comment="公司名称(登录页与导出页眉展示)",
|
||||
),
|
||||
sa.Column(
|
||||
"logo_path",
|
||||
sa.String(length=255),
|
||||
nullable=True,
|
||||
comment="logo 相对路径(文件存 uploads/logo/,null=未上传使用默认)",
|
||||
),
|
||||
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_system_settings")),
|
||||
comment="系统设置表(公司信息,全系统单行)",
|
||||
mysql_charset="utf8mb4",
|
||||
mysql_collate="utf8mb4_general_ci",
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
op.create_table(
|
||||
"approval_orders",
|
||||
sa.Column(
|
||||
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
|
||||
),
|
||||
sa.Column(
|
||||
"order_no",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
comment="审批单号,自动生成 AP{YYYYMMDD}-{4位当日序号}",
|
||||
),
|
||||
sa.Column(
|
||||
"order_type",
|
||||
mysql.TINYINT(),
|
||||
nullable=False,
|
||||
comment="审批对象类型:1二次分配方案 2月度结算(预留)",
|
||||
),
|
||||
sa.Column(
|
||||
"ref_id",
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=False,
|
||||
comment="引用对象ID(order_type=1 时为 businesses.id;固化无外键,删除保护由服务层承担)",
|
||||
),
|
||||
sa.Column(
|
||||
"refer_no",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
comment="引用对象单号(冗余固化,列表页免联查)",
|
||||
),
|
||||
sa.Column(
|
||||
"status",
|
||||
mysql.TINYINT(),
|
||||
server_default="1",
|
||||
nullable=False,
|
||||
comment="审批状态:1待财务复核 2待领导审核 3已通过 4已驳回 5已撤回",
|
||||
),
|
||||
sa.Column(
|
||||
"submitter_id",
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=True,
|
||||
comment="提交人ID(账号删除后置空)",
|
||||
),
|
||||
sa.Column(
|
||||
"submitter_name",
|
||||
sa.String(length=64),
|
||||
nullable=False,
|
||||
comment="提交人用户名(冗余固化)",
|
||||
),
|
||||
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.ForeignKeyConstraint(
|
||||
["submitter_id"],
|
||||
["users.id"],
|
||||
name=op.f("fk_approval_orders_submitter_id"),
|
||||
ondelete="SET NULL",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_approval_orders")),
|
||||
sa.UniqueConstraint("order_no", name=op.f("uk_approval_orders_order_no")),
|
||||
comment="审批单表(营销后勤提交→财务复核→领导审核;开关关闭时自动通过留痕)",
|
||||
mysql_charset="utf8mb4",
|
||||
mysql_collate="utf8mb4_general_ci",
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
op.create_index(
|
||||
"idx_approval_orders_type_status", "approval_orders", ["order_type", "status"], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
"approval_steps",
|
||||
sa.Column(
|
||||
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
|
||||
),
|
||||
sa.Column(
|
||||
"order_id",
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=False,
|
||||
comment="审批单ID(approval_orders.id;单删记录随删)",
|
||||
),
|
||||
sa.Column(
|
||||
"node",
|
||||
mysql.TINYINT(),
|
||||
nullable=False,
|
||||
comment="审批节点:0撤回留痕 1财务复核 2领导审核",
|
||||
),
|
||||
sa.Column(
|
||||
"action", mysql.TINYINT(), nullable=False, comment="动作:1通过 2驳回 3自动通过 4撤回"
|
||||
),
|
||||
sa.Column(
|
||||
"opinion", 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(
|
||||
"create_time",
|
||||
sa.DateTime(),
|
||||
server_default=sa.text("CURRENT_TIMESTAMP"),
|
||||
nullable=False,
|
||||
comment="创建时间",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["operator_id"],
|
||||
["users.id"],
|
||||
name=op.f("fk_approval_steps_operator_id"),
|
||||
ondelete="SET NULL",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["order_id"],
|
||||
["approval_orders.id"],
|
||||
name=op.f("fk_approval_steps_order_id"),
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_approval_steps")),
|
||||
comment="审批节点记录表(append-only;自动通过/撤回均有留痕)",
|
||||
mysql_charset="utf8mb4",
|
||||
mysql_collate="utf8mb4_general_ci",
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
op.create_index("idx_approval_steps_order_id", "approval_steps", ["order_id"], unique=False)
|
||||
op.create_table(
|
||||
"business_secondary_allocations",
|
||||
sa.Column(
|
||||
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
|
||||
),
|
||||
sa.Column(
|
||||
"business_id",
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=False,
|
||||
comment="业务ID(businesses.id;业务删除随删)",
|
||||
),
|
||||
sa.Column(
|
||||
"personnel_id",
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=False,
|
||||
comment="参与业务员ID(personnel.id;被方案引用禁删)",
|
||||
),
|
||||
sa.Column(
|
||||
"personnel_name",
|
||||
sa.String(length=64),
|
||||
nullable=False,
|
||||
comment="参与业务员姓名(冗余固化,离职后回显有名)",
|
||||
),
|
||||
sa.Column(
|
||||
"ratio",
|
||||
sa.Numeric(precision=5, scale=2),
|
||||
nullable=False,
|
||||
comment="分摊比例(0-100 两位小数;不要求合计满额,余额归属待澄清#2)",
|
||||
),
|
||||
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.ForeignKeyConstraint(
|
||||
["business_id"],
|
||||
["businesses.id"],
|
||||
name=op.f("fk_business_secondary_allocations_business_id"),
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["personnel_id"],
|
||||
["personnel.id"],
|
||||
name=op.f("fk_business_secondary_allocations_personnel_id"),
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_business_secondary_allocations")),
|
||||
comment="业务二次分配方案明细表(合同建立时录入;比例不要求合计满额,余额归属待澄清)",
|
||||
mysql_charset="utf8mb4",
|
||||
mysql_collate="utf8mb4_general_ci",
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
op.create_index(
|
||||
"idx_business_secondary_allocations_business_id",
|
||||
"business_secondary_allocations",
|
||||
["business_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"idx_business_secondary_allocations_personnel_id",
|
||||
"business_secondary_allocations",
|
||||
["personnel_id"],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
# ---- 种子数据(生产与开发同源;写法沿用 V1/V3 迁移先例)----
|
||||
|
||||
# 三个审批职能登录角色(2026-10-15 复核审批补充需求;roles 表无 is_builtin
|
||||
# 列,内置保护由 role_service 的 BUILTIN_ROLE_CODES 白名单承担)
|
||||
op.bulk_insert(
|
||||
sa.table(
|
||||
"roles",
|
||||
sa.column("role_code", sa.String),
|
||||
sa.column("role_name", sa.String),
|
||||
sa.column("description", sa.String),
|
||||
sa.column("sort_order", sa.Integer),
|
||||
),
|
||||
[
|
||||
{
|
||||
"role_code": "marketing_ops",
|
||||
"role_name": "营销后勤",
|
||||
"description": "提交与录入(审批链第一节点:月结/二次分配提交)",
|
||||
"sort_order": 3,
|
||||
},
|
||||
{
|
||||
"role_code": "finance",
|
||||
"role_name": "财务",
|
||||
"description": "复核(审批链第二节点)",
|
||||
"sort_order": 4,
|
||||
},
|
||||
{
|
||||
"role_code": "leader",
|
||||
"role_name": "领导",
|
||||
"description": "审核(审批链第三节点;全览只读)",
|
||||
"sort_order": 5,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
# 审批开关参数定义(param_definitions id 沿用 V3 种子的显式 id 方案接续 17/18)
|
||||
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),
|
||||
sa.column("status", sa.Integer),
|
||||
),
|
||||
[
|
||||
{
|
||||
"id": 17,
|
||||
"param_code": "approval.secondary_allocation.enabled",
|
||||
"param_name": "二次分配审批开关",
|
||||
"param_group": "审批开关",
|
||||
"value_type": "BOOL",
|
||||
"unit": None,
|
||||
"dimension_type": "NONE",
|
||||
"options_text": None,
|
||||
"default_value": "0",
|
||||
"value_min": None,
|
||||
"value_max": None,
|
||||
"description": (
|
||||
"0=二次分配方案提交即生效(系统自动通过全节点留痕,留痕不断链);"
|
||||
"1=提交后进入财务复核→领导审核审批流(2026-10-15 复核审批补充需求)"
|
||||
),
|
||||
"sort_order": 17,
|
||||
"status": 1,
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"param_code": "approval.monthly_settlement.enabled",
|
||||
"param_name": "月度结算审批开关",
|
||||
"param_group": "审批开关",
|
||||
"value_type": "BOOL",
|
||||
"unit": None,
|
||||
"dimension_type": "NONE",
|
||||
"options_text": None,
|
||||
"default_value": "0",
|
||||
"value_min": None,
|
||||
"value_max": None,
|
||||
"description": (
|
||||
"预留参数:月度结算审批链开关(M3 ④ 月结子块接入时消费);"
|
||||
"0=结算生成即生效,1=走财务复核→领导审核审批流"
|
||||
),
|
||||
"sort_order": 18,
|
||||
"status": 1,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
# 审批开关联动批次条目(batch_id=1 即 V3 的「初始版本」批次;BOOL 无维度行,
|
||||
# 生效日期 2026-01-01 对齐其他支付环节种子)
|
||||
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),
|
||||
),
|
||||
[
|
||||
{
|
||||
"id": 20,
|
||||
"param_id": 17,
|
||||
"dimension_value": "",
|
||||
"effective_date": date(2026, 1, 1),
|
||||
"value_text": "0",
|
||||
"batch_id": 1,
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"param_id": 18,
|
||||
"dimension_value": "",
|
||||
"effective_date": date(2026, 1, 1),
|
||||
"value_text": "0",
|
||||
"batch_id": 1,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
# 公司信息单行(system_settings 恒为一行,id=1 由迁移保底)
|
||||
op.bulk_insert(
|
||||
sa.table(
|
||||
"system_settings",
|
||||
sa.column("id", sa.BigInteger),
|
||||
sa.column("company_name", sa.String),
|
||||
sa.column("logo_path", sa.String),
|
||||
),
|
||||
[
|
||||
{"id": 1, "company_name": "衍水集团", "logo_path": None},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""回滚迁移。
|
||||
|
||||
整表回滚只保留 `op.drop_table` —— DROP TABLE 会连带删除索引与外键。
|
||||
autogenerate 原样输出的逐个 `op.drop_index` 会失败:idx_approval_steps_order_id
|
||||
与 idx_business_secondary_allocations_personnel_id 分别是外键的唯一支撑索引,
|
||||
单独删除被 MySQL 拒绝(errno 1553),回滚会中断在半途。
|
||||
删除顺序:先删引用方(approval_steps/二次分配),再删被引用方。
|
||||
行级种子反向清理:roles/param_definitions/param_values 三表由 V1/V3 建、
|
||||
本迁移只插行(roles 三角色、参数 17/18 及其版本条目),drop_table
|
||||
不会带走,须显式 DELETE,否则重放 upgrade 时唯一键冲突。
|
||||
"""
|
||||
op.drop_table("business_secondary_allocations")
|
||||
op.drop_table("approval_steps")
|
||||
op.drop_table("approval_orders")
|
||||
op.drop_table("system_settings")
|
||||
op.execute("DELETE FROM param_values WHERE id IN (20, 21)")
|
||||
op.execute("DELETE FROM param_definitions WHERE id IN (17, 18)")
|
||||
op.execute("DELETE FROM roles WHERE role_code IN ('marketing_ops', 'finance', 'leader')")
|
||||
Reference in new issue
Block a user