// services/api/src/public/public.controller.ts —— /public 端点(Task 22 起步: // getPublicAlbum;Task 23 增补 listPublicPhotos / getPublicPhoto)。方法名与 // 契约 operationId 一一对应。匿名访问(契约 security: []): // · 不挂 SessionGuard——控制器级守卫缺席即匿名,认证由持有 publicToken 本身 // 取代(令牌不可枚举,持有链接者均可访问); // · 限流:P1a 不做(P2 引入点赞限流时统一评估公开面配额),此处记录约定。 // 查询参数规整(limit 1..60 缺省 30、cursor 长度):非法 → 400 // VALIDATION_FAILED;locale 取值不在此校验——契约规定不支持的语言按 // defaultLocale 静默回退(Task 5 决策),语义在服务层。 import { Controller, Get, HttpStatus, Param, Query, } from '@nestjs/common'; import { ErrorCodes } from '@photo/contracts'; import { ApiException } from '../api-exception'; import { parseCursor } from '../albums/albums.controller'; import { DEFAULT_PUBLIC_PHOTO_PAGE_SIZE, MAX_PUBLIC_PHOTO_PAGE_SIZE, type PublicAlbumResponse, type PublicPhotoListResponse, PublicService, } from './public.service'; /** limit 查询参数:缺省 30,须为 1..60 的十进制整数(契约 bounds)。 */ export function parsePublicPhotoLimit(raw: string | undefined): number { if (raw === undefined || raw === '') { return DEFAULT_PUBLIC_PHOTO_PAGE_SIZE; } if (!/^\d+$/.test(raw)) { throw new ApiException( ErrorCodes.VALIDATION_FAILED, 'limit 必须是 1-60 的整数', HttpStatus.BAD_REQUEST, ); } const limit = Number.parseInt(raw, 10); if (limit < 1 || limit > MAX_PUBLIC_PHOTO_PAGE_SIZE) { throw new ApiException( ErrorCodes.VALIDATION_FAILED, 'limit 必须是 1-60 的整数', HttpStatus.BAD_REQUEST, ); } return limit; } @Controller('public/albums') export class PublicController { constructor(private readonly publicService: PublicService) {} /** 公开相册信息:locale 缺省/非法/无条目均按 defaultLocale 回退(200)。 */ @Get(':token') getPublicAlbum( @Param('token') token: string, @Query('locale') locale: string | undefined, ): Promise { return this.publicService.getPublicAlbum(token, locale); } /** * 公开照片列表:仅 published;(publishedAt,id) 稳定倒序键集游标;category * 限本册分类;locale 不校验取值(非法值静默回退语义,Task 5 决策)。 */ @Get(':token/photos') listPublicPhotos( @Param('token') token: string, @Query('category') category: string | undefined, @Query('locale') locale: string | undefined, @Query('cursor') cursor: string | undefined, @Query('limit') limit: string | undefined, ): Promise { return this.publicService.listPublicPhotos(token, { category, // locale 仅保证「不因非法值报 400」,列表无语言敏感字段,不向下传。 cursor: parseCursor(cursor), limit: parsePublicPhotoLimit(limit), }); } }