feat(albums): soft-delete albums (admin+owner, hidden & irreversible, both app entries)

契约 deleteAlbum(DELETE /albums/{albumId} → 204 幂等)+ status enum 'deleted':
- 服务端:softDelete(canView 404→canManage 403 防枚举;乐观锁 claim +
  outbox album.updated priority=high + audit album.delete);列表/详情/更新/
  成员/资产/分类/水印/照片/展示位绑定全部 deleted→404;公开端 cn
  (status!=='active')与 ov(isAlbumVisibleOv)天然屏蔽;ov 副本经
  album.updated 载荷 status 直投自动下线
- Admin:列表表格操作「删除」+ 详情页头「删除相册」(确认含不可恢复说明)
- App:操作面板「删除相册」(红字、二次确认、成功退回列表并刷新)
- 验证:API 56/56、App 397/397(analyze 0)、admin typecheck/lint/test/build 绿

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-23 10:13:08 +08:00
parent 9cb960c27a
commit 8e674a902e
31 changed files with 995 additions and 258 deletions
@@ -395,13 +395,20 @@ export class SyncInboxService {
}
if (event.entityType === 'asset') {
const p = event.payload as unknown as AssetUpdatedPayload;
// banner/cover 分列投影(宣传图/封面共用决策 2026-09-23:展示层双向
// 回退,见 public-ov banner 组装);logo 仍无投影位(YAGNI),仅推进
// 版本账本。替换语义同 banner:新事件覆盖旧键。
if (p.kind === 'banner') {
await tx.albums_ov.update({
where: { albumId: event.albumId },
data: { bannerAssetKey: p.objectKey },
});
} else if (p.kind === 'cover') {
await tx.albums_ov.update({
where: { albumId: event.albumId },
data: { coverAssetKey: p.objectKey },
});
}
// cover/logo:ov schema 无投影位(YAGNI,schema 注释),仅推进版本账本。
return undefined;
}
// slot-state(P4b Task 2):全量快照直投 embed_slots_ov(版本单调已由 ②