Files
worldmodel/plans/CrowdRoom/11_asset_library.md
T
gaojie dbcbdbb59e chore: 为所有 md 文件添加 Hugo front matter
- 处理: 74 个 .md 文件
- 跳过: 0 个(无已存在的 front matter)
- 异常: 3 个(H1 缺失,用文件名兜底)
  - plans/PRISM/.research/readmes/3d-llm.md
  - plans/PRISM/.research/readmes/openmask3d.md
  - plans/PRISM/.research/readmes/openscene.md
2026-05-20 22:42:22 +08:00

65 KiB
Raw Blame History

title, date, draft, tags, categories
title date draft tags categories
CrowdRoom · 资产库(Asset Library)模块设计(v0.2 2026-05-20 false
CrowdRoom
众包
3D 重建
导航
Web
数据结构
worldmodel

CrowdRoom · 资产库(Asset Library)模块设计(v0.2

版本v0.22026-05-19)· 子任务 10 产出 定位:把 01_data_schema.md §3.9 中仅作为"平台 CC0 公共素材池"存在的 public.assets 表,升级为一个支持「平台爬取导入 + 用户 UGC 上传 + Remix 衍生」三源流的资产库子系统;同时逐条兑现 05_object_replacement_handbook.md §15 移交的 AL-1 ~ AL-10 硬契约。

本文档不修改任何 v0.2 既有架构——所有对 01_data_schema.md / 02_api_contract.md / 04_web_app_plan.md / 10_governance.md 的扩展统一以「建议增量」标注;落地需在下一版(v0.3)对应文档中追认。

文档语言:简体中文;DDL / TS / JSON 字段命名:英文(与既有契约对齐)。


0. 阅读导航

你的角色 重点章节
后端 / DBA §2 增量 ALTER · §6 端点表
Edge Function / Worker §4 平台导入管线 · §5.2 UGC 预处理 worker · §5.3 自动初筛
Web 前端(AssetPicker §3 Storage 路径 · §6 E-A3 检索 · §7 排序公式
Web 前端(上传向导) §5.1 5 步向导 · §6 E-A1/E-A2
运营 / 审核员 §5.3 审核工作台 · §8 创作者信任分 · §9 协议
法务 §9 版权 · §10 收藏与署名
QA §14 M1 验收 · §15 风险
Reviewer(一眼对账) §2.6 AL 映射表 · §13 关系矩阵 · §14.2 AL 对照表

1. 资产库定位与产品形态

1.1 一句话定义

"CrowdRoom 资产库 = 平台 CC0 公共资产(Quaternius / Poly Haven / ambientCG 等爬取/导入)+ 用户上传自制资产(UGC,CC0 或 CC-BY 二选一)+ 资产 Remix 衍生(用户基于已有资产二次创作)。"

它对应 05_object_replacement_handbook.md §4 AssetPicker 的唯一上游数据源——AssetPicker 拉的所有家具/材质卡片均来自本资产库;同时把 10_governance.md §4 P-W-4 决策的「平台资产 CC0 白名单」从一张孤立的内部素材表,扩展为一个有用户参与、有创作者飞轮的子社区。

1.2 三类资产对照

# 类型 来源 协议(MVP 准入流程 默认 trust_score AssetPicker 排序权重
T1 平台 CC0 source_type='platform' 运营批量爬取/导入(Quaternius、Poly Haven、ambientCG、Sketchfab CC0 子集) 一律 CC0 自动入库 review_status='approved' 80 高(默认头部)
T2 用户上传 source_type='user_upload' 普通用户走 §5 上传向导 CC0MVP 强制);CC-BY 见 v0.5 ROADMAP 自动初筛 + 人工 24-72 h 审 30(新手)/ 50Trusted Creator 跳过人工) 中(按 §7 综合公式)
T3 Remix 衍生 source_type='remix_of' + parent_asset_id 用户 fork 已有资产 → 改 PBR / mesh → 发布 继承父协议(CC0 → 可选;CC-BY → 必须 CC-BY 同 T2 继承父 × 0.8 中低(避免刷量)

MVP 一致性声明(与 10_governance.md §4 P-W-4 对齐):

  • MVP 阶段 T1 全 CC0T2 / T3 用户上传强制 CC004_web_app_plan.md §5.4 顶部「仅显示 CC0」开关始终为 ON 且不可关,等同于"全库即 CC0"
  • v0.5DAU > 5 000)后才开放 CC-BY 4.0 上传选项;CC-BY-SA / CC-BY-NC / 付费素材永不接(避免 Remix 树协议污染)
  • 本资产库不引入 CrowdRoom 平台默认的 CC BY-NC 协议——资产是给所有 Remix 重用的"乐高积木",必须 NC-free

1.3 与 v0.2 既有设计的边界

边界 v0.2 既有 本文档扩展
public.assets 表行数 1 个 Storage 文件 + 9 列字段 仍是 1 张表,仅 ALTER 追加 13 列(§2
资产上传能力 无(仅运营 service_role 写) 新增 8 个 E-A1~E-A8 端点(§6
AssetPicker 检索维度 kind / semantic_class / tags 追加 style_tag / volume_m3 / dominant_color / source_type / license
治理流程 仅"内部抽查"10_governance.md §4 P-W-4 步骤 3 复用 10_governance.md §2.1 既有 NSFW + 敏感词自动审核流,新增「资产审核队列」分支

2. 数据模型增强(AL-1 / AL-2 / AL-4 / AL-8 落地)

2.1 设计原则

  1. 不破坏 v0.201_data_schema.md §3.9 既有 9 列字段(id / kind / name / semantic_class / glb_path / pbr / thumbnail_path / license / source_url / tags / created_at保持不动;本节仅做 ALTER TABLE ... ADD COLUMN IF NOT EXISTS 增量
  2. AL 契约逐字段落地 — 每一列都对应 AL-1 ~ AL-10 中的某一条(见 §2.6 映射表)
  3. 三张支撑表asset_review_queue(审核工单)、asset_reports(举报)、asset_collections(用户收藏/官方风格集)拆为独立表,避免 assets 表膨胀

2.2 assets 表增量 ALTER

-- 建议增量:可在 Supabase SQL Editor 中按顺序执行
-- 与 01_data_schema.md §3.9 既有字段完全兼容,不修改既有列

ALTER TABLE public.assets
  -- ============= AL-1: 朝向 & 包围盒(家具替换对齐契约) =============
  ADD COLUMN IF NOT EXISTS anchor_local jsonb NOT NULL DEFAULT '{}'::jsonb,
    -- 结构:{"position":[x,y,z],"forward_axis":"+Z","up_axis":"+Y"}
    -- position 单位米;forward_axis / up_axis ∈ {+X,-X,+Y,-Y,+Z,-Z}
  ADD COLUMN IF NOT EXISTS bbox_local jsonb NOT NULL DEFAULT '{}'::jsonb,
    -- {"min":[x,y,z],"max":[x,y,z]} 单位米;坐标系右手 +Y 向上(与 glTF 一致)

  -- ============= AL-4: 体量 & 主色(检索维度) =============
  ADD COLUMN IF NOT EXISTS volume_m3 numeric(10,3) NOT NULL DEFAULT 0,
    -- 由 bbox_local 计算的物化列(worker 写入)
  ADD COLUMN IF NOT EXISTS dominant_color text,
    -- '#RRGGBB'LAB 平均后量化);material 类资产用 base_color
    -- furniture 类资产用 thumb_512 的中心 256×256 区域 K-Means(K=3) 最大簇

  -- ============= AL-3: 风格标签 =============
  ADD COLUMN IF NOT EXISTS style_tag text,
    -- enum: 'nordic' | 'industrial' | 'chinese' | 'minimal'
    --     | 'retro' | 'wabi_sabi' | 'bauhaus'
    -- 1 件资产仅 1 个主风格;多风格通过 tags[] 兼顾

  -- ============= AL-7: 协议(v0.2 已有 license,此处扩展约束位置) =============
  -- license 列已存在;仅追加 CHECK 约束(见 §2.3)

  -- ============= 三源流溯源 =============
  ADD COLUMN IF NOT EXISTS source_type text NOT NULL DEFAULT 'platform',
    -- enum: 'platform' | 'user_upload' | 'remix_of'
  ADD COLUMN IF NOT EXISTS parent_asset_id uuid REFERENCES public.assets(id),
    -- 仅 source_type='remix_of' 时非空
  ADD COLUMN IF NOT EXISTS uploader_id uuid REFERENCES public.users(id),
    -- 平台资产为 NULLuser_upload / remix_of 必填

  -- ============= 审核状态机 =============
  ADD COLUMN IF NOT EXISTS reviewer_id uuid REFERENCES public.users(id),
  ADD COLUMN IF NOT EXISTS review_status text NOT NULL DEFAULT 'approved',
    -- enum: 'pending' | 'approved' | 'rejected' | 'withdrawn'
    -- 平台 import 默认 'approved';用户上传默认 'pending'
  ADD COLUMN IF NOT EXISTS rejection_reason text,
    -- 仅 review_status='rejected' 时非空;通过站内信通知作者

  -- ============= 信任分 & 使用统计 =============
  ADD COLUMN IF NOT EXISTS trust_score smallint NOT NULL DEFAULT 50
    CHECK (trust_score BETWEEN 0 AND 100),
    -- 影响 §7 AssetPicker 排序权重;详见 §8
  ADD COLUMN IF NOT EXISTS download_count int NOT NULL DEFAULT 0,
    -- E-A3 详情命中 +1
  ADD COLUMN IF NOT EXISTS use_count int NOT NULL DEFAULT 0,
    -- 被 remix_overlay 引用次数(trigger 自增,见 §2.5

  -- ============= 标签(已存在 tags,此处补充注释) =============
  -- tags text[]v0.2 既有;本文档要求 worker 自动从 CLIP/缩略图提取 ≥ 3 条
  -- 例如 ["wood","mid_century","oak","two_seater"]

  -- ============= 软删(与 v0.2 G-9 对齐) =============
  ADD COLUMN IF NOT EXISTS deleted_at timestamptz NULL,
  ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();

2.3 license 列 CHECK 约束(AL-7 落地)

-- 建议增量:把 v0.2 既有 `license text not null default 'CC0'` 收紧为白名单
ALTER TABLE public.assets
  ADD CONSTRAINT assets_license_whitelist
  CHECK (license IN ('CC0', 'CC-BY-4.0'));
  -- 'CC-BY-SA' / 'CC-BY-NC' / 'commercial' 永不允许(与 P-W-4 对齐)

2.4 索引(AL-4 / AL-10 落地:复合检索 < 200 ms

-- tags 数组 GINAL-4 强制要求;v0.2 §3.9 已存在 assets_tags 索引,此处幂等重建)
CREATE INDEX IF NOT EXISTS assets_tags_gin
  ON public.assets USING gin (tags);

-- 单维 btree
CREATE INDEX IF NOT EXISTS assets_semantic_class_btree
  ON public.assets (semantic_class);
CREATE INDEX IF NOT EXISTS assets_volume_btree
  ON public.assets (volume_m3);
CREATE INDEX IF NOT EXISTS assets_style_btree
  ON public.assets (style_tag);

-- 复合:source_type × review_statusAssetPicker 默认仅拉 approved
CREATE INDEX IF NOT EXISTS assets_source_review_btree
  ON public.assets (source_type, review_status)
  WHERE deleted_at IS NULL;

-- 信任分排序(部分索引仅覆盖 approved)
CREATE INDEX IF NOT EXISTS assets_trust_desc
  ON public.assets (trust_score DESC, use_count DESC)
  WHERE review_status = 'approved' AND deleted_at IS NULL;

-- name 模糊搜索(与 rooms 一致使用 pg_trgm
CREATE INDEX IF NOT EXISTS assets_name_trgm
  ON public.assets USING gin (name gin_trgm_ops);

2.5 RLS 补丁(覆盖既有 §3.9 policy)

-- v0.2 §3.9 只有 assets_select_all 一条 policy;本文档扩展为 4 条
ALTER TABLE public.assets ENABLE ROW LEVEL SECURITY;

-- SELECT:所有人可见 approved 的;owner 可见自己的 pending/rejected/withdrawn
DROP POLICY IF EXISTS assets_select_all ON public.assets;
CREATE POLICY assets_select_public ON public.assets FOR SELECT USING (
  deleted_at IS NULL
  AND (
    review_status = 'approved'
    OR uploader_id = auth.uid()
  )
);

-- INSERT:登录用户可插入 pendinguploader_id 必须 = auth.uid()
CREATE POLICY assets_insert_user ON public.assets FOR INSERT WITH CHECK (
  uploader_id = auth.uid()
  AND source_type IN ('user_upload', 'remix_of')
  AND review_status = 'pending'
);

-- UPDATE:仅 owner 在 pending 期可改元数据;approved 后只读(service_role 走 §5.3 审核工具走 RPC
CREATE POLICY assets_update_owner_pending ON public.assets FOR UPDATE USING (
  uploader_id = auth.uid() AND review_status = 'pending'
) WITH CHECK (
  uploader_id = auth.uid() AND review_status = 'pending'
);

-- DELETEowner 可在 pending 时撤回(软删);approved 后走 E-A8 走 service_role
CREATE POLICY assets_delete_owner_pending ON public.assets FOR DELETE USING (
  uploader_id = auth.uid() AND review_status = 'pending'
);

-- use_count 自增触发器(被 remix_overlay 引用时 +1
CREATE FUNCTION bump_asset_use_count() RETURNS trigger AS $$
DECLARE
  op_record jsonb;
  ref_asset_id uuid;
BEGIN
  -- 扫描 new.overlay->ops[] 中 asset_id 字段
  FOR op_record IN SELECT * FROM jsonb_array_elements(NEW.overlay->'ops')
  LOOP
    ref_asset_id := (op_record->>'asset_id')::uuid;
    IF ref_asset_id IS NOT NULL THEN
      UPDATE public.assets
        SET use_count = use_count + 1, updated_at = now()
        WHERE id = ref_asset_id;
    END IF;
  END LOOP;
  RETURN NEW;
END $$ LANGUAGE plpgsql;

CREATE TRIGGER remixes_bump_assets_use
  AFTER INSERT ON public.remixes
  FOR EACH ROW EXECUTE FUNCTION bump_asset_use_count();

2.6 AL-1 ~ AL-10 字段映射表(对账核心

契约 ID AL 要求一句话 落地字段 / 索引 文档章节
AL-1 家具资产必须有 anchor + forward/up + bbox anchor_local jsonbbbox_local jsonb(§2.2 §2.2 / §4 / §5.2
AL-2 公开资产 semantic_class 非空率 ≥ 99% semantic_classv0.2 既有)+ assets_semantic_class_btree;§5.1.3 上传向导强制选 + §4 worker import 时 fallback unknown 标 reject §2.4 / §5.1
AL-3 tags[] 必含风格 tag(7 类),每件至少 1 个 style_tag text(§2.2 enum 7 类)+ assets_style_btreeworker 自动从 CLIP 推断 + 上传向导默认必填 §2.2 / §4 / §5.1
AL-4 物化 volume_m3 + dominant_colorGIN tags volume_m3 numeric + dominant_color text + assets_tags_gin + assets_volume_btree(§2.2 / §2.4 §2.2 / §6 E-A3
AL-5 .glb ≤ 1 MB(家具)/ ≤ 200 KB(材质贴图集) §3 Storage 路径硬约束 + §5.2 worker 步骤 W-5 拒收超标 §3 / §5.2
AL-6 必须 128×128 webp 缩略图 thumb_128.webp(§3 路径) + §5.2 worker 步骤 W-9 自动生成 §3 / §5.2
AL-7 license 二选一 `CC0 CC-BY` license CHECK 白名单(§2.3)+ §9 协议管理
AL-8 材质 pbr jsonb 必含 base_color_tex/normal_tex/roughness/metallic pbr jsonbv0.2 既有)+ §5.2 worker 步骤 W-7 schema 校验 §5.2 W-7
AL-9 asset_id ↔ glb_path 不可重定向 §3 路径硬规则 public/assets/{asset_id}/model.glbasset_id 永不复用、glb_path 不允许 UPDATEpolicy 限制) §3
AL-10 GET /assets?bbox_filter=&style=&semantic= 复合检索 E-A3 端点(§6) + §2.4 复合索引 §6 / §7

结论AL-1 ~ AL-10 每一条都对应到至少一个具体字段/索引/Worker 步骤/端点10/10 闭环

2.7 支撑表 DDL

2.7.1 asset_review_queue(审核工单)

CREATE TYPE asset_review_action AS ENUM (
  'approve', 'approve_with_edits', 'reject', 'escalate'
);

CREATE TABLE public.asset_review_queue (
  id              uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  asset_id        uuid NOT NULL REFERENCES public.assets(id) ON DELETE CASCADE,
  enqueued_at     timestamptz NOT NULL DEFAULT now(),
  priority        smallint NOT NULL DEFAULT 5,
    -- 1=最高(举报触发的复审)/ 5=新上传 / 9=平台 import 抽查
  auto_signals    jsonb NOT NULL DEFAULT '{}'::jsonb,
    -- {nsfw_score: 0.2, triangle_count: 38912, pii_detected: false, ...}
  reviewer_id     uuid REFERENCES public.users(id),
  reviewed_at     timestamptz,
  action          asset_review_action,
  notes           text,
  sla_deadline    timestamptz NOT NULL DEFAULT (now() + INTERVAL '72 hours')
);

CREATE INDEX queue_pending ON public.asset_review_queue (priority, enqueued_at)
  WHERE reviewed_at IS NULL;
CREATE INDEX queue_asset ON public.asset_review_queue (asset_id);

ALTER TABLE public.asset_review_queue ENABLE ROW LEVEL SECURITY;
-- 仅有 'reviewer' role 或 service_role 可访问(service_role 绕过 RLS
-- reviewer role 通过 auth.jwt() ->> 'role' 区分
CREATE POLICY queue_reviewer_only ON public.asset_review_queue FOR ALL
  USING (auth.jwt() ->> 'role' = 'reviewer');

2.7.2 asset_reports(侵权 / 违规举报)

CREATE TABLE public.asset_reports (
  id              uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  asset_id        uuid NOT NULL REFERENCES public.assets(id) ON DELETE CASCADE,
  reporter_id     uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
  reason          text NOT NULL
    CHECK (reason IN ('copyright', 'nsfw', 'low_quality', 'duplicate',
                      'wrong_metadata', 'malicious', 'other')),
  detail          text,
  created_at      timestamptz NOT NULL DEFAULT now(),
  resolved_at     timestamptz,
  resolution      text
    CHECK (resolution IN ('confirmed_violation', 'false_report', 'duplicate_report')),
  UNIQUE (asset_id, reporter_id)  -- 同人对同资产 24h 内只能举报一次(业务层强制)
);

CREATE INDEX reports_asset ON public.asset_reports (asset_id);
CREATE INDEX reports_pending ON public.asset_reports (created_at)
  WHERE resolved_at IS NULL;

ALTER TABLE public.asset_reports ENABLE ROW LEVEL SECURITY;
CREATE POLICY reports_insert_user ON public.asset_reports FOR INSERT
  WITH CHECK (reporter_id = auth.uid());
CREATE POLICY reports_select_owner ON public.asset_reports FOR SELECT
  USING (reporter_id = auth.uid() OR auth.jwt() ->> 'role' = 'reviewer');

联动:累计 ≥ 3 条独立 asset_reports 行未 resolved 时,触发 assets.review_status 自动改为 pending + 工单进 asset_review_queue 重审(priority=1),与 10_governance.md §2.2「3 独立举报自动隐藏」对齐。

2.7.3 asset_collections(用户收藏夹 / 官方风格集)

CREATE TABLE public.asset_collections (
  id              uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  owner_id        uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
  name            text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 80),
  description     text,
  is_public       boolean NOT NULL DEFAULT false,
  is_official     boolean NOT NULL DEFAULT false,
    -- true = 审核员打包的「官方风格集」,发现页可展示
  asset_ids       uuid[] NOT NULL DEFAULT ARRAY[]::uuid[],
  cover_color     text,
  created_at      timestamptz NOT NULL DEFAULT now(),
  updated_at      timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX collections_owner ON public.asset_collections (owner_id);
CREATE INDEX collections_official ON public.asset_collections (is_official)
  WHERE is_official = true;

ALTER TABLE public.asset_collections ENABLE ROW LEVEL SECURITY;
CREATE POLICY collections_select ON public.asset_collections FOR SELECT
  USING (is_public OR is_official OR owner_id = auth.uid());
CREATE POLICY collections_write_owner ON public.asset_collections FOR ALL
  USING (owner_id = auth.uid()) WITH CHECK (owner_id = auth.uid());
-- is_official 仅 service_role 可写

2.7.4 creator_profiles(创作者信任分扩展)

-- 复用 public.users,新增创作者维度独立列
ALTER TABLE public.users
  ADD COLUMN IF NOT EXISTS creator_trust_score smallint NOT NULL DEFAULT 50
    CHECK (creator_trust_score BETWEEN -100 AND 100),
  ADD COLUMN IF NOT EXISTS creator_badge text
    CHECK (creator_badge IN ('trusted', 'verified', 'pro')),
  ADD COLUMN IF NOT EXISTS creator_uploaded_count int NOT NULL DEFAULT 0,
  ADD COLUMN IF NOT EXISTS creator_use_count_total int NOT NULL DEFAULT 0;

CREATE INDEX users_creator_trust ON public.users (creator_trust_score DESC)
  WHERE deleted_at IS NULL AND creator_uploaded_count > 0;

2.8 字段总览

数量
新增 assets 13 列
新增 users 列(创作者扩展) 4 列
新增表 3 张(asset_review_queueasset_reportsasset_collections
新增索引 6assets+ 5(支撑表)+ 1(users 创作者)= 12 个
新增 trigger 2 个(bump_asset_use_count + assets_glb_path_lock,后者见 §3.3
新增 RLS policy 4assets+ 7(支撑表)= 11 条

3. Storage 目录与命名约定(AL-5 / AL-6 / AL-9 落地)

3.1 公开 bucket 目录扩展

public/assets/{asset_id}/                # asset_id = uuid v4 32 位 hex(无 -
  ├── model.glb            # 主几何,KTX2 + Meshopt 压缩
  │                        #   家具 ≤ 1 MB;材质贴图集 ≤ 200 KBAL-5
  ├── thumb_128.webp       # 128×128 列表缩略图(AL-6
  ├── preview_512.webp     # 512×512 详情页预览
  ├── preview_3d.glb       # 可选,更高 LOD 的预览版(≤ 300 KB,仅 trust>70 资产生成)
  ├── attribution.txt      # CC-BY 必须;CC0 可选(保留原作者署名)
  └── manifest.json        # 资产元信息冗余存储(与 assets 行字段对齐,便于离线检索)

01_data_schema.md §4 的 rooms/ / private/rooms/ 双 bucket 模型并列:本目录复用 v0.2 既有的 public bucket(与 rooms/ 同 bucket,仅顶层 prefix 不同),无需新建 bucket。

3.2 命名硬规则(AL-9 落地)

规则 说明
R-1 asset_id 永不复用 资产被 withdrawnid 不释放;新上传始终拿 gen_random_uuid()
R-2 model.glb 路径不可重定向 glb_path = 'public/assets/{asset_id}/model.glb' 写死;trigger assets_glb_path_lock(§3.3)拒绝任何 UPDATE 改 glb_pathservice_role 也不放行,避免误操作)
R-3 thumb_128.webp 命名固定 不允许 thumb1.webp / cover.webp 等变体——AssetPicker 拼路径用
R-4 .tmp 隔离 Worker 写入时先到 public/assets/{asset_id}/.tmp/,转码完成 rename 到正式路径,避免半成品被读
R-5 attribution.txt UTF-8 多个原作者用 \n 分隔;格式:"<creator_name>" <https://url> CC-BY-4.0

3.3 glb_path 不变性的 SQL 兜底

-- 建议增量:禁止任何客户端改 glb_path(包括 owner / service_role
CREATE FUNCTION assets_glb_path_immutable() RETURNS trigger AS $$
BEGIN
  IF NEW.glb_path IS DISTINCT FROM OLD.glb_path THEN
    RAISE EXCEPTION 'glb_path is immutable (AL-9); asset_id=%', OLD.id
      USING ERRCODE = 'check_violation';
  END IF;
  RETURN NEW;
END $$ LANGUAGE plpgsql;

CREATE TRIGGER assets_glb_path_lock
  BEFORE UPDATE ON public.assets
  FOR EACH ROW EXECUTE FUNCTION assets_glb_path_immutable();

4. 平台资产导入管线(爬虫 / 外部库映射)

4.1 来源清单

来源 协议 类型 MVP 数量目标 P1 目标 备注
Quaterniusgithub.com/quaternius CC0 家具 / 装饰 300 件 2 000 件 Furniture KitPlant Pack
Poly Havenpolyhaven.com CC0 材质 PBR 150 套 800 套 base/normal/roughness/metallic/ao 五贴图集
ambientCG CC0 材质 PBR 100 套 500 套 同上
Sketchfab CC0 子集 CC0 家具 50 件 200 件 每件单独人工 verify 协议Sketchfab 标错率高)
Free3D CC0 子集 CC0 家具 20 件 100 件 P2 再放量

4.2 导入 worker 流程图

flowchart TD
  Src[Quaternius repo / Poly Haven API / ambientCG zip] --> Fetch[1 Fetch 原始文件]
  Fetch --> Detect{2 格式检测}
  Detect -- .glb --> Compress
  Detect -- .fbx / .obj --> Convert[3 gltf-transform convert]
  Convert --> Compress[4 KTX2 + Meshopt 压缩]
  Compress --> Limit{5 size check}
  Limit -- 超 1 MB --> RetryLOD[6a 再 decimate 到 50k tri]
  Limit -- pass --> Bbox[6b 计算 bbox / volume]
  RetryLOD --> Bbox
  Bbox --> Color[7 提取 dominant_color K-Means]
  Color --> Tags[8 CLIP 推断 tags + style_tag]
  Tags --> Thumb[9 headless three.js 生成 thumb_128 / preview_512]
  Thumb --> Attr[10 写 attribution.txt 即便 CC0]
  Attr --> Upload[11 上传到 public/assets/uuid/]
  Upload --> DB[12 INSERT assets row source_type=platform review_status=approved trust_score=80]
  DB --> Done[完成]

4.3 导入脚本骨架(TypeScript / Node.js,从 Quaternius 批量)

// scripts/import_quaternius.ts
//
// 用法:pnpm tsx scripts/import_quaternius.ts --pack=furniture-kit --limit=50
//
// 前置依赖(package.json):
//   "@gltf-transform/core", "@gltf-transform/extensions", "@gltf-transform/functions",
//   "@supabase/supabase-js", "sharp", "open-clip-ts"
// 环境变量:SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY / QUATERNIUS_REPO_DIR

import { NodeIO } from '@gltf-transform/core';
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
import { weld, dedup, meshopt, textureCompress } from '@gltf-transform/functions';
import { createClient } from '@supabase/supabase-js';
import sharp from 'sharp';
import { readdir } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
import { join } from 'node:path';

const SEMANTIC_FROM_FILENAME: Record<string, string> = {
  bed: 'bed', chair: 'chair', table: 'table', sofa: 'sofa', desk: 'table',
  shelf: 'storage', dresser: 'storage', tv: 'television',
};
const STYLE_FROM_PACK: Record<string, string> = {
  'furniture-kit': 'minimal', 'midcentury': 'retro', 'modern': 'nordic',
};

const supa = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);

async function importOne(srcPath: string, pack: string) {
  const asset_id = randomUUID().replace(/-/g, '');
  const name = srcPath.split('/').pop()!.replace(/\.glb$/, '');

  // 1. 加载 + 压缩
  const doc = await io.read(srcPath);
  await doc.transform(
    weld(), dedup(),
    meshopt({ level: 'medium' }),
    textureCompress({ encoder: 'ktx2', targetFormat: 'auto', quality: 90 })
  );
  const glb = await io.writeBinary(doc);
  if (glb.byteLength > 1_000_000) {
    console.warn(`[skip ${name}] ${glb.byteLength}B > 1 MB`); return;
  }

  // 2. bbox / volume
  const scene = doc.getRoot().listScenes()[0];
  let min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
  scene.traverse((node) => {
    const mesh = node.getMesh(); if (!mesh) return;
    mesh.listPrimitives().forEach(p => {
      const pos = p.getAttribute('POSITION'); if (!pos) return;
      for (let i = 0; i < pos.getCount(); i++) {
        const v = [0, 0, 0]; pos.getElement(i, v);
        for (let k = 0; k < 3; k++) {
          if (v[k] < min[k]) min[k] = v[k];
          if (v[k] > max[k]) max[k] = v[k];
        }
      }
    });
  });
  const volume_m3 = (max[0]-min[0]) * (max[1]-min[1]) * (max[2]-min[2]);

  // 3. anchor = bbox 底面中心;forward/up 默认 +Z/+YAL-1
  const anchor_local = {
    position: [(min[0]+max[0])/2, min[1], (min[2]+max[2])/2],
    forward_axis: '+Z', up_axis: '+Y',
  };

  // 4. dominant_color(用首个材质的 baseColorFactor 近似;正式版用 K-Means
  const mat = doc.getRoot().listMaterials()[0];
  const bc = mat?.getBaseColorFactor() ?? [0.6, 0.6, 0.6, 1];
  const dominant_color = '#' + bc.slice(0, 3)
    .map((c: number) => Math.round(c * 255).toString(16).padStart(2, '0')).join('');

  // 5. CLIP tags(伪代码;正式实现挂 open-clip-ts 对预设词典 zero-shot 分类)
  const tags = await clipExtractTags(srcPath);
  const matchedKey = Object.keys(SEMANTIC_FROM_FILENAME)
    .find(k => name.toLowerCase().includes(k));
  const semantic_class = matchedKey ? SEMANTIC_FROM_FILENAME[matchedKey] : 'storage';
  const style_tag = STYLE_FROM_PACK[pack] ?? 'minimal';

  // 6. 缩略图(假设外部已渲好同名 .png;正式版走 headless three.js
  const thumb128 = await sharp(srcPath.replace('.glb', '.png'))
    .resize(128, 128).webp({ quality: 85 }).toBuffer();
  const preview512 = await sharp(srcPath.replace('.glb', '.png'))
    .resize(512, 512).webp({ quality: 90 }).toBuffer();

  // 7. attribution.txtCC0 也保留来源 URL
  const attribution =
    `Quaternius "${name}" — CC0 Public Domain\n` +
    `https://github.com/quaternius/${pack}\n`;

  // 8. 上传 Storage
  const root_path = `assets/${asset_id}`;
  await Promise.all([
    supa.storage.from('public').upload(`${root_path}/model.glb`, glb,
      { contentType: 'model/gltf-binary' }),
    supa.storage.from('public').upload(`${root_path}/thumb_128.webp`, thumb128,
      { contentType: 'image/webp' }),
    supa.storage.from('public').upload(`${root_path}/preview_512.webp`, preview512,
      { contentType: 'image/webp' }),
    supa.storage.from('public').upload(`${root_path}/attribution.txt`,
      Buffer.from(attribution, 'utf-8'), { contentType: 'text/plain' }),
  ]);

  // 9. INSERT assets 行(service_role 绕过 RLS
  const { error } = await supa.from('assets').insert({
    id: asset_id, kind: 'furniture', name, semantic_class,
    glb_path: `public/${root_path}/model.glb`,
    thumbnail_path: `public/${root_path}/thumb_128.webp`,
    license: 'CC0',
    source_url: `https://github.com/quaternius/${pack}`,
    tags, style_tag,
    anchor_local, bbox_local: { min, max },
    volume_m3, dominant_color,
    source_type: 'platform', review_status: 'approved', trust_score: 80,
  });
  if (error) throw error;
  console.log(`[ok] ${name}${asset_id} (${(glb.byteLength/1024).toFixed(1)} KB)`);
}

async function clipExtractTags(p: string): Promise<string[]> {
  // 伪代码:真实实现挂 open-clip-ts,对 ['wood','metal','fabric','glass',...] 做多标签分类
  return ['wood', 'natural', 'mid_century'];
}

(async () => {
  const args = Object.fromEntries(
    process.argv.slice(2).map(s => s.replace(/^--/, '').split('='))
  );
  const dir = join(process.env.QUATERNIUS_REPO_DIR!, args.pack);
  const files = (await readdir(dir)).filter(f => f.endsWith('.glb'));
  for (const f of files.slice(0, parseInt(args.limit ?? '50', 10))) {
    try { await importOne(join(dir, f), args.pack); }
    catch (e) { console.error(`[fail] ${f}: ${(e as Error).message}`); }
  }
})();

审计要点:导入脚本以 SUPABASE_SERVICE_ROLE_KEY 运行,绕过 RLS;仅运营人员可在受控环境执行;每次执行写一行到运营内部审计表(建议增量;不展开 DDL)。

4.4 CC-BY 资产的差异化处理(v0.5 起开放)

步骤 CC0MVP CC-BYv0.5+
attribution.txt 可选 必须,格式 "<creator>" <https://url> CC-BY-4.0
AssetPicker 卡片 不显示作者 卡片底部小字「by {creator}」
Remix 引用时 无追加 overlay 自动追加 attribution: { asset_id, creator, license }
Remix 详情页 不显示 页脚「Assets in this remix」区块列出全部 CC-BY 资产 + 作者署名

5. 用户上传 UGC 资产的完整流程(最核心章节)

5.1 上传入口与 UX

5.1.1 入口路由(建议增量 to 04_web_app_plan.md §1.1 路由表)

ID 路由 描述
R-18(建议增量) /me/assets 用户主页「我的资产」Tab(列表 + 状态徽章)
R-18a(建议增量) /me/assets/new 上传向导(5 步)
R-18b(建议增量) /me/assets/[asset_id]/edit 编辑元数据(仅 review_status='pending' 时可改)
R-19(建议增量) /assets 公共资产浏览页(AssetPicker 的独立网页版)
R-19a(建议增量) /assets/[asset_id] 资产详情页(含使用此资产的 Remix 列表)
R-17b(建议增量) /admin/assets 审核员资产工作台(扩展自既有 R-17 /admin/reports

5.1.2 入口可见性

位置 CTA 文案 触达条件
发现页 / 底部 「成为创作者 — 上传你的第一件资产 →」 登录用户且 creator_uploaded_count=0
用户主页 /u/[handle] 顶部 Tab「我的资产」 仅自己可见上传按钮;他人看到的是已 approved 资产列表
Remix 编辑器 AssetPicker 底部 「找不到合适的?上传自己的 →」按钮 登录用户始终可见
Header 用户菜单 「我的资产」菜单项 登录用户始终可见

5.1.3 上传向导 5 步(/me/assets/new

flowchart LR
  S1[Step1 选文件] --> S2[Step2 预览+确认朝向]
  S2 --> S3[Step3 填元数据]
  S3 --> S4[Step4 协议同意]
  S4 --> S5[Step5 提交 pending]
  S5 -.审核24-72h.-> Done[approved or rejected]
步骤 UI 元素 校验
1 选文件 Dropzone + 文件选择器;接受 .glb / .usdz / .gltf+bin+textures.zip 单文件 ≤ 10 MBworker 端再压到 ≤ 1 MB
2 自动预览 R3F <Canvas> 内嵌即时渲染 + OrbitControls;下方 forward_axis / up_axis 6 选 1 单选器 必须确认朝向,否则下一步按钮禁用
3 元数据 name(≤ 80 字符)/ kind 单选(家具/材质)/ semantic_class 16 选 1(家具)或 6 选 1(材质)/ style_tag 7 选 1 / tags ≤ 8 个 semantic_class、style_tag 必填(AL-2 / AL-3
4 协议 单选「我声明此资产为原创或合法授权」+「我同意按 CC0 发布(MVP 强制)」 两个 checkbox 必勾
5 提交 点击「提交审核」→ 调 E-A1 拿 presigned URL → PUT Storage → 调 E-A2 标 complete → 入审核队列

5.2 自动化预处理 Worker

触发自 E-A2 POST /functions/v1/asset-upload-complete复用 02_api_contract.md §3 既有 Transcode Worker 的基础设施(同 Cloud Run / Docker / 重试策略),但走独立的 asset- 分支函数集。

5.2.1 完整 18 步序列图

sequenceDiagram
    autonumber
    participant U as 用户
    participant Web as Web 上传向导
    participant Edge as Edge Function
    participant Stg as Supabase Storage
    participant DB as Postgres
    participant W as Asset Worker
    participant Mod as Auto Moderator
    participant Q as asset_review_queue
    participant R as Reviewer

    U->>Web: 完成 5 步向导
    Web->>Edge: POST asset-upload-init filename size mime
    Edge->>DB: INSERT assets row review_status=pending source_type=user_upload
    Edge->>Stg: 签 presigned PUT URL TTL=300s
    Edge-->>Web: asset_id + presigned_url
    Web->>Stg: PUT raw file 到 tmp upload bin
    Web->>Edge: POST asset-upload-complete asset_id metadata
    Edge->>W: enqueue process_asset
    W->>Stg: GET tmp upload bin
    W->>W: 格式校验 拒 obj fbx 通过 glb usdz
    W->>W: 安全扫描 embedded scripts malicious buffer
    W->>W: 如 usdz 转 glb usdzconvert
    W->>W: KTX2 Meshopt 压缩
    W->>W: 三角面数检查 家具 50k 上限 超则 reject
    W->>W: 自动计算 bbox volume anchor 默认底面中心
    W->>W: 提取 dominant_color tags CLIP
    W->>W: PBR 完整性校验 材质必含 base_color normal roughness metallic AL-8
    W->>W: 生成 thumb_128 preview_512 webp
    W->>Mod: NSFW NSFWJS 与 PII 人脸检测
    alt 命中 NSFW>=0.9 或 含人脸
        Mod-->>DB: UPDATE assets SET review_status=rejected rejection_reason
        Mod->>U: 站内信通知
    else 通过
        W->>Stg: 上传 model glb thumb preview manifest
        W->>DB: UPDATE assets 完整元数据 review_status 保持 pending
        W->>Q: INSERT asset_review_queue priority=5 sla=72h
        Q->>R: 推送审核任务
    end

5.2.2 步骤明细表

ID 步骤 工具 失败码 是否阻塞
W-1 格式校验 mime + 文件头 magic bytes 检查 ASSET_FORMAT_UNSUPPORTED 阻塞
W-2 安全扫描 自定义 glTF 解析:检查 extras.script / 异常 binary chunk 长度 ASSET_MALICIOUS 阻塞
W-3 usdz→glb usdzconvert USDZ_DECODE_FAILED 阻塞
W-4 压缩 gltf-transform meshopt + textureCompress(ktx2) MESH_COMPRESSION_FAILED 阻塞
W-5 三角面数限制 自研:scene.traverse 累加 indices.count/3 ASSET_OVER_BUDGET(家具 > 50k tri / 材质 quad > 5k tri 阻塞
W-6 bbox / volume / anchor 同 §4.3 import 脚本逻辑
W-7 PBR 校验(材质) 检查 pbr.base_color_tex / normal_tex / roughness / metallic 全在 ASSET_PBR_INCOMPLETEAL-8 阻塞
W-8 dominant_color + tags K-Means + CLIP zero-shot CLIP_INFERENCE_FAILED 可降级(用空 tags 进人工审)
W-9 缩略图 headless three.js → sharp resize webp THUMBNAIL_FAILED 可降级(用占位图)
W-10 Auto Moderator NSFWJS + 人脸 detection(贴图扫描) ASSET_NSFW_BLOCKED / ASSET_PII_DETECTED 直接 rejected

5.3 资产审核(人工 + 自动)

5.3.1 自动初筛(Mod 端)

信号 阈值 动作
NSFWNSFWJS 对 preview_512.webp score ≥ 0.9 直接 review_status='rejected' + rejection_reason='auto_nsfw'
NSFW 0.5 ≤ score < 0.9 入队列 priority=2(高于普通新上传)
三角面数(家具) > 50 000 tri 直接 rejected + rejection_reason='over_triangle_budget'
三角面数(材质 quad > 5 000 tri 同上
贴图含人脸(face detection 任一面孔 rejected + rejection_reason='pii_face_detected'
与既有资产 SHA256 重复 命中 rejected + rejection_reason='duplicate_of=<asset_id>'
重复上传同一文件 同 uploader 24h 内 ≥ 3 次失败 触发用户级速率限制(与 §8 信任分联动)

5.3.2 人工审核工作台(/admin/assets建议增量 to 04_web_app_plan.md R-17

布局(沿用既有 /admin/reports 风格):

区域 内容
左侧列表 asset_review_queuepriority ASC, enqueued_at ASC 排序;徽章显示 sla 剩余时间
右上预览 R3F <Canvas> 360° 旋转预览;显示 triangle_count / volume_m3 / bbox 与压缩后大小
右下元数据 uploader、semantic_class、style_tag、tags、license、auto_signals JSON 摘要
底部动作栏 3 按钮:Approve / Approve with edits / Reject(弹窗填 reason + Escalate(升给 owner-team

5.3.3 三种审核动作

动作 后果 trust_score 起步
Approve review_status='approved',立即可在 AssetPicker 中被搜到 30(默认新上传)
Approve with edits 审核员先补全 anchor_local / tags / style_tag 等字段,再走 Approve 30
Reject 必填 rejection_reasonasset 软删(deleted_at),文件物理保留 7 天供作者下载备份

5.3.4 SLA 与超时回退

  • SLA 72 h(与 10_governance.md §3.1 MVP 1 兼职 reviewer + 30-50 工单/日上限对齐)
  • 超时72 h 未审,资产自动以 trust_score=20 上架(风险,需明确告知 reviewer 团队)+ 工单升级到 owner-team
  • 10_governance.md §2.1 既有 NSFW/敏感词流水线复用阈值NSFW ≥ 0.9 硬下架,0.5-0.9 入队列

5.4 资产 Remix(用户基于已有资产二次创作)

5.4.1 触发入口

  • 资产详情页 /assets/[asset_id] 右上「Fork 这个资产」按钮(MVP 仅 kind='material' 开放;家具 Remix 延后到 v0.5)
  • Fork 后进入轻量编辑器:调 base_color / roughness / metallic 滑块;不允许改 mesh

5.4.2 数据流

parent asset (CC0 or CC-BY)
  |
  v
POST /asset-fork { parent_asset_id, modifications: { pbr_override: {...} } }
  |
  v
INSERT new assets row:
  source_type    = 'remix_of'
  parent_asset_id = <parent>
  uploader_id    = auth.uid()
  review_status  = 'pending'
  license        = 父协议(CC0 父可选 CC0/CC-BYCC-BY 父强制 CC-BY
  pbr            = 父 pbr 叠加 modifications.pbr_override
  trust_score    = round(parent.trust_score * 0.8)

5.4.3 协议传染规则(与 §9 一致)

父 license 子可选 license 强制行为
CC0 CC0 或 CC-BYv0.5+ 子选 CC-BY 时仅署"作为 remix 作者的自己"
CC-BY-4.0 必须 CC-BY-4.0 子的 attribution.txt 必须叠加父的原作者署名(追加,不替换)

6. API 端点增量(AL-10 落地)

建议增量 to 02_api_contract.md §2.1 端点全表,新增 8 条 E-A1~E-A8,沿用既有命名风格与速率限流维度。

6.1 端点全表

ID METHOD 路径 类型 入参 出参 鉴权 速率 业务错误码
E-A1 POST /functions/v1/asset-upload-init Edge { filename, size, mime, kind } { asset_id, presigned_put_url, expires_at } user 10/h/user FILE_TOO_LARGE, ASSET_FORMAT_UNSUPPORTED, QUOTA_EXCEEDED, CREATOR_FROZEN
E-A2 POST /functions/v1/asset-upload-complete Edge { asset_id, name, semantic_class, style_tag, tags[], license, kind, anchor_local? } { asset_id, review_status: 'pending', queue_position } user(owner) 10/h/user ASSET_NOT_FOUND, ASSET_SOURCE_MISSING, ASSET_METADATA_INVALID
E-A3 GET /rest/v1/assets?...(详见 §6.2 PostgREST query string Asset[] anon 60/min/IP RLS_DENIED, SEARCH_QUERY_TOO_SHORT
E-A4 POST /functions/v1/asset-review-action Edge { asset_id, action, reason?, edits? } { ok, new_review_status } reviewer 100/h/reviewer ASSET_NOT_FOUND, ASSET_ALREADY_REVIEWED, REVIEWER_FORBIDDEN
E-A5 POST /functions/v1/asset-fork Edge { parent_asset_id, name, pbr_override?, license } { new_asset_id, review_status: 'pending' } user 5/h/user ASSET_NOT_FOUND, ASSET_FORK_LICENSE_CONFLICT, ASSET_FORK_NON_REMIXABLE
E-A6 POST /functions/v1/asset-report Edge { asset_id, reason, detail? } { report_id } user 5/h/user ASSET_NOT_FOUND, REPORT_DUPLICATE
E-A7 POST /rest/v1/asset_collections PostgREST { name, asset_ids[], is_public, description? } AssetCollection user 20/h/user RLS_DENIED, COLLECTION_NAME_TAKEN
E-A8 DELETE /rest/v1/assets?id=eq.{asset_id} PostgREST path 204 owner(pending) 5/h/user RLS_DENIED, ASSET_ALREADY_APPROVED

注:E-A8 仅 review_status='pending' 时允许 owner 走 PostgREST DELETE(由 §2.5 policy assets_delete_owner_pending 强制);approved 后须改走 E-A4 reviewer 路径(reviewer 可执行 withdrawn 动作,软删并清 Storage 文件)。

6.2 E-A3 资产检索查询参数语义(核心,AL-10 落地)

GET /rest/v1/assets
    ?review_status=eq.approved          # 默认仅展示 approved(前端固定)
    &kind=eq.furniture                  # 'furniture' | 'material'
    &semantic_class=eq.sofa             # 16 类 / 6 类
    &style_tag=eq.nordic                # 7 类
    &bbox_filter=lo,hi                  # 体量范围 m^3
    &color=hue:200,tol:20               # 主色相 hue ± tolHSL 空间)
    &license=in.(CC0,CC-BY-4.0)         # 协议白名单
    &q=oak                              # name + tags 模糊(pg_trgm + GIN
    &source_type=in.(platform,user_upload)
    &order=trust_score.desc,use_count.desc
    &limit=20
    &offset=40

6.2.1 bbox_filter=lo,hi 语义

  • 用户在 AssetPicker 选中一个家具,其原 OBB 体量 vol_target m³;前端拼 lo = vol_target * 0.5, hi = vol_target * 1.5
  • 实现:在 RPC search_assets 中翻译为 WHERE volume_m3 BETWEEN $lo AND $hi
  • 容差:默认 ±50%(含 0.5×~1.5×;过严会让候选集小于 5 件,过松失去过滤意义);与 05_object_replacement_handbook.md §4.2 推荐的 ±20% 兼容(参数化由前端控制)
  • 索引:assets_volume_btree(§2.4

6.2.2 color=hue:200,tol:20 语义

  • 在 HSL 色空间筛主色相 ± tol(角度 0-360)
  • 实现:服务端把 dominant_color 的 hex → HSL,做 WHERE ABS(hue_diff(...)) < $tol
  • 简化版(MVP):先用六色桶(warm/cold/neutral/red/blue/green)做粗筛,避免每次 SQL 调用 HSL 转换函数
  • 进阶(P2):LAB 距离 < 阈值,更感知一致

6.2.3 复合查询 SQL 实现示例

-- 建议增量:暴露 RPC,便于复杂参数封装
CREATE OR REPLACE FUNCTION public.search_assets(
  p_kind text DEFAULT NULL,
  p_semantic_class text DEFAULT NULL,
  p_style_tag text DEFAULT NULL,
  p_vol_lo numeric DEFAULT NULL,
  p_vol_hi numeric DEFAULT NULL,
  p_hue smallint DEFAULT NULL,
  p_hue_tol smallint DEFAULT 20,
  p_q text DEFAULT NULL,
  p_limit int DEFAULT 20,
  p_offset int DEFAULT 0
) RETURNS SETOF public.assets AS $$
  SELECT * FROM public.assets a
  WHERE a.review_status = 'approved'
    AND a.deleted_at IS NULL
    AND (p_kind IS NULL OR a.kind = p_kind)
    AND (p_semantic_class IS NULL OR a.semantic_class = p_semantic_class)
    AND (p_style_tag IS NULL OR a.style_tag = p_style_tag)
    AND (p_vol_lo IS NULL OR a.volume_m3 BETWEEN p_vol_lo AND p_vol_hi)
    AND (p_hue IS NULL OR hue_diff_360(hue_of_hex(a.dominant_color), p_hue) <= p_hue_tol)
    AND (p_q IS NULL OR a.name ILIKE '%'||p_q||'%' OR a.tags && string_to_array(p_q, ','))
  ORDER BY a.trust_score DESC, a.use_count DESC
  LIMIT p_limit OFFSET p_offset;
$$ LANGUAGE sql STABLE;
  • 性能目标100k 行规模下,semantic_class + bbox_filter + style_tag 三维过滤 + 排序 ≤ 200 ms(依赖 §2.4 复合索引)
  • CDN 缓存60 s(按 query string 哈希);登录态附 Cache-Control: private

6.3 错误码增量

建议增量 to 02_api_contract.md §7

错误码 HTTP 含义 出现端点
ASSET_FORMAT_UNSUPPORTED 415 不接受 .obj / .fbx E-A1, W-1
ASSET_OVER_BUDGET 413 三角面数或文件大小超限 W-5
ASSET_PBR_INCOMPLETE 400 材质 PBR 字段缺失(AL-8 W-7
ASSET_NSFW_BLOCKED 403 NSFW score ≥ 0.9 自动下架 Mod
ASSET_PII_DETECTED 403 贴图含人脸/证件 Mod
ASSET_MALICIOUS 400 文件结构异常或含 embedded 脚本 W-2
ASSET_ALREADY_REVIEWED 409 同一 asset 不允许重复审 E-A4
ASSET_FORK_LICENSE_CONFLICT 400 父 CC-BY 子选 CC0 不允许 E-A5
ASSET_FORK_NON_REMIXABLE 403 父资产 kind='furniture' 在 MVP 不允许 fork E-A5
CREATOR_FROZEN 403 creator_trust_score < 0 上传被冻结 E-A1
REVIEWER_FORBIDDEN 403 无 reviewer role E-A4
COLLECTION_NAME_TAKEN 409 同 owner 收藏夹重名 E-A7
ASSET_ALREADY_APPROVED 409 approved 资产不可走 owner DELETE E-A8

7. AssetPicker 排序与发现策略

7.1 默认排序综合公式

AssetPicker 默认查询 order=trust_score.desc, use_count.desc,但实际后端按下列加权重排(在 RPC search_assets 顶层包一层):

score = 0.4 * normalize(trust_score, 0-100)
      + 0.3 * normalize(log1p(use_count), log scale clip 0-1)
      + 0.2 * license_bonus            -- CC0=1.0, CC-BY=0.5
      + 0.1 * recency_decay(created_at) -- 7 天内 1.0, 30 天 0.5, 90 天 0.2
      - 1.0 * is_reported              -- 当前有未结举报扣分(强烈降权)

7.2 冷启动策略

阶段 规则
上传后 0-7 天 trust_score 固定 30,禁止变化;优先暴露给 Remix 编辑器底部「新上传推荐」窗口(占 10% 流量配额)
第 8 天 根据 download_count / use_count / report_count 进入 §8 信任分动态算法
资产首次被 use 时 立即将 trust_score += 2(实时反馈),但单 7 天窗口内增量上限 ≤ 10

7.3 三类资产权重差异

source_type 默认 trust_score 排序额外因子
platform 80 × 1.0
user_upload 30 → 动态 × 0.85(同分数时让位 platform
remix_of round(parent × 0.8) × 0.7(避免刷量)

7.4 防作弊

  • 同 IP 同 asset 24h 内 use_count 增量上限 = 3trigger 端去重)
  • 同 uploader 自我点赞收藏不计 use_count
  • trust_score 24h 内变化幅度 ≤ ±15(防恶意刷分/被刷)

8. 创作者声誉与信任分(AL 衍生需求)

8.1 双重信任分模型

维度 字段 范围 含义
资产级 assets.trust_score 0-100 单件资产的可信度,影响 §7 排序
创作者级 users.creator_trust_score -100 ~ 100 创作者整体声誉,影响上传权限与跳审

8.2 creator_trust_score 计分规则

事件 变化 触发位置
初始注册 = 50 users 默认值
资产 approve +5 E-A4 action=approve
资产 reject -10 E-A4 action=reject
资产被举报且 reviewer 判定违规 -20 asset_reports.resolution='confirmed_violation'
资产被 use_count 累计 ≥ 100 +10 trigger(每件资产仅奖励一次)
资产被收藏(加入 asset_collections)累计 ≥ 100 次 +5 trigger
主动撤回 pending 资产 0 E-A8
申诉胜诉(rejected → approved +15 reviewer 修正

8.3 阈值规则

区间 状态 上传体验
< 0 冻结 E-A1 直接 CREATOR_FROZEN;不再接受上传
0 ~ 29 低信任 上传必须走人工预审(自动初筛不能放行;优先级降至 priority=7)
30 ~ 79 标准 默认流程
80 ~ 100 Trusted Creator 上传可跳过人工审核(仅自动审,approved 后直接上架);creator_badge='trusted'
≥ 95creator_uploaded_count ≥ 50 Pro Creator 同上 + 优先展示在「创作者推荐」位

8.4 与 v0.2 既有信任分系统的对齐

本节是 10_governance.md §3.2 P1 信任分系统的具体落地

v0.2 §3.2 P1 规则 本节对应
初始信任分 50 users.creator_trust_score DEFAULT 50
每次成功举报 +5 §8.2「资产被举报判违规 -20」是被举报方;举报方 +5 走 10_governance.md 现有 user-level 信任分(独立维度),不在本表
满 100 分可申请志愿者审核团 creator_trust_score = 100 AND creator_uploaded_count >= 50 → 弹申请入口;具体审核团 SOP 走 v0.5

9. 版权与协议管理(AL-7 落地)

9.1 MVP 与 v0.5 的两阶段策略

阶段 用户可选 license UX
MVPv0.2~v0.4 仅 CC0 上传向导 §5.1.3 Step 4 默认且禁用切换;显示「MVP 期所有资产强制 CC0 — 详见 社区准则 §4
v0.5+DAU > 5 000 CC0 或 CC-BY-4.0 上传向导 Step 4 加单选;选 CC-BY 必填「原作者名 + 原始 URL」(即便是自己原创,URL 可填自己主页)

9.2 强约束(与 10_governance.md §4 P-W-4 一致)

  • 接受CC0、CC-BY-4.0v0.5+
  • 永不接受
    • CC-BY-SA(病毒条款,污染 Remix 树)
    • CC-BY-NC(与 CrowdRoom 平台 BY-NC 双重叠加,堵死 Remix 商业化)
    • 任何商业素材(涉及税务、对账、退款,超 MVP 预算)

9.3 attribution 自动透传链路

flowchart LR
  A[CC-BY 资产 A] --> Remix[Remix 引用 A]
  Remix --> Overlay[remix_overlay.json 自动追加 attribution: { asset_id, creator, license }]
  Overlay --> RemixPage[Remix 详情页页脚 Assets in this remix]
  RemixPage --> Display[显示 by creator CC-BY 4.0]

9.4 侵权举报与下架流程

用户举报(E-A6
   |
   v
asset_reports INSERT, 累计独立举报数 >= 3
   |
   v
trigger: assets.review_status = 'pending' (自动隐藏)
   +  asset_review_queue INSERT priority=1
   +  Edge Function 通知 uploader 站内信
   |
   v
Reviewer 24h 内决策
   |--- false_report → review_status='approved' 恢复
   |--- confirmed_violation → review_status='withdrawn'
                              + Storage 文件物理删除
                              + uploader.creator_trust_score -= 20
                              + 触发回退:所有 remix_overlay 引用此 asset_id 的 op
                                自动 fallback 到「占位 box」(asset_id 仍可在 overlay 中保留以备申诉)

9.5 「占位 box」fallback 规则

  • Web 端加载 overlay 时,对每个 op.asset_id 调 E-A3 校验存在;
  • 若 asset 已 withdrawndeleted,渲染端用一个灰色 OBB 占位 mesh 替代(尺寸取自原 target_item.obb),上方浮一行 "Asset withdrawn"
  • 05_object_replacement_handbook.md §9 applyOverlay 的「asset 加载失败兜底」分支兼容

10. 资产收藏 / 集合 / 个人主页(社区飞轮)

10.1 用户收藏夹(asset_collectionsis_official=false

  • 用户可建多个收藏夹(如 "我的客厅灵感"、"侘寂风备选"
  • 每个收藏夹 asset_ids[] 上限 200 个;超出走「分页拆分」CTA
  • 收藏夹可 is_public=true 公开,被发现页「灵感板」抓取展示
  • 在 AssetPicker 顶栏可一键「+ 加入收藏夹」(弹出收藏夹选择 + 新建)

10.2 官方风格集(is_official=true

  • 仅 reviewer / owner-team 可创建(service_role 写)
  • 典型示例:
    • "侘寂风家具 30 件套"30 件 style_tag='wabi_sabi' 的精选)
    • "Quaternius Spring 2026 新品"
    • "亚洲设计师精选"v0.5+ 创作者 spotlight
  • 发现页 / 顶部「编辑精选」位轮播展示

10.3 创作者个人主页扩展

扩展 04_web_app_plan.md §1.1 R-4 /u/[handle] 既有页:

新增模块 内容
「我的资产」Tab 网格列出该用户 approved 资产;右上 stats:上传数 / 总使用数 / 总下载数
Trusted Creator 徽章 头像右下角小盾牌(creator_badge='trusted'
Pro Creator 徽章 头像右下角金色齿轮(creator_badge='pro'
「联系/赞赏」按钮 v0.5+MVP 隐藏
收藏夹列表(仅自己可见私有) 公开收藏夹列出

11. 移动端可访问性(iOS App 是否能上传资产?)

11.1 决策:MVP iOS App 不支持上传资产到资产库

维度 理由
用户画像 iOS 端用户的核心动作是「扫描房间」(RoomPlan),不是「建模家具」
工具链 自制 .glb 的工具链都在 PCBlender / 3ds Max / Rhino),手机上无创作场景
审核成本 手机端上传更容易出现"随手拍照伪装 3D"的低质内容,自动初筛拦不住
开发成本 iOS 端走 §5 完整上传向导需重写 SwiftUIMVP 期 ROI 低

11.2 v0.5 起的轻量入口(候选)

  • iPhone 拍照 → Object CaptureiOS 17+)→ 自动生成 .glb → 走简化版 §5 流程
  • 仅支持「桌面摆件」级小物体(< 30 cm 边长)
  • 自动 style_tag='unknown',强制进入 priority=2 人工审

11.3 iOS App 仍可以做的资产相关动作

动作 是否支持
浏览资产库(只读) (MVP 即支持,复用 Web 的 E-A3
收藏资产到 asset_collections
举报资产(E-A6
上传资产 MVP)→ v0.5 Object Capture 路径)
Fork / Remix 资产 MVP)→ v0.5

12. 性能与存储成本

12.1 存储成本估算

资产数 平均单件 总存储 Cloudflare R2 月成本 Supabase Storage 月成本
1 万 600 KBmodel+thumb+preview ~6 GB ~$0.10 免费档内
10 万 600 KB ~60 GB ~$1.50 $1.25(超 1 GB 后 $0.021/GB
100 万 600 KB ~600 GB ~$15 $12.6 → 此规模迁 R2

04_web_app_plan.md §10 / 10_governance.md §4 P-W-6「Cloudflare 免费档 + R2 出口费」一致;MVP 期资产库不会成为成本瓶颈。

12.2 AssetPicker 加载性能

场景 目标 实现
第一屏 20 个卡片 TTI < 1 s 20 × 128×128 webp ≈ 200 KBCDN gzip 后),4G 网络 < 800 ms
滚动加载下一屏 < 300 ms E-A3 limit=20 + offset 分页,CDN 60s 缓存命中率 > 80%
详情页 360° 预览 TTI < 2 s 预加载 model.glb(≤ 1 MBCDN+ 异步加载 preview_3d.glb

12.3 检索性能

规模 三维过滤(semantic+style+bbox 索引
10k 行 ~30 ms btree
100k 行 ~120 ms btree + GIN
1M 行 ~400 ms警戒 需要切 ElasticSearch / MeilisearchP2,纳入 ROADMAP

触发条件:总 approved 资产数 > 100k → 启动 outbox 同步到 Meilisearch;保留 Postgres 为权威源(与 01_data_schema.md §1 D5 全文搜索演进策略一致)。

12.4 Worker 处理能力

资源 单次处理时间 MVP 并发
用户上传 .glb5 MB → 1 MB ~30 s(含 KTX2 + CLIP + 缩略图) 5 并发,60 件/小时
平台 import 批量 ~10 s/件 串行,360 件/小时(夜间跑)

MVP 容量:60 件/小时 × 24 h = 1 440 件/日上限,远高于预期 50 件/日新增。


13. 与 v0.2 设计的关系矩阵

一张表,每行:本文档章节 / 关联的 v0.2 文档章节 / 关系类型。Reviewer 可一眼看清哪些是新增、哪些是已有。

本文档章节 关联 v0.2 章节 关系类型
§1 三类资产定位 10_governance.md §4 P-W-4 深化补充(从"全 CC0 公共池"扩到三源流)
§2 assets 表 13 列增量 01_data_schema.md §3.9 建议增量ALTER 不改既有列)
§2.7.1 asset_review_queue 新表 10_governance.md §2.1 自动审核流 新增能力(资产分支队列)
§2.7.2 asset_reports 新表 02_api_contract.md §2 E-14 通用 report 端点 深化补充(资产专属举报 schema
§2.7.3 asset_collections 新表 新增能力v0.2 无收藏概念)
§2.7.4 creator_trust_score 字段 10_governance.md §3.2 P1 信任分 深化补充(具体落地)
§3 Storage 目录 public/assets/{id}/ 01_data_schema.md §4 建议增量(顶层 prefix 扩展)
§3.3 glb_path_immutable trigger 01_data_schema.md §3.9 建议增量AL-9 兜底)
§4 平台导入管线 10_governance.md §4 P-W-4 步骤 1-3 深化补充(提供具体脚本骨架)
§5 用户上传向导 04_web_app_plan.md §1.1 路由表 建议增量R-18~R-19a 路由)
§5.2 Asset Worker 02_api_contract.md §3 Transcode Worker 深化补充(复用基础设施,新增分支)
§5.3 审核工作台 04_web_app_plan.md R-17 /admin/reports 建议增量R-17b /admin/assets
§6 8 个新端点 E-A1~E-A8 02_api_contract.md §2.1 端点表 建议增量
§6.3 错误码增量 02_api_contract.md §7 建议增量
§7 AssetPicker 排序 05_object_replacement_handbook.md §4 深化补充
§8 创作者信任分 10_governance.md §3.2 深化补充(具体公式)
§9 协议管理 10_governance.md §4 P-W-4、§8 直接引用 + 深化
§9.5 「占位 box」fallback 05_object_replacement_handbook.md §9 / §12 F-5 直接引用
§10 收藏 / 集合 04_web_app_plan.md §1.1 R-4 用户主页 建议增量
§11 iOS 边界 03_ios_app_plan.md §1 路由 新增决策(明确不做)
§12 性能 / 成本 04_web_app_plan.md §10、10_governance.md §4 P-W-6 深化补充

14. M1 验收清单

14.1 12 条 checkbox 验收(M1 = MVP 8 周窗口内必达)

  • M1-01assets 表通过 §2 增量 ALTER 后能存储 1 万条平台资产(数据迁移脚本无报错)
  • M1-02:从 Quaternius 批量导入 100 件家具脚本(§4.3)能跑通,平均单件 < 15 s
  • M1-03:用户能从 /me/assets/new 上传 1 个 .glb 文件(5 步向导 UX 完整)
  • M1-04:上传完成后 5 分钟内进入审核队列(asset_review_queue 有对应行 priority=5
  • M1-05:自动初筛能拒 NSFWscore ≥ 0.9/ 拒大于 50k 三角面数 / 拒含人脸贴图
  • M1-06:审核员在 /admin/assets 工作台 approve 后,资产能在 AssetPicker 中通过 E-A3 被搜到
  • M1-07AssetPicker 按 semantic_class + bbox_filter + style_tag 复合查询响应 < 200 ms100k 行规模)
  • M1-0805_object_replacement_handbook.md §15 的 AL-1 ~ AL-10 全部 10 条契约逐条达成(详见 §14.2 对照表)
  • M1-09:资产被 use_count 累计 ≥ 100 后 assets.trust_score 自动 +10trigger 实测)
  • M1-10:用户能从 /me/assets 创建收藏夹并放入 ≥ 5 个资产,公开后他人可见
  • M1-11CC-BY 资产(v0.5+ 启用)在 Remix 详情页底部正确显示「Asset by {creator} (CC-BY 4.0)」
  • M1-12:侵权举报触发后 24 h 内(confirmed_violation 路径)能从 AssetPicker 下架;占位 box fallback 生效

14.2 AL-1 ~ AL-10 契约最终对照表(双重显式,与 §2.6 互为冗余)

契约 验收条件 测试方法 M1 状态
AL-1 所有 kind='furniture' 资产 anchor_local + bbox_local 非空率 100% SELECT count(*) FROM assets WHERE kind='furniture' AND (anchor_local='{}' OR bbox_local='{}') = 0 [ ]
AL-2 semantic_class 非空率 ≥ 99%01_data_schema.md §3.9 既有字段) 同上 SQL 校验 [ ]
AL-3 style_tag 非空率 ≥ 95%;7 类 enum 覆盖 SELECT style_tag, count(*) FROM assets GROUP BY 1 [ ]
AL-4 volume_m3 > 0 非空率 100%dominant_color 非空率 ≥ 95%GIN tags 索引存在 \d+ public.assets 看索引 + 数据校验 [ ]
AL-5 100% 平台资产 model.glb ≤ 1 MB(家具)/ ≤ 200 KB(材质) Storage 对象大小巡检脚本 [ ]
AL-6 100% 资产有 thumb_128.webp128×128 Storage 路径存在性巡检 [ ]
AL-7 license CHECK IN ('CC0','CC-BY-4.0') 强制;100% 资产协议 = CC0(MVP 阶段) SELECT DISTINCT license FROM assets [ ]
AL-8 kind='material' 资产 100% 含 pbr.base_color_tex / normal_tex / roughness / metallic JSONB key existence SQL [ ]
AL-9 assets_glb_path_lock trigger 启用;任何 UPDATE 改 glb_path 都抛错 故意 UPDATE 测试 [ ]
AL-10 E-A3 接受 bbox_filter / style / semantic / color 复合参数;响应 ≤ 200 ms k6 压测 100k 行规模 [ ]

结论:14.2 对照表 + §2.6 字段映射表共同实现 AL-1 ~ AL-10 的双重显式对账。


15. 风险与开放问题

# 风险 影响 缓解策略
R-1 UGC 资产质量参差 → AssetPicker 体验下降 用户找不到好资产,Remix 转化率下降 §7 排序公式让 platform 资产默认排前;冷启动 7 天压制 user_upload;§8 信任分淘汰低质创作者
R-2 版权审核难度大(用户「原创」声明无法机器验证) 法律风险,版权方追责 MVP 强制 CC0(用户放弃所有权);侵权举报 24 h 内下架 + Storage 删除 + 占位 box fallback;保留申诉通道
R-3 资产规模 > 100k 后检索性能退化 E-A3 响应 > 200 msAssetPicker 卡顿 §12.3 触发条件:> 100k 行 → outbox 同步到 MeilisearchPostgres 保留为权威源(P2,纳入 ROADMAP)
R-4 平台资产爬取的法律边界(Sketchfab CC0 标错率高) 误把非 CC0 当 CC0,被原作者追责 §4.1 Sketchfab 子集每件单独人工 verify;保留 source_url + attribution.txt 即便 CC0
R-5 Remix 衍生的协议传染(CC-BY 链可能很长) 长链署名累积,UI 难以全部展示 §9.3 attribution 透传链路 trigger 自动汇总;详情页页脚折叠展示「View all attributions」
R-6 iOS 不能上传资产是否伤害创作者生态 移动端创作者流失 §11.2 v0.5 引入 Object Capture 轻量入口;MVP 阶段通过「Web 上传 + 邮件分享给 iOS 用户」过渡

章节版本:v0.2 · 草案(子任务 10 产出,2026-05-19 关键收获

  • v0.2 既有 assets 表 13 列字段增量 + 3 张支撑表 = 完整的「平台 + UGC + Remix」三源流资产库
  • 8 个新端点 E-A1~E-A8 + 1 个 RPC search_assets = AssetPicker 复合检索 + UGC 全生命周期
  • 10 条 AL 契约 100% 落地(§2.6 字段映射 + §14.2 验收对照 双重显式)
  • 10_governance.md §4 P-W-4 决策严格一致(MVP 全 CC0v0.5 起 CC-BY

对外契约

契约 给谁 一句话
assets 表 13 列 ALTER + 3 张支撑表 DBA / 后端 在 v0.2 §3.9 既有 schema 上做幂等 ALTER;不破坏既有迁移
8 个新端点 E-A1~E-A8 iOS / Web / 运营 全部走 Edge Function 或 PostgREST,沿用 v0.2 鉴权/限流维度
AL-1 ~ AL-10 全部 10 条契约达成 05_object_replacement_handbook.md 物品替换闭环 资产库提供的元数据足以让 AssetPicker 在 200 ms 内做 OBB 对齐替换
MVP CC0 强制 / v0.5 CC-BY 二选一 法务 / 运营 10_governance.md §4 P-W-4 决策一致
iOS App 仅消费不生产 iOS 团队 MVP 不接 UGC 上传;v0.5 起评估 Object Capture 轻量入口