feat(api): GET /v1/public/albums/{token}/ads 公开广告(全局+定向/现签)

ov 公开端点(ads-carousel Task 9,设计 §5.2):相册校验复用
findPublicAlbumOv(不存在/offline/紧急停用统一 404 ALBUM_NOT_PUBLIC);
可见集 = 全局投放(albumIds=null)∨ 绑定本相册,sortOrder 升序、并列
createdAt 升序;imageUrl 逐条现签 GET(TTL=SIGNED_URL_TTL_SECONDS)。
匿名无 guard、不加独立限速档(观众页单次加载一次,与公开相册读同量级,
量级假设 ≤ 百条:全表捞 + 应用层 JSON 过滤)。

public-ov.e2e 追加 ⑧:prismaOv 直种 albums_ov/ads_ov——过滤/排序/现签
(可取回真实字节)/404 同码同 path/空 items/匿名 200;9/9 通过,
ads.e2e 16/16 回归通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-29 00:44:23 +08:00
parent 9ec8226577
commit 373f0fc533
3 changed files with 163 additions and 0 deletions
@@ -14,6 +14,7 @@ import { parsePublicPhotoLimit } from '../public/public.controller';
import { parseCursor } from '../shared/pagination';
import { validationFailed } from '../albums/support';
import {
type PublicAdListResponse,
type PublicAlbumResponse,
type PublicPhotoDetailResponse,
type PublicPhotoListResponse,
@@ -79,4 +80,14 @@ export class PublicOvController {
): Promise<PublicPhotoDetailResponse> {
return this.publicService.getPublicPhoto(token, photoId);
}
/**
* 公开广告列表:全局投放 + 绑定本册,现签图 URL(广告位轮播 2026-09-28
* 设计 §5.2)。匿名(契约 security: []——观众页单次加载一次,与公开相册读
* 同量级,不加独立限速档)。路由与 ':token/photos' 无参数冲突(并列段)。
*/
@Get(':token/ads')
listPublicAds(@Param('token') token: string): Promise<PublicAdListResponse> {
return this.publicService.listPublicAds(token);
}
}
@@ -47,6 +47,8 @@ export type PublicPhotoListResponse =
components['schemas']['PublicPhotoList'];
export type PublicPhotoDetailResponse =
components['schemas']['PublicPhotoDetail'];
export type PublicAdListResponse =
components['schemas']['PublicAdListResponse'];
type SignedUrlResponse = components['schemas']['SignedUrl'];
/** 与 cn public.service 同值(契约 listPublicPhotos limit bounds)。 */
@@ -299,6 +301,36 @@ export class PublicOvService {
};
}
/**
* 公开广告列表(广告位轮播 2026-09-28 设计 §5.2):相册校验复用公开相册读
* 同一入口(findPublicAlbumOv——不存在/offline/紧急停用统一 404
* ALBUM_NOT_PUBLIC);可见集 = 全局投放(albumIds=null)∨ 绑定本相册。
* 排序 sortOrder 升序、并列 createdAt 升序(副本 @@index 同序)。imageUrl
* 逐条现签 GET(TTL 同公开读 SIGNED_URL_TTL_SECONDS;过期后重拉本端点刷新,
* 契约语义——无 expiresAt 字段,PublicAd 只返 URL 裸串)。无广告空 items
* (非 404)。量级假设广告 ≤ 百条(设计 §5.2 登记):ads_ov 全表捞 + 应用层
* 过滤(JSON 列 includes),与公开相册读同量级——不加独立限速档。
*/
async listPublicAds(token: string): Promise<PublicAdListResponse> {
const album = await this.findPublicAlbumOv(token);
const rows = await this.prisma.ads_ov.findMany({
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }],
});
const visible = rows.filter(
(r) =>
r.albumIds === null || (r.albumIds as string[]).includes(album.albumId),
);
const ttl = signedUrlTtlSeconds();
const items = await Promise.all(
visible.map(async (r) => ({
adId: r.adId,
imageUrl: await this.storage.presignGet(r.imageObjectKey, ttl),
linkUrl: r.linkUrl,
})),
);
return { items };
}
/** hot 排序页:快照清单(TTL 缓存)offset 切片 → 现取行(跳过中途被撤下的)。 */
private async listHot(
albumId: string,