feat/m3-increments #1
436
backend/alembic/versions/79bcc5aa98d4_v6_系统设置_二次分配_审批表与角色参数种子.py
Normal file
436
backend/alembic/versions/79bcc5aa98d4_v6_系统设置_二次分配_审批表与角色参数种子.py
Normal file
@ -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')")
|
||||
@ -25,6 +25,19 @@ TOKEN_TYPE_BEARER = "Bearer"
|
||||
# ===== 角色编码(与 db/migration 初始化数据保持一致)=====
|
||||
ROLE_ADMIN = "admin"
|
||||
ROLE_USER = "user"
|
||||
# 2026-10-15 复核审批补充需求新增职能登录角色:营销后勤(提交+录入)、
|
||||
# 财务(复核)、领导(审核)。roles 表无 is_builtin 列,内置保护用代码
|
||||
# 白名单判定(role_service 两处内置校验改查此集合);
|
||||
# permissions/role_permissions 细粒度激活待业务权限清单(待澄清 #6)。
|
||||
ROLE_MARKETING_OPS = "marketing_ops"
|
||||
ROLE_FINANCE = "finance"
|
||||
ROLE_LEADER = "leader"
|
||||
BUILTIN_ROLE_CODES = {
|
||||
ROLE_ADMIN,
|
||||
ROLE_MARKETING_OPS,
|
||||
ROLE_FINANCE,
|
||||
ROLE_LEADER,
|
||||
}
|
||||
|
||||
# ===== 密码策略 =====
|
||||
# bcrypt 5.0 起对超过 72 字节的密码由「静默截断」改为「抛 ValueError」,
|
||||
@ -70,6 +83,8 @@ MODULE_BUSINESS = "业务管理"
|
||||
MODULE_SHIPMENT = "发货管理"
|
||||
MODULE_RECEIPT = "回款管理"
|
||||
MODULE_RATING = "客户评级"
|
||||
MODULE_SYSTEM_SETTINGS = "系统设置"
|
||||
MODULE_APPROVAL = "审批管理"
|
||||
|
||||
# ===== 单据编号前缀({前缀}{YYYYMMDD}-{当日4位序号},序号按自然日重置)=====
|
||||
# 一期为代码常量(业务未提出配置诉求);如需可配置,走 M1 参数体系新增参数组
|
||||
@ -77,6 +92,7 @@ NO_PREFIX_BUSINESS = "YW"
|
||||
NO_PREFIX_SHIPMENT = "SH"
|
||||
NO_PREFIX_RECEIPT = "HK"
|
||||
NO_PREFIX_PARAM = "P"
|
||||
NO_PREFIX_APPROVAL = "AP"
|
||||
|
||||
ACTION_CREATE = "新增"
|
||||
ACTION_UPDATE = "修改"
|
||||
|
||||
@ -9,8 +9,9 @@
|
||||
`Base.metadata` 包含全部表,漏掉一张就会生成「删除该表」的迁移。
|
||||
"""
|
||||
|
||||
from app.models.approval import ApprovalOrder, ApprovalStep
|
||||
from app.models.base import Base, TimestampMixin
|
||||
from app.models.business import Business, BusinessItem, BusinessStatusLog
|
||||
from app.models.business import Business, BusinessItem, BusinessStatusLog, SecondaryAllocation
|
||||
from app.models.business_role import BusinessRole
|
||||
from app.models.customer import Customer, OwnershipChangeLog
|
||||
from app.models.login_log import LoginLog
|
||||
@ -22,9 +23,12 @@ from app.models.rating import CustomerRatingGrade
|
||||
from app.models.receipt import Receipt
|
||||
from app.models.role import Permission, Role, RolePermission
|
||||
from app.models.shipment import Shipment, ShipmentItem
|
||||
from app.models.system_setting import SystemSetting
|
||||
from app.models.user import User, UserRole
|
||||
|
||||
__all__ = [
|
||||
"ApprovalOrder",
|
||||
"ApprovalStep",
|
||||
"Base",
|
||||
"Business",
|
||||
"BusinessItem",
|
||||
@ -46,8 +50,10 @@ __all__ = [
|
||||
"Receipt",
|
||||
"Role",
|
||||
"RolePermission",
|
||||
"SecondaryAllocation",
|
||||
"Shipment",
|
||||
"ShipmentItem",
|
||||
"SystemSetting",
|
||||
"TimestampMixin",
|
||||
"User",
|
||||
"UserRole",
|
||||
|
||||
128
backend/app/models/approval.py
Normal file
128
backend/app/models/approval.py
Normal file
@ -0,0 +1,128 @@
|
||||
"""审批模型:审批单与节点记录(2026-10-15 复核审批补充需求)。
|
||||
|
||||
三级链路:营销后勤提交 → 财务复核 → 领导审核。行为由参数体系开关驱动
|
||||
(`approval.*.enabled` BOOL):关闭=提交即生成「自动通过」节点留痕,
|
||||
留痕不断链(开发计划 3.2(三));开启=按节点人工审核,一驳全驳。
|
||||
|
||||
依赖说明:节点准入按登录角色编码判定(ROLE_FINANCE/ROLE_LEADER),
|
||||
不动 `permissions`/`role_permissions` 闲置表 —— 细粒度权限清单待业务方
|
||||
答复(待澄清 #6)后再激活。
|
||||
|
||||
ref_id 为**固化无外键**(同 business_status_logs 先例):order_type=2
|
||||
(月结预留)将指向 ④ 尚未落地的表,外键无法统一指向;单据删除保护由
|
||||
服务层承担(详见 approval_service)。
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, String, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import BIGINT_UNSIGNED, TINYINT_TYPE, Base, TimestampMixin, mysql_table_args
|
||||
|
||||
# 审批单状态(approval_orders.status;流转机见 approval_service)
|
||||
APPROVAL_STATUS_PENDING_FINANCE = 1 # 待财务复核
|
||||
APPROVAL_STATUS_PENDING_LEADER = 2 # 待领导审核
|
||||
APPROVAL_STATUS_APPROVED = 3 # 已通过(终态)
|
||||
APPROVAL_STATUS_REJECTED = 4 # 已驳回(终态)
|
||||
APPROVAL_STATUS_WITHDRAWN = 5 # 已撤回(被引用方重新提交时旧在途单注销)
|
||||
|
||||
# 节点(approval_steps.node;0 仅用于撤回留痕)
|
||||
APPROVAL_NODE_FINANCE = 1
|
||||
APPROVAL_NODE_LEADER = 2
|
||||
APPROVAL_NODE_WITHDRAW = 0
|
||||
APPROVAL_NODES = {
|
||||
APPROVAL_STATUS_PENDING_FINANCE: APPROVAL_NODE_FINANCE,
|
||||
APPROVAL_STATUS_PENDING_LEADER: APPROVAL_NODE_LEADER,
|
||||
}
|
||||
|
||||
# 动作(approval_steps.action)
|
||||
APPROVAL_ACTION_APPROVE = 1 # 人工通过
|
||||
APPROVAL_ACTION_REJECT = 2 # 人工驳回
|
||||
APPROVAL_ACTION_AUTO_APPROVE = 3 # 系统自动通过(开关关闭时逐节点留痕)
|
||||
APPROVAL_ACTION_WITHDRAW = 4 # 撤回(业务重新提交时旧在途单注销)
|
||||
|
||||
|
||||
class ApprovalOrder(Base, TimestampMixin):
|
||||
"""审批单:一张待审对象一条审批流(状态可流转,含 update_time)。"""
|
||||
|
||||
__tablename__ = "approval_orders"
|
||||
__table_args__ = mysql_table_args(
|
||||
"审批单表(营销后勤提交→财务复核→领导审核;开关关闭时自动通过留痕)",
|
||||
Index("idx_approval_orders_type_status", "order_type", "status"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||||
)
|
||||
order_no: Mapped[str] = mapped_column(
|
||||
String(32),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
comment="审批单号,自动生成 AP{YYYYMMDD}-{4位当日序号}",
|
||||
)
|
||||
order_type: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE, nullable=False, comment="审批对象类型:1二次分配方案 2月度结算(预留)"
|
||||
)
|
||||
ref_id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED,
|
||||
comment="引用对象ID(order_type=1 时为 businesses.id;固化无外键,删除保护由服务层承担)",
|
||||
)
|
||||
refer_no: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False, comment="引用对象单号(冗余固化,列表页免联查)"
|
||||
)
|
||||
status: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE,
|
||||
nullable=False,
|
||||
server_default="1",
|
||||
comment="审批状态:1待财务复核 2待领导审核 3已通过 4已驳回 5已撤回",
|
||||
)
|
||||
submitter_id: Mapped[int | None] = mapped_column(
|
||||
BIGINT_UNSIGNED,
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
comment="提交人ID(账号删除后置空)",
|
||||
)
|
||||
submitter_name: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, comment="提交人用户名(冗余固化)"
|
||||
)
|
||||
|
||||
|
||||
class ApprovalStep(Base):
|
||||
"""审批节点记录表(append-only):每次动作只增不改,构成时间线。
|
||||
|
||||
不继承 TimestampMixin(留痕无更新语义,同 business_status_logs 先例)。
|
||||
"""
|
||||
|
||||
__tablename__ = "approval_steps"
|
||||
__table_args__ = mysql_table_args(
|
||||
"审批节点记录表(append-only;自动通过/撤回均有留痕)",
|
||||
Index("idx_approval_steps_order_id", "order_id"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||||
)
|
||||
order_id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED,
|
||||
ForeignKey("approval_orders.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
comment="审批单ID(approval_orders.id;单删记录随删)",
|
||||
)
|
||||
node: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE, nullable=False, comment="审批节点:0撤回留痕 1财务复核 2领导审核"
|
||||
)
|
||||
action: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE, nullable=False, comment="动作:1通过 2驳回 3自动通过 4撤回"
|
||||
)
|
||||
opinion: 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="操作人用户名(冗余固化;系统动作记「系统」)"
|
||||
)
|
||||
create_time: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP"), comment="创建时间"
|
||||
)
|
||||
@ -33,6 +33,58 @@ BUSINESS_STATUS_DONE = 2
|
||||
BUSINESS_STATUS_TERMINATED = 3
|
||||
|
||||
|
||||
class SecondaryAllocation(Base, TimestampMixin):
|
||||
"""二次分配方案明细表(2026-10-15 补充需求「二次分配方案」)。
|
||||
|
||||
口径(AI_CONTEXT 待澄清 #1/#2 未答复前的边界):
|
||||
- 只出现在合同刚建立时;修改在首次月结前留痕放行,月结后只读(月结在 ④,
|
||||
终态门槛先行承担「修改不影响已结算月份」)
|
||||
- 分配比例**不要求合计 100%**:余额归属未定(澄清 #2),存储只落参与人
|
||||
与比例,响应以 hint 提示合计/余额,不做强制校验
|
||||
- T+ 无二次分配数据(2026-10-15 回馈):参与人全为系统内人工维护
|
||||
- **分摊计算不在本期**(澄清 #1 分配标的三种口径结果不同):④ 核算时按
|
||||
核算时刻读「当前生效行」做快照,天然隔离历史版本,故无版本表
|
||||
|
||||
personnel 外键 **RESTRICT**:被方案引用的人员禁删——分摊金额待定的记录
|
||||
不允许静默失联(silently deleted)导致回档无法人工查验;personnel_name
|
||||
冗余固化,人离职后方案回显仍有名。uk 锁 (business_id, personnel_id),
|
||||
同一合同的同一参与人仅一行,重复提交在服务层聚合。
|
||||
"""
|
||||
|
||||
__tablename__ = "business_secondary_allocations"
|
||||
__table_args__ = mysql_table_args(
|
||||
# 表/列注释避开「%」字符:SHOW CREATE TABLE 经驱动参数化转义为「%%」,
|
||||
# 会与 ORM 侧 comment 字面断言失配(全库既有注释同此惯例)
|
||||
"业务二次分配方案明细表(合同建立时录入;比例不要求合计满额,余额归属待澄清)",
|
||||
Index("idx_business_secondary_allocations_business_id", "business_id"),
|
||||
Index("idx_business_secondary_allocations_personnel_id", "personnel_id"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||||
)
|
||||
business_id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED,
|
||||
ForeignKey("businesses.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
comment="业务ID(businesses.id;业务删除随删)",
|
||||
)
|
||||
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="参与业务员姓名(冗余固化,离职后回显有名)"
|
||||
)
|
||||
ratio: Mapped[Decimal] = mapped_column(
|
||||
Numeric(5, 2),
|
||||
nullable=False,
|
||||
comment="分摊比例(0-100 两位小数;不要求合计满额,余额归属待澄清#2)",
|
||||
)
|
||||
|
||||
|
||||
class Business(Base, TimestampMixin, TPlusSyncMixin):
|
||||
"""业务表(需求 2.2.1):业务编号自动生成,即报表 2.4.1 的「合同编号」。"""
|
||||
|
||||
|
||||
31
backend/app/models/system_setting.py
Normal file
31
backend/app/models/system_setting.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""系统设置模型:公司信息单例行。
|
||||
|
||||
与 M1 计提参数体系刻意分离:公司名称/logo 是「展示事实」,没有按生效日期
|
||||
时点化演算的语义(参数体系按 effective_date 取值快照),且参数值列
|
||||
value_text 为 String(64) 装不下;故独立单行表。logo 只存相对路径——文件在
|
||||
`backend/uploads/logo/`,独立于数据库备份体积且可单独替换(见
|
||||
system_setting_service)。整个系统恒为单行(迁移种子 id=1 保底,upsert 维护)。
|
||||
"""
|
||||
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import BIGINT_UNSIGNED, Base, TimestampMixin, mysql_table_args
|
||||
|
||||
|
||||
class SystemSetting(Base, TimestampMixin):
|
||||
"""系统设置表:本期仅公司信息(公司名称 / logo 路径),全系统单行 id=1。"""
|
||||
|
||||
__tablename__ = "system_settings"
|
||||
__table_args__ = mysql_table_args("系统设置表(公司信息,全系统单行)")
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||||
)
|
||||
company_name: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, comment="公司名称(登录页与导出页眉展示)"
|
||||
)
|
||||
logo_path: Mapped[str | None] = mapped_column(
|
||||
String(255),
|
||||
comment="logo 相对路径(文件存 uploads/logo/,null=未上传使用默认)",
|
||||
)
|
||||
@ -256,6 +256,40 @@ PARAM_REGISTRY: dict[str, ParamSpec] = {
|
||||
description="试用期仅发基本工资、不发放绩效(归属待 M2 复核)",
|
||||
sort_order=16,
|
||||
),
|
||||
ParamSpec(
|
||||
code="approval.secondary_allocation.enabled",
|
||||
name="二次分配审批开关",
|
||||
group="审批开关",
|
||||
value_type="BOOL",
|
||||
unit=None,
|
||||
dimension_type="NONE",
|
||||
options=None,
|
||||
default_value="0",
|
||||
value_min=None,
|
||||
value_max=None,
|
||||
description=(
|
||||
"0=二次分配方案提交即生效(系统自动通过全节点留痕,留痕不断链);"
|
||||
"1=提交后进入财务复核→领导审核审批流(2026-10-15 复核审批补充需求)"
|
||||
),
|
||||
sort_order=17,
|
||||
),
|
||||
ParamSpec(
|
||||
code="approval.monthly_settlement.enabled",
|
||||
name="月度结算审批开关",
|
||||
group="审批开关",
|
||||
value_type="BOOL",
|
||||
unit=None,
|
||||
dimension_type="NONE",
|
||||
options=None,
|
||||
default_value="0",
|
||||
value_min=None,
|
||||
value_max=None,
|
||||
description=(
|
||||
"预留参数:月度结算审批链开关(M3 ④ 月结子块接入时消费);"
|
||||
"0=结算生成即生效,1=走财务复核→领导审核审批流"
|
||||
),
|
||||
sort_order=18,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
"""角色管理业务编排。
|
||||
|
||||
需求 2.5.1 只定义 admin / user 两个角色,本模块属用户确认的**自主完善**
|
||||
(轻量版:不含权限点分配 —— `permissions` / `role_permissions` 表继续闲置,
|
||||
待业务方给出权限清单后再启用)。
|
||||
需求 2.5.1 定义 admin / user 两个登录角色;2026-10-15 复核审批补充需求
|
||||
新增营销后勤/财务/领导三个职能登录角色(营销后勤与财务/领导分区于审批
|
||||
节点准入)。本模块仍为轻量版(不含权限点分配 —— `permissions` /
|
||||
`role_permissions` 表继续闲置,待业务方给出权限清单后再启用)。
|
||||
|
||||
保护规则(都是系统自举与授权安全的最低防线,不只是界面体验):
|
||||
|
||||
- `admin` 角色**禁删禁停** —— `cli` 建管理员与 `require_admin` 判定都依赖
|
||||
该编码存在,删除会使系统无法自举
|
||||
- **内置角色禁删禁停**(`BUILTIN_ROLE_CODES`:admin / marketing_ops /
|
||||
finance / leader)—— `admin` 被删除会使系统无法自举(cli 建管理员与
|
||||
`require_admin` 判定依赖);财务/领导是审批节点准入锚点,被删会让
|
||||
在途审批流失去可操作人
|
||||
- **有用户绑定的角色禁删** —— `user_roles` 外键是 CASCADE,直接删除会静默
|
||||
剥离绑定用户的权限,属安全与审计事故;删除前的校验走 `FOR UPDATE`
|
||||
锁行,堵住「校验与删除之间并发绑定」的窗口
|
||||
- `role_code` 创建后不可改 —— 它是授权判定(`ROLE_ADMIN` 常量按编码比对)的锚点
|
||||
- `role_code` 创建后不可改 —— 它是授权判定(`BUILTIN_ROLE_CODES` 白名单按编码比对)的锚点
|
||||
- 停用角色**不解除绑定、不影响既有授权**(授权判定只看编码、不看状态),
|
||||
但**不可再分配给用户**(`user_service._resolve_roles` 拦截)
|
||||
"""
|
||||
@ -27,8 +30,8 @@ from app.core.constants import (
|
||||
ACTION_DISABLE,
|
||||
ACTION_ENABLE,
|
||||
ACTION_UPDATE,
|
||||
BUILTIN_ROLE_CODES,
|
||||
MODULE_ROLE,
|
||||
ROLE_ADMIN,
|
||||
)
|
||||
from app.core.exceptions import BadRequestError, ForbiddenError, NotFoundError
|
||||
from app.core.response import PageResult
|
||||
@ -177,10 +180,10 @@ def set_status(
|
||||
|
||||
停用不解除用户绑定、不影响既有授权(授权判定只看编码不看状态),
|
||||
但该角色不可再分配给用户(`user_service._resolve_roles` 拦截);
|
||||
内置管理员角色不可停用。
|
||||
内置角色不可停用。
|
||||
|
||||
:raises NotFoundError: 角色不存在
|
||||
:raises ForbiddenError: 试图停用内置管理员角色
|
||||
:raises ForbiddenError: 试图停用内置角色
|
||||
"""
|
||||
role = role_repository.get_by_id(session, role_id)
|
||||
verb = "启用" if status == 1 else "禁用"
|
||||
@ -198,8 +201,8 @@ def set_status(
|
||||
if role is None:
|
||||
raise NotFoundError("角色不存在")
|
||||
|
||||
if role.role_code == ROLE_ADMIN and status == 0:
|
||||
raise ForbiddenError("内置管理员角色不可停用")
|
||||
if role.role_code in BUILTIN_ROLE_CODES and status == 0:
|
||||
raise ForbiddenError("内置角色不可停用")
|
||||
|
||||
role.status = status
|
||||
out = _to_out(role, _user_count(session, role_id))
|
||||
@ -217,12 +220,12 @@ def delete_role(
|
||||
) -> None:
|
||||
"""删除角色。
|
||||
|
||||
内置管理员角色不可删除;仍有关联用户的角色不可删除(提示先解除绑定)。
|
||||
内置角色不可删除(admin 与三个审批职能角色,见 BUILTIN_ROLE_CODES);仍有关联用户的角色不可删除(提示先解除绑定)。
|
||||
查询走 `FOR UPDATE` 锁行:校验与删除之间若发生并发绑定,CASCADE 会把
|
||||
新绑定一并静默剥离 —— 锁行使并发绑定失败(可见)而非数据被吞(不可见)。
|
||||
|
||||
:raises NotFoundError: 角色不存在
|
||||
:raises ForbiddenError: 试图删除内置管理员角色
|
||||
:raises ForbiddenError: 试图删除内置角色
|
||||
:raises BadRequestError: 角色下仍有绑定的用户
|
||||
"""
|
||||
role = role_repository.get_by_id(session, role_id, for_update=True)
|
||||
@ -239,8 +242,8 @@ def delete_role(
|
||||
if role is None:
|
||||
raise NotFoundError("角色不存在")
|
||||
|
||||
if role.role_code == ROLE_ADMIN:
|
||||
raise ForbiddenError("内置管理员角色不可删除")
|
||||
if role.role_code in BUILTIN_ROLE_CODES:
|
||||
raise ForbiddenError("内置角色不可删除")
|
||||
|
||||
# user_roles 外键是 CASCADE:不拦截的话,删除会静默剥离绑定用户的权限
|
||||
user_count = _user_count(session, role_id)
|
||||
|
||||
@ -86,9 +86,10 @@ def _snapshot_value(client: TestClient, admin: dict, param_code: str, *, as_of:
|
||||
class TestCatalog:
|
||||
"""参数目录:种子完整性与时点化回溯。"""
|
||||
|
||||
def test_目录共16项且覆盖七个分组(self, client: TestClient, admin: dict) -> None:
|
||||
def test_目录共18项且覆盖八个分组(self, client: TestClient, admin: dict) -> None:
|
||||
"""V6 起含审批开关组(16 计提项 + 2 BOOL 开关)。"""
|
||||
items = client.get(_PARAMS, headers=admin["headers"]).json()["data"]
|
||||
assert len(items) == 16
|
||||
assert len(items) == 18
|
||||
groups = {item["param_group"] for item in items}
|
||||
assert groups == {
|
||||
"业务员个人户",
|
||||
@ -98,6 +99,7 @@ class TestCatalog:
|
||||
"支付环节",
|
||||
"产品阈值",
|
||||
"月工资口径",
|
||||
"审批开关",
|
||||
}
|
||||
|
||||
def test_企业户比例按到账日回溯_2千分3千分双种子(self, client: TestClient, admin: dict) -> None:
|
||||
|
||||
@ -129,7 +129,7 @@ class TestSchemaSync:
|
||||
class TestSeedData:
|
||||
"""字典数据与版本标记。"""
|
||||
|
||||
@pytest.mark.parametrize("role_code", ["admin", "user"])
|
||||
@pytest.mark.parametrize("role_code", ["admin", "user", "marketing_ops", "finance", "leader"])
|
||||
def test_角色初始化数据在两处都存在(self, role_code: str) -> None:
|
||||
migration_sql = "\n".join(
|
||||
path.read_text(encoding="utf-8") for path in sorted(MIGRATION_DIR.glob("V*.sql"))
|
||||
|
||||
@ -20,6 +20,10 @@ EXPECTED_TABLES = {
|
||||
"login_logs",
|
||||
"operation_logs",
|
||||
"business_roles",
|
||||
"system_settings",
|
||||
"approval_orders",
|
||||
"approval_steps",
|
||||
"business_secondary_allocations",
|
||||
"param_definitions",
|
||||
"param_change_batches",
|
||||
"param_values",
|
||||
|
||||
@ -18,7 +18,7 @@ from app.services.param_registry import (
|
||||
validate_and_normalize,
|
||||
)
|
||||
|
||||
# 与 db/migration/V3 迁移种子保持一致的 16 项编码
|
||||
# 与 db/migration 迁移种子保持一致的 18 项编码(V3 十六项 + V6 审批开关两项)
|
||||
EXPECTED_CODES = {
|
||||
"salesperson.rate.start",
|
||||
"salesperson.rate.slope",
|
||||
@ -36,14 +36,16 @@ EXPECTED_CODES = {
|
||||
"salary.basic.mode",
|
||||
"probation.annual.threshold",
|
||||
"probation.salary.basic",
|
||||
"approval.secondary_allocation.enabled",
|
||||
"approval.monthly_settlement.enabled",
|
||||
}
|
||||
|
||||
|
||||
class TestRegistry:
|
||||
"""注册表元数据完整性(与迁移种子同源)。"""
|
||||
|
||||
def test_注册表共16项(self) -> None:
|
||||
assert len(PARAM_REGISTRY) == 16
|
||||
def test_注册表共18项(self) -> None:
|
||||
assert len(PARAM_REGISTRY) == 18
|
||||
|
||||
def test_编码集合与迁移种子一致(self) -> None:
|
||||
assert set(PARAM_REGISTRY) == EXPECTED_CODES
|
||||
@ -61,10 +63,27 @@ class TestRegistry:
|
||||
continue
|
||||
if spec.value_type in {"DECIMAL", "INT"}:
|
||||
parse_fraction(spec.default_value) # 不可解析会抛 BadRequestError
|
||||
elif spec.value_type == "BOOL":
|
||||
assert spec.default_value in {"0", "1"}, f"{spec.code} 默认值非 0/1"
|
||||
elif spec.value_type == "ENUM":
|
||||
assert spec.options, f"{spec.code} 为 ENUM 但无选项"
|
||||
assert spec.default_value in spec.options
|
||||
|
||||
def test_BOOL开关_仅接受0与1(self) -> None:
|
||||
"""V6 首例 BOOL 注册的两个审批开关:合法值与非法值边界。"""
|
||||
for code in (
|
||||
"approval.secondary_allocation.enabled",
|
||||
"approval.monthly_settlement.enabled",
|
||||
):
|
||||
spec = PARAM_REGISTRY[code]
|
||||
assert spec.value_type == "BOOL"
|
||||
assert validate_and_normalize(spec, "0") == "0"
|
||||
assert validate_and_normalize(spec, "1") == "1"
|
||||
with pytest.raises(BadRequestError):
|
||||
validate_and_normalize(spec, "true")
|
||||
with pytest.raises(BadRequestError):
|
||||
validate_and_normalize(spec, "2")
|
||||
|
||||
def test_取值域上下限可解析(self) -> None:
|
||||
for spec in PARAM_REGISTRY.values():
|
||||
if spec.value_min is not None:
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 删除顺序遵循外键依赖:先删子表
|
||||
DROP TABLE IF EXISTS `approval_steps`;
|
||||
DROP TABLE IF EXISTS `approval_orders`;
|
||||
DROP TABLE IF EXISTS `business_secondary_allocations`;
|
||||
DROP TABLE IF EXISTS `system_settings`;
|
||||
DROP TABLE IF EXISTS `param_values`;
|
||||
DROP TABLE IF EXISTS `param_change_batches`;
|
||||
DROP TABLE IF EXISTS `param_definitions`;
|
||||
@ -408,6 +412,14 @@ 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);
|
||||
|
||||
-- 2026-10-15 复核审批补充需求:三个职能登录角色(与 V6 迁移种子一致;
|
||||
-- 内置保护由 role_service 的 BUILTIN_ROLE_CODES 白名单承担)
|
||||
INSERT INTO roles (role_code, role_name, description, sort_order) VALUES ('marketing_ops', '营销后勤', '提交与录入(审批链第一节点:月结/二次分配提交)', 3);
|
||||
|
||||
INSERT INTO roles (role_code, role_name, description, sort_order) VALUES ('finance', '财务', '复核(审批链第二节点)', 4);
|
||||
|
||||
INSERT INTO roles (role_code, role_name, description, sort_order) VALUES ('leader', '领导', '审核(审批链第三节点;全览只读)', 5);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 字典数据:业务岗位与计提参数(与 V3 迁移种子一致;口径依据 2026-10-12 业务裁决)
|
||||
-- 显式指定 id:param_values 按 param_id 引用定义,与迁移种子保持同源一致
|
||||
@ -436,7 +448,9 @@ INSERT INTO param_definitions (id, param_code, param_name, param_group, value_ty
|
||||
(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);
|
||||
(16, 'probation.salary.basic', '试用期基本工资标准', '月工资口径', 'DECIMAL', '元', 'NONE', NULL, '3000', '0', NULL, '试用期仅发基本工资、不发放绩效(归属待 M2 复核)', 16),
|
||||
(17, 'approval.secondary_allocation.enabled', '二次分配审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '0', NULL, NULL, '0=二次分配方案提交即生效(系统自动通过全节点留痕,留痕不断链);1=提交后进入财务复核→领导审核审批流(2026-10-15 复核审批补充需求)', 17),
|
||||
(18, 'approval.monthly_settlement.enabled', '月度结算审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '0', NULL, NULL, '预留参数:月度结算审批链开关(M3 ④ 月结子块接入时消费);0=结算生成即生效,1=走财务复核→领导审核审批流', 18);
|
||||
|
||||
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES
|
||||
(1, 1, 'salesperson', '2026-02-17', '10', 1),
|
||||
@ -457,7 +471,9 @@ INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_t
|
||||
(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);
|
||||
(19, 16, '', '2026-01-01', '3000', 1),
|
||||
(20, 17, '', '2026-01-01', '0', 1),
|
||||
(21, 18, '', '2026-01-01', '0', 1);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 业务表(V5 迁移;业务编号自动生成,五项汇总冗余列由服务层事务内维护)
|
||||
@ -663,6 +679,72 @@ INSERT INTO customer_rating_grades (grade_name, min_score, max_score, sort_order
|
||||
('C', 60, 74, 3, '合格'),
|
||||
('D', 0, 59, 4, '待改进');
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 系统设置/二次分配/审批(V6 迁移;公司信息单行;分摊计算待澄清另批)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE system_settings (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
company_name VARCHAR(64) NOT NULL COMMENT '公司名称(登录页与导出页眉展示)',
|
||||
logo_path VARCHAR(255) COMMENT 'logo 相对路径(文件存 uploads/logo/,null=未上传使用默认)',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_system_settings PRIMARY KEY (id)
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='系统设置表(公司信息,全系统单行)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE approval_orders (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
order_no VARCHAR(32) NOT NULL COMMENT '审批单号,自动生成 AP{YYYYMMDD}-{4位当日序号}',
|
||||
order_type TINYINT NOT NULL COMMENT '审批对象类型:1二次分配方案 2月度结算(预留)',
|
||||
ref_id BIGINT UNSIGNED NOT NULL COMMENT '引用对象ID(order_type=1 时为 businesses.id;固化无外键,删除保护由服务层承担)',
|
||||
refer_no VARCHAR(32) NOT NULL COMMENT '引用对象单号(冗余固化,列表页免联查)',
|
||||
status TINYINT NOT NULL COMMENT '审批状态:1待财务复核 2待领导审核 3已通过 4已驳回 5已撤回' DEFAULT '1',
|
||||
submitter_id BIGINT UNSIGNED COMMENT '提交人ID(账号删除后置空)',
|
||||
submitter_name VARCHAR(64) NOT NULL COMMENT '提交人用户名(冗余固化)',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_approval_orders PRIMARY KEY (id),
|
||||
CONSTRAINT uk_approval_orders_order_no UNIQUE (order_no),
|
||||
CONSTRAINT fk_approval_orders_submitter_id FOREIGN KEY(submitter_id) REFERENCES users (id) ON DELETE SET NULL
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='审批单表(营销后勤提交→财务复核→领导审核;开关关闭时自动通过留痕)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE INDEX idx_approval_orders_type_status ON approval_orders (order_type, status);
|
||||
|
||||
CREATE TABLE approval_steps (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
order_id BIGINT UNSIGNED NOT NULL COMMENT '审批单ID(approval_orders.id;单删记录随删)',
|
||||
node TINYINT NOT NULL COMMENT '审批节点:0撤回留痕 1财务复核 2领导审核',
|
||||
action TINYINT NOT NULL COMMENT '动作:1通过 2驳回 3自动通过 4撤回',
|
||||
opinion VARCHAR(255) COMMENT '审批意见(驳回时填写)',
|
||||
operator_id BIGINT UNSIGNED COMMENT '操作人ID(自动通过/系统动作置空)',
|
||||
operator_name VARCHAR(64) NOT NULL COMMENT '操作人用户名(冗余固化;系统动作记「系统」)',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_approval_steps PRIMARY KEY (id),
|
||||
CONSTRAINT fk_approval_steps_operator_id FOREIGN KEY(operator_id) REFERENCES users (id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_approval_steps_order_id FOREIGN KEY(order_id) REFERENCES approval_orders (id) ON DELETE CASCADE
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='审批节点记录表(append-only;自动通过/撤回均有留痕)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE INDEX idx_approval_steps_order_id ON approval_steps (order_id);
|
||||
|
||||
CREATE TABLE business_secondary_allocations (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
business_id BIGINT UNSIGNED NOT NULL COMMENT '业务ID(businesses.id;业务删除随删)',
|
||||
personnel_id BIGINT UNSIGNED NOT NULL COMMENT '参与业务员ID(personnel.id;被方案引用禁删)',
|
||||
personnel_name VARCHAR(64) NOT NULL COMMENT '参与业务员姓名(冗余固化,离职后回显有名)',
|
||||
ratio DECIMAL(5, 2) NOT NULL COMMENT '分摊比例(0-100 两位小数;不要求合计满额,余额归属待澄清#2)',
|
||||
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_secondary_allocations PRIMARY KEY (id),
|
||||
CONSTRAINT fk_business_secondary_allocations_business_id FOREIGN KEY(business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_business_secondary_allocations_personnel_id FOREIGN KEY(personnel_id) REFERENCES personnel (id) ON DELETE RESTRICT
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='业务二次分配方案明细表(合同建立时录入;比例不要求合计满额,余额归属待澄清)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE INDEX idx_business_secondary_allocations_business_id ON business_secondary_allocations (business_id);
|
||||
|
||||
CREATE INDEX idx_business_secondary_allocations_personnel_id ON business_secondary_allocations (personnel_id);
|
||||
|
||||
-- 公司信息单行(system_settings 恒为一行,id=1 由迁移保底)
|
||||
INSERT INTO system_settings (id, company_name, logo_path) VALUES (1, '衍水集团', NULL);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 迁移版本记录:须等于当前 head,使 alembic upgrade head 成为空操作
|
||||
-- ---------------------------------------------------------------------------
|
||||
@ -671,4 +753,4 @@ CREATE TABLE alembic_version (
|
||||
CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COLLATE utf8mb4_general_ci;
|
||||
|
||||
INSERT INTO alembic_version (version_num) VALUES ('8efc189b131f');
|
||||
INSERT INTO alembic_version (version_num) VALUES ('79bcc5aa98d4');
|
||||
|
||||
80
db/migration/V6__add_settings_allocations_approvals.sql
Normal file
80
db/migration/V6__add_settings_allocations_approvals.sql
Normal file
@ -0,0 +1,80 @@
|
||||
-- Running upgrade 8efc189b131f -> 79bcc5aa98d4
|
||||
|
||||
CREATE TABLE system_settings (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
company_name VARCHAR(64) NOT NULL COMMENT '公司名称(登录页与导出页眉展示)',
|
||||
logo_path VARCHAR(255) COMMENT 'logo 相对路径(文件存 uploads/logo/,null=未上传使用默认)',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_system_settings PRIMARY KEY (id)
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='系统设置表(公司信息,全系统单行)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE approval_orders (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
order_no VARCHAR(32) NOT NULL COMMENT '审批单号,自动生成 AP{YYYYMMDD}-{4位当日序号}',
|
||||
order_type TINYINT NOT NULL COMMENT '审批对象类型:1二次分配方案 2月度结算(预留)',
|
||||
ref_id BIGINT UNSIGNED NOT NULL COMMENT '引用对象ID(order_type=1 时为 businesses.id;固化无外键,删除保护由服务层承担)',
|
||||
refer_no VARCHAR(32) NOT NULL COMMENT '引用对象单号(冗余固化,列表页免联查)',
|
||||
status TINYINT NOT NULL COMMENT '审批状态:1待财务复核 2待领导审核 3已通过 4已驳回 5已撤回' DEFAULT '1',
|
||||
submitter_id BIGINT UNSIGNED COMMENT '提交人ID(账号删除后置空)',
|
||||
submitter_name VARCHAR(64) NOT NULL COMMENT '提交人用户名(冗余固化)',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_approval_orders PRIMARY KEY (id),
|
||||
CONSTRAINT fk_approval_orders_submitter_id FOREIGN KEY(submitter_id) REFERENCES users (id) ON DELETE SET NULL,
|
||||
CONSTRAINT uk_approval_orders_order_no UNIQUE (order_no)
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='审批单表(营销后勤提交→财务复核→领导审核;开关关闭时自动通过留痕)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE INDEX idx_approval_orders_type_status ON approval_orders (order_type, status);
|
||||
|
||||
CREATE TABLE approval_steps (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
order_id BIGINT UNSIGNED NOT NULL COMMENT '审批单ID(approval_orders.id;单删记录随删)',
|
||||
node TINYINT NOT NULL COMMENT '审批节点:0撤回留痕 1财务复核 2领导审核',
|
||||
action TINYINT NOT NULL COMMENT '动作:1通过 2驳回 3自动通过 4撤回',
|
||||
opinion VARCHAR(255) COMMENT '审批意见(驳回时填写)',
|
||||
operator_id BIGINT UNSIGNED COMMENT '操作人ID(自动通过/系统动作置空)',
|
||||
operator_name VARCHAR(64) NOT NULL COMMENT '操作人用户名(冗余固化;系统动作记「系统」)',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_approval_steps PRIMARY KEY (id),
|
||||
CONSTRAINT fk_approval_steps_operator_id FOREIGN KEY(operator_id) REFERENCES users (id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_approval_steps_order_id FOREIGN KEY(order_id) REFERENCES approval_orders (id) ON DELETE CASCADE
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='审批节点记录表(append-only;自动通过/撤回均有留痕)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE INDEX idx_approval_steps_order_id ON approval_steps (order_id);
|
||||
|
||||
CREATE TABLE business_secondary_allocations (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
business_id BIGINT UNSIGNED NOT NULL COMMENT '业务ID(businesses.id;业务删除随删)',
|
||||
personnel_id BIGINT UNSIGNED NOT NULL COMMENT '参与业务员ID(personnel.id;被方案引用禁删)',
|
||||
personnel_name VARCHAR(64) NOT NULL COMMENT '参与业务员姓名(冗余固化,离职后回显有名)',
|
||||
ratio NUMERIC(5, 2) NOT NULL COMMENT '分摊比例(0-100 两位小数;不要求合计满额,余额归属待澄清#2)',
|
||||
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_secondary_allocations PRIMARY KEY (id),
|
||||
CONSTRAINT fk_business_secondary_allocations_business_id FOREIGN KEY(business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_business_secondary_allocations_personnel_id FOREIGN KEY(personnel_id) REFERENCES personnel (id) ON DELETE RESTRICT
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='业务二次分配方案明细表(合同建立时录入;比例不要求合计满额,余额归属待澄清)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE INDEX idx_business_secondary_allocations_business_id ON business_secondary_allocations (business_id);
|
||||
|
||||
CREATE INDEX idx_business_secondary_allocations_personnel_id ON business_secondary_allocations (personnel_id);
|
||||
|
||||
INSERT INTO roles (role_code, role_name, description, sort_order) VALUES ('marketing_ops', '营销后勤', '提交与录入(审批链第一节点:月结/二次分配提交)', 3);
|
||||
|
||||
INSERT INTO roles (role_code, role_name, description, sort_order) VALUES ('finance', '财务', '复核(审批链第二节点)', 4);
|
||||
|
||||
INSERT INTO roles (role_code, role_name, description, sort_order) VALUES ('leader', '领导', '审核(审批链第三节点;全览只读)', 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, status) VALUES (17, 'approval.secondary_allocation.enabled', '二次分配审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '0', NULL, NULL, '0=二次分配方案提交即生效(系统自动通过全节点留痕,留痕不断链);1=提交后进入财务复核→领导审核审批流(2026-10-15 复核审批补充需求)', 17, 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, status) VALUES (18, 'approval.monthly_settlement.enabled', '月度结算审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '0', NULL, NULL, '预留参数:月度结算审批链开关(M3 ④ 月结子块接入时消费);0=结算生成即生效,1=走财务复核→领导审核审批流', 18, 1);
|
||||
|
||||
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (20, 17, '', '2026-01-01', '0', 1);
|
||||
|
||||
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (21, 18, '', '2026-01-01', '0', 1);
|
||||
|
||||
INSERT INTO system_settings (id, company_name, logo_path) VALUES (1, '衍水集团', NULL);
|
||||
|
||||
UPDATE alembic_version SET version_num='79bcc5aa98d4' WHERE alembic_version.version_num = '8efc189b131f';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user