# ADR 02: 跨项目资源共享 > **状态**:提议中 > **日期**:2026-02-07 > **决策者**:架构团队 --- ## 目录 - [ADR 02: 跨项目资源共享](#adr-02-跨项目资源共享) - [目录](#目录) - [背景](#背景) - [当前架构问题](#当前架构问题) - [1. 项目内容类型选择的困扰](#1-项目内容类型选择的困扰) - [2. 无法跨项目复用资源](#2-无法跨项目复用资源) - [业务需求分析](#业务需求分析) - [决策](#决策) - [核心方案](#核心方案) - [设计原则](#设计原则) - [架构设计](#架构设计) - [1. 数据库表结构](#1-数据库表结构) - [1.1 项目资源共享表(project\_resource\_shares)](#11-项目资源共享表project_resource_shares) - [1.2 项目表调整(projects)](#12-项目表调整projects) - [2. 共享粒度设计](#2-共享粒度设计) - [2.1 共享类型(share\_type)](#21-共享类型share_type) - [2.2 共享示例](#22-共享示例) - [3. 选择文件夹的处理](#3-选择文件夹的处理) - [4. 查询逻辑设计](#4-查询逻辑设计) - [4.1 查询项目资源(包含共享资源)](#41-查询项目资源包含共享资源) - [4.2 前端切换显示](#42-前端切换显示) - [5. 权限控制](#5-权限控制) - [5.1 共享权限检查](#51-共享权限检查) - [5.2 共享资源的使用权限](#52-共享资源的使用权限) - [5.3 变体标签的归属](#53-变体标签的归属) - [6. API 设计](#6-api-设计) - [6.1 新建项目时共享资源](#61-新建项目时共享资源) - [6.2 查询项目资源(包含共享资源)](#62-查询项目资源包含共享资源) - [6.3 管理共享关系](#63-管理共享关系) - [7. 前端交互流程](#7-前端交互流程) - [7.1 新建项目时共享资源](#71-新建项目时共享资源) - [7.2 共享素材弹窗界面设计](#72-共享素材弹窗界面设计) - [与 ADR 01 的关系](#与-adr-01-的关系) - [架构层级对比](#架构层级对比) - [核心区别](#核心区别) - [协同工作](#协同工作) - [影响分析](#影响分析) - [正面影响](#正面影响) - [负面影响](#负面影响) - [缓解措施](#缓解措施) - [实现计划](#实现计划) - [Phase 1:数据库迁移(2-3天)](#phase-1数据库迁移2-3天) - [Phase 2:后端服务层(6-8天)](#phase-2后端服务层6-8天) - [Phase 3:API 接口开发(4-5天)](#phase-3api-接口开发4-5天) - [Phase 4:前端适配(5-6天)](#phase-4前端适配5-6天) - [Phase 5:数据迁移执行(1天)](#phase-5数据迁移执行1天) - [Phase 6:监控与优化(持续)](#phase-6监控与优化持续) - [方案 B:复制模式(已拒绝)](#方案-b复制模式已拒绝) - [方案 C:混合模式(已拒绝)](#方案-c混合模式已拒绝) - [相关文档](#相关文档) - [架构决策记录](#架构决策记录) - [技术栈规范(jointo-tech-stack)](#技术栈规范jointo-tech-stack) - [变更记录](#变更记录) --- ## 背景 ### 当前架构问题 在现有架构中,项目资源(角色、场景、道具、素材)归属于父级项目(参见 [ADR 01](01-project-level-resource-ownership.md)),但存在以下问题: #### 1. 项目内容类型选择的困扰 **当前设计**: - 新建项目时,用户需要选择项目内容类型(content_type): - 1 = ad(广告片) - 2 = movie(电影) - 3 = series(剧集) - 4 = anime(动漫) - 5 = short(短视频) - 6 = concept(概念片) **问题**: - 用户在创建项目时,往往不确定项目的最终形态 - 项目内容类型选择增加了用户的认知负担 - 项目类型对系统功能没有实质性影响(仅用于分类和统计) - 用户更关心的是资源共享,而非项目分类 #### 2. 无法跨项目复用资源 **场景 1:跨项目复用角色** ``` 项目 A:《西游记》第一季 ├─ 角色:孙悟空、猪八戒、唐僧 └─ 场景:花果山、高老庄、长安城 项目 B:《西游记》第二季(新项目) └─ 需求:复用第一季的角色和场景 └─ 问题:无法直接引用,只能重新创建 ``` **场景 2:团队资源库** ``` 团队共享项目:《角色资源库》 ├─ 角色:100+ 通用角色(如"商务男性"、"学生女性") └─ 场景:50+ 通用场景(如"办公室"、"咖啡厅") 新项目:《品牌广告》 └─ 需求:使用团队资源库的角色和场景 └─ 问题:无法引用,只能复制粘贴 ``` **场景 3:素材复用** ``` 项目 A:《产品宣传片》 └─ 实拍素材:产品展示视频、工厂实景 项目 B:《产品广告》 └─ 需求:复用项目 A 的实拍素材 └─ 问题:无法引用,需要重新上传 ``` ### 业务需求分析 | 需求场景 | 当前方案 | 痛点 | 期望方案 | |---------|---------|------|---------| | 跨项目复用角色 | 手动重新创建 | 重复劳动,数据不一致 | 直接引用其他项目的角色 | | 团队资源库 | 复制粘贴 | 无法同步更新,管理混乱 | 共享资源库,统一管理 | | 素材复用 | 重新上传 | 浪费存储空间,上传耗时 | 引用其他项目的素材 | | 项目内容类型选择 | 创建时必选(广告/电影/剧集等) | 认知负担,对功能无实质影响 | 去掉内容类型选择,简化创建流程 | ## 决策 ### 核心方案 1. **去掉项目内容类型选择**:删除新建项目时的 `content_type` 选择(广告/电影/剧集等),简化创建流程 2. **新增跨项目资源共享**:在新建项目时,可以选择共享其他项目的素材 ### 设计原则 1. **引用模式,而非复制**:共享资源通过引用关联,而非复制数据 2. **权限控制**:只能共享自己有权限访问的项目 3. **灵活粒度**:支持整个项目、特定资源类型、特定资源的共享 4. **资源不可编辑**:共享的资源本身不可编辑(名称、描述等基本信息) 5. **可添加变体标签**:可以为共享资源添加新的变体标签(如"美猴王"),标签归属目标项目 6. **可断开连接**:可以随时断开共享关系 7. **保留 content_type 字段**:保留数据库字段用于统计和分析,但不强制用户选择 --- ## 架构设计 ### 1. 数据库表结构 #### 1.1 项目资源共享表(project_resource_shares) **职责**:记录项目之间的资源共享关系 ```sql -- 创建表 CREATE TABLE project_resource_shares ( share_id UUID PRIMARY KEY, -- 应用层生成 UUID v7 source_project_id UUID NOT NULL, -- 资源来源项目(被共享的项目) target_project_id UUID NOT NULL, -- 资源目标项目(使用资源的项目) share_type SMALLINT NOT NULL, -- 共享类型:1=整个项目, 2=特定资源类型, 3=特定资源 resource_type SMALLINT, -- 资源类型:1=角色, 2=场景, 3=道具, 4=素材(share_type=2时必填) resource_id UUID, -- 资源ID(share_type=3时必填) status SMALLINT DEFAULT 1, -- 状态:1=激活, 2=已断开 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_by UUID NOT NULL, -- 创建人(目标项目的所有者) updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT project_resource_shares_unique UNIQUE(source_project_id, target_project_id, share_type, resource_type, resource_id) ); -- 添加索引 CREATE INDEX idx_project_resource_shares_source ON project_resource_shares(source_project_id); CREATE INDEX idx_project_resource_shares_target ON project_resource_shares(target_project_id); CREATE INDEX idx_project_resource_shares_status ON project_resource_shares(status); -- 添加中文注释 COMMENT ON TABLE project_resource_shares IS '项目资源共享表:记录项目之间的资源共享关系'; COMMENT ON COLUMN project_resource_shares.share_id IS '共享ID(UUID v7,应用层生成)'; COMMENT ON COLUMN project_resource_shares.source_project_id IS '资源来源项目ID(被共享的项目)'; COMMENT ON COLUMN project_resource_shares.target_project_id IS '资源目标项目ID(使用资源的项目)'; COMMENT ON COLUMN project_resource_shares.share_type IS '共享类型:1=整个项目(all), 2=特定资源类型(type), 3=特定资源(resource)'; COMMENT ON COLUMN project_resource_shares.resource_type IS '资源类型:1=角色(character), 2=场景(location), 3=道具(prop), 4=素材(material)'; COMMENT ON COLUMN project_resource_shares.resource_id IS '资源ID(指向 project_characters/locations/props/resources)'; COMMENT ON COLUMN project_resource_shares.status IS '状态:1=激活(active), 2=已断开(disconnected)'; COMMENT ON COLUMN project_resource_shares.created_by IS '创建人(目标项目的所有者)'; ``` **关键设计说明**: - ⚠️ **禁止外键约束**:`source_project_id`, `target_project_id`, `resource_id` 不使用 `REFERENCES` - ⚠️ **UUID v7 应用层生成**:主键不使用 `DEFAULT uuid_generate_v7()` - ✅ **枚举使用 SMALLINT**:`share_type`, `resource_type`, `status` 使用 SMALLINT + IntEnum 模式 - ✅ **唯一约束**:防止重复共享同一资源 - ✅ **软删除**:通过 `status` 字段标记断开,而非物理删除 - ✅ **删除 permission 字段**:共享资源统一为只读,不可编辑基本信息 #### 1.2 项目表调整(projects) **调整**:将 `content_type` 字段改为可选(nullable),不强制用户选择 ```sql -- 将 content_type 字段改为可选 ALTER TABLE projects ALTER COLUMN content_type DROP NOT NULL; -- 更新表注释 COMMENT ON COLUMN projects.content_type IS '项目内容类型(可选):1=ad(广告片), 2=movie(电影), 3=series(剧集), 4=anime(动漫), 5=short(短视频), 6=concept(概念片)'; ``` **影响分析**: - ✅ **简化用户体验**:新建项目时无需选择内容类型 - ✅ **保留字段**:保留 content_type 字段用于统计和分析 - ✅ **向后兼容**:现有项目的 content_type 值保持不变 - ⚠️ **代码调整**:前端删除内容类型选择组件,后端允许 content_type 为 NULL ### 2. 共享粒度设计 #### 2.1 共享类型(share_type) | 类型 | 值 | 说明 | 示例 | |------|---|------|------| | 整个项目 | 1 | 共享项目的所有资源(角色、场景、道具、素材) | 共享《西游记》第一季的所有资源 | | 特定资源类型 | 2 | 共享项目的某一类资源(如所有角色) | 共享《西游记》第一季的所有角色 | | 特定资源 | 3 | 共享项目的某个具体资源(如"孙悟空") | 共享《西游记》第一季的"孙悟空"角色 | #### 2.2 共享示例 **示例 1:共享整个项目** ```sql INSERT INTO project_resource_shares ( share_id, source_project_id, target_project_id, share_type, resource_type, resource_id, status, created_by ) VALUES ( '01936d8a-7b2c-7890-abcd-ef1234567890', 'source-project-id', -- 《西游记》第一季 'target-project-id', -- 《西游记》第二季 1, -- 整个项目 NULL, -- 不指定资源类型 NULL, -- 不指定资源ID 1, -- 激活 'user-id' ); ``` **示例 2:共享特定资源类型(所有角色)** ```sql INSERT INTO project_resource_shares ( share_id, source_project_id, target_project_id, share_type, resource_type, resource_id, status, created_by ) VALUES ( '01936d8a-7b2c-7890-abcd-ef1234567891', 'source-project-id', -- 《角色资源库》 'target-project-id', -- 《品牌广告》 2, -- 特定资源类型 1, -- 角色 NULL, -- 不指定资源ID 1, -- 激活 'user-id' ); ``` **示例 3:共享特定资源("孙悟空")** ```sql INSERT INTO project_resource_shares ( share_id, source_project_id, target_project_id, share_type, resource_type, resource_id, status, created_by ) VALUES ( '01936d8a-7b2c-7890-abcd-ef1234567892', 'source-project-id', -- 《西游记》第一季 'target-project-id', -- 《西游记》番外篇 3, -- 特定资源 1, -- 角色 'character-id', -- "孙悟空"的ID 1, -- 激活 'user-id' ); ``` ### 3. 选择文件夹的处理 **需求**:用户可以选择文件夹,意思是选择文件夹下的所有父项目 **实现**:批量创建共享记录 ```python async def share_folder_projects( folder_id: UUID, target_project_id: UUID, share_type: int, resource_type: Optional[int], user_id: UUID ) -> List[UUID]: """ 共享文件夹下的所有父项目 Args: folder_id: 文件夹ID target_project_id: 目标项目ID share_type: 共享类型(1=整个项目, 2=特定资源类型) resource_type: 资源类型(share_type=2时必填) user_id: 用户ID Returns: 创建的共享记录ID列表 """ # 1. 查询文件夹下的所有父项目(parent_project_id = NULL) parent_projects = await self.db.fetch_all( """ SELECT project_id FROM projects WHERE folder_id = $1 AND parent_project_id IS NULL AND status = 1 """, folder_id ) # 2. 为每个父项目创建共享记录 share_ids = [] for project in parent_projects: share_id = generate_uuid_v7() await self.db.execute( """ INSERT INTO project_resource_shares ( share_id, source_project_id, target_project_id, share_type, resource_type, resource_id, status, created_by ) VALUES ($1, $2, $3, $4, $5, NULL, 1, $6) ON CONFLICT (source_project_id, target_project_id, share_type, resource_type, resource_id) DO NOTHING """, share_id, project['project_id'], target_project_id, share_type, resource_type, user_id ) share_ids.append(share_id) return share_ids ``` ### 4. 查询逻辑设计 #### 4.1 查询项目资源(包含共享资源) **需求**:在项目资源面板中,用户可以切换显示"我的资源"或"共享资源" **实现**:UNION ALL 查询 ```sql -- 查询项目的所有角色(包含共享资源) SELECT c.character_id, c.project_id, c.name, c.description, c.role_type, 'own' AS source_type, -- 标记为自己的资源 NULL AS share_id FROM project_characters c WHERE c.project_id = $1 -- 目标项目ID UNION ALL -- 共享类型 1:整个项目 SELECT c.character_id, c.project_id, c.name, c.description, c.role_type, 'shared' AS source_type, -- 标记为共享资源 s.share_id FROM project_resource_shares s JOIN project_characters c ON c.project_id = s.source_project_id WHERE s.target_project_id = $1 AND s.share_type = 1 -- 整个项目 AND s.status = 1 -- 激活状态 UNION ALL -- 共享类型 2:特定资源类型(角色) SELECT c.character_id, c.project_id, c.name, c.description, c.role_type, 'shared' AS source_type, s.share_id FROM project_resource_shares s JOIN project_characters c ON c.project_id = s.source_project_id WHERE s.target_project_id = $1 AND s.share_type = 2 -- 特定资源类型 AND s.resource_type = 1 -- 角色 AND s.status = 1 UNION ALL -- 共享类型 3:特定资源 SELECT c.character_id, c.project_id, c.name, c.description, c.role_type, 'shared' AS source_type, s.share_id FROM project_resource_shares s JOIN project_characters c ON c.character_id = s.resource_id WHERE s.target_project_id = $1 AND s.share_type = 3 -- 特定资源 AND s.resource_type = 1 -- 角色 AND s.status = 1 ORDER BY source_type, name; ``` #### 4.2 前端切换显示 **前端实现**: ```typescript // 资源来源类型 type ResourceSourceType = 'own' | 'shared' | 'all'; // 查询参数 interface GetResourcesParams { projectId: string; sourceType: ResourceSourceType; // 'own'=我的资源, 'shared'=共享资源, 'all'=全部 page: number; pageSize: number; } // API 调用 const getProjectCharacters = async (params: GetResourcesParams) => { const response = await fetch( `/api/v1/projects/${params.projectId}/resource-library/characters?` + `source_type=${params.sourceType}&page=${params.page}&page_size=${params.pageSize}` ); return response.json(); }; // 前端组件 const ProjectResourcePanel = () => { const [sourceType, setSourceType] = useState('all'); return (
全部资源 我的资源 共享资源
); }; ``` ### 5. 权限控制 #### 5.1 共享权限检查 **规则**:只能共享自己有权限访问的项目 ```python async def check_share_permission( user_id: UUID, source_project_id: UUID ) -> bool: """ 检查用户是否有权限共享源项目的资源 规则: 1. 用户是源项目的所有者 2. 用户是源项目的成员(editor 或 viewer) """ # 检查是否为所有者 project = await self.project_repo.get_by_id(source_project_id) if project.owner_id == user_id: return True # 检查是否为成员 member = await self.project_member_repo.get_member(source_project_id, user_id) if member and member.role in ['owner', 'editor', 'viewer']: return True return False ``` #### 5.2 共享资源的使用权限 **核心规则**: 1. **资源本身不可编辑**:不能修改共享资源的基本信息(名称、描述、角色类型等) 2. **可以添加变体标签**:可以为共享资源添加新的变体标签 3. **变体标签归属目标项目**:新增的变体标签属于目标项目,不影响源项目 **示例场景**: ``` 源项目:《西游记》第一季 └─ 角色:孙悟空 ├─ 变体标签:少年(源项目创建) └─ 变体标签:青年(源项目创建) 目标项目:《西游记》第二季(共享了"孙悟空"角色) └─ 角色:孙悟空(共享,不可编辑) ├─ 变体标签:少年(源项目,只读) ├─ 变体标签:青年(源项目,只读) └─ 变体标签:美猴王(目标项目创建,可编辑)✨ ``` **实现逻辑**: ```python async def check_resource_edit_permission( user_id: UUID, resource_id: UUID, resource_type: int ) -> Dict[str, bool]: """ 检查用户对资源的编辑权限 返回: { 'can_edit_resource': bool, # 是否可以编辑资源本身 'can_add_tags': bool, # 是否可以添加变体标签 'is_shared': bool # 是否为共享资源 } """ # 1. 查询资源所属项目 if resource_type == 1: # 角色 resource = await self.db.fetch_one( "SELECT project_id FROM project_characters WHERE character_id = $1", resource_id ) elif resource_type == 2: # 场景 resource = await self.db.fetch_one( "SELECT project_id FROM project_locations WHERE location_id = $1", resource_id ) elif resource_type == 3: # 道具 resource = await self.db.fetch_one( "SELECT project_id FROM project_props WHERE prop_id = $1", resource_id ) else: return {'can_edit_resource': False, 'can_add_tags': False, 'is_shared': False} # 2. 检查是否为自己的项目 project = await self.project_repo.get_by_id(resource['project_id']) if project.owner_id == user_id: return { 'can_edit_resource': True, # 自己的资源,可以编辑 'can_add_tags': True, # 可以添加变体标签 'is_shared': False } # 3. 检查是否为共享资源 share = await self.db.fetch_one( """ SELECT share_id FROM project_resource_shares WHERE source_project_id = $1 AND ( (share_type = 1) OR -- 整个项目 (share_type = 2 AND resource_type = $2) OR -- 特定资源类型 (share_type = 3 AND resource_id = $3) -- 特定资源 ) AND status = 1 """, resource['project_id'], resource_type, resource_id ) if share: return { 'can_edit_resource': False, # 共享资源,不可编辑 'can_add_tags': True, # 可以添加变体标签 'is_shared': True } return {'can_edit_resource': False, 'can_add_tags': False, 'is_shared': False} ``` #### 5.3 变体标签的归属 **设计**:变体标签通过 `project_element_tags` 表管理,标签归属于创建它的项目 ```sql -- project_element_tags 表结构(已存在) CREATE TABLE project_element_tags ( tag_id UUID PRIMARY KEY, screenplay_id UUID, -- 标签来源剧本(可选,用于追溯) element_type SMALLINT NOT NULL, -- 元素类型:1=角色, 2=场景, 3=道具 element_id UUID NOT NULL, -- 元素ID(指向 project_characters/locations/props) tag_label VARCHAR(100) NOT NULL, key VARCHAR(100), description TEXT, display_order INTEGER DEFAULT 0, meta_data JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); ``` **关键说明**: - ✅ **标签不直接归属项目**:标签通过 `element_id` 关联到资源,资源归属项目 - ✅ **共享资源可以添加标签**:目标项目可以为共享资源添加新的变体标签 - ✅ **标签查询需要区分来源**:查询时需要区分哪些标签是源项目的,哪些是目标项目的 **查询逻辑**: ```python async def get_resource_tags( resource_id: UUID, resource_type: int, current_project_id: UUID ) -> Dict[str, List]: """ 查询资源的变体标签,区分来源 返回: { 'own_tags': [...], # 当前项目创建的标签(可编辑) 'shared_tags': [...] # 源项目的标签(只读) } """ # 1. 查询资源所属项目 resource = await self.get_resource_project(resource_id, resource_type) # 2. 查询所有标签 all_tags = await self.db.fetch_all( """ SELECT * FROM project_element_tags WHERE element_type = $1 AND element_id = $2 ORDER BY display_order """, resource_type, resource_id ) # 3. 区分标签来源 own_tags = [] shared_tags = [] for tag in all_tags: # 通过 screenplay_id 判断标签来源 if tag['screenplay_id']: screenplay = await self.screenplay_repo.get_by_id(tag['screenplay_id']) if screenplay.project_id == current_project_id: own_tags.append(tag) else: shared_tags.append(tag) else: # 没有 screenplay_id 的标签,默认为共享标签 shared_tags.append(tag) return { 'own_tags': own_tags, 'shared_tags': shared_tags } ``` ### 6. API 设计 #### 6.1 新建项目时共享资源 **接口**:`POST /api/v1/projects` **请求体**: ```json { "name": "《西游记》第二季", "description": "续集项目", "folder_id": "folder-id", "shared_resources": [ { "source_type": "project", // 'folder' | 'project' | 'resource' "source_id": "source-project-id", // 文件夹ID、项目ID或资源ID "share_type": 1, // 1=整个项目, 2=特定资源类型, 3=特定资源 "resource_type": null // share_type=2时必填:1=角色, 2=场景, 3=道具, 4=素材 }, { "source_type": "folder", "source_id": "folder-id", "share_type": 1, "resource_type": null }, { "source_type": "resource", "source_id": "character-id", "share_type": 3, "resource_type": 1 } ] } ``` **响应**: ```json { "success": true, "code": 200, "message": "项目创建成功", "data": { "project": { "project_id": "new-project-id", "name": "《西游记》第二季", "description": "续集项目", "folder_id": "folder-id", "created_at": "2026-02-07T10:00:00Z" }, "shared_resources": { "total": 3, "shares": [ { "share_id": "share-id-1", "source_project_id": "source-project-id", "share_type": 1, "status": "active" }, { "share_id": "share-id-2", "source_project_id": "folder-project-1-id", "share_type": 1, "status": "active" }, { "share_id": "share-id-3", "source_project_id": "folder-project-2-id", "share_type": 1, "status": "active" } ] } }, "timestamp": "2026-02-07T10:00:00Z" } ``` #### 6.2 查询项目资源(包含共享资源) **接口**:`GET /api/v1/projects/{project_id}/resource-library/characters` **查询参数**: - `source_type`: 资源来源类型(`own` | `shared` | `all`,默认 `all`) - `page`: 页码 - `page_size`: 每页数量 **响应**: ```json { "success": true, "code": 200, "message": "查询成功", "data": { "items": [ { "character_id": "char-001", "project_id": "own-project-id", "name": "孙悟空", "description": "齐天大圣", "role_type": 1, "source_type": "own", // 'own' | 'shared' "share_id": null, "can_edit_resource": true, // 是否可以编辑资源本身 "can_add_tags": true // 是否可以添加变体标签 }, { "character_id": "char-002", "project_id": "source-project-id", "name": "猪八戒", "description": "天蓬元帅", "role_type": 2, "source_type": "shared", "share_id": "share-id-1", "can_edit_resource": false, // 共享资源,不可编辑 "can_add_tags": true // 可以添加变体标签 } ], "total": 2, "page": 1, "page_size": 20 }, "timestamp": "2026-02-07T10:00:00Z" } ``` #### 6.3 管理共享关系 **添加共享**:`POST /api/v1/projects/{project_id}/shares` ```json { "source_project_id": "source-project-id", "share_type": 1, "resource_type": null, "resource_id": null } ``` **断开共享**:`DELETE /api/v1/projects/{project_id}/shares/{share_id}` **查询共享列表**:`GET /api/v1/projects/{project_id}/shares` ```json { "success": true, "data": { "items": [ { "share_id": "share-id-1", "source_project_id": "source-project-id", "source_project_name": "《西游记》第一季", "share_type": 1, "resource_type": null, "status": 1, "created_at": "2026-02-07T10:00:00Z" } ], "total": 1 } } ``` ### 7. 前端交互流程 #### 7.1 新建项目时共享资源 **时序图**: ```mermaid sequenceDiagram actor User as 用户 participant Frontend as 前端 participant API as ProjectAPI participant ProjectService as ProjectService participant ShareService as ShareService participant DB as 数据库 User->>Frontend: 点击"新建项目" Frontend->>User: 显示新建项目表单 User->>Frontend: 填写项目名称、描述 User->>Frontend: 点击"共享素材"按钮 Frontend->>API: GET /api/v1/folders/tree?include_resources=true Note over Frontend,API: 查询文件夹树和项目资源统计 API->>DB: 查询文件夹、项目、资源统计 DB-->>API: 返回树形数据 API-->>Frontend: 返回文件夹树数据 Frontend->>User: 打开共享素材弹窗(树形表格) Note over Frontend,User: 表头:[多选] [名称] [角色] [场景] [道具]
显示文件夹和项目的层级结构 User->>Frontend: 展开文件夹"我的项目" Frontend->>Frontend: 显示文件夹下的子文件夹和项目 User->>Frontend: 展开项目"《西游记》第一季" Frontend->>API: GET /api/v1/projects/{project_id}/resources/summary Note over Frontend,API: 查询项目的具体资源列表 API->>DB: 查询 project_characters/locations/props DB-->>API: 返回资源列表 API-->>Frontend: 返回资源数据 Frontend->>User: 显示项目下的具体资源(角色、场景、道具) alt 勾选整个文件夹 User->>Frontend: 勾选文件夹"我的项目" Frontend->>Frontend: 自动勾选文件夹下的所有项目 Note over Frontend: 记录 share_type=1(整个项目)
为每个项目创建共享记录 else 勾选整个项目 User->>Frontend: 勾选项目"《西游记》第一季" Frontend->>Frontend: 自动勾选项目下的所有资源 Note over Frontend: 记录 share_type=1(整个项目) else 勾选具体资源 User->>Frontend: 勾选角色"孙悟空"、场景"花果山" Frontend->>Frontend: 记录选中的资源 Note over Frontend: 记录 share_type=3(特定资源)
resource_type + resource_id end User->>Frontend: 点击"确认" Frontend->>Frontend: 构建 shared_resources 数组 User->>Frontend: 点击"创建项目" Frontend->>API: POST /api/v1/projects
{name, description, shared_resources} API->>ProjectService: create_project() ProjectService->>DB: INSERT INTO projects DB-->>ProjectService: 返回 project_id ProjectService->>ShareService: create_shares(project_id, shared_resources) loop 处理每个共享配置 alt 勾选了文件夹 ShareService->>DB: 查询文件夹下的所有父项目 DB-->>ShareService: 返回项目列表 loop 为每个项目创建共享记录 ShareService->>DB: INSERT INTO project_resource_shares
(share_type=1, 整个项目) end else 勾选了整个项目 ShareService->>DB: INSERT INTO project_resource_shares
(share_type=1, 整个项目) else 勾选了具体资源 ShareService->>DB: INSERT INTO project_resource_shares
(share_type=3, 特定资源) end end ShareService-->>ProjectService: 返回共享记录列表 ProjectService-->>API: 返回项目和共享信息 API-->>Frontend: 200 OK Frontend->>Frontend: 跳转到项目详情页 Frontend->>User: 显示创建成功 ``` #### 7.2 共享素材弹窗界面设计 **树形结构**: ``` ┌─────────────────────────────────────────────────────────────────────┐ │ 共享素材 [关闭 ✕] │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ [ ] ▶ 📁 我的项目 │ │ [✓] ▼ 📁 西游记系列 │ │ [ ] ▶ 📂 《西游记》第一季 (角色 15, 场景 8, 道具 12) │ │ [✓] ▼ � 《西游记》第二季 (角色 18, 场景 10, 道具 15) │ │ [✓] 👤 孙悟空 │ │ [ ] 👤 猪八戒 │ │ [✓] � 唐僧 │ │ [✓] 🏠 花果山 │ │ [ ] 🏠 高老庄 │ │ [✓] 🎨 金箍棒 │ │ [ ] ▶ 📁 广告项目 │ │ [ ] ▶ 📁 角色资源库 │ │ │ │ [取消] [确认共享 (已选 5 项)] │ └─────────────────────────────────────────────────────────────────────┘ ``` **交互说明**: 1. **层级结构**: - 📁 文件夹(可展开/折叠) - 📂 项目(可展开/折叠,显示资源统计) - 👤 角色资源 - 🏠 场景资源 - 🎨 道具资源 2. **多选逻辑**: - **勾选文件夹**:自动勾选文件夹下的所有项目(不展开) - **勾选项目**:自动勾选项目下的所有资源(不展开) - **勾选具体资源**:只勾选该资源 - **取消勾选**:自动取消子级的所有勾选 3. **展开/折叠**: - 点击 ▶ 展开,显示子级内容 - 点击 ▼ 折叠,隐藏子级内容 - 展开项目时,懒加载项目的具体资源列表 4. **确认按钮**: - 显示已选数量:"确认共享 (已选 5 项)" - 点击后构建 shared_resources 数组 **前端数据结构**: ```typescript interface TreeNode { id: string; type: 'folder' | 'project' | 'character' | 'location' | 'prop'; name: string; parentId?: string; children?: TreeNode[]; // 项目特有字段 characterCount?: number; locationCount?: number; propCount?: number; // 资源特有字段 resourceType?: 1 | 2 | 3; // 1=角色, 2=场景, 3=道具 // UI 状态 expanded: boolean; checked: boolean; indeterminate: boolean; // 半选状态(部分子级被选中) } interface SelectedResource { type: 'folder' | 'project' | 'resource'; id: string; name: string; // 文件夹类型 projectIds?: string[]; // 文件夹下的所有项目ID // 项目类型 shareType: 1 | 3; // 1=整个项目, 3=特定资源 // 资源类型 resourceType?: 1 | 2 | 3; resourceId?: string; } ``` **API 接口**: ```typescript // 1. 查询文件夹树(包含项目资源统计) GET /api/v1/folders/tree?include_resources=true Response: { "success": true, "data": { "folders": [ { "folder_id": "folder-001", "name": "西游记系列", "parent_folder_id": null, "children": [ { "folder_id": "folder-002", "name": "第一季", "parent_folder_id": "folder-001" } ], "projects": [ { "project_id": "project-001", "name": "《西游记》第一季", "character_count": 15, "location_count": 8, "prop_count": 12 } ] } ] } } // 2. 查询项目的具体资源列表(懒加载) GET /api/v1/projects/{project_id}/resources/summary Response: { "success": true, "data": { "characters": [ { "character_id": "char-001", "name": "孙悟空", "role_type": 1 }, { "character_id": "char-002", "name": "猪八戒", "role_type": 2 } ], "locations": [ { "location_id": "loc-001", "name": "花果山" } ], "props": [ { "prop_id": "prop-001", "name": "金箍棒" } ] } } ``` **构建 shared_resources 数组**: ```typescript const buildSharedResources = (selectedNodes: TreeNode[]): SharedResource[] => { const resources: SharedResource[] = []; for (const node of selectedNodes) { if (node.type === 'folder') { // 勾选了文件夹:为文件夹下的每个项目创建共享记录 const projects = getFolderProjects(node.id); for (const project of projects) { resources.push({ source_type: 'project', source_id: project.project_id, share_type: 1, // 整个项目 resource_type: null }); } } else if (node.type === 'project') { // 勾选了项目:创建整个项目的共享记录 resources.push({ source_type: 'project', source_id: node.id, share_type: 1, // 整个项目 resource_type: null }); } else { // 勾选了具体资源:创建特定资源的共享记录 resources.push({ source_type: 'resource', source_id: node.id, share_type: 3, // 特定资源 resource_type: node.resourceType }); } } return resources; }; ``` --- ## 与 ADR 01 的关系 ### 架构层级对比 **ADR 01:父子项目内部共享** ``` 父级项目(parent_project_id = NULL) ├─ project_characters(资源归属父级项目) ├─ project_locations ├─ project_props └─ project_resources ↓ 自动共享 子项目1(parent_project_id = 父级项目ID) └─ screenplay_element_refs(剧本引用父级资源) ↓ 自动共享 子项目2(parent_project_id = 父级项目ID) └─ screenplay_element_refs(剧本引用父级资源) ``` **ADR 02:跨项目体系共享** ``` 项目体系 A(《西游记》第一季) └─ 父级项目 A └─ project_characters(孙悟空、猪八戒、唐僧) ↓ 跨项目共享(通过 project_resource_shares) 项目体系 B(《西游记》第二季) └─ 父级项目 B └─ 可以引用项目体系 A 的资源 ``` ### 核心区别 | 维度 | ADR 01(父子项目共享) | ADR 02(跨项目共享) | |------|---------------------|-------------------| | **共享范围** | 同一项目体系内部(父子项目) | 不同项目体系之间 | | **共享方式** | 自动共享(通过 parent_project_id) | 手动配置(通过 project_resource_shares) | | **权限控制** | 继承父级项目权限 | 独立配置权限(只读/可编辑) | | **数据归属** | 资源归属父级项目 | 资源归属源项目,目标项目引用 | | **使用场景** | 多集项目、续集、番外篇 | 跨项目复用、团队资源库、素材复用 | ### 协同工作 两种共享机制可以同时使用: ``` 项目体系 A(《角色资源库》) └─ 父级项目 A └─ project_characters(100+ 通用角色) ↓ ADR 02:跨项目共享 项目体系 B(《品牌广告》) └─ 父级项目 B ├─ 引用项目体系 A 的角色(通过 project_resource_shares) └─ 自己的角色(project_characters) ↓ ADR 01:父子项目共享 ├─ 子项目 B1(15秒版本) │ └─ 可以使用父级项目 B 的所有资源(包括引用的资源) └─ 子项目 B2(30秒版本) └─ 可以使用父级项目 B 的所有资源(包括引用的资源) ``` --- ## 影响分析 ### 正面影响 1. **提升资源复用效率** - 跨项目复用角色、场景、道具、素材 - 减少重复创建和上传 - 节省存储空间 2. **简化用户体验** - 去掉项目内容类型选择,降低认知负担 - 新建项目流程更简洁 - 灵活的共享粒度(整个项目/特定类型/特定资源) 3. **支持团队协作** - 团队资源库统一管理 - 资源更新自动同步 - 权限控制灵活 4. **扩展性增强** - 为资源市场功能打基础 - 支持跨组织资源共享(未来) - 支持资源版本管理(未来) ### 负面影响 1. **实现成本** - 新增数据库表(project_resource_shares) - 修改项目表(content_type 改为可选) - 查询逻辑复杂度增加(UNION ALL) - 前后端代码调整 - 预计工作量:**18-22人天** 2. **数据迁移风险** - 需要修改 content_type 字段约束 - 可能影响现有功能 - 需要充分测试 3. **查询性能** - UNION ALL 查询可能影响性能 - 需要索引优化 - 需要缓存策略 4. **权限管理复杂度** - 需要检查共享权限 - 需要区分只读和可编辑权限 - 需要处理权限变更 ### 缓解措施 1. **性能优化** - 为所有关联字段创建索引 - 使用 Repository 层封装复杂查询 - 对高频查询结果进行缓存 - 分页查询,避免一次性加载大量数据 2. **风险控制** - 分阶段迁移(先测试环境,再生产环境) - 保留旧字段数据,支持快速回滚 - 充分的单元测试和集成测试 - 灰度发布,逐步开放功能 3. **用户体验** - 提供共享资源的可视化标识(🔒 图标) - 清晰的权限提示(只读/可编辑) - 共享资源来源追溯(显示来源项目名称) - 断开共享的确认提示 --- ## 实现计划 ### Phase 1:数据库迁移(2-3天) **任务**: - [ ] 编写数据库迁移脚本 - 创建 `project_resource_shares` 表 - 修改 `projects.content_type` 字段为可选(ALTER COLUMN DROP NOT NULL) - 创建索引和约束 - [ ] 在测试环境执行迁移 - [ ] 数据一致性校验 **负责人**:后端团队 **风险**:低 ### Phase 2:后端服务层(6-8天) **任务**: - [ ] 创建 Repository 层 - `ProjectResourceShareRepository` - 更新 `ProjectRepository`(content_type 改为可选) - [ ] 创建 Service 层 - `ProjectResourceShareService.create_share()` - `ProjectResourceShareService.get_shared_resources()` - `ProjectResourceShareService.disconnect_share()` - 更新 `ProjectService.create_project()`(支持 shared_resources 参数,content_type 可选) - 更新 `ResourceLibraryService`(支持 source_type 过滤) - [ ] 权限检查逻辑 - `check_share_permission()` - `check_resource_edit_permission()` - [ ] 编写单元测试 **负责人**:后端团队 **风险**:中等 ### Phase 3:API 接口开发(4-5天) **任务**: - [ ] 更新项目创建接口 - `POST /api/v1/projects`(支持 shared_resources 参数) - [ ] 新增共享管理接口 - `POST /api/v1/projects/{project_id}/shares` - `GET /api/v1/projects/{project_id}/shares` - `DELETE /api/v1/projects/{project_id}/shares/{share_id}` - [ ] 更新资源查询接口 - `GET /api/v1/projects/{project_id}/resource-library/*`(支持 source_type 参数) - [ ] 编写 API 文档 - [ ] 集成测试 **负责人**:后端团队 **风险**:中等 ### Phase 4:前端适配(5-6天) **任务**: - [ ] 更新项目创建流程 - 删除项目内容类型选择(content_type) - 新增"共享素材"功能 - 实现共享素材弹窗(选择文件夹/项目/资源) - [ ] 更新资源面板 - 新增 Tabs 切换(全部/我的/共享) - 显示资源来源标识(🔒 图标) - 显示变体标签来源(👁️ 只读,✏️ 可编辑) - 支持为共享资源添加变体标签 - [ ] 新增共享管理页面 - 查看共享列表 - 断开共享 - [ ] 类型定义更新 - `client/src/types/project.ts`(content_type 改为可选) - `client/src/types/resource.ts` - [ ] 前端测试 **负责人**:前端团队 **风险**:中等 ### Phase 5:数据迁移执行(1天) **任务**: - [ ] 备份现有数据 - [ ] 在生产环境执行迁移脚本 - 创建 project_resource_shares 表 - 修改 projects.content_type 为可选 - [ ] 数据一致性校验 - [ ] 监控错误日志 - [ ] 性能监控 **负责人**:运维团队 + 后端团队 **风险**:中等 ### Phase 6:监控与优化(持续) **任务**: - [ ] 监控新接口的调用情况 - [ ] 监控查询性能 - [ ] 收集用户反馈 - [ ] 优化索引和查询 - [ ] 优化缓存策略 **负责人**:全团队 **风险**:低 --- ## 相关文档 ### 架构决策记录 - [ADR 01: 资源归属从剧本级迁移到项目级](01-project-level-resource-ownership.md) - [ADR 003: 剧本拆解与素材关联架构设计](003-screenplay-resource-architecture.md) ### 实现指南 - [跨项目资源共享:选择资源后的入库与关联](../guides/cross-project-resource-sharing-persistence.md)(前端映射、后端处理流程、与 CreateProjectModal 对接) ### 技术栈规范(jointo-tech-stack) - [数据库设计规范](../../.claude/skills/jointo-tech-stack/references/database.md) - [数据库迁移规范](../../.claude/skills/jointo-tech-stack/references/migration.md) - [后端架构规范](../../.claude/skills/jointo-tech-stack/references/backend.md) - [API 设计规范](../../.claude/skills/jointo-tech-stack/references/api-design.md) --- ## 变更记录 **v1.0 (2026-02-07)** - ✅ **初始版本**: - 提出跨项目资源共享方案 - 去掉项目内容类型选择(content_type 改为可选) - 新增项目资源共享表(project_resource_shares) - 支持三种共享粒度(整个项目/特定资源类型/特定资源) - 支持选择文件夹(批量共享文件夹下的所有父项目) - 前端支持切换显示"我的资源"或"共享资源" - 共享资源权限设计: - 资源本身不可编辑(名称、描述等基本信息) - 可以为共享资源添加新的变体标签 - 变体标签归属目标项目,不影响源项目 - 与 ADR 01 的关系说明 - 完整的实现计划(Phase 1-6) --- **状态**:提议中 **决策日期**:2026-02-07 **最后更新**:2026-02-07 **预计工作量**:18-22人天 **预计完成时间**:3-4周