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:
@@ -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')
|
||||
Reference in New Issue
Block a user