Files
photo-live/services/api/src/photos/album-photos.controller.ts
T
浅念 8f4ad11bd5 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>
2026-09-21 14:09:35 +08:00

81 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}
}