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>
This commit is contained in:
浅念
2026-09-21 14:09:35 +08:00
parent 3b5f8d2d77
commit 8f4ad11bd5
21 changed files with 1428 additions and 40 deletions
@@ -22,7 +22,11 @@ import {
import { userOf } from '../albums/albums.controller';
import { AuthenticatedRequest, SessionGuard } from '../auth/session.guard';
import { parseCursor, parseLimit } from '../shared/pagination';
import { PhotoHideBatchDto, parseVisibilityFilter } from './dto';
import {
PhotoHideBatchDto,
parseScopeFilter,
parseVisibilityFilter,
} from './dto';
import {
PhotosService,
type AdminPhotoListResponse,
@@ -37,12 +41,15 @@ export class AlbumPhotosController {
/**
* 管理照片列表:全部可见性状态可见(区别于公开列表仅 published),
* (uploadedAt,id) 稳定倒序键集游标;条目内嵌 uploader 脱敏信息。
* scope=own(UI-01):协作者显式查询自己上传(服务端过滤);缺省 all
* 行为不变(仅 owner/admin)。
*/
@Get()
listAlbumPhotos(
@Param('albumId') albumId: string,
@Query('visibility') visibility: string | undefined,
@Query('categoryId') categoryId: string | undefined,
@Query('scope') scope: string | undefined,
@Query('cursor') cursor: string | undefined,
@Query('limit') limit: string | undefined,
@Req() req: AuthenticatedRequest,
@@ -51,6 +58,7 @@ export class AlbumPhotosController {
visibility: parseVisibilityFilter(visibility),
// 空串视为缺省(同公开列表 category 规整惯例)。
categoryId: categoryId === '' ? undefined : categoryId,
scope: parseScopeFilter(scope),
cursor: parseCursor(cursor),
limit: parseLimit(limit),
});