Files
photo-live/docs/plans/progress/P4b-验收记录.md
T
浅念 22188ce3ee docs: p4b acceptance record
- P4b 验收记录(A11/A22 简化级/A23 组件/A12+T15 证据、偏差 16 项、
  P5 移交清单、测试统计 web 655 +66 / mobile 280 零漂移)
- 总纲 P4b 进度注记:P1-P4b 全部本地阶段完成,仅余 P5 真实环境接入
  (G5/G6 + D03/D05/D08/D09/D10/D13)
- P4b 计划勾选回填 + 完成头注

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 23:59:21 +08:00

159 lines
16 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.
# P4b 验收记录
- **阶段**:P4b 传播组件 + 全端语言验收(计划 `docs/plans/2026-09-20-P4b-悬浮组件与全端语言.md`,Task 1–7)
- **日期**:2026-09-20/21(契约→后端→loader→轮播页→管理页同日链式推进,语言验收与回归门跨夜收口)
- **基线 commits**:`17d09bb..HEAD`(tag `p4a` → 本记录提交;共 9 个提交 = 2 计划(含评审修正)+ 6 实施 + 1 本记录)
- **执行模式**:子代理驱动(每任务先写失败测试→实现→通过→提交;契约批次先行 gen + redocly 门);Task 6 语言验收与 Task 7 回归门/本记录由主代理执行
- **回归门**:web 侧 `pnpm lint && pnpm typecheck && pnpm test && pnpm build && pnpm e2e` 全绿(lint 10/10、typecheck 13/13、test 12/12、build 10/10 包任务;数字见文末统计表);mobile 侧 `flutter analyze` **No issues found!** + `flutter test` **280/280**(P4b 仅补错误码镜像常量 + 注释,语义零漂移)
- **基础设施**:`infra/docker-compose.yml --profile ov`(mysql/redis/minio 双地域)healthy;e2e 均为进程内真实 Nest 应用 + 真实 MinIO/Redis/MySQL + 真实 outbox→ov 传播 + 真实 BullMQ worker;Playwright 以 viewer publicDir 宿主夹具页(`/__test__/embed-host.html`)承载真实 loader.js→iframe→ov 公开端点全链
---
## 任务清单与提交
| Task | 内容 | Commit |
|------|------|--------|
| 1 | 契约扩展 P4b 批次(embed-slots 管理/公开端点 + ETag/304 + constants EMBED_* 轮询参数) | `eb51069` |
| 2 | 展示位后端(cn 管理 admin-only + slot-state 同步 + ov 副本 + 公开 feed + 限速 + e2e) | `aee4b7a` |
| 3 | embed-loader(版本化 /embed/v1/loader.js,postMessage 白名单、窄屏规则、收起记忆) | `ddb4c81` |
| 4 | 轮播 iframe 页 `/embed/:slotToken`(configVersion 旧响应丢弃、不可见暂停、ETag 轮询) | `8f2c21d` |
| 5 | admin 展示位管理页(列表/设置表单/绑定换绑/嵌入代码卡) | `0238f50` |
| 6 | T15 全端语言验收 + 漏译修复(四端机器守护 + 运行时抽查) | `7385785` |
| 7 | 回归门 + 本记录 | 本提交 |
---
## 验收证据(按 A 编号)
### A11 悬浮组件(加载器 + 轮播 + 自动更新 + 不破坏宿主)
**embed-loader 单元**(`packages/embed-loader/src/params.test.js`,11 例):data-* 参数解析(缺省全集/slot 清洗/lang/position 白名单回退/interval 夹取 [3000,30000])、**窄屏 <480px 强制 sm + 默认收起**(评审缺口:移动端可用)、查询参数拼接、**slot token 编码(防路径注入)**、resize 高度夹取 [60,640] 与非法值忽略。
**宿主页全链 Playwright**(`apps/viewer/e2e/embed.spec.ts`,8 例,真实后端 + 真实 loader.js):
- 宿主夹具页引入 loader → iframe 出现 → `lang=auto` → 相册 defaultLocale 标题;3 张照片按 publishedAt 倒序就位;intervalMs 切换(active 照片 src 轮换断言);**点击照片 → 新标签打开 viewer 单图深链(noopener,lang 跟随 resolvedLocale)**
- `lang=en` → iframe 内英文标题 + 深链 `lang=en`(T12/A12 embed 双语,Task 6 补)
- 收起按钮 → postMessage collapse → wrapper 高度 0 + 把手出现 + sessionStorage 记忆 → 刷新仍收起 → 把手点击展开恢复轮询
- 窄屏视口(375×667)默认收起 + sm 尺寸(280px)+ 把手展开可达
- **新照片发布 → 真实管线(cn→outbox→ov)→ 轮询窗口内队尾追加**(A11 数据路径)
- 空相册 → 「照片即将上线」小卡;幽灵 token → 静默 collapse(iframe 内无错误 UI,防探测)
### A22 展示位管理(简化级:admin-only 权限)
**api e2e** `services/api/test/embed.e2e-spec.ts`(真实 cn+ov 双应用 + 真实 outbox/sync-inbox,9 例):
- ① admin 创建/列表 + **非 admin 403** + 校验 400 + 创建即 slot-state 事件(未绑定 → 公开 404)
- ② 绑定相册 → ov 公开数据:照片 publishedAt 倒序 / lang 标题回退(相册 defaultLocale)/ **ETag 304**(If-None-Match 命中零负载)
- ③ 新照片发布 → ETag 必变 → 200 增量(A11 数据路径)
- ④ **换绑相册:configVersion+1 → ov 服务新相册照片**(20 张上限)——loader 只认 token,宿主无需改动
- ⑤ 相册下线(publicEnabled=false)→ 公开端 404;恢复 → 200
- ⑥ 解绑 → 404;复绑 → 200;停用(软下线 enabled=false)→ 404
- ⑦ slot-state 乱序/重复投递幂等(版本比较,复用 sync harness)
- ⑧ 公开端点限速:slotToken+IP 固定窗超额 429(**304 同样消耗预算**)
**viewer 纯测试**(`apps/viewer/src/__tests__/embed-feed.spec.ts`,18 例)之 A22 核心:**旧响应丢弃双守卫**(请求派发序号 seq 过期丢弃 + configVersion 低于本地已接受值丢弃)、同版本合并(新照片队尾追加/消失淘汰/URL 刷新)、**版本升 → 全量重置**(换绑)、404 静默 + collapse 上报。
**admin Playwright** `apps/admin/e2e/embed-slots.spec.ts`(4 例):新建(设置表单 → 行回显);**绑定相册(A22 确认文案 → configVersion 递增 → 经 ov API 断言公开 feed 返回相册照片)**;换绑(configVersion+1 → ov feed 切换新相册——换绑不改官网);停用(软下线 → ov 404)。vitest `src/utils/system/embedSlots.spec.ts` 9 例(缺省/夹取/整体替换载荷/EMBED_ORIGIN 代码卡拼接/绑定三类确认文案携 A22 短语/zh-en 键集一致)。
**权限简化说明(偏差 #1/#2)**:「创建者只操作被授权展示位」语义本阶段未出——embed_slot_members 表已建(迁移)但无授权 UI/API,管理权限=仅公司 admin(A22 简化级,镜像 P2 A25 部分交付模式;授权面移交 P5/后续按需)。
### A23 组件部分(不可见暂停 + 轮询经济性)
`apps/viewer/src/__tests__/embed-feed.spec.ts`(18 例)之 A23:**document.visibilityState hidden → 停轮询停切换,恢复 → 立即校准一次**;**轮询退避(连续失败 5 次间隔 ×5,成功即恢复)**;collapse 停止计时器/expand 恢复并补报 resize;**ETag/If-None-Match 轮询(304 = 零处理零负载)**。Playwright 增量用例(embed.spec「newly published photo appears within the poll window」)以 2s 轮询窗口验证真实数据到达。参数默认集中在 `packages/contracts/src/constants.ts`(EMBED_POLL_INTERVAL_MS=30000 / MAX_FAILURES=5 / BACKOFF_MULTIPLIER=5,env 可调——P5 冻结,见移交)。
### A12 + T15 全端语言验收(Task 6)
**机器覆盖断言(新增 3 个守护测试文件)**:
| 端 | 守护 | 内容 |
|----|------|------|
| viewer | `src/__tests__/error-coverage.spec.ts`(3 例) | 契约 ErrorCodes 枚举全集在 `src/api/errors.ts` 映射表无缺漏;表内每个文案键在 zh-CN/en 词条均存在且非空;**zh-CN/en 词条键集递归相等** |
| admin | `src/utils/http/error.spec.ts`(3 例) | 同上三断言(映射表 `API_CODE_MSG_KEYS`,locale zh.json/en.json 键集相等) |
| mobile | `test/l10n/arb_completeness_test.dart`(P3 落地,引用) | zh/en arb 消息键集合相等 + 占位符 token 集合相等 + `@key` 元数据一致(本次 280/280 复跑通过) |
**api 侧口径**:服务端不做文案本地化——错误信封 `{code, message, requestId}` 的 message 为英文诊断兜底,客户端按稳定 code 映射多语言(openapi.yaml info.description 契约定)。枚举单源性由 `packages/contracts/src/__tests__/events.test.ts` 的 **openapi Error.code enum ↔ errors.ts ErrorCodes 编译期双向恒等守护**保证(P2 落地,引用);api 全部错误码均经 `ErrorCodes.*` 常量抛出(TS 收敛,无字面量漂移)。故「无漏译」断言面 = 三客户端映射表全覆盖(上表)。
**漏译发现与修复(4 处,全部由机器审计捕获后修复)**:
1. **viewer `viewer.photoRemoved` en 词条缺失**(zh 有 en 无——键集相等断言直接命中;补 'This photo has been removed')
2. **admin 映射表与 locale 词条脱节**:locale 侧 apiMsg 10 码词条早已存在,映射表仅登记 6 码——ALBUM_NOT_FOUND/PHOTO_NOT_*/UPLOAD_*/MEMBER_CONFLICT 实际回退英文服务端文案;按枚举全集登记(+RATE_LIMITED/ALBUM_NOT_PUBLIC/DOWNLOAD_DISABLED/SYNC_AUTH_FAILED 四码新词条 ×2 语言)
3. **viewer DOWNLOAD_DISABLED 观众可达但无专属文案**(P4a 下载两级开关中途关闭时只有通用「下载失败」):新增 `errors.downloadDisabled` 双语词条 + `useDownload` 按码接线(专属提示,vitest 新用例固化)
4. **mobile 错误码镜像漂移**:P4a 增补的 DOWNLOAD_DISABLED 未镜像进 `lib/core/error_codes.dart`(「1:1 对照」注释计数 17 → 实际契约 18)——补常量 + 计数修正(App 无下载面,文案走 errorGeneric 兜底)
**运行时抽查(Playwright,真实 DOM)**:
- **admin en 主旅程**:`apps/admin/e2e/locale-en.spec.ts`——登录页顶栏语言菜单(用户级设置)切 English → 表单/占位符/按钮英文(Welcome back / Enter account / Login)→ 正确登录 → 相册管理列表页英文(Album Management / New Album)→ 刷新后偏好持久仍英文。zh 版主旅程由既有 login/albums/embed-slots/users 系列覆盖(引用)
- **viewer en 旅程**:既有各 spec 覆盖(i18n.spec 切换语言重渲染 + `lang` 属性 + localStorage 持久;feed/likes/download/journey 等跨页面用例断言英文文案,引用 P1b/P4a 记录)
- **embed iframe 双语**:embed.spec `lang=auto → zh-CN`(defaultLocale 回退)与 `lang=en`(Task 6 新增)两语言渲染断言 + 深链 lang 跟随
- **mobile 语言切换**:P3 IT 腿已过(`test/app/locale_test.dart` + arb 守护;本次复跑 280/280,引用 P3 记录)
**无法机器验证的残留**:无(四端均为键集/枚举机器断言 + 代表性真实 DOM 抽查;截图级逐屏对比不单独留档,以既有各页面 Playwright 断言为准)。
---
## 偏差与发现登记
### 计划内(计划/代码注释明示)
| # | 事项 | 说明 |
|---|------|------|
| 1 | embed_slot_members 建而未用 | 表结构与迁移已落(P4b Task 2),授权 UI/API 不出——P5/后续按需 |
| 2 | 展示位权限简化 admin-only | 需求「管理员统一管理 + 创建者操作被授权展示位」的后半语义简化为仅公司 admin 可管理(A22 简化级,镜像 P2 A25 部分模式);绑定校验退化为 admin 恒真 |
| 3 | ETag 四元组 | `"{configVersion}-{photoCount}-{maxPublishedAtMs}-{maxPhotoId}"`(计划评审修正:纯实体版本会因新照片小版本号 304 陈旧;含 photoCount/publishedAt 后新照片发布必变) |
| 4 | esbuild devDep | loader 打包+minify 复用依赖树内 esbuild 显式声明;产物 `packages/embed-loader/dist/v1/loader.js` 提交入库(≤5KB、无 console) |
| 5 | contracts 子路径导出 | `./constants`(P4b Task 3 既有:浏览器安全入口,根 index 含 internal-sync 的 node:crypto);Task 6 同理增补 `./errors`(errors.ts 纯 TS,viewer 生产代码运行时枚举引用走此入口) |
| 6 | CORS exposedHeaders ETag | ov 公开端点需跨源暴露 ETag 头供 loader 页读取(embed-ov 模块 CORS 配置) |
| 7 | eslint ignore loader 产物 | `dist/v1/loader.js` 为打包产物,格式检查豁免(eslint ignore 配置) |
| 8 | settings 整体替换语义 | PATCH settings 变更送完整对象(契约 Json 字段整体语义,非逐字段 merge);admin 表单侧保证字段齐全(embedSlots.spec 固化) |
| 9 | 宿主夹具页入 publicDir | `apps/viewer/public/__test__/embed-host.html` 仅测试资产(计划约定:不入 viewer 生产路由);模拟第三方引 loader script,查询参数透传 data-* + 同源注入 `__embedPollMs` 加速轮询断言 |
| 10 | 轮询缺省跟随 interval 查询参数 | `clampEmbedIntervalMs(intervalParam) ?? EMBED_POLL_INTERVAL_MS`——展示间隔与轮询间隔共用一个参数(3s 轮播即 3s 轮询);独立调节仅测试注入,P5 若需解耦再拆参数(D10) |
| 11 | 跨源路径自动化以同源覆盖 | 夹具页与 viewer 同源,真实第三方跨源场景以「消息 origin === iframe contentWindow 源」白名单校验(loader 侧实现 + 评审保证);sandbox 必含 allow-same-origin(opaque origin 会拒掉自身 resize/collapse 消息,计划评审修正已落) |
### 计划外(本记录补登)
| # | 事项 | 影响/处理 |
|---|------|-----------|
| 12 | admin apiMsg 词条与映射表脱节(Task 6 发现) | 模板 locale 早有 10 码词条但映射表仅 6 码——上传/相册域错误实际回退英文服务端文案(真漏译);按枚举全集登记修复 + 守护测试防回退 |
| 13 | viewer.photoRemoved en 缺失(Task 6 发现) | 轮播中被移除照片的轻提示 en 侧无词条;键集相等断言捕获,补齐 |
| 14 | mobile 错误码镜像漏 DOWNLOAD_DISABLED(Task 6 发现) | P4a 契约增补未同步镜像(注释仍写 17 项);补常量 + 计数修正(文案走 errorGeneric 兜底,App 无下载面) |
| 15 | admin 增 @photo/contracts devDep(仅测试消费) | 错误码枚举单源引用进守护测试;admin tsconfig 为 classic moduleResolution(不走 package exports),测试内从根入口导入(node 环境 node:crypto 无碍),生产代码不受影响 |
| 16 | viewer 下载失败提示按码分化 | `useDownload` 的 failed 由布尔改为文案键(''/download.failed/errors.downloadDisabled),PhotoLightbox 直接渲染——DOWNLOAD_DISABLED 观众专属文案(T15 修复项 ③) |
## 移交 P5
- **真实域名与部署**:EMBED_ORIGIN env(loader src 自推导已支持 CDN 部署;admin 代码卡拼接已按 env)
- **CDN 预热**:/embed/v1/loader.js 与轮播页静态资产上 CDN;版本路径 /embed/v1/ 长期稳定(v2 才破坏性变更)
- **embed 页 frame-ancestors CSP**:宿主接入 CSP 要求(script-src/frame-src {EMBED_ORIGIN})已文档化(packages/embed-loader/README-embed.md);**服务端响应头 frame-ancestors 配置 P5**(本地仿真未配)
- **展示位授权 UI 按需**:embed_slot_members 表已建;「创建者操作被授权展示位」API/UI + 绑定校验升级(A22 完整级)
- **轮询参数冻结(D10)**:EMBED_POLL_INTERVAL_MS=30000 / MAX_FAILURES=5 / BACKOFF_MULTIPLIER=5、公开端点限速窗——env 可调,P5 实测后与 D05/D10 一并冻结
- **待用户决策**:D03 分发方式、D05 并发目标、D08 用户接口契约(G4)、D09、D10、D13(总纲决策挂起清单);G2 真机验证(P3 清单)、G5(字体打包/真实 OSS)、G6(跨地域演练)仍待真实环境
---
## 测试统计汇总(2026-09-20/21 回归门实测)
**Web 侧**(P4a 出口 589 → 655,+66)
| 包 | 单测 suites(files)/tests | e2e suites(tests) |
|----|--------------------------|-------------------|
| @photo/contracts | 4 / 29 | – |
| @photo/storage | 1 / 6 | – |
| @photo/embed-loader(P4b 新) | 1 / 11 | –(宿主页用例计入 viewer playwright) |
| @photo/api-cn | 7 / 51 | **23 / 247**(P3 239 → +8:embed slots ①-⑧ 全链) |
| @photo/worker | 2 / 14 | 2 / 13 |
| @photo/sync | –(passWithNoTests) | 4 / 27 |
| @photo/viewer | vitest 19 / 136(P4a 17/114 → +2 文件 +22:embed-feed 18 + error-coverage 3 + download 新用例) | playwright 8 spec / 32(+1 spec/+8:embed.spec 宿主全链含 lang=en) |
| @photo/admin | vitest 14 / 69(P4a 12/57 → +2 文件 +12:embedSlots 9 + error 3) | playwright 6 spec / 20(+2 spec/+5:embed-slots 4 + locale-en 1) |
| **web 合计** | **48 / 316**(jest 14 suites / 100 + vitest 34 files / 216) | **43 / 339**(jest 29 suites / 287 + playwright 14 spec / 52) |
- 附加门:lint 10/10、typecheck 13/13、test 12/12、build 10/10 包任务全绿(turbo)。
**移动端**(P4b 仅补镜像常量,语义零漂移)
| 套件 | 结果 |
|------|------|
| `flutter analyze` | **No issues found!**(0 issue,9.7s) |
| `flutter test` | **280 / 280 全过**(`00:14 +280: All tests passed!`) |
**总量**:web 316 + 339 = 655(P4a 出口 589 → **+66**:单测 +45、e2e +21);mobile flutter 280(零漂移)。本次回归门合计 **935**。