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.
 

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 的实拍素材
  └─ 问题:无法引用,需要重新上传

业务需求分析

需求场景 当前方案 痛点 期望方案
跨项目复用角色 手动重新创建 重复劳动,数据不一致 直接引用其他项目的角色
团队资源库 复制粘贴 无法同步更新,管理混乱 共享资源库,统一管理
素材复用 重新上传 浪费存储空间,上传耗时 引用其他项目的素材
项目内容类型选择 创建时必选(广告/电影/剧集等) 认知负担,对功能无实质影响 去掉内容类型选择,简化创建流程

决策

核心方案

  1. 去掉项目内容类型选择:删除新建项目时的 content_type 选择(广告/电影/剧集等),简化创建流程
  2. 新增跨项目资源共享:在新建项目时,可以选择共享其他项目的素材

设计原则

  1. 引用模式,而非复制:共享资源通过引用关联,而非复制数据
  2. 权限控制:只能共享自己有权限访问的项目
  3. 灵活粒度:支持整个项目、特定资源类型、特定资源的共享
  4. 资源不可编辑:共享的资源本身不可编辑(名称、描述等基本信息)
  5. 可添加变体标签:可以为共享资源添加新的变体标签(如"美猴王"),标签归属目标项目
  6. 可断开连接:可以随时断开共享关系
  7. 保留 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()
  • 枚举使用 SMALLINTshare_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 共享资源的使用权限

核心规则

  1. 资源本身不可编辑:不能修改共享资源的基本信息(名称、描述、角色类型等)
  2. 可以添加变体标签:可以为共享资源添加新的变体标签
  3. 变体标签归属目标项目:新增的变体标签属于目标项目,不影响源项目

示例场景

源项目:《西游记》第一季
  └─ 角色:孙悟空
      ├─ 变体标签:少年(源项目创建)
      └─ 变体标签:青年(源项目创建)

目标项目:《西游记》第二季(共享了"孙悟空"角色)
  └─ 角色:孙悟空(共享,不可编辑)
      ├─ 变体标签:少年(源项目,只读)
      ├─ 变体标签:青年(源项目,只读)
      └─ 变体标签:美猴王(目标项目创建,可编辑)✨

实现逻辑

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 项)]   │
└─────────────────────────────────────────────────────────────────────┘

交互说明

  1. 层级结构

    • 📁 文件夹(可展开/折叠)
    • 📂 项目(可展开/折叠,显示资源统计)
    • 👤 角色资源
    • 🏠 场景资源
    • 🎨 道具资源
  2. 多选逻辑

    • 勾选文件夹:自动勾选文件夹下的所有项目(不展开)
    • 勾选项目:自动勾选项目下的所有资源(不展开)
    • 勾选具体资源:只勾选该资源
    • 取消勾选:自动取消子级的所有勾选
  3. 展开/折叠

    • 点击 ▶ 展开,显示子级内容
    • 点击 ▼ 折叠,隐藏子级内容
    • 展开项目时,懒加载项目的具体资源列表
  4. 确认按钮

    • 显示已选数量:"确认共享 (已选 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 的所有资源(包括引用的资源)

影响分析

正面影响

  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:监控与优化(持续)

任务

  • 监控新接口的调用情况
  • 监控查询性能
  • 收集用户反馈
  • 优化索引和查询
  • 优化缓存策略

负责人:全团队 风险:低


相关文档

架构决策记录

实现指南

技术栈规范(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周