1247 lines
65 KiB
Markdown
1247 lines
65 KiB
Markdown
---
|
||
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` 分隔;格式:`"<creator_name>" <https://url> 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<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/+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<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 起开放)
|
||
|
||
| 步骤 | CC0(MVP) | CC-BY(v0.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`](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 `<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`](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=<asset_id>'` |
|
||
| 重复上传同一文件 | 同 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 `<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_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 = <parent>
|
||
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 轻量入口 |
|
||
|
||
|
||
|