diff --git a/packages/contracts/openapi.yaml b/packages/contracts/openapi.yaml index e6e39c9..4567f8e 100644 --- a/packages/contracts/openapi.yaml +++ b/packages/contracts/openapi.yaml @@ -50,6 +50,11 @@ tags: 展示位管理(P4b 悬浮轮播组件挂载点):创建、设置与相册绑定/解绑。 权限简化决策:仅公司 admin 可管理展示位(成员授权面 P5 按需补); 无删除端点——enabled=false 即下线(软停用,公开端随即 404)。 + - name: ads + description: | + 广告位管理(观众页相册头部轮播素材):创建、换图、启停、排序与相册 + 绑定/全局投放。仅公司 admin 可管理;图片两步直传(staging 预签名 → + 创建/更新时核验转存),不走变体流水线、原图直出(上限 2MB)。 - name: public description: 观众匿名只读(相册信息与公开照片) - name: interactions @@ -1942,6 +1947,202 @@ paths: '500': $ref: '#/components/responses/InternalError' + /admin/ads: + get: + operationId: listAdminAds + tags: [ads] + summary: 广告位列表 + description: | + 广告位治理列表(仅 admin):按 sortOrder 升序、并列 createdAt 升序 + 全量返回(运营量级 <20,不分页);imageUrl 为现签 GET,过期后重拉 + 本列表刷新。albumIds=null 为全局投放。 + security: + - bearerAuth: [] + responses: + '200': + description: 广告位列表(全量)。 + content: + application/json: + schema: + $ref: '#/components/schemas/AdminAdListResponse' + '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: createAd + tags: [ads] + summary: 创建广告位 + description: | + 创建广告位(仅 admin):服务端核验 staging 对象(存在 + bytes + + contentType 与申明一致,banner complete 同口径)后 copyIfAbsent + 转存版本化终键,落库并写 outbox ad.updated(entityVersion=1)。 + albumIds 未提供或空数组归一为 null(全局投放);元素须为存在的相册 + ID(相册删除不级联清理,容失效见设计 §2.1)。 + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AdCreateRequest' + responses: + '201': + description: 创建后的广告位条目。 + content: + application/json: + schema: + $ref: '#/components/schemas/AdminAdItem' + '400': + description: | + 参数校验失败:linkUrl 非 http(s)/超长、albumIds 含不存在的相册 + (VALIDATION_FAILED),或 staging 核验失败——对象缺失/字节或类型 + 与申明不一致(UPLOAD_VERIFY_FAILED)。 + 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/ads/uploads: + post: + operationId: initAdUpload + tags: [ads] + summary: 初始化广告图直传 + description: | + 广告图两步直传第一步(仅 admin):签发 staging 对象的临时直传凭证, + 键 photo-live/{env}/ads/staging/{cuid}.{ext}。字节上限收紧为 2MB + (AD_UPLOAD_MAX_BYTES:广告不走变体流水线、原图直出且轮播多张同屏, + 独立于照片域 UPLOAD_MAX_BYTES 的 50MB,设计 §3);类型白名单 + image/jpeg|png|heic。无会话概念(重复转存由创建端 copyIfAbsent + 禁止覆盖语义承担)。 + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AdUploadInitRequest' + responses: + '201': + description: staging 对象键与临时直传凭证。 + content: + application/json: + schema: + $ref: '#/components/schemas/AdUploadInitResponse' + '400': + description: 参数校验失败(VALIDATION_FAILED):超过 2MB 限额、类型不接受或文件名扩展名非法。 + 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/ads/{adId}: + parameters: + - $ref: '#/components/parameters/AdId' + patch: + operationId: updateAd + tags: [ads] + summary: 更新广告位 + description: | + 广告位部分更新(仅 admin):title/linkUrl/enabled/sortOrder/albumIds + 任一变更递增 entityVersion 并重发整行快照(ad.updated;停用 true→false + 走 high 优先级)。albumIds:未提供=不动,null=转全局,数组=整体替换; + 换图时 imageStagingKey/imageBytes/imageContentType 三者必须同时提供 + (核验后转存新版本键,旧对象不回收,OSS 生命周期兜底)。 + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AdUpdateRequest' + responses: + '200': + description: 更新后的广告位条目。 + content: + application/json: + schema: + $ref: '#/components/schemas/AdminAdItem' + '400': + description: | + 参数校验失败(VALIDATION_FAILED):字段非法、换图三字段未同时提供 + 或 staging 核验失败(UPLOAD_VERIFY_FAILED)。 + 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: 广告位不存在(AD_NOT_FOUND)。 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/InternalError' + delete: + operationId: deleteAd + tags: [ads] + summary: 硬删除广告位 + description: >- + 硬删行并写 ad.deleted 墓碑(high 优先级;ov 删 ads_ov 投影行且不被 + 旧事件复活)。cn 侧数据不保留、无恢复端点;重复删除 404。 + security: + - bearerAuth: [] + responses: + '204': + description: 已删除。 + '401': + $ref: '#/components/responses/Unauthorized' + '403': + description: 仅 admin 可管理广告位(AUTH_FORBIDDEN)。 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: 广告位不存在(AD_NOT_FOUND)。 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + # ─────────────────────────── public ─────────────────────────── /public/albums/{token}: @@ -2069,6 +2270,36 @@ paths: '500': $ref: '#/components/responses/InternalError' + /public/albums/{token}/ads: + parameters: + - $ref: '#/components/parameters/AlbumPublicToken' + get: + operationId: listPublicAds + tags: [public] + summary: 公开广告列表 + description: | + 观众页相册头部轮播数据(ov 域部署):返回「全局投放(albumIds=null) + ∨ 绑定本相册」的启用广告,按 sortOrder 升序、并列 createdAt 升序; + imageUrl 为现签 GET(TTL=SIGNED_URL_TTL_SECONDS)。无广告返回空 + items(非 404)。量级假设广告 ≤ 百条:ads_ov 全表捞 + 应用层过滤, + 不加独立限速档(与公开相册读同量级,设计 §5.2)。 + security: [] + responses: + '200': + description: 公开广告列表(可为空数组)。 + content: + application/json: + schema: + $ref: '#/components/schemas/PublicAdListResponse' + '404': + description: 相册不存在或未公开(ALBUM_NOT_PUBLIC,不区分以避免枚举)。 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/InternalError' + /public/embed-slots/{slotToken}: parameters: - $ref: '#/components/parameters/EmbedSlotToken' @@ -2630,6 +2861,13 @@ components: description: 展示位公开令牌(不可枚举,持有即可访问;格式见 EmbedSlot.publicToken)。 schema: type: string + AdId: + name: adId + in: path + required: true + description: 广告位 ID。 + schema: + type: string CursorQuery: name: cursor in: query @@ -4526,6 +4764,186 @@ components: type: [string, 'null'] description: 绑定相册 ID;null 解绑。 + # ── 广告位(管理端,cn 域,2026-09-28) ── + + AdminAdItem: + type: object + description: | + 广告位条目(管理视角)。albumIds 为绑定相册 ID 列表(null=全局投放, + 空数组归一为 null);imageUrl 为现签 GET(TTL=SIGNED_URL_TTL_SECONDS), + 过期后重拉列表刷新。 + required: + - id + - imageObjectKey + - imageUrl + - linkUrl + - enabled + - sortOrder + - albumIds + - createdAt + - updatedAt + properties: + id: + type: string + readOnly: true + description: 广告位 ID。 + title: + type: [string, 'null'] + description: 后台备注名(不展示观众)。 + imageObjectKey: + type: string + readOnly: true + description: 版本化终键(换图递增版本,禁止覆盖)。 + imageUrl: + type: string + readOnly: true + description: 现签 GET(TTL=SIGNED_URL_TTL_SECONDS)。 + linkUrl: + type: string + description: 跳转链接(观众点击新开标签)。 + enabled: + type: boolean + description: 是否启用;false 公开端不可见(ov 投影行删除,无墓碑下线)。 + sortOrder: + type: integer + description: 排序权重(升序,并列按 createdAt 升序决胜)。 + albumIds: + description: 绑定相册 ID 列表;null=全局投放(未绑定)。 + type: [array, 'null'] + items: + type: string + createdAt: + type: string + format: date-time + readOnly: true + description: 创建时间。 + updatedAt: + type: string + format: date-time + readOnly: true + description: 最近更新时间。 + + AdminAdListResponse: + type: object + description: 广告位列表(全量不分页;sortOrder 升序、并列 createdAt 升序)。 + required: [items] + properties: + items: + type: array + items: + $ref: '#/components/schemas/AdminAdItem' + + AdCreateRequest: + type: object + description: | + 创建广告位请求;albumIds 未提供或空数组=全局投放(落库归一 null), + 元素须为存在的相册 ID(去重后校验)。 + required: [linkUrl, imageStagingKey, imageBytes, imageContentType] + properties: + title: + type: string + maxLength: 120 + description: 后台备注名(不展示观众)。 + linkUrl: + type: string + maxLength: 2048 + pattern: '^https?://' + description: 跳转链接(http/https)。 + imageStagingKey: + type: string + description: POST /admin/ads/uploads 返回的 staging 对象键。 + imageBytes: + type: integer + minimum: 1 + maximum: 2097152 + description: 申明字节数(须与 staging 对象一致;上限 AD_UPLOAD_MAX_BYTES=2MB)。 + imageContentType: + type: string + enum: [image/jpeg, image/png, image/heic] + description: 申明类型(须与 staging 对象一致);image/heic 为暂定项,解码能力待 G2 真机验证后确认。 + sortOrder: + type: integer + minimum: 0 + description: 排序权重(缺省 0)。 + albumIds: + type: array + items: + type: string + maxLength: 64 + description: 绑定相册 ID 列表;未提供或空=全局。 + + AdUpdateRequest: + type: object + description: | + 部分更新请求(仅提交的字段被修改)。albumIds:未提供=不动,null=转 + 全局,数组=整体替换;imageStagingKey/imageBytes/imageContentType 三者 + 必须同时提供(换图,服务端核验 staging 后转存新版本键)。 + properties: + title: + type: string + maxLength: 120 + description: 后台备注名(不展示观众)。 + linkUrl: + type: string + maxLength: 2048 + pattern: '^https?://' + description: 跳转链接(http/https)。 + enabled: + type: boolean + description: 启停;false 公开端不可见(停用事件走 high 优先级)。 + sortOrder: + type: integer + minimum: 0 + description: 排序权重。 + albumIds: + description: 绑定相册 ID 列表;null=转全局,数组=整体替换。 + type: [array, 'null'] + items: + type: string + maxLength: 64 + imageStagingKey: + type: string + description: 换图用 staging 对象键(与 imageBytes/imageContentType 同时提供)。 + imageBytes: + type: integer + minimum: 1 + maximum: 2097152 + description: 换图申明字节数(上限 AD_UPLOAD_MAX_BYTES=2MB)。 + imageContentType: + type: string + enum: [image/jpeg, image/png, image/heic] + description: 换图申明类型。 + + AdUploadInitRequest: + type: object + description: 广告图直传初始化请求(无会话概念,幂等由创建端 copyIfAbsent 承担)。 + required: [filename, bytes, contentType] + properties: + filename: + type: string + maxLength: 255 + description: 原始文件名(取扩展名定 staging 键)。 + bytes: + type: integer + minimum: 1 + maximum: 2097152 + description: 申明字节数(上限 AD_UPLOAD_MAX_BYTES=2MB,超限 400)。 + contentType: + type: string + enum: [image/jpeg, image/png, image/heic] + description: MIME 类型白名单;image/heic 为暂定项,解码能力待 G2 真机验证后确认。 + + AdUploadInitResponse: + type: object + required: [objectKey, credential] + properties: + objectKey: + type: string + readOnly: true + description: staging 键 photo-live/{env}/ads/staging/{cuid}.{ext}(创建/更新时引用)。 + credential: + $ref: '#/components/schemas/UploadCredential' + # ── 公开(观众端) ── SignedUrl: @@ -4792,6 +5210,35 @@ components: items: $ref: '#/components/schemas/PublicEmbedPhoto' + # ── 广告位公开数据(观众端,ov 域,2026-09-28) ── + + PublicAd: + type: object + description: 观众端广告条目(仅展示所需字段,广告标识与关闭由前端渲染)。 + required: [adId, imageUrl, linkUrl] + properties: + adId: + type: string + readOnly: true + description: 广告位 ID。 + imageUrl: + type: string + readOnly: true + description: 现签 GET(TTL=SIGNED_URL_TTL_SECONDS,过期后重拉本端点刷新)。 + linkUrl: + type: string + description: 跳转链接(新开标签)。 + + PublicAdListResponse: + type: object + description: 公开广告列表(无广告为空 items,非 404)。 + required: [items] + properties: + items: + type: array + items: + $ref: '#/components/schemas/PublicAd' + # ── 互动(观众端,ov 域) ── DownloadVariant: @@ -4908,6 +5355,7 @@ components: - photo - asset - slot-state + - ad SyncPriority: type: string diff --git a/packages/contracts/src/constants.ts b/packages/contracts/src/constants.ts index 2de3937..73faa82 100644 --- a/packages/contracts/src/constants.ts +++ b/packages/contracts/src/constants.ts @@ -54,3 +54,14 @@ export const EMBED_POLL_BACKOFF_MULTIPLIER = 5; export const EMBED_WINDOW_REQUESTS = 120; /** 展示位限速固定窗秒数(轮询 30s/次 + 页面重载突发,窗口内余量充足)。 */ export const EMBED_WINDOW_SECONDS = 60; + +// ── 广告位(2026-09-28 设计 §3)── +// 对照关系:照片域上限的真实位置在 services/api/src/uploads/support.ts 的 +// DEFAULT_UPLOAD_MAX_BYTES(env 可覆盖、惰性读取,随请求生效);本常量落 +// contracts 是因为 openapi 描述引用它且三端(api 校验/admin 前端预检)需要 +// 单一出处,而广告上限为固定收紧值、无需 env 覆盖——故不照搬 support.ts +// 的惰性读 env 模式。 + +/** 广告图字节上限(2MB):广告不走变体流水线、原图直出且轮播多张同屏, + * 比 UPLOAD_MAX_BYTES(50MB,照片域)收紧(设计 §3)。 */ +export const AD_UPLOAD_MAX_BYTES = 2 * 1024 * 1024; diff --git a/packages/contracts/src/events.ts b/packages/contracts/src/events.ts index 2aa443a..3311311 100644 --- a/packages/contracts/src/events.ts +++ b/packages/contracts/src/events.ts @@ -168,6 +168,7 @@ export interface SlotSettingsPayload { // ad.updated 载荷:ads_ov 投影重建所需的**全量快照**——每次管理面变更整行替换 // (slot.updated 同款);adId 供发送器推导 entityId(outbox 表无此列)。 // albumIds null=全局投放;enabled=false 消费侧按「删投影行」处理(无墓碑下线)。 +// OutboxEvent.albumId 信封约定:ad.* 事件恒写空串 ''(定向广告亦然,绑定关系在载荷 albumIds,设计 §4)。 export interface AdUpdatedPayload { adId: string; imageObjectKey: string; diff --git a/packages/contracts/src/generated/openapi.d.ts b/packages/contracts/src/generated/openapi.d.ts index 6ad9491..002e149 100644 --- a/packages/contracts/src/generated/openapi.d.ts +++ b/packages/contracts/src/generated/openapi.d.ts @@ -857,6 +857,92 @@ export interface paths { patch: operations["updateEmbedSlot"]; trace?: never; }; + "/admin/ads": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * 广告位列表 + * @description 广告位治理列表(仅 admin):按 sortOrder 升序、并列 createdAt 升序 + * 全量返回(运营量级 <20,不分页);imageUrl 为现签 GET,过期后重拉 + * 本列表刷新。albumIds=null 为全局投放。 + */ + get: operations["listAdminAds"]; + put?: never; + /** + * 创建广告位 + * @description 创建广告位(仅 admin):服务端核验 staging 对象(存在 + bytes + + * contentType 与申明一致,banner complete 同口径)后 copyIfAbsent + * 转存版本化终键,落库并写 outbox ad.updated(entityVersion=1)。 + * albumIds 未提供或空数组归一为 null(全局投放);元素须为存在的相册 + * ID(相册删除不级联清理,容失效见设计 §2.1)。 + */ + post: operations["createAd"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/ads/uploads": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * 初始化广告图直传 + * @description 广告图两步直传第一步(仅 admin):签发 staging 对象的临时直传凭证, + * 键 photo-live/{env}/ads/staging/{cuid}.{ext}。字节上限收紧为 2MB + * (AD_UPLOAD_MAX_BYTES:广告不走变体流水线、原图直出且轮播多张同屏, + * 独立于照片域 UPLOAD_MAX_BYTES 的 50MB,设计 §3);类型白名单 + * image/jpeg|png|heic。无会话概念(重复转存由创建端 copyIfAbsent + * 禁止覆盖语义承担)。 + */ + post: operations["initAdUpload"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/ads/{adId}": { + parameters: { + query?: never; + header?: never; + path: { + /** @description 广告位 ID。 */ + adId: components["parameters"]["AdId"]; + }; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * 硬删除广告位 + * @description 硬删行并写 ad.deleted 墓碑(high 优先级;ov 删 ads_ov 投影行且不被 旧事件复活)。cn 侧数据不保留、无恢复端点;重复删除 404。 + */ + delete: operations["deleteAd"]; + options?: never; + head?: never; + /** + * 更新广告位 + * @description 广告位部分更新(仅 admin):title/linkUrl/enabled/sortOrder/albumIds + * 任一变更递增 entityVersion 并重发整行快照(ad.updated;停用 true→false + * 走 high 优先级)。albumIds:未提供=不动,null=转全局,数组=整体替换; + * 换图时 imageStagingKey/imageBytes/imageContentType 三者必须同时提供 + * (核验后转存新版本键,旧对象不回收,OSS 生命周期兜底)。 + */ + patch: operations["updateAd"]; + trace?: never; + }; "/public/albums/{token}": { parameters: { query?: never; @@ -931,6 +1017,33 @@ export interface paths { patch?: never; trace?: never; }; + "/public/albums/{token}/ads": { + parameters: { + query?: never; + header?: never; + path: { + /** @description 相册公开令牌(不可枚举,持有即可访问)。 */ + token: components["parameters"]["AlbumPublicToken"]; + }; + cookie?: never; + }; + /** + * 公开广告列表 + * @description 观众页相册头部轮播数据(ov 域部署):返回「全局投放(albumIds=null) + * ∨ 绑定本相册」的启用广告,按 sortOrder 升序、并列 createdAt 升序; + * imageUrl 为现签 GET(TTL=SIGNED_URL_TTL_SECONDS)。无广告返回空 + * items(非 404)。量级假设广告 ≤ 百条:ads_ov 全表捞 + 应用层过滤, + * 不加独立限速档(与公开相册读同量级,设计 §5.2)。 + */ + get: operations["listPublicAds"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/public/embed-slots/{slotToken}": { parameters: { query?: never; @@ -2315,6 +2428,109 @@ export interface components { /** @description 绑定相册 ID;null 解绑。 */ albumId?: string | null; }; + /** + * @description 广告位条目(管理视角)。albumIds 为绑定相册 ID 列表(null=全局投放, + * 空数组归一为 null);imageUrl 为现签 GET(TTL=SIGNED_URL_TTL_SECONDS), + * 过期后重拉列表刷新。 + */ + AdminAdItem: { + /** @description 广告位 ID。 */ + readonly id: string; + /** @description 后台备注名(不展示观众)。 */ + title?: string | null; + /** @description 版本化终键(换图递增版本,禁止覆盖)。 */ + readonly imageObjectKey: string; + /** @description 现签 GET(TTL=SIGNED_URL_TTL_SECONDS)。 */ + readonly imageUrl: string; + /** @description 跳转链接(观众点击新开标签)。 */ + linkUrl: string; + /** @description 是否启用;false 公开端不可见(ov 投影行删除,无墓碑下线)。 */ + enabled: boolean; + /** @description 排序权重(升序,并列按 createdAt 升序决胜)。 */ + sortOrder: number; + /** @description 绑定相册 ID 列表;null=全局投放(未绑定)。 */ + albumIds: string[] | null; + /** + * Format: date-time + * @description 创建时间。 + */ + readonly createdAt: string; + /** + * Format: date-time + * @description 最近更新时间。 + */ + readonly updatedAt: string; + }; + /** @description 广告位列表(全量不分页;sortOrder 升序、并列 createdAt 升序)。 */ + AdminAdListResponse: { + items: components["schemas"]["AdminAdItem"][]; + }; + /** + * @description 创建广告位请求;albumIds 未提供或空数组=全局投放(落库归一 null), + * 元素须为存在的相册 ID(去重后校验)。 + */ + AdCreateRequest: { + /** @description 后台备注名(不展示观众)。 */ + title?: string; + /** @description 跳转链接(http/https)。 */ + linkUrl: string; + /** @description POST /admin/ads/uploads 返回的 staging 对象键。 */ + imageStagingKey: string; + /** @description 申明字节数(须与 staging 对象一致;上限 AD_UPLOAD_MAX_BYTES=2MB)。 */ + imageBytes: number; + /** + * @description 申明类型(须与 staging 对象一致);image/heic 为暂定项,解码能力待 G2 真机验证后确认。 + * @enum {string} + */ + imageContentType: "image/jpeg" | "image/png" | "image/heic"; + /** @description 排序权重(缺省 0)。 */ + sortOrder?: number; + /** @description 绑定相册 ID 列表;未提供或空=全局。 */ + albumIds?: string[]; + }; + /** + * @description 部分更新请求(仅提交的字段被修改)。albumIds:未提供=不动,null=转 + * 全局,数组=整体替换;imageStagingKey/imageBytes/imageContentType 三者 + * 必须同时提供(换图,服务端核验 staging 后转存新版本键)。 + */ + AdUpdateRequest: { + /** @description 后台备注名(不展示观众)。 */ + title?: string; + /** @description 跳转链接(http/https)。 */ + linkUrl?: string; + /** @description 启停;false 公开端不可见(停用事件走 high 优先级)。 */ + enabled?: boolean; + /** @description 排序权重。 */ + sortOrder?: number; + /** @description 绑定相册 ID 列表;null=转全局,数组=整体替换。 */ + albumIds?: string[] | null; + /** @description 换图用 staging 对象键(与 imageBytes/imageContentType 同时提供)。 */ + imageStagingKey?: string; + /** @description 换图申明字节数(上限 AD_UPLOAD_MAX_BYTES=2MB)。 */ + imageBytes?: number; + /** + * @description 换图申明类型。 + * @enum {string} + */ + imageContentType?: "image/jpeg" | "image/png" | "image/heic"; + }; + /** @description 广告图直传初始化请求(无会话概念,幂等由创建端 copyIfAbsent 承担)。 */ + AdUploadInitRequest: { + /** @description 原始文件名(取扩展名定 staging 键)。 */ + filename: string; + /** @description 申明字节数(上限 AD_UPLOAD_MAX_BYTES=2MB,超限 400)。 */ + bytes: number; + /** + * @description MIME 类型白名单;image/heic 为暂定项,解码能力待 G2 真机验证后确认。 + * @enum {string} + */ + contentType: "image/jpeg" | "image/png" | "image/heic"; + }; + AdUploadInitResponse: { + /** @description staging 键 photo-live/{env}/ads/staging/{cuid}.{ext}(创建/更新时引用)。 */ + readonly objectKey: string; + credential: components["schemas"]["UploadCredential"]; + }; /** @description 私有对象的短期签名 URL;不得写入持久分享链接,过期后经原入口刷新。 */ SignedUrl: { /** @description 签名访问 URL。 */ @@ -2459,6 +2675,19 @@ export interface components { /** @description 近期照片(publishedAt 与 ID 稳定倒序,小尺寸缩略图)。 */ photos: components["schemas"]["PublicEmbedPhoto"][]; }; + /** @description 观众端广告条目(仅展示所需字段,广告标识与关闭由前端渲染)。 */ + PublicAd: { + /** @description 广告位 ID。 */ + readonly adId: string; + /** @description 现签 GET(TTL=SIGNED_URL_TTL_SECONDS,过期后重拉本端点刷新)。 */ + readonly imageUrl: string; + /** @description 跳转链接(新开标签)。 */ + linkUrl: string; + }; + /** @description 公开广告列表(无广告为空 items,非 404)。 */ + PublicAdListResponse: { + items: components["schemas"]["PublicAd"][]; + }; /** * @description 下载版本(P4a A18):browse=当前生效浏览图(水印版); * original=原图(需 originalDownloadEnabled 更高级开关)。 @@ -2526,7 +2755,7 @@ export interface components { * @description 同步实体类型(slot-state 为预留给后续阶段)。 * @enum {string} */ - SyncEntityType: "album" | "category" | "photo" | "asset" | "slot-state"; + SyncEntityType: "album" | "category" | "photo" | "asset" | "slot-state" | "ad"; /** * @description 推送优先级:high 为相册下线、照片隐藏/删除等需即时生效的事件 * (发送器即发,不等批间隔);normal 走常规批量推送。 @@ -2753,6 +2982,8 @@ export interface components { EmbedSlotId: string; /** @description 展示位公开令牌(不可枚举,持有即可访问;格式见 EmbedSlot.publicToken)。 */ EmbedSlotToken: string; + /** @description 广告位 ID。 */ + AdId: string; /** @description 分页游标(上一页响应的 nextCursor)。 */ CursorQuery: string; /** @description 请求语言;缺省或取值不支持时按相册 defaultLocale 回退(不报 400,响应 resolvedLocale 指示实际返回语言)。 */ @@ -4881,6 +5112,228 @@ export interface operations { 500: components["responses"]["InternalError"]; }; }; + listAdminAds: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 广告位列表(全量)。 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AdminAdListResponse"]; + }; + }; + 401: components["responses"]["Unauthorized"]; + /** @description 仅 admin 可管理广告位(AUTH_FORBIDDEN)。 */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 500: components["responses"]["InternalError"]; + }; + }; + createAd: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AdCreateRequest"]; + }; + }; + responses: { + /** @description 创建后的广告位条目。 */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AdminAdItem"]; + }; + }; + /** + * @description 参数校验失败:linkUrl 非 http(s)/超长、albumIds 含不存在的相册 + * (VALIDATION_FAILED),或 staging 核验失败——对象缺失/字节或类型 + * 与申明不一致(UPLOAD_VERIFY_FAILED)。 + */ + 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"]; + }; + }; + initAdUpload: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AdUploadInitRequest"]; + }; + }; + responses: { + /** @description staging 对象键与临时直传凭证。 */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AdUploadInitResponse"]; + }; + }; + /** @description 参数校验失败(VALIDATION_FAILED):超过 2MB 限额、类型不接受或文件名扩展名非法。 */ + 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"]; + }; + }; + deleteAd: { + parameters: { + query?: never; + header?: never; + path: { + /** @description 广告位 ID。 */ + adId: components["parameters"]["AdId"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 已删除。 */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["Unauthorized"]; + /** @description 仅 admin 可管理广告位(AUTH_FORBIDDEN)。 */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description 广告位不存在(AD_NOT_FOUND)。 */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + updateAd: { + parameters: { + query?: never; + header?: never; + path: { + /** @description 广告位 ID。 */ + adId: components["parameters"]["AdId"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AdUpdateRequest"]; + }; + }; + responses: { + /** @description 更新后的广告位条目。 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AdminAdItem"]; + }; + }; + /** + * @description 参数校验失败(VALIDATION_FAILED):字段非法、换图三字段未同时提供 + * 或 staging 核验失败(UPLOAD_VERIFY_FAILED)。 + */ + 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 广告位不存在(AD_NOT_FOUND)。 */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 500: components["responses"]["InternalError"]; + }; + }; getPublicAlbum: { parameters: { query?: { @@ -5022,6 +5475,39 @@ export interface operations { 500: components["responses"]["InternalError"]; }; }; + listPublicAds: { + parameters: { + query?: never; + header?: never; + path: { + /** @description 相册公开令牌(不可枚举,持有即可访问)。 */ + token: components["parameters"]["AlbumPublicToken"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 公开广告列表(可为空数组)。 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PublicAdListResponse"]; + }; + }; + /** @description 相册不存在或未公开(ALBUM_NOT_PUBLIC,不区分以避免枚举)。 */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 500: components["responses"]["InternalError"]; + }; + }; getPublicEmbedSlot: { parameters: { query?: {