diff --git a/docs/plans/2026-09-20-P4a-后台完备与账号治理.md b/docs/plans/2026-09-20-P4a-后台完备与账号治理.md new file mode 100644 index 0000000..2be46a8 --- /dev/null +++ b/docs/plans/2026-09-20-P4a-后台完备与账号治理.md @@ -0,0 +1,104 @@ +# 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 统一查漏译)。