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

13 KiB
Raw Permalink Blame History

漂流瓶 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. 仓库结构

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 可用于候选加速,但缓存丢失不能改变事实结果。

瓶子状态:

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,统一响应:

{
  "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 次。