Commit Graph

4 Commits

Author SHA1 Message Date
浅念 8f4ad11bd5 feat(contracts,api): UI-01 cover, preview, scope=own and embed config extensions
契约扩展(全部可选/可空,旧客户端零感知)+ cn/ov 实现 + e2e 权限矩阵:
- AlbumSummary += cover(kind=cover state=effective 最新行的短期签名 URL,
  列表/详情按页批量组装非 N+1;未配置为 null)
- AdminPhoto += preview{thumb,browse}(变体齐备且未删除才签发;隐藏仅
  admin/owner;协作者仅自己上传,scope=own 服务端过滤 uploaderId,
  成员资格逐请求裁决)
- listAlbumPhotos += scope=all|own(缺省 all 行为不变:仅 owner/admin,
  协作者 403;own 任何有效成员)
- EmbedSlotSettings += imageFit/offsetX/offsetBottom/zIndex(cn PATCH 校验
  → slot-state 投影 → ov 公开回读;settings 为 Json 列免迁移,读侧
  normalizeStoredSettings 兜旧形状默认值)
- Flutter 模型同步(SignedUrl/PhotoPreview/AlbumPhotoScope,可空容错)

e2e:ui01-contracts 9 例(封面选取/字节取回/N+1 计数断言、preview 权限
矩阵、scope 过滤与 403/404/400)+ embed ⑨ 配置链路;全量 e2e 257 通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-21 14:09:35 +08:00
浅念 955bea49b5 feat(sync): stat snapshot return path with version replacement
P2 Task 9 统计回传(ov→cn):
- cn 迁移:albums 增 viewCountReplica/statsVersion/statsSyncedAt、
  photos 增 statsVersion(like_count 已有副本位;无 VARCHAR 新列不涉校对手决)
- cn 接收端 POST /v1/internal/sync/stat-snapshots(InternalSyncGuard):
  整批单事务,逐快照 updateMany WHERE statsVersion < statVersion —— 仅更高
  statVersion 以权威 total 整体替换副本(绝不相加),重复/乱序幂等忽略;
  accepted 只数实际应用条数;views 应用同时推进 statsSyncedAt(A27 展示)
- ov 回传发送器 services/sync/stat-return.ts:聚合 outbox_stat_events 未发送
  行按(kind, photoId|albumId)取 MAX(statVersion) 折叠突发 → HMAC 推送 →
  200 后按计数器标记截至该版本全部源行 sentAt;失败留待下轮重发;main.ts
  第三循环 STAT_RETURN_INTERVAL_MS(默认 60s,env 可调)
- 契约 AlbumSummary + statsSyncedAt(string|null readOnly,+gen);listAlbums/
  getAlbum 经 toSummaryResponse 暴露(管理 UI 列留 P4a)
- e2e(services/sync stat-return.e2e-spec,7 例):逐次点赞回传替换式收敛
  (2→5→10 非累加)、浏览窗口去重回传 + statsSyncedAt、重复回传 accepted=0
  值不动(A27 不重复累计)、乱序高版本替换(total 可下调)+ 低版本忽略、
  发送端折叠只发最高版本且被折叠行随批确认、管理列表 likeCount/statsSyncedAt
  展示、新端点 401/400 守卫

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 18:45:43 +08:00
浅念 1faefa4227 feat(api): album update with independent upload/public switches
PATCH /v1/albums/{albumId}(契约 updateAlbum):部分更新,各开关互相独立;
translations 全量替换(deleteMany+createMany),defaultLocale 可同请求切换但
替换后的翻译集必须覆盖最终 defaultLocale。权限顺序 404(不可见)→403(可见
不可管)与 getAlbum 一致防枚举;合并校验(endAt vs 存量 startAt、单边提交)
在事务内对最终状态裁决。audit album.update 记录 metadata.fields(契约属性序)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 22:05:36 +08:00
浅念 1c48a4108b feat(api): album creation with translations and owner membership
Task 15: POST/GET /v1/albums, GET /v1/albums/{id}.
- create: single transaction writes albums + album_translations +
  album_members(owner) + audit_logs(album.create); publicToken 24-char
  [0-9a-z] with one P2002 retry; duplicate-locale input rejected as 400.
- dto: class-validator per contract AlbumCreateRequest, incl. cross-field
  rules (defaultLocale must be covered by translations; endAt>startAt when
  present; timezone string-only per P1a scope).
- get: PermissionsService.canViewAlbum gate -> 404 ALBUM_NOT_FOUND for
  non-member/nonexistent alike (anti-enumeration); admin bypass; joins
  categories (empty until Task 17).
- list: admin sees all, members see joined albums; stable createdAt DESC,
  id DESC keyset pagination via new shared/cursor.ts (encode/decode +
  typed where-builder, designed for Task 23 photo list reuse).
- e2e: services/api/test/albums.e2e-spec.ts (12 cases, seeded sessions).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 21:51:50 +08:00