Files
photo-live/services/api/src/public/public.controller.ts
T
浅念 47fb81ad31 chore(api): p1a final-review hygiene items
- contracts: add NOT_FOUND error code (framework-level 404 for unknown
  routes); Error enum + regen committed
- api: 404 default mapping in AllExceptionsFilter now NOT_FOUND (was
  ALBUM_NOT_FOUND); business 404s unchanged via explicit ApiException codes
- api: extract configureApp (prefix/CORS/x-powered-by) shared by main-cn
  bootstrap and e2e; disable x-powered-by, asserted in e2e
- api: move parseCursor/parseLimit (+page-size bounds) from
  albums.controller to shared/pagination; members/uploads/public re-pointed
- infra: bind compose published ports to 127.0.0.1 (mysql/redis/minio);
  .env.example endpoints follow suit
- e2e: unknown-route 404 NOT_FOUND + x-powered-by absent cases added
  (api e2e 120 -> 122)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 08:18:30 +08:00

97 lines
3.5 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 '../shared/pagination';
import {
DEFAULT_PUBLIC_PHOTO_PAGE_SIZE,
MAX_PUBLIC_PHOTO_PAGE_SIZE,
type PublicAlbumResponse,
type PublicPhotoDetailResponse,
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),
});
}
/**
* 公开单图:深链定位;隐藏/删除/换册后的旧链接 404 PHOTO_NOT_PUBLIC。
* 每次调用现签 thumb/browse(契约规定的过期刷新入口)。
*/
@Get(':token/photos/:photoId')
getPublicPhoto(
@Param('token') token: string,
@Param('photoId') photoId: string,
): Promise<PublicPhotoDetailResponse> {
return this.publicService.getPublicPhoto(token, photoId);
}
}