45 KiB
ADR 02: 跨项目资源共享
状态:提议中 日期:2026-02-07 决策者:架构团队
目录
背景
当前架构问题
在现有架构中,项目资源(角色、场景、道具、素材)归属于父级项目(参见 ADR 01),但存在以下问题:
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 的实拍素材
└─ 问题:无法引用,需要重新上传
业务需求分析
| 需求场景 | 当前方案 | 痛点 | 期望方案 |
|---|---|---|---|
| 跨项目复用角色 | 手动重新创建 | 重复劳动,数据不一致 | 直接引用其他项目的角色 |
| 团队资源库 | 复制粘贴 | 无法同步更新,管理混乱 | 共享资源库,统一管理 |
| 素材复用 | 重新上传 | 浪费存储空间,上传耗时 | 引用其他项目的素材 |
| 项目内容类型选择 | 创建时必选(广告/电影/剧集等) | 认知负担,对功能无实质影响 | 去掉内容类型选择,简化创建流程 |
决策
核心方案
- 去掉项目内容类型选择:删除新建项目时的
content_type选择(广告/电影/剧集等),简化创建流程 - 新增跨项目资源共享:在新建项目时,可以选择共享其他项目的素材
设计原则
- 引用模式,而非复制:共享资源通过引用关联,而非复制数据
- 权限控制:只能共享自己有权限访问的项目
- 灵活粒度:支持整个项目、特定资源类型、特定资源的共享
- 资源不可编辑:共享的资源本身不可编辑(名称、描述等基本信息)
- 可添加变体标签:可以为共享资源添加新的变体标签(如"美猴王"),标签归属目标项目
- 可断开连接:可以随时断开共享关系
- 保留 content_type 字段:保留数据库字段用于统计和分析,但不强制用户选择
架构设计
1. 数据库表结构
1.1 项目资源共享表(project_resource_shares)
职责:记录项目之间的资源共享关系
-- 创建表
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),不强制用户选择
-- 将 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:共享整个项目
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:共享特定资源类型(所有角色)
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:共享特定资源("孙悟空")
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. 选择文件夹的处理
需求:用户可以选择文件夹,意思是选择文件夹下的所有父项目
实现:批量创建共享记录
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 查询
-- 查询项目的所有角色(包含共享资源)
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 前端切换显示
前端实现:
// 资源来源类型
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<ResourceSourceType>('all');
return (
<div>
<Tabs value={sourceType} onValueChange={setSourceType}>
<TabsList>
<TabsTrigger value="all">全部资源</TabsTrigger>
<TabsTrigger value="own">我的资源</TabsTrigger>
<TabsTrigger value="shared">共享资源</TabsTrigger>
</TabsList>
</Tabs>
<ResourceList sourceType={sourceType} />
</div>
);
};
5. 权限控制
5.1 共享权限检查
规则:只能共享自己有权限访问的项目
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 共享资源的使用权限
核心规则:
- 资源本身不可编辑:不能修改共享资源的基本信息(名称、描述、角色类型等)
- 可以添加变体标签:可以为共享资源添加新的变体标签
- 变体标签归属目标项目:新增的变体标签属于目标项目,不影响源项目
示例场景:
源项目:《西游记》第一季
└─ 角色:孙悟空
├─ 变体标签:少年(源项目创建)
└─ 变体标签:青年(源项目创建)
目标项目:《西游记》第二季(共享了"孙悟空"角色)
└─ 角色:孙悟空(共享,不可编辑)
├─ 变体标签:少年(源项目,只读)
├─ 变体标签:青年(源项目,只读)
└─ 变体标签:美猴王(目标项目创建,可编辑)✨
实现逻辑:
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 表管理,标签归属于创建它的项目
-- 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关联到资源,资源归属项目 - ✅ 共享资源可以添加标签:目标项目可以为共享资源添加新的变体标签
- ✅ 标签查询需要区分来源:查询时需要区分哪些标签是源项目的,哪些是目标项目的
查询逻辑:
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
请求体:
{
"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
}
]
}
响应:
{
"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: 每页数量
响应:
{
"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
{
"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
{
"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 新建项目时共享资源
时序图:
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: 表头:[多选] [名称] [角色] [场景] [道具]<br/>显示文件夹和项目的层级结构
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(整个项目)<br/>为每个项目创建共享记录
else 勾选整个项目
User->>Frontend: 勾选项目"《西游记》第一季"
Frontend->>Frontend: 自动勾选项目下的所有资源
Note over Frontend: 记录 share_type=1(整个项目)
else 勾选具体资源
User->>Frontend: 勾选角色"孙悟空"、场景"花果山"
Frontend->>Frontend: 记录选中的资源
Note over Frontend: 记录 share_type=3(特定资源)<br/>resource_type + resource_id
end
User->>Frontend: 点击"确认"
Frontend->>Frontend: 构建 shared_resources 数组
User->>Frontend: 点击"创建项目"
Frontend->>API: POST /api/v1/projects<br/>{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<br/>(share_type=1, 整个项目)
end
else 勾选了整个项目
ShareService->>DB: INSERT INTO project_resource_shares<br/>(share_type=1, 整个项目)
else 勾选了具体资源
ShareService->>DB: INSERT INTO project_resource_shares<br/>(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 项)] │
└─────────────────────────────────────────────────────────────────────┘
交互说明:
-
层级结构:
- 📁 文件夹(可展开/折叠)
- 📂 项目(可展开/折叠,显示资源统计)
- 👤 角色资源
- 🏠 场景资源
- 🎨 道具资源
-
多选逻辑:
- 勾选文件夹:自动勾选文件夹下的所有项目(不展开)
- 勾选项目:自动勾选项目下的所有资源(不展开)
- 勾选具体资源:只勾选该资源
- 取消勾选:自动取消子级的所有勾选
-
展开/折叠:
- 点击 ▶ 展开,显示子级内容
- 点击 ▼ 折叠,隐藏子级内容
- 展开项目时,懒加载项目的具体资源列表
-
确认按钮:
- 显示已选数量:"确认共享 (已选 5 项)"
- 点击后构建 shared_resources 数组
前端数据结构:
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 接口:
// 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 数组:
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 的所有资源(包括引用的资源)
影响分析
正面影响
-
提升资源复用效率
- 跨项目复用角色、场景、道具、素材
- 减少重复创建和上传
- 节省存储空间
-
简化用户体验
- 去掉项目内容类型选择,降低认知负担
- 新建项目流程更简洁
- 灵活的共享粒度(整个项目/特定类型/特定资源)
-
支持团队协作
- 团队资源库统一管理
- 资源更新自动同步
- 权限控制灵活
-
扩展性增强
- 为资源市场功能打基础
- 支持跨组织资源共享(未来)
- 支持资源版本管理(未来)
负面影响
-
实现成本
- 新增数据库表(project_resource_shares)
- 修改项目表(content_type 改为可选)
- 查询逻辑复杂度增加(UNION ALL)
- 前后端代码调整
- 预计工作量:18-22人天
-
数据迁移风险
- 需要修改 content_type 字段约束
- 可能影响现有功能
- 需要充分测试
-
查询性能
- UNION ALL 查询可能影响性能
- 需要索引优化
- 需要缓存策略
-
权限管理复杂度
- 需要检查共享权限
- 需要区分只读和可编辑权限
- 需要处理权限变更
缓解措施
-
性能优化
- 为所有关联字段创建索引
- 使用 Repository 层封装复杂查询
- 对高频查询结果进行缓存
- 分页查询,避免一次性加载大量数据
-
风险控制
- 分阶段迁移(先测试环境,再生产环境)
- 保留旧字段数据,支持快速回滚
- 充分的单元测试和集成测试
- 灰度发布,逐步开放功能
-
用户体验
- 提供共享资源的可视化标识(🔒 图标)
- 清晰的权限提示(只读/可编辑)
- 共享资源来源追溯(显示来源项目名称)
- 断开共享的确认提示
实现计划
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}/sharesGET /api/v1/projects/{project_id}/sharesDELETE /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:监控与优化(持续)
任务:
- 监控新接口的调用情况
- 监控查询性能
- 收集用户反馈
- 优化索引和查询
- 优化缓存策略
负责人:全团队 风险:低
相关文档
架构决策记录
实现指南
- 跨项目资源共享:选择资源后的入库与关联(前端映射、后端处理流程、与 CreateProjectModal 对接)
技术栈规范(jointo-tech-stack)
变更记录
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周