docs(plan): p1b implementation plan (approved with advisory fixes)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-18 08:05:06 +08:00
parent 43de31ffda
commit 62382eca3c
@@ -0,0 +1,123 @@
# P1b 观众页与管理后台 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在 P1a 后端核心链路之上交付:观众页(Vue3 轻量入口:活动信息/瀑布流/大图/多语言/分类/新图轮询)、管理后台最小页(Art Design Pro:登录/相册/成员)、宣传图上传链路、管理端照片列表与账号查询接口、Playwright 冒烟——完成总纲 P1 阶段整体出口。
**Architecture:** apps/viewer(Vue 3 + Vite + TS,独立轻量入口,读 cn 公开 API)+ apps/admin(Art Design Pro 模板裁剪,读管理 API)+ services/api 增量(assets 上传/管理照片列表/账号查询)。后端继续契约先行:本计划先扩 openapi.yaml 再实现。对齐《实施总纲》P1 明细与 P1a 验收记录移交清单。
**Tech Stack:** 沿用 P1a(NestJS/Prisma/MySQL/StoragePort-MinIO;Node ≥22.12——.nvmrc 22 / engines 对齐,开发机实际 24)+ Vue 3.4 · Vite 5 · TypeScript · vue-i18n · vue-router · Pinia(viewer 轻量,Pinia 仅 admin 用)· Art Design Pro(锁定版本,执行时登记仓库地址与精确版本)· Playwright(**devDep 置于仓库根**,viewer/admin e2e 共用)
**对应验收:** A08(大图操作基础级)、A12 部分(语言切换)、R09(宣传图)、P1a 终审修缮项。A11/A14 等随 P2。
**依赖基线:** tag `p1a`(8a09001+43de31f),compose 基础设施运行中。
---
## Task 1: P1a 终审修缮(4 项)
**Files:**
- Modify: `packages/contracts/src/errors.ts` + `openapi.yaml`(新增 `NOT_FOUND` 错误码:框架级未知路由/资源;Error enum + gen)
- Modify: `services/api/src/all-exceptions.filter.ts`(404 默认映射改 `NOT_FOUND`;业务 404 全部经 ApiException 显式携带原码——回归现有 e2e)
- Modify: `services/api/src/main-cn.ts`(`app.disable('x-powered-by')` 或等价 helmet 配置;e2e 断言头消失)
- 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`
## Task 2: 契约扩展 —— assets 上传 / 管理照片列表 / 账号查询
**Files:**
- Modify: `packages/contracts/openapi.yaml`(+gen 提交)
- [ ] 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`
## 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`
## 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`
## 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`
## 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`
## 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`
## 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`
## 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`
## 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`
## 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`
## 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`
---
## 执行注意
- 沿用 P1a 全部约定:TDD 节奏、根目录执行、.env 唯一、新 env 同步 example、契约先行(改 openapi 必 gen+提交)、e2e reset 隔离、turbo concurrency=1、jest maxWorkers=1。
- viewer/admin 的 node 工具链同一 Node 24;Vite dev 端口避开 8080(建议 5173 viewer / 5174 admin,CORS_ORIGINS 已含 5173,admin 端口需补入 env)。
- Playwright 首次安装浏览器走 `pnpm exec playwright install chromium`(仅 chromium,控制体积)。
- Art Design Pro 若与根 ESLint 9 冲突:admin 包内保留模板自带 lint 配置并在根配置 ignore 该包(登记偏差),不强改模板;**执行时在 apps/admin/README 或 package.json 登记模板来源仓库与锁定版本**。
- pnpm 10 默认拦截依赖构建脚本:引入 Vite/esbuild/admin 模板依赖时若遇到二进制缺失,向根 package.json 增补 `pnpm.onlyBuiltDependencies`(esbuild 等)后重装,登记偏差。
- UI 实现遵循 frontend-design 直觉但不做过度视觉打磨——P1b 是功能最小页,视觉完善随 P4a。