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
@@ -624,4 +624,45 @@ describe('UI-01 契约扩展:cover / preview / scope=own (e2e)', () => {
},
});
});
it('cover 回退 banner(宣传图/封面共用:仅 banner 相册 → cover=banner 对象)', async () => {
// 直种仅 banner 相册(复用本文件资产直种模式)。
const album = await prisma.albums.create({
data: {
publicToken: makePublicToken(),
ownerId: 'u-ui01-coverfb',
startAt: new Date('2026-10-03T02:00:00.000Z'),
endAt: new Date('2026-10-03T12:00:00.000Z'),
defaultLocale: 'zh-CN',
},
});
await prisma.album_translations.create({
data: { albumId: album.id, locale: 'zh-CN', title: '仅宣传图相册' },
});
const key = `photo-live/dev/albums/${album.id}/assets/u01assetbannerfb00000000.jpg`;
await putObject(key, Buffer.from([0x51, 0x52, 0x53]));
await prisma.album_assets.create({
data: {
id: 'u01assetbannerfb00000000',
albumId: album.id,
kind: 'banner',
locale: null,
objectKey: key,
state: 'effective',
expectedBytes: 3,
contentType: 'image/jpeg',
},
});
const res = await listAlbums(adminToken);
expect(res.status).toBe(200);
const item = (res.body.items as Array<{ id: string; cover: { url: string } | null }>).find(
(row) => row.id === album.id,
);
expect(item).toBeDefined();
expect(item?.cover).not.toBeNull();
const fetched = await fetch(item!.cover!.url);
expect(fetched.status).toBe(200);
expect(Buffer.from(await fetched.arrayBuffer()).length).toBe(3);
});
});