Files
photo-live/docs/plans/2026-09-20-P4a-后台完备与账号治理.md
T
2026-09-20 16:39:03 +08:00

105 lines
14 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.
# P4a 后台完备 + 账号治理 实施计划
> **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 源先行)与同步管理页。
**Architecture:** 全部在既有 cn-api / admin / viewer 上增量;水印重生成走 BullMQ 新队列(cn 侧,worker 消费);用户同步为独立调度任务(services/api 内 @Cron 风格 setInterval + 互斥锁)+ mock provider(D08 到位后换适配器);用量统计以 photos/photos_ov 行数据 + 可配置计数表为基础(真实流量计费数据 P5 接 OSS 用量 API,登记偏差)。
**Tech Stack:** 沿用既有(NestJS/Prisma/BullMQ/Art Design Pro/Vue3 viewer);唯一新依赖 = admin 端 qrcode 渲染库(Task 9 分享卡)。
**对应验收:** A16 A17 A18(下载开关组合)A20(水印重生成不破坏)A23(告警+组件暂停已有 P2,本阶段补后台展示)A29(mock 级用户同步)。
**依赖基线:** tag `p3`(45d4651)。**D06 前置**:下载/水印默认值——沿用 P1 默认(downloadEnabled=true、originalDownloadEnabled=false、watermark 关闭),水印开启为相册级显式操作,登记为工作默认(用户可随时改)。
**G4 边界**:用户同步以 mock provider 落地全语义(锁/水位/幂等/失败不停用/补拉);真实接口契约到位后仅替换 provider(P5/G4)。
---
## Task 1: 契约扩展(P4a 批次)
**Files:** Modify `packages/contracts/openapi.yaml`(+gen)
- [ ] 新增路径(遵循既有风格):
- `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)
- `GET /public/albums/{token}/photos/{photoId}/download?variant=browse|original` → 200 {url, expiresAt, variant}(匿名;按相册两级开关校验,A18;404 PHOTO_NOT_PUBLIC/403 ALBUM_NOT_PUBLIC…下载关闭=404 NOT_FOUND 语义?→ 用 403 DOWNLOAD_DISABLED 新码)——**新增错误码 DOWNLOAD_DISABLED**
- 账号管理(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`
## 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`
## 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`
## 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`
## 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`
## 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`
## 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)`
## Task 8: admin 账号治理 + 用户同步页
- [ ] 账号管理页:列表(过滤/分页)、行操作(激活/停用/重置密码弹窗显示生成密码一次性/角色编辑);同步管理页:状态卡(上次成功/水位/计数)、运行历史、失败明细抽屉、手动补拉按钮(防抖+运行中禁用)
- [ ] Playwright(真实后端+mock 源注入:admin e2e 已有 harness 模式):账号全流程(停用→列表状态→重置→用新密码登录 viewer 无关/admin 登录验证);同步页(触发补拉→状态刷新→失败明细可见)
- [ ] 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`
## 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`
---
## 执行注意
- 沿用全部既有约定(校对手决/契约先行 gen/reset 隔离/turbo 串行/maxWorkers/偏差登记/Co-Authored-By)。
- 水印涉及的文化字体问题:SVG 文字用系统 sans-serif(Noto Sans CJK 于 sharp 内置渲染链可用性在 e2e 验证,不可用则英文水印先行+中文登记 G5 项)。
- 批量上限 200/次(契约 minItems/maxItems);管理列表过滤不做全文搜索(query=前缀)。
- 用户同步**绝不**写 password_hash/role/album_members——映射表隔离(技术方案 3.1A 红线)。
- A23 的"组件不可见暂停"已在 P2 viewer 落地,本阶段只补后台证据链;Tasks 8-9 全部新页面文案走模板 i18n(zh/en 词条,P4b T15/A12 统一查漏译)。