"""基础数据管理六表:人员/辐射区域/拓展计划/产品/客户/归属变更留痕(M2) - personnel / personnel_regions / personnel_plans:人员档案及其 1:N 子表 - products:产品主数据(类别锚定折算阈值参数) - customers:客户档案(公司名称+负责人为业务唯一标识) - ownership_change_logs:归属变更留痕(append-only,不回溯历史) - 六张表的 T+ 同步预留列(source_no 等)全部可空、不设唯一键(对照表 #26) Revision ID: e1133a7c670a Revises: bfbda9c3191d Create Date: 2026-10-13 09:36:02.219248 """ 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 = 'e1133a7c670a' down_revision: str | Sequence[str] | None = 'bfbda9c3191d' branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: """应用迁移。""" # ### commands auto generated by Alembic - please adjust! ### op.create_table('products', sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'), sa.Column('product_name', sa.String(length=128), nullable=False, comment='产品名称'), sa.Column('spec_model', sa.String(length=128), server_default='', nullable=False, comment='规格型号(空串而非 NULL:参与组合唯一键,MySQL 组合索引对 NULL 不去重)'), sa.Column('unit', sa.String(length=16), nullable=True, comment='计量单位'), sa.Column('stock_quantity', sa.Numeric(precision=14, scale=2), nullable=True, comment='数量(T+ 同步原样存储,语义待 IT 确认)'), sa.Column('sale_price', sa.Numeric(precision=14, scale=4), nullable=True, comment='销售单价(元,T+ 同步)'), sa.Column('product_category', sa.String(length=32), nullable=False, comment='产品类别:municipal_water市政水利 rail_transit轨道交通(决定折算阈值 30%/20%;T+同步不覆盖)'), sa.Column('status', mysql.TINYINT(), server_default='1', nullable=False, comment='状态:1启用 0停用'), sa.Column('remark', sa.String(length=255), nullable=True, comment='备注'), sa.Column('create_time', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False, comment='创建时间'), sa.Column('update_time', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), nullable=False, comment='更新时间'), sa.Column('source_no', sa.String(length=64), nullable=True, comment='T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)'), sa.Column('t_plus_id', sa.String(length=64), nullable=True, comment='T+ 系统内部标识'), sa.Column('sync_batch_no', sa.String(length=32), nullable=True, comment='同步批次号(同步模块写入)'), sa.Column('synced_at', sa.DateTime(), nullable=True, comment='最近同步时间'), sa.PrimaryKeyConstraint('id', name=op.f('pk_products')), sa.UniqueConstraint('product_name', 'spec_model', name='uk_products_name_spec'), comment='产品表(类别锚定折算阈值参数;T+同步不覆盖产品类别)', mysql_charset='utf8mb4', mysql_collate='utf8mb4_general_ci', mysql_engine='InnoDB' ) op.create_index(op.f('idx_products_source_no'), 'products', ['source_no'], unique=False) op.create_table('ownership_change_logs', sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'), sa.Column('owner_type', mysql.TINYINT(), nullable=False, comment='归属类型:1客户归属 2业务归属'), sa.Column('customer_id', mysql.BIGINT(unsigned=True), nullable=True, comment='客户ID(固化,owner_type=1 时非空)'), sa.Column('business_id', mysql.BIGINT(unsigned=True), nullable=True, comment='业务ID(固化,owner_type=2 时非空)'), sa.Column('old_owner_personnel_id', mysql.BIGINT(unsigned=True), nullable=True, comment='变更前负责营销人员ID(固化)'), sa.Column('new_owner_personnel_id', mysql.BIGINT(unsigned=True), nullable=True, comment='变更后负责营销人员ID(固化)'), sa.Column('old_assistant_personnel_id', mysql.BIGINT(unsigned=True), nullable=True, comment='变更前协助营销人员ID(固化,仅业务归属使用)'), sa.Column('new_assistant_personnel_id', mysql.BIGINT(unsigned=True), nullable=True, comment='变更后协助营销人员ID(固化,仅业务归属使用)'), sa.Column('change_reason', sa.String(length=255), nullable=True, comment='变更原因'), sa.Column('operator_id', mysql.BIGINT(unsigned=True), nullable=True, comment='操作人ID(账号删除后置空)'), sa.Column('operator_name', sa.String(length=64), nullable=False, comment='操作人用户名(冗余固化)'), sa.Column('create_time', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False, comment='创建时间'), sa.ForeignKeyConstraint(['operator_id'], ['users.id'], name=op.f('fk_ownership_change_logs_operator_id'), ondelete='SET NULL'), sa.PrimaryKeyConstraint('id', name=op.f('pk_ownership_change_logs')), comment='归属变更记录表(append-only;客户/业务归属变更留痕,不回溯历史)', mysql_charset='utf8mb4', mysql_collate='utf8mb4_general_ci', mysql_engine='InnoDB' ) op.create_index(op.f('idx_ownership_change_logs_customer_id'), 'ownership_change_logs', ['customer_id'], unique=False) op.create_index(op.f('idx_ownership_change_logs_business_id'), 'ownership_change_logs', ['business_id'], unique=False) op.create_index(op.f('idx_ownership_change_logs_create_time'), 'ownership_change_logs', ['create_time'], unique=False) op.create_table('personnel', sa.Column('id', mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment='主键ID'), sa.Column('personnel_name', sa.String(length=64), nullable=False, comment='姓名'), sa.Column('business_role_id', mysql.BIGINT(unsigned=True), nullable=False, comment='岗位角色ID(决定计提公式模板;T+同步不覆盖)'), sa.Column('manager_id', mysql.BIGINT(unsigned=True), nullable=True, comment='直属营销经理ID(NULL=经理本人或未分配;团队成员关系锚点)'), sa.Column('phone', sa.String(length=20), nullable=True, comment='联系电话'), sa.Column('hire_date', sa.Date(), nullable=True, comment='入职日期'), sa.Column('is_probation', mysql.TINYINT(), server_default='0', nullable=False, comment='是否试用期:1是 0否(试用期绩效照算不发放)'), sa.Column('salary_standard', sa.Numeric(precision=12, scale=2), nullable=True, comment='月工资标准(元;T+同步不覆盖)'), sa.Column('status', mysql.TINYINT(), server_default='1', nullable=False, comment='状态:1在职 0离职(离职人员不可再被业务/客户归属引用)'), sa.Column('remark', sa.String(length=255), nullable=True, comment='备注'), sa.Column('create_time', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False, comment='创建时间'), sa.Column('update_time', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), nullable=False, comment='更新时间'), sa.Column('source_no', sa.String(length=64), nullable=True, comment='T+ 原始单据号(同步幂等去重锚点;唯一键待 IT 定键后升级)'), sa.Column('t_plus_id', sa.String(length=64), nullable=True, comment='T+ 系统内部标识'), sa.Column('sync_batch_no', sa.String(length=32), nullable=True, comment='同步批次号(同步模块写入)'), sa.Column('synced_at', sa.DateTime(), nullable=True, comment='最近同步时间'), sa.ForeignKeyConstraint(['business_role_id'], ['business_roles.id'], name=op.f('fk_personnel_business_role_id'), ondelete='RESTRICT'), sa.ForeignKeyConstraint(['manager_id'], ['personnel.id'], name=op.f('fk_personnel_manager_id'), ondelete='RESTRICT'), sa.PrimaryKeyConstraint('id', name=op.f('pk_personnel')), comment='营销人员表(T+同步不覆盖岗位与工资标准)', mysql_charset='utf8mb4', mysql_collate='utf8mb4_general_ci', mysql_engine='InnoDB' ) op.create_index(op.f('idx_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) # ### end Alembic commands ### 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 会连带删除索引与外键。 """ op.drop_table('personnel_regions') op.drop_table('personnel_plans') op.drop_table('customers') op.drop_table('personnel') op.drop_table('ownership_change_logs') op.drop_table('products')