refactor(alembic): squash all migrations into clean initial_schema

- Replace 8 messy migration files (~2000+ lines) with single clean initial_schema (215 lines)
- All table comments defined inline at CREATE TABLE time (no more alter_column spam)
- Final table names used directly (mjk_broll_categories, etc. — no rename chain)
- Includes diagnosis report at docs/alembic-diagnosis-report.md
This commit is contained in:
小鱼开发
2026-05-15 17:35:54 +08:00
parent d71cfb8449
commit 542bc1f070
10 changed files with 396 additions and 981 deletions
@@ -1,155 +0,0 @@
"""
初始 schema — 创建所有业务表
包含:
- mjk_users(用户)
- mjk_user_devices(单设备登录)
- mjk_user_points(积分汇总)
- mjk_point_batches(积分批次)
- mjk_point_transactions(积分流水)
- mjk_point_recharge_orders(充值订单)
设计决策:
- 无外键约束:业务层软删除,不依赖数据库级联
- 无手动索引:Unique 约束自带索引,其余索引按需后续添加
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "509aa8b53d81"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ====== mjk_users ======
op.create_table(
"mjk_users",
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("mobile", sa.String(length=20), nullable=False),
sa.Column("password_hash", sa.String(length=255), nullable=True),
sa.Column("status", sa.String(length=20), nullable=False),
sa.Column("nickname", sa.String(length=64), nullable=True),
sa.Column("avatar_url", sa.Text(), nullable=True),
sa.Column("source", sa.String(length=32), nullable=False),
sa.Column("invited_by", sa.String(length=36), nullable=True),
sa.Column("last_login_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_login_ip", sa.String(length=45), nullable=True),
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("extra", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("mobile"),
)
# ====== mjk_user_devices ======
op.create_table(
"mjk_user_devices",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("device_id", sa.String(length=64), nullable=False),
sa.Column("device_name", sa.String(length=128), nullable=True),
sa.Column("os_info", sa.String(length=128), nullable=True),
sa.Column("app_version", sa.String(length=32), nullable=True),
sa.Column("refresh_token_hash", sa.String(length=64), nullable=True),
sa.Column("last_active_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("user_id"),
)
# ====== mjk_user_points ======
op.create_table(
"mjk_user_points",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("balance", sa.Integer(), nullable=False),
sa.Column("total_recharged", sa.Integer(), nullable=False),
sa.Column("total_consumed", sa.Integer(), nullable=False),
sa.Column("total_expired", sa.Integer(), nullable=False),
sa.Column("total_refunded", sa.Integer(), nullable=False),
sa.Column("frozen", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("user_id"),
)
# ====== mjk_point_batches ======
op.create_table(
"mjk_point_batches",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("amount", sa.Integer(), nullable=False),
sa.Column("remaining", sa.Integer(), nullable=False),
sa.Column("frozen", sa.Integer(), nullable=False),
sa.Column("expired_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("source", sa.String(length=32), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
# ====== mjk_point_transactions ======
op.create_table(
"mjk_point_transactions",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("type", sa.String(length=20), nullable=False),
sa.Column("amount", sa.Integer(), nullable=False),
sa.Column("balance_before", sa.Integer(), nullable=False),
sa.Column("balance_after", sa.Integer(), nullable=False),
sa.Column("source_type", sa.String(length=32), nullable=True),
sa.Column("source_id", sa.String(length=64), nullable=True),
sa.Column("batch_id", sa.BigInteger(), nullable=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
# ====== mjk_point_recharge_orders ======
op.create_table(
"mjk_point_recharge_orders",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("points", sa.Integer(), nullable=False),
sa.Column("amount_rmb", sa.Integer(), nullable=False),
sa.Column("out_trade_no", sa.String(length=64), nullable=True),
sa.Column("prepay_id", sa.String(length=64), nullable=True),
sa.Column("wx_order_no", sa.String(length=64), nullable=True),
sa.Column("openid", sa.String(length=64), nullable=True),
sa.Column("client_ip", sa.String(length=45), nullable=True),
sa.Column("trade_type", sa.String(length=16), nullable=True),
sa.Column("status", sa.String(length=20), nullable=False),
sa.Column("paid_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("closed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("request_params", sa.Text(), nullable=True),
sa.Column("request_response", sa.Text(), nullable=True),
sa.Column("notify_raw", sa.Text(), nullable=True),
sa.Column("notify_verified", sa.Boolean(), nullable=False),
sa.Column("query_result", sa.Text(), nullable=True),
sa.Column("error_code", sa.String(length=32), nullable=True),
sa.Column("error_msg", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("out_trade_no"),
)
def downgrade() -> None:
op.drop_table("mjk_point_recharge_orders")
op.drop_table("mjk_point_transactions")
op.drop_table("mjk_point_batches")
op.drop_table("mjk_user_points")
op.drop_table("mjk_user_devices")
op.drop_table("mjk_users")
@@ -1,593 +0,0 @@
"""add broll material tables
Revision ID: 69274ce979a5
Revises: 8aa48b89a07d
Create Date: 2026-05-11 13:56:36.332738
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '69274ce979a5'
down_revision: Union[str, Sequence[str], None] = '8aa48b89a07d'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('mjk_categories',
sa.Column('slug', sa.String(length=128), nullable=False, comment='分类标识符,URL友好格式'),
sa.Column('name', sa.String(length=256), nullable=False, comment='分类中文名称,三级分类直接对应 scene 标准化后的值'),
sa.Column('parent_id', sa.BigInteger(), nullable=True, comment='父分类ID,NULL 表示根分类(一级)'),
sa.Column('level', sa.BigInteger(), nullable=False, comment='层级:1=一级(大阶段),2=二级(工序),3=三级(场景)'),
sa.Column('sort_order', sa.BigInteger(), nullable=False, comment='排序权重,装修流程有先后顺序'),
sa.Column('status', sa.String(length=16), nullable=False, comment='状态:active(启用)/ disabled(停用)/ deleted(软删除)'),
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['parent_id'], ['mjk_categories.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('slug')
)
op.create_table('mjk_tags',
sa.Column('name', sa.String(length=64), nullable=False, comment='标签名称,如 近景、白天、水管'),
sa.Column('category', sa.String(length=32), nullable=True, comment='标签维度:scene(场景)/ element(元素)/ style(风格)/ mood(情绪)/ time(时间)'),
sa.Column('sort_order', sa.Integer(), nullable=False, comment='排序权重'),
sa.Column('status', sa.String(length=16), nullable=False, comment='状态:active(启用)/ disabled(停用)'),
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('mjk_materials',
sa.Column('category_id', sa.BigInteger(), nullable=False, comment='所属三级分类ID,关联 mjk_categories'),
sa.Column('title', sa.String(length=256), nullable=False, comment='素材标题/文件名,运营后台识别用'),
sa.Column('url', sa.String(length=1024), nullable=False, comment='七牛云 CDN 访问地址,FFmpeg合成和前端播放直接使用'),
sa.Column('duration', sa.Float(), nullable=False, comment='视频时长(秒),FFmpeg probe 提取,入库时必须大于0'),
sa.Column('usage_count', sa.BigInteger(), nullable=False, comment='累计使用次数,驱动加权随机算法'),
sa.Column('status', sa.String(length=16), nullable=False, comment='状态:active(可用)/ disabled(下架)/ deleted(软删除)'),
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['category_id'], ['mjk_categories.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.alter_column('mjk_point_batches', 'user_id',
existing_type=sa.UUID(),
comment='用户 ID',
existing_nullable=False)
op.alter_column('mjk_point_batches', 'amount',
existing_type=sa.INTEGER(),
comment='初始积分',
existing_nullable=False)
op.alter_column('mjk_point_batches', 'remaining',
existing_type=sa.INTEGER(),
comment='剩余可用积分',
existing_nullable=False)
op.alter_column('mjk_point_batches', 'expired_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
comment='过期时间(created_at + 180 天)',
existing_nullable=False)
op.alter_column('mjk_point_batches', 'source',
existing_type=sa.VARCHAR(length=32),
comment='来源:wxpay / invite / gift / compensation',
existing_nullable=False)
op.alter_column('mjk_point_recharge_orders', 'user_id',
existing_type=sa.UUID(),
comment='用户 ID',
existing_nullable=False)
op.alter_column('mjk_point_recharge_orders', 'points',
existing_type=sa.INTEGER(),
comment='充值积分数',
existing_nullable=False)
op.alter_column('mjk_point_recharge_orders', 'amount_rmb',
existing_type=sa.INTEGER(),
comment='人民币金额(单位:分,如 500 = 5 元)',
existing_nullable=False)
op.alter_column('mjk_point_recharge_orders', 'out_trade_no',
existing_type=sa.VARCHAR(length=64),
comment='商户订单号(传给微信的 out_trade_no',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'prepay_id',
existing_type=sa.VARCHAR(length=64),
comment='微信预支付会话标识(统一下单返回)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'wx_order_no',
existing_type=sa.VARCHAR(length=64),
comment='微信支付订单号(微信侧唯一标识)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'openid',
existing_type=sa.VARCHAR(length=64),
comment='用户微信 OpenID(统一下单必需)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'client_ip',
existing_type=sa.VARCHAR(length=45),
comment='用户下单时的 IP 地址',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'trade_type',
existing_type=sa.VARCHAR(length=16),
comment='交易类型:JSAPI / NATIVE / APP',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'status',
existing_type=sa.VARCHAR(length=20),
comment='订单状态:pending / paid / failed / closed',
existing_nullable=False)
op.alter_column('mjk_point_recharge_orders', 'paid_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
comment='支付成功时间',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'closed_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
comment='订单关闭时间(超时未支付)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'request_params',
existing_type=sa.TEXT(),
comment='统一下单请求参数(JSON 格式,用于排查请求侧问题)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'request_response',
existing_type=sa.TEXT(),
comment='统一下单响应内容(JSON 格式,用于排查微信返回)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'notify_raw',
existing_type=sa.TEXT(),
comment='微信回调原始内容(XML/JSON,用于排查回调问题)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'notify_verified',
existing_type=sa.BOOLEAN(),
comment='回调签名是否验证通过',
existing_nullable=False)
op.alter_column('mjk_point_recharge_orders', 'query_result',
existing_type=sa.TEXT(),
comment='主动查询订单结果(JSON 格式,用于二次确认)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'error_code',
existing_type=sa.VARCHAR(length=32),
comment='错误码(微信返回或系统异常)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'error_msg',
existing_type=sa.TEXT(),
comment='错误描述(用于快速定位问题)',
existing_nullable=True)
op.alter_column('mjk_point_transactions', 'user_id',
existing_type=sa.UUID(),
comment='用户 ID',
existing_nullable=False)
op.alter_column('mjk_point_transactions', 'type',
existing_type=sa.VARCHAR(length=20),
comment='变动类型:recharge / consume / expire / refund',
existing_nullable=False)
op.alter_column('mjk_point_transactions', 'amount',
existing_type=sa.INTEGER(),
comment='变动数量(正数)',
existing_nullable=False)
op.alter_column('mjk_point_transactions', 'balance_before',
existing_type=sa.INTEGER(),
comment='变动前总余额',
existing_nullable=False)
op.alter_column('mjk_point_transactions', 'balance_after',
existing_type=sa.INTEGER(),
comment='变动后总余额',
existing_nullable=False)
op.alter_column('mjk_point_transactions', 'source_type',
existing_type=sa.VARCHAR(length=32),
comment='消费来源类型:script / polish / voice_clone / tts / video',
existing_nullable=True)
op.alter_column('mjk_point_transactions', 'source_id',
existing_type=sa.VARCHAR(length=64),
comment='关联的任务 ID 或订单 ID',
existing_nullable=True)
op.alter_column('mjk_point_transactions', 'batch_id',
existing_type=sa.BIGINT(),
comment='关联的积分批次 ID(消费时记录从哪个批次扣)',
existing_nullable=True)
op.alter_column('mjk_point_transactions', 'duration',
existing_type=sa.DOUBLE_PRECISION(precision=53),
comment='时长(秒),按秒计费业务记录',
existing_nullable=True)
op.alter_column('mjk_point_transactions', 'category',
existing_type=sa.VARCHAR(length=32),
comment='业务分类:脚本生成 / 配音合成 / 视频生成 / 压制成片 / 字幕烧录 / 封面设计 / 充值',
existing_nullable=True)
op.alter_column('mjk_point_transactions', 'description',
existing_type=sa.TEXT(),
comment='描述',
existing_nullable=True)
op.alter_column('mjk_user_devices', 'user_id',
existing_type=sa.UUID(),
comment='用户 ID(唯一约束,强制单设备登录)',
existing_nullable=False)
op.alter_column('mjk_user_devices', 'device_id',
existing_type=sa.VARCHAR(length=64),
comment='设备唯一标识(前端生成)',
existing_nullable=False)
op.alter_column('mjk_user_devices', 'device_name',
existing_type=sa.VARCHAR(length=128),
comment="设备名称(如 'MacBook Pro'",
existing_nullable=True)
op.alter_column('mjk_user_devices', 'os_info',
existing_type=sa.VARCHAR(length=128),
comment='操作系统信息',
existing_nullable=True)
op.alter_column('mjk_user_devices', 'app_version',
existing_type=sa.VARCHAR(length=32),
comment='应用版本号',
existing_nullable=True)
op.alter_column('mjk_user_devices', 'refresh_token_hash',
existing_type=sa.VARCHAR(length=64),
comment='Refresh Token SHA256 哈希(用于校验和撤销)',
existing_nullable=True)
op.alter_column('mjk_user_devices', 'last_active_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
comment='最后活跃时间',
existing_nullable=False)
op.alter_column('mjk_user_points', 'user_id',
existing_type=sa.UUID(),
comment='用户 ID',
existing_nullable=False)
op.alter_column('mjk_user_points', 'balance',
existing_type=sa.INTEGER(),
comment='当前积分余额(允许欠费为负)',
existing_nullable=False)
op.alter_column('mjk_user_points', 'total_recharged',
existing_type=sa.INTEGER(),
comment='累计充值积分',
existing_nullable=False)
op.alter_column('mjk_user_points', 'total_consumed',
existing_type=sa.INTEGER(),
comment='累计消费积分',
existing_nullable=False)
op.alter_column('mjk_user_points', 'total_expired',
existing_type=sa.INTEGER(),
comment='累计过期积分',
existing_nullable=False)
op.alter_column('mjk_users', 'mobile',
existing_type=sa.VARCHAR(length=20),
comment='手机号,登录账号',
existing_nullable=False)
op.alter_column('mjk_users', 'password_hash',
existing_type=sa.VARCHAR(length=255),
comment='密码哈希(bcrypt),预留字段',
existing_nullable=True)
op.alter_column('mjk_users', 'status',
existing_type=sa.VARCHAR(length=20),
comment='账号状态',
existing_nullable=False)
op.alter_column('mjk_users', 'nickname',
existing_type=sa.VARCHAR(length=64),
comment='用户昵称',
existing_nullable=True)
op.alter_column('mjk_users', 'avatar_url',
existing_type=sa.TEXT(),
comment='头像 URL',
existing_nullable=True)
op.alter_column('mjk_users', 'source',
existing_type=sa.VARCHAR(length=32),
comment='注册来源',
existing_nullable=False)
op.alter_column('mjk_users', 'invited_by',
existing_type=sa.VARCHAR(length=36),
comment='邀请人 user_id',
existing_nullable=True)
op.alter_column('mjk_users', 'last_login_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
comment='最后登录时间',
existing_nullable=True)
op.alter_column('mjk_users', 'last_login_ip',
existing_type=sa.VARCHAR(length=45),
comment='最后登录 IP(IPv6 最大 45 字符)',
existing_nullable=True)
op.alter_column('mjk_users', 'deleted_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
comment='注销时间(软删除标记)',
existing_nullable=True)
op.alter_column('mjk_users', 'extra',
existing_type=postgresql.JSONB(astext_type=sa.Text()),
comment='冗余字段,备用',
existing_nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('mjk_users', 'extra',
existing_type=postgresql.JSONB(astext_type=sa.Text()),
comment=None,
existing_comment='冗余字段,备用',
existing_nullable=False)
op.alter_column('mjk_users', 'deleted_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
comment=None,
existing_comment='注销时间(软删除标记)',
existing_nullable=True)
op.alter_column('mjk_users', 'last_login_ip',
existing_type=sa.VARCHAR(length=45),
comment=None,
existing_comment='最后登录 IP(IPv6 最大 45 字符)',
existing_nullable=True)
op.alter_column('mjk_users', 'last_login_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
comment=None,
existing_comment='最后登录时间',
existing_nullable=True)
op.alter_column('mjk_users', 'invited_by',
existing_type=sa.VARCHAR(length=36),
comment=None,
existing_comment='邀请人 user_id',
existing_nullable=True)
op.alter_column('mjk_users', 'source',
existing_type=sa.VARCHAR(length=32),
comment=None,
existing_comment='注册来源',
existing_nullable=False)
op.alter_column('mjk_users', 'avatar_url',
existing_type=sa.TEXT(),
comment=None,
existing_comment='头像 URL',
existing_nullable=True)
op.alter_column('mjk_users', 'nickname',
existing_type=sa.VARCHAR(length=64),
comment=None,
existing_comment='用户昵称',
existing_nullable=True)
op.alter_column('mjk_users', 'status',
existing_type=sa.VARCHAR(length=20),
comment=None,
existing_comment='账号状态',
existing_nullable=False)
op.alter_column('mjk_users', 'password_hash',
existing_type=sa.VARCHAR(length=255),
comment=None,
existing_comment='密码哈希(bcrypt),预留字段',
existing_nullable=True)
op.alter_column('mjk_users', 'mobile',
existing_type=sa.VARCHAR(length=20),
comment=None,
existing_comment='手机号,登录账号',
existing_nullable=False)
op.alter_column('mjk_user_points', 'total_expired',
existing_type=sa.INTEGER(),
comment=None,
existing_comment='累计过期积分',
existing_nullable=False)
op.alter_column('mjk_user_points', 'total_consumed',
existing_type=sa.INTEGER(),
comment=None,
existing_comment='累计消费积分',
existing_nullable=False)
op.alter_column('mjk_user_points', 'total_recharged',
existing_type=sa.INTEGER(),
comment=None,
existing_comment='累计充值积分',
existing_nullable=False)
op.alter_column('mjk_user_points', 'balance',
existing_type=sa.INTEGER(),
comment=None,
existing_comment='当前积分余额(允许欠费为负)',
existing_nullable=False)
op.alter_column('mjk_user_points', 'user_id',
existing_type=sa.UUID(),
comment=None,
existing_comment='用户 ID',
existing_nullable=False)
op.alter_column('mjk_user_devices', 'last_active_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
comment=None,
existing_comment='最后活跃时间',
existing_nullable=False)
op.alter_column('mjk_user_devices', 'refresh_token_hash',
existing_type=sa.VARCHAR(length=64),
comment=None,
existing_comment='Refresh Token SHA256 哈希(用于校验和撤销)',
existing_nullable=True)
op.alter_column('mjk_user_devices', 'app_version',
existing_type=sa.VARCHAR(length=32),
comment=None,
existing_comment='应用版本号',
existing_nullable=True)
op.alter_column('mjk_user_devices', 'os_info',
existing_type=sa.VARCHAR(length=128),
comment=None,
existing_comment='操作系统信息',
existing_nullable=True)
op.alter_column('mjk_user_devices', 'device_name',
existing_type=sa.VARCHAR(length=128),
comment=None,
existing_comment="设备名称(如 'MacBook Pro'",
existing_nullable=True)
op.alter_column('mjk_user_devices', 'device_id',
existing_type=sa.VARCHAR(length=64),
comment=None,
existing_comment='设备唯一标识(前端生成)',
existing_nullable=False)
op.alter_column('mjk_user_devices', 'user_id',
existing_type=sa.UUID(),
comment=None,
existing_comment='用户 ID(唯一约束,强制单设备登录)',
existing_nullable=False)
op.alter_column('mjk_point_transactions', 'description',
existing_type=sa.TEXT(),
comment=None,
existing_comment='描述',
existing_nullable=True)
op.alter_column('mjk_point_transactions', 'category',
existing_type=sa.VARCHAR(length=32),
comment=None,
existing_comment='业务分类:脚本生成 / 配音合成 / 视频生成 / 压制成片 / 字幕烧录 / 封面设计 / 充值',
existing_nullable=True)
op.alter_column('mjk_point_transactions', 'duration',
existing_type=sa.DOUBLE_PRECISION(precision=53),
comment=None,
existing_comment='时长(秒),按秒计费业务记录',
existing_nullable=True)
op.alter_column('mjk_point_transactions', 'batch_id',
existing_type=sa.BIGINT(),
comment=None,
existing_comment='关联的积分批次 ID(消费时记录从哪个批次扣)',
existing_nullable=True)
op.alter_column('mjk_point_transactions', 'source_id',
existing_type=sa.VARCHAR(length=64),
comment=None,
existing_comment='关联的任务 ID 或订单 ID',
existing_nullable=True)
op.alter_column('mjk_point_transactions', 'source_type',
existing_type=sa.VARCHAR(length=32),
comment=None,
existing_comment='消费来源类型:script / polish / voice_clone / tts / video',
existing_nullable=True)
op.alter_column('mjk_point_transactions', 'balance_after',
existing_type=sa.INTEGER(),
comment=None,
existing_comment='变动后总余额',
existing_nullable=False)
op.alter_column('mjk_point_transactions', 'balance_before',
existing_type=sa.INTEGER(),
comment=None,
existing_comment='变动前总余额',
existing_nullable=False)
op.alter_column('mjk_point_transactions', 'amount',
existing_type=sa.INTEGER(),
comment=None,
existing_comment='变动数量(正数)',
existing_nullable=False)
op.alter_column('mjk_point_transactions', 'type',
existing_type=sa.VARCHAR(length=20),
comment=None,
existing_comment='变动类型:recharge / consume / expire / refund',
existing_nullable=False)
op.alter_column('mjk_point_transactions', 'user_id',
existing_type=sa.UUID(),
comment=None,
existing_comment='用户 ID',
existing_nullable=False)
op.alter_column('mjk_point_recharge_orders', 'error_msg',
existing_type=sa.TEXT(),
comment=None,
existing_comment='错误描述(用于快速定位问题)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'error_code',
existing_type=sa.VARCHAR(length=32),
comment=None,
existing_comment='错误码(微信返回或系统异常)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'query_result',
existing_type=sa.TEXT(),
comment=None,
existing_comment='主动查询订单结果(JSON 格式,用于二次确认)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'notify_verified',
existing_type=sa.BOOLEAN(),
comment=None,
existing_comment='回调签名是否验证通过',
existing_nullable=False)
op.alter_column('mjk_point_recharge_orders', 'notify_raw',
existing_type=sa.TEXT(),
comment=None,
existing_comment='微信回调原始内容(XML/JSON,用于排查回调问题)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'request_response',
existing_type=sa.TEXT(),
comment=None,
existing_comment='统一下单响应内容(JSON 格式,用于排查微信返回)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'request_params',
existing_type=sa.TEXT(),
comment=None,
existing_comment='统一下单请求参数(JSON 格式,用于排查请求侧问题)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'closed_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
comment=None,
existing_comment='订单关闭时间(超时未支付)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'paid_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
comment=None,
existing_comment='支付成功时间',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'status',
existing_type=sa.VARCHAR(length=20),
comment=None,
existing_comment='订单状态:pending / paid / failed / closed',
existing_nullable=False)
op.alter_column('mjk_point_recharge_orders', 'trade_type',
existing_type=sa.VARCHAR(length=16),
comment=None,
existing_comment='交易类型:JSAPI / NATIVE / APP',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'client_ip',
existing_type=sa.VARCHAR(length=45),
comment=None,
existing_comment='用户下单时的 IP 地址',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'openid',
existing_type=sa.VARCHAR(length=64),
comment=None,
existing_comment='用户微信 OpenID(统一下单必需)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'wx_order_no',
existing_type=sa.VARCHAR(length=64),
comment=None,
existing_comment='微信支付订单号(微信侧唯一标识)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'prepay_id',
existing_type=sa.VARCHAR(length=64),
comment=None,
existing_comment='微信预支付会话标识(统一下单返回)',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'out_trade_no',
existing_type=sa.VARCHAR(length=64),
comment=None,
existing_comment='商户订单号(传给微信的 out_trade_no',
existing_nullable=True)
op.alter_column('mjk_point_recharge_orders', 'amount_rmb',
existing_type=sa.INTEGER(),
comment=None,
existing_comment='人民币金额(单位:分,如 500 = 5 元)',
existing_nullable=False)
op.alter_column('mjk_point_recharge_orders', 'points',
existing_type=sa.INTEGER(),
comment=None,
existing_comment='充值积分数',
existing_nullable=False)
op.alter_column('mjk_point_recharge_orders', 'user_id',
existing_type=sa.UUID(),
comment=None,
existing_comment='用户 ID',
existing_nullable=False)
op.alter_column('mjk_point_batches', 'source',
existing_type=sa.VARCHAR(length=32),
comment=None,
existing_comment='来源:wxpay / invite / gift / compensation',
existing_nullable=False)
op.alter_column('mjk_point_batches', 'expired_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
comment=None,
existing_comment='过期时间(created_at + 180 天)',
existing_nullable=False)
op.alter_column('mjk_point_batches', 'remaining',
existing_type=sa.INTEGER(),
comment=None,
existing_comment='剩余可用积分',
existing_nullable=False)
op.alter_column('mjk_point_batches', 'amount',
existing_type=sa.INTEGER(),
comment=None,
existing_comment='初始积分',
existing_nullable=False)
op.alter_column('mjk_point_batches', 'user_id',
existing_type=sa.UUID(),
comment=None,
existing_comment='用户 ID',
existing_nullable=False)
op.drop_table('mjk_materials')
op.drop_table('mjk_tags')
op.drop_table('mjk_categories')
# ### end Alembic commands ###
@@ -1,32 +0,0 @@
"""rename_mjk_to_mjk_broll
Revision ID: 7a412121e69a
Revises: d0a7c5a375c6
Create Date: 2026-05-15 15:48:40.219522
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '7a412121e69a'
down_revision: Union[str, Sequence[str], None] = 'd0a7c5a375c6'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""空镜素材表增加 broll 模块前缀,统一命名规范。"""
op.rename_table('mjk_categories', 'mjk_broll_categories')
op.rename_table('mjk_materials', 'mjk_broll_materials')
op.rename_table('mjk_tags', 'mjk_broll_tags')
def downgrade() -> None:
"""恢复旧表名。"""
op.rename_table('mjk_broll_tags', 'mjk_tags')
op.rename_table('mjk_broll_materials', 'mjk_materials')
op.rename_table('mjk_broll_categories', 'mjk_categories')
@@ -1,27 +0,0 @@
"""
积分流水表添加 category 字段
用于业务分类展示和筛选。
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "8aa48b89a07d"
down_revision: Union[str, None] = "95eb1a1c0af9"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column(
"mjk_point_transactions",
sa.Column("category", sa.String(32), nullable=True),
)
def downgrade() -> None:
op.drop_column("mjk_point_transactions", "category")
@@ -1,27 +0,0 @@
"""
积分流水表添加 duration 字段
用于记录按秒计费业务的时长(TTS、数字人视频等)。
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "95eb1a1c0af9"
down_revision: Union[str, None] = "ccf61ff6f4bb"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column(
"mjk_point_transactions",
sa.Column("duration", sa.Float(), nullable=True),
)
def downgrade() -> None:
op.drop_column("mjk_point_transactions", "duration")
@@ -0,0 +1,215 @@
"""initial_schema
Revision ID: c3a0e1c71ce6
Revises:
Create Date: 2026-05-15 17:31:52.560351
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = 'c3a0e1c71ce6'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('app_releases',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('version', sa.String(length=20), nullable=False),
sa.Column('release_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('notes', sa.Text(), nullable=False),
sa.Column('mandatory', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_app_releases_version'), 'app_releases', ['version'], unique=True)
op.create_table('mjk_broll_categories',
sa.Column('slug', sa.String(length=128), nullable=False, comment='分类标识符,URL友好格式'),
sa.Column('name', sa.String(length=256), nullable=False, comment='分类中文名称,三级分类直接对应 scene 标准化后的值'),
sa.Column('parent_id', sa.BigInteger(), nullable=True, comment='父分类ID,NULL 表示根分类(一级)'),
sa.Column('level', sa.BigInteger(), nullable=False, comment='层级:1=一级(大阶段),2=二级(工序),3=三级(场景)'),
sa.Column('sort_order', sa.BigInteger(), nullable=False, comment='排序权重,装修流程有先后顺序'),
sa.Column('status', sa.String(length=16), nullable=False, comment='状态:active(启用)/ disabled(停用)/ deleted(软删除)'),
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['parent_id'], ['mjk_broll_categories.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('slug')
)
op.create_table('mjk_broll_tags',
sa.Column('name', sa.String(length=64), nullable=False, comment='标签名称,如 近景、白天、水管'),
sa.Column('category', sa.String(length=32), nullable=True, comment='标签维度:scene(场景)/ element(元素)/ style(风格)/ mood(情绪)/ time(时间)'),
sa.Column('sort_order', sa.Integer(), nullable=False, comment='排序权重'),
sa.Column('status', sa.String(length=16), nullable=False, comment='状态:active(启用)/ disabled(停用)'),
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('mjk_cover_backgrounds',
sa.Column('script_code', sa.String(length=64), nullable=False, comment='关联脚本大类 code,如 bk(装修避坑)'),
sa.Column('title', sa.String(length=256), nullable=False, comment='背景图名称,运营识别用'),
sa.Column('url', sa.String(length=1024), nullable=False, comment='七牛云 CDN 图片地址'),
sa.Column('sort_order', sa.BigInteger(), nullable=False, comment='排序权重,数字越小越靠前'),
sa.Column('status', sa.String(length=16), nullable=False, comment='状态:active(启用)/ disabled(停用)/ deleted(软删除)'),
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('mjk_point_batches',
sa.Column('user_id', sa.UUID(), nullable=False, comment='用户 ID'),
sa.Column('amount', sa.Integer(), nullable=False, comment='初始积分'),
sa.Column('remaining', sa.Integer(), nullable=False, comment='剩余可用积分'),
sa.Column('expired_at', sa.DateTime(timezone=True), nullable=False, comment='过期时间(created_at + 180 天)'),
sa.Column('source', sa.String(length=32), nullable=False, comment='来源:wxpay / invite / gift / compensation'),
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('mjk_point_recharge_orders',
sa.Column('user_id', sa.UUID(), nullable=False, comment='用户 ID'),
sa.Column('points', sa.Integer(), nullable=False, comment='充值积分数'),
sa.Column('amount_rmb', sa.Integer(), nullable=False, comment='人民币金额(单位:分,如 500 = 5 元)'),
sa.Column('out_trade_no', sa.String(length=64), nullable=True, comment='商户订单号(传给微信的 out_trade_no'),
sa.Column('prepay_id', sa.String(length=64), nullable=True, comment='微信预支付会话标识(统一下单返回)'),
sa.Column('wx_order_no', sa.String(length=64), nullable=True, comment='微信支付订单号(微信侧唯一标识)'),
sa.Column('openid', sa.String(length=64), nullable=True, comment='用户微信 OpenID(统一下单必需)'),
sa.Column('client_ip', sa.String(length=45), nullable=True, comment='用户下单时的 IP 地址'),
sa.Column('trade_type', sa.String(length=16), nullable=True, comment='交易类型:JSAPI / NATIVE / APP'),
sa.Column('status', sa.String(length=20), nullable=False, comment='订单状态:pending / paid / failed / closed'),
sa.Column('paid_at', sa.DateTime(timezone=True), nullable=True, comment='支付成功时间'),
sa.Column('closed_at', sa.DateTime(timezone=True), nullable=True, comment='订单关闭时间(超时未支付)'),
sa.Column('request_params', sa.Text(), nullable=True, comment='统一下单请求参数(JSON 格式,用于排查请求侧问题)'),
sa.Column('request_response', sa.Text(), nullable=True, comment='统一下单响应内容(JSON 格式,用于排查微信返回)'),
sa.Column('notify_raw', sa.Text(), nullable=True, comment='微信回调原始内容(XML/JSON,用于排查回调问题)'),
sa.Column('notify_verified', sa.Boolean(), nullable=False, comment='回调签名是否验证通过'),
sa.Column('query_result', sa.Text(), nullable=True, comment='主动查询订单结果(JSON 格式,用于二次确认)'),
sa.Column('error_code', sa.String(length=32), nullable=True, comment='错误码(微信返回或系统异常)'),
sa.Column('error_msg', sa.Text(), nullable=True, comment='错误描述(用于快速定位问题)'),
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('out_trade_no')
)
op.create_table('mjk_point_transactions',
sa.Column('user_id', sa.UUID(), nullable=False, comment='用户 ID'),
sa.Column('type', sa.String(length=20), nullable=False, comment='变动类型:recharge / consume / expire / refund'),
sa.Column('amount', sa.Integer(), nullable=False, comment='变动数量(正数)'),
sa.Column('balance_before', sa.Integer(), nullable=False, comment='变动前总余额'),
sa.Column('balance_after', sa.Integer(), nullable=False, comment='变动后总余额'),
sa.Column('source_type', sa.String(length=32), nullable=True, comment='消费来源类型:script / polish / voice_clone / tts / video'),
sa.Column('source_id', sa.String(length=64), nullable=True, comment='关联的任务 ID 或订单 ID'),
sa.Column('batch_id', sa.BigInteger(), nullable=True, comment='关联的积分批次 ID(消费时记录从哪个批次扣)'),
sa.Column('duration', sa.Float(), nullable=True, comment='时长(秒),按秒计费业务记录'),
sa.Column('category', sa.String(length=32), nullable=True, comment='业务分类:脚本生成 / 配音合成 / 视频生成 / 压制成片 / 字幕烧录 / 封面设计 / 充值'),
sa.Column('description', sa.Text(), nullable=True, comment='描述'),
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('mjk_user_devices',
sa.Column('user_id', sa.UUID(), nullable=False, comment='用户 ID(唯一约束,强制单设备登录)'),
sa.Column('device_id', sa.String(length=64), nullable=False, comment='设备唯一标识(前端生成)'),
sa.Column('device_name', sa.String(length=128), nullable=True, comment="设备名称(如 'MacBook Pro'"),
sa.Column('os_info', sa.String(length=128), nullable=True, comment='操作系统信息'),
sa.Column('app_version', sa.String(length=32), nullable=True, comment='应用版本号'),
sa.Column('refresh_token_hash', sa.String(length=64), nullable=True, comment='Refresh Token SHA256 哈希(用于校验和撤销)'),
sa.Column('last_active_at', sa.DateTime(timezone=True), nullable=False, comment='最后活跃时间'),
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id')
)
op.create_table('mjk_user_points',
sa.Column('user_id', sa.UUID(), nullable=False, comment='用户 ID'),
sa.Column('balance', sa.Integer(), nullable=False, comment='当前积分余额(允许欠费为负)'),
sa.Column('total_recharged', sa.Integer(), nullable=False, comment='累计充值积分'),
sa.Column('total_consumed', sa.Integer(), nullable=False, comment='累计消费积分'),
sa.Column('total_expired', sa.Integer(), nullable=False, comment='累计过期积分'),
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id')
)
op.create_table('mjk_users',
sa.Column('mobile', sa.String(length=20), nullable=False, comment='手机号,登录账号'),
sa.Column('password_hash', sa.String(length=255), nullable=True, comment='密码哈希(bcrypt),预留字段'),
sa.Column('status', sa.String(length=20), nullable=False, comment='账号状态'),
sa.Column('nickname', sa.String(length=64), nullable=True, comment='用户昵称'),
sa.Column('avatar_url', sa.Text(), nullable=True, comment='头像 URL'),
sa.Column('source', sa.String(length=32), nullable=False, comment='注册来源'),
sa.Column('invited_by', sa.String(length=36), nullable=True, comment='邀请人 user_id'),
sa.Column('last_login_at', sa.DateTime(timezone=True), nullable=True, comment='最后登录时间'),
sa.Column('last_login_ip', sa.String(length=45), nullable=True, comment='最后登录 IP(IPv6 最大 45 字符)'),
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True, comment='注销时间(软删除标记)'),
sa.Column('extra', postgresql.JSONB(astext_type=sa.Text()), nullable=False, comment='冗余字段,备用'),
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('mobile')
)
op.create_table('mjk_broll_materials',
sa.Column('category_id', sa.BigInteger(), nullable=False, comment='所属三级分类ID,关联 mjk_broll_categories'),
sa.Column('title', sa.String(length=256), nullable=False, comment='素材标题/文件名,运营后台识别用'),
sa.Column('url', sa.String(length=1024), nullable=False, comment='七牛云 CDN 访问地址,FFmpeg合成和前端播放直接使用'),
sa.Column('duration', sa.Float(), nullable=False, comment='视频时长(秒),FFmpeg probe 提取,入库时必须大于0'),
sa.Column('usage_count', sa.BigInteger(), nullable=False, comment='累计使用次数,驱动加权随机算法'),
sa.Column('status', sa.String(length=16), nullable=False, comment='状态:active(可用)/ disabled(下架)/ deleted(软删除)'),
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['category_id'], ['mjk_broll_categories.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('release_packages',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('release_id', sa.Integer(), nullable=False),
sa.Column('platform', sa.String(length=20), nullable=False),
sa.Column('architecture', sa.String(length=20), nullable=False),
sa.Column('filename', sa.String(length=255), nullable=False),
sa.Column('file_url', sa.String(length=500), nullable=False),
sa.Column('file_size', sa.BigInteger(), nullable=False),
sa.Column('signature', sa.Text(), nullable=False),
sa.Column('download_count', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['release_id'], ['app_releases.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('release_id', 'platform', 'architecture', name='uix_pkg_platform_arch')
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('release_packages')
op.drop_table('mjk_broll_materials')
op.drop_table('mjk_users')
op.drop_table('mjk_user_points')
op.drop_table('mjk_user_devices')
op.drop_table('mjk_point_transactions')
op.drop_table('mjk_point_recharge_orders')
op.drop_table('mjk_point_batches')
op.drop_table('mjk_cover_backgrounds')
op.drop_table('mjk_broll_tags')
op.drop_table('mjk_broll_categories')
op.drop_index(op.f('ix_app_releases_version'), table_name='app_releases')
op.drop_table('app_releases')
# ### end Alembic commands ###
@@ -1,45 +0,0 @@
"""
删除积分表废弃字段
- mjk_user_points.frozen(冻结逻辑已删除)
- mjk_user_points.total_refunded(退款逻辑已删除)
- mjk_point_batches.frozen(冻结逻辑已删除)
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "ccf61ff6f4bb"
down_revision: Union[str, None] = "509aa8b53d81"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 删除 mjk_user_points 的废弃字段
op.drop_column("mjk_user_points", "frozen")
op.drop_column("mjk_user_points", "total_refunded")
# 删除 mjk_point_batches 的废弃字段
op.drop_column("mjk_point_batches", "frozen")
def downgrade() -> None:
# 恢复 mjk_user_points 字段
op.add_column(
"mjk_user_points",
sa.Column("frozen", sa.Integer(), nullable=False, server_default="0"),
)
op.add_column(
"mjk_user_points",
sa.Column("total_refunded", sa.Integer(), nullable=False, server_default="0"),
)
# 恢复 mjk_point_batches 字段
op.add_column(
"mjk_point_batches",
sa.Column("frozen", sa.Integer(), nullable=False, server_default="0"),
)
@@ -1,62 +0,0 @@
"""
add app update tables
Revision ID: d0a7c5a375c6
Revises: e02c96e264d9
Create Date: 2026-05-11 09:30:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "d0a7c5a375c6"
down_revision: Union[str, None] = "e02c96e264d9"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"app_releases",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("version", sa.String(length=20), nullable=False),
sa.Column("release_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("notes", sa.Text(), nullable=False),
sa.Column("mandatory", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("version"),
)
op.create_index("ix_app_releases_version", "app_releases", ["version"], unique=False)
op.create_table(
"release_packages",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("release_id", sa.Integer(), nullable=False),
sa.Column("platform", sa.String(length=20), nullable=False),
sa.Column("architecture", sa.String(length=20), nullable=False),
sa.Column("filename", sa.String(length=255), nullable=False),
sa.Column("file_url", sa.String(length=500), nullable=False),
sa.Column("file_size", sa.BigInteger(), nullable=False),
sa.Column("signature", sa.Text(), nullable=False),
sa.Column("download_count", sa.Integer(), nullable=False, server_default=sa.text("0")),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["release_id"], ["app_releases.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("release_id", "platform", "architecture", name="uix_pkg_platform_arch"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("release_packages")
op.drop_index("ix_app_releases_version", table_name="app_releases")
op.drop_table("app_releases")
# ### end Alembic commands ###
@@ -1,40 +0,0 @@
"""add cover_backgrounds table
Revision ID: e02c96e264d9
Revises: 69274ce979a5
Create Date: 2026-05-11 20:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'e02c96e264d9'
down_revision: Union[str, Sequence[str], None] = '69274ce979a5'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
op.create_table(
'mjk_cover_backgrounds',
sa.Column('script_code', sa.String(length=64), nullable=False, comment='脚本大类 code(如 bk'),
sa.Column('title', sa.String(length=256), nullable=True, comment='背景图标题'),
sa.Column('url', sa.String(length=1024), nullable=False, comment='七牛云 CDN 地址'),
sa.Column('sort_order', sa.BigInteger(), nullable=False, comment='排序权重,越小越靠前', server_default='0'),
sa.Column('status', sa.String(length=16), nullable=False, comment='状态:active / disabled', server_default='active'),
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id'),
)
op.create_index('ix_cover_backgrounds_script_code', 'mjk_cover_backgrounds', ['script_code'], unique=False)
def downgrade() -> None:
"""Downgrade schema."""
op.drop_index('ix_cover_backgrounds_script_code', table_name='mjk_cover_backgrounds')
op.drop_table('mjk_cover_backgrounds')