Files
plp/docs/superpowers/specs/2026-08-03-drift-bottle-web-mvp-design.md
T
2026-09-14 08:47:52 +08:00

293 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 漂流瓶 Web MVP 设计规格
**版本:** V1.0
**日期:** 2026-08-03
**状态:** 已确认
**项目目录:** `/data/drift-bottle`
## 1. 目标与范围
第一阶段交付一个可真实运行、适配移动端并可安装为 PWA 的漂流瓶 Web MVP,用于验证完整 P0 社交闭环。项目作为独立仓库从零开发,不修改 `/data/bottle` 旧项目。
### 1.1 P0 功能
- 演示短信验证码登录。
- Access Token、Refresh Token 轮换、设备会话与退出登录。
- 匿名资料。
- 投瓶、自动/模拟人工审核、瓶子入池。
- 随机捞取、租约、退回和每日配额。
- 首次回复原子消费瓶子并建立唯一会话。
- Socket.IO 双向文字消息、断网重连、历史同步与未读状态。
- 举报、双向隔离式拉黑和处罚状态。
- 最小审核后台:审核队列、举报处置和账号处罚。
- 本机容器化部署及局域网 IP + 端口预览地址。
### 1.2 本阶段不实现
- iOS、Android、HarmonyOS 安装包及厂商 Push。
- 腾讯云 IM、真实短信、微信登录和 Apple 登录。
- 图片、语音、视频、群聊、附近的人和个性化推荐。
- RocketMQ、Kubernetes、对象存储和完整微服务拆分。
这些能力通过适配器与领域边界预留,不在 MVP 中提供伪成功实现。
## 2. 技术方案
采用独立 TypeScript Monorepo:
- Web:Vue 3、Vite、Pinia、Vue Router、PWA、Socket.IO Client。
- API:NestJS、REST、Socket.IO Gateway、OpenAPI。
- 数据:PostgreSQL 16 为权威数据源,Redis 7 用于验证码、快速限流和临时连接状态。
- ORM:Prisma,数据库迁移纳入版本控制。
- 测试:Vitest、Nest Testing、Supertest、Testcontainers 或 Docker Compose 集成环境、Playwright E2E。
- 部署:Docker Compose 启动 PostgreSQL、Redis、API 与 Web/Nginx。
MVP 异步任务使用 PostgreSQL Transactional Outbox 和独立 NestJS Worker 进程轮询消费。事件与 Handler 保持幂等,后续可替换为 RocketMQ。
## 3. 仓库结构
```text
drift-bottle/
├── apps/
│ ├── web/ # 移动端 PWA
│ ├── api/ # NestJS REST + Socket.IO
│ └── worker/ # Outbox、审核、通知任务
├── packages/
│ ├── contracts/ # DTO、错误码、共享类型
│ ├── config/ # ESLint、TSConfig 等共享配置
│ └── ui/ # 设计 Token 与通用组件
├── prisma/
│ ├── schema.prisma
│ ├── migrations/
│ └── seed.ts
├── tests/
│ └── e2e/
├── infra/
│ ├── compose.yaml
│ └── nginx.conf
├── docs/
│ └── superpowers/
├── .env.example
├── package.json
└── pnpm-workspace.yaml
```
## 4. 模块边界
### 4.1 Auth
生成和验证演示短信码、签发短期 JWT、轮换 Refresh Token、维护设备会话及注销。
开发环境返回 `debugCode` 并在登录页明确标注“演示验证码”;生产模式不得返回验证码。Refresh Token 放入 `HttpOnly + SameSite` Cookie,数据库只保存哈希;Access Token 仅保存在 Web 运行内存中。
### 4.2 Profile
内部 `accountId` 与公开不可枚举的 `publicId` 分离。陌生用户只能看到匿名昵称、头像色和简介,不能获得手机号或内部 ID。
### 4.3 Bottle 与 Match
Bottle 管理内容及状态机,Match 管理候选排除、租约、历史和每日配额。PostgreSQL 事务是最终裁决者;Redis 可用于候选加速,但缓存丢失不能改变事实结果。
瓶子状态:
```text
REVIEWING -> REJECTED
-> MANUAL_REVIEW -> IN_POOL
-> IN_POOL -> LEASED -> IN_POOL
-> CONSUMED
-> REMOVED / CLOSED
```
单瓶只允许建立一个一对一会话。领取产生短租约;首次回复通过数据库唯一约束与 CAS 原子消费。
### 4.4 Conversation
业务数据库决定成员关系、会话状态、发言权限、拉黑和处罚。Socket.IO 仅是实时传输层。所有消息先通过 REST 或 Socket 业务命令完成身份、成员、审核和幂等校验,再写入数据库;成功落库后才向双方广播。
### 4.5 Safety 与 Admin
本地规则模拟内容审核:普通内容自动通过;命中配置的拒绝词直接拒绝;命中复审词进入人工队列。审核后台提供待审、通过、拒绝、举报处置和处罚操作。所有后台操作写审计日志。
### 4.6 Notification
MVP 提供站内通知及未读数。浏览器通知可作为渐进增强,但不能声称等价于系统厂商 Push;Push Adapter 保留到原生阶段接入。
## 5. 核心数据模型
- `accounts`:账号、手机号密文/检索哈希、状态、Token 版本。
- `anonymous_profiles`:公开 ID、昵称、头像色、简介和审核状态。
- `sessions`:设备 ID、Refresh Token 哈希、到期和撤销时间。
- `verification_codes`:Redis 中的验证码哈希、过期时间和尝试次数。
- `bottles`:作者、文本、审核状态、池状态、版本和时间。
- `bottle_pick_leases`:瓶子、捞取者、状态和过期时间。
- `bottle_pick_history`:捞取历史,唯一键为瓶子与捞取者。
- `daily_usage`:用户、日期、投瓶数和捞瓶数。
- `conversations`:来源瓶子、状态、最后消息时间,来源瓶子唯一。
- `conversation_members`:成员、匿名快照、已读序号和拉黑时间。
- `messages`:会话、发送者、客户端消息 ID、序号、正文与审核状态。
- `blocks`:拉黑者与被拉黑者,组合唯一。
- `reports`:举报目标、原因、详情、状态和处理结果。
- `moderation_tasks`:审核对象、规则结果、风险标签和状态。
- `sanctions`:账号处罚等级、原因和有效期。
- `notifications`:站内通知、已读时间。
- `outbox_events`:领域事件、状态、重试次数和下次重试时间。
- `audit_logs`:后台操作者、动作、目标和脱敏元数据。
## 6. API 与错误约定
REST 前缀为 `/api/v1`,统一响应:
```json
{
"code": "OK",
"message": "success",
"data": {},
"requestId": "uuid"
}
```
采用游标分页。创建类接口接受 `Idempotency-Key` 或稳定客户端 ID。主要错误码:
- `AUTH_TOKEN_EXPIRED`
- `AUTH_REFRESH_REUSED`
- `BOTTLE_DAILY_LIMIT`
- `BOTTLE_POOL_EMPTY`
- `BOTTLE_LEASE_EXPIRED`
- `CONTENT_REJECTED`
- `USER_BLOCKED`
- `CONVERSATION_FORBIDDEN`
- `MESSAGE_DUPLICATE`
OpenAPI 是 HTTP 契约源;共享 contracts 包用于 Socket 事件与 Web 编译期类型。
## 7. 核心流程
### 7.1 登录
1. 客户端提交手机号;服务端按手机号与 IP 限流。
2. 服务端生成验证码,Redis 只保存 HMAC/哈希与短 TTL。
3. 开发环境在响应中返回 `debugCode`。
4. 验证成功后创建账号、匿名资料和设备会话。
5. 返回 Access Token,并设置 Refresh Token HttpOnly Cookie。
6. Access Token 过期后客户端只发起一次串行刷新;Refresh Token 每次刷新都轮换。
7. 旧 Refresh Token 再次使用视为重放,撤销该设备会话。
### 7.2 投瓶和审核
1. 校验身份、处罚状态、长度与每日投瓶配额。
2. 创建 `REVIEWING` 瓶子及审核任务。
3. Worker 执行模拟审核:通过则设置 `IN_POOL`;拒绝则设为 `REJECTED`;复审则等待管理员。
4. 审核失败或 Worker 中断时内容保持不可见,不允许 fail-open。
### 7.3 捞瓶
1. 校验账号、处罚和每日配额。
2. 查询小批候选并排除本人、已捞历史、双向拉黑、非池中状态和活跃租约。
3. 在短事务中锁定候选,CAS `IN_POOL -> LEASED`,写租约、历史和权威计数。
4. 并发失败时有限重试;无候选返回 `BOTTLE_POOL_EMPTY`。
5. 租约超时由 Worker 设回 `IN_POOL`;展示成功即计一次捞取次数。
### 7.4 首次回复和建会话
1. 客户端提交 `bottleId + leaseId + firstMessage + clientMsgId`。
2. 校验租约持有者、拉黑、处罚和内容审核。
3. 数据库事务 CAS 消费瓶子,创建唯一会话、双方成员和首条消息。
4. 相同 `clientMsgId` 重试返回已有消息;并发回复通过唯一约束返回同一会话。
5. 事务提交后经 Outbox 广播 Socket 事件和站内通知。
### 7.5 实时消息和同步
- Socket 握手携带短期 Access Token。
- 发送命令包含 `conversationId/clientMsgId/text`。
- 服务端再次校验成员、拉黑、处罚、内容和消息频率。
- 消息以会话内递增 `seq` 持久化后确认发送成功。
- 客户端断线重连后用每个会话的最后 `seq` 拉取缺失消息。
- UI 状态为 `sending/sent/failed/recalled`,服务端未确认前不显示为成功。
### 7.6 拉黑和举报
- 拉黑后立即禁止双方继续发消息,并从相互候选中排除。
- 举报保存目标快照、原因和上下文消息 ID;正文不进入普通日志。
- 管理员处置产生处罚和审计日志。
## 8. Web 体验设计
移动优先,支持桌面居中模拟手机视口。视觉使用深海渐变、柔和漂浮粒子和半透明玻璃卡片;所有核心动作保持高对比度和单手可达。
页面包括:
1. 手机号登录页。
2. 海面首页:扔瓶、捞瓶、每日剩余次数。
3. 投瓶编辑与审核结果页。
4. 捞瓶结果、退回和首次回复页。
5. 会话列表及未读标记。
6. 实时聊天页。
7. 我的瓶子与状态页。
8. 设置、拉黑列表和退出登录。
9. 独立后台登录及审核/举报工作台。
PWA 提供 manifest、图标、主题色和基础离线壳。业务写操作离线时明确提示失败,不在本地伪造服务端成功。
## 9. 安全要求
- 密码或密钥不提交仓库,提供 `.env.example`。
- 手机号使用字段级加密,并保存独立 HMAC 用于查找;响应和日志统一掩码。
- JWT 仅包含最小声明,不含手机号。
- CORS、Cookie、CSRF 策略按同源部署设置;状态变更接口校验 Origin,并使用 SameSite Cookie。
- DTO 白名单验证,限制正文长度;数据库查询始终携带当前用户成员条件。
- Socket 与 REST 使用相同授权服务。
- 限流维度包括手机号、账号、IP 和设备 ID。
- 日志禁止记录 Token、Cookie、验证码、手机号全文及聊天正文。
- 管理后台使用独立角色,所有处置操作审计。
## 10. 错误处理与降级
- PostgreSQL 不可用:写操作失败并明确返回,不伪装成功。
- Redis 不可用:验证码登录与快速限流暂停;既有 Access Token 的只读能力可继续,权威配额不绕过。
- Worker 不可用:新瓶保持审核中,Outbox 事件待恢复后重试。
- Socket 不可用:历史消息仍可通过 REST 查询;发送失败可凭相同 `clientMsgId` 重试。
- PWA 离线:仅显示缓存壳和明确离线状态,不缓存敏感 API 响应。
## 11. 测试与验收
### 11.1 自动化测试
- 单元:瓶子状态机、候选排除、配额、审核规则、Token 轮换和处罚规则。
- 集成:PostgreSQL 事务、Redis 验证码、Outbox 重试、Socket 鉴权和消息落库。
- 契约:OpenAPI 响应结构、错误码和 Socket 事件 Schema。
- E2E:使用两个独立浏览器上下文完成登录、投瓶、捞取、首次回复、实时聊天、断线恢复、举报和拉黑。
- 安全:未授权、IDOR、Token 重放、重复提交、消息重放和日志敏感信息扫描。
### 11.2 P0 验收
1. 未登录业务 API 返回 401,Web 跳转登录页。
2. 自己、已拉黑和已捞过的瓶子不会返回。
3. 并发捞同一瓶只有一个有效租约。
4. 并发回复同一瓶只建立一个会话,幂等重试得到同一结果。
5. 第 11 次投瓶和第 21 次捞瓶被拒绝,跨 UTC+8 自然日恢复。
6. 审核超时或失败内容不入池,人工通过后自动入池。
7. 拉黑后双方无法继续发消息,也不会互相匹配。
8. 两个浏览器上下文能实时双向聊天;断线重连后按序补齐历史且不重复。
9. 举报能够进入后台并被处置,处罚立即影响业务权限。
10. 后台查询与处置均有审计日志。
11. `docker compose up` 后健康检查、迁移、Seed 与 E2E 均能真实运行。
12. 局域网设备可通过服务器 IP 和端口打开移动端页面。
## 12. 演进路径
- 客户端:Web 契约稳定后创建 uni-app x 客户端;HarmonyOS 先执行 HAP、签名、IM、Push、深链和 UTS/ArkTS 桥接 PoC。
- IM:定义 `ImTransport` 边界,将本地 Socket.IO 替换为腾讯云 IM,同时保留业务授权、审核和索引。
- Push:通过 `PushAdapter` 接 APNs、Android 厂商通道和 HarmonyOS Push Kit。
- 基础设施:当 Outbox 堆积或消费者扩展需求出现时接入 RocketMQ;图片进入范围后接对象存储与隔离审核。
- 部署:上线前从 Compose 升级到国内云双可用区托管容器及托管 PostgreSQL/Redis/MQ。
## 13. 已确认决策
- 使用独立项目 `/data/drift-bottle`,不修改旧 `/data/bottle`。
- 第一阶段为移动端 Web/PWA,不声称已完成原生三端能力。
- 登录使用演示验证码,但实现完整安全 Token 生命周期。
- 聊天使用 Socket.IO,但业务权限和事实数据留在后端。
- 第一阶段交付完整 P0 闭环。
- 单瓶单会话;瓶子展示成功即消耗捞取次数。
- 每日配额按 UTC+8 自然日:投瓶 10 次、捞瓶 20 次。