Files
photo-live/docs/plans/progress/P1a-验收记录.md
T
浅念 8a09001525 docs: p1a acceptance record
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 00:06:44 +08:00

89 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P1a 验收记录
- **阶段**:P1a 后端核心链路(计划 `docs/plans/2026-09-17-P1a-后端核心链路.md`,Task 1–26)
- **日期**:2026-09-18
- **基线 commits**:`d973c71..HEAD`(`chore: init repository skeleton` 起,共 28 个提交,不含本记录提交)
- **执行模式**:子代理驱动 + 双审(每任务先写失败测试→实现→通过→提交;跨任务由子代理接力,主代理与复核代理双重审查)
- **回归门**:`pnpm lint && pnpm typecheck && pnpm test && pnpm e2e && pnpm build` 全绿(数字见文末统计表)
- **基础设施**:photo-live MySQL/Redis/MinIO compose(`infra/`),测试全部跑在真实容器上(MySQL TRUNCATE 隔离 + MinIO 前缀清理 + Redis/BullMQ 真队列)
---
## A01 认证与会话(API 级)
- 证据:`services/api/test/auth.e2e-spec.ts` **14 用例**全绿——正确登录(tokenHash=sha256、库内不存明文、audit auth.login)、错误密码/不存在账号同码 401(防枚举)、**未激活(credentialStatus=pending)/停用(sourceStatus=disabled)/本地锁定(localLocked)正确密码也不放行**、限速锁定与解锁窗口、会话滑动续期(旧令牌不轮换)、登出即撤销(refresh 401)、seed-admin 幂等(重跑不改写密码,`seed.spec.ts`)。
- 全链路佐证:`e2e.full-chain.e2e-spec.ts` admin/staffB 均走真实 `POST /auth/password` 登录。
- **App 端(微信登录等)留 P3**:P1a 仅密码会话体系。
## A02 权限逐次校验(防中间人/越权)
- 证据:`services/api/test/permissions.spec.ts` **11 用例**——canViewAlbum/canManageAlbum/canUploadToAlbum/canManagePhoto 对 owner/collaborator/admin/非成员/被移除成员的全组合,且每次调用直查 DB 不缓存。
- e2e 全程逐次校验:uploads.e2e(非成员/他册成员 404、被移除成员 renew/complete 403)、photos.e2e(协作者对他人上传 403)、**full-chain(owner 移除 staffB 后,同一有效会话的 init → 404 ALBUM_NOT_FOUND、管理历史上传 → 404 PHOTO_NOT_FOUND,权限链即刻失效)**。
## A03 上传幂等与并发安全
- 证据:`services/api/test/uploads.e2e-spec.ts`(init 幂等复用同 uploadId+photoId、同 clientTaskId 异参 409、**同名文件不同 clientTaskId 两会话两照片不覆盖**、complete 幂等不重复入队/不重复 audit、字节/类型不符 400 后会话可重试、staging 删后 re-complete 走 copyIfAbsent created:false)。
- full-chain 断言:同名 `show.jpg` 两传(900px 真 JPEG 双 presign PUT 实写)→ complete 其一调两次 → photo_jobs 各恰 1 条 → Worker 发布后 **outbox 全库恰 2 条 photo.published,无第三条**;重复 processPhoto → `already` 不再发自带事件。
## A06 上传即公开(发布事务)
- 证据:`services/worker/test/publish.e2e-spec.ts` **7 用例**——发布事务(变体对象 + photo_variants×2 + photos ready/published/entityVersion+1 + outbox payload 契约全集,单事务一致)、重复处理幂等、不可处理态跳过、sweepOnce 认领/僵尸重排、真实 BullMQ Worker happy 路径、**损坏文件 3 次退避终败且 visibility 保持 unpublished(失败不公开)**、pending_upload 跳过。
- full-chain:API complete 入队 → 真实 Worker 消费 → 双照片 ready+published、MinIO 变体对象(head 复核 jpeg 非零字节)、photo_jobs done(attempt=1)。
- **Worker 进程冒烟(Task 21 关注点)**:`pnpm dev:worker` 实机启动,`photo worker started`(concurrency=2、sweep 30s)后首轮 sweep `claimed=0` 正常完成,无崩溃退出。
## A07 公开读性能/稳定性(后端级)
- 证据:`services/api/test/public.e2e-spec.ts`——**35 张已发布照片跨页稳定**(缺省 30 + 次页 5,并集恰 35 无重复;`(publishedAt,id)` 键集游标从不回看已发射行)、同 publishedAt 批次 id DESC 决胜、hidden/deleted/unpublished 永不出现、无变体行的发布照片防御性跳过不炸公开页、签名 URL 可取回真实字节。压测级指标留 G1。
## A13 凭证安全(设计级)
- presign 短期凭证:上传 PUT 与读取 GET 均为**预签名 URL,TTL 由 `SIGNED_URL_TTL_SECONDS=900`(env)控制**,会话过期滑动续期走 renew。
- **无长期客户端密钥**:客户端仅持有会话 token(32B 随机、库存 sha256 哈希)。
- **DB 不存签名 URL**:库中只有对象键(objectKey),URL 每次请求现签(公开单图即契约规定的过期刷新入口)。
- OSS 生命周期核验(staging 过期清理等)随 G1/P5 落实。
## HEIC 实测结论(D12 复核项)
- **HEVC 压缩 HEIF**:预编译 libvips 无 HEVC 编码器,编码用例**自跳过**(`pipeline.spec.ts` 捕获后 skip 并说明);iPhone 真机样本端到端验证留 **G2**。
- **AV1 压缩 HEIF**:走同一 libheif 解码路径与 `heif` 格式桶,解码→JPEG 变体用例**通过**。
---
## 偏差与发现登记
| # | 事项 | 处理/结论 |
|---|------|-----------|
| 1 | quay.io 镜像 | MinIO 镜像改用 `quay.io/minio/*`(Docker Hub 官方仓库已删除);P2 ov 侧同样 |
| 2 | Prisma 校对 | migrate 产物与 schema 校对存在手工修正流程(生成后人工核对差异再提交) |
| 3 | `pnpm --` 语法 | 透传参数给包脚本必须 `pnpm <cmd> -- <args>`,曾因缺 `--` 踩坑,已记入执行注意 |
| 4 | presigner 不签 content-type | S3 presign PUT 不覆盖 Content-Type 头 → complete 时以 `head().contentType` 与申明值比对兜底(uploads.e2e 有专例) |
| 5 | P2002 matcher 存量 bug | `isUniqueViolationOn` 对复合索引匹配存在存量缺陷,已修复并回归 |
| 6 | maxWorkers/turbo concurrency 串行化 | api jest `maxWorkers: 1` + turbo `concurrency: 1`:DB 型 spec 共库并行会互相 TRUNCATE 脏数据 |
| 7 | turbo e2e 并发竞态 | `test:e2e` 同样串行化(依赖同 MySQL/Redis/MinIO 实例),避免跨包 e2e 并发互扰 |
| 8 | collaborator 重新公开 403 | hidden→published 恢复 P1a 从严仅 owner/admin(**待需求确认**,文件头已登记;photos.e2e 覆盖该 403) |
| 9 | nest-pino → nestjs-pino | 包名勘误:正确包为 `nestjs-pino`(曾误引 `nest-pino`) |
| 10 | ESM 方案 (a) | `@photo/contracts` 为 ESM 包,运行时经 `require(esm)` 消费 dist(Node ≥ 22.12 前提;jest 侧 moduleNameMapper 映射 TS 源) |
| 11 | request-id UUID | 请求 id 采用 UUID(错误信封/日志/响应头三处一致,`x-request-id`) |
## 未尽事项 / 移交
- **P1b**:viewer 基础页;admin 最小页;**宣传图/封面/Logo 上传链路**(album_assets presign+确认,复用照片上传机制,R09 依赖);**GET /albums/{albumId}/photos 管理照片列表**(含未发布/隐藏态);**用户账号查询接口**(邀请协作者按 account 搜索);Playwright 冒烟。
- **P2 关注**:outbox 已落库但**暂无消费者**(副本/通知接入时按 eventId 去重、entityVersion 单调消费);**公开端点无限速**(点赞限流引入时统一评估公开面配额)。
- **G1/P5**:OSS 生命周期核验;**G2**:HEVC-HEIF 真机样本端到端验证。
---
## 测试统计汇总(2026-09-18 回归门实测)
| 包 | 单测 suites/tests | e2e suites/tests |
|----|------------------|------------------|
| @photo/contracts | 2 / 8 | – |
| @photo/db-cn | –(schema 校验随 migrate/生成流程) | – |
| @photo/storage | 1 / 6(真实 MinIO 集成) | – |
| @photo/api-cn | 6 / 46 | **9 / 120**(auth 14、albums 20、members-categories 15、uploads 20、photos 14、public 17、health 4、db.semantics 6、full-chain 10) |
| @photo/worker | 1 / 8 | 1 / 7(publish) |
| **合计** | **10 / 68** | **10 / 127** |
- 附加门:lint 5/5 包、typecheck 8 任务、build 5 任务全绿;`pnpm dev:worker` 启动冒烟通过。
- 总测试数(单测 + e2e):**195**。