video-compliance-ai/backend/alembic/versions/003_user_org_project_task.py
Your Name 4c9b2f1263 feat: Brief附件/项目平台/规则AI解析/消息中心修复 + 项目创建通知
- Brief 支持代理商附件上传 (迁移 007)
- 项目新增 platform 字段 (迁移 008),前端创建/展示平台信息
- 修复 AI 规则解析:处理中文引号导致 JSON 解析失败的问题
- 修复消息中心崩溃:补全后端消息类型映射 + fallback 保护
- 项目创建时自动发送消息通知
- .gitignore 排除 backend/data/ 数据库文件

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 19:00:03 +08:00

247 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""添加用户、组织、项目、任务表
Revision ID: 003
Revises: 002
Create Date: 2026-02-09
"""
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 = '003'
down_revision: Union[str, None] = '002'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 创建枚举类型
user_role_enum = postgresql.ENUM(
'brand', 'agency', 'creator',
name='user_role_enum',
create_type=False,
)
user_role_enum.create(op.get_bind(), checkfirst=True)
task_stage_enum = postgresql.ENUM(
'script_upload', 'script_ai_review', 'script_agency_review', 'script_brand_review',
'video_upload', 'video_ai_review', 'video_agency_review', 'video_brand_review',
'completed', 'rejected',
name='task_stage_enum',
create_type=False,
)
task_stage_enum.create(op.get_bind(), checkfirst=True)
# 扩展 task_status_enum添加 Task 模型需要的值
op.execute("ALTER TYPE task_status_enum ADD VALUE IF NOT EXISTS 'passed'")
op.execute("ALTER TYPE task_status_enum ADD VALUE IF NOT EXISTS 'force_passed'")
# 用户表
op.create_table(
'users',
sa.Column('id', sa.String(64), primary_key=True),
sa.Column('email', sa.String(255), unique=True, nullable=True, index=True),
sa.Column('phone', sa.String(20), unique=True, nullable=True, index=True),
sa.Column('password_hash', sa.String(255), nullable=False),
sa.Column('name', sa.String(100), nullable=False),
sa.Column('avatar', sa.String(2048), nullable=True),
sa.Column('role', postgresql.ENUM('brand', 'agency', 'creator', name='user_role_enum', create_type=False), nullable=False, index=True),
sa.Column('is_active', sa.Boolean(), default=True, nullable=False),
sa.Column('is_verified', sa.Boolean(), default=False, nullable=False),
sa.Column('last_login_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('refresh_token', sa.String(512), nullable=True),
sa.Column('refresh_token_expires_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False),
)
# 品牌方表
op.create_table(
'brands',
sa.Column('id', sa.String(64), primary_key=True),
sa.Column('user_id', sa.String(64), sa.ForeignKey('users.id', ondelete='CASCADE'), unique=True, nullable=False),
sa.Column('name', sa.String(255), nullable=False),
sa.Column('logo', sa.String(2048), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('contact_name', sa.String(100), nullable=True),
sa.Column('contact_phone', sa.String(20), nullable=True),
sa.Column('contact_email', sa.String(255), nullable=True),
sa.Column('final_review_enabled', sa.Boolean(), default=True, nullable=False),
sa.Column('is_active', sa.Boolean(), default=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False),
)
# 代理商表
op.create_table(
'agencies',
sa.Column('id', sa.String(64), primary_key=True),
sa.Column('user_id', sa.String(64), sa.ForeignKey('users.id', ondelete='CASCADE'), unique=True, nullable=False),
sa.Column('name', sa.String(255), nullable=False),
sa.Column('logo', sa.String(2048), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('contact_name', sa.String(100), nullable=True),
sa.Column('contact_phone', sa.String(20), nullable=True),
sa.Column('contact_email', sa.String(255), nullable=True),
sa.Column('force_pass_enabled', sa.Boolean(), default=True, nullable=False),
sa.Column('is_active', sa.Boolean(), default=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False),
)
# 达人表
op.create_table(
'creators',
sa.Column('id', sa.String(64), primary_key=True),
sa.Column('user_id', sa.String(64), sa.ForeignKey('users.id', ondelete='CASCADE'), unique=True, nullable=False),
sa.Column('name', sa.String(255), nullable=False),
sa.Column('avatar', sa.String(2048), nullable=True),
sa.Column('bio', sa.Text(), nullable=True),
sa.Column('douyin_account', sa.String(100), nullable=True),
sa.Column('xiaohongshu_account', sa.String(100), nullable=True),
sa.Column('bilibili_account', sa.String(100), nullable=True),
sa.Column('is_active', sa.Boolean(), default=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False),
)
# 品牌方-代理商关联表
op.create_table(
'brand_agency',
sa.Column('brand_id', sa.String(64), sa.ForeignKey('brands.id', ondelete='CASCADE'), primary_key=True),
sa.Column('agency_id', sa.String(64), sa.ForeignKey('agencies.id', ondelete='CASCADE'), primary_key=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column('is_active', sa.Boolean(), default=True),
)
# 代理商-达人关联表
op.create_table(
'agency_creator',
sa.Column('agency_id', sa.String(64), sa.ForeignKey('agencies.id', ondelete='CASCADE'), primary_key=True),
sa.Column('creator_id', sa.String(64), sa.ForeignKey('creators.id', ondelete='CASCADE'), primary_key=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column('is_active', sa.Boolean(), default=True),
)
# 项目表
op.create_table(
'projects',
sa.Column('id', sa.String(64), primary_key=True),
sa.Column('brand_id', sa.String(64), sa.ForeignKey('brands.id', ondelete='CASCADE'), nullable=False, index=True),
sa.Column('name', sa.String(255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('start_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('deadline', sa.DateTime(timezone=True), nullable=True),
sa.Column('status', sa.String(20), default='active', nullable=False, index=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False),
)
# 项目-代理商关联表
op.create_table(
'project_agency',
sa.Column('project_id', sa.String(64), sa.ForeignKey('projects.id', ondelete='CASCADE'), primary_key=True),
sa.Column('agency_id', sa.String(64), sa.ForeignKey('agencies.id', ondelete='CASCADE'), primary_key=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column('is_active', sa.Boolean(), default=True),
)
# Brief 表
op.create_table(
'briefs',
sa.Column('id', sa.String(64), primary_key=True),
sa.Column('project_id', sa.String(64), sa.ForeignKey('projects.id', ondelete='CASCADE'), unique=True, nullable=False, index=True),
sa.Column('file_url', sa.String(2048), nullable=True),
sa.Column('file_name', sa.String(255), nullable=True),
sa.Column('selling_points', postgresql.JSON(), nullable=True),
sa.Column('blacklist_words', postgresql.JSON(), nullable=True),
sa.Column('competitors', postgresql.JSON(), nullable=True),
sa.Column('brand_tone', sa.Text(), nullable=True),
sa.Column('min_duration', sa.Integer(), nullable=True),
sa.Column('max_duration', sa.Integer(), nullable=True),
sa.Column('other_requirements', sa.Text(), nullable=True),
sa.Column('attachments', postgresql.JSON(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False),
)
# 任务表
op.create_table(
'tasks',
sa.Column('id', sa.String(64), primary_key=True),
sa.Column('project_id', sa.String(64), sa.ForeignKey('projects.id', ondelete='CASCADE'), nullable=False, index=True),
sa.Column('agency_id', sa.String(64), sa.ForeignKey('agencies.id', ondelete='CASCADE'), nullable=False, index=True),
sa.Column('creator_id', sa.String(64), sa.ForeignKey('creators.id', ondelete='CASCADE'), nullable=False, index=True),
sa.Column('name', sa.String(255), nullable=False),
sa.Column('sequence', sa.Integer(), default=1, nullable=False),
sa.Column('stage', postgresql.ENUM(
'script_upload', 'script_ai_review', 'script_agency_review', 'script_brand_review',
'video_upload', 'video_ai_review', 'video_agency_review', 'video_brand_review',
'completed', 'rejected',
name='task_stage_enum', create_type=False
), default='script_upload', nullable=False, index=True),
# 脚本相关
sa.Column('script_file_url', sa.String(2048), nullable=True),
sa.Column('script_file_name', sa.String(255), nullable=True),
sa.Column('script_uploaded_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('script_ai_score', sa.Integer(), nullable=True),
sa.Column('script_ai_result', postgresql.JSON(), nullable=True),
sa.Column('script_ai_reviewed_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('script_agency_status', postgresql.ENUM('pending', 'processing', 'passed', 'rejected', 'force_passed', name='task_status_enum', create_type=False), nullable=True),
sa.Column('script_agency_comment', sa.Text(), nullable=True),
sa.Column('script_agency_reviewer_id', sa.String(64), nullable=True),
sa.Column('script_agency_reviewed_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('script_brand_status', postgresql.ENUM('pending', 'processing', 'passed', 'rejected', 'force_passed', name='task_status_enum', create_type=False), nullable=True),
sa.Column('script_brand_comment', sa.Text(), nullable=True),
sa.Column('script_brand_reviewer_id', sa.String(64), nullable=True),
sa.Column('script_brand_reviewed_at', sa.DateTime(timezone=True), nullable=True),
# 视频相关
sa.Column('video_file_url', sa.String(2048), nullable=True),
sa.Column('video_file_name', sa.String(255), nullable=True),
sa.Column('video_duration', sa.Integer(), nullable=True),
sa.Column('video_thumbnail_url', sa.String(2048), nullable=True),
sa.Column('video_uploaded_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('video_ai_score', sa.Integer(), nullable=True),
sa.Column('video_ai_result', postgresql.JSON(), nullable=True),
sa.Column('video_ai_reviewed_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('video_agency_status', postgresql.ENUM('pending', 'processing', 'passed', 'rejected', 'force_passed', name='task_status_enum', create_type=False), nullable=True),
sa.Column('video_agency_comment', sa.Text(), nullable=True),
sa.Column('video_agency_reviewer_id', sa.String(64), nullable=True),
sa.Column('video_agency_reviewed_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('video_brand_status', postgresql.ENUM('pending', 'processing', 'passed', 'rejected', 'force_passed', name='task_status_enum', create_type=False), nullable=True),
sa.Column('video_brand_comment', sa.Text(), nullable=True),
sa.Column('video_brand_reviewer_id', sa.String(64), nullable=True),
sa.Column('video_brand_reviewed_at', sa.DateTime(timezone=True), nullable=True),
# 申诉相关
sa.Column('appeal_count', sa.Integer(), default=1, nullable=False),
sa.Column('is_appeal', sa.Boolean(), default=False, nullable=False),
sa.Column('appeal_reason', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False),
)
def downgrade() -> None:
op.drop_table('tasks')
op.drop_table('briefs')
op.drop_table('project_agency')
op.drop_table('projects')
op.drop_table('agency_creator')
op.drop_table('brand_agency')
op.drop_table('creators')
op.drop_table('agencies')
op.drop_table('brands')
op.drop_table('users')
# 删除枚举类型
op.execute("DROP TYPE IF EXISTS task_stage_enum")
op.execute("DROP TYPE IF EXISTS user_role_enum")