cbd4068776
- Rename app_releases → mjk_app_releases - Rename release_packages → mjk_release_packages - Update ForeignKey reference and migration file - Add pre-commit hook: check_table_prefix.py to prevent future violations
216 lines
15 KiB
Python
216 lines
15 KiB
Python
"""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('mjk_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_mjk_app_releases_version'), 'mjk_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('mjk_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'], ['mjk_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('mjk_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_mjk_app_releases_version'), table_name='mjk_app_releases')
|
||
op.drop_table('mjk_app_releases')
|
||
# ### end Alembic commands ###
|