docs: p4a acceptance record

P4a Task 10 收口:回归门全绿(web lint/typecheck/build 9/9 包任务,
单测 271 + e2e 318 = 589;mobile analyze 0 issue + 280/280 零漂移)。
验收记录含 A16/A17/A18/A20/A23/A29 证据、D06 工作默认登记、契约
增补汇总(36→51 operations + DOWNLOAD_DISABLED)、偏差 14 项、
G4 待真实接口清单(UserSourcePort 替换点 + D08 字段确认)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-20 21:21:08 +08:00
parent f08d68bebf
commit 17d09bbb01
3 changed files with 285 additions and 38 deletions
@@ -1,5 +1,7 @@
# P4a 后台完备 + 账号治理 实施计划
> **✅ 阶段已完成(2026-09-20,tag `p4a`,基线 45d4651..HEAD)。** 完成状态的权威记录是 [docs/plans/progress/P4a-验收记录.md](./progress/P4a-验收记录.md);Task 1-10 全部交付、勾选已回填。偏差(用量实时 SUM、SVG 文字水印 + G5 字体、mock 源 department 忽略、下载 window.open 策略、/admin/users 双模式等 14 项)与 G4 待真实接口清单(UserSourcePort 替换点 + D08 字段确认)见验收记录。
> **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:** 管理后台达到运营完备:照片批量管理(逐项结果)、负责人交接、水印版本化与历史重生成、下载两级控制(含 viewer 下载 UI 与微信引导)、用量统计与阈值告警、账号管理(激活/停用/重置密码/角色)、用户定期同步任务(mock 源先行)与同步管理页。
@@ -20,7 +22,7 @@
**Files:** Modify `packages/contracts/openapi.yaml`(+gen)
- [ ] 新增路径(遵循既有风格):
- [x] 新增路径(遵循既有风格):
- `POST /albums/{albumId}/photos/hide-batch`:{photoIds: string[] (1..200)} → 200 {results: [{photoId, ok, code?}]}(逐项结果,A16);403/404 同管理列表
- `POST /admin/albums/{albumId}/transfer`:{targetUserId, keepAsCollaborator: boolean} → 200 {album}(A17:owner 转移、历史上传人不变)
- `POST /albums/{albumId}/watermark-jobs`:{config: {enabled, position?, opacity?, scalePercent?, text?{content?, fontSize?}}} → 202 {jobId, configVersion};`GET /albums/{albumId}/watermark-jobs` → {current: {configVersion, config}, jobs: [{jobId, configVersion, state, progress, total, failed, errorCode?, createdAt}]}(A18/A20)
@@ -28,70 +30,70 @@
- 账号管理(tag admin):`GET /admin/users?query=&status=&cursor=`(管理列表:id/account/displayName/role/sourceStatus/localLocked/credentialStatus/locale/createdAt);`PATCH /admin/users/{id}` {role?, localLocked?, displayName?};`POST /admin/users/{id}/reset-password` {newPassword} → 200 {initialPassword?}(管理员重置:直接设新密码返回给管理员,或生成随机——决策:body 可选 newPassword,缺省服务端生成随机返回,e2e 两种都测);`POST /admin/users/{id}/activate` / `deactivate`(deactivate=localLocked=true+撤销该用户全部会话)
- 用户同步管理(tag admin):`GET /admin/user-sync/status` → {lastRunAt, lastSuccessAt, state, sourceVersion, counts: {added,updated,disabled,failed}};`GET /admin/user-sync/runs?cursor=`(历史批次);`GET /admin/user-sync/errors?runId=`(失败明细);`POST /admin/user-sync/runs`(手动补拉触发 → 202 {runId})
- 用量(tag admin):`GET /admin/usage/summary` → {albums: [{albumId, title, photoCount, originalBytes, variantBytes, totalBytes}], totals, thresholds: {storageBytes?, perAlbumBytes?}, alerts: [{id, kind, albumId?, message, createdAt, resolvedAt?}]};`PUT /admin/usage/thresholds` {storageBytes?, perAlbumBytes?}(告警接收人 P5 通知渠道,本阶段落库+列表,A23 后台侧)
- [ ] gen + lint + 全回归绿;⏸ 快速确认点(要点摘要给用户)
- [ ] Commit `feat(contracts): p4a admin governance, watermark, download and usage endpoints`
- [x] gen + lint + 全回归绿;⏸ 快速确认点(要点摘要给用户)
- [x] Commit `feat(contracts): p4a admin governance, watermark, download and usage endpoints`
## Task 2: 批量隐藏 + 负责人交接(后端)
- [ ] e2e(先行):批量隐藏 3 张(2 成功 1 已删→ok:false code PHOTO_NOT_PUBLIC)逐项结果+audit(photo.hide×2);outbox photo.hidden×2 高优先级;ov 端同步不可见(复用 sync 测试基建)。交接:admin 转移给 staffB、keepAsCollaborator=true/false 两例→owner 变更+成员角色正确+历史上传人不变(photos.uploaderId 不动)+audit album.transfer;非 admin 403;目标无效账号 400
- [ ] 实现(album 域事务 + PermissionsService:仅 admin 可交接)
- [ ] Commit `feat(api): batch hide with per-item results and owner transfer`
- [x] e2e(先行):批量隐藏 3 张(2 成功 1 已删→ok:false code PHOTO_NOT_PUBLIC)逐项结果+audit(photo.hide×2);outbox photo.hidden×2 高优先级;ov 端同步不可见(复用 sync 测试基建)。交接:admin 转移给 staffB、keepAsCollaborator=true/false 两例→owner 变更+成员角色正确+历史上传人不变(photos.uploaderId 不动)+audit album.transfer;非 admin 403;目标无效账号 400
- [x] 实现(album 域事务 + PermissionsService:仅 admin 可交接)
- [x] Commit `feat(api): batch hide with per-item results and owner transfer`
## Task 3: 水印版本化 + 历史重生成(后端+worker)
- [ ] cn 迁移(校对手决):albums 已有 watermarkVersion;新增 `watermark_configs`(albumId+configVersion 唯一, config Json, createdAt)与 `watermark_jobs`(jobId, albumId, configVersion, state pending/running/done/failed, progress/total/failed 计数, errorCode, timestamps)
- [ ] worker 新消费者(QUEUE_WATERMARK,cn Redis):逐张:读原图→按 config 合成新水印图(sharp composite:position/opacity/scale/text 基本实现,文字水印用 SVG render——sharp 无原生 text,登记实现方式)→ 写 variants 新键 `{version}/watermark.jpg`→upsert photo_variants(variant=watermark, version=新)→**原子**:全部成功才切换(job 级聚合,失败照片保留旧浏览图,A20)→ 完成时 outbox album.updated(新 watermarkVersion)→ 失败可重试(job 重入按 photo 粒度幂等)
- [ ] 设置 API:POST watermark-jobs(创建 config 版本+job 入队,仅创建者/admin);GET 状态
- [ ] e2e:开启水印→传 1 新照(发布图含水印变体)→发起历史重生成(3 张)→进度推进→done→GET 列表照片 browse URL 指向水印版(bytes 变化/内容校验)→原图 objectKey 不变(A20 核心);中途 1 张注入失败→job failed→重试→done;关闭水印→新照片无水印
- [ ] Commit `feat(worker): watermark versioning with atomic history regeneration`
- [x] cn 迁移(校对手决):albums 已有 watermarkVersion;新增 `watermark_configs`(albumId+configVersion 唯一, config Json, createdAt)与 `watermark_jobs`(jobId, albumId, configVersion, state pending/running/done/failed, progress/total/failed 计数, errorCode, timestamps)
- [x] worker 新消费者(QUEUE_WATERMARK,cn Redis):逐张:读原图→按 config 合成新水印图(sharp composite:position/opacity/scale/text 基本实现,文字水印用 SVG render——sharp 无原生 text,登记实现方式)→ 写 variants 新键 `{version}/watermark.jpg`→upsert photo_variants(variant=watermark, version=新)→**原子**:全部成功才切换(job 级聚合,失败照片保留旧浏览图,A20)→ 完成时 outbox album.updated(新 watermarkVersion)→ 失败可重试(job 重入按 photo 粒度幂等)
- [x] 设置 API:POST watermark-jobs(创建 config 版本+job 入队,仅创建者/admin);GET 状态
- [x] e2e:开启水印→传 1 新照(发布图含水印变体)→发起历史重生成(3 张)→进度推进→done→GET 列表照片 browse URL 指向水印版(bytes 变化/内容校验)→原图 objectKey 不变(A20 核心);中途 1 张注入失败→job failed→重试→done;关闭水印→新照片无水印
- [x] Commit `feat(worker): watermark versioning with atomic history regeneration`
## Task 4: 下载端点 + viewer 下载 UI(T12 收口)
- [ ] 后端:GET download(ov 侧:读 photos_ov + albums_ov 开关副本;签名 URL 短期;variant 校验:original 需 originalDownloadEnabled,browse 需 downloadEnabled;关闭→403 DOWNLOAD_DISABLED;日志不记完整签名)——注意此端点在 ov 域(观众用),契约放 public tag
- [ ] viewer:大图工具栏下载按钮(按 album 开关显示:两级都关→无按钮;browse→下载当前浏览图;original 也开→下拉两项);微信内置浏览器检测(UA)→引导浮层(保存图片/复制链接文案,l10n);下载失败(403/过期)重取一次
- [ ] e2e(ov):组合矩阵(开+开/开+关/关+关)×variant;过期 URL 刷新;Playwright:viewer 大图下载按钮出现性随相册配置、微信 UA mock 引导浮层
- [ ] Commit `feat(ov,viewer): two-tier download with wechat guidance`
- [x] 后端:GET download(ov 侧:读 photos_ov + albums_ov 开关副本;签名 URL 短期;variant 校验:original 需 originalDownloadEnabled,browse 需 downloadEnabled;关闭→403 DOWNLOAD_DISABLED;日志不记完整签名)——注意此端点在 ov 域(观众用),契约放 public tag
- [x] viewer:大图工具栏下载按钮(按 album 开关显示:两级都关→无按钮;browse→下载当前浏览图;original 也开→下拉两项);微信内置浏览器检测(UA)→引导浮层(保存图片/复制链接文案,l10n);下载失败(403/过期)重取一次
- [x] e2e(ov):组合矩阵(开+开/开+关/关+关)×variant;过期 URL 刷新;Playwright:viewer 大图下载按钮出现性随相册配置、微信 UA mock 引导浮层
- [x] Commit `feat(ov,viewer): two-tier download with wechat guidance`
## Task 5: 用量统计与阈值告警(后端 + A23 后台侧)
- [ ] 统计查询:albums 聚合(photos bytes + photo_variants bytes per album)实时 SUM(P1 规模够用;P5 换物化/OSS 用量,登记);thresholds 表(单行配置)+ alerts 表(超阈写入一次直到回落,重复告警去重:kind+albumId 活跃告警唯一)
- [ ] 检查点:照片发布/删除时、阈值修改时评估(worker 发布路径与 photos PATCH 钩子);A23 断言:超阈产生 alert→后台 summary 可见→阈值上调→旧 alert resolvedAt 标记
- [ ] e2e:造假 bytes(直改 DB)→ 告警生成/去重/解除全链
- [ ] Commit `feat(api): usage summary with threshold alerts`
- [x] 统计查询:albums 聚合(photos bytes + photo_variants bytes per album)实时 SUM(P1 规模够用;P5 换物化/OSS 用量,登记);thresholds 表(单行配置)+ alerts 表(超阈写入一次直到回落,重复告警去重:kind+albumId 活跃告警唯一)
- [x] 检查点:照片发布/删除时、阈值修改时评估(worker 发布路径与 photos PATCH 钩子);A23 断言:超阈产生 alert→后台 summary 可见→阈值上调→旧 alert resolvedAt 标记
- [x] e2e:造假 bytes(直改 DB)→ 告警生成/去重/解除全链
- [x] Commit `feat(api): usage summary with threshold alerts`
## Task 6: 账号管理(后端)
- [ ] 管理列表(游标+过滤 query/status)、PATCH(role/localLocked/displayName;**不越权改 sourceStatus——只读展示**)、reset-password(指定或生成随机;撤销其他会话;audit)、deactivate(localLocked=true + 撤销全部会话 + audit)/activate(**§10 语义:credentialStatus pending→active + 经 reset-password 机制出初始凭据**——闭环 Task 7 同步创建的待激活用户)
- [ ] e2e:矩阵(停用→该用户登录 401+已有 token 401;重置→旧密码失效新密码可登;role 改变生效;非 admin 全 403;audit 全程;**Task 7 联动:同步创建的 pending 用户登录 401 → activate+重置 → 可登录**)
- [ ] Commit `feat(api): admin account governance endpoints`
- [x] 管理列表(游标+过滤 query/status)、PATCH(role/localLocked/displayName;**不越权改 sourceStatus——只读展示**)、reset-password(指定或生成随机;撤销其他会话;audit)、deactivate(localLocked=true + 撤销全部会话 + audit)/activate(**§10 语义:credentialStatus pending→active + 经 reset-password 机制出初始凭据**——闭环 Task 7 同步创建的待激活用户)
- [x] e2e:矩阵(停用→该用户登录 401+已有 token 401;重置→旧密码失效新密码可登;role 改变生效;非 admin 全 403;audit 全程;**Task 7 联动:同步创建的 pending 用户登录 401 → activate+重置 → 可登录**)
- [x] Commit `feat(api): admin account governance endpoints`
## Task 7: 用户定期同步(mock 源,T30 语义全量)
- [ ] cn 迁移:user_sync_runs(id, trigger cron|manual, state running|success|partial|failed, cursor 水位, counts Json, startedAt/finishedAt, error)、user_sync_errors(runId, externalId, code, detail)、external_user_mappings(已有?P1 无——建:userId+sourceSystem+externalUserId 唯一, sourceVersion)、users 增 sourceSystem?(用 mappings 表即可,不改 users)
- [ ] MockUserSource provider:可配置 fixtures(分页/增量版本/改名/停用/新增/空页/超时失败模式),接口:listPage(cursor), 与真实 D08 对齐的方法签名(适配器接口 UserSourcePort)
- [ ] 同步引擎(api 内定时 SYNC_USERS_INTERVAL 默认 5min + 手动触发共用):互斥锁(Redis SETNX cn)→ 读水位→分页拉取→逐条:externalId 映射存在→幂等更新(displayName,sourceVersion 比较);不存在→建待激活用户(credentialStatus=pending + 随机初始密码不入库不可登,等管理员激活)→ 显式停用字段→localLocked? NO:sourceStatus=disabled + 撤会话;空响应/分页中断→**不动任何用户**(记录 failed run)→ 提交水位
- [ ] 状态/历史/失败明细/手动补拉 API(Task 1 契约)
- [ ] e2e(mock 全场景):首轮全量(3 用户)→改名幂等→停用撤会话→新增待激活→手动补拉与定时互斥→空响应不误伤→失败批次水位不进→**部分无效记录(state=partial,坏行入 errors 其余照常)→增量模式下周期全量对账→上游恢复(disabled→active)不解除 localLocked(技术方案 3.1A:本地锁定独立)**
- [ ] Commit `feat(api): scheduled user sync with mock source (T30 semantics)`
- [x] cn 迁移:user_sync_runs(id, trigger cron|manual, state running|success|partial|failed, cursor 水位, counts Json, startedAt/finishedAt, error)、user_sync_errors(runId, externalId, code, detail)、external_user_mappings(已有?P1 无——建:userId+sourceSystem+externalUserId 唯一, sourceVersion)、users 增 sourceSystem?(用 mappings 表即可,不改 users)
- [x] MockUserSource provider:可配置 fixtures(分页/增量版本/改名/停用/新增/空页/超时失败模式),接口:listPage(cursor), 与真实 D08 对齐的方法签名(适配器接口 UserSourcePort)
- [x] 同步引擎(api 内定时 SYNC_USERS_INTERVAL 默认 5min + 手动触发共用):互斥锁(Redis SETNX cn)→ 读水位→分页拉取→逐条:externalId 映射存在→幂等更新(displayName,sourceVersion 比较);不存在→建待激活用户(credentialStatus=pending + 随机初始密码不入库不可登,等管理员激活)→ 显式停用字段→localLocked? NO:sourceStatus=disabled + 撤会话;空响应/分页中断→**不动任何用户**(记录 failed run)→ 提交水位
- [x] 状态/历史/失败明细/手动补拉 API(Task 1 契约)
- [x] e2e(mock 全场景):首轮全量(3 用户)→改名幂等→停用撤会话→新增待激活→手动补拉与定时互斥→空响应不误伤→失败批次水位不进→**部分无效记录(state=partial,坏行入 errors 其余照常)→增量模式下周期全量对账→上游恢复(disabled→active)不解除 localLocked(技术方案 3.1A:本地锁定独立)**
- [x] Commit `feat(api): scheduled user sync with mock source (T30 semantics)`
## Task 8: admin 账号治理 + 用户同步页
- [ ] 账号管理页:列表(过滤/分页)、行操作(激活/停用/重置密码弹窗显示生成密码一次性/角色编辑);同步管理页:状态卡(上次成功/水位/计数)、运行历史、失败明细抽屉、手动补拉按钮(防抖+运行中禁用)
- [ ] Playwright(真实后端+mock 源注入:admin e2e 已有 harness 模式):账号全流程(停用→列表状态→重置→用新密码登录 viewer 无关/admin 登录验证);同步页(触发补拉→状态刷新→失败明细可见)
- [ ] Commit `feat(admin): account governance and user sync pages`
- [x] 账号管理页:列表(过滤/分页)、行操作(激活/停用/重置密码弹窗显示生成密码一次性/角色编辑);同步管理页:状态卡(上次成功/水位/计数)、运行历史、失败明细抽屉、手动补拉按钮(防抖+运行中禁用)
- [x] Playwright(真实后端+mock 源注入:admin e2e 已有 harness 模式):账号全流程(停用→列表状态→重置→用新密码登录 viewer 无关/admin 登录验证);同步页(触发补拉→状态刷新→失败明细可见)
- [x] Commit `feat(admin): account governance and user sync pages`
## Task 9: admin 相册治理增量(批量隐藏/交接/水印/下载设置/用量页/分享码)
- [ ] 照片管理页:多选+批量隐藏(逐项结果 toast 汇总 成功x/失败y+失败列表)、交接对话框(选目标账号+是否保留协作者)、行内展示 P2 回传的**统计副本与同步时间**(statsSyncedAt/likeCount 副本列);相册详情:水印卡(开关+参数表单+版本历史+发起重生成+进度条轮询)、下载开关组已有(P1b 编辑表单)——补"测试水印"预览(生成 1 张水印示例展示)、**分享链接与二维码卡**(评审缺口:viewer URL(VITE_VIEWER_BASE)+ qrcode 库渲染 + 复制——需新增 qrcode 依赖,修正"无新依赖"表述)
- [ ] 用量页:汇总表(相册容量/照片数)+阈值设置表单+告警列表(未解决/已解决 tab)
- [ ] Playwright:批量隐藏旅程(3 选 2 隐藏→逐项结果→viewer 侧 2 消失)、交接旅程、水印重生成进度到 done、用量阈值触发告警可见、分享二维码渲染(canvas/dataURL 断言)
- [ ] Commit `feat(admin): batch ops, transfer, watermark, usage and qr share pages`
- [x] 照片管理页:多选+批量隐藏(逐项结果 toast 汇总 成功x/失败y+失败列表)、交接对话框(选目标账号+是否保留协作者)、行内展示 P2 回传的**统计副本与同步时间**(statsSyncedAt/likeCount 副本列);相册详情:水印卡(开关+参数表单+版本历史+发起重生成+进度条轮询)、下载开关组已有(P1b 编辑表单)——补"测试水印"预览(生成 1 张水印示例展示)、**分享链接与二维码卡**(评审缺口:viewer URL(VITE_VIEWER_BASE)+ qrcode 库渲染 + 复制——需新增 qrcode 依赖,修正"无新依赖"表述)
- [x] 用量页:汇总表(相册容量/照片数)+阈值设置表单+告警列表(未解决/已解决 tab)
- [x] Playwright:批量隐藏旅程(3 选 2 隐藏→逐项结果→viewer 侧 2 消失)、交接旅程、水印重生成进度到 done、用量阈值触发告警可见、分享二维码渲染(canvas/dataURL 断言)
- [x] Commit `feat(admin): batch ops, transfer, watermark, usage and qr share pages`
## Task 10: 回归门 + P4a 验收记录
- [ ] 全仓回归绿(web+mobile analyze/test 不降级)+ 记录数字
- [ ] `docs/plans/progress/P4a-验收记录.md`(A16/A17/A18/A20/A23/A29 mock 级证据、D06 工作默认登记、偏差表[用量实时 SUM、文字水印 SVG 实现、下载端点在 ov 域、mock 源方法签名、A23 接收人 P5]、G4 待真实接口清单、移交 P4b/P5)
- [ ] 总纲 P4a 进度注记;`git tag p4a`;Commit `docs: p4a acceptance record`
- [x] 全仓回归绿(web+mobile analyze/test 不降级)+ 记录数字
- [x] `docs/plans/progress/P4a-验收记录.md`(A16/A17/A18/A20/A23/A29 mock 级证据、D06 工作默认登记、偏差表[用量实时 SUM、文字水印 SVG 实现、下载端点在 ov 域、mock 源方法签名、A23 接收人 P5]、G4 待真实接口清单、移交 P4b/P5)
- [x] 总纲 P4a 进度注记;`git tag p4a`;Commit `docs: p4a acceptance record`
---