Files
photo-live/services/api/src/public-ov/public-ov.controller.ts
T
浅念 0fcf18cc5c feat(ov): public read api on replicas with hot sorting
P2 Task 6(ov 公开读 API + viewer 切换):
- ov 装配 PublicOvModule(仅 app-ov.module):/public/albums/{token}、
  /photos、/photos/{id} 三端点同契约读 ov 副本——translations/categories
  自 JSON 列解析(防御性过滤),banner 经 ov 侧 STORAGE_PORT_OV 预签名
  (同本地 MinIO;只读属约定,真实 OSS 只读凭证 P5),viewCount 取
  album_stats 权威累计(无行默认 0;计数端点 Task 8 接线)
- 排序:latest=发布时间 keyset(shared/cursor.ts 同 cn);hot=likeTotal
  desc + photoId desc 决胜,顺序按 `${albumId}|${category}` 短期快照
  (内存 TTL 默认 30s,HOT_SORT_TTL_MS 惰性读取)——计数逐请求现读、
  顺序冻结窗口内一致,偏移游标(base64url 整数)在快照内严格稳定;
  非法 sort 400 VALIDATION_FAILED(cn 不感知该参数)
- 契约:viewCount 放宽为 integer|null(cn null、ov 权威值);
  listPublicPhotos 增 sort 枚举(latest|hot,缺省 latest);
  PhotoPublishedPayload 增 categoryId——补上 Tasks 3/4 管线缺口
  (payloads/publish.service/sync-inbox 投影 + 关联测试随改)
- e2e(services/api/test/public-ov.e2e-spec.ts,7 用例):双入口同进程
  起 cn+ov,cn API 种子→真实发送管线投递;断言相册/列表/单图与 cn
  深等(仅签名 URL 剔除)、banner 真字节、参数矩阵 400/404、hot 排序
  TTL 内快照(计数新/顺序旧)+TTL 过期翻转+偏移翻页、隐藏/下线/紧急
  停用逐态撤下
- viewer 切 ov:API 基址 VITE_API_BASE(缺省 http://127.0.0.1:8081,
  空串=同源网关;Vite 只读 apps/viewer/.env*,根 .env 对前端无效),
  移除 /v1 dev 代理(CORS_ORIGINS 增 127.0.0.1:5175 直连两形);
  playwright webServer 增 dev:ov;e2e 夹具 flushSync(albumId)=cn
  bootstrap 快照 + deliverPendingOnce(sender 新导出)经真实管线投影
  种子,deleteSeededPhoto 改软删让墓碑可同步;14 个 Playwright 全绿
  (journey 管理/staffB 腿仍走 cn 8080)

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

83 lines
3.3 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-ov/public-ov.controller.ts —— ov 公开端点
// (P2 Task 6)。路由/方法名与 cn public.controller 一一对应(契约同形:
// 路径前缀不变、部署域名区分),差异仅两处:
// · 新增 sort 查询参数(latest|hot;hot 仅 ov 实现——cn 不感知,见契约
// listPublicPhotos sort 描述)。非法取值 400 VALIDATION_FAILED(与
// limit/cursor 同口径;locale 仍保持「非法值静默回退」的 Task 5 决策);
// · 服务层读 ov 副本(PublicOvService)。
// limit/cursor 规整直接复用 cn public.controller 的解析函数(缺省 30、1..60、
// cursor 长度上限)——两入口共享同一套校验源,杜绝漂移。匿名访问(无
// SessionGuard;认证由持有 publicToken 取代)。
import { Controller, Get, Param, Query } from '@nestjs/common';
// limit 规整复用 cn public.controller 的导出(同一校验源,杜绝两入口漂移)。
import { parsePublicPhotoLimit } from '../public/public.controller';
import { parseCursor } from '../shared/pagination';
import { validationFailed } from '../albums/support';
import {
type PublicAlbumResponse,
type PublicPhotoDetailResponse,
type PublicPhotoListResponse,
PublicOvService,
} from './public-ov.service';
/** sort 查询参数解析:缺省 latest;非法 → 400(见文件头口径说明)。 */
export function parsePublicPhotoSort(
raw: string | undefined,
): 'latest' | 'hot' {
if (raw === undefined || raw === '') {
return 'latest';
}
if (raw === 'latest' || raw === 'hot') {
return raw;
}
throw validationFailed('sort 必须是 latest 或 hot');
}
@Controller('public/albums')
export class PublicOvController {
constructor(private readonly publicService: PublicOvService) {}
/** 公开相册信息:locale 缺省/非法/无条目均按 defaultLocale 回退(200)。 */
@Get(':token')
getPublicAlbum(
@Param('token') token: string,
@Query('locale') locale: string | undefined,
): Promise<PublicAlbumResponse> {
return this.publicService.getPublicAlbum(token, locale);
}
/**
* 公开照片列表:latest=(publishedAt,photoId) 键集游标(缺省);hot=点赞
* 快照 offset 游标(30s 缓冲,服务层语义)。category 限本册分类。
*/
@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,
@Query('sort') sort: string | undefined,
): Promise<PublicPhotoListResponse> {
return this.publicService.listPublicPhotos(token, {
category,
// locale 仅保证「不因非法值报 400」,列表无语言敏感字段,不向下传。
cursor: parseCursor(cursor),
limit: parsePublicPhotoLimit(limit),
sort: parsePublicPhotoSort(sort),
});
}
/**
* 公开单图:深链定位;隐藏/删除/换册后的旧链接 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);
}
}