docs: p1b acceptance record

- P1b-验收记录.md: A08 (viewer.spec + journey), A12 partial (viewer
  i18n switch/fallback + admin template i18n; data-lang is P4b), R09
  (assets.e2e + admin upload + viewer render + journey banner), P1a
  final-review 4 items registered (47fb81a, absent from P1a record),
  9-row deviation table (ports 5175/5176 + cms conflict disclosure,
  staff-wide account lookup, art-design-pro v3.0.2 pin + lint ignore,
  no thumbnails in AdminPhoto, session-scoped banner preview,
  errorCode/pending_upload omissions, useCursorList, esbuild
  onlyBuiltDependencies), P2 handover, per-package test stats
- master roadmap: P1 fully complete (P1a + P1b), tag p1; G1 formally
  registered as skipped (no OSS credentials provided)
- P1b plan: all task checkboxes marked with completion notes

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-18 12:56:59 +08:00
parent 40f8f37ca4
commit b5145d6860
3 changed files with 119 additions and 36 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ Schema 所有权:国内库表(users、albums、photos、upload_sessions、ou
|---|---|---|---|---|---|
| P1 底座 + 国内核心链路 | compose 一键起环境;密码登录→建相册→上传会话→核验→处理→自动发布→观众页瀑布流/大图/短期签名 URL 刷新(读 cn 公开 API);可见性状态机与单张隐藏/软删除;seed 与脚本化 E2E;admin 最小页(后段) | T02 T05(后端部分)T06 T07,T11 部分 | A01(API 与后台级,App 端随 P3)A02 A03 A06 A13(凭证设计级,OSS 生命周期全量核验随 G1/P5);A07/A08 基础级 | 回归全绿 + 验收记录;可选 G1 | G1(可选) |
> **进度:P1a(后端链路分片)已于 2026-09-18 完成**,tag `p1a`,验收记录见 docs/plans/progress/P1a-验收记录.md(195 测试全绿,A01-A03/A06/A07/A13 证据落档)。P1b(viewer + admin 最小页 + 宣传图上传链路 + 管理照片列表 + 账号查询接口)计划待编。
> **进度:P1 整体完成(P1a + P1b),2026-09-18,tag `p1`**。P1a 验收记录见 docs/plans/progress/P1a-验收记录.md(195 测试全绿,A01-A03/A06/A07/A13 证据落档);P1b 验收记录见 docs/plans/progress/P1b-验收记录.md(A08 基础级 / A12 部分 / R09 / P1a 终审修缮补登,全仓 324 测试绿,含跨端 Playwright 联调冒烟)。**G1(可选关口)正式登记跳过**:真实 OSS 凭证未提供,按出口约定「无凭证显式跳过并登记」处理(本地 MinIO 已覆盖 presign/head/禁止覆盖/受控复制全语义,凭证到位后可补跑)。
| P2 双地域仿真 + 互动 | ov-app + outbox/inbox 同步(版本、墓碑、高优先级下线、补漏对账);点赞批次幂等;WebSocket 气泡(自身批次过滤、后台暂停);浏览统计窗口去重;统计快照回传;观众页切换至 ov-api 并新增最新/热门排序 | T25(仿真部分)T26 T13 T19 T27 T24,T11 完成 | A09 A10 A14 A21 A26 A27(仿真级) | 回归全绿 + 验收记录;D07/D11 初值确认 | 无 |
| P3 Flutter App | 登录、相册配置、协作、持久化上传队列(准备阶段状态机、账号隔离、跨设备服务端历史)、连拍入队、原生后台传输桥接 | T03 T08 T09 T21 | A04 A05 A15 A19(代码 + 模拟器级) | 回归全绿 + 验收记录 | G2(强制)、G3(资源) |
| P4a 后台完备 + 账号治理 | Art Design Pro 全功能后台:相册/照片/用量管理与告警、上传公开开关、批量隐藏、负责人交接、水印版本与历史重生成、下载两级控制、后台语言切换与账号管理(激活停用/重置密码/角色)、用户定期同步任务与管理页(mock 源) | T10 T20 T22 T12 T30 T04(mock 部分) | A16 A17 A18 A20 A23 A29(mock 级) | 回归全绿 + 验收记录;D06 前置 | G4(真实接口) |
@@ -23,93 +23,93 @@
- Create: `services/api/src/shared/pagination.ts`(parseCursor/parseLimit 从 albums.controller.ts 迁入;members/uploads/public 改引用;删除原处导出)
- Modify: `infra/docker-compose.yml`(宿主绑定改 `127.0.0.1:3306:3306` 等四项——注意 Windows 下 Docker Desktop 需重建容器 `docker compose up -d --force-recreate`,数据卷保留;同步把 `.env.example` 各 localhost 改 127.0.0.1 防 ::1 解析边缘)
- [ ] Step 1: 失败测试先行(404 未知路由 → code NOT_FOUND;响应无 x-powered-by)
- [ ] Step 2: 实现四项 + 契约 gen + 全量回归绿(api e2e 120 基线不降)
- [ ] Step 3: Commit `chore(api): p1a final-review hygiene items`
- [x] Step 1: 失败测试先行(404 未知路由 → code NOT_FOUND;响应无 x-powered-by)
- [x] Step 2: 实现四项 + 契约 gen + 全量回归绿(api e2e 120 基线不降)
- [x] Step 3: Commit `chore(api): p1a final-review hygiene items`
## Task 2: 契约扩展 —— assets 上传 / 管理照片列表 / 账号查询
**Files:**
- Modify: `packages/contracts/openapi.yaml`(+gen 提交)
- [ ] Step 1: 新增路径(字段以技术方案 §4 album_assets 与既定模式为准):
- [x] Step 1: 新增路径(字段以技术方案 §4 album_assets 与既定模式为准):
- `POST /albums/{albumId}/assets`:{kind: banner|cover|logo, locale?, filename, bytes, contentType} → 201 {assetId, objectKey, credential(UploadCredential 同形)};`POST /albums/{albumId}/assets/{assetId}/complete` → 200 {assetId, url(短期签名)}(服务端 head 核验后挂到相册;同 kind+locale 替换语义=新行生效、旧行置空,响应取最新——公开响应已按此取 latest)
- `GET /albums/{albumId}/photos`:管理列表(query: visibility?、categoryId?、cursor、limit)→ PhotoRecord 分页(含未发布/隐藏/删除外全部状态;上传人信息 account/displayName)
- `GET /admin/users?accountPrefix=&limit=`:admin/owner 可用,按 account 前缀(≥2 字符)搜索激活用户,返回 {id, account, displayName} 脱敏列表(邀请协作者前置;不返回邮箱手机)
- 语义与错误沿用既有码(ALBUM_NOT_FOUND/AUTH_FORBIDDEN/VALIDATION_FAILED/UPLOAD_VERIFY_FAILED)
- [ ] Step 2: `gen` + `lint:api` 绿;⏸ 快速确认点:向用户展示三组新端点要点(可跳过逐字段评审)
- [ ] Step 3: Commit `feat(contracts): p1b asset upload, management photo list, account lookup`
- [x] Step 2: `gen` + `lint:api` 绿;⏸ 快速确认点:向用户展示三组新端点要点(可跳过逐字段评审)
- [x] Step 3: Commit `feat(contracts): p1b asset upload, management photo list, account lookup`
## Task 3: assets 上传链路(后端)
**Files:**
- Create: `services/api/src/albums/assets.service.ts` + controller 挂 AlbumsModule;`test/assets.e2e-spec.ts`
- [ ] Step 1: 失败 e2e:owner 上传 banner(presign PUT 真实 jpeg → complete)→ 201/200;DB album_assets 行 + MinIO 对象;`GET /public/albums/{token}` banner 返回可取回的签名 URL;替换上传(同 kind 新 complete)→ 公开响应取新 URL;非 owner 403;bytes/contentType 校验同 uploads 域;kind=logo locale 维度可选;audit album.asset.add
- [ ] Step 2: 实现(复用 uploads 域的 presign/核验/大小白名单模式;对象键 `photo-live/dev/albums/{albumId}/assets/{assetId}.{ext}`;无幂等 clientTaskId 需求——按 assetId 一次性会话即可)
- [ ] Step 3: 通过 + Commit `feat(api): album asset upload chain`
- [x] Step 1: 失败 e2e:owner 上传 banner(presign PUT 真实 jpeg → complete)→ 201/200;DB album_assets 行 + MinIO 对象;`GET /public/albums/{token}` banner 返回可取回的签名 URL;替换上传(同 kind 新 complete)→ 公开响应取新 URL;非 owner 403;bytes/contentType 校验同 uploads 域;kind=logo locale 维度可选;audit album.asset.add
- [x] Step 2: 实现(复用 uploads 域的 presign/核验/大小白名单模式;对象键 `photo-live/dev/albums/{albumId}/assets/{assetId}.{ext}`;无幂等 clientTaskId 需求——按 assetId 一次性会话即可)
- [x] Step 3: 通过 + Commit `feat(api): album asset upload chain`
## Task 4: 管理照片列表 + 账号查询(后端)
**Files:**
- Modify: `services/api/src/photos/`(新增 list 管理);Create `services/api/src/admin/users-lookup.*`;`test/photos-admin.e2e-spec.ts`
- [ ] Step 1: 失败 e2e:owner/admin 查管理列表(全状态可见、visibility 过滤、分页稳定、uploader 脱敏信息);collaborator → 403;账号查询:admin/owner 前缀搜索(<2 字符 400、不泄露凭据字段、限 20 条);outlier 401/403
- [ ] Step 2: 实现(共享 keyset;账号查询 `WHERE account LIKE prefix%` 走 bin 校对大小写敏感——文档说明)
- [ ] Step 3: 通过 + Commit `feat(api): management photo list and account lookup`
- [x] Step 1: 失败 e2e:owner/admin 查管理列表(全状态可见、visibility 过滤、分页稳定、uploader 脱敏信息);collaborator → 403;账号查询:admin/owner 前缀搜索(<2 字符 400、不泄露凭据字段、限 20 条);outlier 401/403
- [x] Step 2: 实现(共享 keyset;账号查询 `WHERE account LIKE prefix%` 走 bin 校对大小写敏感——文档说明)
- [x] Step 3: 通过 + Commit `feat(api): management photo list and account lookup`(完成:8ba98ac;**用户裁决 2026-09-18**:账号查询维持"全体已认证 staff"策略;dev 端口改为 viewer 5175 / admin 5176——避免与用户 cms 项目 5173 冲突)
## Task 5: viewer 脚手架
**Files:**
- Create: `apps/viewer/`(Vite + Vue3 + TS:package.json @photo/viewer、vite.config(proxy /v1 → :8080 dev)、router(`/a/:token` 活动页、`/a/:token/p/:photoId` 单图定位)、i18n(zh-CN/en 词条文件、localStorage 持久、检测浏览器语言)、api client(fetch 封装 + 契约生成类型 + 错误码→文案映射表)、Pinia 不引入(保持轻量))
- [ ] Step 1: `pnpm create vite` 起最小骨架并裁剪至需求;加入 workspace;lint/typecheck/build/**test(vitest)/test:e2e(Playwright)** 全部接入 turbo(Task 12 回归门依赖)
- [ ] Step 2: 冒烟:dev server 起、路由可达、i18n 切换生效(vitest 组件测试 2-3 个)
- [ ] Step 3: Commit `feat(viewer): vite vue3 scaffold with routing and i18n`
- [x] Step 1: `pnpm create vite` 起最小骨架并裁剪至需求;加入 workspace;lint/typecheck/build/**test(vitest)/test:e2e(Playwright)** 全部接入 turbo(Task 12 回归门依赖)
- [x] Step 2: 冒烟:dev server 起、路由可达、i18n 切换生效(vitest 组件测试 2-3 个)
- [x] Step 3: Commit `feat(viewer): vite vue3 scaffold with routing and i18n`
## Task 6: viewer 活动页(信息区 + 空态 + 时区)
- [ ] Step 1: 组件测试 + 实现:宣传图(banner 签名 URL、无则优雅降级布局)、活动信息(标题/介绍/时间按相册 timezone 以 Intl.DateTimeFormat 展示、地点)、语言切换入口(易发现)、空相册本地化空态("照片即将上线")、viewCount 占位(null 不渲染)
- [ ] Step 2: Playwright 前置:`apps/viewer/e2e/`(配置 + fixtures:seed 脚本起真实后端或 mock —— 决策:用真实后端 + Playwright,compose 依赖;fixture 走 API seed)
- [ ] Step 3: Commit `feat(viewer): album page with locale fallback and timezone display`
- [x] Step 1: 组件测试 + 实现:宣传图(banner 签名 URL、无则优雅降级布局)、活动信息(标题/介绍/时间按相册 timezone 以 Intl.DateTimeFormat 展示、地点)、语言切换入口(易发现)、空相册本地化空态("照片即将上线")、viewCount 占位(null 不渲染)
- [x] Step 2: Playwright 前置:`apps/viewer/e2e/`(配置 + fixtures:seed 脚本起真实后端或 mock —— 决策:用真实后端 + Playwright,compose 依赖;fixture 走 API seed)
- [x] Step 3: Commit `feat(viewer): album page with locale fallback and timezone display`
## Task 7: viewer 瀑布流
- [ ] Step 1: 组件/交互实现:双列瀑布流(CSS columns 或绝对定位两列——选绝对定位方案以支持滚动位置恢复;条目高按 width/height 预留)、IntersectionObserver 懒加载图片(签名 URL 加载失败重试一次=刷新签名)、分类筛选 chips、游标分页无限滚动(nextCursor 耗尽停)、新照片轮询提示(30s 间隔 polling 最新页,有新图显示顶部提示条"新照片"点击刷新——不自动插入避免跳动,需求 1.2 建议基线)
- [ ] Step 2: Playwright:滚动加载两页、分类切换、(轮询提示用注入延时验证)
- [ ] Step 3: Commit `feat(viewer): waterfall feed with lazy load, filter and new-photo hint`
- [x] Step 1: 组件/交互实现:双列瀑布流(CSS columns 或绝对定位两列——选绝对定位方案以支持滚动位置恢复;条目高按 width/height 预留)、IntersectionObserver 懒加载图片(签名 URL 加载失败重试一次=刷新签名)、分类筛选 chips、游标分页无限滚动(nextCursor 耗尽停)、新照片轮询提示(30s 间隔 polling 最新页,有新图显示顶部提示条"新照片"点击刷新——不自动插入避免跳动,需求 1.2 建议基线)
- [x] Step 2: Playwright:滚动加载两页、分类切换、(轮询提示用注入延时验证)
- [x] Step 3: Commit `feat(viewer): waterfall feed with lazy load, filter and new-photo hint`
## Task 8: viewer 大图查看器
- [ ] Step 1: 实现:点击开大图(browse URL)、左右切换(保持列表浏览序列)、双指/滚轮缩放(CSS transform 受控)、关闭回原列表位置(滚动锚点)、单图定位路由(`/a/:token/p/:photoId`:先加载该照片再拉相邻页)、404 照片本地化提示、签名过期→静默重取单图接口刷新
- [ ] Step 2: Playwright:开-关回位、切换序列、深链直达
- [ ] Step 3: Commit `feat(viewer): photo viewer with zoom, navigation and deep link`
- [x] Step 1: 实现:点击开大图(browse URL)、左右切换(保持列表浏览序列)、双指/滚轮缩放(CSS transform 受控)、关闭回原列表位置(滚动锚点)、单图定位路由(`/a/:token/p/:photoId`:先加载该照片再拉相邻页)、404 照片本地化提示、签名过期→静默重取单图接口刷新
- [x] Step 2: Playwright:开-关回位、切换序列、深链直达
- [x] Step 3: Commit `feat(viewer): photo viewer with zoom, navigation and deep link`
## Task 9: admin 引入 Art Design Pro
**Files:**
- Create: `apps/admin/`(模板引入——git clone 指定版本或下载 release zip;锁定版本记录在 package.json + README 注明来源与裁剪说明)
- [ ] Step 1: 引入并裁剪(保留布局/登录/权限框架与依赖最小集;移除 demo 页;name @photo/admin 入 workspace;lint/typecheck/build 接 turbo——模板自带 eslint 需对齐根配置或豁免说明)
- [ ] Step 2: 登录对接:模板登录页改调 `POST /v1/auth/password`(sessionToken 内存+sessionStorage、401 统一跳登录、refresh 定时静默续期);dev proxy /v1 → 8080
- [ ] Step 3: 冒烟(手动 + vitest 冒烟);Commit `feat(admin): art-design-pro integration with local session auth`
- [x] Step 1: 引入并裁剪(保留布局/登录/权限框架与依赖最小集;移除 demo 页;name @photo/admin 入 workspace;lint/typecheck/build 接 turbo——模板自带 eslint 需对齐根配置或豁免说明)
- [x] Step 2: 登录对接:模板登录页改调 `POST /v1/auth/password`(sessionToken 内存+sessionStorage、401 统一跳登录、refresh 定时静默续期);dev proxy /v1 → 8080
- [x] Step 3: 冒烟(手动 + vitest 冒烟);Commit `feat(admin): art-design-pro integration with local session auth`
## Task 10: admin 相册与成员页
- [ ] Step 1: 实现:相册列表(游标分页、开关状态展示)、创建(多语言表单 zh-CN/en、校验同契约)、编辑(开关独立、translations 全量替换语义提示);相册详情含成员管理(账号前缀搜索邀请、移除、角色展示)与分类管理(增改删)——用 Task 2 的账号查询与管理列表接口
- [ ] Step 2: Playwright(admin 流:登录→建相册→邀成员→传 banner→管理列表可见照片状态)可与 Task 11 合并冒烟
- [ ] Step 3: Commit `feat(admin): album management pages with members and categories`
- [x] Step 1: 实现:相册列表(游标分页、开关状态展示)、创建(多语言表单 zh-CN/en、校验同契约)、编辑(开关独立、translations 全量替换语义提示);相册详情含成员管理(账号前缀搜索邀请、移除、角色展示)与分类管理(增改删)——用 Task 2 的账号查询与管理列表接口
- [x] Step 2: Playwright(admin 流:登录→建相册→邀成员→传 banner→管理列表可见照片状态)可与 Task 11 合并冒烟
- [x] Step 3: Commit `feat(admin): album management pages with members and categories`
## Task 11: admin 上传与 viewer 联调冒烟(Playwright 全流程)
- [ ] Step 1: Playwright 完整剧本(真实后端):admin 登录→建相册(zh+en)→传 banner→建分类→邀 staffB;staffB(API 或 UI 若简单)上传 3 张真图→worker 处理(compose 内起 dev:worker 或测试内直调);viewer:开活动页(en)→瀑布流 3 张→分类过滤→大图切换→深链;admin 隐藏 1 张→viewer 列表变 2;空相册态冒烟另起一例
- [ ] Step 2: 全绿 + Commit `test(e2e): p1b cross-frontend playwright journey`
- [x] Step 1: Playwright 完整剧本(真实后端):admin 登录→建相册(zh+en)→传 banner→建分类→邀 staffB;staffB(API 或 UI 若简单)上传 3 张真图→worker 处理(compose 内起 dev:worker 或测试内直调);viewer:开活动页(en)→瀑布流 3 张→分类过滤→大图切换→深链;admin 隐藏 1 张→viewer 列表变 2;空相册态冒烟另起一例(完成:40f8f37——`apps/viewer/e2e/journey.spec.ts`。staffB 走 API 上传、worker 测试内直调 PublishService 发布事务(BullMQ 队列消费路径由 api full-chain e2e 覆盖);viewer 以 zh 打开后切 en 断言内容随切再回切;admin dev 以 viewer playwright 第三个 webServer 拉起(5176 复用);空相册态并入同一剧本收尾)
- [x] Step 2: 全绿 + Commit `test(e2e): p1b cross-frontend playwright journey`(完成:40f8f37)
## Task 12: P1b 回归门 + P1 阶段验收记录
- [ ] Step 1: 全量回归 `pnpm lint && pnpm typecheck && pnpm test && pnpm e2e && pnpm build` 全绿(含新增 viewer/admin 任务)
- [ ] Step 2: 写 `docs/plans/progress/P1b-验收记录.md`(A08 基础级、A12 部分、R09、**P1a 终审修缮 4 项的落实登记**——P1a 记录偏差表未含、此处补登、偏差登记、P2 移交:outbox 消费/限速/热门排序);更新总纲 P1 行进度注记(P1 整体完成、G1 可选关口状态)
- [ ] Step 3: `git tag p1`;Commit `docs: p1b acceptance record`
- [x] Step 1: 全量回归 `pnpm lint && pnpm typecheck && pnpm test && pnpm e2e && pnpm build` 全绿(含新增 viewer/admin 任务)(完成:lint 7/7、typecheck 10/10、build 7/7;单测 148 + e2e 176 = 324)
- [x] Step 2: 写 `docs/plans/progress/P1b-验收记录.md`(A08 基础级、A12 部分、R09、**P1a 终审修缮 4 项的落实登记**——P1a 记录偏差表未含、此处补登、偏差登记、P2 移交:outbox 消费/限速/热门排序);更新总纲 P1 行进度注记(P1 整体完成、G1 可选关口状态)(完成:G1 凭证未提供,正式登记跳过)
- [x] Step 3: `git tag p1`;Commit `docs: p1b acceptance record`(完成:tag 打在本记录提交)
---
+83
View File
@@ -0,0 +1,83 @@
# P1b 验收记录
- **阶段**:P1b 观众页与管理后台(计划 `docs/plans/2026-09-18-P1b-观众页与管理后台.md`,Task 1–12)
- **日期**:2026-09-18
- **基线 commits**:`62382ec..HEAD`(P1b 计划提交之后至跨端冒烟 `40f8f37`,共 12 个实施提交,不含本记录提交)
- **执行模式**:子代理驱动 + 双审(每任务先写失败测试→实现→通过→提交;跨任务由子代理接力,主代理与复核代理双重审查)
- **回归门**:`pnpm lint && pnpm typecheck && pnpm test && pnpm e2e && pnpm build` 全绿(数字见文末统计表;viewer/admin 的 vitest + playwright 任务全部入列)
- **基础设施**:photo-live MySQL/Redis/MinIO compose(`infra/`,端口绑定 127.0.0.1),前端 e2e 打真实 API + 真实 MinIO 对象(种子走 Prisma/storage 直种或真实 UI/API 链路)
---
## A08 大图操作(基础级,Web 端)
- 证据:`apps/viewer/e2e/viewer.spec.ts` **4 用例**全绿——点击开大图(browse 经签名 URL 取回真实字节,naturalWidth > 0)、左右切换保持列表序列且 URL 同步 replace(深链可分享)、滚轮缩放受控 CSS transform + 双击复位、**关闭精确回到原滚动位置**、深链直达目标照片(背后列表构成完整序列)、404/已下线照片深链本地化提示 + 可用返回链接、越过末页边界自动拉次页扩展序列(31/32)。
- 单测佐证:`apps/viewer/src/__tests__/`(lightbox / viewer-route / feed 等,vitest 65 用例的一部分——切换序列、URL 语义、滚动回位、缩放边界)。
- 跨端佐证:`apps/viewer/e2e/journey.spec.ts`(Task 11)开图→缩放→关闭→深链→被隐照片深链 404 提示全链走真实数据。
- App 端滑动手势/性能级验收随 P3(A08 移动级)。
## A12 多语言(部分:viewer + admin)
- viewer:`apps/viewer/e2e/album.spec.ts`——语言切换按钮 zh↔en **内容随切**(标题/描述/场地/时间格式/空态/页面 title)、**en UI + zh-only 相册内容回退 zh**(服务端 resolvedLocale=zh-CN,UI chrome 保持用户所选语言)、chips/错误/空态文案双语;词条完整性有 vitest(i18n.spec.ts 等)。瀑布流/大图/深链提示文案随 locale 全覆盖(feed.spec / viewer.spec 断言中文文案)。
- admin:art-design-pro 模板 i18n(zh/en)沿用,业务词条(albumsManage.*/topBar.*/common.* 等)入 zh/en 语言包;登录/相册/照片管理 e2e 的文案断言均走真实渲染(占位符、按钮、状态标签、确认框)。
- **组件 data-lang 语言配置属 P4b**(embed-loader);A12 全端语言验收(四端切换/回退/漏译)随 P4b T15。
## R09 宣传图(顶部 banner)
- API 级:`services/api/test/assets.e2e-spec.ts` **14 用例**——banner presign init→PUT→complete 上传链(复用照片上传机制)、生效态/替换语义、locale 资产解析、公开端点签名 URL 下发。
- admin UI:`apps/admin/e2e/albums.spec.ts`——真实 jpeg 走 file chooser → init/PUT/complete → 状态与预览。
- viewer UI:`apps/viewer/e2e/album.spec.ts`——banner 签名 URL 取回真实字节渲染(naturalWidth > 0);无 banner 相册不渲染图。
- 跨端闭环:`journey.spec.ts`——admin UI 上传的 banner 在 viewer 活动页真实渲染。
## P1a 终审修缮 4 项落实登记(Task 1,commit `47fb81a`)
> P1a 验收记录(`8a09001`)偏差表未含下列各项——终审发生于该记录落档之后,按计划 Task 1 在此补登:
1. **框架级 404 错误码**:contracts 新增 `NOT_FOUND`;AllExceptionsFilter 未知路由默认映射由 `ALBUM_NOT_FOUND` 改为 `NOT_FOUND`(业务 404 仍走显式 ApiException 专用码,防枚举语义不变)。
2. **configureApp 抽取**:prefix/CORS/x-powered-by 统一配置由 main-cn 与 e2e 共享;禁用 `x-powered-by` 并有 e2e 断言。
3. **分页参数抽取**:parseCursor/parseLimit(含页大小边界)从 albums.controller 移入 `shared/pagination`,members/uploads/public 一并复用。
4. **compose 端口回环绑定**:MySQL/Redis/MinIO 发布端口绑定 127.0.0.1(不暴露局域网),`.env.example` 端点同步。
- 随附 e2e:unknown-route 404 `NOT_FOUND` + `x-powered-by` 缺席两例(api e2e 120 → 122)。
---
## 偏差与发现登记
| # | 事项 | 处理/结论 |
|---|------|-----------|
| 1 | dev 端口 5175/5176 | **用户裁决 2026-09-18**:viewer 5175 / admin 5176(原计划 5173/5174)。事由:开发机 5173 已被用户另一 cms 项目占用,首次起 viewer dev 时发生端口冲突;裁决固定 5175/5176 并同步 CORS_ORIGINS(commit `04b5641`,事故在此披露) |
| 2 | 账号查询访问策略 | **用户裁决 2026-09-18**:契约原描述 admin/owner 可查,实作放宽为**全体已认证 staff**(邀请发起方不限创建者;泄露面仅激活账号的最小展示字段 id/account/displayName,接收方为已认证内部人员) |
| 3 | Art Design Pro v3.0.2 锁定 | 模板来源仓库/tag/commit 与裁剪清单登记于 `apps/admin/SOURCE.md` + package.json `//` 字段;模板自带 eslint9 flat 配置与根配置规则集不同——**根 ESLint ignore `apps/admin/**`**(偏差注册,不强改模板);模板版本锁定,升级需重新评估 |
| 4 | AdminPhoto 无缩略图 | 管理照片列表条目不含缩略图(不为列表逐条签 URL,控制签名量/带宽);P4a 后台完备时再评估 |
| 5 | banner 预览会话级 | admin banner 上传成功后的预览图走当次会话内存 URL,不落库回显(刷新即失,生效事实以 album_assets 行为准);e2e 在同一页面会话内断言预览 |
| 6 | errorCode 不在 PhotoRecord | 管理列表契约 PhotoRecord 未含 errorCode(处理失败详情不进列表;失败原因排查走单图/日志),P4a 完善 |
| 7 | pending_upload 不在管理列表 | listAlbumPhotos 不返回 pending_upload(未 complete 的会话草稿不算已提交照片),管理页只列已提交域 |
| 8 | useCursorList 替代 useTable | admin 列表页自建游标分页 composable(对齐后端 keyset 契约),不用模板 useTable 的偏移分页 |
| 9 | esbuild onlyBuiltDependencies | pnpm 10 默认拦截依赖构建脚本,根 package.json 白名单放行 esbuild(Vite 平台二进制下载),登记偏差 |
## 未尽事项 / 移交
- **P2**:outbox 已落库仍**无消费者**(按 eventId 去重、entityVersion 单调消费);**公开端点无限速**;观众页**最新/热门排序**(契约与 UI 随 P2 扩展,当前仅 publishedAt 序);**浏览统计**(窗口去重)。
- **P3**:UI 上传链路(App/后台界面上传;P1b staffB 上传走 API——journey 即按此验收)。
- **P4a**:AdminPhoto 缩略图、批量隐藏、账号治理、后台语言切换完整验收;banner 预览持久回显。
- **P4b**:传播组件 data-lang(A12 全端验收)。
- **G1**:真实 OSS 凭证冒烟——**凭证未提供,按总纲 P1 出口约定「无凭证显式跳过并登记」正式登记为跳过**(本地 MinIO 已覆盖 presign/head/禁止覆盖/受控复制全语义)。
---
## 测试统计汇总(2026-09-18 回归门实测)
| 包 | 单测 suites(files)/tests | e2e suites(tests) |
|----|--------------------------|-------------------|
| @photo/contracts | 2 / 8 | – |
| @photo/db-cn | –(schema 校验随 migrate/生成流程) | – |
| @photo/storage | 1 / 6(真实 MinIO 集成) | – |
| @photo/api-cn | 6 / 46 | **12 / 148**(auth 14、albums 20、members-categories 15、uploads 20、photos 14、public 17、**assets 14、photos-admin 6、users-lookup 6**、health 6、db.semantics 6、full-chain 10) |
| @photo/worker | 1 / 8 | 1 / 7(publish) |
| @photo/viewer | vitest **10 / 65** | playwright **5 个 spec / 14 用例**(album 5、feed 3、viewer 4、smoke 1、**journey 1**) |
| @photo/admin | vitest **4 / 15** | playwright **2 个 spec / 7 用例**(login 5、albums 2) |
| **合计** | **24 / 148** | **20 / 176**(jest 13 suites / 155 用例 + playwright 7 个 spec / 21 用例) |
- 附加门:lint 7/7 任务、typecheck 10/10 任务、build 7/7 任务全绿(viewer 产物 166 KB js / admin 产物含 element-plus 按需分包)。
- 总测试数(单测 + e2e):**324**。
- 跨端联调(Task 11):viewer playwright 配置以第三个 webServer 拉起 admin dev(5176,复用已在跑实例),`journey.spec.ts` 单剧本贯通 admin UI → staffB API → worker 发布事务 → viewer UI → admin 隐藏 → viewer 收敛 → 空态,22–35s 稳定通过。