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
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 <= 7media_type >= 1 AND media_type <= 5status >= 1 AND status <= 3message_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 <= 3order_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_atupdate_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
创建人:系统
审核状态:待审核