8f4ad11bd5
契约扩展(全部可选/可空,旧客户端零感知)+ 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>
81 lines
3.0 KiB
TypeScript
81 lines
3.0 KiB
TypeScript
// services/api/src/photos/album-photos.controller.ts —— /albums/{albumId}/photos
|
||
// 管理列表端点(P1b Task 4)+ 批量隐藏(P4a Task 2)。方法名与契约
|
||
// operationId 一一对应(listAlbumPhotos/hidePhotosBatch,tag [photos]——路由
|
||
// 挂在相册子路径下,域仍属照片管理)。控制器级 SessionGuard:端点需认证。
|
||
// 控制器只做认证用户透传 + 查询参数规整(visibility 本域 dto / categoryId
|
||
// 空串归一 / cursor+limit 走 shared/pagination,同契约 bounds);权限与 HTTP
|
||
// 映射由 PhotosService 抛 ApiException(404 ALBUM_NOT_FOUND / 403
|
||
// AUTH_FORBIDDEN / 400 VALIDATION_FAILED)。注意 DTO 值导入(非 import type):
|
||
// @Body 参数类型经 emitDecoratorMetadata 进入设计期元数据。
|
||
import {
|
||
Body,
|
||
Controller,
|
||
Get,
|
||
HttpCode,
|
||
HttpStatus,
|
||
Param,
|
||
Post,
|
||
Query,
|
||
Req,
|
||
UseGuards,
|
||
} from '@nestjs/common';
|
||
import { userOf } from '../albums/albums.controller';
|
||
import { AuthenticatedRequest, SessionGuard } from '../auth/session.guard';
|
||
import { parseCursor, parseLimit } from '../shared/pagination';
|
||
import {
|
||
PhotoHideBatchDto,
|
||
parseScopeFilter,
|
||
parseVisibilityFilter,
|
||
} from './dto';
|
||
import {
|
||
PhotosService,
|
||
type AdminPhotoListResponse,
|
||
type BatchHideResult,
|
||
} from './photos.service';
|
||
|
||
@Controller('albums/:albumId/photos')
|
||
@UseGuards(SessionGuard)
|
||
export class AlbumPhotosController {
|
||
constructor(private readonly photosService: PhotosService) {}
|
||
|
||
/**
|
||
* 管理照片列表:全部可见性状态可见(区别于公开列表仅 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,
|
||
): Promise<AdminPhotoListResponse> {
|
||
return this.photosService.listAlbumPhotos(userOf(req), albumId, {
|
||
visibility: parseVisibilityFilter(visibility),
|
||
// 空串视为缺省(同公开列表 category 规整惯例)。
|
||
categoryId: categoryId === '' ? undefined : categoryId,
|
||
scope: parseScopeFilter(scope),
|
||
cursor: parseCursor(cursor),
|
||
limit: parseLimit(limit),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 批量隐藏(A16):逐项结果(部分成功);单批 1..200(DTO 层 400)。
|
||
* 契约 200(非 201:批量结果不是新建资源的表述)。
|
||
*/
|
||
@Post('hide-batch')
|
||
@HttpCode(HttpStatus.OK)
|
||
hidePhotosBatch(
|
||
@Param('albumId') albumId: string,
|
||
@Body() dto: PhotoHideBatchDto,
|
||
@Req() req: AuthenticatedRequest,
|
||
): Promise<BatchHideResult> {
|
||
return this.photosService.hideBatch(userOf(req), albumId, dto.photoIds);
|
||
}
|
||
}
|