Files
plp/docs/superpowers/plans/2026-08-03-drift-bottle-web-mvp.md
T
2026-09-14 08:50:50 +08:00

22 KiB
Raw Blame History

漂流瓶 Web MVP 实现计划

面向 AI 代理的工作者: 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(- [ ])语法来跟踪进度。

目标: 在独立仓库中构建并部署一个包含安全登录、扔/捞瓶、审核、实时聊天、举报与拉黑闭环的移动端 Web/PWA。

架构: pnpm TypeScript Monorepo 包含 Vue 3 Web、NestJS API/Socket.IO、NestJS Worker 与共享 contracts。PostgreSQL 保存权威事实,Redis 保存验证码和快速限流状态,Transactional Outbox 驱动审核与通知异步任务,Docker Compose 提供可复现运行环境。

技术栈: TypeScript、pnpm、Vue 3、Vite、Pinia、Vue Router、NestJS、Socket.IO、Prisma、PostgreSQL 16、Redis 7、Vitest、Supertest、Playwright、Docker Compose、Nginx。


文件结构

  • package.json、pnpm-workspace.yaml、tsconfig.base.json:Monorepo 命令与共享编译配置。
  • .env.example:不含秘密的完整环境变量契约。
  • packages/contracts/src/*:REST/Socket DTO、错误码、分页和共享实体类型。
  • apps/api/src/main.ts:NestJS HTTP/Socket 启动入口与全局安全配置。
  • apps/api/src/common/*:统一响应、异常、请求 ID、鉴权守卫和当前用户。
  • apps/api/src/auth/*:演示验证码、JWT、Refresh Token 轮换、设备会话。
  • apps/api/src/profile/*:匿名资料。
  • apps/api/src/bottle/*:投瓶、状态机与用户瓶子列表。
  • apps/api/src/match/*:候选排除、租约、退回和配额。
  • apps/api/src/conversation/*:首次回复、会话、消息历史和 Socket Gateway。
  • apps/api/src/safety/*:审核规则、举报、拉黑、处罚和后台接口。
  • apps/api/src/notification/*:站内通知与未读数。
  • apps/api/src/outbox/*:事务事件写入和消费租约。
  • apps/worker/src/*:审核、租约回收、通知与重试 Worker。
  • apps/web/src/api/*:鉴权刷新串行化与 REST 客户端。
  • apps/web/src/stores/*:登录态、瓶子、会话和通知状态。
  • apps/web/src/router/*:页面路由与登录守卫。
  • apps/web/src/views/*:登录、首页、投瓶、捞瓶、会话、聊天、我的、设置和审核台。
  • apps/web/src/components/*:底部导航、瓶子卡片、模态框、消息气泡和状态组件。
  • apps/web/src/styles/*:设计 Token、深海主题、响应式与无障碍样式。
  • apps/web/public/*:PWA Manifest 和图标。
  • prisma/schema.prisma、prisma/migrations/*、prisma/seed.ts:数据库模型、迁移和演示数据。
  • tests/integration/*:跨模块事务、幂等、Socket 和安全测试。
  • tests/e2e/*:双用户浏览器完整业务链路。
  • infra/compose.yaml、infra/nginx.conf、apps/*/Dockerfile:生产式本机部署。
  • README.md:启动、测试、演示账号、部署和故障排查。

任务 1:初始化 Monorepo 与质量门禁

文件:

  • 创建:package.json

  • 创建:pnpm-workspace.yaml

  • 创建:tsconfig.base.json

  • 创建:.gitignore

  • 创建:.env.example

  • 创建:packages/contracts/package.json

  • 创建:packages/contracts/src/index.ts

  • 测试:packages/contracts/src/index.test.ts

  • 步骤 1:编写失败的 contracts 测试

import { describe, expect, it } from 'vitest';
import { ErrorCode, ok } from './index';
describe('contracts', () => {
  it('creates the uniform response envelope', () => {
    expect(ok({ id: '1' }, 'req-1')).toEqual({ code: ErrorCode.OK, message: 'success', data: { id: '1' }, requestId: 'req-1' });
  });
});
  • 步骤 2:运行测试并确认因包和实现不存在而失败

运行:corepack pnpm install && corepack pnpm --filter @drift/contracts test
预期:FAIL,无法解析 ./index 导出。

  • 步骤 3:建立 workspace、严格 TypeScript、ESLint/Prettier/Vitest 命令并实现 contracts
export enum ErrorCode { OK = 'OK', AUTH_TOKEN_EXPIRED = 'AUTH_TOKEN_EXPIRED', AUTH_REFRESH_REUSED = 'AUTH_REFRESH_REUSED', BOTTLE_DAILY_LIMIT = 'BOTTLE_DAILY_LIMIT', BOTTLE_POOL_EMPTY = 'BOTTLE_POOL_EMPTY', BOTTLE_LEASE_EXPIRED = 'BOTTLE_LEASE_EXPIRED', CONTENT_REJECTED = 'CONTENT_REJECTED', USER_BLOCKED = 'USER_BLOCKED', CONVERSATION_FORBIDDEN = 'CONVERSATION_FORBIDDEN', MESSAGE_DUPLICATE = 'MESSAGE_DUPLICATE' }
export const ok = <T>(data: T, requestId: string) => ({ code: ErrorCode.OK, message: 'success', data, requestId });
  • 步骤 4:运行 contracts 测试、类型检查和格式检查

运行:corepack pnpm --filter @drift/contracts test && corepack pnpm typecheck && corepack pnpm lint
预期:全部退出码为 0。

  • 步骤 5:Commit
git add package.json pnpm-workspace.yaml tsconfig.base.json .gitignore .env.example packages
git commit -m "chore: 初始化 TypeScript monorepo"

任务 2:建立数据库模型、迁移与测试基础设施

文件:

  • 创建:prisma/schema.prisma

  • 创建:prisma/migrations/0001_init/migration.sql

  • 创建:prisma/seed.ts

  • 创建:apps/api/src/database/prisma.service.ts

  • 创建:tests/integration/database.spec.ts

  • 创建:infra/compose.test.yaml

  • 步骤 1:编写数据库约束失败测试

it('rejects a second conversation for the same bottle', async () => {
  await prisma.conversation.create({ data: firstConversation });
  await expect(prisma.conversation.create({ data: secondConversation })).rejects.toMatchObject({ code: 'P2002' });
});

同时测试 BottlePickHistory(bottleId,pickerId)、Block(blockerId,blockedId) 与 Message(conversationId,clientMsgId) 组合唯一。

  • 步骤 2:启动测试 PostgreSQL/Redis 并确认迁移或模型缺失导致失败

运行:docker compose -f infra/compose.test.yaml up -d && corepack pnpm test:integration -- database.spec.ts
预期:FAIL,Prisma Schema 或表不存在。

  • 步骤 3:实现规格中的全部核心模型、枚举、索引、唯一约束和 UTC 时间字段

关键约束:

model Conversation {
  id             String @id @default(uuid()) @db.Uuid
  sourceBottleId String @unique @db.Uuid
  status         ConversationStatus @default(ACTIVE)
  createdAt      DateTime @default(now())
  lastMessageAt  DateTime @default(now())
}
model Message {
  id String @id @default(uuid()) @db.Uuid
  conversationId String @db.Uuid
  clientMsgId String
  seq BigInt
  @@unique([conversationId, clientMsgId])
  @@unique([conversationId, seq])
}
  • 步骤 4:应用迁移、生成客户端、运行约束测试并 Seed 两个用户及池中瓶子

运行:corepack pnpm prisma generate && corepack pnpm prisma migrate deploy && corepack pnpm prisma db seed && corepack pnpm test:integration -- database.spec.ts
预期:迁移成功,所有唯一约束测试 PASS。

  • 步骤 5:Commit
git add prisma apps/api/src/database tests/integration/database.spec.ts infra/compose.test.yaml
git commit -m "feat: 建立漂流瓶权威数据模型"

任务 3:实现 API 骨架、统一协议与健康检查

文件:

  • 创建:apps/api/package.json

  • 创建:apps/api/src/main.ts

  • 创建:apps/api/src/app.module.ts

  • 创建:apps/api/src/common/http-response.interceptor.ts

  • 创建:apps/api/src/common/domain.exception.ts

  • 创建:apps/api/src/common/request-id.middleware.ts

  • 创建:apps/api/src/health/health.controller.ts

  • 测试:apps/api/src/health/health.e2e-spec.ts

  • 步骤 1:编写健康检查与统一错误响应测试

await request(app.getHttpServer()).get('/api/v1/health').expect(200).expect(({ body }) => {
  expect(body.code).toBe('OK');
  expect(body.data.status).toBe('ok');
  expect(body.requestId).toBeTruthy();
});
  • 步骤 2:运行并确认 Nest 应用尚不存在

运行:corepack pnpm --filter @drift/api test -- health.e2e-spec.ts
预期:FAIL,无法加载 AppModule。

  • 步骤 3:实现 /api/v1 前缀、DTO 白名单、Helmet、同源 CORS、请求 ID、统一响应和异常映射

启动时必须开启 ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }),并在 /api/v1/health 同时检查数据库与 Redis。

  • 步骤 4:运行 API 测试

运行:corepack pnpm --filter @drift/api test
预期:PASS。

  • 步骤 5:Commit
git add apps/api
git commit -m "feat: 添加 NestJS API 基础设施"

任务 4:实现演示短信登录与安全 Token 生命周期

文件:

  • 创建:apps/api/src/auth/auth.module.ts

  • 创建:apps/api/src/auth/auth.controller.ts

  • 创建:apps/api/src/auth/auth.service.ts

  • 创建:apps/api/src/auth/token.service.ts

  • 创建:apps/api/src/auth/auth.guard.ts

  • 创建:apps/api/src/auth/dto/*.ts

  • 测试:apps/api/src/auth/auth.e2e-spec.ts

  • 步骤 1:编写验证码、登录、串行轮换、退出和 Refresh 重放测试

const sent = await request(server).post('/api/v1/auth/sms/send').send({ phone: '13800000001', deviceId: 'web-a' }).expect(201);
const login = await request(server).post('/api/v1/auth/sms/login').send({ phone: '13800000001', code: sent.body.data.debugCode, deviceId: 'web-a' }).expect(201);
expect(login.headers['set-cookie'][0]).toContain('refresh_token=');

重用已轮换 Cookie 必须返回 AUTH_REFRESH_REUSED,且当前设备会话被撤销。

  • 步骤 2:运行测试确认路由不存在

运行:corepack pnpm --filter @drift/api test -- auth.e2e-spec.ts
预期:FAIL,接口返回 404。

  • 步骤 3:实现手机号规范化/HMAC、验证码 Redis 哈希与 TTL、限流、账号创建、JWT、Refresh 哈希/轮换和注销

开发模式返回 debugCode;NODE_ENV=production 响应中必须无该字段。Cookie 设置 HttpOnly; SameSite=Strict; Path=/api/v1/auth,生产模式追加 Secure。

  • 步骤 4:运行登录安全测试与日志敏感数据扫描

运行:corepack pnpm --filter @drift/api test -- auth.e2e-spec.ts && corepack pnpm test:security
预期:全部 PASS,测试日志中不存在完整手机号、验证码、JWT 或 Cookie。

  • 步骤 5:Commit
git add apps/api/src/auth tests
git commit -m "feat: 实现演示验证码和令牌轮换"

任务 5:实现匿名资料、投瓶状态机与异步审核

文件:

  • 创建:apps/api/src/profile/*

  • 创建:apps/api/src/bottle/*

  • 创建:apps/api/src/outbox/*

  • 创建:apps/worker/package.json

  • 创建:apps/worker/src/main.ts

  • 创建:apps/worker/src/moderation.processor.ts

  • 测试:tests/integration/bottle-moderation.spec.ts

  • 步骤 1:编写投瓶配额、拒绝词、复审词、普通文本入池与 Outbox 幂等测试

expect(await throwBottle('祝你今天开心')).toMatchObject({ reviewStatus: 'REVIEWING' });
await worker.runOnce();
expect(await getBottle()).toMatchObject({ reviewStatus: 'APPROVED', poolStatus: 'IN_POOL' });

循环第 11 次投瓶必须返回 BOTTLE_DAILY_LIMIT。

  • 步骤 2:运行测试确认模块不存在

运行:corepack pnpm test:integration -- bottle-moderation.spec.ts
预期:FAIL,无法解析 Bottle 服务。

  • 步骤 3:实现匿名 Profile、Bottle 状态机、UTC+8 日期键、事务配额、审核任务与 Outbox Worker

规则配置固定为可测试集合:拒绝词进入 REJECTED,复审词进入 MANUAL_REVIEW,其余进入 IN_POOL。Worker 使用事件 ID 去重、FOR UPDATE SKIP LOCKED 领取和指数退避字段。

  • 步骤 4:运行相关单元/集成测试并验证重复消费不改变结果

运行:corepack pnpm test:unit && corepack pnpm test:integration -- bottle-moderation.spec.ts
预期:PASS。

  • 步骤 5:Commit
git add apps/api/src/profile apps/api/src/bottle apps/api/src/outbox apps/worker tests/integration/bottle-moderation.spec.ts
git commit -m "feat: 实现投瓶和异步内容审核"

任务 6:实现随机捞取、排除、租约和并发一致性

文件:

  • 创建:apps/api/src/match/match.module.ts

  • 创建:apps/api/src/match/match.controller.ts

  • 创建:apps/api/src/match/match.service.ts

  • 创建:apps/api/src/match/candidate.repository.ts

  • 创建:apps/worker/src/lease-reaper.processor.ts

  • 测试:tests/integration/match-concurrency.spec.ts

  • 步骤 1:编写本人/已捞/双向拉黑排除、21 次配额、并发租约和超时回池测试

const results = await Promise.allSettled([pick(userA), pick(userB)]);
expect(results.filter(x => x.status === 'fulfilled')).toHaveLength(1);
expect(await activeLeaseCount(bottle.id)).toBe(1);
  • 步骤 2:运行测试确认失败

运行:corepack pnpm test:integration -- match-concurrency.spec.ts
预期:FAIL,MatchService 不存在。

  • 步骤 3:实现小批随机候选、事务行锁、CAS、租约历史、展示即计次和有限重试

查询必须排除作者、历史、双向 block、非 IN_POOL 和活跃租约;事务以数据库为最终裁决,候选为空返回 BOTTLE_POOL_EMPTY。

  • 步骤 4:连续运行并发测试 20 次以排除偶发竞态

运行:for i in $(seq 1 20); do corepack pnpm test:integration -- match-concurrency.spec.ts || exit 1; done
预期:20 次全部 PASS。

  • 步骤 5:Commit
git add apps/api/src/match apps/worker/src/lease-reaper.processor.ts tests/integration/match-concurrency.spec.ts
git commit -m "feat: 实现公平捞瓶和领取租约"

任务 7:实现首次回复、会话与 Socket.IO 消息

文件:

  • 创建:apps/api/src/conversation/conversation.module.ts

  • 创建:apps/api/src/conversation/conversation.controller.ts

  • 创建:apps/api/src/conversation/conversation.service.ts

  • 创建:apps/api/src/conversation/chat.gateway.ts

  • 创建:apps/api/src/conversation/socket-auth.adapter.ts

  • 测试:tests/integration/conversation-concurrency.spec.ts

  • 测试:tests/integration/socket-chat.spec.ts

  • 步骤 1:编写并发首次回复、clientMsgId 幂等、非成员拒绝、顺序消息和断线补齐测试

const [a, b] = await Promise.all([
  reply({ bottleId, leaseId, clientMsgId: 'first-1', text: '你好' }),
  reply({ bottleId, leaseId, clientMsgId: 'first-1', text: '你好' }),
]);
expect(a.conversationId).toBe(b.conversationId);
expect(await conversationCount(bottleId)).toBe(1);
  • 步骤 2:运行测试确认失败

运行:corepack pnpm test:integration -- conversation-concurrency.spec.ts socket-chat.spec.ts
预期:FAIL,会话模块和 Socket namespace 不存在。

  • 步骤 3:实现事务建会话、会话内 seq、成员授权、消息审核、持久化后 ACK、房间广播和游标历史接口

Socket namespace 使用 /chat,事件使用 message:send、message:ack、message:new、conversation:read。握手和每条命令均校验 Access Token 与成员状态。

  • 步骤 4:运行会话/Socket 测试并模拟断开后重连

运行:corepack pnpm test:integration -- conversation-concurrency.spec.ts socket-chat.spec.ts
预期:PASS,重连同步无缺失、无重复且 seq 递增。

  • 步骤 5:Commit
git add apps/api/src/conversation tests/integration/conversation-concurrency.spec.ts tests/integration/socket-chat.spec.ts
git commit -m "feat: 实现实时匿名会话"

任务 8:实现举报、拉黑、处罚、通知与审核后台 API

文件:

  • 创建:apps/api/src/safety/*

  • 创建:apps/api/src/notification/*

  • 创建:apps/api/src/admin/*

  • 创建:apps/worker/src/notification.processor.ts

  • 测试:tests/integration/safety-admin.spec.ts

  • 步骤 1:编写拉黑即时禁止消息/匹配、举报快照、管理员处置和审计测试

await block(userA, userB);
await expect(sendMessage(userB, conversationId, 'still there?')).rejects.toMatchObject({ code: 'USER_BLOCKED' });
expect(await auditLogsFor(report.id)).toContainEqual(expect.objectContaining({ action: 'REPORT_RESOLVED' }));
  • 步骤 2:运行测试确认失败

运行:corepack pnpm test:integration -- safety-admin.spec.ts
预期:FAIL,Safety/Admin 模块不存在。

  • 步骤 3:实现举报、双向业务隔离、处罚检查、最小管理员角色、审核/举报处置和站内通知

管理员接口置于 /api/v1/admin,普通 Token 必须返回 403;审计元数据不得包含完整手机号或消息正文。

  • 步骤 4:运行治理测试和 IDOR 测试

运行:corepack pnpm test:integration -- safety-admin.spec.ts && corepack pnpm test:security
预期:PASS。

  • 步骤 5:Commit
git add apps/api/src/safety apps/api/src/notification apps/api/src/admin apps/worker/src/notification.processor.ts tests/integration/safety-admin.spec.ts
git commit -m "feat: 完成举报拉黑和审核处置闭环"

任务 9:实现移动端 Vue 3 PWA

文件:

  • 创建:apps/web/package.json

  • 创建:apps/web/vite.config.ts

  • 创建:apps/web/src/main.ts

  • 创建:apps/web/src/App.vue

  • 创建:apps/web/src/api/client.ts

  • 创建:apps/web/src/api/socket.ts

  • 创建:apps/web/src/stores/*.ts

  • 创建:apps/web/src/router/index.ts

  • 创建:apps/web/src/views/*.vue

  • 创建:apps/web/src/components/*.vue

  • 创建:apps/web/src/styles/*.css

  • 创建:apps/web/public/manifest.webmanifest

  • 测试:apps/web/src/**/*.spec.ts

  • 步骤 1:编写登录刷新串行化、路由守卫、消息去重和核心组件测试

it('coalesces concurrent 401 refreshes', async () => {
  await Promise.all([client.get('/me'), client.get('/conversations')]);
  expect(refreshSpy).toHaveBeenCalledTimes(1);
});
  • 步骤 2:运行 Web 测试确认应用不存在

运行:corepack pnpm --filter @drift/web test
预期:FAIL,Vite/Vue 入口不存在。

  • 步骤 3:实现完整页面、深海设计系统、无障碍状态、Pinia 数据流、Socket 重连与 PWA

必须具备 375px 移动视口、桌面居中布局、44px 最小触控区域、键盘焦点样式、加载/空/错误/离线状态;Access Token 只在内存中,刷新由单 Promise 锁合并。

  • 步骤 4:运行组件测试、类型检查、生产构建及 PWA Manifest 检查

运行:corepack pnpm --filter @drift/web test && corepack pnpm --filter @drift/web typecheck && corepack pnpm --filter @drift/web build
预期:全部 PASS,生成 apps/web/dist/manifest.webmanifest。

  • 步骤 5:Commit
git add apps/web
git commit -m "feat: 构建漂流瓶移动端 PWA"

任务 10:实现双用户完整 E2E

文件:

  • 创建:playwright.config.ts

  • 创建:tests/e2e/auth.spec.ts

  • 创建:tests/e2e/bottle-chat.spec.ts

  • 创建:tests/e2e/safety.spec.ts

  • 创建:tests/e2e/admin.spec.ts

  • 步骤 1:编写两个独立浏览器上下文的失败 E2E

const alice = await browser.newContext();
const bob = await browser.newContext();
// Alice 登录并投瓶;Bob 登录、捞取、首次回复;双方实时互发;Bob 断线后补齐;Alice 举报并拉黑。

测试必须通过页面交互完成,不直接调用领域服务绕开 UI。

  • 步骤 2:运行 E2E 并记录首个真实失败点

运行:corepack pnpm test:e2e
预期:初次 FAIL,并指向尚未正确接线的页面或接口。

  • 步骤 3:只修复 E2E 暴露的接线、选择器、等待条件与真实缺陷

使用 data-testid 稳定选择器;禁止固定 sleep,等待 API 响应、Socket ACK 或可见状态。

  • 步骤 4:运行全部 E2E,包含登录、完整社交闭环、治理和后台审计

运行:corepack pnpm test:e2e
预期:全部 PASS,并保留失败时 trace/screenshot/video。

  • 步骤 5:Commit
git add playwright.config.ts tests/e2e apps
git commit -m "test: 覆盖双用户漂流瓶完整流程"

任务 11:容器化部署与局域网预览

文件:

  • 创建:apps/api/Dockerfile

  • 创建:apps/worker/Dockerfile

  • 创建:apps/web/Dockerfile

  • 创建:infra/compose.yaml

  • 创建:infra/nginx.conf

  • 创建:infra/healthcheck.sh

  • 创建:README.md

  • 步骤 1:编写部署冒烟脚本并确认服务未启动时失败

curl -fsS http://127.0.0.1:${APP_PORT}/api/v1/health | jq -e '.code == "OK" and .data.status == "ok"'
curl -fsS http://127.0.0.1:${APP_PORT}/ | grep -q '<div id="app">'
  • 步骤 2:运行冒烟脚本确认连接失败

运行:bash infra/healthcheck.sh
预期:FAIL,目标端口未监听。

  • 步骤 3:实现多阶段镜像、同源 Nginx 代理、健康检查、迁移 Job、持久卷和最小权限容器

Compose 服务为 postgres、redis、migrate、api、worker、web;API/Worker 等待迁移成功,Web 仅暴露配置端口并代理 /api 与 /socket.io。

  • 步骤 4:真实构建、启动、迁移、Seed、健康检查并从局域网地址请求

运行:docker compose -f infra/compose.yaml up -d --build && bash infra/healthcheck.sh
预期:所有服务 healthy,首页和 API 健康检查均成功。

  • 步骤 5:Commit
git add apps/*/Dockerfile infra README.md
git commit -m "ops: 添加可复现本机部署"

任务 12:最终质量、安全和验收验证

文件:

  • 修改:README.md

  • 创建:docs/verification/2026-08-03-mvp-verification.md

  • 步骤 1:运行完整静态、单元、集成、E2E 和生产构建门禁

运行:corepack pnpm lint && corepack pnpm typecheck && corepack pnpm test:unit && corepack pnpm test:integration && corepack pnpm test:e2e && corepack pnpm build
预期:全部退出码 0。

  • 步骤 2:运行并发、幂等、权限和敏感日志专项测试

运行:corepack pnpm test:security && corepack pnpm test:concurrency
预期:全部 PASS。

  • 步骤 3:验证 Compose 状态、健康端点、监听端口和局域网 IP

运行:docker compose -f infra/compose.yaml ps && bash infra/healthcheck.sh && hostname -I
预期:所有必要容器 healthy,输出至少一个可访问 IP。

  • 步骤 4:将真实命令、退出码、测试统计、容器状态和预览 URL 写入验证报告

验证报告不得填写推测结果;只记录命令实际输出。README 写明演示验证码流程、管理员 Seed 登录方式、启动/停止/重置命令和数据清理风险。

  • 步骤 5:最终 Commit
git add README.md docs/verification/2026-08-03-mvp-verification.md
git commit -m "docs: 记录 MVP 验收结果"