13 KiB
漂流瓶 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_EXPIREDAUTH_REFRESH_REUSEDBOTTLE_DAILY_LIMITBOTTLE_POOL_EMPTYBOTTLE_LEASE_EXPIREDCONTENT_REJECTEDUSER_BLOCKEDCONVERSATION_FORBIDDENMESSAGE_DUPLICATE
OpenAPI 是 HTTP 契约源;共享 contracts 包用于 Socket 事件与 Web 编译期类型。
7. 核心流程
7.1 登录
- 客户端提交手机号;服务端按手机号与 IP 限流。
- 服务端生成验证码,Redis 只保存 HMAC/哈希与短 TTL。
- 开发环境在响应中返回
debugCode。 - 验证成功后创建账号、匿名资料和设备会话。
- 返回 Access Token,并设置 Refresh Token HttpOnly Cookie。
- Access Token 过期后客户端只发起一次串行刷新;Refresh Token 每次刷新都轮换。
- 旧 Refresh Token 再次使用视为重放,撤销该设备会话。
7.2 投瓶和审核
- 校验身份、处罚状态、长度与每日投瓶配额。
- 创建
REVIEWING瓶子及审核任务。 - Worker 执行模拟审核:通过则设置
IN_POOL;拒绝则设为REJECTED;复审则等待管理员。 - 审核失败或 Worker 中断时内容保持不可见,不允许 fail-open。
7.3 捞瓶
- 校验账号、处罚和每日配额。
- 查询小批候选并排除本人、已捞历史、双向拉黑、非池中状态和活跃租约。
- 在短事务中锁定候选,CAS
IN_POOL -> LEASED,写租约、历史和权威计数。 - 并发失败时有限重试;无候选返回
BOTTLE_POOL_EMPTY。 - 租约超时由 Worker 设回
IN_POOL;展示成功即计一次捞取次数。
7.4 首次回复和建会话
- 客户端提交
bottleId + leaseId + firstMessage + clientMsgId。 - 校验租约持有者、拉黑、处罚和内容审核。
- 数据库事务 CAS 消费瓶子,创建唯一会话、双方成员和首条消息。
- 相同
clientMsgId重试返回已有消息;并发回复通过唯一约束返回同一会话。 - 事务提交后经 Outbox 广播 Socket 事件和站内通知。
7.5 实时消息和同步
- Socket 握手携带短期 Access Token。
- 发送命令包含
conversationId/clientMsgId/text。 - 服务端再次校验成员、拉黑、处罚、内容和消息频率。
- 消息以会话内递增
seq持久化后确认发送成功。 - 客户端断线重连后用每个会话的最后
seq拉取缺失消息。 - UI 状态为
sending/sent/failed/recalled,服务端未确认前不显示为成功。
7.6 拉黑和举报
- 拉黑后立即禁止双方继续发消息,并从相互候选中排除。
- 举报保存目标快照、原因和上下文消息 ID;正文不进入普通日志。
- 管理员处置产生处罚和审计日志。
8. Web 体验设计
移动优先,支持桌面居中模拟手机视口。视觉使用深海渐变、柔和漂浮粒子和半透明玻璃卡片;所有核心动作保持高对比度和单手可达。
页面包括:
- 手机号登录页。
- 海面首页:扔瓶、捞瓶、每日剩余次数。
- 投瓶编辑与审核结果页。
- 捞瓶结果、退回和首次回复页。
- 会话列表及未读标记。
- 实时聊天页。
- 我的瓶子与状态页。
- 设置、拉黑列表和退出登录。
- 独立后台登录及审核/举报工作台。
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 验收
- 未登录业务 API 返回 401,Web 跳转登录页。
- 自己、已拉黑和已捞过的瓶子不会返回。
- 并发捞同一瓶只有一个有效租约。
- 并发回复同一瓶只建立一个会话,幂等重试得到同一结果。
- 第 11 次投瓶和第 21 次捞瓶被拒绝,跨 UTC+8 自然日恢复。
- 审核超时或失败内容不入池,人工通过后自动入池。
- 拉黑后双方无法继续发消息,也不会互相匹配。
- 两个浏览器上下文能实时双向聊天;断线重连后按序补齐历史且不重复。
- 举报能够进入后台并被处置,处罚立即影响业务权限。
- 后台查询与处置均有审计日志。
docker compose up后健康检查、迁移、Seed 与 E2E 均能真实运行。- 局域网设备可通过服务器 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 次。