# 项目素材服务 - 流程图与关系图 > **文档版本**:v1.0 > **最后更新**:2026-02-02 > **关联文档**:[项目素材服务](./project-resource-service.md) --- ## 目录 1. [素材上传流程](#素材上传流程) 2. [AI 生成素材流程](#ai-生成素材流程) 3. [素材删除流程](#素材删除流程) 4. [素材与分镜关联流程](#素材与分镜关联流程) 5. [数据库表关系图](#数据库表关系图) 6. [素材类型枚举](#素材类型枚举) 7. [服务依赖关系图](#服务依赖关系图) 8. [文件去重机制](#文件去重机制) --- ## 素材上传流程 ```mermaid sequenceDiagram participant U as 用户 participant F as 前端 participant A as API 服务 participant PS as ProjectService participant PRS as ProjectResourceService participant FS as FileStorageService participant DB as 数据库 participant S3 as 对象存储 U->>F: 上传素材文件 F->>F: 选择素材类型(character/scene/prop/footage) F->>F: 可选:关联标签(element_tag_id) F->>A: POST /api/v1/projects/{id}/resources A->>PRS: upload_resource() PRS->>PS: 检查项目存在性和权限 PS->>DB: 查询项目信息 DB-->>PS: 返回项目数据 PS-->>PRS: 验证通过 alt 素材类型为 footage PRS->>PRS: 检查文件类型(图片或视频) alt 文件为视频 PRS->>PRS: 验证视频大小(<= 500MB) PRS->>PRS: 使用 ffmpeg 获取视频元数据 else 文件为图片 PRS->>PRS: 验证图片大小(<= 20MB) PRS->>PRS: 获取图片尺寸 end else 其他素材类型 PRS->>PRS: 验证图片类型和大小(<= 20MB) PRS->>PRS: 获取图片尺寸 end PRS->>FS: upload_file() FS->>FS: 计算文件 SHA256 校验和 FS->>DB: 检查文件是否已存在(去重) alt 文件已存在 FS->>DB: 增加引用计数 else 文件不存在 FS->>S3: 上传文件到对象存储 S3-->>FS: 返回文件 URL FS->>DB: 创建 file_checksums 记录 end FS-->>PRS: 返回文件元数据 alt 文件为视频 PRS->>PRS: 使用 ffmpeg 提取第一帧 PRS->>FS: 上传视频缩略图 FS->>S3: 上传缩略图 S3-->>FS: 返回缩略图 URL FS-->>PRS: 返回缩略图元数据 else 文件为图片 PRS->>PRS: 生成 300x300 缩略图 PRS->>FS: 上传图片缩略图 FS->>S3: 上传缩略图 S3-->>FS: 返回缩略图 URL FS-->>PRS: 返回缩略图元数据 end alt 提供了 element_tag_id PRS->>DB: 查询标签信息 DB-->>PRS: 返回 element_name 和 tag_label PRS->>PRS: 填充冗余字段 end PRS->>PRS: 生成 UUID v7 PRS->>DB: 创建 project_resources 记录 DB-->>PRS: 返回素材信息 PRS-->>A: 返回素材数据 A-->>F: 返回成功响应 F-->>U: 显示上传成功 ``` **流程说明**: 1. **项目权限检查**:确保用户有权限在项目中上传素材 2. **文件类型验证**: - `character/scene/prop`:仅支持图片(JPEG、PNG、GIF、WebP、BMP),最大 20MB - `footage`:支持图片和视频(MP4、MOV、AVI、WebM),图片最大 20MB,视频最大 500MB 3. **文件去重**:通过 SHA256 校验和检查文件是否已存在 4. **缩略图生成**: - 图片:生成 300x300 缩略图 - 视频:使用 ffmpeg 提取第一帧作为缩略图 5. **标签关联**:如果提供 `element_tag_id`,自动填充 `element_name` 和 `tag_label` 冗余字段 6. **UUID v7 生成**:应用层生成主键 --- ## AI 生成素材流程 ```mermaid sequenceDiagram participant U as 用户 participant F as 前端 participant A as API 服务 participant AI as AI Service participant Q as Celery 队列 participant W as Worker participant PRS as ProjectResourceService participant FS as FileStorageService participant DB as 数据库 participant S3 as 对象存储 U->>F: 输入提示词生成素材 F->>A: POST /api/v1/ai/generate-image A->>AI: 创建 AI 任务 AI->>DB: 检查积分余额 AI->>DB: 创建 AI 任务记录 AI->>DB: 扣除积分 AI->>Q: 提交异步任务 AI-->>A: 返回任务 ID A-->>F: 返回任务 ID F-->>U: 显示生成中 Q->>W: 分配任务 W->>W: 调用 AI 大模型 API W->>W: 获取生成的图片数据 W->>PRS: create_ai_generated_resource() PRS->>PRS: 解析图片格式和尺寸 PRS->>FS: upload_file() FS->>FS: 计算 SHA256 校验和 FS->>S3: 上传图片 S3-->>FS: 返回文件 URL FS->>DB: 创建 file_checksums 记录 FS-->>PRS: 返回文件元数据 PRS->>PRS: 生成 300x300 缩略图 PRS->>FS: 上传缩略图 FS->>S3: 上传缩略图 S3-->>FS: 返回缩略图 URL FS-->>PRS: 返回缩略图元数据 alt 提供了 element_tag_id PRS->>DB: 查询标签信息 DB-->>PRS: 返回 element_name 和 tag_label PRS->>PRS: 填充冗余字段 end PRS->>PRS: 生成 UUID v7 PRS->>DB: 创建 project_resources 记录 PRS->>DB: 关联 ai_job_id DB-->>PRS: 返回素材信息 W->>DB: 更新 AI 任务状态(completed) loop 轮询任务状态 F->>A: GET /api/v1/ai/jobs/{taskId} A->>DB: 查询任务状态 alt 任务完成 DB-->>A: 返回素材信息 A-->>F: 返回素材数据 F->>F: 显示生成结果 F-->>U: 显示素材图片 end end ``` **流程说明**: 1. **积分检查**:AI Service 检查用户积分余额 2. **异步任务**:创建 Celery 任务,立即返回 task_id 3. **AI 生成**:Worker 调用 AI 大模型 API 生成图片 4. **文件处理**:与上传流程类似,计算校验和、生成缩略图 5. **任务关联**:记录 `ai_job_id`,便于追溯生成历史 6. **轮询状态**:前端轮询任务状态,完成后显示结果 --- ## 素材删除流程 ```mermaid sequenceDiagram participant U as 用户 participant F as 前端 participant A as API 服务 participant PRS as ProjectResourceService participant FS as FileStorageService participant DB as 数据库 U->>F: 点击删除素材 F->>A: GET /api/v1/resources/{id}/usage A->>PRS: check_resource_usage() PRS->>DB: 查询 usage_count DB-->>PRS: 返回使用情况 PRS-->>A: 返回使用统计 A-->>F: 返回 usage_count alt usage_count > 0 F->>F: 显示警告对话框 F-->>U: 提示"素材正在被 N 个分镜使用" U->>F: 选择是否强制删除 alt 用户取消 F-->>U: 取消删除 else 用户确认强制删除 F->>A: DELETE /api/v1/resources/{id}?force=true end else usage_count = 0 F->>F: 显示确认对话框 U->>F: 确认删除 F->>A: DELETE /api/v1/resources/{id}?force=false end A->>PRS: delete_resource() PRS->>DB: 检查项目权限 alt force=false 且 usage_count > 0 PRS-->>A: 抛出 ValidationError A-->>F: 返回 400 错误 F-->>U: 显示错误提示 else 允许删除 PRS->>DB: 软删除(设置 deleted_at) PRS->>FS: decrease_reference_count() FS->>DB: 减少文件引用计数 alt 引用计数为 0 FS->>DB: 标记文件可清理 Note over FS: 定时任务会清理无引用文件 end FS-->>PRS: 返回成功 PRS-->>A: 返回成功 A-->>F: 返回成功响应 F->>F: 从列表中移除素材 F-->>U: 显示删除成功 end ``` **流程说明**: 1. **使用检查**:删除前检查 `usage_count`(被多少个分镜使用) 2. **用户确认**: - `usage_count > 0`:显示警告,询问是否强制删除 - `usage_count = 0`:直接确认删除 3. **删除保护**:默认禁止删除正在使用的素材(`force=false`) 4. **强制删除**:`force=true` 时允许删除,但会导致分镜失去素材引用 5. **软删除**:设置 `deleted_at`,不物理删除记录 6. **引用计数**:减少文件引用计数,引用为 0 时标记可清理 --- ## 素材与分镜关联流程 ```mermaid sequenceDiagram participant U as 用户 participant F as 前端 participant A as API 服务 participant SRS as StoryboardResourceService participant PRS as ProjectResourceService participant DB as 数据库 U->>F: 将素材拖拽到分镜 F->>A: POST /api/v1/storyboards/{id}/resources A->>SRS: add_resource_to_storyboard() SRS->>DB: 检查分镜存在性 SRS->>PRS: 检查素材存在性 PRS->>DB: 查询素材信息 DB-->>PRS: 返回素材数据 PRS-->>SRS: 验证通过 SRS->>DB: 检查是否已关联 alt 已关联 SRS-->>A: 返回错误 A-->>F: 返回 400 错误 F-->>U: 提示"素材已添加" else 未关联 SRS->>DB: 创建 storyboard_resources 记录 SRS->>DB: 增加素材 usage_count DB-->>SRS: 返回关联信息 SRS-->>A: 返回成功 A-->>F: 返回关联数据 F->>F: 更新分镜资源列表 F-->>U: 显示素材已添加 end Note over U,DB: 从分镜移除素材 U->>F: 点击移除素材 F->>A: DELETE /api/v1/storyboards/{id}/resources/{resourceId} A->>SRS: remove_resource_from_storyboard() SRS->>DB: 删除 storyboard_resources 记录 SRS->>DB: 减少素材 usage_count DB-->>SRS: 返回成功 SRS-->>A: 返回成功 A-->>F: 返回成功响应 F->>F: 从列表中移除素材 F-->>U: 显示移除成功 ``` **流程说明**: 1. **添加素材到分镜**: - 检查分镜和素材存在性 - 检查是否已关联(避免重复) - 创建 `storyboard_resources` 关联记录 - 增加素材的 `usage_count` 2. **从分镜移除素材**: - 删除 `storyboard_resources` 关联记录 - 减少素材的 `usage_count` 3. **引用计数维护**:`usage_count` 由 `StoryboardResourceService` 自动维护 --- ## 数据库表关系图 ```mermaid erDiagram projects ||--o{ project_resources : "归属" users ||--o{ project_resources : "创建" screenplay_element_tags ||--o{ project_resources : "关联标签" ai_jobs ||--o{ project_resources : "AI生成" resources ||--o{ project_resources : "来源-后期扩展" file_checksums ||--o{ project_resources : "文件去重" project_resources ||--o{ storyboard_resources : "关联" storyboards ||--o{ storyboard_resources : "使用" projects { uuid project_id PK "项目ID" text name "项目名称" uuid created_by FK "创建者" } project_resources { uuid project_resource_id PK "素材ID-UUID_v7" uuid project_id FK "项目ID-逻辑外键" text name "素材名称" smallint type "素材类型-1到4" text file_url "文件URL" text thumbnail_url "缩略图URL" bigint file_size "文件大小" text checksum "文件校验和" uuid element_tag_id FK "标签ID-逻辑外键" text element_name "元素名称-冗余" text tag_label "标签名称-冗余" uuid ai_job_id FK "AI任务ID-逻辑外键" jsonb meta_data "元数据" int usage_count "引用计数" uuid created_by FK "创建者-逻辑外键" timestamptz created_at "创建时间" timestamptz deleted_at "删除时间" } storyboard_resources { uuid storyboard_resource_id PK "关联ID-UUID_v7" uuid storyboard_id FK "分镜ID-逻辑外键" uuid project_resource_id FK "素材ID-逻辑外键" smallint resource_type "素材类型-1到4" int display_order "显示顺序" timestamptz created_at "创建时间" } screenplay_element_tags { uuid element_tag_id PK "标签ID" text element_name "元素名称" text tag_label "标签名称" } file_checksums { text checksum PK "文件校验和" text file_url "文件URL" int reference_count "引用计数" } ai_jobs { uuid ai_job_id PK "AI任务ID" text status "任务状态" } users { uuid user_id PK "用户ID" text nickname "昵称" } storyboards { uuid storyboard_id PK "分镜ID" uuid project_id FK "项目ID" text title "分镜标题" } resources { uuid resource_id PK "资源ID" text name "资源名称" } ``` **说明**: - 所有外键关系均为**逻辑外键**(无物理约束) - 引用完整性由应用层(Service/Repository)保证 - `element_name` 和 `tag_label` 为冗余字段,优化查询性能 - `usage_count` 记录素材被多少个分镜使用 - `file_checksums` 表实现文件去重 --- ## 素材类型枚举 ```mermaid graph LR ResourceType[素材类型 SMALLINT] --> Character[1 = character 角色] ResourceType --> Scene[2 = scene 场景] ResourceType --> Prop[3 = prop 道具] ResourceType --> Footage[4 = footage 实拍] Character --> CharDesc[主角、配角、群演形象] Scene --> SceneDesc[室内外场景背景] Prop --> PropDesc[物品、工具、装饰] Footage --> FootageDesc[航拍、瞰景、实拍视频] Character --> AISupport1[✅ 支持 AI 生成] Scene --> AISupport2[✅ 支持 AI 生成] Prop --> AISupport3[✅ 支持 AI 生成] Footage --> AISupport4[❌ 不支持 AI 生成] Character --> TagSupport1[✅ 可关联标签] Scene --> TagSupport2[✅ 可关联标签] Prop --> TagSupport3[✅ 可关联标签] Footage --> TagSupport4[❌ 不可关联标签] style ResourceType fill:#e1f5e1 style Footage fill:#ffe1e1 ``` **素材类型说明**: | 类型 | 数值 | 字符串 | 描述 | AI 生成 | 标签关联 | 文件类型 | |------|------|--------|------|---------|----------|----------| | **角色** | 1 | character | 主角、配角、群演形象 | ✅ | ✅ | 图片 | | **场景** | 2 | scene | 室内外场景背景 | ✅ | ✅ | 图片 | | **道具** | 3 | prop | 物品、工具、装饰 | ✅ | ✅ | 图片 | | **实拍** | 4 | footage | 航拍、瞰景、实拍视频 | ❌ | ❌ | 图片/视频 | **数据库约束**: ```sql -- 实拍素材不能有标签关联 CONSTRAINT project_resources_footage_no_tag_check CHECK ( (type = 4 AND element_tag_id IS NULL AND element_name IS NULL AND tag_label IS NULL) OR (type IN (1, 2, 3)) ) ``` --- ## 服务依赖关系图 ```mermaid graph TB API[API 路由层] --> PRS[ProjectResourceService] PRS --> PS[ProjectService] PRS --> FS[FileStorageService] PRS --> Repo[ProjectResourceRepository] PS --> DB[(PostgreSQL)] FS --> DB FS --> S3[对象存储 MinIO/S3] Repo --> DB PRS --> TagRepo[ScreenplayTagRepository] PRS --> ScreenplayRepo[ScreenplayRepository] TagRepo --> DB ScreenplayRepo --> DB SRS[StoryboardResourceService] --> PRS SRS --> DB AI[AI Service] --> PRS AI --> Queue[Celery 队列] Queue --> Worker[Worker] Worker --> PRS style PRS fill:#e1f5e1 style FS fill:#fff4e1 style S3 fill:#e1f0ff ``` **依赖说明**: 1. **ProjectResourceService**(核心服务): - 依赖 `ProjectService`:检查项目权限 - 依赖 `FileStorageService`:文件上传和去重 - 依赖 `ScreenplayTagRepository`:验证标签关联 - 依赖 `ScreenplayRepository`:验证标签归属项目 2. **StoryboardResourceService**: - 调用 `ProjectResourceService` 检查素材存在性 - 维护素材的 `usage_count` 3. **AI Service**: - 通过 Celery Worker 调用 `ProjectResourceService.create_ai_generated_resource()` - 异步生成素材 4. **FileStorageService**: - 上传文件到对象存储(MinIO/S3) - 管理文件去重(`file_checksums` 表) --- ## 文件去重机制 ```mermaid graph TB Upload[上传文件] --> CalcHash[计算 SHA256 校验和] CalcHash --> CheckDB{文件已存在?} CheckDB -->|是| IncRef[增加引用计数] CheckDB -->|否| UploadS3[上传到 S3] UploadS3 --> CreateRecord[创建 file_checksums 记录] CreateRecord --> SetRef[设置引用计数 = 1] IncRef --> CreateResource[创建 project_resources 记录] SetRef --> CreateResource CreateResource --> ReuseURL[复用已有文件 URL] Delete[删除素材] --> DecRef[减少引用计数] DecRef --> CheckRef{引用计数 = 0?} CheckRef -->|是| MarkClean[标记文件可清理] CheckRef -->|否| Keep[保留文件] MarkClean --> CronJob[定时任务清理] CronJob --> DeleteS3[从 S3 删除文件] style CheckDB fill:#fff4e1 style CheckRef fill:#fff4e1 style UploadS3 fill:#e1f0ff ``` **去重机制说明**: 1. **上传阶段**: - 计算文件的 SHA256 校验和 - 查询 `file_checksums` 表检查文件是否已存在 - 如果存在:增加引用计数,复用已有 URL - 如果不存在:上传到 S3,创建新记录 2. **删除阶段**: - 软删除 `project_resources` 记录 - 减少 `file_checksums` 的引用计数 - 引用计数为 0 时,标记文件可清理 3. **清理阶段**: - 定时任务扫描引用计数为 0 的文件 - 从 S3 删除物理文件 - 删除 `file_checksums` 记录 **优点**: - 节省存储空间(相同文件只存储一份) - 加快上传速度(已存在的文件秒传) - 安全删除(引用计数为 0 才物理删除) --- ## 相关文档 - [项目素材服务](./project-resource-service.md) - 服务实现、API 接口、数据库设计 - [项目管理服务](./project-service.md) - 项目权限管理 - [文件存储服务](../file-storage/file-storage-service.md) - 文件上传和去重 - [分镜管理服务](../storyboard/storyboard-service.md) - 素材与分镜关联 - [AI 服务](../ai/ai-service.md) - AI 生成素材 --- **文档版本**:v1.0 **最后更新**:2026-02-02