修复拆镜复刻路由,修补API参数说明,版本迁移异常修复

This commit is contained in:
2026-06-17 11:25:11 +08:00
parent 0bad14936b
commit 5d5e6485ee
6 changed files with 493 additions and 291 deletions
@@ -8,122 +8,262 @@ from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy import inspect
# revision identifiers, used by Alembic.
revision: str = '287c6c064c5d'
down_revision: Union[str, None] = '9216bca75ccf'
revision: str = "287c6c064c5d"
down_revision: Union[str, None] = "9216bca75ccf"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def _inspector():
return inspect(op.get_bind())
def _table_exists(table_name: str) -> bool:
return table_name in _inspector().get_table_names(schema="public")
def _column_exists(table_name: str, column_name: str) -> bool:
if not _table_exists(table_name):
return False
columns = _inspector().get_columns(table_name, schema="public")
return any(column["name"] == column_name for column in columns)
def _index_exists(table_name: str, index_name: str) -> bool:
if not _table_exists(table_name):
return False
indexes = _inspector().get_indexes(table_name, schema="public")
return any(index["name"] == index_name for index in indexes)
def _create_pre_test_template_table() -> None:
op.create_table(
"pre_test_template",
sa.Column("id", sa.String(length=32), nullable=False, comment="主键"),
sa.Column("name", sa.String(length=128), nullable=False, comment="模板名称"),
sa.Column("user_id", sa.String(length=32), nullable=False, comment="用户id"),
sa.Column("note", sa.Text(), nullable=True, comment="模板备注"),
sa.Column("platform", sa.String(length=32), nullable=True, comment="投放平台(AD/QIANCHUAN/LOCAL"),
sa.Column("external_action", sa.String(length=64), nullable=True, comment="转化目标"),
sa.Column("cpa_bid", sa.Float(), nullable=True, comment="目标转化成本:[1, 10000]"),
sa.Column("audience_gender", sa.String(length=16), nullable=True, comment="性别(ALL/MALE/FEMALE"),
sa.Column("audience_age", sa.Text(), nullable=True, comment="受众年龄,JSON数组, 格式:[ALL,18-23, 24-30, 31-40, 41-49, 50+]"),
sa.Column("audience_region", sa.Text(), nullable=True, comment="受众地区,JSON数组(二级行政区域code)"),
sa.Column("audience_network", sa.Text(), nullable=True, comment="网络类型,JSON数组, 格式:[ALL,5G,4G,3G,2G,WIFI]"),
sa.Column("cus_name", sa.String(length=256), nullable=True, comment="客户主体名称"),
sa.Column("pricing_type", sa.String(length=16), nullable=True, comment="出价类型(OCPC/CPA/OCPM"),
sa.Column("cost_cap", sa.Boolean(), nullable=True, comment="是否最优成本出价(仅AD支持)"),
sa.Column("target_cost", sa.Boolean(), nullable=True, comment="是否稳定成本出价(仅AD支持)"),
sa.Column("nobid", sa.Boolean(), nullable=True, comment="是否最大转化出价(仅AD支持)"),
sa.Column("cpc_bid", sa.Float(), nullable=True, comment="目标点击成本:[1, 10000]"),
sa.Column("budget", sa.Float(), nullable=True, comment="预算金额:[1, 10000]"),
sa.Column("is_default", sa.Boolean(), nullable=True, comment="是否默认模板"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint("id", name="pre_test_template_pkey"),
)
if not _index_exists("pre_test_template", "ix_pre_test_template_user_id"):
op.create_index(
"ix_pre_test_template_user_id",
"pre_test_template",
["user_id"],
unique=False,
)
def _upgrade_existing_pre_test_template_table() -> None:
if not _column_exists("pre_test_template", "note"):
op.add_column(
"pre_test_template",
sa.Column("note", sa.Text(), nullable=True, comment="模板备注"),
)
if _column_exists("pre_test_template", "platform"):
op.alter_column(
"pre_test_template",
"platform",
existing_type=sa.VARCHAR(length=32),
nullable=True,
existing_comment="投放平台(AD/QIANCHUAN/LOCAL",
)
if _column_exists("pre_test_template", "external_action"):
op.alter_column(
"pre_test_template",
"external_action",
existing_type=sa.VARCHAR(length=64),
nullable=True,
existing_comment="转化目标",
)
if _column_exists("pre_test_template", "audience_gender"):
op.alter_column(
"pre_test_template",
"audience_gender",
existing_type=sa.VARCHAR(length=16),
nullable=True,
existing_comment="性别(ALL/MALE/FEMALE",
)
if _column_exists("pre_test_template", "audience_age"):
op.alter_column(
"pre_test_template",
"audience_age",
existing_type=sa.TEXT(),
comment="受众年龄,JSON数组, 格式:[ALL,18-23, 24-30, 31-40, 41-49, 50+]",
existing_comment="受众年龄,JSON数组",
existing_nullable=True,
)
if _column_exists("pre_test_template", "audience_network"):
op.alter_column(
"pre_test_template",
"audience_network",
existing_type=sa.TEXT(),
comment="网络类型,JSON数组, 格式:[ALL,5G,4G,3G,2G,WIFI]",
existing_comment="网络类型,JSON数组",
existing_nullable=True,
)
if _column_exists("pre_test_template", "pricing_type"):
op.alter_column(
"pre_test_template",
"pricing_type",
existing_type=sa.VARCHAR(length=16),
nullable=True,
existing_comment="出价类型(OCPC/CPA/OCPM",
)
if _column_exists("pre_test_template", "cost_cap"):
op.alter_column(
"pre_test_template",
"cost_cap",
existing_type=sa.BOOLEAN(),
nullable=True,
existing_comment="是否最优成本出价(仅AD支持)",
)
if _column_exists("pre_test_template", "target_cost"):
op.alter_column(
"pre_test_template",
"target_cost",
existing_type=sa.BOOLEAN(),
nullable=True,
existing_comment="是否稳定成本出价(仅AD支持)",
)
if _column_exists("pre_test_template", "nobid"):
op.alter_column(
"pre_test_template",
"nobid",
existing_type=sa.BOOLEAN(),
nullable=True,
existing_comment="是否最大转化出价(仅AD支持)",
)
if _column_exists("pre_test_template", "is_default"):
op.alter_column(
"pre_test_template",
"is_default",
existing_type=sa.BOOLEAN(),
nullable=True,
existing_comment="是否默认模板",
)
if _column_exists("pre_test_template", "status"):
op.drop_column("pre_test_template", "status")
if _column_exists("pre_test_template", "description"):
op.drop_column("pre_test_template", "description")
if not _index_exists("pre_test_template", "ix_pre_test_template_user_id"):
op.create_index(
"ix_pre_test_template_user_id",
"pre_test_template",
["user_id"],
unique=False,
)
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('pre_test_template', sa.Column('note', sa.Text(), nullable=True, comment='模板备注'))
op.alter_column('pre_test_template', 'platform',
existing_type=sa.VARCHAR(length=32),
nullable=True,
existing_comment='投放平台(AD/QIANCHUAN/LOCAL')
op.alter_column('pre_test_template', 'external_action',
existing_type=sa.VARCHAR(length=64),
nullable=True,
existing_comment='转化目标')
op.alter_column('pre_test_template', 'audience_gender',
existing_type=sa.VARCHAR(length=16),
nullable=True,
existing_comment='性别(ALL/MALE/FEMALE')
op.alter_column('pre_test_template', 'audience_age',
existing_type=sa.TEXT(),
comment='受众年龄,JSON数组, 格式:[ALL,18-23, 24-30, 31-40, 41-49, 50+]',
existing_comment='受众年龄,JSON数组',
existing_nullable=True)
op.alter_column('pre_test_template', 'audience_network',
existing_type=sa.TEXT(),
comment='网络类型,JSON数组, 格式:[ALL,5G,4G,3G,2G,WIFI]',
existing_comment='网络类型,JSON数组',
existing_nullable=True)
op.alter_column('pre_test_template', 'pricing_type',
existing_type=sa.VARCHAR(length=16),
nullable=True,
existing_comment='出价类型(OCPC/CPA/OCPM')
op.alter_column('pre_test_template', 'cost_cap',
existing_type=sa.BOOLEAN(),
nullable=True,
existing_comment='是否最优成本出价(仅AD支持)')
op.alter_column('pre_test_template', 'target_cost',
existing_type=sa.BOOLEAN(),
nullable=True,
existing_comment='是否稳定成本出价(仅AD支持)')
op.alter_column('pre_test_template', 'nobid',
existing_type=sa.BOOLEAN(),
nullable=True,
existing_comment='是否最大转化出价(仅AD支持)')
op.alter_column('pre_test_template', 'is_default',
existing_type=sa.BOOLEAN(),
nullable=True,
existing_comment='是否默认模板')
op.drop_column('pre_test_template', 'status')
op.drop_column('pre_test_template', 'description')
op.add_column('resources_material', sa.Column('task_id', sa.String(length=32), nullable=True, comment='前测任务id'))
op.add_column('resources_material', sa.Column('note', sa.Text(), nullable=True, comment='前测失败备注或者其他备注'))
op.add_column('resources_material', sa.Column('status', sa.String(length=16), nullable=True, comment='前测状态(FAILED/PENDING/SUCCESS'))
op.add_column('resources_material', sa.Column('pre_result', sa.Text(), nullable=True, comment='前测结果,JSON数组对象'))
op.add_column('resources_material', sa.Column('pre_test_template_id', sa.String(length=32), nullable=True, comment='前测模板id'))
op.create_index(op.f('ix_resources_material_task_id'), 'resources_material', ['task_id'], unique=False)
# ### end Alembic commands ###
# pre_test_template 是这版迁移缺失的核心表。
# 如果不存在,直接按当前模型源码创建完整表。
# 如果已存在,则按原迁移逻辑补 note、调整 nullable/comment、删除旧字段。
if not _table_exists("pre_test_template"):
_create_pre_test_template_table()
else:
_upgrade_existing_pre_test_template_table()
# resources_material 追加字段,按源码迁移逻辑保留,同时增加字段存在判断,避免重复执行报错。
if _table_exists("resources_material"):
if not _column_exists("resources_material", "task_id"):
op.add_column(
"resources_material",
sa.Column("task_id", sa.String(length=32), nullable=True, comment="前测任务id"),
)
if not _column_exists("resources_material", "note"):
op.add_column(
"resources_material",
sa.Column("note", sa.Text(), nullable=True, comment="前测失败备注或者其他备注"),
)
if not _column_exists("resources_material", "status"):
op.add_column(
"resources_material",
sa.Column("status", sa.String(length=16), nullable=True, comment="前测状态(FAILED/PENDING/SUCCESS"),
)
if not _column_exists("resources_material", "pre_result"):
op.add_column(
"resources_material",
sa.Column("pre_result", sa.Text(), nullable=True, comment="前测结果,JSON数组对象"),
)
if not _column_exists("resources_material", "pre_test_template_id"):
op.add_column(
"resources_material",
sa.Column("pre_test_template_id", sa.String(length=32), nullable=True, comment="前测模板id"),
)
if not _index_exists("resources_material", "ix_resources_material_task_id"):
op.create_index(
"ix_resources_material_task_id",
"resources_material",
["task_id"],
unique=False,
)
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_resources_material_task_id'), table_name='resources_material')
op.drop_column('resources_material', 'pre_test_template_id')
op.drop_column('resources_material', 'pre_result')
op.drop_column('resources_material', 'status')
op.drop_column('resources_material', 'note')
op.drop_column('resources_material', 'task_id')
op.add_column('pre_test_template', sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True, comment='模板描述'))
op.add_column('pre_test_template', sa.Column('status', sa.INTEGER(), autoincrement=False, nullable=False, comment='状态(0=禁用,1=启用)'))
op.alter_column('pre_test_template', 'is_default',
existing_type=sa.BOOLEAN(),
nullable=False,
existing_comment='是否默认模板')
op.alter_column('pre_test_template', 'nobid',
existing_type=sa.BOOLEAN(),
nullable=False,
existing_comment='是否最大转化出价(仅AD支持)')
op.alter_column('pre_test_template', 'target_cost',
existing_type=sa.BOOLEAN(),
nullable=False,
existing_comment='是否稳定成本出价(仅AD支持)')
op.alter_column('pre_test_template', 'cost_cap',
existing_type=sa.BOOLEAN(),
nullable=False,
existing_comment='是否最优成本出价(仅AD支持)')
op.alter_column('pre_test_template', 'pricing_type',
existing_type=sa.VARCHAR(length=16),
nullable=False,
existing_comment='出价类型(OCPC/CPA/OCPM')
op.alter_column('pre_test_template', 'audience_network',
existing_type=sa.TEXT(),
comment='网络类型,JSON数组',
existing_comment='网络类型,JSON数组, 格式:[ALL,5G,4G,3G,2G,WIFI]',
existing_nullable=True)
op.alter_column('pre_test_template', 'audience_age',
existing_type=sa.TEXT(),
comment='受众年龄,JSON数组',
existing_comment='受众年龄,JSON数组, 格式:[ALL,18-23, 24-30, 31-40, 41-49, 50+]',
existing_nullable=True)
op.alter_column('pre_test_template', 'audience_gender',
existing_type=sa.VARCHAR(length=16),
nullable=False,
existing_comment='性别(ALL/MALE/FEMALE')
op.alter_column('pre_test_template', 'external_action',
existing_type=sa.VARCHAR(length=64),
nullable=False,
existing_comment='转化目标')
op.alter_column('pre_test_template', 'platform',
existing_type=sa.VARCHAR(length=32),
nullable=False,
existing_comment='投放平台(AD/QIANCHUAN/LOCAL')
op.drop_column('pre_test_template', 'note')
# ### end Alembic commands ###
if _table_exists("resources_material"):
if _index_exists("resources_material", "ix_resources_material_task_id"):
op.drop_index("ix_resources_material_task_id", table_name="resources_material")
if _column_exists("resources_material", "pre_test_template_id"):
op.drop_column("resources_material", "pre_test_template_id")
if _column_exists("resources_material", "pre_result"):
op.drop_column("resources_material", "pre_result")
if _column_exists("resources_material", "status"):
op.drop_column("resources_material", "status")
if _column_exists("resources_material", "note"):
op.drop_column("resources_material", "note")
if _column_exists("resources_material", "task_id"):
op.drop_column("resources_material", "task_id")
# 这一版迁移负责新增 pre_test_template 表,所以回滚时删除该表。
if _table_exists("pre_test_template"):
if _index_exists("pre_test_template", "ix_pre_test_template_user_id"):
op.drop_index("ix_pre_test_template_user_id", table_name="pre_test_template")
op.drop_table("pre_test_template")