47fb81ad31
- 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>
97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
// 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);
|
||
}
|
||
}
|