docs: 添加漂流瓶 Web MVP 实现计划
This commit is contained in:
@@ -0,0 +1,553 @@
|
|||||||
|
# 漂流瓶 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 测试**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:编写数据库约束失败测试**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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 时间字段**
|
||||||
|
|
||||||
|
关键约束:
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:编写健康检查与统一错误响应测试**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 重放测试**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 幂等测试**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 次配额、并发租约和超时回池测试**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 幂等、非成员拒绝、顺序消息和断线补齐测试**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:编写拉黑即时禁止消息/匹配、举报快照、管理员处置和审计测试**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:编写登录刷新串行化、路由守卫、消息去重和核心组件测试**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:编写部署冒烟脚本并确认服务未启动时失败**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add README.md docs/verification/2026-08-03-mvp-verification.md
|
||||||
|
git commit -m "docs: 记录 MVP 验收结果"
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user