dbcbdbb59e
- 处理: 74 个 .md 文件 - 跳过: 0 个(无已存在的 front matter) - 异常: 3 个(H1 缺失,用文件名兜底) - plans/PRISM/.research/readmes/3d-llm.md - plans/PRISM/.research/readmes/openmask3d.md - plans/PRISM/.research/readmes/openscene.md
698 lines
40 KiB
Markdown
698 lines
40 KiB
Markdown
---
|
||
title: "CrowdRoom · iOS App 设计(v0.2)"
|
||
date: 2026-05-20
|
||
draft: false
|
||
tags: ["CrowdRoom", "众包", "3D 重建", "导航", "隐私", "iOS"]
|
||
categories: ["worldmodel"]
|
||
---
|
||
|
||
# CrowdRoom · iOS App 设计(v0.2)
|
||
|
||
> **版本**:v0.2(2026-05-19)
|
||
> **v0.2 修订**:回写 G-4(§8.4 PrivacyManifest 新节,Apple 强制项)、G-5(§8.1 `NSLocationWhenInUseUsageDescription` 文案明确「精确 GPS 不会上传」)、G-8(§1.2 IA 跳转图追加 `PrivacyDetail` 节点)。源决策见 [`09_privacy.md`](09_privacy.md) §4 P-2 / P-3 与 §6.2 C-P2-6。
|
||
|
||
> 本章承接 [`00_overview.md`](00_overview.md) §4 架构图、[`01_data_schema.md`](01_data_schema.md) 的 9 张表与 `layer_manifest.json` schema、以及 [`02_api_contract.md`](02_api_contract.md) §2 的 19 个端点(v0.2,原 16 个 + 3 个新增)与 §8.1 的 5 条 iOS 硬契约(iOS-X1 ~ iOS-X5),落地为一份**可直接交付给 iOS 工程团队**的 App 设计。
|
||
>
|
||
> 复用 [`plans/iphone/iphone_simplified_plan.md`](../iphone/iphone_simplified_plan.md) 的扫描引导思想与 [`plans/iphone/roomplan_accuracy_and_cad_export.md`](../iphone/roomplan_accuracy_and_cad_export.md) 的 RoomPlan 精度边界,但裁剪到「**只上传、不出图**」的轻量端。
|
||
>
|
||
> **iOS 端只做三件事:采集(RoomPlan)+ 端侧脱敏(Vision + CIGaussianBlur)+ 上传到 Supabase**。Remix 编辑、Web 渲染、转码、审核——一概不做。
|
||
|
||
---
|
||
|
||
## 1. iOS App 信息架构(IA)
|
||
|
||
### 1.1 整体导航:5 Tab + 模态扫描
|
||
|
||
CrowdRoom iOS 采用 5 个底部 Tab + 一个独立模态扫描页的结构。**扫描入口故意放在 Tab Bar 正中间作为大按钮**(参考 Instagram Reels 的 + 设计),强化「采集」是 App 的第一动作。
|
||
|
||
| Tab | 图标 | 功能 |
|
||
|-----|------|------|
|
||
| 1. 发现 | 🔭 | 公共 feed、搜索、标签筛选 |
|
||
| 2. 通知 | 🔔 | 我的房间被 Remix / 评论 / 点赞、转码完成通知 |
|
||
| 3. 扫描 ★ | +(中间凸起大按钮) | **唤起 RoomCaptureView 模态**,全屏覆盖;扫描结束回 Tab 1 或我的 |
|
||
| 4. 我的房间 | 📁 | 我上传的 / 草稿 / 失败需要重试 |
|
||
| 5. 我的 | 👤 | Profile、配额、设置、登录态 |
|
||
|
||
> Remix 编辑入口**只在 Web 端**(见 [`02_api_contract.md`](02_api_contract.md) §4),iOS 端只提供「在浏览器中打开并 Remix」的深链接跳转,不放独立 Tab。
|
||
|
||
### 1.2 页面跳转图
|
||
|
||
```mermaid
|
||
graph TD
|
||
Launch[启动页]
|
||
Auth[登录 注册页]
|
||
AppleLogin[Sign in with Apple]
|
||
EmailLogin[邮箱登录]
|
||
|
||
TabDiscover[Tab1 发现 Feed]
|
||
TabNotif[Tab2 通知列表]
|
||
TabScan[Tab3 扫描入口大按钮]
|
||
TabMyRooms[Tab4 我的房间]
|
||
TabProfile[Tab5 我的]
|
||
|
||
SearchPage[搜索 Tag 筛选页]
|
||
RoomDetail[房间详情 360 预览]
|
||
CommentList[评论列表]
|
||
ReportSheet[举报弹层]
|
||
RemixLaunchSheet[Remix 跳转确认弹层]
|
||
WebRemix[Safari 跳转 Web Remix 编辑器]
|
||
|
||
ScanIntro[扫描引导页 三步]
|
||
ScanCapture[RoomPlan 扫描中]
|
||
ScanReview[扫描完成预览页]
|
||
Redacting[端侧脱敏进度页]
|
||
UploadForm[上传表单 标题 标签 可见性]
|
||
UploadProgress[上传进度页]
|
||
TranscodeWait[等待转码页 Realtime]
|
||
UploadDone[发布完成页]
|
||
|
||
DraftList[草稿列表]
|
||
QuotaPage[配额详情页]
|
||
SettingsPage[设置页]
|
||
PrivacyPage[隐私脱敏策略页]
|
||
PrivacyDetail[单房间 隐私详情 脱敏检测框列表]
|
||
AccountDelete[注销账号并删除全部数据]
|
||
|
||
Launch --> Auth
|
||
Auth --> AppleLogin
|
||
Auth --> EmailLogin
|
||
AppleLogin --> TabDiscover
|
||
EmailLogin --> TabDiscover
|
||
|
||
TabDiscover --> SearchPage
|
||
TabDiscover --> RoomDetail
|
||
SearchPage --> RoomDetail
|
||
RoomDetail --> CommentList
|
||
RoomDetail --> ReportSheet
|
||
RoomDetail --> RemixLaunchSheet
|
||
RemixLaunchSheet --> WebRemix
|
||
|
||
TabScan --> ScanIntro
|
||
ScanIntro --> ScanCapture
|
||
ScanCapture --> ScanReview
|
||
ScanReview --> Redacting
|
||
Redacting --> UploadForm
|
||
UploadForm --> UploadProgress
|
||
UploadProgress --> TranscodeWait
|
||
TranscodeWait --> UploadDone
|
||
UploadDone --> RoomDetail
|
||
|
||
TabNotif --> RoomDetail
|
||
TabMyRooms --> RoomDetail
|
||
TabMyRooms --> DraftList
|
||
DraftList --> UploadForm
|
||
|
||
TabProfile --> QuotaPage
|
||
TabProfile --> SettingsPage
|
||
SettingsPage --> PrivacyPage
|
||
SettingsPage --> AccountDelete
|
||
TabMyRooms --> RoomDetail
|
||
RoomDetail --> PrivacyDetail
|
||
```
|
||
|
||
> 共 **24 个页面节点**(v0.2,原 22 个 + v0.2 / G-8 追加 `PrivacyDetail` + v0.2 / P-4 配套追加 `AccountDelete`)。
|
||
>
|
||
> 🔄 **v0.2 — 回写自 G-8**:上图追加 `PrivacyDetail` 节点(路径 `我的房间 → 单房间详情 → 隐私详情`),对应 [`09_privacy.md`](09_privacy.md) §4 P-2 「owner 可在『我的房间 → 隐私详情』查看每个 bbox 缩略图并对漏检/误检发起『申请重做』工单」。`AccountDelete` 节点为 [`09_privacy.md`](09_privacy.md) §4 P-4 三阶段注销流程的 iOS 入口(路径 `Tab5 我的 → 设置 → 注销账号并删除全部数据`),调用 [`02_api_contract.md`](02_api_contract.md) §2 E-18 `account-delete`。
|
||
|
||
---
|
||
|
||
## 2. 核心用户流程(Critical Flows)
|
||
|
||
### 2.1 Flow A:扫描 → 端侧脱敏 → 三段式上传 → 等待转码 → 发布
|
||
|
||
> ✅ 契约 iOS-X1(端侧人脸检测 + 高斯模糊)、iOS-X2(三段式上传)、iOS-X3(JSON 原样)、iOS-X4(Realtime 订阅)、iOS-X5(预查配额)全部出现在此流程
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
autonumber
|
||
participant U as 用户
|
||
participant App as iOS App
|
||
participant V as Vision CoreImage
|
||
participant E as Edge Function
|
||
participant S as Supabase Storage
|
||
participant DB as Postgres
|
||
participant W as Transcode Worker
|
||
participant RT as Realtime Channel
|
||
|
||
U->>App: 点击 Tab 中间 + 按钮
|
||
Note over App: iOS-X5 进扫描页前预查配额
|
||
App->>E: GET functions v1 quota
|
||
E-->>App: tier free storage used limit
|
||
alt 配额超限
|
||
App-->>U: 本地拦截 弹窗提示升级
|
||
end
|
||
|
||
U->>App: 走完三步引导 开始 RoomCaptureView
|
||
App->>App: RoomPlan 采集 5 10 分钟
|
||
U->>App: 点击完成
|
||
App->>App: 导出 source usdz 与 roomplan json
|
||
Note over App: iOS-X3 RoomPlan JSON 原样保留 不做坐标变换
|
||
|
||
Note over App,V: iOS-X1 端侧脱敏开始
|
||
App->>V: 解压 usdz 遍历每张贴图 PNG JPEG
|
||
V->>V: VNDetectFaceRectanglesRequest
|
||
V-->>App: 命中 face bbox list
|
||
App->>V: CIGaussianBlur radius 18 仅作用于命中区域
|
||
V-->>App: 模糊后贴图
|
||
App->>App: 重新打包 usdz 同时把 face bbox 累积到 redactions
|
||
|
||
U->>App: 填写标题 标签 可见性 公开 私有
|
||
App->>E: POST upload-init title tags bytes_source
|
||
E->>DB: insert rooms 与 room_versions status uploading
|
||
E->>S: sign presigned URL usdz 与 json
|
||
E-->>App: version_id presigned_usdz presigned_json
|
||
Note over App,S: iOS-X2 三段式 第二段 直传 Storage
|
||
App->>S: PUT source usdz 大于 30 MB 走分片 multipart
|
||
App->>S: PUT source roomplan json
|
||
App->>E: POST upload-complete version_id redactions
|
||
E->>DB: update status queued insert redactions rows
|
||
E->>W: POST enqueue version_id
|
||
|
||
Note over App,RT: iOS-X4 订阅 Realtime 不轮询
|
||
App->>RT: subscribe channel room room_id
|
||
W->>W: usdz to glb 分层标注 manifest 缩略图
|
||
W->>E: POST transcode-done status ready paths
|
||
E->>DB: update status ready current_version_id
|
||
E->>RT: broadcast event ready version_id
|
||
RT-->>App: WebSocket push 收到 ready
|
||
App-->>U: 弹卡片 你的扫描已上线 跳房间详情
|
||
```
|
||
|
||
### 2.2 Flow B:浏览 → 房间详情 → 触发 Remix → 跳 Web
|
||
|
||
> iOS 端不做 Remix 编辑(编辑在 Web 端 R3F 渲染器里),仅做发起入口。Universal Link 透传 `room_id + version_id`,落地 Web 域名 `crowdroom.app/r/{room_id}?remix=1`。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
autonumber
|
||
participant U as 用户
|
||
participant App as iOS App
|
||
participant PG as PostgREST
|
||
participant CDN as CDN
|
||
participant SF as SFSafariViewController
|
||
|
||
U->>App: Tab 发现 下拉刷新
|
||
App->>PG: GET rest v1 rooms visibility eq public order created_at desc limit 20
|
||
PG-->>App: Room 列表 含 thumbnail_path
|
||
App->>CDN: GET thumbnail webp 列表卡片
|
||
CDN-->>App: 缩略图
|
||
|
||
U->>App: 点击某张卡片
|
||
App->>PG: GET rest v1 rooms id eq room_id with current_version
|
||
PG-->>App: Room 详情
|
||
|
||
Note over App: iOS 端仅展示静态 360 缩略图轮播
|
||
Note over App: 不在端内渲染 glb 太重 留给 Web
|
||
|
||
U->>App: 点击 Remix 按钮
|
||
App-->>U: 弹层 提示 Remix 编辑器在浏览器中体验更好
|
||
U->>App: 点击 确认
|
||
App->>SF: 打开 https crowdroom app r room_id remix 1
|
||
SF-->>U: 进入 Web Remix 编辑器
|
||
```
|
||
|
||
### 2.3 Flow C:通知到达 → 跳详情
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
autonumber
|
||
participant RT as Realtime Channel
|
||
participant App as iOS App 后台 或 前台
|
||
participant UN as UNUserNotificationCenter
|
||
participant U as 用户
|
||
participant PG as PostgREST
|
||
|
||
Note over App,RT: App 启动时 已订阅 user user_id 个人频道
|
||
RT-->>App: push event remix_created 或 comment_new
|
||
alt App 在前台
|
||
App-->>U: Toast 提示 你的房间被 Remix
|
||
else App 在后台
|
||
App->>UN: scheduleLocalNotification 标题 房间被 Remix
|
||
UN-->>U: 系统通知中心展示
|
||
U->>UN: 点击通知
|
||
UN->>App: 唤起 app 携带 deeplink room_id
|
||
end
|
||
App->>PG: GET rest v1 rooms id eq room_id
|
||
PG-->>App: Room 详情
|
||
App-->>U: 跳房间详情页 评论列表锚到该条
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 关键技术栈与库选型表
|
||
|
||
| 模块 | 推荐方案 | 备选 | 一行理由 |
|
||
|------|---------|------|---------|
|
||
| 语言 / 最低 iOS | **Swift 5.9 + iOS 17.0** | iOS 16.0 | RoomPlan API 在 17+ 增加了 second-pass mesh 优化与更稳的 `MultiRoom` 支持;Realtime SDK 也对 17 友好 |
|
||
| UI 框架 | **SwiftUI 主,UIKit 局部桥接** | 纯 UIKit | SwiftUI 写 Tab/列表/表单效率高;扫描页的 `RoomCaptureView` 用 `UIViewControllerRepresentable` 桥接 |
|
||
| RoomPlan 集成 | **`RoomCaptureView` 标准引导** | `RoomCaptureSession` 自定义 | MVP 不重写引导动画;标准 View 自带语音/手势提示,省 2 周 UX 工时;后续如要换皮再切 Session |
|
||
| 端侧人脸检测 | **Vision `VNDetectFaceRectanglesRequest`** | CoreML + 自训模型 | 系统级、无依赖、iPhone 13+ 单张 1024×1024 < 80 ms;Revision 3 召回率够 |
|
||
| 贴图脱敏 | **Core Image `CIGaussianBlur` + 蒙版合成** | Metal Shader 自写 | CIFilter 链路成熟、GPU 加速、`CIContext` 直接渲到 `CGImage` 写回 PNG |
|
||
| .usdz 解包/重打包 | **`ModelIO` + `USDKit`(iOS 17)+ `Compression` framework** | 外挂 `usdzconvert` CLI | iOS 沙箱不能跑 CLI;`ModelIO` 支持读 USD,贴图替换走解包→改文件→重压 zip(usdz 本质是 zip) |
|
||
| 网络层 | **URLSession + async/await + `URLSessionUploadTask`(background config)** | Alamofire | 无三方依赖;`backgroundSessionConfiguration` 是 App 进后台续传的唯一官方路径 |
|
||
| Supabase SDK | **[supabase-swift](https://github.com/supabase-community/supabase-swift) ≥ 2.0** | 手撸 REST + WebSocket | 官方维护、Realtime channel + Auth + Storage 三件套统一 SDK |
|
||
| Realtime 监听 | **`supabase-swift` Realtime channel `room:{id}` + `user:{id}`** | 自建 SSE | iOS-X4 强制要求订阅模型;SDK 处理重连与心跳 |
|
||
| 本地缓存 | **SwiftData(iOS 17)** | Core Data / Realm | 草稿、未完成上传任务、Feed 分页缓存;SwiftData 与 SwiftUI 双向绑定省胶水代码 |
|
||
| 性能/崩溃监控 | **MetricKit(系统)+ Sentry iOS SDK** | Firebase Crashlytics | MetricKit 拿 RoomPlan 期 GPU/热量数据;Sentry 与 Supabase 后端 Sentry 共享 issue 视图 |
|
||
| 深链接 | **Universal Links(apple-app-site-association)** | URL Scheme | Web Remix 编辑器跳回 App、通知点击跳详情都走 UL;URL Scheme 不安全且会被 Safari 拦截 |
|
||
| 图像渲染 | **`Image(uiImage:)` + `AsyncImage`** + 自建磁盘缓存 | Kingfisher / SDWebImage | 列表缩略图为主,自建 LRU 200 MB 够用;不引三方避免主线程阻塞 |
|
||
|
||
---
|
||
|
||
## 4. 端侧脱敏管线详细设计(iOS-X1 落地)
|
||
|
||
> ✅ 契约 iOS-X1:上传前必须在端侧跑人脸检测 + 贴图高斯模糊,并把命中区域写入 `redactions[]`,不依赖服务端二次脱敏。本节是 X1 的**完整工程落地**。
|
||
|
||
### 4.1 管线流程
|
||
|
||
```mermaid
|
||
graph TD
|
||
A[RoomPlan 导出 source usdz] --> B[Compression 解包 usdz 到 tmp redact 目录]
|
||
B --> C[ModelIO 枚举所有 MDLTexture 引用]
|
||
C --> D[拿到贴图文件路径列表 PNG JPEG]
|
||
D --> E[对每张贴图执行 Vision 人脸检测]
|
||
E --> F{检测到人脸}
|
||
F -- 无 --> G[原图不动]
|
||
F -- 有 --> H[CIGaussianBlur radius 18 渲染整图]
|
||
H --> I[用人脸 bbox 做蒙版 仅模糊区域合成回原图]
|
||
I --> J[写回贴图文件 同名覆盖]
|
||
G --> K[累计到 redactions list]
|
||
J --> K
|
||
K --> L[Compression 重新打包 usdz]
|
||
L --> M[校验 sha256 与体积 失败回滚]
|
||
M --> N[redactions 数组准备 POST 到 upload-complete]
|
||
```
|
||
|
||
### 4.2 Swift 伪代码骨架
|
||
|
||
```swift
|
||
import RoomPlan
|
||
import Vision
|
||
import CoreImage
|
||
import ModelIO
|
||
import Compression
|
||
|
||
// 脱敏管线主入口
|
||
// 输入 RoomPlan 导出的 sourceUSDZ URL
|
||
// 输出 脱敏后的新 usdz URL 与 redactions 数组
|
||
func redactUSDZ(at sourceURL: URL) async throws -> (URL, [Redaction]) {
|
||
// 1 解包 usdz 实质是无压缩 zip 容器
|
||
let workDir = FileManager.default.temporaryDirectory
|
||
.appendingPathComponent("redact_\(UUID().uuidString)")
|
||
try unzipUSDZ(source: sourceURL, into: workDir)
|
||
|
||
// 2 用 ModelIO 枚举所有贴图引用
|
||
let asset = MDLAsset(url: workDir.appendingPathComponent("scene.usdc"))
|
||
let textureURLs = collectTextureURLs(in: asset, workDir: workDir)
|
||
|
||
// 3 对每张贴图跑 Vision 检测 + 模糊
|
||
var redactions: [Redaction] = []
|
||
let ctx = CIContext(options: [.useSoftwareRenderer: false])
|
||
|
||
for texURL in textureURLs {
|
||
guard let cgImage = loadCGImage(texURL) else { continue }
|
||
|
||
// 3 1 Vision 人脸检测
|
||
let request = VNDetectFaceRectanglesRequest()
|
||
request.revision = VNDetectFaceRectanglesRequestRevision3
|
||
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
|
||
try handler.perform([request])
|
||
guard let faces = request.results, !faces.isEmpty else { continue }
|
||
|
||
// 3 2 命中 用 CIGaussianBlur 整图模糊
|
||
let ciOriginal = CIImage(cgImage: cgImage)
|
||
let blurred = ciOriginal
|
||
.applyingFilter("CIGaussianBlur", parameters: [kCIInputRadiusKey: 18.0])
|
||
.cropped(to: ciOriginal.extent)
|
||
|
||
// 3 3 把人脸 bbox 转成蒙版图 把模糊层贴回原图
|
||
let maskCI = buildFaceMask(faces: faces, extent: ciOriginal.extent)
|
||
let composited = blurred.applyingFilter(
|
||
"CIBlendWithMask",
|
||
parameters: [
|
||
kCIInputBackgroundImageKey: ciOriginal,
|
||
kCIInputMaskImageKey: maskCI
|
||
]
|
||
)
|
||
|
||
// 3 4 写回贴图文件 同名覆盖 保持 ModelIO 引用不变
|
||
guard let outCG = ctx.createCGImage(composited, from: composited.extent) else { continue }
|
||
try writePNG(outCG, to: texURL)
|
||
|
||
// 3 5 记录到 redactions 数组 上传时 POST 给 Edge Function
|
||
for face in faces {
|
||
redactions.append(Redaction(
|
||
textureRef: texURL.lastPathComponent,
|
||
kind: "face",
|
||
region: face.boundingBox, // 归一化坐标 0 1
|
||
method: "gaussian_blur_r18"
|
||
))
|
||
}
|
||
}
|
||
|
||
// 4 重新打包 usdz 校验
|
||
let redactedURL = workDir.appendingPathComponent("redacted.usdz")
|
||
try zipUSDZ(folder: workDir, output: redactedURL)
|
||
try validatePackaged(redactedURL)
|
||
|
||
return (redactedURL, redactions)
|
||
}
|
||
```
|
||
|
||
> 关键点:`usdz` 是无压缩 zip,重新打包必须用 `compression_encode_buffer` 的 `COMPRESSION_LZFSE_NONE` 或直接走 store-only zip——否则 Apple 工具链识别不出来。
|
||
|
||
### 4.3 性能预算
|
||
|
||
| 阶段 | iPhone 15 Pro 目标 | iPhone 13 Pro 目标 | 备注 |
|
||
|------|------------------|------------------|------|
|
||
| 解包 60 MB usdz | < 0.5 s | < 1.0 s | I/O bound |
|
||
| Vision 检测 20 张 1024² 贴图 | < 2.5 s | < 4.5 s | Neural Engine |
|
||
| CIGaussianBlur + 合成 | < 1.5 s | < 2.5 s | GPU bound |
|
||
| 重打包 | < 0.5 s | < 1.0 s | I/O |
|
||
| **总计** | **≤ 8 s** | **≤ 12 s** | iPhone 12 Pro 预期 16 s 见 §10 风险 |
|
||
|
||
UI:脱敏期间显示带百分比的进度页,文案「正在检查照片中的人脸…」。
|
||
|
||
### 4.4 失败兜底
|
||
|
||
- **重试机制**:单张贴图脱敏失败(如 ModelIO 解不开纹理)→ 重试 1 次。整体管线失败 → 重试最多 3 次。
|
||
- **3 次失败后**:弹窗提示「我们没能自动模糊照片里的人脸。建议你**关闭含人的扫描区域重扫**,或**手动框选要模糊的区域**」,给出两个按钮:「重新扫描」与「手动标注后上传」。
|
||
- **手动标注降级**:进入一个 `UIScrollView` 缩略图墙,让用户长按贴图后框选矩形,前端写到 `redactions[kind=manual]`,仍走相同上传流。
|
||
- **「不脱敏直接上传」按钮一律不提供**——这是 iOS-X1 的硬约束。
|
||
|
||
---
|
||
|
||
## 5. 三段式上传实现(iOS-X2 落地)
|
||
|
||
> ✅ 契约 iOS-X2:上传走 `upload-init → 直传 Storage → upload-complete` 三步,禁止把 `.usdz` 字节流塞进 Edge Function body。
|
||
|
||
### 5.1 顺序图
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
autonumber
|
||
participant App as iOS App
|
||
participant E as Edge Function
|
||
participant S as Supabase Storage
|
||
|
||
App->>E: POST upload-init title tags bytes_source
|
||
E-->>App: version_id presigned_usdz presigned_json expires_at
|
||
App->>App: 创建 URLSession backgroundConfiguration identifier crowdroom upload version_id
|
||
par 并行 双 PUT
|
||
App->>S: PUT source usdz 大于 30 MB 走 multipart 5 MB chunk
|
||
App->>S: PUT source roomplan json
|
||
end
|
||
S-->>App: 200 OK 两次
|
||
App->>E: POST upload-complete version_id redactions
|
||
E-->>App: status queued
|
||
```
|
||
|
||
### 5.2 关键实现要点
|
||
|
||
| 场景 | 策略 |
|
||
|------|------|
|
||
| **大文件分片** | `.usdz > 30 MB` 启用 multipart PUT,5 MB 一片,并行 3 路;Supabase Storage 支持 S3 兼容的 multipart |
|
||
| **断点续传** | 用 `URLSessionUploadTask` + 自管理 `Range` 头;SwiftData 表 `upload_chunks` 记录每片状态(pending/sent/acked),App 重启后扫表续传 |
|
||
| **App 进后台** | `URLSession(configuration: .background(withIdentifier:))` 让系统在 App 被挂起后继续传;完成时通过 `application(_:handleEventsForBackgroundURLSession:completionHandler:)` 唤醒 |
|
||
| **网络切换** | 注册 `NWPathMonitor`,从 Wi-Fi 切到蜂窝时**暂停**上传并弹窗:「当前已切换到蜂窝网络,继续上传可能产生流量费用」(默认开关:仅 Wi-Fi) |
|
||
| **电量低于 20%** | 启动上传前查 `UIDevice.current.batteryLevel`,< 0.20 时弹窗「电量较低,是否仍继续上传?」并禁用 multipart 并行(降为 1 路) |
|
||
| **超时与退避** | 单片超时 60 s,整体 20 分钟;指数退避 5 s → 15 s → 45 s,3 次失败后转 `failed` 入草稿 |
|
||
| **presigned 过期** | `expires_at` 提前 30 s 触发 `upload-init` 重签;server-side 已设 15 min TTL,足以覆盖大文件 |
|
||
|
||
### 5.3 错误码 → 用户文案映射
|
||
|
||
| 业务错误码(来自 [`02_api_contract.md`](02_api_contract.md) §7) | iOS 文案 | 后续动作 |
|
||
|---|---|---|
|
||
| `QUOTA_EXCEEDED` | "本月上传额度已用完,下月 1 号重置" | 跳「我的 → 配额详情」 |
|
||
| `FILE_TOO_LARGE` | "扫描文件超过 50 MB,请尝试缩小扫描范围" | 跳「重新扫描」 |
|
||
| `INVALID_TAGS` | "标签数量超过 8 个或含非法字符" | 高亮表单标签输入框 |
|
||
| `STORAGE_FORBIDDEN` | "上传授权已过期,正在重新申请…" | 自动调 `upload-init` 重签 1 次 |
|
||
| `NETWORK_TIMEOUT`(本地判定) | "网络连接超时,已为你保存草稿" | 写入草稿表,Tab 4 可见 |
|
||
| `REDACTION_INVALID` | "脱敏信息格式错误,请重新扫描" | 跳「重新扫描」;同时 Sentry 上报(端 bug) |
|
||
| `ROOM_TRANSCODE_FAILED` | "云端处理失败" | 展示「重试」按钮 → `transcode-retry`;3 次失败后建议人工反馈 |
|
||
| `WORKER_UNAVAILABLE` | "服务繁忙,已自动加入队列,1 分钟后重试" | 60 s 后自动重发 `upload-complete` |
|
||
| `UNAUTHENTICATED` | "登录已过期,请重新登录" | 弹登录页(保留草稿) |
|
||
| `RATE_LIMITED` | "操作过于频繁,请稍后再试" | 30 s 冷却倒计时 |
|
||
|
||
---
|
||
|
||
## 6. Realtime 转码进度 UI(iOS-X4 落地)
|
||
|
||
> ✅ 契约 iOS-X4:监听 Realtime channel `room:{room_id}` 等待 `ready` 事件,**禁止轮询** `room_versions.status`。
|
||
|
||
### 6.1 等待页 UX
|
||
|
||
进入「等待转码」页后展示一个环形进度(CircularProgressView),中间显示阶段文案,下方有「在后台等待」与「取消并保存草稿」两个按钮。
|
||
|
||
| 阶段(来自 `room_versions.status` enum) | 文案 | 环形进度估算(无真实百分比,按阶段递增) |
|
||
|------|------|--------------------------------|
|
||
| `uploading` | "上传中…" | 0 → 25% |
|
||
| `queued` | "已加入处理队列" | 25% → 35% |
|
||
| `transcoding` | "解析中 / 分层中 / 生成预览…" | 35% → 90%(每收一次 progress payload +5%) |
|
||
| `ready` | "发布完成 🎉" | 100% |
|
||
| `failed` | "处理失败" | 红色 × ,展示重试按钮 |
|
||
|
||
> 阶段进度**没有真实百分比**(Worker 不上报中间百分比),iOS 端用「阶段映射 + 时间外推」假装平滑;Realtime payload 一旦到 `ready`,直接跳 100%。
|
||
|
||
### 6.2 Realtime 订阅代码骨架
|
||
|
||
```swift
|
||
let channel = supabase.realtime.channel("room:\(roomId)")
|
||
channel.on("broadcast", filter: .init(event: "transcode_progress")) { msg in
|
||
// 阶段切换 触发 UI 动画
|
||
Task { await viewModel.updatePhase(msg.payload["status"] as? String) }
|
||
}
|
||
channel.on("broadcast", filter: .init(event: "ready")) { msg in
|
||
Task { await viewModel.markReady(versionId: msg.payload["version_id"] as? String) }
|
||
}
|
||
channel.on("broadcast", filter: .init(event: "failed")) { msg in
|
||
Task { await viewModel.markFailed(error: msg.payload["error"] as? String) }
|
||
}
|
||
await channel.subscribe()
|
||
```
|
||
|
||
**重连**:Supabase SDK 内置 30 s 心跳 + 指数退避重连。断线期间错过的事件由 App 重连后**主动**调一次 `GET /rest/v1/room_versions?id=eq.{id}` 补查最终状态(这是唯一允许的「兜底查询」,不是轮询)。
|
||
|
||
### 6.3 后台模式与本地推送
|
||
|
||
- App 进后台时 Realtime channel 会被 iOS 暂停(WebSocket 不能在后台保活)
|
||
- 解决方案:App 进后台前若仍在等待转码 → 注册 `BGAppRefreshTask`,约 15 分钟后唤醒一次后台拉取
|
||
- 后台任务执行时调 `GET /rest/v1/room_versions?id=eq.{id}` 单次查询,若已 `ready` → 通过 `UNUserNotificationCenter` 发本地通知「你的扫描已发布」
|
||
- 不依赖服务端 APNs push(MVP 不接苹果推送证书),全部走本地通知
|
||
|
||
### 6.4 失败重试入口
|
||
|
||
- `failed` 事件到达时,等待页转为「失败页」,展示错误码对应文案 + 「重试」按钮
|
||
- 「重试」按钮调 `POST /functions/v1/transcode-retry`(02_api_contract 未列,作为 iOS 期望接口提给后端)
|
||
- 失败 3 次后,「重试」按钮隐藏,改显「联系客服」入口(跳邮件 `mailto:`)
|
||
|
||
---
|
||
|
||
## 7. 扫描引导 UX 与质量门控
|
||
|
||
> 复用 [`plans/iphone/iphone_simplified_plan.md`](../iphone/iphone_simplified_plan.md) §3.1 的「准备 / 慢速移动 / 覆盖检查」三阶段思想,但裁剪为消费级三步引导。精度边界引用 [`plans/iphone/roomplan_accuracy_and_cad_export.md`](../iphone/roomplan_accuracy_and_cad_export.md) §1.2 的 ±3 cm 墙面 / ±5 cm 家具。
|
||
|
||
### 7.1 三步引导
|
||
|
||
| 步骤 | 标题 | 文案与示意 | 用户操作 |
|
||
|------|------|----------|---------|
|
||
| **Step 1 · 准备** | "把房间整理一下" | 三条建议:① 打开所有灯 ② 移走移动物体(宠物/人) ③ 清洁 LiDAR 镜头 | 点「我准备好了」 |
|
||
| **Step 2 · 慢走** | "用 0.3 m/s 的速度环绕房间" | 短视频示意 + RoomCaptureView 自带的语音引导一起播 | 进入 `RoomCaptureView`,扫描 5–10 分钟 |
|
||
| **Step 3 · 完成** | "看一下你的房间" | 展示 RoomPlan 生成的 wireframe 预览 + 质量分 | 「重扫」或「下一步:脱敏并上传」 |
|
||
|
||
### 7.2 实时质量提示(扫描中浮层)
|
||
|
||
在 `RoomCaptureView` 上叠加一个 SwiftUI 浮层,每 1 s 从 `RoomCaptureSession.Delegate` 读取一次状态:
|
||
|
||
| 信号 | 检测方式 | 浮层提示 |
|
||
|------|---------|---------|
|
||
| **覆盖率不足** | RoomPlan `instructions == .lowTexture` 或墙面置信度 `.low` 的占比 > 30% | "🔍 这面墙再扫一遍" |
|
||
| **漏扫墙面** | 房间未闭合(拓扑检测:墙数 < 3 或存在自由端) | "↩️ 似乎少了一面墙" |
|
||
| **强反光面** | 检测到 `Window`、`Mirror`(用 RoomPlan category)正对镜头 | "⚠️ 镜面会影响精度,请侧 30° 扫描" |
|
||
| **速度过快** | ARKit transform 一阶差分 > 0.6 m/s 持续 2 s | "🐢 慢一点,0.3 m/s 最佳" |
|
||
|
||
### 7.3 完成后质量分(A/B/C)
|
||
|
||
扫描结束后用一个简单规则给房间打分(**不是机器学习模型**,是规则引擎,避免 MVP 复杂度):
|
||
|
||
| 维度 | A 档 | B 档 | C 档 |
|
||
|------|------|------|------|
|
||
| 墙面 confidence high 占比 | ≥ 90% | 70–90% | < 70% |
|
||
| 家具识别数 | ≥ 5 件 | 2–4 件 | < 2 件 |
|
||
| 房间闭合 | ✅ | ✅ | ❌ 拓扑不闭合 |
|
||
| 扫描时长 | 5–10 min | 3–5 min | < 3 min |
|
||
|
||
**任一维度命中 C → 整体 C 档**。
|
||
|
||
- **A/B 档**:直接进上传流,UI 给绿勾或黄勾标记
|
||
- **C 档**:弹窗「这次扫描质量较低,建议重扫;如仍上传,我们会给它打『C 档』标签,发现页排序权重会降低」——**仍允许上传**(社区数据不挑食),但 `rooms.quality_grade` 字段标 C,[`02_api_contract.md`](02_api_contract.md) E-06 列表查询时排序权重 × 0.5
|
||
|
||
---
|
||
|
||
## 8. 隐私与权限请求文案
|
||
|
||
### 8.1 `Info.plist` 条目
|
||
|
||
| Key | 中文 usage description |
|
||
|-----|----------------------|
|
||
| `NSCameraUsageDescription` | "CrowdRoom 需要相机权限来扫描你的房间。我们只会使用相机进行 3D 建模,不会单独保存照片到相册。" |
|
||
| `NSPhotoLibraryAddUsageDescription` | "CrowdRoom 需要相册写入权限,以便把你扫描完成的 3D 房间预览图保存到相册(可选)。" |
|
||
| `NSLocationWhenInUseUsageDescription` | **v0.2 文案**:"CrowdRoom 可选使用你的位置,仅为给你扫描的房间打上「城市」级标签(约 5 km 精度),**该城市标签会显示在你的公开房间页**;**你的精确 GPS 坐标不会上传、不会存储到服务器**。你可以随时在「设置 → 隐私 → 位置打标」里关闭。" |
|
||
| `NSMicrophoneUsageDescription` | **不申请**(RoomPlan 不需要录音) |
|
||
| `NSUserTrackingUsageDescription` | **不申请**(MVP 不做跨 App 跟踪,见 8.2) |
|
||
|
||
> 🔄 **v0.2 — 回写自 G-5**:本节根据 [`09_privacy.md`](09_privacy.md) §4 P-3 决策追加。`NSLocationWhenInUseUsageDescription` 文案在 v0.2 起必须显式包含「**城市标签会显示在公开房间页**」与「**精确 GPS 不会上传**」两句——前者满足 PR-3 用户可控的「授权时即告知公开后果」,后者满足 PR-2 最小数据采集的「告知不持有原始坐标」。该文案变更同时对齐 [`01_data_schema.md`](01_data_schema.md) §3.2 `rooms.location_city`(= `location_label` 别名)字段语义与 [`04_web_app_plan.md`](04_web_app_plan.md) §8.2 详情页元信息行。
|
||
|
||
### 8.2 ATT(App Tracking Transparency)
|
||
|
||
**结论:MVP 不申请 ATT**。
|
||
|
||
- CrowdRoom MVP 不接广告 SDK、不与第三方数据公司共享 IDFA
|
||
- Sentry / MetricKit 不使用 IDFA,走 Apple 私有指标
|
||
- 后续如接入 Apple Search Ads 归因,再单独申请 ATT 并补充权限弹窗文案
|
||
- 这一决策与本任务规划的「隐私治理」预留接口由子任务 5 收口
|
||
|
||
### 8.3 首次启动权限弹窗顺序
|
||
|
||
1. 进入扫描页 → 申请 `Camera`(必要)
|
||
2. 扫描完成后想保存预览图到相册 → 申请 `PhotoLibraryAdd`(按需)
|
||
3. 上传表单页填写「城市」标签时 → 申请 `LocationWhenInUse`(按需,可跳过)
|
||
|
||
> ✅ 契约 iOS-X5 衍生:进入扫描页时同时调 `GET /functions/v1/quota`,与相机权限请求**并行**发起,弹窗与配额预检不互相阻塞。
|
||
|
||
### 8.4 PrivacyManifest(`PrivacyInfo.xcprivacy`)
|
||
|
||
> 🔄 **v0.2 — 回写自 G-4**:本节根据 [`09_privacy.md`](09_privacy.md) §6.2 C-P2-6(Apple 2024 春起强制,本已 cross-ref 为「MVP 待补」)新增。Apple App Store Connect 在 Xcode 15+ 上传时会自动跑 **Privacy Manifest Aggregate Report**;缺失或第三方 SDK 未声明会**直接拒审**。
|
||
|
||
#### 8.4.1 我们自己声明(CrowdRoom App bundle 内)
|
||
|
||
`PrivacyInfo.xcprivacy` 必须列出 4 类 Required Reason API 使用类别(CrowdRoom 业务场景下能触发的常见类别):
|
||
|
||
| `NSPrivacyAccessedAPICategory` 键 | 触发场景 | 选择的 reason code |
|
||
|---|---|---|
|
||
| `NSPrivacyAccessedAPICategoryFileTimestamp` | 读 `.usdz` / `.roomplan.json` 文件 `creationDate` 用于上传顺序排序 | `C617.1`(在 App 内用于显示给用户) |
|
||
| `NSPrivacyAccessedAPICategorySystemBootTime` | MetricKit / Sentry 性能事件相对时间戳 | `35F9.1`(计算相对设备启动时间) |
|
||
| `NSPrivacyAccessedAPICategoryDiskSpace` | 扫描前预估「设备剩余空间是否够暂存原始 .usdz」 | `85F4.1`(在 App 内显示空间不足提示) |
|
||
| `NSPrivacyAccessedAPICategoryUserDefaults` | 持久化「位置打标开关 / 端侧脱敏统计」等用户偏好 | `CA92.1`(同一 App 内读写) |
|
||
|
||
同时声明 **数据收集分类**(`NSPrivacyCollectedDataTypes`),与 App Store Connect Privacy Nutrition Label 同源:
|
||
|
||
| 数据类型 | 是否收集 | 用途 | 关联用户 |
|
||
|---|---|---|---|
|
||
| `NSPrivacyCollectedDataTypeUserID` | ✅ | `App Functionality`(鉴权) | 关联 |
|
||
| `NSPrivacyCollectedDataTypeEmailAddress` | ✅ | `App Functionality`(账号恢复) | 关联 |
|
||
| `NSPrivacyCollectedDataTypeOtherUserContent`(房间几何 + 标题 + 评论) | ✅ | `App Functionality` | 关联 |
|
||
| `NSPrivacyCollectedDataTypeCoarseLocation`(城市级 5 km) | ✅(可选) | `App Functionality`(公开标签) | 关联 |
|
||
| `NSPrivacyCollectedDataTypePreciseLocation` | ❌ | — | — |
|
||
| `NSPrivacyCollectedDataTypeCrashData` | ✅ | `App Functionality`(Sentry) | 不关联 |
|
||
| `NSPrivacyCollectedDataTypePerformanceData` | ✅ | `Analytics`(PostHog,需 opt-in) | 不关联 |
|
||
| `NSPrivacyCollectedDataTypeAdvertisingData` | ❌ | — | — |
|
||
| `NSPrivacyCollectedDataTypeTrackingID`(IDFA) | ❌ | — | — |
|
||
|
||
#### 8.4.2 第三方 SDK PrivacyManifest 自查表
|
||
|
||
每个引入的 SDK 都必须**自带** `PrivacyInfo.xcprivacy`(Apple 维护一份强制 SDK 名单:`OpenSSL / FMDB / SQLite` 等通用基础库以及主流分析/广告 SDK 都在其中)。MVP 引入的 SDK 自查状态:
|
||
|
||
| SDK | 是否在 Apple 强制列表 | SDK 已自带 PrivacyManifest? | 版本下限 | 备注 |
|
||
|---|---|---|---|---|
|
||
| **Supabase Swift**(GoTrue + PostgREST + Storage + Realtime) | ⚠ 部分依赖(如 SQLite)在列 | ✅ 自 v2.0+ 已自带;底层 `URLSession` 无需 | v2.5.0+ | Apple 通用网络栈无需自带 |
|
||
| **Sentry-Cocoa** | ✅ 在列 | ✅ 自 v8.20+ 自带 | v8.25+ | 关键:必须升级到 ≥ v8.20 才能过审 |
|
||
| **PostHog iOS** | ✅ 在列(涉及 `UserDefaults` + `SystemBootTime`) | ✅ 自 v3.0+ 自带 | v3.5+ | MVP 仅在用户 opt-in 后初始化 |
|
||
| **Lucide Icons**(仅 SVG 资源,无 runtime) | ❌ 不在列 | n/a | n/a | 资源包,不申报 |
|
||
| **Apple Vision / RoomPlan / ARKit** | n/a | n/a(系统框架) | iOS 17+ | 系统框架不计入第三方 |
|
||
|
||
> **CI 检查项**:在 GitHub Actions 上跑 `xcodebuild` 时附加 `-checkPrivacyManifest YES`(Xcode 15.3+ 支持的隐式 lint),任何缺失会直接 build fail。
|
||
|
||
#### 8.4.3 上传到 App Store Connect 时的签名验证清单
|
||
|
||
按以下清单逐项勾选,否则放弃发版:
|
||
|
||
1. ☐ Xcode 项目根目录下存在 `PrivacyInfo.xcprivacy`(不是 `Resources/` 子目录)
|
||
2. ☐ Archive 后 `.ipa` 解包,`PrivacyInfo.xcprivacy` 出现在主 bundle 根
|
||
3. ☐ 所有 `Frameworks/` 下的第三方 `.framework` / `.xcframework` 内部存在 `PrivacyInfo.xcprivacy`
|
||
4. ☐ App Store Connect 上传后 24h 内查看 **Privacy Manifest Aggregate Report**,确认无 `Missing reason code` 与 `Missing data type` 警告
|
||
5. ☐ 如有警告 → 找到对应 SDK 升级版本 → 重新 archive 上传
|
||
6. ☐ 与 App Store Connect 「应用隐私详情」(Privacy Nutrition Label)字段逐项核对(同源;不一致会被 Apple 人工标红)
|
||
|
||
> **拒审风险等级**:高。Apple 2024 春起对漏报的 PrivacyManifest 直接拒审(不再警告);本节的 6 项 checklist 必须在每次 minor 版发版前由 release manager 复核一次。
|
||
|
||
---
|
||
|
||
## 9. MVP 范围与不做项
|
||
|
||
### 9.1 MVP(与 [`00_overview.md`](00_overview.md) §7.1 同步,8 周窗口)能交付
|
||
|
||
| 模块 | 交付物 |
|
||
|------|--------|
|
||
| Auth | Sign in with Apple + 邮箱登录两条路径 |
|
||
| Tab 1 发现 | 公共 feed 分页 + 标签筛选 + 搜索 |
|
||
| Tab 2 通知 | 评论 / 点赞 / Remix 三类通知 + 转码完成通知 |
|
||
| Tab 3 扫描 | RoomPlan 标准 `RoomCaptureView` + 三步引导 + 质量分 + 端侧脱敏 + 三段式上传 |
|
||
| Tab 4 我的房间 | 已发布 / 草稿 / 失败 三个分段控制器 |
|
||
| Tab 5 我的 | Profile、配额详情、设置、隐私脱敏策略说明 |
|
||
| 详情页 | 静态 360° 缩略图轮播 + 评论 + 点赞 + 跳 Web Remix |
|
||
| Realtime | `room:{id}` 与 `user:{id}` 两个 channel 订阅 |
|
||
| 配额 | 进扫描前预检 + 错误码本地拦截 |
|
||
|
||
### 9.2 明确不做
|
||
|
||
- ❌ **iOS 端 Remix 编辑器**(统一在 Web 端做,原因:R3F + drei 在 iOS WKWebView 性能足够;做原生 SceneKit Remix 工作量约等于重写一遍 Web 渲染器)
|
||
- ❌ **AR 内即时换家具预览**(QuickLook 也不接,避免「我以为我换了,实际服务端没收到」的双状态问题)
|
||
- ❌ **协同编辑**(Remix 走 fork 模型)
|
||
- ❌ **Android / iPad 专属布局**(iPad 用 iPhone scaled,MVP 不做 split view 适配)
|
||
- ❌ **Apple Watch / visionOS 端**
|
||
- ❌ **本地 .glb 渲染**(详情页只展示缩略图,3D 渲染交给 Web)
|
||
- ❌ **离线模式**(草稿可保留,但浏览必须联网)
|
||
- ❌ **自训 ML 模型做家具识别**(一律走 RoomPlan 内置 16 类语义)
|
||
|
||
---
|
||
|
||
## 10. 风险与开放问题
|
||
|
||
| # | 风险 / 开放问题 | 当前判断 | 待解答 |
|
||
|---|---------------|---------|--------|
|
||
| **R-iOS-1** | **RoomPlan 在低光场景失败率高**([`roomplan_accuracy_and_cad_export.md`](../iphone/roomplan_accuracy_and_cad_export.md) §1.3 提到光照影响 ±1–3 cm,且极弱光会直接拒绝开始扫描) | UX 兜底:扫描前自动检测 `AVCaptureDevice.iso`,过高时弹窗建议开灯;仍允许强行扫描 | 是否需要在 App 内置「补光手电」开关?(受发热限制,可能不实用) |
|
||
| **R-iOS-2** | **端侧脱敏在 iPhone 12 Pro 上耗时超目标**(§4.3 估算 16 s,超 8 s 预算 2 倍) | 12 Pro 是 RoomPlan 最低支持设备(无 12 Pro 就无 LiDAR),不能放弃;预计提示「正在处理,预计 15 秒」即可 | 是否对 12/12 Pro 默认降级到「仅检测 + 不模糊,把任务转交服务端」?——但这违反 iOS-X1,需要子任务 5 隐私治理章节回答能否破例 |
|
||
| **R-iOS-3** | **`.usdz` 重新打包工具链稳定性**:iOS 沙箱无法跑 `usdzconvert` CLI,只能用 `ModelIO` + 手动 zip,`ModelIO` 对 USD 写回支持有限 | 备选方案:不重打包,把脱敏后的贴图作为「平行文件」一起 PUT,让 Worker 端做替换合并。代价是 Worker 改造一次 | 是否可接受 Worker 帮忙合并?此决策由子任务 5 与后端方共同确认 |
|
||
| **R-iOS-4** | **Universal Link 在国内 Safari 跳转受限**:部分国产浏览器(QQ/微信)不会触发 UL | iOS 端从微信打开链接时,引导用户「点右上角 → 在 Safari 中打开」;这是行业通病不再投入解决 | 是否做微信小程序版的 Remix 编辑入口?P2 再说 |
|
||
| **R-iOS-5** | **Realtime channel 在长时间后台时丢事件**:WebSocket 在 iOS 后台 30 s 内被杀;BGAppRefreshTask 调度由系统决定,最长可能 1 小时才唤醒一次 | 兜底:每次 App 前台化时主动调一次 `GET /rest/v1/room_versions?id=in.(...)` 补查所有「进行中」版本 | 此「补查」是否会被风控误判为「轮询」?需要与 iOS-X4 契约文字微调(明确:「补查」≠「轮询」) |
|
||
|
||
---
|
||
|
||
## 11. 给子任务 5(隐私治理)的契约要点
|
||
|
||
iOS 端在端侧脱敏管线(§4)、权限文案(§8)、风险 R-iOS-2/R-iOS-3 处与隐私治理章节有强耦合。子任务 5 撰写隐私治理总章时**必须保证**以下要点:
|
||
|
||
| # | 治理章节必须保证 X | 与本章对应 |
|
||
|---|------------------|-----------|
|
||
| **P-1** | 明确「端侧脱敏失败 3 次后是否可降级到服务端二次脱敏」的策略——若不允许,则 iOS-X1 维持硬约束;若允许,则需定义服务端二次脱敏 SLA | §4.4 + R-iOS-2 |
|
||
| **P-2** | 给出 `redactions[]` 表的保留期与可见性策略(用户能否查看自己上传的人脸 bbox 数据) | §4.1 末尾的 redactions POST |
|
||
| **P-3** | 明确「位置标签是否在公开页面展示」——若展示,则需补强 `NSLocationWhenInUseUsageDescription` 文案中的「会被其他用户看到」声明 | §8.1 location 文案 |
|
||
| **P-4** | 给出用户「申请删除我所有数据」的端到端流程,iOS 端需要提供入口(设置 → 账号 → 注销账号并删除全部数据) | §1.2 SettingsPage 待加子页 |
|
||
| **P-5** | 明确「ATT 何时启用」的触发条件(接入哪种 SDK 必须申请 ATT),iOS 端据此决定是否在某个版本灰度推 ATT 弹窗 | §8.2 |
|
||
| **P-6** | 明确「未成年用户保护」策略:是否需要在 iOS 端首次启动时弹年龄确认;CrowdRoom 内容是否在 App Store 标 17+ | 本章未覆盖,留给子任务 5 |
|
||
|
||
---
|
||
|
||
## 12. 本章小结
|
||
|
||
| 关键产出 | 一句话 |
|
||
|----------|--------|
|
||
| **5 Tab + 22 页面 IA** | 扫描放中间凸起按钮强化采集动作;Remix 编辑全在 Web,iOS 只做发起入口 |
|
||
| **3 个核心 Flow** | A 扫描脱敏上传等待发布 / B 浏览跳 Web Remix / C 通知跳详情 |
|
||
| **13 项技术栈选型** | SwiftUI + iOS 17 + RoomCaptureView + Vision + CoreImage + supabase-swift + SwiftData |
|
||
| **端侧脱敏管线** | RoomPlan 导出 → ModelIO 解包 → Vision 检测 → CIGaussianBlur 蒙版合成 → 重打包,iPhone 15 Pro ≤ 8 s |
|
||
| **三段式上传 + 10 条错误码文案** | upload-init → 直传 Storage → upload-complete,背景 URLSession + 断点续传 + 分片 + 电量检查 |
|
||
| **Realtime 5 阶段进度 UI** | uploading → queued → transcoding → ready/failed,禁止轮询,后台用 BGAppRefreshTask 兜底 |
|
||
| **三步扫描引导 + A/B/C 质量分** | 复用 iPhone 简易方案的扫描节奏,规则引擎打分,C 档允许上传但排序降权 |
|
||
| **6 条治理契约移交** | 端侧脱敏破例、redactions 保留期、位置可见性、删除流程、ATT、未成年保护 |
|
||
|
||
读完本章你应能:
|
||
- ✅ 给 iOS 工程师一份 8 周内可交付的功能清单
|
||
- ✅ 评审端侧脱敏方案是否真的能落到 iPhone 12 Pro 上
|
||
- ✅ 接手子任务 5 时知道隐私治理章节要回答哪 6 个问题
|
||
|
||
---
|
||
|
||
**章节版本**:v0.1 · 草案
|
||
**关键收获**:iOS 端是 CrowdRoom 的**唯一采集入口**与**最重的隐私防线**——5 条硬契约(iOS-X1~X5)全部落到具体章节;3D 渲染、Remix 编辑、转码、审核一概甩给 Web 与服务端,保持 iOS 端「轻、快、合规」三件套。 |