"""cps v2: 活动 payload(淘口令/京东链接) + 群多平台/sid 可空 + 点击 event_type Revision ID: cps_v2_platforms Revises: cps_link_tables Create Date: 2026-06-17 11:00:00.000000 接入淘宝/京东: - cps_activity.payload 淘宝整段淘口令文本 / 京东推广链接(美团仍用 act_id) - cps_group.platforms 该群发哪些平台(多选);sid 改可空(纯淘宝/京东无 sid) - cps_link.sid 可空 + target_url 加长(淘口令较长) - cps_click.event_type visit / copy(淘宝落地页"复制口令") """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql revision: str = "cps_v2_platforms" down_revision: Union[str, Sequence[str], None] = "cps_link_tables" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def _has_column(insp: sa.Inspector, table: str, column: str) -> bool: return any(c["name"] == column for c in insp.get_columns(table)) def upgrade() -> None: bind = op.get_bind() insp = sa.inspect(bind) # SQLite 不认 ::jsonb cast(会报 "unrecognized token: :"),只有 PG 用显式 cast。 # 两边列类型都靠上面的 with_variant 区分,这里只管 server_default 文本。 platforms_default = ( sa.text("'[\"meituan\"]'::jsonb") if bind.dialect.name == "postgresql" else sa.text("'[\"meituan\"]'") ) # 活动:淘宝淘口令 / 京东链接 # 加 add_column 存在性守卫:本迁移历史上在 SQLite 第一次跑时加完 payload 即在 # platforms 那步炸掉(::jsonb),DDL 非事务不回滚 → 半应用状态。守卫让其可自愈重跑。 if not _has_column(insp, "cps_activity", "payload"): op.add_column("cps_activity", sa.Column("payload", sa.Text(), nullable=True)) # 群:多平台(现有群都是美团,server_default 回填)+ sid 可空。 # SQLite 不支持 ALTER COLUMN,必须用 batch(重建表)才能改 nullable;PG 走原生 ALTER。 with op.batch_alter_table("cps_group") as batch_op: if not _has_column(insp, "cps_group", "platforms"): batch_op.add_column( sa.Column( "platforms", sa.JSON().with_variant(postgresql.JSONB(), "postgresql"), nullable=False, server_default=platforms_default, ) ) batch_op.alter_column("sid", existing_type=sa.String(64), nullable=True) # link:sid 可空 + target 加长 with op.batch_alter_table("cps_link") as batch_op: batch_op.alter_column("sid", existing_type=sa.String(64), nullable=True) batch_op.alter_column("target_url", existing_type=sa.String(1024), type_=sa.String(2048)) # click:sid 可空 + 事件类型 with op.batch_alter_table("cps_click") as batch_op: batch_op.alter_column("sid", existing_type=sa.String(64), nullable=True) if not _has_column(insp, "cps_click", "event_type"): batch_op.add_column( sa.Column("event_type", sa.String(16), nullable=False, server_default="visit") ) def downgrade() -> None: with op.batch_alter_table("cps_click") as batch_op: batch_op.drop_column("event_type") batch_op.alter_column("sid", existing_type=sa.String(64), nullable=False) with op.batch_alter_table("cps_link") as batch_op: batch_op.alter_column("target_url", existing_type=sa.String(2048), type_=sa.String(1024)) batch_op.alter_column("sid", existing_type=sa.String(64), nullable=False) with op.batch_alter_table("cps_group") as batch_op: batch_op.drop_column("platforms") batch_op.alter_column("sid", existing_type=sa.String(64), nullable=False) with op.batch_alter_table("cps_activity") as batch_op: batch_op.drop_column("payload")