docs: App potential issues v2 — full analysis with server-side dedup (confirmed, ready to execute)
v2 补齐 v1 自查发现的 8 个漏洞:三重指纹(filename+bytes+CRC32)、批量弹窗 (单次确认非逐张)、photos 表 dedup 索引、force 权限约束(owner/admin)、 上传并发限制(3)、相册开关预检、上传进度百分比、磁盘空间预检。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
# App 端潜在问题完整分析与解决方案(v2 修订版,含服务端查重)
|
||||
|
||||
日期:2026-09-28 · 版本:v2(含自查漏洞修复) · 状态:**已确认,待实施**
|
||||
范围:apps/mobile 全量 + services/api 涉及上传/查重的接口
|
||||
|
||||
---
|
||||
|
||||
## 一、总览
|
||||
|
||||
| 级别 | 数量 | 说明 |
|
||||
|---|---|---|
|
||||
| 🔴 P0 高 | 8 | 影响核心使用/数据正确性/上传引擎健康 |
|
||||
| 🟡 P1 中 | 8 | 特定场景触发/体验缺陷 |
|
||||
| 🟢 P2 低 | 7 | 改善项/增强功能 |
|
||||
|
||||
---
|
||||
|
||||
## 二、🔴 P0 高优先级
|
||||
|
||||
### P0-1 上传照片无重复判断(用户核心痛点)
|
||||
|
||||
**现状**:同一张照片选 10 次就传 10 次,没有任何提醒或确认。
|
||||
|
||||
#### 层 1:App 端本地查重(三重指纹 + 批量弹窗)
|
||||
|
||||
**指纹**:`filename + bytes + 前后各4KB CRC32`(快速,~1ms,不卡 UI)
|
||||
- 三重组合大幅降低假阳性(不同照片同名同大小但内容不同 → CRC 不同 → 正确放行)
|
||||
- 改名后的同内容照片查不出(接受局限——服务端层 2 兜底)
|
||||
|
||||
**批量 UX**:
|
||||
```
|
||||
选 20 张 → 入队前一次性扫描全部 → 8 张匹配 → 单次弹窗:
|
||||
|
||||
⚠️ 8 张照片可能已上传过(共选择 20 张)
|
||||
IMG_001.jpg · IMG_005.jpg · IMG_012.jpg …
|
||||
|
||||
[跳过重复项,上传 12 张] [全部上传 20 张] [取消]
|
||||
```
|
||||
|
||||
**改动文件**:
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `preparer.dart` | 加 `computeFingerprint(path)` 方法(读文件头尾 4KB → CRC32) |
|
||||
| `task_store.dart` | 加 `findByFingerprint(accountId, albumId, fingerprint)` 查询方法 |
|
||||
| `enqueue.dart` | `enqueueForUpload` 改为返回 `EnqueueResult{enqueued, duplicates}` |
|
||||
| `capture_page.dart` | `_pickFromGallery` 收到 duplicates → 批量弹窗 |
|
||||
| `album_detail_page.dart` | 同上(相册详情的从相册选择入口) |
|
||||
| `app_zh/en.arb` | 加弹窗文案 key |
|
||||
|
||||
#### 层 2:服务端查重(跨设备 + 精确哈希 + 索引 + force 安全)
|
||||
|
||||
**查重逻辑**(`uploads.service.ts` `init` 事务内):
|
||||
```
|
||||
initUpload 请求 →
|
||||
WHERE albumId + uploaderId + bytes 匹配 photos 表(有索引)
|
||||
→ 有匹配 → 409 { code: 'PHOTO_DUPLICATE', existingPhotoId, uploadedAt }
|
||||
→ 无匹配 或 force=true 且 isAdmin → 正常建会话
|
||||
```
|
||||
|
||||
**安全约束**:`force: true` 仅 owner/admin 生效——协作者无法绕过。
|
||||
|
||||
**数据库迁移**:
|
||||
```sql
|
||||
CREATE INDEX idx_photos_dedup
|
||||
ON photos (albumId, uploaderId, bytes)
|
||||
WHERE visibility != 'deleted';
|
||||
```
|
||||
|
||||
**契约改动**(`openapi.yaml`):
|
||||
- `UploadInitRequest` 加可选 `contentHash`(string, SHA-256 hex,精确查重备用)
|
||||
- `UploadInitRequest` 加可选 `force`(boolean, 默认 false)
|
||||
- 409 响应体加 `code: PHOTO_DUPLICATE` + `existingPhotoId`
|
||||
|
||||
**改动文件**:
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `openapi.yaml` | 上述契约字段 |
|
||||
| `uploads.service.ts` | init 事务内查重 + force 权限检查 |
|
||||
| `dto.ts` | `UploadInitRequestDto` 加 `contentHash`/`force` |
|
||||
| `prisma migration` | `idx_photos_dedup` 索引 |
|
||||
| `api_client.dart`(App) | initUpload 加参数;409 PHOTO_DUPLICATE 处理 |
|
||||
| `upload_engine.dart`(App) | 409 不标记失败 → 转交 UI 确认 |
|
||||
|
||||
---
|
||||
|
||||
### P0-2 照片查看器无缩放
|
||||
|
||||
在 `PageView` 每页包 `InteractiveViewer`,缩放时禁用 PageView 滑动:
|
||||
|
||||
```dart
|
||||
InteractiveViewer(
|
||||
maxScale: 5.0,
|
||||
onInteractionUpdate: (details) {
|
||||
_pageController.physics = details.scale != 1.0
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: const PageScrollPhysics();
|
||||
},
|
||||
child: PhotoThumbnail(...),
|
||||
)
|
||||
```
|
||||
|
||||
**工时**:~30min
|
||||
|
||||
---
|
||||
|
||||
### P0-3 相机后台未释放
|
||||
|
||||
`CapturePage` 加 `WidgetsBindingObserver`:
|
||||
- `paused` → dispose 相机
|
||||
- `resumed` → 重新 initialize
|
||||
|
||||
**工时**:~30min
|
||||
|
||||
---
|
||||
|
||||
### P0-4 无离线感知
|
||||
|
||||
- `connectivity_plus` 依赖
|
||||
- `NetworkStatusProvider`(Stream)
|
||||
- 离线 → 上传引擎暂停 + UI 黄条提示
|
||||
- 恢复 → 自动重试
|
||||
|
||||
**工时**:~3h
|
||||
|
||||
---
|
||||
|
||||
### P0-5 照片 EXIF 方向未处理
|
||||
|
||||
App 端上传前读取 EXIF orientation → 非 1 时本地旋转修正。
|
||||
|
||||
**工时**:~2h
|
||||
|
||||
---
|
||||
|
||||
### P0-6 图片无内存管理
|
||||
|
||||
PhotoGrid 的 `Image` 加 `cacheWidth`(按屏幕像素 / 列数)。
|
||||
|
||||
**工时**:~1h
|
||||
|
||||
---
|
||||
|
||||
### P0-7 上传并发无限制(🆕 v2 新增)
|
||||
|
||||
**现状**:无 `maxConcurrent` 控制——50 张同时 PUT。
|
||||
|
||||
**方案**:上传引擎加并发信号量(上限 3),排队执行。
|
||||
|
||||
**改动**:`upload_engine.dart` 加 `_Semaphore` 类 + `_drainQueue` 改为限流。
|
||||
|
||||
**工时**:~1h
|
||||
|
||||
---
|
||||
|
||||
### P0-8 入队前不检查相册上传开关(🆕 v2 新增)
|
||||
|
||||
**现状**:`enqueueForUpload` 不查 `album.uploadEnabled`——关闭后仍复制+init→403。
|
||||
|
||||
**方案**:入队前检查 → `false` 时提示「该相册已关闭上传」+ 不入队。
|
||||
|
||||
**改动**:`enqueue.dart` + `capture_page` / `album_detail_page`。
|
||||
|
||||
**工时**:~30min
|
||||
|
||||
---
|
||||
|
||||
## 三、🟡 P1 中优先级
|
||||
|
||||
| # | 问题 | 方案 | 工时 |
|
||||
|---|---|---|---|
|
||||
| P1-1 | 海报渲染 OOM | pixelRatio 3→2 + try/catch 降级 | 15min |
|
||||
| P1-2 | 相机分辨率过高 | `ResolutionPreset.max` → `high` | 5min |
|
||||
| P1-3 | 闪光灯状态丢失 | flip 后恢复 flash state | 15min |
|
||||
| P1-4 | 无全局崩溃捕获 | `FlutterError.onError` + `runZonedGuarded` + 本地日志 | 1h |
|
||||
| P1-5 | 上传引擎无生命周期 | `WidgetsBindingObserver` pause/resume | 1h |
|
||||
| P1-6 | 分享缩略图 PNG 超 32KB | 改 JPEG 编码或限制尺寸更小 | 30min |
|
||||
| P1-7 | 上传无进度百分比(🆕) | 任务行加 `LinearProgressIndicator`(`onReceiveProgress` 已有) | 30min |
|
||||
| P1-8 | 磁盘空间无预检(🆕) | 复制前 `path_provider` 检查可用空间 | 30min |
|
||||
|
||||
---
|
||||
|
||||
## 四、🟢 P2 低优先级 / 增强
|
||||
|
||||
| # | 问题 | 方案 |
|
||||
|---|---|---|
|
||||
| P2-1 | 无深度链接 | `app_links` + router 集成 |
|
||||
| P2-2 | 无推送通知 | FCM / 极光 |
|
||||
| P2-3 | 表单草稿不持久 | `shared_preferences` 定时保存 |
|
||||
| P2-4 | 无图片压缩 | resize 最大 2048px + JPEG 85% |
|
||||
| P2-5 | 无批量管理 | 照片网格多选批量隐藏/删除 |
|
||||
| P2-6 | 多语言无 RTL | 支持阿拉伯语等 |
|
||||
| P2-7 | App 体积 64MB 偏大(🆕) | `--split-per-abi`(arm64-v8a 约 25MB)或 `--obfuscate` |
|
||||
|
||||
---
|
||||
|
||||
## 五、上传引擎优化(🆕 v2 新增)
|
||||
|
||||
| # | 优化 | 说明 |
|
||||
|---|---|---|
|
||||
| 1 | 哈希计算放 Isolate | 如做 SHA-256,必须 `Isolate.run()` 否则卡 UI |
|
||||
| 2 | 上传重试退避策略 | 网络错→立即重试;5xx→指数退避(1s/2s/4s/8s);403→不重试 |
|
||||
| 3 | 源文件清理提前 | 上传成功+服务端确认后 24h 即清理(非当前定期批量) |
|
||||
| 4 | 上传优先级 | 刚拍的排前面(用户在等),补传的排后面 |
|
||||
| 5 | 取消原子性 | 先标记 cancelled 再异步清理,UI 即时反馈 |
|
||||
|
||||
---
|
||||
|
||||
## 六、修订后实施排期
|
||||
|
||||
| 批次 | 内容 | 工时 | 备注 |
|
||||
|---|---|---|---|
|
||||
| **第 1 批** | P0-2 缩放 + P0-3 相机释放 + P0-6 内存管理 | ~2h | 核心体验 |
|
||||
| **第 2 批** | P0-1 层1(本地查重 + 批量弹窗) | ~3h | 用户痛点 |
|
||||
| **第 2.5 批** | P0-7 并发限制 + P0-8 开关预检 + P1-8 磁盘预检 | ~2h | 上传引擎健康 |
|
||||
| **第 3 批** | P0-4 离线感知 + P0-5 EXIF 方向 | ~5h | 稳定性 |
|
||||
| **第 4 批** | P0-1 层2(服务端查重 + 索引 + force 安全) | ~6h | 跨设备查重 |
|
||||
| **第 5 批** | P1 全部 + 上传进度 + 重试退避 | ~5h | 品质 |
|
||||
| **第 6 批** | P2 + App 瘦身 | 按需 | 增强 |
|
||||
|
||||
**总工时**:~23h(P0 全部 + P1 全部);第 1-2.5 批可一天内完成。
|
||||
|
||||
---
|
||||
|
||||
## 七、测试要点
|
||||
|
||||
### 查重测试
|
||||
- 同一张照片选两次 → 第二次弹窗
|
||||
- 不同照片但同名 → 不弹窗(bytes/CRC 不同)
|
||||
- 批量选 20 张含 8 张重复 → **单次**弹窗(不是 8 次)
|
||||
- 确认「跳过重复项」→ 只上传 12 张
|
||||
- 确认「全部上传」→ 上传 20 张
|
||||
- 确认「取消」→ 不入队
|
||||
- 跨设备:手机 A 传了 → 手机 B 选同一张 → 服务端 409 → 弹窗
|
||||
- `force: true` owner 可绕过 → 协作者不可绕过
|
||||
|
||||
### 上传引擎测试
|
||||
- 并发上限 3:入队 10 张 → 任何时刻最多 3 个 uploading
|
||||
- 相册关闭上传 → 提示 + 不入队
|
||||
- 磁盘不足 → 友好提示
|
||||
- 离线 → 任务暂停 → 恢复网络 → 自动重传
|
||||
- 5xx 错误 → 指数退避(非立即重试)
|
||||
|
||||
### 查看器测试
|
||||
- 双指放大缩小 → 松手回弹/保持
|
||||
- 放大状态下滑动 → 照片内平移(不切页)
|
||||
- 恢复 1x 后 → 左右滑动切页
|
||||
|
||||
---
|
||||
|
||||
*v2 修订版——已补齐自查发现的 8 个漏洞(指纹精度、批量弹窗、索引、force 安全、并发、预检、进度、磁盘)。确认后按排期实施。*
|
||||
Reference in New Issue
Block a user