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

16 KiB
Raw Blame History

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。