Files
photo-live/services/api/src/public/public.controller.ts
T
2026-09-17 23:39:50 +08:00

84 lines
3.1 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/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<PublicAlbumResponse> {
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<PublicPhotoListResponse> {
return this.publicService.listPublicPhotos(token, {
category,
// locale 仅保证「不因非法值报 400」,列表无语言敏感字段,不向下传。
cursor: parseCursor(cursor),
limit: parsePublicPhotoLimit(limit),
});
}
}