diff --git a/.env.example b/.env.example index 795bbf2..b89d88d 100644 --- a/.env.example +++ b/.env.example @@ -63,3 +63,10 @@ VISIT_WINDOW_REQUESTS=60 # 每窗请求数上限——签名现算无幂等面,限速定位是误客户端缓冲,P5 速率层接防)。 DOWNLOAD_WINDOW_REQUESTS=120 CLEANUP_INTERVAL_MINUTES=60 +# P4b Task 1:嵌入组件(悬浮轮播 iframe)公开端点轮询参数——间隔毫秒 / +# 连续失败上限 / 失败退避倍数(达到上限后间隔 ×倍数,成功即恢复)。默认值见 +# packages/contracts/src/constants.ts;P4b Task 4 起被 apps/viewer 嵌入页读取 +# (ETag 304 命中时零负载,A23 省流量)。 +EMBED_POLL_INTERVAL_MS=30000 +EMBED_POLL_MAX_FAILURES=5 +EMBED_POLL_BACKOFF_MULTIPLIER=5 diff --git a/packages/contracts/openapi.yaml b/packages/contracts/openapi.yaml index a8cac72..4f72a8a 100644 --- a/packages/contracts/openapi.yaml +++ b/packages/contracts/openapi.yaml @@ -45,6 +45,11 @@ tags: description: 照片可见性调整(单项与批量)与软删除(工作人员) - name: admin description: 管理辅助查询与账号治理(用户管理、相册交接、用户同步、用量统计) + - name: embed-slots + description: | + 展示位管理(P4b 悬浮轮播组件挂载点):创建、设置与相册绑定/解绑。 + 权限简化决策:仅公司 admin 可管理展示位(成员授权面 P5 按需补); + 无删除端点——enabled=false 即下线(软停用,公开端随即 404)。 - name: public description: 观众匿名只读(相册信息与公开照片) - name: interactions @@ -1636,6 +1641,141 @@ paths: '500': $ref: '#/components/responses/InternalError' + /admin/embed-slots: + get: + operationId: listEmbedSlots + tags: [embed-slots] + summary: 展示位列表 + description: | + 展示位(悬浮轮播组件挂载点)治理列表,游标分页(按 createdAt 与 ID + 稳定倒序);仅公司 admin 可用(P4b 权限简化:成员授权面 P5 按需补)。 + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/CursorQuery' + - name: limit + in: query + description: 每页条数。 + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + responses: + '200': + description: 展示位列表页。 + content: + application/json: + schema: + $ref: '#/components/schemas/EmbedSlotList' + '400': + description: 查询参数校验失败(VALIDATION_FAILED;cursor/limit 非法)。 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + description: 仅 admin 可管理展示位(AUTH_FORBIDDEN)。 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/InternalError' + post: + operationId: createEmbedSlot + tags: [embed-slots] + summary: 创建展示位 + description: | + 创建展示位(仅 admin):服务端生成不可枚举 publicToken(嵌入代码 + data-slot 引用);初始 enabled=true、albumId=null(未绑定,公开端 + 404)、configVersion=1。settings 整体缺省时按各字段缺省值落库。 + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EmbedSlotCreateRequest' + responses: + '201': + description: 创建后的展示位条目。 + content: + application/json: + schema: + $ref: '#/components/schemas/EmbedSlot' + '400': + description: 请求体校验失败(VALIDATION_FAILED;name 长度非法、settings 字段越界或取值非法)。 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + description: 仅 admin 可管理展示位(AUTH_FORBIDDEN)。 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/InternalError' + + /admin/embed-slots/{id}: + parameters: + - $ref: '#/components/parameters/EmbedSlotId' + patch: + operationId: updateEmbedSlot + tags: [embed-slots] + summary: 更新展示位 + description: | + 展示位部分更新(仅 admin):name/enabled/settings/albumId 任一管理面 + 变更均递增 configVersion 并触发 slot-state 同步事件(嵌入端配置版本 + 变化即整体重置渲染)。albumId 语义:非 null 为绑定/换绑目标相册 + (相册须存在,不存在 404 ALBUM_NOT_FOUND),null 为解绑(公开端随即 + 404);albumId 值本身变化(含换绑与解绑)即视为配置变更。settings + 为整体替换(提供时字段齐全)。无删除端点:enabled=false 即下线。 + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EmbedSlotUpdateRequest' + responses: + '200': + description: 更新后的展示位条目。 + content: + application/json: + schema: + $ref: '#/components/schemas/EmbedSlot' + '400': + description: 参数校验失败(VALIDATION_FAILED;name 长度非法、settings 字段越界或取值非法、空请求体)。 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + description: 仅 admin 可管理展示位(AUTH_FORBIDDEN)。 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: 展示位不存在(NOT_FOUND),或绑定目标相册不存在(ALBUM_NOT_FOUND)。 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/InternalError' + # ─────────────────────────── public ─────────────────────────── /public/albums/{token}: @@ -1763,6 +1903,72 @@ paths: '500': $ref: '#/components/responses/InternalError' + /public/embed-slots/{slotToken}: + parameters: + - $ref: '#/components/parameters/EmbedSlotToken' + get: + operationId: getPublicEmbedSlot + tags: [public] + summary: 展示位公开数据(嵌入组件数据源) + description: | + 悬浮轮播组件的数据源(ov 域部署,匿名只读;嵌入 iframe 轮询调用): + 返回展示位绑定相册的近期已发布照片(≤20,按 publishedAt 与 ID 稳定 + 倒序,小尺寸缩略图省流量)与相册摘要。lang 语义:auto(缺省)或取值 + 不支持时按相册 defaultLocale 回退(不报 400,与公开相册一致)。 + + - 防探测:slotToken 不存在、展示位停用(enabled=false)或未绑定相册 + 一律 404 NOT_FOUND(同码同文案,不区分原因,避免枚举探测)。 + - 条件请求(A23 省流量):200/304 响应携带强 ETag,取值为四元组 + `"{configVersion}-{photoCount}-{maxPublishedAtMs}-{maxPhotoId}"` + (含照片条数与最新发布时间——新照片发布必变,避免仅比对实体版本 + 时新照片小版本导致 304 陈旧);请求携带 If-None-Match 且匹配时 + 返回 304(无响应体,仍携带 ETag)。 + security: [] + parameters: + - name: lang + in: query + description: 内容语言;auto(缺省)按相册 defaultLocale,取值不支持时静默回退(不报 400)。 + schema: + $ref: '#/components/schemas/EmbedSlotLang' + - name: If-None-Match + in: header + description: 条件请求:先前响应返回的 ETag 值;匹配则返回 304。 + schema: + type: string + responses: + '200': + description: 展示位公开数据。 + headers: + ETag: + description: 强校验器(组成与失效语义见端点 description)。 + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/PublicEmbedSlot' + '304': + description: 内容未变(If-None-Match 匹配);无响应体。 + headers: + ETag: + description: 当前 ETag(与匹配值一致)。 + schema: + type: string + '400': + description: 查询参数校验失败(VALIDATION_FAILED;lang 取值不支持时不报错,按相册 defaultLocale 回退)。 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: 展示位不存在、已停用或未绑定相册(NOT_FOUND;不区分以避免枚举)。 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/InternalError' + # ─────────────────────────── interactions ─────────────────────────── /public/albums/{token}/photos/{photoId}/likes: @@ -2242,6 +2448,20 @@ components: description: 相册公开令牌(不可枚举,持有即可访问)。 schema: type: string + EmbedSlotId: + name: id + in: path + required: true + description: 展示位 ID。 + schema: + type: string + EmbedSlotToken: + name: slotToken + in: path + required: true + description: 展示位公开令牌(不可枚举,持有即可访问;格式见 EmbedSlot.publicToken)。 + schema: + type: string CursorQuery: name: cursor in: query @@ -3775,6 +3995,154 @@ components: items: $ref: '#/components/schemas/UsageAlert' + # ── 展示位(管理端,P4b) ── + + EmbedSlotLang: + type: string + description: | + 展示位内容语言(设置与公开查询共用):auto=按相册 defaultLocale, + 其余为指定语言(取值不支持时服务端静默回退相册 defaultLocale)。 + enum: + - auto + - zh-CN + - en + + EmbedSlotSettings: + type: object + description: | + 展示位展示设置。整体提交语义:创建时 settings 可整体缺省(各字段按 + 下列缺省值落库),提供时字段齐全;更新为整体替换(未单独提交 + settings 则保持不变)。响应中字段恒齐全。 + required: [position, size, intervalMs, lang, collapsedDefault] + properties: + position: + type: string + description: 悬浮位置(宿主页面左/右侧)。 + enum: + - left + - right + default: right + size: + type: string + description: 组件尺寸(sm=窄屏降级档,宿主 viewport<480px 时自动降为 sm)。 + enum: + - sm + - md + default: md + intervalMs: + type: integer + minimum: 3000 + maximum: 30000 + description: 轮播切换间隔毫秒。 + default: 5000 + lang: + $ref: '#/components/schemas/EmbedSlotLang' + collapsedDefault: + type: boolean + description: 初始是否收起(收起后仅展示小把手,点击展开)。 + default: false + + EmbedSlot: + type: object + description: | + 展示位条目(管理视角)。albumId/albumTitle 为当前绑定相册(null=未 + 绑定,公开端 404);albumTitle 按相册 defaultLocale 解析(管理列表 + 展示用)。任一管理面变更递增 configVersion。 + required: + - id + - publicToken + - name + - enabled + - albumId + - albumTitle + - configVersion + - settings + - updatedAt + properties: + id: + type: string + readOnly: true + description: 展示位 ID。 + publicToken: + type: string + readOnly: true + pattern: '^[0-9a-z]{20,32}$' + description: 展示位公开令牌(嵌入代码 data-slot 引用,不可枚举)。 + name: + type: string + minLength: 1 + maxLength: 64 + description: 展示位名称(管理标识)。 + enabled: + type: boolean + description: 是否启用;false 即下线(公开端 404,等同删除语义)。 + albumId: + type: [string, 'null'] + readOnly: true + description: 绑定相册 ID;未绑定为 null。 + albumTitle: + type: [string, 'null'] + readOnly: true + description: 绑定相册标题(按相册 defaultLocale 解析);未绑定为 null。 + configVersion: + type: integer + minimum: 1 + readOnly: true + description: 配置版本(任一管理面变更递增;嵌入端据此整体重置渲染,亦为公开端 ETag 组成部分)。 + settings: + $ref: '#/components/schemas/EmbedSlotSettings' + updatedAt: + type: string + format: date-time + readOnly: true + description: 最近更新时间。 + + EmbedSlotList: + allOf: + - $ref: '#/components/schemas/CursorPageMeta' + - type: object + required: [items] + properties: + items: + type: array + description: 展示位条目(按 createdAt 与 ID 稳定倒序)。 + items: + $ref: '#/components/schemas/EmbedSlot' + + EmbedSlotCreateRequest: + type: object + description: 创建展示位请求;settings 可整体缺省(各字段按缺省值落库),提供时字段齐全。 + required: [name] + properties: + name: + type: string + minLength: 1 + maxLength: 64 + description: 展示位名称。 + settings: + $ref: '#/components/schemas/EmbedSlotSettings' + + EmbedSlotUpdateRequest: + type: object + description: | + 展示位部分更新请求(仅提交的字段被修改,空请求体 400)。settings 为 + 整体替换(提供时字段齐全);albumId:非 null 绑定/换绑目标相册(相册 + 须存在),null 解绑(公开端随即 404)——albumId 值变化即 configVersion+1。 + properties: + name: + type: string + minLength: 1 + maxLength: 64 + description: 展示位名称。 + enabled: + type: boolean + description: 启停;false 即下线(软停用,无删除端点)。 + settings: + $ref: '#/components/schemas/EmbedSlotSettings' + albumId: + type: [string, 'null'] + description: 绑定相册 ID;null 解绑。 + # ── 公开(观众端) ── SignedUrl: @@ -3958,6 +4326,78 @@ components: items: $ref: '#/components/schemas/PublicPhoto' + # ── 展示位公开数据(观众端,ov 域,P4b) ── + + PublicEmbedAlbum: + type: object + description: 展示位绑定相册摘要(title 已按 lang→相册 defaultLocale 回退解析)。 + required: [publicToken, title, defaultLocale] + properties: + publicToken: + type: string + readOnly: true + pattern: '^[0-9a-z]{20,32}$' + description: 相册公开令牌(点击照片跳转 viewer 相册页用)。 + title: + type: string + readOnly: true + description: 相册标题(实际返回语言)。 + defaultLocale: + $ref: '#/components/schemas/Locale' + + PublicEmbedPhoto: + type: object + description: | + 展示位照片条目(小尺寸缩略图省流量,与公开列表的 browse 大图区分; + 签名 URL 为短期凭证,过期后经本端点刷新)。 + required: [photoId, thumb, width, height] + properties: + photoId: + type: string + readOnly: true + description: 照片 ID(跳转 viewer 单图定位用)。 + thumb: + $ref: '#/components/schemas/SignedUrl' + width: + type: integer + readOnly: true + description: 缩略图宽度(像素);用于占位,减少加载跳动。 + height: + type: integer + readOnly: true + description: 缩略图高度(像素)。 + + PublicEmbedSlot: + type: object + description: | + 展示位公开数据(嵌入组件数据源;ov 只读副本)。photos 为近期已发布 + 照片,按 publishedAt 与 ID 稳定倒序,上限 20(空数组=相册暂无已发布 + 照片,嵌入端展示空态卡)。album 当前恒非 null(未绑定/停用的展示位 + 公开端一律 404 防探测);null 为契约前瞻保留,嵌入端按空态处理。 + required: [slotToken, configVersion, album, photos] + properties: + slotToken: + type: string + readOnly: true + pattern: '^[0-9a-z]{20,32}$' + description: 展示位公开令牌(回显请求路径)。 + configVersion: + type: integer + minimum: 1 + readOnly: true + description: 配置版本(嵌入端变更检测:版本变化即整体重置渲染)。 + album: + description: 绑定相册摘要;未绑定为 null(当前部署不会出现,见 schema 描述)。 + anyOf: + - $ref: '#/components/schemas/PublicEmbedAlbum' + - type: 'null' + photos: + type: array + maxItems: 20 + description: 近期照片(publishedAt 与 ID 稳定倒序,小尺寸缩略图)。 + items: + $ref: '#/components/schemas/PublicEmbedPhoto' + # ── 互动(观众端,ov 域) ── DownloadVariant: diff --git a/packages/contracts/src/constants.ts b/packages/contracts/src/constants.ts index e7615d8..ae39e38 100644 --- a/packages/contracts/src/constants.ts +++ b/packages/contracts/src/constants.ts @@ -39,3 +39,14 @@ export const DOWNLOAD_WINDOW_REQUESTS = 120; export const DOWNLOAD_WINDOW_SECONDS = 60; /** ov 互动流水清理调度间隔分钟(like_batches/visit_requests/visit_sessions)。 */ export const CLEANUP_INTERVAL_MINUTES = 60; + +// ── P4b 嵌入组件(悬浮轮播)── +// 展示位轮播 iframe 的公开端点轮询参数(ETag 304 命中时零负载;默认值可被 +// env 覆盖,登记见根 .env.example,P4b Task 4 起被 apps/viewer 嵌入页读取)。 + +/** 展示位轮询间隔毫秒(拉新照片/检测 configVersion 变化)。 */ +export const EMBED_POLL_INTERVAL_MS = 30000; +/** 轮询连续失败上限:达到后退避(间隔 × EMBED_POLL_BACKOFF_MULTIPLIER)。 */ +export const EMBED_POLL_MAX_FAILURES = 5; +/** 轮询失败退避倍数(连续失败达上限后拉长间隔,成功即恢复正常间隔)。 */ +export const EMBED_POLL_BACKOFF_MULTIPLIER = 5; diff --git a/packages/contracts/src/generated/openapi.d.ts b/packages/contracts/src/generated/openapi.d.ts index 08b4bed..83c5ec5 100644 --- a/packages/contracts/src/generated/openapi.d.ts +++ b/packages/contracts/src/generated/openapi.d.ts @@ -760,6 +760,61 @@ export interface paths { patch?: never; trace?: never; }; + "/admin/embed-slots": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * 展示位列表 + * @description 展示位(悬浮轮播组件挂载点)治理列表,游标分页(按 createdAt 与 ID + * 稳定倒序);仅公司 admin 可用(P4b 权限简化:成员授权面 P5 按需补)。 + */ + get: operations["listEmbedSlots"]; + put?: never; + /** + * 创建展示位 + * @description 创建展示位(仅 admin):服务端生成不可枚举 publicToken(嵌入代码 + * data-slot 引用);初始 enabled=true、albumId=null(未绑定,公开端 + * 404)、configVersion=1。settings 整体缺省时按各字段缺省值落库。 + */ + post: operations["createEmbedSlot"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/embed-slots/{id}": { + parameters: { + query?: never; + header?: never; + path: { + /** @description 展示位 ID。 */ + id: components["parameters"]["EmbedSlotId"]; + }; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * 更新展示位 + * @description 展示位部分更新(仅 admin):name/enabled/settings/albumId 任一管理面 + * 变更均递增 configVersion 并触发 slot-state 同步事件(嵌入端配置版本 + * 变化即整体重置渲染)。albumId 语义:非 null 为绑定/换绑目标相册 + * (相册须存在,不存在 404 ALBUM_NOT_FOUND),null 为解绑(公开端随即 + * 404);albumId 值本身变化(含换绑与解绑)即视为配置变更。settings + * 为整体替换(提供时字段齐全)。无删除端点:enabled=false 即下线。 + */ + patch: operations["updateEmbedSlot"]; + trace?: never; + }; "/public/albums/{token}": { parameters: { query?: never; @@ -834,6 +889,40 @@ export interface paths { patch?: never; trace?: never; }; + "/public/embed-slots/{slotToken}": { + parameters: { + query?: never; + header?: never; + path: { + /** @description 展示位公开令牌(不可枚举,持有即可访问;格式见 EmbedSlot.publicToken)。 */ + slotToken: components["parameters"]["EmbedSlotToken"]; + }; + cookie?: never; + }; + /** + * 展示位公开数据(嵌入组件数据源) + * @description 悬浮轮播组件的数据源(ov 域部署,匿名只读;嵌入 iframe 轮询调用): + * 返回展示位绑定相册的近期已发布照片(≤20,按 publishedAt 与 ID 稳定 + * 倒序,小尺寸缩略图省流量)与相册摘要。lang 语义:auto(缺省)或取值 + * 不支持时按相册 defaultLocale 回退(不报 400,与公开相册一致)。 + * + * - 防探测:slotToken 不存在、展示位停用(enabled=false)或未绑定相册 + * 一律 404 NOT_FOUND(同码同文案,不区分原因,避免枚举探测)。 + * - 条件请求(A23 省流量):200/304 响应携带强 ETag,取值为四元组 + * `"{configVersion}-{photoCount}-{maxPublishedAtMs}-{maxPhotoId}"` + * (含照片条数与最新发布时间——新照片发布必变,避免仅比对实体版本 + * 时新照片小版本导致 304 陈旧);请求携带 If-None-Match 且匹配时 + * 返回 304(无响应体,仍携带 ETag)。 + */ + get: operations["getPublicEmbedSlot"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/public/albums/{token}/photos/{photoId}/likes": { parameters: { query?: never; @@ -1945,6 +2034,93 @@ export interface components { /** @description 告警列表(createdAt 倒序,含已解除)。 */ alerts: components["schemas"]["UsageAlert"][]; }; + /** + * @description 展示位内容语言(设置与公开查询共用):auto=按相册 defaultLocale, + * 其余为指定语言(取值不支持时服务端静默回退相册 defaultLocale)。 + * @enum {string} + */ + EmbedSlotLang: "auto" | "zh-CN" | "en"; + /** + * @description 展示位展示设置。整体提交语义:创建时 settings 可整体缺省(各字段按 + * 下列缺省值落库),提供时字段齐全;更新为整体替换(未单独提交 + * settings 则保持不变)。响应中字段恒齐全。 + */ + EmbedSlotSettings: { + /** + * @description 悬浮位置(宿主页面左/右侧)。 + * @default right + * @enum {string} + */ + position: "left" | "right"; + /** + * @description 组件尺寸(sm=窄屏降级档,宿主 viewport<480px 时自动降为 sm)。 + * @default md + * @enum {string} + */ + size: "sm" | "md"; + /** + * @description 轮播切换间隔毫秒。 + * @default 5000 + */ + intervalMs: number; + lang: components["schemas"]["EmbedSlotLang"]; + /** + * @description 初始是否收起(收起后仅展示小把手,点击展开)。 + * @default false + */ + collapsedDefault: boolean; + }; + /** + * @description 展示位条目(管理视角)。albumId/albumTitle 为当前绑定相册(null=未 + * 绑定,公开端 404);albumTitle 按相册 defaultLocale 解析(管理列表 + * 展示用)。任一管理面变更递增 configVersion。 + */ + EmbedSlot: { + /** @description 展示位 ID。 */ + readonly id: string; + /** @description 展示位公开令牌(嵌入代码 data-slot 引用,不可枚举)。 */ + readonly publicToken: string; + /** @description 展示位名称(管理标识)。 */ + name: string; + /** @description 是否启用;false 即下线(公开端 404,等同删除语义)。 */ + enabled: boolean; + /** @description 绑定相册 ID;未绑定为 null。 */ + readonly albumId: string | null; + /** @description 绑定相册标题(按相册 defaultLocale 解析);未绑定为 null。 */ + readonly albumTitle: string | null; + /** @description 配置版本(任一管理面变更递增;嵌入端据此整体重置渲染,亦为公开端 ETag 组成部分)。 */ + readonly configVersion: number; + settings: components["schemas"]["EmbedSlotSettings"]; + /** + * Format: date-time + * @description 最近更新时间。 + */ + readonly updatedAt: string; + }; + EmbedSlotList: components["schemas"]["CursorPageMeta"] & { + /** @description 展示位条目(按 createdAt 与 ID 稳定倒序)。 */ + items: components["schemas"]["EmbedSlot"][]; + }; + /** @description 创建展示位请求;settings 可整体缺省(各字段按缺省值落库),提供时字段齐全。 */ + EmbedSlotCreateRequest: { + /** @description 展示位名称。 */ + name: string; + settings?: components["schemas"]["EmbedSlotSettings"]; + }; + /** + * @description 展示位部分更新请求(仅提交的字段被修改,空请求体 400)。settings 为 + * 整体替换(提供时字段齐全);albumId:非 null 绑定/换绑目标相册(相册 + * 须存在),null 解绑(公开端随即 404)——albumId 值变化即 configVersion+1。 + */ + EmbedSlotUpdateRequest: { + /** @description 展示位名称。 */ + name?: string; + /** @description 启停;false 即下线(软停用,无删除端点)。 */ + enabled?: boolean; + settings?: components["schemas"]["EmbedSlotSettings"]; + /** @description 绑定相册 ID;null 解绑。 */ + albumId?: string | null; + }; /** @description 私有对象的短期签名 URL;不得写入持久分享链接,过期后经原入口刷新。 */ SignedUrl: { /** @description 签名访问 URL。 */ @@ -2042,6 +2218,43 @@ export interface components { PublicPhotoList: components["schemas"]["CursorPageMeta"] & { items: components["schemas"]["PublicPhoto"][]; }; + /** @description 展示位绑定相册摘要(title 已按 lang→相册 defaultLocale 回退解析)。 */ + PublicEmbedAlbum: { + /** @description 相册公开令牌(点击照片跳转 viewer 相册页用)。 */ + readonly publicToken: string; + /** @description 相册标题(实际返回语言)。 */ + readonly title: string; + defaultLocale: components["schemas"]["Locale"]; + }; + /** + * @description 展示位照片条目(小尺寸缩略图省流量,与公开列表的 browse 大图区分; + * 签名 URL 为短期凭证,过期后经本端点刷新)。 + */ + PublicEmbedPhoto: { + /** @description 照片 ID(跳转 viewer 单图定位用)。 */ + readonly photoId: string; + thumb: components["schemas"]["SignedUrl"]; + /** @description 缩略图宽度(像素);用于占位,减少加载跳动。 */ + readonly width: number; + /** @description 缩略图高度(像素)。 */ + readonly height: number; + }; + /** + * @description 展示位公开数据(嵌入组件数据源;ov 只读副本)。photos 为近期已发布 + * 照片,按 publishedAt 与 ID 稳定倒序,上限 20(空数组=相册暂无已发布 + * 照片,嵌入端展示空态卡)。album 当前恒非 null(未绑定/停用的展示位 + * 公开端一律 404 防探测);null 为契约前瞻保留,嵌入端按空态处理。 + */ + PublicEmbedSlot: { + /** @description 展示位公开令牌(回显请求路径)。 */ + readonly slotToken: string; + /** @description 配置版本(嵌入端变更检测:版本变化即整体重置渲染)。 */ + readonly configVersion: number; + /** @description 绑定相册摘要;未绑定为 null(当前部署不会出现,见 schema 描述)。 */ + album: components["schemas"]["PublicEmbedAlbum"] | null; + /** @description 近期照片(publishedAt 与 ID 稳定倒序,小尺寸缩略图)。 */ + photos: components["schemas"]["PublicEmbedPhoto"][]; + }; /** * @description 下载版本(P4a A18):browse=当前生效浏览图(水印版); * original=原图(需 originalDownloadEnabled 更高级开关)。 @@ -2332,6 +2545,10 @@ export interface components { PhotoId: string; /** @description 相册公开令牌(不可枚举,持有即可访问)。 */ AlbumPublicToken: string; + /** @description 展示位 ID。 */ + EmbedSlotId: string; + /** @description 展示位公开令牌(不可枚举,持有即可访问;格式见 EmbedSlot.publicToken)。 */ + EmbedSlotToken: string; /** @description 分页游标(上一页响应的 nextCursor)。 */ CursorQuery: string; /** @description 请求语言;缺省或取值不支持时按相册 defaultLocale 回退(不报 400,响应 resolvedLocale 指示实际返回语言)。 */ @@ -4132,6 +4349,151 @@ export interface operations { 500: components["responses"]["InternalError"]; }; }; + listEmbedSlots: { + parameters: { + query?: { + /** @description 分页游标(上一页响应的 nextCursor)。 */ + cursor?: components["parameters"]["CursorQuery"]; + /** @description 每页条数。 */ + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 展示位列表页。 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EmbedSlotList"]; + }; + }; + /** @description 查询参数校验失败(VALIDATION_FAILED;cursor/limit 非法)。 */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 401: components["responses"]["Unauthorized"]; + /** @description 仅 admin 可管理展示位(AUTH_FORBIDDEN)。 */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 500: components["responses"]["InternalError"]; + }; + }; + createEmbedSlot: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EmbedSlotCreateRequest"]; + }; + }; + responses: { + /** @description 创建后的展示位条目。 */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EmbedSlot"]; + }; + }; + /** @description 请求体校验失败(VALIDATION_FAILED;name 长度非法、settings 字段越界或取值非法)。 */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 401: components["responses"]["Unauthorized"]; + /** @description 仅 admin 可管理展示位(AUTH_FORBIDDEN)。 */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 500: components["responses"]["InternalError"]; + }; + }; + updateEmbedSlot: { + parameters: { + query?: never; + header?: never; + path: { + /** @description 展示位 ID。 */ + id: components["parameters"]["EmbedSlotId"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EmbedSlotUpdateRequest"]; + }; + }; + responses: { + /** @description 更新后的展示位条目。 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EmbedSlot"]; + }; + }; + /** @description 参数校验失败(VALIDATION_FAILED;name 长度非法、settings 字段越界或取值非法、空请求体)。 */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 401: components["responses"]["Unauthorized"]; + /** @description 仅 admin 可管理展示位(AUTH_FORBIDDEN)。 */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description 展示位不存在(NOT_FOUND),或绑定目标相册不存在(ALBUM_NOT_FOUND)。 */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 500: components["responses"]["InternalError"]; + }; + }; getPublicAlbum: { parameters: { query?: { @@ -4273,6 +4635,65 @@ export interface operations { 500: components["responses"]["InternalError"]; }; }; + getPublicEmbedSlot: { + parameters: { + query?: { + /** @description 内容语言;auto(缺省)按相册 defaultLocale,取值不支持时静默回退(不报 400)。 */ + lang?: components["schemas"]["EmbedSlotLang"]; + }; + header?: { + /** @description 条件请求:先前响应返回的 ETag 值;匹配则返回 304。 */ + "If-None-Match"?: string; + }; + path: { + /** @description 展示位公开令牌(不可枚举,持有即可访问;格式见 EmbedSlot.publicToken)。 */ + slotToken: components["parameters"]["EmbedSlotToken"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 展示位公开数据。 */ + 200: { + headers: { + /** @description 强校验器(组成与失效语义见端点 description)。 */ + ETag?: string; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PublicEmbedSlot"]; + }; + }; + /** @description 内容未变(If-None-Match 匹配);无响应体。 */ + 304: { + headers: { + /** @description 当前 ETag(与匹配值一致)。 */ + ETag?: string; + [name: string]: unknown; + }; + content?: never; + }; + /** @description 查询参数校验失败(VALIDATION_FAILED;lang 取值不支持时不报错,按相册 defaultLocale 回退)。 */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description 展示位不存在、已停用或未绑定相册(NOT_FOUND;不区分以避免枚举)。 */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 500: components["responses"]["InternalError"]; + }; + }; likePhotoBatch: { parameters: { query?: never;