--- title: "CrowdRoom · 资产库(Asset Library)模块设计(v0.2)" date: 2026-05-20 draft: false tags: ["CrowdRoom", "众包", "3D 重建", "导航", "Web", "数据结构"] categories: ["CrowdRoom"] --- # CrowdRoom · 资产库(Asset Library)模块设计(v0.2) > **版本**:v0.2(2026-05-19)· 子任务 10 产出 > **定位**:把 [`01_data_schema.md`](01_data_schema.md) §3.9 中**仅作为"平台 CC0 公共素材池"**存在的 `public.assets` 表,**升级**为一个支持「平台爬取导入 + 用户 UGC 上传 + Remix 衍生」三源流的资产库子系统;同时**逐条兑现** [`05_object_replacement_handbook.md`](05_object_replacement_handbook.md) §15 移交的 **AL-1 ~ AL-10** 硬契约。 > > **本文档不修改任何 v0.2 既有架构**——所有对 [`01_data_schema.md`](01_data_schema.md) / [`02_api_contract.md`](02_api_contract.md) / [`04_web_app_plan.md`](04_web_app_plan.md) / [`10_governance.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`](05_object_replacement_handbook.md) §4 AssetPicker 的**唯一上游数据源**——AssetPicker 拉的所有家具/材质卡片均来自本资产库;同时把 [`10_governance.md`](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 上传向导 | CC0(MVP 强制);CC-BY 见 v0.5 ROADMAP | 自动初筛 + 人工 24-72 h 审 | **30**(新手)/ **50**(Trusted 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`](10_governance.md) §4 P-W-4 对齐): > > - MVP 阶段 T1 全 CC0;T2 / T3 用户上传**强制 CC0**([`04_web_app_plan.md`](04_web_app_plan.md) §5.4 顶部「仅显示 CC0」开关**始终为 ON 且不可关**,等同于"全库即 CC0") > - v0.5(DAU > 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`](10_governance.md) §4 P-W-4 步骤 3) | 复用 [`10_governance.md`](10_governance.md) §2.1 既有 NSFW + 敏感词自动审核流,新增「资产审核队列」分支 | --- ## 2. 数据模型增强(AL-1 / AL-2 / AL-4 / AL-8 落地) ### 2.1 设计原则 1. **不破坏 v0.2** — [`01_data_schema.md`](01_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 ```sql -- 建议增量:可在 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), -- 平台资产为 NULL;user_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 落地) ```sql -- 建议增量:把 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) ```sql -- tags 数组 GIN(AL-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_status(AssetPicker 默认仅拉 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) ```sql -- 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:登录用户可插入 pending(uploader_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' ); -- DELETE:owner 可在 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 jsonb`、`bbox_local jsonb`(§2.2) | §2.2 / §4 / §5.2 | | **AL-2** | 公开资产 `semantic_class` 非空率 ≥ 99% | `semantic_class`(v0.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_btree`;worker 自动从 CLIP 推断 + 上传向导默认必填 | §2.2 / §4 / §5.1 | | **AL-4** | 物化 `volume_m3` + `dominant_color`;GIN 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 协议管理 | §2.3 / §9 | | **AL-8** | 材质 `pbr` jsonb 必含 base_color_tex/normal_tex/roughness/metallic | `pbr jsonb`(v0.2 既有)+ §5.2 worker 步骤 W-7 schema 校验 | §5.2 W-7 | | **AL-9** | asset_id ↔ glb_path 不可重定向 | §3 路径硬规则 `public/assets/{asset_id}/model.glb`;asset_id 永不复用、glb_path 不允许 UPDATE(policy 限制) | §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`(审核工单) ```sql 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`(侵权 / 违规举报) ```sql 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`](10_governance.md) §2.2「3 独立举报自动隐藏」对齐。 #### 2.7.3 `asset_collections`(用户收藏夹 / 官方风格集) ```sql 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`(创作者信任分扩展) ```sql -- 复用 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_queue`、`asset_reports`、`asset_collections`) | | 新增索引 | 6(`assets`)+ 5(支撑表)+ 1(users 创作者)= 12 个 | | 新增 trigger | 2 个(`bump_asset_use_count` + `assets_glb_path_lock`,后者见 §3.3) | | 新增 RLS policy | 4(`assets`)+ 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 KB(AL-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`](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 永不复用** | 资产被 `withdrawn` 后 `id` 不释放;新上传始终拿 `gen_random_uuid()` | | **R-2 model.glb 路径不可重定向** | `glb_path = 'public/assets/{asset_id}/model.glb'` 写死;trigger `assets_glb_path_lock`(§3.3)拒绝任何 UPDATE 改 `glb_path`(service_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` 分隔;格式:`"" CC-BY-4.0` | ### 3.3 glb_path 不变性的 SQL 兜底 ```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 目标 | 备注 | |------|------|------|-------------|---------|------| | **Quaternius**(github.com/quaternius) | CC0 | 家具 / 装饰 | 300 件 | 2 000 件 | `Furniture Kit`、`Plant Pack` 等 | | **Poly Haven**(polyhaven.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 流程图 ```mermaid 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 批量) ```typescript // 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 = { bed: 'bed', chair: 'chair', table: 'table', sofa: 'sofa', desk: 'table', shelf: 'storage', dresser: 'storage', tv: 'television', }; const STYLE_FROM_PACK: Record = { '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/+Y(AL-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.txt(CC0 也保留来源 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 { // 伪代码:真实实现挂 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 起开放) | 步骤 | CC0(MVP) | CC-BY(v0.5+) | |------|-----------|--------------| | `attribution.txt` | 可选 | **必须**,格式 `"" 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`](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`) ```mermaid 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 MB(worker 端再压到 ≤ 1 MB) | | **2 自动预览** | R3F `` 内嵌即时渲染 + 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`](02_api_contract.md) §3 既有 Transcode Worker 的基础设施(同 Cloud Run / Docker / 重试策略),但走独立的 `asset-` 分支函数集。 #### 5.2.1 完整 18 步序列图 ```mermaid 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_INCOMPLETE`(AL-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 端) | 信号 | 阈值 | 动作 | |------|------|------| | NSFW(NSFWJS 对 `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='` | | 重复上传同一文件 | 同 uploader 24h 内 ≥ 3 次失败 | 触发用户级速率限制(与 §8 信任分联动) | #### 5.3.2 人工审核工作台(`/admin/assets`,**建议增量** to [`04_web_app_plan.md`](04_web_app_plan.md) R-17) 布局(沿用既有 `/admin/reports` 风格): | 区域 | 内容 | |------|------| | 左侧列表 | `asset_review_queue` 按 `priority ASC, enqueued_at ASC` 排序;徽章显示 sla 剩余时间 | | 右上预览 | R3F `` 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_reason`;asset 软删(`deleted_at`),文件物理保留 7 天供作者下载备份 | — | #### 5.3.4 SLA 与超时回退 - **SLA 72 h**(与 [`10_governance.md`](10_governance.md) §3.1 MVP 1 兼职 reviewer + 30-50 工单/日上限对齐) - **超时**:72 h 未审,资产自动以 `trust_score=20` 上架(**风险**,需明确告知 reviewer 团队)+ 工单升级到 owner-team - 与 [`10_governance.md`](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 = uploader_id = auth.uid() review_status = 'pending' license = 父协议(CC0 父可选 CC0/CC-BY;CC-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-BY(v0.5+) | 子选 CC-BY 时仅署"作为 remix 作者的自己" | | CC-BY-4.0 | **必须** CC-BY-4.0 | 子的 `attribution.txt` 必须**叠加**父的原作者署名(追加,不替换) | --- ## 6. API 端点增量(AL-10 落地) **建议增量** to [`02_api_contract.md`](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 ± tol(HSL 空间) &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`](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 实现示例 ```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`](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 增量上限 = 3(trigger 端去重) - 同 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'` | | `≥ 95` 且 `creator_uploaded_count ≥ 50` | **Pro Creator** | 同上 + 优先展示在「创作者推荐」位 | ### 8.4 与 v0.2 既有信任分系统的对齐 本节是 [`10_governance.md`](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`](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 | |------|----------------|-----| | **MVP(v0.2~v0.4)** | 仅 CC0 | 上传向导 §5.1.3 Step 4 默认且禁用切换;显示「MVP 期所有资产强制 CC0 — 详见 [社区准则 §4](../CrowdRoom/10_governance.md#4-web-移交契约p-w-3--p-w-6-逐条决策)」 | | **v0.5+(DAU > 5 000)** | CC0 或 CC-BY-4.0 | 上传向导 Step 4 加单选;选 CC-BY 必填「原作者名 + 原始 URL」(即便是自己原创,URL 可填自己主页) | ### 9.2 强约束(与 [`10_governance.md`](10_governance.md) §4 P-W-4 一致) - ✅ **接受**:CC0、CC-BY-4.0(v0.5+) - ❌ **永不接受**: - **CC-BY-SA**(病毒条款,污染 Remix 树) - **CC-BY-NC**(与 CrowdRoom 平台 BY-NC 双重叠加,堵死 Remix 商业化) - **任何商业素材**(涉及税务、对账、退款,超 MVP 预算) ### 9.3 attribution 自动透传链路 ```mermaid 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 已 `withdrawn` 或 `deleted`,渲染端用一个灰色 OBB 占位 mesh 替代(尺寸取自原 `target_item.obb`),上方浮一行 `"Asset withdrawn"` - 与 [`05_object_replacement_handbook.md`](05_object_replacement_handbook.md) §9 `applyOverlay` 的「asset 加载失败兜底」分支兼容 --- ## 10. 资产收藏 / 集合 / 个人主页(社区飞轮) ### 10.1 用户收藏夹(`asset_collections`,`is_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`](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 的工具链都在 PC(Blender / 3ds Max / Rhino),手机上无创作场景 | | **审核成本** | 手机端上传更容易出现"随手拍照伪装 3D"的低质内容,自动初筛拦不住 | | **开发成本** | iOS 端走 §5 完整上传向导需重写 SwiftUI;MVP 期 ROI 低 | ### 11.2 v0.5 起的轻量入口(候选) - iPhone 拍照 → Object Capture(iOS 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 KB(model+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`](04_web_app_plan.md) §10 / [`10_governance.md`](10_governance.md) §4 P-W-6「Cloudflare 免费档 + R2 出口费」一致;MVP 期资产库不会成为成本瓶颈。 ### 12.2 AssetPicker 加载性能 | 场景 | 目标 | 实现 | |------|------|------| | 第一屏 20 个卡片 | TTI < 1 s | 20 × 128×128 webp ≈ 200 KB(CDN gzip 后),4G 网络 < 800 ms | | 滚动加载下一屏 | < 300 ms | E-A3 `limit=20` + offset 分页,CDN 60s 缓存命中率 > 80% | | 详情页 360° 预览 | TTI < 2 s | 预加载 model.glb(≤ 1 MB,CDN)+ 异步加载 preview_3d.glb | ### 12.3 检索性能 | 规模 | 三维过滤(semantic+style+bbox)| 索引 | |------|------------------------------|------| | 10k 行 | ~30 ms | btree | | 100k 行 | ~120 ms | btree + GIN | | 1M 行 | ~400 ms(**警戒**) | 需要切 ElasticSearch / Meilisearch(P2,纳入 ROADMAP) | > **触发条件**:总 `approved` 资产数 > 100k → 启动 outbox 同步到 Meilisearch;保留 Postgres 为权威源(与 [`01_data_schema.md`](01_data_schema.md) §1 D5 全文搜索演进策略一致)。 ### 12.4 Worker 处理能力 | 资源 | 单次处理时间 | MVP 并发 | |------|------------|---------| | 用户上传 .glb(5 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`](10_governance.md) §4 P-W-4 | **深化补充**(从"全 CC0 公共池"扩到三源流) | | §2 `assets` 表 13 列增量 | [`01_data_schema.md`](01_data_schema.md) §3.9 | **建议增量**(ALTER 不改既有列) | | §2.7.1 `asset_review_queue` 新表 | [`10_governance.md`](10_governance.md) §2.1 自动审核流 | **新增能力**(资产分支队列) | | §2.7.2 `asset_reports` 新表 | [`02_api_contract.md`](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`](10_governance.md) §3.2 P1 信任分 | **深化补充**(具体落地) | | §3 Storage 目录 `public/assets/{id}/` | [`01_data_schema.md`](01_data_schema.md) §4 | **建议增量**(顶层 prefix 扩展) | | §3.3 `glb_path_immutable` trigger | [`01_data_schema.md`](01_data_schema.md) §3.9 | **建议增量**(AL-9 兜底) | | §4 平台导入管线 | [`10_governance.md`](10_governance.md) §4 P-W-4 步骤 1-3 | **深化补充**(提供具体脚本骨架) | | §5 用户上传向导 | [`04_web_app_plan.md`](04_web_app_plan.md) §1.1 路由表 | **建议增量**(R-18~R-19a 路由) | | §5.2 Asset Worker | [`02_api_contract.md`](02_api_contract.md) §3 Transcode Worker | **深化补充**(复用基础设施,新增分支) | | §5.3 审核工作台 | [`04_web_app_plan.md`](04_web_app_plan.md) R-17 `/admin/reports` | **建议增量**(R-17b `/admin/assets`) | | §6 8 个新端点 E-A1~E-A8 | [`02_api_contract.md`](02_api_contract.md) §2.1 端点表 | **建议增量** | | §6.3 错误码增量 | [`02_api_contract.md`](02_api_contract.md) §7 | **建议增量** | | §7 AssetPicker 排序 | [`05_object_replacement_handbook.md`](05_object_replacement_handbook.md) §4 | **深化补充** | | §8 创作者信任分 | [`10_governance.md`](10_governance.md) §3.2 | **深化补充**(具体公式) | | §9 协议管理 | [`10_governance.md`](10_governance.md) §4 P-W-4、§8 | **直接引用 + 深化** | | §9.5 「占位 box」fallback | [`05_object_replacement_handbook.md`](05_object_replacement_handbook.md) §9 / §12 F-5 | **直接引用** | | §10 收藏 / 集合 | [`04_web_app_plan.md`](04_web_app_plan.md) §1.1 R-4 用户主页 | **建议增量** | | §11 iOS 边界 | [`03_ios_app_plan.md`](03_ios_app_plan.md) §1 路由 | **新增决策**(明确不做) | | §12 性能 / 成本 | [`04_web_app_plan.md`](04_web_app_plan.md) §10、[`10_governance.md`](10_governance.md) §4 P-W-6 | **深化补充** | --- ## 14. M1 验收清单 ### 14.1 12 条 checkbox 验收(M1 = MVP 8 周窗口内必达) - [ ] **M1-01**:`assets` 表通过 §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**:自动初筛能拒 NSFW(score ≥ 0.9)/ 拒大于 50k 三角面数 / 拒含人脸贴图 - [ ] **M1-06**:审核员在 `/admin/assets` 工作台 approve 后,资产能在 AssetPicker 中通过 E-A3 被搜到 - [ ] **M1-07**:AssetPicker 按 `semantic_class + bbox_filter + style_tag` 复合查询响应 < 200 ms(100k 行规模) - [ ] **M1-08**:[`05_object_replacement_handbook.md`](05_object_replacement_handbook.md) §15 的 **AL-1 ~ AL-10 全部 10 条契约**逐条达成(详见 §14.2 对照表) - [ ] **M1-09**:资产被 use_count 累计 ≥ 100 后 `assets.trust_score` 自动 +10(trigger 实测) - [ ] **M1-10**:用户能从 `/me/assets` 创建收藏夹并放入 ≥ 5 个资产,公开后他人可见 - [ ] **M1-11**:CC-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`](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.webp`(128×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 ms,AssetPicker 卡顿 | §12.3 触发条件:> 100k 行 → outbox 同步到 Meilisearch;Postgres 保留为权威源(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`](10_governance.md) §4 P-W-4 决策严格一致(MVP 全 CC0,v0.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`](05_object_replacement_handbook.md) 物品替换闭环 | 资产库提供的元数据足以让 AssetPicker 在 200 ms 内做 OBB 对齐替换 | | **MVP CC0 强制 / v0.5 CC-BY 二选一** | 法务 / 运营 | 与 [`10_governance.md`](10_governance.md) §4 P-W-4 决策一致 | | **iOS App 仅消费不生产** | iOS 团队 | MVP 不接 UGC 上传;v0.5 起评估 Object Capture 轻量入口 |