docs: 添加漂流瓶 Web MVP 设计规格

This commit is contained in:
root
2026-09-14 08:47:52 +08:00
commit bdd384c3e6
@@ -0,0 +1,292 @@
# 漂流瓶 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 次。