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:
parent
ca88ee7a0b
commit
20fcf06f51
@ -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
|
Create Date: 2026-10-13 09:36:02.219248
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
@ -18,8 +19,8 @@ import sqlalchemy as sa
|
|||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = 'e1133a7c670a'
|
revision: str = "e1133a7c670a"
|
||||||
down_revision: str | Sequence[str] | None = 'bfbda9c3191d'
|
down_revision: str | Sequence[str] | None = "bfbda9c3191d"
|
||||||
branch_labels: str | Sequence[str] | None = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: 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:
|
def upgrade() -> None:
|
||||||
"""应用迁移。"""
|
"""应用迁移。"""
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('products',
|
op.create_table(
|
||||||
sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'),
|
"products",
|
||||||
sa.Column('product_name', sa.String(length=128), nullable=False, comment='产品名称'),
|
sa.Column(
|
||||||
sa.Column('spec_model', sa.String(length=128), server_default='', nullable=False, comment='规格型号(空串而非 NULL:参与组合唯一键,MySQL 组合索引对 NULL 不去重)'),
|
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
|
||||||
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("product_name", sa.String(length=128), nullable=False, comment="产品名称"),
|
||||||
sa.Column('sale_price', sa.Numeric(precision=14, scale=4), nullable=True, comment='销售单价(元,T+ 同步)'),
|
sa.Column(
|
||||||
sa.Column('product_category', sa.String(length=32), nullable=False, comment='产品类别:municipal_water市政水利 rail_transit轨道交通(决定折算阈值 30%/20%;T+同步不覆盖)'),
|
"spec_model",
|
||||||
sa.Column('status', mysql.TINYINT(), server_default='1', nullable=False, comment='状态:1启用 0停用'),
|
sa.String(length=128),
|
||||||
sa.Column('remark', sa.String(length=255), nullable=True, comment='备注'),
|
server_default="",
|
||||||
sa.Column('create_time', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False, comment='创建时间'),
|
nullable=False,
|
||||||
sa.Column('update_time', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), nullable=False, comment='更新时间'),
|
comment="规格型号(空串而非 NULL:参与组合唯一键,MySQL 组合索引对 NULL 不去重)",
|
||||||
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("unit", sa.String(length=16), nullable=True, comment="计量单位"),
|
||||||
sa.Column('sync_batch_no', sa.String(length=32), nullable=True, comment='同步批次号(同步模块写入)'),
|
sa.Column(
|
||||||
sa.Column('synced_at', sa.DateTime(), nullable=True, comment='最近同步时间'),
|
"stock_quantity",
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_products')),
|
sa.Numeric(precision=14, scale=2),
|
||||||
sa.UniqueConstraint('product_name', 'spec_model', name='uk_products_name_spec'),
|
nullable=True,
|
||||||
comment='产品表(类别锚定折算阈值参数;T+同步不覆盖产品类别)',
|
comment="数量(T+ 同步原样存储,语义待 IT 确认)",
|
||||||
mysql_charset='utf8mb4',
|
),
|
||||||
mysql_collate='utf8mb4_general_ci',
|
sa.Column(
|
||||||
mysql_engine='InnoDB'
|
"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_index(op.f("idx_products_source_no"), "products", ["source_no"], unique=False)
|
||||||
op.create_table('ownership_change_logs',
|
op.create_table(
|
||||||
sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'),
|
"ownership_change_logs",
|
||||||
sa.Column('owner_type', mysql.TINYINT(), nullable=False, comment='归属类型:1客户归属 2业务归属'),
|
sa.Column(
|
||||||
sa.Column('customer_id', mysql.BIGINT(unsigned=True), nullable=True, comment='客户ID(固化,owner_type=1 时非空)'),
|
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
|
||||||
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(
|
||||||
sa.Column('new_owner_personnel_id', mysql.BIGINT(unsigned=True), nullable=True, comment='变更后负责营销人员ID(固化)'),
|
"owner_type", mysql.TINYINT(), nullable=False, comment="归属类型:1客户归属 2业务归属"
|
||||||
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(
|
||||||
sa.Column('change_reason', sa.String(length=255), nullable=True, comment='变更原因'),
|
"customer_id",
|
||||||
sa.Column('operator_id', mysql.BIGINT(unsigned=True), nullable=True, comment='操作人ID(账号删除后置空)'),
|
mysql.BIGINT(unsigned=True),
|
||||||
sa.Column('operator_name', sa.String(length=64), nullable=False, comment='操作人用户名(冗余固化)'),
|
nullable=True,
|
||||||
sa.Column('create_time', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False, comment='创建时间'),
|
comment="客户ID(固化,owner_type=1 时非空)",
|
||||||
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')),
|
sa.Column(
|
||||||
comment='归属变更记录表(append-only;客户/业务归属变更留痕,不回溯历史)',
|
"business_id",
|
||||||
mysql_charset='utf8mb4',
|
mysql.BIGINT(unsigned=True),
|
||||||
mysql_collate='utf8mb4_general_ci',
|
nullable=True,
|
||||||
mysql_engine='InnoDB'
|
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.create_index(op.f('idx_ownership_change_logs_business_id'), 'ownership_change_logs', ['business_id'], unique=False)
|
op.f("idx_ownership_change_logs_customer_id"),
|
||||||
op.create_index(op.f('idx_ownership_change_logs_create_time'), 'ownership_change_logs', ['create_time'], unique=False)
|
"ownership_change_logs",
|
||||||
op.create_table('personnel',
|
["customer_id"],
|
||||||
sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'),
|
unique=False,
|
||||||
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.create_index(op.f('idx_personnel_manager_id'), 'personnel', ['manager_id'], unique=False)
|
op.f("idx_ownership_change_logs_business_id"),
|
||||||
op.create_index(op.f('idx_personnel_source_no'), 'personnel', ['source_no'], unique=False)
|
"ownership_change_logs",
|
||||||
op.create_table('customers',
|
["business_id"],
|
||||||
sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'),
|
unique=False,
|
||||||
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.create_index(op.f('idx_customers_owner_personnel_id'), 'customers', ['owner_personnel_id'], unique=False)
|
op.f("idx_ownership_change_logs_create_time"),
|
||||||
op.create_index(op.f('idx_customers_source_no'), 'customers', ['source_no'], unique=False)
|
"ownership_change_logs",
|
||||||
op.create_table('personnel_plans',
|
["create_time"],
|
||||||
sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'),
|
unique=False,
|
||||||
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(
|
||||||
op.create_table('personnel_regions',
|
"personnel",
|
||||||
sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'),
|
sa.Column(
|
||||||
sa.Column('personnel_id', mysql.BIGINT(unsigned=True), nullable=False, comment='人员ID(personnel.id)'),
|
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
|
||||||
sa.Column('region_name', sa.String(length=128), nullable=False, comment='区域名称(产品辐射地理范围)'),
|
),
|
||||||
sa.Column('remark', sa.String(length=255), nullable=True, comment='备注'),
|
sa.Column("personnel_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(
|
||||||
sa.Column('update_time', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), nullable=False, comment='更新时间'),
|
"business_role_id",
|
||||||
sa.ForeignKeyConstraint(['personnel_id'], ['personnel.id'], name=op.f('fk_personnel_regions_personnel_id'), ondelete='RESTRICT'),
|
mysql.BIGINT(unsigned=True),
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_personnel_regions')),
|
nullable=False,
|
||||||
comment='产品辐射区域表',
|
comment="岗位角色ID(决定计提公式模板;T+同步不覆盖)",
|
||||||
mysql_charset='utf8mb4',
|
),
|
||||||
mysql_collate='utf8mb4_general_ci',
|
sa.Column(
|
||||||
mysql_engine='InnoDB'
|
"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 ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
@ -173,9 +482,9 @@ def downgrade() -> None:
|
|||||||
单独 drop_index 会报 MySQL 1553(needed in a foreign key constraint)。
|
单独 drop_index 会报 MySQL 1553(needed in a foreign key constraint)。
|
||||||
**整表回滚应只保留 `op.drop_table`**,DROP TABLE 会连带删除索引与外键。
|
**整表回滚应只保留 `op.drop_table`**,DROP TABLE 会连带删除索引与外键。
|
||||||
"""
|
"""
|
||||||
op.drop_table('personnel_regions')
|
op.drop_table("personnel_regions")
|
||||||
op.drop_table('personnel_plans')
|
op.drop_table("personnel_plans")
|
||||||
op.drop_table('customers')
|
op.drop_table("customers")
|
||||||
op.drop_table('personnel')
|
op.drop_table("personnel")
|
||||||
op.drop_table('ownership_change_logs')
|
op.drop_table("ownership_change_logs")
|
||||||
op.drop_table('products')
|
op.drop_table("products")
|
||||||
|
|||||||
@ -66,6 +66,16 @@ MODULE_PARAM = "计提参数"
|
|||||||
MODULE_PERSONNEL = "人员管理"
|
MODULE_PERSONNEL = "人员管理"
|
||||||
MODULE_PRODUCT = "产品管理"
|
MODULE_PRODUCT = "产品管理"
|
||||||
MODULE_CUSTOMER = "客户管理"
|
MODULE_CUSTOMER = "客户管理"
|
||||||
|
MODULE_BUSINESS = "业务管理"
|
||||||
|
MODULE_SHIPMENT = "发货管理"
|
||||||
|
MODULE_RECEIPT = "回款管理"
|
||||||
|
MODULE_RATING = "客户评级"
|
||||||
|
|
||||||
|
# ===== 单据编号前缀({前缀}{YYYYMMDD}-{当日4位序号},序号按自然日重置)=====
|
||||||
|
# 一期为代码常量(业务未提出配置诉求);如需可配置,走 M1 参数体系新增参数组
|
||||||
|
NO_PREFIX_BUSINESS = "YW"
|
||||||
|
NO_PREFIX_SHIPMENT = "SH"
|
||||||
|
NO_PREFIX_RECEIPT = "HK"
|
||||||
|
|
||||||
ACTION_CREATE = "新增"
|
ACTION_CREATE = "新增"
|
||||||
ACTION_UPDATE = "修改"
|
ACTION_UPDATE = "修改"
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from app.models.base import Base, TimestampMixin
|
from app.models.base import Base, TimestampMixin
|
||||||
|
from app.models.business import Business, BusinessItem, BusinessStatusLog
|
||||||
from app.models.business_role import BusinessRole
|
from app.models.business_role import BusinessRole
|
||||||
from app.models.customer import Customer, OwnershipChangeLog
|
from app.models.customer import Customer, OwnershipChangeLog
|
||||||
from app.models.login_log import LoginLog
|
from app.models.login_log import LoginLog
|
||||||
@ -17,13 +18,20 @@ from app.models.operation_log import OperationLog
|
|||||||
from app.models.param import ParamChangeBatch, ParamDefinition, ParamValue
|
from app.models.param import ParamChangeBatch, ParamDefinition, ParamValue
|
||||||
from app.models.personnel import Personnel, PersonnelPlan, PersonnelRegion
|
from app.models.personnel import Personnel, PersonnelPlan, PersonnelRegion
|
||||||
from app.models.product import Product
|
from app.models.product import Product
|
||||||
|
from app.models.rating import CustomerRatingGrade
|
||||||
|
from app.models.receipt import Receipt
|
||||||
from app.models.role import Permission, Role, RolePermission
|
from app.models.role import Permission, Role, RolePermission
|
||||||
|
from app.models.shipment import Shipment, ShipmentItem
|
||||||
from app.models.user import User, UserRole
|
from app.models.user import User, UserRole
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Base",
|
"Base",
|
||||||
|
"Business",
|
||||||
|
"BusinessItem",
|
||||||
"BusinessRole",
|
"BusinessRole",
|
||||||
|
"BusinessStatusLog",
|
||||||
"Customer",
|
"Customer",
|
||||||
|
"CustomerRatingGrade",
|
||||||
"LoginLog",
|
"LoginLog",
|
||||||
"OperationLog",
|
"OperationLog",
|
||||||
"OwnershipChangeLog",
|
"OwnershipChangeLog",
|
||||||
@ -35,8 +43,11 @@ __all__ = [
|
|||||||
"PersonnelPlan",
|
"PersonnelPlan",
|
||||||
"PersonnelRegion",
|
"PersonnelRegion",
|
||||||
"Product",
|
"Product",
|
||||||
|
"Receipt",
|
||||||
"Role",
|
"Role",
|
||||||
"RolePermission",
|
"RolePermission",
|
||||||
|
"Shipment",
|
||||||
|
"ShipmentItem",
|
||||||
"TimestampMixin",
|
"TimestampMixin",
|
||||||
"User",
|
"User",
|
||||||
"UserRole",
|
"UserRole",
|
||||||
|
|||||||
218
backend/app/models/business.py
Normal file
218
backend/app/models/business.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
"""业务数据模型:业务单、产品明细与状态流转留痕。
|
||||||
|
|
||||||
|
- 汇总冗余列(total_* 五列)由服务层在明细/发货/回款增删改时**事务内单点
|
||||||
|
重算**,禁手工直改 —— 列表页与 M3 起付判定直接读冗余列,避免每次 JOIN
|
||||||
|
聚合;代价是所有写路径必须重算(business_service 统一收口)
|
||||||
|
- 毛利润相关值(毛利润额/毛利润率/折算毛利润额)**不落库**:它们是
|
||||||
|
sale_amount/cost_amount + 时点化阈值参数的派生值,落库会在成本调整或
|
||||||
|
参数改版后漂移出「假快照」;M2 以只读毛利润视图实时计算(复用
|
||||||
|
formula.profit_rate/converted_profit),M3 核算台账才是法定固化点
|
||||||
|
- product_category 存在明细行级:混合业务按产品逐条计算(对照表 #14)
|
||||||
|
- 运费仅记录展示,不计入毛利润口径(对照表 #2);协助营销人员一期核算
|
||||||
|
不使用(对照表 #15 剔除协助 25% 分成),字段保留仅记录事实(需规 2.2.1)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from sqlalchemy import Date, DateTime, ForeignKey, Index, Numeric, String, text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.models.base import (
|
||||||
|
BIGINT_UNSIGNED,
|
||||||
|
TINYINT_TYPE,
|
||||||
|
Base,
|
||||||
|
TimestampMixin,
|
||||||
|
TPlusSyncMixin,
|
||||||
|
mysql_table_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 业务状态(businesses.status,流转规则见 business_service:终态不可变更)
|
||||||
|
BUSINESS_STATUS_ACTIVE = 1
|
||||||
|
BUSINESS_STATUS_DONE = 2
|
||||||
|
BUSINESS_STATUS_TERMINATED = 3
|
||||||
|
|
||||||
|
|
||||||
|
class Business(Base, TimestampMixin, TPlusSyncMixin):
|
||||||
|
"""业务表(需求 2.2.1):业务编号自动生成,即报表 2.4.1 的「合同编号」。"""
|
||||||
|
|
||||||
|
__tablename__ = "businesses"
|
||||||
|
__table_args__ = mysql_table_args(
|
||||||
|
"业务表(业务编号自动生成;T+同步不覆盖业务状态)",
|
||||||
|
Index("idx_businesses_customer_id", "customer_id"),
|
||||||
|
Index("idx_businesses_owner_personnel_id", "owner_personnel_id"),
|
||||||
|
Index("idx_businesses_status", "status"),
|
||||||
|
Index("idx_businesses_business_date", "business_date"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||||||
|
)
|
||||||
|
business_no: Mapped[str] = mapped_column(
|
||||||
|
String(32),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
comment="业务编号,自动生成 YW{YYYYMMDD}-{4位当日序号}(报表合同编号)",
|
||||||
|
)
|
||||||
|
customer_id: Mapped[int] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED,
|
||||||
|
ForeignKey("customers.id", ondelete="RESTRICT"),
|
||||||
|
nullable=False,
|
||||||
|
comment="客户ID(customers.id)",
|
||||||
|
)
|
||||||
|
business_date: Mapped[date | None] = mapped_column(Date, comment="业务日期(T+ 同步口径)")
|
||||||
|
status: Mapped[int] = mapped_column(
|
||||||
|
TINYINT_TYPE,
|
||||||
|
nullable=False,
|
||||||
|
server_default="1",
|
||||||
|
comment="业务状态:1执行中 2已完成 3已终止(终态不可变更;T+同步不覆盖)",
|
||||||
|
)
|
||||||
|
owner_personnel_id: Mapped[int | None] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED,
|
||||||
|
ForeignKey("personnel.id", ondelete="RESTRICT"),
|
||||||
|
comment="负责营销人员ID(核算归属锚点;NULL=待指定,M3 核算前置校验非空;变更留痕不回溯)",
|
||||||
|
)
|
||||||
|
assistant_personnel_id: Mapped[int | None] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED,
|
||||||
|
ForeignKey("personnel.id", ondelete="RESTRICT"),
|
||||||
|
comment="协助营销人员ID(一期核算不使用,对照表 #15;仅记录事实)",
|
||||||
|
)
|
||||||
|
is_special: Mapped[int] = mapped_column(
|
||||||
|
TINYINT_TYPE,
|
||||||
|
nullable=False,
|
||||||
|
server_default="0",
|
||||||
|
comment="特殊业务标记:1是 0否(公司参与等)",
|
||||||
|
)
|
||||||
|
special_note: Mapped[str | None] = mapped_column(String(255), comment="特殊业务说明")
|
||||||
|
total_sale_amount: Mapped[Decimal | None] = mapped_column(
|
||||||
|
Numeric(14, 2),
|
||||||
|
server_default="0",
|
||||||
|
comment="销售额合计(明细行汇总冗余,服务层事务内维护,禁手工直改;不含运费口径)",
|
||||||
|
)
|
||||||
|
total_cost_amount: Mapped[Decimal | None] = mapped_column(
|
||||||
|
Numeric(14, 2),
|
||||||
|
server_default="0",
|
||||||
|
comment="成本合计(明细行汇总冗余,服务层事务内维护,禁手工直改)",
|
||||||
|
)
|
||||||
|
total_ship_amount: Mapped[Decimal | None] = mapped_column(
|
||||||
|
Numeric(14, 2),
|
||||||
|
server_default="0",
|
||||||
|
comment="发货金额合计(发货明细汇总冗余,服务层事务内维护,禁手工直改)",
|
||||||
|
)
|
||||||
|
total_received_amount: Mapped[Decimal | None] = mapped_column(
|
||||||
|
Numeric(14, 2),
|
||||||
|
server_default="0",
|
||||||
|
comment="回款合计(回款记录汇总冗余,服务层事务内维护;M3 起付判定直接消费)",
|
||||||
|
)
|
||||||
|
total_freight_amount: Mapped[Decimal | None] = mapped_column(
|
||||||
|
Numeric(14, 2),
|
||||||
|
server_default="0",
|
||||||
|
comment="运费合计(发货单汇总冗余,仅记录展示;不计入毛利润口径,对照表 #2)",
|
||||||
|
)
|
||||||
|
remark: Mapped[str | None] = mapped_column(String(255), comment="备注")
|
||||||
|
|
||||||
|
|
||||||
|
class BusinessItem(Base, TimestampMixin):
|
||||||
|
"""业务产品明细表(需求 2.2.1/2.2.4):毛利润的行级源数据。
|
||||||
|
|
||||||
|
product_id 可空=自由录入产品(配合「数据提取」一键入产品库);
|
||||||
|
产品名称/规格冗余固化,防产品改名影响历史业务。
|
||||||
|
sale_amount 由服务层按 数量×单价 计算固化,不信任前端传值。
|
||||||
|
cost_amount 来源待 T+ 对接确认(可空,人工可录先行)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "business_items"
|
||||||
|
__table_args__ = mysql_table_args(
|
||||||
|
"业务产品明细表(毛利润行级源数据;指导价 T+同步不覆盖)",
|
||||||
|
Index("idx_business_items_business_id", "business_id"),
|
||||||
|
Index("idx_business_items_product_id", "product_id"),
|
||||||
|
Index("idx_business_items_product_category", "product_category"),
|
||||||
|
)
|
||||||
|
|
||||||
|
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="RESTRICT"),
|
||||||
|
nullable=False,
|
||||||
|
comment="业务ID(businesses.id)",
|
||||||
|
)
|
||||||
|
product_id: Mapped[int | None] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED,
|
||||||
|
ForeignKey("products.id", ondelete="RESTRICT"),
|
||||||
|
comment="产品ID(products.id;NULL=自由录入,未入产品库)",
|
||||||
|
)
|
||||||
|
product_name: Mapped[str] = mapped_column(
|
||||||
|
String(128), nullable=False, comment="产品名称(引用产品时冗余固化)"
|
||||||
|
)
|
||||||
|
spec_model: Mapped[str] = mapped_column(
|
||||||
|
String(128),
|
||||||
|
nullable=False,
|
||||||
|
server_default="''",
|
||||||
|
comment="规格型号(冗余固化;空串对齐 products 组合唯一键口径)",
|
||||||
|
)
|
||||||
|
unit: Mapped[str | None] = mapped_column(String(16), comment="计量单位")
|
||||||
|
product_category: Mapped[str] = mapped_column(
|
||||||
|
String(32),
|
||||||
|
nullable=False,
|
||||||
|
comment="产品类别(行级折算锚点,混合业务按产品逐条计算,对照表 #14;值域同 PRODUCT_CATEGORIES)",
|
||||||
|
)
|
||||||
|
quantity: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, comment="数量(>0)")
|
||||||
|
sale_price: Mapped[Decimal | None] = mapped_column(
|
||||||
|
Numeric(14, 4), comment="销售单价(元;T+ 同步或人工)"
|
||||||
|
)
|
||||||
|
sale_amount: Mapped[Decimal] = mapped_column(
|
||||||
|
Numeric(14, 2),
|
||||||
|
nullable=False,
|
||||||
|
comment="销售额 = 数量×销售单价(服务层计算固化;不含运费口径,毛利润率分母)",
|
||||||
|
)
|
||||||
|
cost_amount: Mapped[Decimal | None] = mapped_column(
|
||||||
|
Numeric(14, 2),
|
||||||
|
comment="成本额(T+ 同步或人工录入,来源待 T+ 确认;毛利润额 = 销售额 − 成本额)",
|
||||||
|
)
|
||||||
|
guided_price: Mapped[Decimal | None] = mapped_column(
|
||||||
|
Numeric(14, 4), comment="指导价(人工录入;T+同步不覆盖,需规 2.2.1/2.2.4)"
|
||||||
|
)
|
||||||
|
remark: Mapped[str | None] = mapped_column(String(255), comment="备注")
|
||||||
|
|
||||||
|
|
||||||
|
class BusinessStatusLog(Base):
|
||||||
|
"""业务状态变更记录表(append-only,需求 2.2.1「支持状态变更记录」)。
|
||||||
|
|
||||||
|
不继承 TimestampMixin(留痕无更新语义);business_id 为无外键的固化 ID
|
||||||
|
(businesses 建表在本表之后的迁移批次内 —— 实为同批,但留痕统一口径:
|
||||||
|
被引用方删除后记录仍完整可读,删除保护由服务层检查承担)。
|
||||||
|
首条记录在业务创建时写入(old=new=1 执行中),详情页时间线从创建起展示。
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "business_status_logs"
|
||||||
|
__table_args__ = mysql_table_args(
|
||||||
|
"业务状态变更记录表(append-only)",
|
||||||
|
Index("idx_business_status_logs_business_id", "business_id"),
|
||||||
|
Index("idx_business_status_logs_create_time", "create_time"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||||||
|
)
|
||||||
|
business_id: Mapped[int] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED,
|
||||||
|
comment="业务ID(固化,无外键;删除保护由服务层承担)",
|
||||||
|
)
|
||||||
|
old_status: Mapped[int] = mapped_column(
|
||||||
|
TINYINT_TYPE, nullable=False, comment="变更前状态(创建首条为 1)"
|
||||||
|
)
|
||||||
|
new_status: Mapped[int] = mapped_column(TINYINT_TYPE, nullable=False, comment="变更后状态")
|
||||||
|
change_reason: 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="创建时间"
|
||||||
|
)
|
||||||
46
backend/app/models/rating.py
Normal file
46
backend/app/models/rating.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""客户评级等级配置模型(需求 2.2.5)。
|
||||||
|
|
||||||
|
评级计算器定位为「仅打分工具,不影响绩效金额」(对照表 #25):评分记录
|
||||||
|
不落库,等级**映射关系**可配置由本表承载。判定规则:按 sort_order 升序取
|
||||||
|
第一个 min_score ≤ 总分的等级 —— 单边判定避免区间边界(如 75 分)在
|
||||||
|
「≥75 且 ≤89」与「>74 且 <75」两套写法间的重叠/遗漏歧义;max_score 仅展示。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from sqlalchemy import Integer, Numeric, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.models.base import BIGINT_UNSIGNED, Base, TimestampMixin, mysql_table_args
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerRatingGrade(Base, TimestampMixin):
|
||||||
|
"""客户评级等级配置表(需求 2.2.5「等级对应关系可配置」)。
|
||||||
|
|
||||||
|
种子四条随迁移初始化:A 优秀 90-100 / B 良好 75-89 / C 合格 60-74 /
|
||||||
|
D 待改进 0-59。区间不重叠与 0≤min≤max≤100 由应用层校验。
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "customer_rating_grades"
|
||||||
|
__table_args__ = mysql_table_args(
|
||||||
|
"客户评级等级配置表(等级映射可配置;判定按 sort_order 升序首个 min_score≤总分)"
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||||||
|
)
|
||||||
|
grade_name: Mapped[str] = mapped_column(
|
||||||
|
String(32), nullable=False, unique=True, comment="等级名称(如 A/优秀)"
|
||||||
|
)
|
||||||
|
min_score: Mapped[Decimal] = mapped_column(
|
||||||
|
Numeric(5, 1),
|
||||||
|
nullable=False,
|
||||||
|
comment="总分下限(含);判定=按 sort_order 升序首个 min_score ≤ 总分",
|
||||||
|
)
|
||||||
|
max_score: Mapped[Decimal] = mapped_column(
|
||||||
|
Numeric(5, 1), nullable=False, comment="总分上限(展示用)"
|
||||||
|
)
|
||||||
|
sort_order: Mapped[int] = mapped_column(
|
||||||
|
Integer, nullable=False, server_default="0", comment="排序(判定顺序,升序)"
|
||||||
|
)
|
||||||
|
remark: Mapped[str | None] = mapped_column(String(255), comment="说明")
|
||||||
83
backend/app/models/receipt.py
Normal file
83
backend/app/models/receipt.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
"""回款模型:回款记录与批次追溯。
|
||||||
|
|
||||||
|
- shipment_id 为**手动指定**的发货批次(对照表 #18):可空=暂未指定;
|
||||||
|
必须与 business_id 同属一个业务(服务层强校验,无数据库级约束可表达
|
||||||
|
跨表同属关系);指定后回款天数方可由 formula.payment_days 派生
|
||||||
|
- settlement_status 是 **M3 核算防重锚点**(需规 2.3.3「每笔回款仅核算一次,
|
||||||
|
台账记录核销状态」):预置于本表避免 M3 反向迁移改 M2 表;M3 台账建立后
|
||||||
|
由核算服务写入,M2 仅维护 0(未核算)
|
||||||
|
- receipt_date 是 M3 参数版本匹配日(对照表 #11:按回款到账日取时点参数)
|
||||||
|
- 一期禁负数金额(≥0):红字/退款冲销需求出现时先补设计再放开
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from sqlalchemy import Date, ForeignKey, Index, Numeric, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.models.base import (
|
||||||
|
BIGINT_UNSIGNED,
|
||||||
|
TINYINT_TYPE,
|
||||||
|
Base,
|
||||||
|
TimestampMixin,
|
||||||
|
TPlusSyncMixin,
|
||||||
|
mysql_table_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 绩效核算状态(receipts.settlement_status)
|
||||||
|
SETTLEMENT_PENDING = 0
|
||||||
|
SETTLEMENT_DONE = 1
|
||||||
|
|
||||||
|
|
||||||
|
class Receipt(Base, TimestampMixin, TPlusSyncMixin):
|
||||||
|
"""回款表(需求 2.2.3):按业务登记到账款项,批次号系统内唯一。"""
|
||||||
|
|
||||||
|
__tablename__ = "receipts"
|
||||||
|
__table_args__ = mysql_table_args(
|
||||||
|
"回款表(手动指定发货批次;已核算回款禁改禁删)",
|
||||||
|
Index("idx_receipts_business_id", "business_id"),
|
||||||
|
Index("idx_receipts_receipt_date", "receipt_date"),
|
||||||
|
Index("idx_receipts_settlement_status", "settlement_status"),
|
||||||
|
Index("idx_receipts_shipment_id", "shipment_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||||||
|
)
|
||||||
|
receipt_batch_no: Mapped[str] = mapped_column(
|
||||||
|
String(32),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
comment="回款批次号,自动生成 HK{YYYYMMDD}-{4位当日序号}(系统内唯一;T+ 同步回款以 source_no 幂等)",
|
||||||
|
)
|
||||||
|
business_id: Mapped[int] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED,
|
||||||
|
ForeignKey("businesses.id", ondelete="RESTRICT"),
|
||||||
|
nullable=False,
|
||||||
|
comment="业务ID(businesses.id)",
|
||||||
|
)
|
||||||
|
shipment_id: Mapped[int | None] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED,
|
||||||
|
ForeignKey("shipments.id", ondelete="RESTRICT"),
|
||||||
|
comment="发货单ID(shipments.id;手动指定的回款对应批次,须与本行业务一致;NULL=暂未指定)",
|
||||||
|
)
|
||||||
|
receipt_date: Mapped[date] = mapped_column(
|
||||||
|
Date, nullable=False, comment="回款到账日(M3 按此日匹配参数版本,对照表 #11)"
|
||||||
|
)
|
||||||
|
amount: Mapped[Decimal] = mapped_column(
|
||||||
|
Numeric(14, 2), nullable=False, comment="回款金额(≥0;一期禁负数/红字)"
|
||||||
|
)
|
||||||
|
is_barter: Mapped[int] = mapped_column(
|
||||||
|
TINYINT_TYPE,
|
||||||
|
nullable=False,
|
||||||
|
server_default="0",
|
||||||
|
comment="以物抵顶标记:1是 0否(M3 核算按应发绩效×barter.discount 折算,M2 仅标记)",
|
||||||
|
)
|
||||||
|
settlement_status: Mapped[int] = mapped_column(
|
||||||
|
TINYINT_TYPE,
|
||||||
|
nullable=False,
|
||||||
|
server_default="0",
|
||||||
|
comment="绩效核算状态:0未核算 1已核算(M3 核算防重锚点;已核算回款禁改禁删)",
|
||||||
|
)
|
||||||
|
remark: Mapped[str | None] = mapped_column(String(255), comment="备注")
|
||||||
115
backend/app/models/shipment.py
Normal file
115
backend/app/models/shipment.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
"""发货模型:发货单与发货产品明细。
|
||||||
|
|
||||||
|
- ship_date 是**回款天数计算基准日**(对照表 #1:发货日首日计第 1 天),
|
||||||
|
回款指定本发货单后由 formula.payment_days 派生回款天数,不在本表落库
|
||||||
|
- 运费仅记录展示,不计入毛利润口径(对照表 #2)
|
||||||
|
- total_amount 为明细行汇总冗余,服务层事务内维护
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from sqlalchemy import Date, ForeignKey, Index, Numeric, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.models.base import (
|
||||||
|
BIGINT_UNSIGNED,
|
||||||
|
Base,
|
||||||
|
TimestampMixin,
|
||||||
|
TPlusSyncMixin,
|
||||||
|
mysql_table_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Shipment(Base, TimestampMixin, TPlusSyncMixin):
|
||||||
|
"""发货单表(需求 2.2.2):每笔发货对应一个业务,支持分批发货。"""
|
||||||
|
|
||||||
|
__tablename__ = "shipments"
|
||||||
|
__table_args__ = mysql_table_args(
|
||||||
|
"发货单表(发货日为回款天数基准日,发货日首日计第 1 天)",
|
||||||
|
Index("idx_shipments_business_id", "business_id"),
|
||||||
|
Index("idx_shipments_ship_date", "ship_date"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||||||
|
)
|
||||||
|
shipment_no: Mapped[str] = mapped_column(
|
||||||
|
String(32),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
comment="发货单号,自动生成 SH{YYYYMMDD}-{4位当日序号}(内部追溯用)",
|
||||||
|
)
|
||||||
|
business_id: Mapped[int] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED,
|
||||||
|
ForeignKey("businesses.id", ondelete="RESTRICT"),
|
||||||
|
nullable=False,
|
||||||
|
comment="业务ID(businesses.id;终态业务禁止新发货)",
|
||||||
|
)
|
||||||
|
ship_date: Mapped[date] = mapped_column(
|
||||||
|
Date, nullable=False, comment="发货日期(回款天数基准日,发货日首日=第 1 天)"
|
||||||
|
)
|
||||||
|
freight_amount: Mapped[Decimal] = mapped_column(
|
||||||
|
Numeric(14, 2),
|
||||||
|
nullable=False,
|
||||||
|
server_default="0",
|
||||||
|
comment="运费金额(仅记录展示;不计入毛利润口径,对照表 #2)",
|
||||||
|
)
|
||||||
|
total_amount: Mapped[Decimal] = mapped_column(
|
||||||
|
Numeric(14, 2),
|
||||||
|
nullable=False,
|
||||||
|
server_default="0",
|
||||||
|
comment="发货总金额(明细行汇总冗余,服务层事务内维护,禁手工直改)",
|
||||||
|
)
|
||||||
|
remark: Mapped[str | None] = mapped_column(String(255), comment="备注")
|
||||||
|
|
||||||
|
|
||||||
|
class ShipmentItem(Base, TimestampMixin):
|
||||||
|
"""发货产品明细表(需求 2.2.2):记录发货的产品名称、规格、数量等。
|
||||||
|
|
||||||
|
价格口径两可(T+ 供价或人工录入,待 IT 确认):sale_price 可空人工补,
|
||||||
|
amount 由服务层按 数量×单价 计算固化。
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "shipment_items"
|
||||||
|
__table_args__ = mysql_table_args(
|
||||||
|
"发货产品明细表",
|
||||||
|
Index("idx_shipment_items_shipment_id", "shipment_id"),
|
||||||
|
Index("idx_shipment_items_product_id", "product_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||||||
|
)
|
||||||
|
shipment_id: Mapped[int] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED,
|
||||||
|
ForeignKey("shipments.id", ondelete="RESTRICT"),
|
||||||
|
nullable=False,
|
||||||
|
comment="发货单ID(shipments.id)",
|
||||||
|
)
|
||||||
|
product_id: Mapped[int | None] = mapped_column(
|
||||||
|
BIGINT_UNSIGNED,
|
||||||
|
ForeignKey("products.id", ondelete="RESTRICT"),
|
||||||
|
comment="产品ID(products.id;NULL=自由录入)",
|
||||||
|
)
|
||||||
|
product_name: Mapped[str] = mapped_column(
|
||||||
|
String(128), nullable=False, comment="产品名称(引用产品时冗余固化)"
|
||||||
|
)
|
||||||
|
spec_model: Mapped[str] = mapped_column(
|
||||||
|
String(128),
|
||||||
|
nullable=False,
|
||||||
|
server_default="''",
|
||||||
|
comment="规格型号(冗余固化;空串对齐 products 组合唯一键口径)",
|
||||||
|
)
|
||||||
|
unit: Mapped[str | None] = mapped_column(String(16), comment="计量单位")
|
||||||
|
quantity: Mapped[Decimal] = mapped_column(
|
||||||
|
Numeric(14, 2), nullable=False, comment="发货数量(>0)"
|
||||||
|
)
|
||||||
|
sale_price: Mapped[Decimal | None] = mapped_column(
|
||||||
|
Numeric(14, 4), comment="发货单价(元;T+ 同步或人工,口径待 IT 确认)"
|
||||||
|
)
|
||||||
|
amount: Mapped[Decimal] = mapped_column(
|
||||||
|
Numeric(14, 2),
|
||||||
|
nullable=False,
|
||||||
|
comment="行金额 = 数量×单价(服务层计算固化,不信任前端传值)",
|
||||||
|
)
|
||||||
@ -29,6 +29,13 @@ EXPECTED_TABLES = {
|
|||||||
"products",
|
"products",
|
||||||
"customers",
|
"customers",
|
||||||
"ownership_change_logs",
|
"ownership_change_logs",
|
||||||
|
"businesses",
|
||||||
|
"business_items",
|
||||||
|
"business_status_logs",
|
||||||
|
"shipments",
|
||||||
|
"shipment_items",
|
||||||
|
"receipts",
|
||||||
|
"customer_rating_grades",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 单复数同形的集合名词:本身即复数语义,不受「以 s 结尾」检查约束
|
# 单复数同形的集合名词:本身即复数语义,不受「以 s 结尾」检查约束
|
||||||
|
|||||||
@ -459,6 +459,210 @@ INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_t
|
|||||||
(18, 15, '', '2026-01-01', '18000', 1),
|
(18, 15, '', '2026-01-01', '18000', 1),
|
||||||
(19, 16, '', '2026-01-01', '3000', 1);
|
(19, 16, '', '2026-01-01', '3000', 1);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 业务表(V5 迁移;业务编号自动生成,五项汇总冗余列由服务层事务内维护)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TABLE businesses (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||||
|
business_no VARCHAR(32) NOT NULL COMMENT '业务编号,自动生成 YW{YYYYMMDD}-{4位当日序号}(报表合同编号)',
|
||||||
|
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID(customers.id)',
|
||||||
|
business_date DATE COMMENT '业务日期(T+ 同步口径)',
|
||||||
|
status TINYINT NOT NULL COMMENT '业务状态:1执行中 2已完成 3已终止(终态不可变更;T+同步不覆盖)' DEFAULT '1',
|
||||||
|
owner_personnel_id BIGINT UNSIGNED COMMENT '负责营销人员ID(核算归属锚点;NULL=待指定,M3 核算前置校验非空;变更留痕不回溯)',
|
||||||
|
assistant_personnel_id BIGINT UNSIGNED COMMENT '协助营销人员ID(一期核算不使用,对照表 #15;仅记录事实)',
|
||||||
|
is_special TINYINT NOT NULL COMMENT '特殊业务标记:1是 0否(公司参与等)' DEFAULT '0',
|
||||||
|
special_note VARCHAR(255) COMMENT '特殊业务说明',
|
||||||
|
total_sale_amount DECIMAL(14, 2) COMMENT '销售额合计(明细行汇总冗余,服务层事务内维护,禁手工直改;不含运费口径)' DEFAULT '0',
|
||||||
|
total_cost_amount DECIMAL(14, 2) COMMENT '成本合计(明细行汇总冗余,服务层事务内维护,禁手工直改)' DEFAULT '0',
|
||||||
|
total_ship_amount DECIMAL(14, 2) COMMENT '发货金额合计(发货明细汇总冗余,服务层事务内维护,禁手工直改)' DEFAULT '0',
|
||||||
|
total_received_amount DECIMAL(14, 2) COMMENT '回款合计(回款记录汇总冗余,服务层事务内维护;M3 起付判定直接消费)' DEFAULT '0',
|
||||||
|
total_freight_amount DECIMAL(14, 2) COMMENT '运费合计(发货单汇总冗余,仅记录展示;不计入毛利润口径,对照表 #2)' DEFAULT '0',
|
||||||
|
remark VARCHAR(255) COMMENT '备注',
|
||||||
|
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
source_no VARCHAR(64) COMMENT 'T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)',
|
||||||
|
t_plus_id VARCHAR(64) COMMENT 'T+ 系统内部标识',
|
||||||
|
sync_batch_no VARCHAR(32) COMMENT '同步批次号(同步模块写入)',
|
||||||
|
synced_at DATETIME COMMENT '最近同步时间',
|
||||||
|
CONSTRAINT pk_businesses PRIMARY KEY (id),
|
||||||
|
CONSTRAINT uk_businesses_business_no UNIQUE (business_no),
|
||||||
|
CONSTRAINT fk_businesses_customer_id FOREIGN KEY(customer_id) REFERENCES customers (id) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT fk_businesses_owner_personnel_id FOREIGN KEY(owner_personnel_id) REFERENCES personnel (id) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT fk_businesses_assistant_personnel_id FOREIGN KEY(assistant_personnel_id) REFERENCES personnel (id) ON DELETE RESTRICT
|
||||||
|
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='业务表(业务编号自动生成;T+同步不覆盖业务状态)' COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE INDEX idx_businesses_business_date ON businesses (business_date);
|
||||||
|
|
||||||
|
CREATE INDEX idx_businesses_customer_id ON businesses (customer_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_businesses_owner_personnel_id ON businesses (owner_personnel_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_businesses_source_no ON businesses (source_no);
|
||||||
|
|
||||||
|
CREATE INDEX idx_businesses_status ON businesses (status);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 业务产品明细表(V5 迁移;行级类别为折算锚点,指导价 T+ 同步不覆盖)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TABLE business_items (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||||
|
business_id BIGINT UNSIGNED NOT NULL COMMENT '业务ID(businesses.id)',
|
||||||
|
product_id BIGINT UNSIGNED COMMENT '产品ID(products.id;NULL=自由录入,未入产品库)',
|
||||||
|
product_name VARCHAR(128) NOT NULL COMMENT '产品名称(引用产品时冗余固化)',
|
||||||
|
spec_model VARCHAR(128) NOT NULL COMMENT '规格型号(冗余固化;空串对齐 products 组合唯一键口径)' DEFAULT '',
|
||||||
|
unit VARCHAR(16) COMMENT '计量单位',
|
||||||
|
product_category VARCHAR(32) NOT NULL COMMENT '产品类别(行级折算锚点,混合业务按产品逐条计算,对照表 #14;值域同 PRODUCT_CATEGORIES)',
|
||||||
|
quantity DECIMAL(14, 2) NOT NULL COMMENT '数量(>0)',
|
||||||
|
sale_price DECIMAL(14, 4) COMMENT '销售单价(元;T+ 同步或人工)',
|
||||||
|
sale_amount DECIMAL(14, 2) NOT NULL COMMENT '销售额 = 数量×销售单价(服务层计算固化;不含运费口径,毛利润率分母)',
|
||||||
|
cost_amount DECIMAL(14, 2) COMMENT '成本额(T+ 同步或人工录入,来源待 T+ 确认;毛利润额 = 销售额 − 成本额)',
|
||||||
|
guided_price DECIMAL(14, 4) COMMENT '指导价(人工录入;T+同步不覆盖,需规 2.2.1/2.2.4)',
|
||||||
|
remark VARCHAR(255) 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_business_items PRIMARY KEY (id),
|
||||||
|
CONSTRAINT fk_business_items_business_id FOREIGN KEY(business_id) REFERENCES businesses (id) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT fk_business_items_product_id FOREIGN KEY(product_id) REFERENCES products (id) ON DELETE RESTRICT
|
||||||
|
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='业务产品明细表(毛利润行级源数据;指导价 T+同步不覆盖)' COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE INDEX idx_business_items_business_id ON business_items (business_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_business_items_product_category ON business_items (product_category);
|
||||||
|
|
||||||
|
CREATE INDEX idx_business_items_product_id ON business_items (product_id);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 业务状态变更记录表(V5 迁移,append-only;创建时写首条 old=new=1)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TABLE business_status_logs (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||||
|
business_id BIGINT UNSIGNED NOT NULL COMMENT '业务ID(固化,无外键;删除保护由服务层承担)',
|
||||||
|
old_status TINYINT NOT NULL COMMENT '变更前状态(创建首条为 1)',
|
||||||
|
new_status TINYINT NOT NULL COMMENT '变更后状态',
|
||||||
|
change_reason 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_business_status_logs PRIMARY KEY (id),
|
||||||
|
CONSTRAINT fk_business_status_logs_operator_id FOREIGN KEY(operator_id) REFERENCES users (id) ON DELETE SET NULL
|
||||||
|
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='业务状态变更记录表(append-only)' COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE INDEX idx_business_status_logs_business_id ON business_status_logs (business_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_business_status_logs_create_time ON business_status_logs (create_time);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 发货单表(V5 迁移;发货日为回款天数基准日,发货日首日计第 1 天)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TABLE shipments (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||||
|
shipment_no VARCHAR(32) NOT NULL COMMENT '发货单号,自动生成 SH{YYYYMMDD}-{4位当日序号}(内部追溯用)',
|
||||||
|
business_id BIGINT UNSIGNED NOT NULL COMMENT '业务ID(businesses.id;终态业务禁止新发货)',
|
||||||
|
ship_date DATE NOT NULL COMMENT '发货日期(回款天数基准日,发货日首日=第 1 天)',
|
||||||
|
freight_amount DECIMAL(14, 2) NOT NULL COMMENT '运费金额(仅记录展示;不计入毛利润口径,对照表 #2)' DEFAULT '0',
|
||||||
|
total_amount DECIMAL(14, 2) NOT NULL COMMENT '发货总金额(明细行汇总冗余,服务层事务内维护,禁手工直改)' DEFAULT '0',
|
||||||
|
remark VARCHAR(255) COMMENT '备注',
|
||||||
|
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
source_no VARCHAR(64) COMMENT 'T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)',
|
||||||
|
t_plus_id VARCHAR(64) COMMENT 'T+ 系统内部标识',
|
||||||
|
sync_batch_no VARCHAR(32) COMMENT '同步批次号(同步模块写入)',
|
||||||
|
synced_at DATETIME COMMENT '最近同步时间',
|
||||||
|
CONSTRAINT pk_shipments PRIMARY KEY (id),
|
||||||
|
CONSTRAINT uk_shipments_shipment_no UNIQUE (shipment_no),
|
||||||
|
CONSTRAINT fk_shipments_business_id FOREIGN KEY(business_id) REFERENCES businesses (id) ON DELETE RESTRICT
|
||||||
|
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='发货单表(发货日为回款天数基准日,发货日首日计第 1 天)' COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE INDEX idx_shipments_business_id ON shipments (business_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_shipments_ship_date ON shipments (ship_date);
|
||||||
|
|
||||||
|
CREATE INDEX idx_shipments_source_no ON shipments (source_no);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 发货产品明细表(V5 迁移)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TABLE shipment_items (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||||
|
shipment_id BIGINT UNSIGNED NOT NULL COMMENT '发货单ID(shipments.id)',
|
||||||
|
product_id BIGINT UNSIGNED COMMENT '产品ID(products.id;NULL=自由录入)',
|
||||||
|
product_name VARCHAR(128) NOT NULL COMMENT '产品名称(引用产品时冗余固化)',
|
||||||
|
spec_model VARCHAR(128) NOT NULL COMMENT '规格型号(冗余固化;空串对齐 products 组合唯一键口径)' DEFAULT '',
|
||||||
|
unit VARCHAR(16) COMMENT '计量单位',
|
||||||
|
quantity DECIMAL(14, 2) NOT NULL COMMENT '发货数量(>0)',
|
||||||
|
sale_price DECIMAL(14, 4) COMMENT '发货单价(元;T+ 同步或人工,口径待 IT 确认)',
|
||||||
|
amount DECIMAL(14, 2) 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_shipment_items PRIMARY KEY (id),
|
||||||
|
CONSTRAINT fk_shipment_items_shipment_id FOREIGN KEY(shipment_id) REFERENCES shipments (id) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT fk_shipment_items_product_id FOREIGN KEY(product_id) REFERENCES products (id) ON DELETE RESTRICT
|
||||||
|
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='发货产品明细表' COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE INDEX idx_shipment_items_product_id ON shipment_items (product_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_shipment_items_shipment_id ON shipment_items (shipment_id);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 回款表(V5 迁移;手动指定发货批次,已核算回款禁改禁删)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TABLE receipts (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||||
|
receipt_batch_no VARCHAR(32) NOT NULL COMMENT '回款批次号,自动生成 HK{YYYYMMDD}-{4位当日序号}(系统内唯一;T+ 同步回款以 source_no 幂等)',
|
||||||
|
business_id BIGINT UNSIGNED NOT NULL COMMENT '业务ID(businesses.id)',
|
||||||
|
shipment_id BIGINT UNSIGNED COMMENT '发货单ID(shipments.id;手动指定的回款对应批次,须与本行业务一致;NULL=暂未指定)',
|
||||||
|
receipt_date DATE NOT NULL COMMENT '回款到账日(M3 按此日匹配参数版本,对照表 #11)',
|
||||||
|
amount DECIMAL(14, 2) NOT NULL COMMENT '回款金额(≥0;一期禁负数/红字)',
|
||||||
|
is_barter TINYINT NOT NULL COMMENT '以物抵顶标记:1是 0否(M3 核算按应发绩效×barter.discount 折算,M2 仅标记)' DEFAULT '0',
|
||||||
|
settlement_status TINYINT NOT NULL COMMENT '绩效核算状态:0未核算 1已核算(M3 核算防重锚点;已核算回款禁改禁删)' DEFAULT '0',
|
||||||
|
remark VARCHAR(255) COMMENT '备注',
|
||||||
|
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
source_no VARCHAR(64) COMMENT 'T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)',
|
||||||
|
t_plus_id VARCHAR(64) COMMENT 'T+ 系统内部标识',
|
||||||
|
sync_batch_no VARCHAR(32) COMMENT '同步批次号(同步模块写入)',
|
||||||
|
synced_at DATETIME COMMENT '最近同步时间',
|
||||||
|
CONSTRAINT pk_receipts PRIMARY KEY (id),
|
||||||
|
CONSTRAINT uk_receipts_receipt_batch_no UNIQUE (receipt_batch_no),
|
||||||
|
CONSTRAINT fk_receipts_business_id FOREIGN KEY(business_id) REFERENCES businesses (id) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT fk_receipts_shipment_id FOREIGN KEY(shipment_id) REFERENCES shipments (id) ON DELETE RESTRICT
|
||||||
|
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='回款表(手动指定发货批次;已核算回款禁改禁删)' COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE INDEX idx_receipts_business_id ON receipts (business_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_receipts_receipt_date ON receipts (receipt_date);
|
||||||
|
|
||||||
|
CREATE INDEX idx_receipts_settlement_status ON receipts (settlement_status);
|
||||||
|
|
||||||
|
CREATE INDEX idx_receipts_shipment_id ON receipts (shipment_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_receipts_source_no ON receipts (source_no);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 客户评级等级配置表(V5 迁移;判定按 sort_order 升序首个 min_score≤总分)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TABLE customer_rating_grades (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||||
|
grade_name VARCHAR(32) NOT NULL COMMENT '等级名称(如 A/优秀)',
|
||||||
|
min_score DECIMAL(5, 1) NOT NULL COMMENT '总分下限(含);判定=按 sort_order 升序首个 min_score ≤ 总分',
|
||||||
|
max_score DECIMAL(5, 1) NOT NULL COMMENT '总分上限(展示用)',
|
||||||
|
sort_order INT NOT NULL COMMENT '排序(判定顺序,升序)' DEFAULT '0',
|
||||||
|
remark VARCHAR(255) 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_customer_rating_grades PRIMARY KEY (id),
|
||||||
|
CONSTRAINT uk_customer_rating_grades_grade_name UNIQUE (grade_name)
|
||||||
|
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='客户评级等级配置表(等级映射可配置;判定按 sort_order 升序首个 min_score≤总分)' COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 字典数据:评级等级(与 V5 迁移种子一致;评分项固定 5 项×20 分为前端常量,对照表 #25)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
INSERT INTO customer_rating_grades (grade_name, min_score, max_score, sort_order, remark) VALUES
|
||||||
|
('A', 90, 100, 1, '优秀'),
|
||||||
|
('B', 75, 89, 2, '良好'),
|
||||||
|
('C', 60, 74, 3, '合格'),
|
||||||
|
('D', 0, 59, 4, '待改进');
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
-- 迁移版本记录:须等于当前 head,使 alembic upgrade head 成为空操作
|
-- 迁移版本记录:须等于当前 head,使 alembic upgrade head 成为空操作
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
@ -467,4 +671,4 @@ CREATE TABLE alembic_version (
|
|||||||
CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)
|
CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)
|
||||||
)CHARSET=utf8mb4 ENGINE=InnoDB COLLATE utf8mb4_general_ci;
|
)CHARSET=utf8mb4 ENGINE=InnoDB COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
INSERT INTO alembic_version (version_num) VALUES ('e1133a7c670a');
|
INSERT INTO alembic_version (version_num) VALUES ('8efc189b131f');
|
||||||
|
|||||||
186
db/migration/V5__add_business_data_tables.sql
Normal file
186
db/migration/V5__add_business_data_tables.sql
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
-- Running upgrade e1133a7c670a -> 8efc189b131f
|
||||||
|
|
||||||
|
CREATE TABLE customer_rating_grades (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||||
|
grade_name VARCHAR(32) NOT NULL COMMENT '等级名称(如 A/优秀)',
|
||||||
|
min_score NUMERIC(5, 1) NOT NULL COMMENT '总分下限(含);判定=按 sort_order 升序首个 min_score ≤ 总分',
|
||||||
|
max_score NUMERIC(5, 1) NOT NULL COMMENT '总分上限(展示用)',
|
||||||
|
sort_order INTEGER NOT NULL COMMENT '排序(判定顺序,升序)' DEFAULT '0',
|
||||||
|
remark VARCHAR(255) 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_customer_rating_grades PRIMARY KEY (id),
|
||||||
|
CONSTRAINT uk_customer_rating_grades_grade_name UNIQUE (grade_name)
|
||||||
|
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='客户评级等级配置表(等级映射可配置;判定按 sort_order 升序首个 min_score≤总分)' COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE business_status_logs (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||||
|
business_id BIGINT UNSIGNED NOT NULL COMMENT '业务ID(固化,无外键;删除保护由服务层承担)',
|
||||||
|
old_status TINYINT NOT NULL COMMENT '变更前状态(创建首条为 1)',
|
||||||
|
new_status TINYINT NOT NULL COMMENT '变更后状态',
|
||||||
|
change_reason 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_business_status_logs PRIMARY KEY (id),
|
||||||
|
CONSTRAINT fk_business_status_logs_operator_id FOREIGN KEY(operator_id) REFERENCES users (id) ON DELETE SET NULL
|
||||||
|
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='业务状态变更记录表(append-only)' COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE INDEX idx_business_status_logs_business_id ON business_status_logs (business_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_business_status_logs_create_time ON business_status_logs (create_time);
|
||||||
|
|
||||||
|
CREATE TABLE businesses (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||||
|
business_no VARCHAR(32) NOT NULL COMMENT '业务编号,自动生成 YW{YYYYMMDD}-{4位当日序号}(报表合同编号)',
|
||||||
|
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID(customers.id)',
|
||||||
|
business_date DATE COMMENT '业务日期(T+ 同步口径)',
|
||||||
|
status TINYINT NOT NULL COMMENT '业务状态:1执行中 2已完成 3已终止(终态不可变更;T+同步不覆盖)' DEFAULT '1',
|
||||||
|
owner_personnel_id BIGINT UNSIGNED COMMENT '负责营销人员ID(核算归属锚点;NULL=待指定,M3 核算前置校验非空;变更留痕不回溯)',
|
||||||
|
assistant_personnel_id BIGINT UNSIGNED COMMENT '协助营销人员ID(一期核算不使用,对照表 #15;仅记录事实)',
|
||||||
|
is_special TINYINT NOT NULL COMMENT '特殊业务标记:1是 0否(公司参与等)' DEFAULT '0',
|
||||||
|
special_note VARCHAR(255) COMMENT '特殊业务说明',
|
||||||
|
total_sale_amount NUMERIC(14, 2) COMMENT '销售额合计(明细行汇总冗余,服务层事务内维护,禁手工直改;不含运费口径)' DEFAULT '0',
|
||||||
|
total_cost_amount NUMERIC(14, 2) COMMENT '成本合计(明细行汇总冗余,服务层事务内维护,禁手工直改)' DEFAULT '0',
|
||||||
|
total_ship_amount NUMERIC(14, 2) COMMENT '发货金额合计(发货明细汇总冗余,服务层事务内维护,禁手工直改)' DEFAULT '0',
|
||||||
|
total_received_amount NUMERIC(14, 2) COMMENT '回款合计(回款记录汇总冗余,服务层事务内维护;M3 起付判定直接消费)' DEFAULT '0',
|
||||||
|
total_freight_amount NUMERIC(14, 2) COMMENT '运费合计(发货单汇总冗余,仅记录展示;不计入毛利润口径,对照表 #2)' DEFAULT '0',
|
||||||
|
remark VARCHAR(255) COMMENT '备注',
|
||||||
|
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
source_no VARCHAR(64) COMMENT 'T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)',
|
||||||
|
t_plus_id VARCHAR(64) COMMENT 'T+ 系统内部标识',
|
||||||
|
sync_batch_no VARCHAR(32) COMMENT '同步批次号(同步模块写入)',
|
||||||
|
synced_at DATETIME COMMENT '最近同步时间',
|
||||||
|
CONSTRAINT pk_businesses PRIMARY KEY (id),
|
||||||
|
CONSTRAINT fk_businesses_assistant_personnel_id FOREIGN KEY(assistant_personnel_id) REFERENCES personnel (id) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT fk_businesses_customer_id FOREIGN KEY(customer_id) REFERENCES customers (id) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT fk_businesses_owner_personnel_id FOREIGN KEY(owner_personnel_id) REFERENCES personnel (id) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT uk_businesses_business_no UNIQUE (business_no)
|
||||||
|
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='业务表(业务编号自动生成;T+同步不覆盖业务状态)' COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE INDEX idx_businesses_business_date ON businesses (business_date);
|
||||||
|
|
||||||
|
CREATE INDEX idx_businesses_customer_id ON businesses (customer_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_businesses_owner_personnel_id ON businesses (owner_personnel_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_businesses_source_no ON businesses (source_no);
|
||||||
|
|
||||||
|
CREATE INDEX idx_businesses_status ON businesses (status);
|
||||||
|
|
||||||
|
CREATE TABLE business_items (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||||
|
business_id BIGINT UNSIGNED NOT NULL COMMENT '业务ID(businesses.id)',
|
||||||
|
product_id BIGINT UNSIGNED COMMENT '产品ID(products.id;NULL=自由录入,未入产品库)',
|
||||||
|
product_name VARCHAR(128) NOT NULL COMMENT '产品名称(引用产品时冗余固化)',
|
||||||
|
spec_model VARCHAR(128) NOT NULL COMMENT '规格型号(冗余固化;空串对齐 products 组合唯一键口径)' DEFAULT '',
|
||||||
|
unit VARCHAR(16) COMMENT '计量单位',
|
||||||
|
product_category VARCHAR(32) NOT NULL COMMENT '产品类别(行级折算锚点,混合业务按产品逐条计算,对照表 #14;值域同 PRODUCT_CATEGORIES)',
|
||||||
|
quantity NUMERIC(14, 2) NOT NULL COMMENT '数量(>0)',
|
||||||
|
sale_price NUMERIC(14, 4) COMMENT '销售单价(元;T+ 同步或人工)',
|
||||||
|
sale_amount NUMERIC(14, 2) NOT NULL COMMENT '销售额 = 数量×销售单价(服务层计算固化;不含运费口径,毛利润率分母)',
|
||||||
|
cost_amount NUMERIC(14, 2) COMMENT '成本额(T+ 同步或人工录入,来源待 T+ 确认;毛利润额 = 销售额 − 成本额)',
|
||||||
|
guided_price NUMERIC(14, 4) COMMENT '指导价(人工录入;T+同步不覆盖,需规 2.2.1/2.2.4)',
|
||||||
|
remark VARCHAR(255) 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_business_items PRIMARY KEY (id),
|
||||||
|
CONSTRAINT fk_business_items_business_id FOREIGN KEY(business_id) REFERENCES businesses (id) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT fk_business_items_product_id FOREIGN KEY(product_id) REFERENCES products (id) ON DELETE RESTRICT
|
||||||
|
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='业务产品明细表(毛利润行级源数据;指导价 T+同步不覆盖)' COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE INDEX idx_business_items_business_id ON business_items (business_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_business_items_product_category ON business_items (product_category);
|
||||||
|
|
||||||
|
CREATE INDEX idx_business_items_product_id ON business_items (product_id);
|
||||||
|
|
||||||
|
CREATE TABLE shipments (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||||
|
shipment_no VARCHAR(32) NOT NULL COMMENT '发货单号,自动生成 SH{YYYYMMDD}-{4位当日序号}(内部追溯用)',
|
||||||
|
business_id BIGINT UNSIGNED NOT NULL COMMENT '业务ID(businesses.id;终态业务禁止新发货)',
|
||||||
|
ship_date DATE NOT NULL COMMENT '发货日期(回款天数基准日,发货日首日=第 1 天)',
|
||||||
|
freight_amount NUMERIC(14, 2) NOT NULL COMMENT '运费金额(仅记录展示;不计入毛利润口径,对照表 #2)' DEFAULT '0',
|
||||||
|
total_amount NUMERIC(14, 2) NOT NULL COMMENT '发货总金额(明细行汇总冗余,服务层事务内维护,禁手工直改)' DEFAULT '0',
|
||||||
|
remark VARCHAR(255) COMMENT '备注',
|
||||||
|
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
source_no VARCHAR(64) COMMENT 'T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)',
|
||||||
|
t_plus_id VARCHAR(64) COMMENT 'T+ 系统内部标识',
|
||||||
|
sync_batch_no VARCHAR(32) COMMENT '同步批次号(同步模块写入)',
|
||||||
|
synced_at DATETIME COMMENT '最近同步时间',
|
||||||
|
CONSTRAINT pk_shipments PRIMARY KEY (id),
|
||||||
|
CONSTRAINT fk_shipments_business_id FOREIGN KEY(business_id) REFERENCES businesses (id) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT uk_shipments_shipment_no UNIQUE (shipment_no)
|
||||||
|
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='发货单表(发货日为回款天数基准日,发货日首日计第 1 天)' COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE INDEX idx_shipments_business_id ON shipments (business_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_shipments_ship_date ON shipments (ship_date);
|
||||||
|
|
||||||
|
CREATE INDEX idx_shipments_source_no ON shipments (source_no);
|
||||||
|
|
||||||
|
CREATE TABLE receipts (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||||
|
receipt_batch_no VARCHAR(32) NOT NULL COMMENT '回款批次号,自动生成 HK{YYYYMMDD}-{4位当日序号}(系统内唯一;T+ 同步回款以 source_no 幂等)',
|
||||||
|
business_id BIGINT UNSIGNED NOT NULL COMMENT '业务ID(businesses.id)',
|
||||||
|
shipment_id BIGINT UNSIGNED COMMENT '发货单ID(shipments.id;手动指定的回款对应批次,须与本行业务一致;NULL=暂未指定)',
|
||||||
|
receipt_date DATE NOT NULL COMMENT '回款到账日(M3 按此日匹配参数版本,对照表 #11)',
|
||||||
|
amount NUMERIC(14, 2) NOT NULL COMMENT '回款金额(≥0;一期禁负数/红字)',
|
||||||
|
is_barter TINYINT NOT NULL COMMENT '以物抵顶标记:1是 0否(M3 核算按应发绩效×barter.discount 折算,M2 仅标记)' DEFAULT '0',
|
||||||
|
settlement_status TINYINT NOT NULL COMMENT '绩效核算状态:0未核算 1已核算(M3 核算防重锚点;已核算回款禁改禁删)' DEFAULT '0',
|
||||||
|
remark VARCHAR(255) COMMENT '备注',
|
||||||
|
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
source_no VARCHAR(64) COMMENT 'T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)',
|
||||||
|
t_plus_id VARCHAR(64) COMMENT 'T+ 系统内部标识',
|
||||||
|
sync_batch_no VARCHAR(32) COMMENT '同步批次号(同步模块写入)',
|
||||||
|
synced_at DATETIME COMMENT '最近同步时间',
|
||||||
|
CONSTRAINT pk_receipts PRIMARY KEY (id),
|
||||||
|
CONSTRAINT fk_receipts_business_id FOREIGN KEY(business_id) REFERENCES businesses (id) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT fk_receipts_shipment_id FOREIGN KEY(shipment_id) REFERENCES shipments (id) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT uk_receipts_receipt_batch_no UNIQUE (receipt_batch_no)
|
||||||
|
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='回款表(手动指定发货批次;已核算回款禁改禁删)' COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE INDEX idx_receipts_business_id ON receipts (business_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_receipts_receipt_date ON receipts (receipt_date);
|
||||||
|
|
||||||
|
CREATE INDEX idx_receipts_settlement_status ON receipts (settlement_status);
|
||||||
|
|
||||||
|
CREATE INDEX idx_receipts_shipment_id ON receipts (shipment_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_receipts_source_no ON receipts (source_no);
|
||||||
|
|
||||||
|
CREATE TABLE shipment_items (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||||
|
shipment_id BIGINT UNSIGNED NOT NULL COMMENT '发货单ID(shipments.id)',
|
||||||
|
product_id BIGINT UNSIGNED COMMENT '产品ID(products.id;NULL=自由录入)',
|
||||||
|
product_name VARCHAR(128) NOT NULL COMMENT '产品名称(引用产品时冗余固化)',
|
||||||
|
spec_model VARCHAR(128) NOT NULL COMMENT '规格型号(冗余固化;空串对齐 products 组合唯一键口径)' DEFAULT '',
|
||||||
|
unit VARCHAR(16) COMMENT '计量单位',
|
||||||
|
quantity NUMERIC(14, 2) NOT NULL COMMENT '发货数量(>0)',
|
||||||
|
sale_price NUMERIC(14, 4) COMMENT '发货单价(元;T+ 同步或人工,口径待 IT 确认)',
|
||||||
|
amount NUMERIC(14, 2) 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_shipment_items PRIMARY KEY (id),
|
||||||
|
CONSTRAINT fk_shipment_items_product_id FOREIGN KEY(product_id) REFERENCES products (id) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT fk_shipment_items_shipment_id FOREIGN KEY(shipment_id) REFERENCES shipments (id) ON DELETE RESTRICT
|
||||||
|
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='发货产品明细表' COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE INDEX idx_shipment_items_product_id ON shipment_items (product_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_shipment_items_shipment_id ON shipment_items (shipment_id);
|
||||||
|
|
||||||
|
INSERT INTO customer_rating_grades (grade_name, min_score, max_score, sort_order, remark) VALUES ('A', 90, 100, 1, '优秀');
|
||||||
|
|
||||||
|
INSERT INTO customer_rating_grades (grade_name, min_score, max_score, sort_order, remark) VALUES ('B', 75, 89, 2, '良好');
|
||||||
|
|
||||||
|
INSERT INTO customer_rating_grades (grade_name, min_score, max_score, sort_order, remark) VALUES ('C', 60, 74, 3, '合格');
|
||||||
|
|
||||||
|
INSERT INTO customer_rating_grades (grade_name, min_score, max_score, sort_order, remark) VALUES ('D', 0, 59, 4, '待改进');
|
||||||
|
|
||||||
|
UPDATE alembic_version SET version_num='8efc189b131f' WHERE alembic_version.version_num = 'e1133a7c670a';
|
||||||
|
|
||||||
115
db/queries/业务数据.sql
Normal file
115
db/queries/业务数据.sql
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- 业务数据查询留档(M2 业务数据管理)
|
||||||
|
--
|
||||||
|
-- 设计要点:
|
||||||
|
-- - businesses 的 total_* 五列为服务层维护的汇总冗余,查询直接读冗余列
|
||||||
|
-- 而非实时 JOIN 聚合;本文件同时给出「对账用」的实时聚合口径
|
||||||
|
-- - 回款天数 = formula.payment_days(ship_date, receipt_date),即
|
||||||
|
-- DATEDIFF(receipt_date, ship_date) + 1(发货日首日计第 1 天,对照表 #1),
|
||||||
|
-- 应用层以纯函数为准,SQL 版仅供人工对账
|
||||||
|
-- - 毛利润率/折算毛利润额不落库:视图按行实时计算(阈值取 M1 参数快照)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- 1. 业务台账(列表页;冗余列直读)
|
||||||
|
SELECT b.id,
|
||||||
|
b.business_no,
|
||||||
|
c.company_name,
|
||||||
|
c.customer_type,
|
||||||
|
b.status,
|
||||||
|
b.business_date,
|
||||||
|
p.personnel_name AS owner_name,
|
||||||
|
pa.personnel_name AS assistant_name,
|
||||||
|
b.total_sale_amount,
|
||||||
|
b.total_cost_amount,
|
||||||
|
b.total_ship_amount,
|
||||||
|
b.total_received_amount,
|
||||||
|
b.total_freight_amount
|
||||||
|
FROM businesses b
|
||||||
|
JOIN customers c ON c.id = b.customer_id
|
||||||
|
LEFT JOIN personnel p ON p.id = b.owner_personnel_id
|
||||||
|
LEFT JOIN personnel pa ON pa.id = b.assistant_personnel_id
|
||||||
|
WHERE (:keyword IS NULL OR b.business_no LIKE CONCAT('%', :keyword, '%')
|
||||||
|
OR c.company_name LIKE CONCAT('%', :keyword, '%'))
|
||||||
|
AND (:status IS NULL OR b.status = :status)
|
||||||
|
AND (:customer_id IS NULL OR b.customer_id = :customer_id)
|
||||||
|
AND (:owner_personnel_id IS NULL OR b.owner_personnel_id = :owner_personnel_id)
|
||||||
|
ORDER BY b.id DESC
|
||||||
|
LIMIT :limit OFFSET :offset;
|
||||||
|
|
||||||
|
-- 2. 汇总冗余对账(五列与明细/发货/回款实时聚合的一致性校验,T11 集成用例同口径)
|
||||||
|
SELECT b.id,
|
||||||
|
b.total_sale_amount,
|
||||||
|
COALESCE((SELECT SUM(i.sale_amount) FROM business_items i WHERE i.business_id = b.id), 0) AS live_sale,
|
||||||
|
b.total_ship_amount,
|
||||||
|
COALESCE((SELECT SUM(si.amount) FROM shipments s JOIN shipment_items si ON si.shipment_id = s.id
|
||||||
|
WHERE s.business_id = b.id), 0) AS live_ship,
|
||||||
|
b.total_received_amount,
|
||||||
|
COALESCE((SELECT SUM(r.amount) FROM receipts r WHERE r.business_id = b.id), 0) AS live_received,
|
||||||
|
b.total_freight_amount,
|
||||||
|
COALESCE((SELECT SUM(s.freight_amount) FROM shipments s WHERE s.business_id = b.id), 0) AS live_freight
|
||||||
|
FROM businesses b
|
||||||
|
WHERE b.total_sale_amount <> COALESCE((SELECT SUM(i.sale_amount) FROM business_items i WHERE i.business_id = b.id), 0)
|
||||||
|
OR b.total_ship_amount <> COALESCE((SELECT SUM(si.amount) FROM shipments s JOIN shipment_items si ON si.shipment_id = s.id
|
||||||
|
WHERE s.business_id = b.id), 0)
|
||||||
|
OR b.total_received_amount <> COALESCE((SELECT SUM(r.amount) FROM receipts r WHERE r.business_id = b.id), 0)
|
||||||
|
OR b.total_freight_amount <> COALESCE((SELECT SUM(s.freight_amount) FROM shipments s WHERE s.business_id = b.id), 0);
|
||||||
|
|
||||||
|
-- 3. 未指定发货批次的回款(M3 核算前置校验名单;列表页筛选项)
|
||||||
|
SELECT r.id, r.receipt_batch_no, b.business_no, r.receipt_date, r.amount, r.is_barter
|
||||||
|
FROM receipts r
|
||||||
|
JOIN businesses b ON b.id = r.business_id
|
||||||
|
WHERE r.shipment_id IS NULL
|
||||||
|
AND r.settlement_status = 0
|
||||||
|
ORDER BY r.receipt_date;
|
||||||
|
|
||||||
|
-- 4. 回款天数对账视图(SQL 版仅供人工核对;应用层以 formula.payment_days 为准)
|
||||||
|
SELECT r.id,
|
||||||
|
r.receipt_batch_no,
|
||||||
|
b.business_no,
|
||||||
|
s.shipment_no,
|
||||||
|
s.ship_date,
|
||||||
|
r.receipt_date,
|
||||||
|
DATEDIFF(r.receipt_date, s.ship_date) + 1 AS payment_days
|
||||||
|
FROM receipts r
|
||||||
|
JOIN businesses b ON b.id = r.business_id
|
||||||
|
LEFT JOIN shipments s ON s.id = r.shipment_id
|
||||||
|
WHERE r.shipment_id IS NOT NULL
|
||||||
|
ORDER BY r.receipt_date DESC;
|
||||||
|
|
||||||
|
-- 5. 按产品类别的毛利润汇总(毛利润视图的类别汇总口径;阈值需按 M1 参数快照逐日匹配,
|
||||||
|
-- 此处仅聚合源数据,折算在应用层以 formula.converted_profit 计算)
|
||||||
|
SELECT i.product_category,
|
||||||
|
SUM(i.sale_amount) AS sale_total,
|
||||||
|
SUM(i.cost_amount) AS cost_total,
|
||||||
|
SUM(i.sale_amount - COALESCE(i.cost_amount, 0)) AS profit_total
|
||||||
|
FROM business_items i
|
||||||
|
WHERE i.business_id = :business_id
|
||||||
|
GROUP BY i.product_category;
|
||||||
|
|
||||||
|
-- 6. 业务状态流转时间线(详情页;创建首条 old=new=1)
|
||||||
|
SELECT l.old_status, l.new_status, l.change_reason, l.operator_name, l.create_time
|
||||||
|
FROM business_status_logs l
|
||||||
|
WHERE l.business_id = :business_id
|
||||||
|
ORDER BY l.create_time, l.id;
|
||||||
|
|
||||||
|
-- 7. 发货单与明细(列表 + 行展开)
|
||||||
|
SELECT s.id, s.shipment_no, b.business_no, s.ship_date, s.freight_amount, s.total_amount
|
||||||
|
FROM shipments s
|
||||||
|
JOIN businesses b ON b.id = s.business_id
|
||||||
|
WHERE (:business_id IS NULL OR s.business_id = :business_id)
|
||||||
|
AND (:ship_date_from IS NULL OR s.ship_date >= :ship_date_from)
|
||||||
|
AND (:ship_date_to IS NULL OR s.ship_date <= :ship_date_to)
|
||||||
|
ORDER BY s.id DESC
|
||||||
|
LIMIT :limit OFFSET :offset;
|
||||||
|
|
||||||
|
SELECT si.product_name, si.spec_model, si.unit, si.quantity, si.sale_price, si.amount
|
||||||
|
FROM shipment_items si
|
||||||
|
WHERE si.shipment_id = :shipment_id
|
||||||
|
ORDER BY si.id;
|
||||||
|
|
||||||
|
-- 8. 评级等级判定(前端按 sort_order 升序首个 min_score ≤ 总分取等级;SQL 版供后端复用)
|
||||||
|
SELECT grade_name, min_score, max_score
|
||||||
|
FROM customer_rating_grades
|
||||||
|
WHERE min_score <= :total_score
|
||||||
|
ORDER BY sort_order
|
||||||
|
LIMIT 1;
|
||||||
Loading…
Reference in New Issue
Block a user