You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

14 KiB

AI 对话记录服务表结构创建

日期:2026-02-03
类型:数据库迁移
迁移文件20260203_1600_create_ai_conversations_tables.py
相关文档AI 对话记录服务


变更概述

创建 AI 对话记录服务的核心表结构,支持用户与 AI 的多轮对话、上下文管理和任务关联。

核心设计:多态关联 + 标签系统

  • 支持不同类型的 AI 生成任务(分镜/角色/场景/道具/资源/音效/配音)
  • 支持同一对象的不同变体(通过 tag_id 区分装扮/角度/时间/状态)
  • 支持不同媒体类型(图片/视频/音频/3D模型/文本)
  • 唯一性约束:同一用户、同一目标、同一标签、同一媒体类型只能有一个活跃会话

新增表

1. ai_conversations(AI 对话会话表)

用途:记录对话会话的基本信息

字段

字段名 类型 约束 说明
conversation_id UUID PK 对话会话唯一标识(UUID v7)
user_id UUID NOT NULL 用户 ID - 应用层验证
project_id UUID NULL 项目 ID(可选)- 应用层验证
target_type SMALLINT NOT NULL 目标类型(1=分镜 2=角色 3=场景 4=道具 5=资源 6=音效 7=配音)
target_id UUID NOT NULL 目标对象 ID - 应用层验证
tag_id UUID NULL 标签 ID(可选,用于区分变体)- 应用层验证
media_type SMALLINT NOT NULL 媒体类型(1=图片 2=视频 3=音频 4=3D模型 5=文本)
title TEXT NULL 会话标题
status SMALLINT NOT NULL, DEFAULT 1 会话状态(1=活跃 2=已归档 3=已删除)
message_count INTEGER NOT NULL, DEFAULT 0 消息数量
last_message_at TIMESTAMPTZ NULL 最后一条消息时间
metadata JSONB NOT NULL, DEFAULT '{}' 额外元数据
created_at TIMESTAMPTZ NOT NULL, DEFAULT now() 创建时间
updated_at TIMESTAMPTZ NOT NULL, DEFAULT now() 更新时间

约束

  • target_type >= 1 AND target_type <= 7
  • media_type >= 1 AND media_type <= 5
  • status >= 1 AND status <= 3
  • message_count >= 0

索引

  • idx_ai_conversations_user_id - 用户查询
  • idx_ai_conversations_project_id - 项目查询(部分索引,WHERE project_id IS NOT NULL)
  • idx_ai_conversations_target - 目标类型+ID 查询
  • idx_ai_conversations_target_tag - 目标类型+ID+标签 查询
  • idx_ai_conversations_tag_id - 标签查询(部分索引,WHERE tag_id IS NOT NULL)
  • idx_ai_conversations_media_type - 媒体类型查询
  • idx_ai_conversations_status - 状态查询
  • idx_ai_conversations_created_at - 创建时间排序
  • idx_ai_conversations_last_message_at - 最后消息时间排序(部分索引)
  • idx_ai_conversations_user_status - 用户+状态复合查询
  • idx_ai_conversations_user_target_tag_media - 完整查询条件复合索引
  • idx_ai_conversations_metadata_gin - 元数据 GIN 索引

唯一约束

  • idx_ai_conversations_unique_active - 同一用户、同一目标、同一标签、同一媒体类型只能有一个活跃会话(NULLS NOT DISTINCT)

触发器

  • update_ai_conversations_updated_at - 自动更新 updated_at

2. ai_conversation_messages(AI 对话消息表)

用途:记录对话中的每条消息

字段

字段名 类型 约束 说明
message_id UUID PK 消息唯一标识(UUID v7)
conversation_id UUID NOT NULL 对话会话 ID - 应用层验证
user_id UUID NOT NULL 用户 ID - 应用层验证
ai_job_id UUID NULL 关联的 AI 任务 ID - 应用层验证
role SMALLINT NOT NULL 消息角色(1=用户 2=AI 3=系统)
content TEXT NOT NULL 消息内容
metadata JSONB NOT NULL, DEFAULT '{}' 额外元数据
order_index INTEGER NOT NULL 消息顺序
created_at TIMESTAMPTZ NOT NULL, DEFAULT now() 创建时间
updated_at TIMESTAMPTZ NOT NULL, DEFAULT now() 更新时间

约束

  • role >= 1 AND role <= 3
  • order_index >= 0

索引

  • idx_ai_conversation_messages_conversation_id - 对话查询
  • idx_ai_conversation_messages_user_id - 用户查询
  • idx_ai_conversation_messages_ai_job_id - AI 任务查询(部分索引,WHERE ai_job_id IS NOT NULL)
  • idx_ai_conversation_messages_role - 角色查询
  • idx_ai_conversation_messages_created_at - 创建时间排序
  • idx_ai_conversation_messages_conversation_order - 对话+顺序复合索引
  • idx_ai_conversation_messages_metadata_gin - 元数据 GIN 索引
  • idx_ai_conversation_messages_content_gin - 内容全文搜索 GIN 索引

触发器

  • update_ai_conversation_messages_updated_at - 自动更新 updated_at
  • update_conversation_stats_trigger - 自动更新会话统计信息(message_count, last_message_at)

触发器函数

update_conversation_stats()

用途:自动更新对话会话的统计信息

逻辑

  • INSERT 消息时:message_count + 1,更新 last_message_at
  • DELETE 消息时:message_count - 1

触发时机:AFTER INSERT OR DELETE ON ai_conversation_messages


Python 枚举定义

ConversationStatus(对话会话状态)

class ConversationStatus(IntEnum):
    """对话会话状态"""
    ACTIVE = 1      # 活跃
    ARCHIVED = 2    # 已归档
    DELETED = 3     # 已删除

TargetType(目标类型)

class TargetType(IntEnum):
    """目标类型"""
    STORYBOARD = 1      # 分镜
    CHARACTER = 2       # 角色
    SCENE = 3           # 场景
    PROP = 4            # 道具
    RESOURCE = 5        # 通用资源
    SOUND_EFFECT = 6    # 音效
    VOICEOVER = 7       # 配音

MediaType(媒体类型)

class MediaType(IntEnum):
    """媒体类型"""
    IMAGE = 1           # 图片
    VIDEO = 2           # 视频
    AUDIO = 3           # 音频
    MODEL_3D = 4        # 3D模型
    TEXT = 5            # 文本

MessageRole(消息角色)

class MessageRole(IntEnum):
    """消息角色"""
    USER = 1        # 用户消息
    ASSISTANT = 2   # AI 回复
    SYSTEM = 3      # 系统消息

设计亮点

1. 多态关联 + 标签系统

问题:不同类型的 AI 生成任务需要独立的对话上下文,同一对象的不同变体也需要独立的对话。

解决方案

  • target_type + target_id:支持多种目标类型(分镜/角色/场景/道具/资源/音效/配音)
  • tag_id:支持同一对象的不同变体(如角色的不同装扮、分镜的不同角度)
  • media_type:支持不同媒体类型(图片/视频/音频/3D模型/文本)

示例

  • 分镜001-正面角度-图片生成:target_type=1, target_id=分镜001ID, tag_id=正面角度TagID, media_type=1
  • 分镜001-侧面角度-图片生成:target_type=1, target_id=分镜001ID, tag_id=侧面角度TagID, media_type=1
  • 角色-张三-少年装扮-图片生成:target_type=2, target_id=张三ID, tag_id=少年装扮TagID, media_type=1

2. 唯一性约束(NULLS NOT DISTINCT)

规则:同一用户、同一目标、同一标签、同一媒体类型只能有一个活跃会话。

实现

CREATE UNIQUE INDEX idx_ai_conversations_unique_active 
ON ai_conversations (user_id, target_type, target_id, tag_id, media_type) 
NULLS NOT DISTINCT
WHERE status = 1;

注意:PostgreSQL 14+ 支持 NULLS NOT DISTINCT,使得 NULL 值被视为相同,确保唯一性约束正确工作。

3. 自动更新会话统计

触发器update_conversation_stats_trigger

功能

  • 插入消息时:自动增加 message_count,更新 last_message_at
  • 删除消息时:自动减少 message_count

优势:避免手动维护统计信息,确保数据一致性。

4. 全文搜索支持

索引idx_ai_conversation_messages_content_gin

功能:支持对消息内容进行全文搜索

示例

SELECT * FROM ai_conversation_messages
WHERE to_tsvector('simple', content) @@ to_tsquery('simple', '咖啡厅 & 温馨');

应用层引用完整性保证

由于采用无物理外键设计,应用层需要保证以下引用完整性:

1. 验证目标对象是否存在

async def _validate_target(self, target_type: int, target_id: UUID) -> bool:
    """验证目标对象是否存在"""
    if target_type == TargetType.STORYBOARD:
        from app.repositories.storyboard_repository import StoryboardRepository
        repo = StoryboardRepository(self.db)
        return await repo.exists(target_id)
    elif target_type == TargetType.CHARACTER:
        from app.repositories.screenplay_character_repository import ScreenplayCharacterRepository
        repo = ScreenplayCharacterRepository(self.db)
        return await repo.exists(target_id)
    # ... 其他类型

2. 验证标签是否存在且属于该对象

async def _validate_tag(self, tag_id: UUID, target_type: int, target_id: UUID) -> bool:
    """验证标签是否存在且属于该对象"""
    from app.repositories.screenplay_tag_repository import ScreenplayTagRepository
    
    repo = ScreenplayTagRepository(self.db)
    tag = await repo.get_by_id(tag_id)
    
    if not tag:
        return False
    
    # 验证标签是否属于该对象
    if tag.element_id != target_id:
        return False
    
    # 验证标签的元素类型是否匹配
    from app.models.screenplay_tag import ElementType
    element_type_map = {
        TargetType.STORYBOARD: ElementType.STORYBOARD,
        TargetType.CHARACTER: ElementType.CHARACTER,
        TargetType.SCENE: ElementType.SCENE,
        TargetType.PROP: ElementType.PROP,
    }
    
    expected_element_type = element_type_map.get(target_type)
    if tag.element_type != expected_element_type:
        return False
    
    return True

3. 验证组合有效性

async def _validate_combination(self, target_type: int, media_type: int) -> bool:
    """验证 target_type 和 media_type 的组合是否有效"""
    VALID_COMBINATIONS = {
        TargetType.STORYBOARD: [MediaType.IMAGE, MediaType.VIDEO, MediaType.AUDIO],
        TargetType.CHARACTER: [MediaType.IMAGE, MediaType.AUDIO],
        TargetType.SCENE: [MediaType.IMAGE],
        TargetType.PROP: [MediaType.IMAGE],
        TargetType.RESOURCE: [MediaType.IMAGE, MediaType.VIDEO, MediaType.AUDIO],
        TargetType.SOUND_EFFECT: [MediaType.AUDIO],
        TargetType.VOICEOVER: [MediaType.AUDIO],
    }
    
    valid_media_types = VALID_COMBINATIONS.get(target_type, [])
    if media_type not in valid_media_types:
        raise ValidationError(
            f"目标类型 {target_type} 不支持媒体类型 {media_type}"
        )
    return True

使用场景映射

场景 target_type target_id tag_id media_type 说明
分镜001基础图片 1 分镜001ID NULL 1 生成基础分镜图片
分镜001正面角度 1 分镜001ID 正面TagID 1 生成正面角度图片
分镜001侧面角度 1 分镜001ID 侧面TagID 1 生成侧面角度图片
分镜001视频 1 分镜001ID NULL 2 生成分镜视频
角色-张三基础形象 2 张三ID NULL 1 生成基础角色形象
角色-张三-少年装扮 2 张三ID 少年TagID 1 生成少年装扮图片
角色-张三-成年装扮 2 张三ID 成年TagID 1 生成成年装扮图片
场景-花果山-白天 3 花果山ID 白天TagID 1 生成白天场景图片
场景-花果山-夜晚 3 花果山ID 夜晚TagID 1 生成夜晚场景图片
独立音效生成 6 音效ID NULL 3 生成独立音效
独立配音生成 7 配音ID NULL 3 生成独立配音

查询示例

查询"分镜001"的所有对话

SELECT * FROM ai_conversations
WHERE user_id = '019d1234-5678-7abc-def0-111111111111'
  AND target_type = 1  -- STORYBOARD
  AND target_id = '019d1234-5678-7abc-def0-222222222222'  -- 分镜001的ID
  AND status = 1;  -- ACTIVE

查询"分镜001-正面角度"的"图片生成"对话

SELECT * FROM ai_conversations
WHERE user_id = '019d1234-5678-7abc-def0-111111111111'
  AND target_type = 1  -- STORYBOARD
  AND target_id = '019d1234-5678-7abc-def0-222222222222'  -- 分镜001的ID
  AND tag_id = '019d1234-5678-7abc-def0-333333333333'  -- 正面角度TagID
  AND media_type = 1  -- IMAGE
  AND status = 1;

查询对话的所有消息(按顺序)

SELECT * FROM ai_conversation_messages
WHERE conversation_id = '019d1234-5678-7abc-def0-444444444444'
ORDER BY order_index ASC;

全文搜索消息内容

SELECT * FROM ai_conversation_messages
WHERE to_tsvector('simple', content) @@ to_tsquery('simple', '咖啡厅 & 温馨')
ORDER BY created_at DESC;

迁移执行

升级

docker exec jointo-server-app alembic upgrade head

回滚

docker exec jointo-server-app alembic downgrade 20260203_1500

相关文档


创建日期:2026-02-03
创建人:系统
审核状态:待审核