"""init push tables Revision ID: 0002_init_push_tables Revises: 0001_init_content_tables Create Date: 2026-02-03 """ from __future__ import annotations from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import mysql # revision identifiers, used by Alembic. revision = "0002_init_push_tables" down_revision = "0001_init_content_tables" branch_labels = None depends_on = None def upgrade() -> None: # push_tokens op.create_table( "push_tokens", sa.Column( "id", mysql.BIGINT(unsigned=True), primary_key=True, autoincrement=True, comment="主键", ), sa.Column("client_user_id", sa.String(length=64), nullable=False, comment="客户端用户标识(UUID)"), sa.Column( "platform", sa.Enum("ios", "android", name="push_platform"), nullable=False, comment="平台", ), sa.Column("push_token", sa.String(length=255), nullable=False, comment="Expo Push Token"), sa.Column("app_id", sa.String(length=255), nullable=False, comment="bundle id / package name(用于隔离)"), sa.Column( "env", sa.Enum("dev", "prod", name="push_env"), nullable=False, comment="环境隔离", ), sa.Column( "is_active", sa.Boolean(), server_default=sa.text("1"), nullable=False, comment="是否有效(发送失败且不可恢复时置为 false)", ), sa.Column("last_seen_at", sa.DateTime(), server_default=sa.func.now(), nullable=False, comment="最后一次上报时间"), sa.UniqueConstraint("env", "app_id", "push_token", name="uniq_env_app_token"), mysql_charset="utf8mb4", ) op.create_index("idx_push_tokens_client_user_id", "push_tokens", ["client_user_id"], unique=False) op.create_index("idx_push_tokens_is_active", "push_tokens", ["is_active"], unique=False) # push_preferences op.create_table( "push_preferences", sa.Column("client_user_id", sa.String(length=64), primary_key=True, comment="客户端用户标识(UUID)"), sa.Column( "enabled", sa.Boolean(), server_default=sa.text("0"), nullable=False, comment="是否开启每日提醒(enabled=false 或 times_per_day=0 均视为关闭)", ), sa.Column( "times_per_day", sa.SmallInteger(), server_default="0", nullable=False, comment="每天推送次数(0~5)", ), sa.Column("timezone", sa.String(length=64), nullable=True, comment="IANA 时区(例如 Asia/Shanghai),来自客户端上报"), sa.Column("locale", sa.String(length=32), nullable=True, comment="客户端语言(例如 zh-CN/en/zh-TW),用于文案语言选择"), sa.Column("user_profile_json", sa.JSON(), nullable=True, comment="用户画像(V1.2;可选)。用于 Push 场景推荐文案生成。"), sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False, comment="更新时间"), mysql_charset="utf8mb4", ) # push_send_log op.create_table( "push_send_log", sa.Column( "id", mysql.BIGINT(unsigned=True), primary_key=True, autoincrement=True, comment="主键", ), sa.Column("client_user_id", sa.String(length=64), nullable=False, comment="客户端用户标识(UUID)"), sa.Column("local_date", sa.Date(), nullable=False, comment="用户时区的本地日期(用于幂等)"), sa.Column("slot_index", sa.SmallInteger(), nullable=False, comment="当天第几条(1..times_per_day)"), sa.Column("scheduled_at", sa.DateTime(), nullable=False, comment="计划发送时间(UTC)"), sa.Column("sent_at", sa.DateTime(), nullable=True, comment="实际发送时间(UTC)"), sa.Column("status", sa.String(length=16), server_default="scheduled", nullable=False, comment="scheduled/sent/failed"), sa.Column("error", sa.Text(), nullable=True, comment="失败原因(可选)"), sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False, comment="创建时间"), sa.UniqueConstraint("client_user_id", "local_date", "slot_index", name="uniq_user_date_slot"), mysql_charset="utf8mb4", ) op.create_index("idx_push_log_user_date", "push_send_log", ["client_user_id", "local_date"], unique=False) op.create_index("idx_push_log_status", "push_send_log", ["status"], unique=False) def downgrade() -> None: op.drop_index("idx_push_log_status", table_name="push_send_log") op.drop_index("idx_push_log_user_date", table_name="push_send_log") op.drop_table("push_send_log") op.drop_table("push_preferences") op.drop_index("idx_push_tokens_is_active", table_name="push_tokens") op.drop_index("idx_push_tokens_client_user_id", table_name="push_tokens") op.drop_table("push_tokens")