feat(business): 新增业务数据七表及 V5 迁移(业务/明细/状态留痕/发货/回款/评级配置)

- businesses 五项汇总冗余列服务层事务内维护;毛利润不落库(派生值,M3 台账固化)
- 行级 product_category 折算锚点(#14);回款 settlement_status 预置为 M3 防重锚点
- 评级等级种子四条(迁移+init-dev 双写);编号前缀常量 YW/SH/HK
- 双轨 SQL 同步(V4/V5 重导,schema_sync+test_models 164 例全绿)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
liuwenxinandClaude Code committed 2026-10-13 11:22:34 +08:00
1 parent ca88ee7a0b
commit 20fcf06f51
12 files changed
+2138 -138

No files matched your search

@@ -0,0 +1,696 @@
"""业务数据管理七表:业务/业务明细/状态留痕/发货单/发货明细/回款/评级等级配置(M2)
- businesses:业务编号自动生成(报表 2.4.1「合同编号」),五项汇总冗余列
由服务层事务内维护;毛利润相关值不落库(派生值,M3 台账才是固化点)
- business_items:行级 product_category 为折算锚点(对照表 #14),指导价
人工录入、T+ 同步不覆盖
- business_status_logs:append-only,创建时写首条(old=new=1)
- shipments/shipment_items:发货日为回款天数基准日(对照表 #1)
- receipts:手动指定发货批次(#18);settlement_status 为 M3 核算防重锚点
- customer_rating_grades:等级映射可配置,种子四条随迁移初始化
Revision ID: 8efc189b131f
Revises: e1133a7c670a
Create Date: 2026-10-13 11:14:17.545320
"""
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "8efc189b131f"
down_revision: str | Sequence[str] | None = "e1133a7c670a"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
# 评级等级种子(与 init-dev.sql 双写一致;口径见模型 docstring)
_GRADE_SEED = [
{"grade_name": "A", "min_score": 90, "max_score": 100, "sort_order": 1, "remark": "优秀"},
{"grade_name": "B", "min_score": 75, "max_score": 89, "sort_order": 2, "remark": "良好"},
{"grade_name": "C", "min_score": 60, "max_score": 74, "sort_order": 3, "remark": "合格"},
{"grade_name": "D", "min_score": 0, "max_score": 59, "sort_order": 4, "remark": "待改进"},
]
def upgrade() -> None:
"""应用迁移。"""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"customer_rating_grades",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column(
"grade_name", sa.String(length=32), nullable=False, comment="等级名称(如 A/优秀)"
),
sa.Column(
"min_score",
sa.Numeric(precision=5, scale=1),
nullable=False,
comment="总分下限(含);判定=按 sort_order 升序首个 min_score ≤ 总分",
),
sa.Column(
"max_score",
sa.Numeric(precision=5, scale=1),
nullable=False,
comment="总分上限(展示用)",
),
sa.Column(
"sort_order",
sa.Integer(),
server_default="0",
nullable=False,
comment="排序(判定顺序,升序)",
),
sa.Column("remark", sa.String(length=255), nullable=True, 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.PrimaryKeyConstraint("id", name=op.f("pk_customer_rating_grades")),
sa.UniqueConstraint("grade_name", name=op.f("uk_customer_rating_grades_grade_name")),
comment="客户评级等级配置表(等级映射可配置;判定按 sort_order 升序首个 min_score≤总分)",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_table(
"business_status_logs",
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(固化,无外键;删除保护由服务层承担)",
),
sa.Column(
"old_status", mysql.TINYINT(), nullable=False, comment="变更前状态(创建首条为 1)"
),
sa.Column("new_status", mysql.TINYINT(), nullable=False, comment="变更后状态"),
sa.Column(
"change_reason", 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_business_status_logs_operator_id"),
ondelete="SET NULL",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_business_status_logs")),
comment="业务状态变更记录表(append-only)",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_index(
"idx_business_status_logs_business_id",
"business_status_logs",
["business_id"],
unique=False,
)
op.create_index(
"idx_business_status_logs_create_time",
"business_status_logs",
["create_time"],
unique=False,
)
op.create_table(
"businesses",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column(
"business_no",
sa.String(length=32),
nullable=False,
comment="业务编号,自动生成 YW{YYYYMMDD}-{4位当日序号}(报表合同编号)",
),
sa.Column(
"customer_id",
mysql.BIGINT(unsigned=True),
nullable=False,
comment="客户ID(customers.id)",
),
sa.Column("business_date", sa.Date(), nullable=True, comment="业务日期(T+ 同步口径)"),
sa.Column(
"status",
mysql.TINYINT(),
server_default="1",
nullable=False,
comment="业务状态:1执行中 2已完成 3已终止(终态不可变更;T+同步不覆盖)",
),
sa.Column(
"owner_personnel_id",
mysql.BIGINT(unsigned=True),
nullable=True,
comment="负责营销人员ID(核算归属锚点;NULL=待指定,M3 核算前置校验非空;变更留痕不回溯)",
),
sa.Column(
"assistant_personnel_id",
mysql.BIGINT(unsigned=True),
nullable=True,
comment="协助营销人员ID(一期核算不使用,对照表 #15;仅记录事实)",
),
sa.Column(
"is_special",
mysql.TINYINT(),
server_default="0",
nullable=False,
comment="特殊业务标记:1是 0否(公司参与等)",
),
sa.Column("special_note", sa.String(length=255), nullable=True, comment="特殊业务说明"),
sa.Column(
"total_sale_amount",
sa.Numeric(precision=14, scale=2),
server_default="0",
nullable=True,
comment="销售额合计(明细行汇总冗余,服务层事务内维护,禁手工直改;不含运费口径)",
),
sa.Column(
"total_cost_amount",
sa.Numeric(precision=14, scale=2),
server_default="0",
nullable=True,
comment="成本合计(明细行汇总冗余,服务层事务内维护,禁手工直改)",
),
sa.Column(
"total_ship_amount",
sa.Numeric(precision=14, scale=2),
server_default="0",
nullable=True,
comment="发货金额合计(发货明细汇总冗余,服务层事务内维护,禁手工直改)",
),
sa.Column(
"total_received_amount",
sa.Numeric(precision=14, scale=2),
server_default="0",
nullable=True,
comment="回款合计(回款记录汇总冗余,服务层事务内维护;M3 起付判定直接消费)",
),
sa.Column(
"total_freight_amount",
sa.Numeric(precision=14, scale=2),
server_default="0",
nullable=True,
comment="运费合计(发货单汇总冗余,仅记录展示;不计入毛利润口径,对照表 #2)",
),
sa.Column("remark", sa.String(length=255), nullable=True, 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.Column(
"source_no",
sa.String(length=64),
nullable=True,
comment="T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)",
),
sa.Column("t_plus_id", sa.String(length=64), nullable=True, comment="T+ 系统内部标识"),
sa.Column(
"sync_batch_no",
sa.String(length=32),
nullable=True,
comment="同步批次号(同步模块写入)",
),
sa.Column("synced_at", sa.DateTime(), nullable=True, comment="最近同步时间"),
sa.ForeignKeyConstraint(
["assistant_personnel_id"],
["personnel.id"],
name=op.f("fk_businesses_assistant_personnel_id"),
ondelete="RESTRICT",
),
sa.ForeignKeyConstraint(
["customer_id"],
["customers.id"],
name=op.f("fk_businesses_customer_id"),
ondelete="RESTRICT",
),
sa.ForeignKeyConstraint(
["owner_personnel_id"],
["personnel.id"],
name=op.f("fk_businesses_owner_personnel_id"),
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_businesses")),
sa.UniqueConstraint("business_no", name=op.f("uk_businesses_business_no")),
comment="业务表(业务编号自动生成;T+同步不覆盖业务状态)",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_index("idx_businesses_business_date", "businesses", ["business_date"], unique=False)
op.create_index("idx_businesses_customer_id", "businesses", ["customer_id"], unique=False)
op.create_index(
"idx_businesses_owner_personnel_id", "businesses", ["owner_personnel_id"], unique=False
)
op.create_index(op.f("idx_businesses_source_no"), "businesses", ["source_no"], unique=False)
op.create_index("idx_businesses_status", "businesses", ["status"], unique=False)
op.create_table(
"business_items",
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(
"product_id",
mysql.BIGINT(unsigned=True),
nullable=True,
comment="产品ID(products.id;NULL=自由录入,未入产品库)",
),
sa.Column(
"product_name",
sa.String(length=128),
nullable=False,
comment="产品名称(引用产品时冗余固化)",
),
sa.Column(
"spec_model",
sa.String(length=128),
server_default="",
nullable=False,
comment="规格型号(冗余固化;空串对齐 products 组合唯一键口径)",
),
sa.Column("unit", sa.String(length=16), nullable=True, comment="计量单位"),
sa.Column(
"product_category",
sa.String(length=32),
nullable=False,
comment="产品类别(行级折算锚点,混合业务按产品逐条计算,对照表 #14;值域同 PRODUCT_CATEGORIES)",
),
sa.Column(
"quantity", sa.Numeric(precision=14, scale=2), nullable=False, comment="数量(>0)"
),
sa.Column(
"sale_price",
sa.Numeric(precision=14, scale=4),
nullable=True,
comment="销售单价(元;T+ 同步或人工)",
),
sa.Column(
"sale_amount",
sa.Numeric(precision=14, scale=2),
nullable=False,
comment="销售额 = 数量×销售单价(服务层计算固化;不含运费口径,毛利润率分母)",
),
sa.Column(
"cost_amount",
sa.Numeric(precision=14, scale=2),
nullable=True,
comment="成本额(T+ 同步或人工录入,来源待 T+ 确认;毛利润额 = 销售额 − 成本额)",
),
sa.Column(
"guided_price",
sa.Numeric(precision=14, scale=4),
nullable=True,
comment="指导价(人工录入;T+同步不覆盖,需规 2.2.1/2.2.4)",
),
sa.Column("remark", sa.String(length=255), nullable=True, 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(
["business_id"],
["businesses.id"],
name=op.f("fk_business_items_business_id"),
ondelete="RESTRICT",
),
sa.ForeignKeyConstraint(
["product_id"],
["products.id"],
name=op.f("fk_business_items_product_id"),
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_business_items")),
comment="业务产品明细表(毛利润行级源数据;指导价 T+同步不覆盖)",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_index(
"idx_business_items_business_id", "business_items", ["business_id"], unique=False
)
op.create_index(
"idx_business_items_product_category", "business_items", ["product_category"], unique=False
)
op.create_index("idx_business_items_product_id", "business_items", ["product_id"], unique=False)
op.create_table(
"shipments",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column(
"shipment_no",
sa.String(length=32),
nullable=False,
comment="发货单号,自动生成 SH{YYYYMMDD}-{4位当日序号}(内部追溯用)",
),
sa.Column(
"business_id",
mysql.BIGINT(unsigned=True),
nullable=False,
comment="业务ID(businesses.id;终态业务禁止新发货)",
),
sa.Column(
"ship_date",
sa.Date(),
nullable=False,
comment="发货日期(回款天数基准日,发货日首日=第 1 天)",
),
sa.Column(
"freight_amount",
sa.Numeric(precision=14, scale=2),
server_default="0",
nullable=False,
comment="运费金额(仅记录展示;不计入毛利润口径,对照表 #2)",
),
sa.Column(
"total_amount",
sa.Numeric(precision=14, scale=2),
server_default="0",
nullable=False,
comment="发货总金额(明细行汇总冗余,服务层事务内维护,禁手工直改)",
),
sa.Column("remark", sa.String(length=255), nullable=True, 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.Column(
"source_no",
sa.String(length=64),
nullable=True,
comment="T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)",
),
sa.Column("t_plus_id", sa.String(length=64), nullable=True, comment="T+ 系统内部标识"),
sa.Column(
"sync_batch_no",
sa.String(length=32),
nullable=True,
comment="同步批次号(同步模块写入)",
),
sa.Column("synced_at", sa.DateTime(), nullable=True, comment="最近同步时间"),
sa.ForeignKeyConstraint(
["business_id"],
["businesses.id"],
name=op.f("fk_shipments_business_id"),
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_shipments")),
sa.UniqueConstraint("shipment_no", name=op.f("uk_shipments_shipment_no")),
comment="发货单表(发货日为回款天数基准日,发货日首日计第 1 天)",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_index("idx_shipments_business_id", "shipments", ["business_id"], unique=False)
op.create_index("idx_shipments_ship_date", "shipments", ["ship_date"], unique=False)
op.create_index(op.f("idx_shipments_source_no"), "shipments", ["source_no"], unique=False)
op.create_table(
"receipts",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column(
"receipt_batch_no",
sa.String(length=32),
nullable=False,
comment="回款批次号,自动生成 HK{YYYYMMDD}-{4位当日序号}(系统内唯一;T+ 同步回款以 source_no 幂等)",
),
sa.Column(
"business_id",
mysql.BIGINT(unsigned=True),
nullable=False,
comment="业务ID(businesses.id)",
),
sa.Column(
"shipment_id",
mysql.BIGINT(unsigned=True),
nullable=True,
comment="发货单ID(shipments.id;手动指定的回款对应批次,须与本行业务一致;NULL=暂未指定)",
),
sa.Column(
"receipt_date",
sa.Date(),
nullable=False,
comment="回款到账日(M3 按此日匹配参数版本,对照表 #11)",
),
sa.Column(
"amount",
sa.Numeric(precision=14, scale=2),
nullable=False,
comment="回款金额(≥0;一期禁负数/红字)",
),
sa.Column(
"is_barter",
mysql.TINYINT(),
server_default="0",
nullable=False,
comment="以物抵顶标记:1是 0否(M3 核算按应发绩效×barter.discount 折算,M2 仅标记)",
),
sa.Column(
"settlement_status",
mysql.TINYINT(),
server_default="0",
nullable=False,
comment="绩效核算状态:0未核算 1已核算(M3 核算防重锚点;已核算回款禁改禁删)",
),
sa.Column("remark", sa.String(length=255), nullable=True, 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.Column(
"source_no",
sa.String(length=64),
nullable=True,
comment="T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)",
),
sa.Column("t_plus_id", sa.String(length=64), nullable=True, comment="T+ 系统内部标识"),
sa.Column(
"sync_batch_no",
sa.String(length=32),
nullable=True,
comment="同步批次号(同步模块写入)",
),
sa.Column("synced_at", sa.DateTime(), nullable=True, comment="最近同步时间"),
sa.ForeignKeyConstraint(
["business_id"],
["businesses.id"],
name=op.f("fk_receipts_business_id"),
ondelete="RESTRICT",
),
sa.ForeignKeyConstraint(
["shipment_id"],
["shipments.id"],
name=op.f("fk_receipts_shipment_id"),
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_receipts")),
sa.UniqueConstraint("receipt_batch_no", name=op.f("uk_receipts_receipt_batch_no")),
comment="回款表(手动指定发货批次;已核算回款禁改禁删)",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_index("idx_receipts_business_id", "receipts", ["business_id"], unique=False)
op.create_index("idx_receipts_receipt_date", "receipts", ["receipt_date"], unique=False)
op.create_index(
"idx_receipts_settlement_status", "receipts", ["settlement_status"], unique=False
)
op.create_index("idx_receipts_shipment_id", "receipts", ["shipment_id"], unique=False)
op.create_index(op.f("idx_receipts_source_no"), "receipts", ["source_no"], unique=False)
op.create_table(
"shipment_items",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column(
"shipment_id",
mysql.BIGINT(unsigned=True),
nullable=False,
comment="发货单ID(shipments.id)",
),
sa.Column(
"product_id",
mysql.BIGINT(unsigned=True),
nullable=True,
comment="产品ID(products.id;NULL=自由录入)",
),
sa.Column(
"product_name",
sa.String(length=128),
nullable=False,
comment="产品名称(引用产品时冗余固化)",
),
sa.Column(
"spec_model",
sa.String(length=128),
server_default="",
nullable=False,
comment="规格型号(冗余固化;空串对齐 products 组合唯一键口径)",
),
sa.Column("unit", sa.String(length=16), nullable=True, comment="计量单位"),
sa.Column(
"quantity", sa.Numeric(precision=14, scale=2), nullable=False, comment="发货数量(>0)"
),
sa.Column(
"sale_price",
sa.Numeric(precision=14, scale=4),
nullable=True,
comment="发货单价(元;T+ 同步或人工,口径待 IT 确认)",
),
sa.Column(
"amount",
sa.Numeric(precision=14, scale=2),
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(
["product_id"],
["products.id"],
name=op.f("fk_shipment_items_product_id"),
ondelete="RESTRICT",
),
sa.ForeignKeyConstraint(
["shipment_id"],
["shipments.id"],
name=op.f("fk_shipment_items_shipment_id"),
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_shipment_items")),
comment="发货产品明细表",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_index("idx_shipment_items_product_id", "shipment_items", ["product_id"], unique=False)
op.create_index(
"idx_shipment_items_shipment_id", "shipment_items", ["shipment_id"], unique=False
)
# ### end Alembic commands ###
# 评级等级种子(等级映射可配置;双轨:init-dev.sql 同步写入)
op.bulk_insert(
sa.table(
"customer_rating_grades",
sa.column("grade_name", sa.String),
sa.column("min_score", sa.Numeric),
sa.column("max_score", sa.Numeric),
sa.column("sort_order", sa.Integer),
sa.column("remark", sa.String),
),
_GRADE_SEED,
)
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 连带删除索引与外键);
# 逐条 drop_index 会先于 drop_table 摘掉外键支撑索引而报 MySQL 1553
op.drop_table("shipment_items")
op.drop_table("receipts")
op.drop_table("shipments")
op.drop_table("business_items")
op.drop_table("businesses")
op.drop_table("business_status_logs")
op.drop_table("customer_rating_grades")
@@ -11,6 +11,7 @@ Revises: bfbda9c3191d
Create Date: 2026-10-13 09:36:02.219248
"""
from collections.abc import Sequence
from alembic import op
@@ -18,8 +19,8 @@ import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = 'e1133a7c670a'
down_revision: str | Sequence[str] | None = 'bfbda9c3191d'
revision: str = "e1133a7c670a"
down_revision: str | Sequence[str] | None = "bfbda9c3191d"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
@@ -27,141 +28,449 @@ depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""应用迁移。"""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('products',
sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'),
sa.Column('product_name', sa.String(length=128), nullable=False, comment='产品名称'),
sa.Column('spec_model', sa.String(length=128), server_default='', nullable=False, comment='规格型号(空串而非 NULL:参与组合唯一键,MySQL 组合索引对 NULL 不去重)'),
sa.Column('unit', sa.String(length=16), nullable=True, comment='计量单位'),
sa.Column('stock_quantity', sa.Numeric(precision=14, scale=2), nullable=True, comment='数量(T+ 同步原样存储,语义待 IT 确认)'),
sa.Column('sale_price', sa.Numeric(precision=14, scale=4), nullable=True, comment='销售单价(元,T+ 同步)'),
sa.Column('product_category', sa.String(length=32), nullable=False, comment='产品类别:municipal_water市政水利 rail_transit轨道交通(决定折算阈值 30%/20%;T+同步不覆盖)'),
sa.Column('status', mysql.TINYINT(), server_default='1', nullable=False, comment='状态:1启用 0停用'),
sa.Column('remark', sa.String(length=255), nullable=True, 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.Column('source_no', sa.String(length=64), nullable=True, comment='T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)'),
sa.Column('t_plus_id', sa.String(length=64), nullable=True, comment='T+ 系统内部标识'),
sa.Column('sync_batch_no', sa.String(length=32), nullable=True, comment='同步批次号(同步模块写入)'),
sa.Column('synced_at', sa.DateTime(), nullable=True, comment='最近同步时间'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_products')),
sa.UniqueConstraint('product_name', 'spec_model', name='uk_products_name_spec'),
comment='产品表(类别锚定折算阈值参数;T+同步不覆盖产品类别)',
mysql_charset='utf8mb4',
mysql_collate='utf8mb4_general_ci',
mysql_engine='InnoDB'
op.create_table(
"products",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column("product_name", sa.String(length=128), nullable=False, comment="产品名称"),
sa.Column(
"spec_model",
sa.String(length=128),
server_default="",
nullable=False,
comment="规格型号(空串而非 NULL:参与组合唯一键,MySQL 组合索引对 NULL 不去重)",
),
sa.Column("unit", sa.String(length=16), nullable=True, comment="计量单位"),
sa.Column(
"stock_quantity",
sa.Numeric(precision=14, scale=2),
nullable=True,
comment="数量(T+ 同步原样存储,语义待 IT 确认)",
),
sa.Column(
"sale_price",
sa.Numeric(precision=14, scale=4),
nullable=True,
comment="销售单价(元,T+ 同步)",
),
sa.Column(
"product_category",
sa.String(length=32),
nullable=False,
comment="产品类别:municipal_water市政水利 rail_transit轨道交通(决定折算阈值 30%/20%;T+同步不覆盖)",
),
sa.Column(
"status",
mysql.TINYINT(),
server_default="1",
nullable=False,
comment="状态:1启用 0停用",
),
sa.Column("remark", sa.String(length=255), nullable=True, 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.Column(
"source_no",
sa.String(length=64),
nullable=True,
comment="T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)",
),
sa.Column("t_plus_id", sa.String(length=64), nullable=True, comment="T+ 系统内部标识"),
sa.Column(
"sync_batch_no",
sa.String(length=32),
nullable=True,
comment="同步批次号(同步模块写入)",
),
sa.Column("synced_at", sa.DateTime(), nullable=True, comment="最近同步时间"),
sa.PrimaryKeyConstraint("id", name=op.f("pk_products")),
sa.UniqueConstraint("product_name", "spec_model", name="uk_products_name_spec"),
comment="产品表(类别锚定折算阈值参数;T+同步不覆盖产品类别)",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_index(op.f('idx_products_source_no'), 'products', ['source_no'], unique=False)
op.create_table('ownership_change_logs',
sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'),
sa.Column('owner_type', mysql.TINYINT(), nullable=False, comment='归属类型:1客户归属 2业务归属'),
sa.Column('customer_id', mysql.BIGINT(unsigned=True), nullable=True, comment='客户ID(固化,owner_type=1 时非空)'),
sa.Column('business_id', mysql.BIGINT(unsigned=True), nullable=True, comment='业务ID(固化,owner_type=2 时非空)'),
sa.Column('old_owner_personnel_id', mysql.BIGINT(unsigned=True), nullable=True, comment='变更前负责营销人员ID(固化)'),
sa.Column('new_owner_personnel_id', mysql.BIGINT(unsigned=True), nullable=True, comment='变更后负责营销人员ID(固化)'),
sa.Column('old_assistant_personnel_id', mysql.BIGINT(unsigned=True), nullable=True, comment='变更前协助营销人员ID(固化,仅业务归属使用)'),
sa.Column('new_assistant_personnel_id', mysql.BIGINT(unsigned=True), nullable=True, comment='变更后协助营销人员ID(固化,仅业务归属使用)'),
sa.Column('change_reason', 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_ownership_change_logs_operator_id'), ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_ownership_change_logs')),
comment='归属变更记录表(append-only;客户/业务归属变更留痕,不回溯历史)',
mysql_charset='utf8mb4',
mysql_collate='utf8mb4_general_ci',
mysql_engine='InnoDB'
op.create_index(op.f("idx_products_source_no"), "products", ["source_no"], unique=False)
op.create_table(
"ownership_change_logs",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column(
"owner_type", mysql.TINYINT(), nullable=False, comment="归属类型:1客户归属 2业务归属"
),
sa.Column(
"customer_id",
mysql.BIGINT(unsigned=True),
nullable=True,
comment="客户ID(固化,owner_type=1 时非空)",
),
sa.Column(
"business_id",
mysql.BIGINT(unsigned=True),
nullable=True,
comment="业务ID(固化,owner_type=2 时非空)",
),
sa.Column(
"old_owner_personnel_id",
mysql.BIGINT(unsigned=True),
nullable=True,
comment="变更前负责营销人员ID(固化)",
),
sa.Column(
"new_owner_personnel_id",
mysql.BIGINT(unsigned=True),
nullable=True,
comment="变更后负责营销人员ID(固化)",
),
sa.Column(
"old_assistant_personnel_id",
mysql.BIGINT(unsigned=True),
nullable=True,
comment="变更前协助营销人员ID(固化,仅业务归属使用)",
),
sa.Column(
"new_assistant_personnel_id",
mysql.BIGINT(unsigned=True),
nullable=True,
comment="变更后协助营销人员ID(固化,仅业务归属使用)",
),
sa.Column("change_reason", 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_ownership_change_logs_operator_id"),
ondelete="SET NULL",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_ownership_change_logs")),
comment="归属变更记录表(append-only;客户/业务归属变更留痕,不回溯历史)",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_index(op.f('idx_ownership_change_logs_customer_id'), 'ownership_change_logs', ['customer_id'], unique=False)
op.create_index(op.f('idx_ownership_change_logs_business_id'), 'ownership_change_logs', ['business_id'], unique=False)
op.create_index(op.f('idx_ownership_change_logs_create_time'), 'ownership_change_logs', ['create_time'], unique=False)
op.create_table('personnel',
sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'),
sa.Column('personnel_name', sa.String(length=64), nullable=False, comment='姓名'),
sa.Column('business_role_id', mysql.BIGINT(unsigned=True), nullable=False, comment='岗位角色ID(决定计提公式模板;T+同步不覆盖)'),
sa.Column('manager_id', mysql.BIGINT(unsigned=True), nullable=True, comment='直属营销经理ID(NULL=经理本人或未分配;团队成员关系锚点)'),
sa.Column('phone', sa.String(length=20), nullable=True, comment='联系电话'),
sa.Column('hire_date', sa.Date(), nullable=True, comment='入职日期'),
sa.Column('is_probation', mysql.TINYINT(), server_default='0', nullable=False, comment='是否试用期:1是 0否(试用期绩效照算不发放)'),
sa.Column('salary_standard', sa.Numeric(precision=12, scale=2), nullable=True, comment='月工资标准(元;T+同步不覆盖)'),
sa.Column('status', mysql.TINYINT(), server_default='1', nullable=False, comment='状态:1在职 0离职(离职人员不可再被业务/客户归属引用)'),
sa.Column('remark', sa.String(length=255), nullable=True, 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.Column('source_no', sa.String(length=64), nullable=True, comment='T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)'),
sa.Column('t_plus_id', sa.String(length=64), nullable=True, comment='T+ 系统内部标识'),
sa.Column('sync_batch_no', sa.String(length=32), nullable=True, comment='同步批次号(同步模块写入)'),
sa.Column('synced_at', sa.DateTime(), nullable=True, comment='最近同步时间'),
sa.ForeignKeyConstraint(['business_role_id'], ['business_roles.id'], name=op.f('fk_personnel_business_role_id'), ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['manager_id'], ['personnel.id'], name=op.f('fk_personnel_manager_id'), ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_personnel')),
comment='营销人员表(T+同步不覆盖岗位与工资标准)',
mysql_charset='utf8mb4',
mysql_collate='utf8mb4_general_ci',
mysql_engine='InnoDB'
op.create_index(
op.f("idx_ownership_change_logs_customer_id"),
"ownership_change_logs",
["customer_id"],
unique=False,
)
op.create_index(op.f('idx_personnel_business_role_id'), 'personnel', ['business_role_id'], unique=False)
op.create_index(op.f('idx_personnel_manager_id'), 'personnel', ['manager_id'], unique=False)
op.create_index(op.f('idx_personnel_source_no'), 'personnel', ['source_no'], unique=False)
op.create_table('customers',
sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'),
sa.Column('company_name', sa.String(length=128), nullable=False, comment='公司名称'),
sa.Column('owner_name', sa.String(length=64), nullable=False, comment='客户负责人姓名(客户公司负责人的人名,非系统人员)'),
sa.Column('contact_person', sa.String(length=64), nullable=True, comment='联系人'),
sa.Column('contact_phone', sa.String(length=20), nullable=True, comment='联系电话'),
sa.Column('address', sa.String(length=255), nullable=True, comment='地址'),
sa.Column('industry', sa.String(length=64), nullable=True, comment='所属行业(自由文本)'),
sa.Column('customer_type', sa.String(length=32), server_default='personal', nullable=False, comment='客户分类:personal个人户 manager_personal经理个人户 enterprise企业户(计提规则匹配锚点;T+同步不覆盖)'),
sa.Column('owner_personnel_id', mysql.BIGINT(unsigned=True), nullable=True, comment='归属负责营销人员ID(手动修改不回溯历史,变更留痕见 ownership_change_logs)'),
sa.Column('remark', sa.String(length=255), nullable=True, 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.Column('source_no', sa.String(length=64), nullable=True, comment='T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)'),
sa.Column('t_plus_id', sa.String(length=64), nullable=True, comment='T+ 系统内部标识'),
sa.Column('sync_batch_no', sa.String(length=32), nullable=True, comment='同步批次号(同步模块写入)'),
sa.Column('synced_at', sa.DateTime(), nullable=True, comment='最近同步时间'),
sa.ForeignKeyConstraint(['owner_personnel_id'], ['personnel.id'], name=op.f('fk_customers_owner_personnel_id'), ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_customers')),
sa.UniqueConstraint('company_name', 'owner_name', name='uk_customers_company_owner'),
comment='客户表(公司名称+负责人为业务唯一标识;T+同步不覆盖客户分类)',
mysql_charset='utf8mb4',
mysql_collate='utf8mb4_general_ci',
mysql_engine='InnoDB'
op.create_index(
op.f("idx_ownership_change_logs_business_id"),
"ownership_change_logs",
["business_id"],
unique=False,
)
op.create_index(op.f('idx_customers_customer_type'), 'customers', ['customer_type'], unique=False)
op.create_index(op.f('idx_customers_owner_personnel_id'), 'customers', ['owner_personnel_id'], unique=False)
op.create_index(op.f('idx_customers_source_no'), 'customers', ['source_no'], unique=False)
op.create_table('personnel_plans',
sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'),
sa.Column('personnel_id', mysql.BIGINT(unsigned=True), nullable=False, comment='人员ID(personnel.id)'),
sa.Column('plan_period', sa.String(length=32), nullable=False, comment='计划期间(如 2026Q1、2026-05)'),
sa.Column('plan_content', sa.String(length=500), nullable=False, comment='计划内容'),
sa.Column('target_desc', sa.String(length=255), nullable=True, comment='指标说明(文字描述)'),
sa.Column('source_no', sa.String(length=64), nullable=True, comment='T+ 原始条目号(预留)'),
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(['personnel_id'], ['personnel.id'], name=op.f('fk_personnel_plans_personnel_id'), ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_personnel_plans')),
comment='市场拓展计划表',
mysql_charset='utf8mb4',
mysql_collate='utf8mb4_general_ci',
mysql_engine='InnoDB'
op.create_index(
op.f("idx_ownership_change_logs_create_time"),
"ownership_change_logs",
["create_time"],
unique=False,
)
op.create_index(op.f('idx_personnel_plans_personnel_id'), 'personnel_plans', ['personnel_id'], unique=False)
op.create_table('personnel_regions',
sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'),
sa.Column('personnel_id', mysql.BIGINT(unsigned=True), nullable=False, comment='人员ID(personnel.id)'),
sa.Column('region_name', sa.String(length=128), nullable=False, comment='区域名称(产品辐射地理范围)'),
sa.Column('remark', sa.String(length=255), nullable=True, 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(['personnel_id'], ['personnel.id'], name=op.f('fk_personnel_regions_personnel_id'), ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_personnel_regions')),
comment='产品辐射区域表',
mysql_charset='utf8mb4',
mysql_collate='utf8mb4_general_ci',
mysql_engine='InnoDB'
op.create_table(
"personnel",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column("personnel_name", sa.String(length=64), nullable=False, comment="姓名"),
sa.Column(
"business_role_id",
mysql.BIGINT(unsigned=True),
nullable=False,
comment="岗位角色ID(决定计提公式模板;T+同步不覆盖)",
),
sa.Column(
"manager_id",
mysql.BIGINT(unsigned=True),
nullable=True,
comment="直属营销经理ID(NULL=经理本人或未分配;团队成员关系锚点)",
),
sa.Column("phone", sa.String(length=20), nullable=True, comment="联系电话"),
sa.Column("hire_date", sa.Date(), nullable=True, comment="入职日期"),
sa.Column(
"is_probation",
mysql.TINYINT(),
server_default="0",
nullable=False,
comment="是否试用期:1是 0否(试用期绩效照算不发放)",
),
sa.Column(
"salary_standard",
sa.Numeric(precision=12, scale=2),
nullable=True,
comment="月工资标准(元;T+同步不覆盖)",
),
sa.Column(
"status",
mysql.TINYINT(),
server_default="1",
nullable=False,
comment="状态:1在职 0离职(离职人员不可再被业务/客户归属引用)",
),
sa.Column("remark", sa.String(length=255), nullable=True, 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.Column(
"source_no",
sa.String(length=64),
nullable=True,
comment="T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)",
),
sa.Column("t_plus_id", sa.String(length=64), nullable=True, comment="T+ 系统内部标识"),
sa.Column(
"sync_batch_no",
sa.String(length=32),
nullable=True,
comment="同步批次号(同步模块写入)",
),
sa.Column("synced_at", sa.DateTime(), nullable=True, comment="最近同步时间"),
sa.ForeignKeyConstraint(
["business_role_id"],
["business_roles.id"],
name=op.f("fk_personnel_business_role_id"),
ondelete="RESTRICT",
),
sa.ForeignKeyConstraint(
["manager_id"],
["personnel.id"],
name=op.f("fk_personnel_manager_id"),
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_personnel")),
comment="营销人员表(T+同步不覆盖岗位与工资标准)",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_index(
op.f("idx_personnel_business_role_id"), "personnel", ["business_role_id"], unique=False
)
op.create_index(op.f("idx_personnel_manager_id"), "personnel", ["manager_id"], unique=False)
op.create_index(op.f("idx_personnel_source_no"), "personnel", ["source_no"], unique=False)
op.create_table(
"customers",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column("company_name", sa.String(length=128), nullable=False, comment="公司名称"),
sa.Column(
"owner_name",
sa.String(length=64),
nullable=False,
comment="客户负责人姓名(客户公司负责人的人名,非系统人员)",
),
sa.Column("contact_person", sa.String(length=64), nullable=True, comment="联系人"),
sa.Column("contact_phone", sa.String(length=20), nullable=True, comment="联系电话"),
sa.Column("address", sa.String(length=255), nullable=True, comment="地址"),
sa.Column("industry", sa.String(length=64), nullable=True, comment="所属行业(自由文本)"),
sa.Column(
"customer_type",
sa.String(length=32),
server_default="personal",
nullable=False,
comment="客户分类:personal个人户 manager_personal经理个人户 enterprise企业户(计提规则匹配锚点;T+同步不覆盖)",
),
sa.Column(
"owner_personnel_id",
mysql.BIGINT(unsigned=True),
nullable=True,
comment="归属负责营销人员ID(手动修改不回溯历史,变更留痕见 ownership_change_logs)",
),
sa.Column("remark", sa.String(length=255), nullable=True, 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.Column(
"source_no",
sa.String(length=64),
nullable=True,
comment="T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)",
),
sa.Column("t_plus_id", sa.String(length=64), nullable=True, comment="T+ 系统内部标识"),
sa.Column(
"sync_batch_no",
sa.String(length=32),
nullable=True,
comment="同步批次号(同步模块写入)",
),
sa.Column("synced_at", sa.DateTime(), nullable=True, comment="最近同步时间"),
sa.ForeignKeyConstraint(
["owner_personnel_id"],
["personnel.id"],
name=op.f("fk_customers_owner_personnel_id"),
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_customers")),
sa.UniqueConstraint("company_name", "owner_name", name="uk_customers_company_owner"),
comment="客户表(公司名称+负责人为业务唯一标识;T+同步不覆盖客户分类)",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_index(
op.f("idx_customers_customer_type"), "customers", ["customer_type"], unique=False
)
op.create_index(
op.f("idx_customers_owner_personnel_id"), "customers", ["owner_personnel_id"], unique=False
)
op.create_index(op.f("idx_customers_source_no"), "customers", ["source_no"], unique=False)
op.create_table(
"personnel_plans",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column(
"personnel_id",
mysql.BIGINT(unsigned=True),
nullable=False,
comment="人员ID(personnel.id)",
),
sa.Column(
"plan_period",
sa.String(length=32),
nullable=False,
comment="计划期间(如 2026Q1、2026-05)",
),
sa.Column("plan_content", sa.String(length=500), nullable=False, comment="计划内容"),
sa.Column(
"target_desc", sa.String(length=255), nullable=True, comment="指标说明(文字描述)"
),
sa.Column(
"source_no", sa.String(length=64), nullable=True, comment="T+ 原始条目号(预留)"
),
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(
["personnel_id"],
["personnel.id"],
name=op.f("fk_personnel_plans_personnel_id"),
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_personnel_plans")),
comment="市场拓展计划表",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_index(
op.f("idx_personnel_plans_personnel_id"), "personnel_plans", ["personnel_id"], unique=False
)
op.create_table(
"personnel_regions",
sa.Column(
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
),
sa.Column(
"personnel_id",
mysql.BIGINT(unsigned=True),
nullable=False,
comment="人员ID(personnel.id)",
),
sa.Column(
"region_name",
sa.String(length=128),
nullable=False,
comment="区域名称(产品辐射地理范围)",
),
sa.Column("remark", sa.String(length=255), nullable=True, 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(
["personnel_id"],
["personnel.id"],
name=op.f("fk_personnel_regions_personnel_id"),
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_personnel_regions")),
comment="产品辐射区域表",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
mysql_engine="InnoDB",
)
op.create_index(
op.f("idx_personnel_regions_personnel_id"),
"personnel_regions",
["personnel_id"],
unique=False,
)
op.create_index(op.f('idx_personnel_regions_personnel_id'), 'personnel_regions', ['personnel_id'], unique=False)
# ### end Alembic commands ###
@@ -173,9 +482,9 @@ def downgrade() -> None:
单独 drop_index 会报 MySQL 1553(needed in a foreign key constraint)。
**整表回滚应只保留 `op.drop_table`**,DROP TABLE 会连带删除索引与外键。
"""
op.drop_table('personnel_regions')
op.drop_table('personnel_plans')
op.drop_table('customers')
op.drop_table('personnel')
op.drop_table('ownership_change_logs')
op.drop_table('products')
op.drop_table("personnel_regions")
op.drop_table("personnel_plans")
op.drop_table("customers")
op.drop_table("personnel")
op.drop_table("ownership_change_logs")
op.drop_table("products")