// 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 { 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 { return this.photosService.hideBatch(userOf(req), albumId, dto.photoIds); } }