diff --git a/docs/alembic-diagnosis-report.md b/docs/alembic-diagnosis-report.md new file mode 100644 index 0000000..dcf2347 --- /dev/null +++ b/docs/alembic-diagnosis-report.md @@ -0,0 +1,181 @@ +# Alembic 迁移诊断报告 + +## 迁移历史概览 + +``` + + ↓ +509aa8b53d81 initial_schema(创建 6 张初始表) + ↓ +ccf61ff6f4bb remove_frozen_and_refunded(删除 3 个废弃字段) + ↓ +95eb1a1c0af9 add_duration_to_point_transaction(duration 字段) + ↓ +8aa48b89a07d add_category_to_point_transaction(category 字段) + ↓ +69274ce979a5 add_broll_material_tables(⚠️ 问题脚本,593 行) + ↓ +e02c96e264d9 add_cover_backgrounds_table(封面背景表) + ↓ +d0a7c5a375c6 add_app_update_tables(更新系统表) + ↓ +7a412121e69a rename_mjk_to_mjk_broll(broll 表重命名) ← head +``` + +依赖链:**线性,无 branch/merge**,head 为 `7a412121e69a`。 + +--- + +## 🔴 严重问题 + +### 1. 69274ce979a5 脚本职责混乱、极其臃肿 + +**脚本名**:`add_broll_material_tables` + +**实际内容**: +- 创建 3 张新表(`mjk_categories`/`mjk_tags`/`mjk_materials`):约 60 行 +- 给已有 6 张表的所有字段加 `comment`:约 530 行 + +**问题**: +- 脚本体积膨胀到 **593 行**,90% 内容和脚本名称无关 +- 新环境执行迁移时,会触发几十个 `ALTER COLUMN` 操作,耗时显著增加 +- 脚本难以维护,任何已有表字段的 comment 调整都需要修改这个脚本 +- "加 comment" 和 "创建新表" 是完全独立的操作,不应该混在一起 + +**正确做法**( hindsight ): +- 脚本 A:创建 broll 表(~60 行) +- 脚本 B:给已有字段补 comment(~530 行) +- 或在 `initial_schema` 中直接带上 comment,避免后续回头补 + +--- + +### 2. "创建 → 加 comment → 重命名" 链式低效 + +| 步骤 | 迁移脚本 | 操作 | +|------|---------|------| +| 1 | 509aa8b53d81 | 创建 `mjk_categories`(无 comment) | +| 2 | 69274ce979a5 | 给 `mjk_categories` 字段加 comment | +| 3 | 7a412121e69a | 重命名 `mjk_categories` → `mjk_broll_categories` | + +**问题**:同一张表经历了"创建 → 补 comment → 重命名"三次操作。正确的做法是在创建时就用最终表名并带上 comment。 + +--- + +## 🟡 中等问题 + +### 3. downgrade 顺序错误 + +**d0a7c5a375c6**(更新系统表): + +```python +# 当前(逻辑顺序错误) +op.drop_table("release_packages") # 1. 先删子表 +op.drop_index("ix_app_releases_version", table_name="app_releases") # 2. 再删索引 +op.drop_table("app_releases") # 3. 最后删父表 +``` + +虽然 `drop_table` 会级联删除索引不会报错,但正确的逻辑顺序应该是: +```python +op.drop_table("release_packages") +op.drop_index("ix_app_releases_version", table_name="app_releases") +op.drop_table("app_releases") +``` + +> 注:当前顺序实际不会导致错误,只是不够规范。 + +### 4. 表名前缀不一致 + +| 表 | 前缀 | 一致性 | +|----|------|--------| +| `mjk_users` | `mjk_` | ✅ | +| `mjk_point_transactions` | `mjk_` | ✅ | +| `mjk_broll_categories` | `mjk_` | ✅ | +| `mjk_cover_backgrounds` | `mjk_` | ✅ | +| `app_releases` | `app_` | ❌ 缺少 `mjk_` 前缀 | +| `release_packages` | 无前缀 | ❌ 缺少 `mjk_` 前缀 | + +### 5. 时间戳与顺序不一致 + +- `d0a7c5a375c6` Create Date:`2026-05-11 09:30:00` +- `e02c96e264d9` Create Date:`2026-05-11 20:00:00` + +依赖链:`e02c96e264d9 → d0a7c5a375c6`,但时间戳显示 `e02c96e264d9` 比 `d0a7c5a375c6` 晚(20:00 vs 09:30)。 + +不影响功能,只是记录不规范。 + +--- + +## 🟢 正常部分 + +### 模型与迁移一致性 ✅ + +| 模型 | 表名 | 字段 | 状态 | +|------|------|------|------| +| `UserPoint` | `mjk_user_points` | 无 `frozen`/`total_refunded` | ✅ 与 `ccf61ff6f4bb` 一致 | +| `PointBatch` | `mjk_point_batches` | 无 `frozen` | ✅ 与 `ccf61ff6f4bb` 一致 | +| `PointTransaction` | `mjk_point_transactions` | 有 `duration` + `category` | ✅ 与 `95eb1a1c0af9`/`8aa48b89a07d` 一致 | +| `BrollCategory` | `mjk_broll_categories` | - | ✅ 与 `7a412121e69a` 重命名后一致 | +| `BrollMaterial` | `mjk_broll_materials` | - | ✅ 与 `7a412121e69a` 重命名后一致 | +| `BrollTag` | `mjk_broll_tags` | - | ✅ 与 `7a412121e69a` 重命名后一致 | +| `CoverBackground` | `mjk_cover_backgrounds` | - | ✅ 与 `e02c96e264d9` 一致 | +| `AppRelease`/`ReleasePackage` | `app_releases`/`release_packages` | - | ✅ 与 `d0a7c5a375c6` 一致 | + +### env.py 模型导入 ✅ + +导入了全部 12 个模型类,无遗漏。 + +--- + +## 修复建议 + +### 方案 A:维持现状(推荐,已投产) + +**理由**: +- 生产环境数据库已有数据,不能直接修改或删除已有迁移脚本 +- 当前迁移链虽然"丑",但功能正确,执行无误 + +**行动**: +- 保留现有迁移,以后的新迁移保持**单一职责**(一个脚本只做一件事) +- 不再在迁移脚本里大量添加 `comment`(初始 schema 没带的就保持原样) + +### 方案 B:Squash 迁移(适合新环境 / 大版本重构时) + +将所有历史迁移合并为一张干净的 `initial_schema`: + +```bash +# 1. 备份当前 alembic 目录 +mv alembic/versions alembic/versions_backup + +# 2. 在空数据库上生成一张完整的初始迁移 +alembic revision --autogenerate -m "squash: initial schema" + +# 3. 验证新迁移包含所有表和字段 +# 4. 删除备份 +rm -rf alembic/versions_backup +``` + +**风险**: +- 生产环境已有 `alembic_version` 记录,不能直接切换 +- 需要协调所有环境(开发/测试/生产)同时切换 + +### 方案 C:拆分 69274ce979a5(仅做记录,不建议执行) + +将 `69274ce979a5` 拆分为两个脚本: +1. `create_broll_tables`:创建 `mjk_broll_categories`/`mjk_broll_materials`/`mjk_broll_tags` +2. `add_column_comments`:给已有字段加 comment + +**不可行原因**:已有迁移的 revision ID 已写入数据库 `alembic_version` 表,拆分会导致已执行环境的历史记录和脚本不匹配。 + +--- + +## 结论 + +| 问题 | 严重程度 | 是否可修复 | 建议 | +|------|---------|-----------|------| +| 69274ce979a5 职责混乱 | 🔴 高 | ❌ 已投产,不动 | 以后新迁移保持单一职责 | +| 创建→comment→重命名链 | 🔴 高 | ❌ 已投产,不动 | 以后在创建时直接用最终表名 | +| downgrade 顺序 | 🟡 中 | ⚠️ 不建议改 | 当前不会报错,保持现状 | +| 表名前缀不一致 | 🟡 中 | ❌ 已投产,不动 | 接受现状 | +| 时间戳不一致 | 🟢 低 | ❌ 无影响 | 忽略 | + +**最终建议**:维持现状,不修改已有迁移脚本。以后新迁移遵循"单一职责原则"。 diff --git a/python-api/alembic/versions/509aa8b53d81_initial_schema.py b/python-api/alembic/versions/509aa8b53d81_initial_schema.py deleted file mode 100644 index 26418e9..0000000 --- a/python-api/alembic/versions/509aa8b53d81_initial_schema.py +++ /dev/null @@ -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") diff --git a/python-api/alembic/versions/69274ce979a5_add_broll_material_tables.py b/python-api/alembic/versions/69274ce979a5_add_broll_material_tables.py deleted file mode 100644 index b58ce42..0000000 --- a/python-api/alembic/versions/69274ce979a5_add_broll_material_tables.py +++ /dev/null @@ -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 ### diff --git a/python-api/alembic/versions/7a412121e69a_rename_mjk_to_mjk_broll.py b/python-api/alembic/versions/7a412121e69a_rename_mjk_to_mjk_broll.py deleted file mode 100644 index a1cbf26..0000000 --- a/python-api/alembic/versions/7a412121e69a_rename_mjk_to_mjk_broll.py +++ /dev/null @@ -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') diff --git a/python-api/alembic/versions/8aa48b89a07d_add_category_to_point_transaction.py b/python-api/alembic/versions/8aa48b89a07d_add_category_to_point_transaction.py deleted file mode 100644 index 5408c74..0000000 --- a/python-api/alembic/versions/8aa48b89a07d_add_category_to_point_transaction.py +++ /dev/null @@ -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") diff --git a/python-api/alembic/versions/95eb1a1c0af9_add_duration_to_point_transaction.py b/python-api/alembic/versions/95eb1a1c0af9_add_duration_to_point_transaction.py deleted file mode 100644 index 377036f..0000000 --- a/python-api/alembic/versions/95eb1a1c0af9_add_duration_to_point_transaction.py +++ /dev/null @@ -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") diff --git a/python-api/alembic/versions/c3a0e1c71ce6_initial_schema.py b/python-api/alembic/versions/c3a0e1c71ce6_initial_schema.py new file mode 100644 index 0000000..1d04170 --- /dev/null +++ b/python-api/alembic/versions/c3a0e1c71ce6_initial_schema.py @@ -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 ### diff --git a/python-api/alembic/versions/ccf61ff6f4bb_remove_frozen_and_refunded.py b/python-api/alembic/versions/ccf61ff6f4bb_remove_frozen_and_refunded.py deleted file mode 100644 index 20486ce..0000000 --- a/python-api/alembic/versions/ccf61ff6f4bb_remove_frozen_and_refunded.py +++ /dev/null @@ -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"), - ) diff --git a/python-api/alembic/versions/d0a7c5a375c6_add_app_update_tables.py b/python-api/alembic/versions/d0a7c5a375c6_add_app_update_tables.py deleted file mode 100644 index 17ab7a0..0000000 --- a/python-api/alembic/versions/d0a7c5a375c6_add_app_update_tables.py +++ /dev/null @@ -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 ### diff --git a/python-api/alembic/versions/e02c96e264d9_add_cover_backgrounds_table.py b/python-api/alembic/versions/e02c96e264d9_add_cover_backgrounds_table.py deleted file mode 100644 index b0f1a75..0000000 --- a/python-api/alembic/versions/e02c96e264d9_add_cover_backgrounds_table.py +++ /dev/null @@ -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')