feat(contracts,api): UI-01 cover, preview, scope=own and embed config extensions

契约扩展(全部可选/可空,旧客户端零感知)+ cn/ov 实现 + e2e 权限矩阵:
- AlbumSummary += cover(kind=cover state=effective 最新行的短期签名 URL,
  列表/详情按页批量组装非 N+1;未配置为 null)
- AdminPhoto += preview{thumb,browse}(变体齐备且未删除才签发;隐藏仅
  admin/owner;协作者仅自己上传,scope=own 服务端过滤 uploaderId,
  成员资格逐请求裁决)
- listAlbumPhotos += scope=all|own(缺省 all 行为不变:仅 owner/admin,
  协作者 403;own 任何有效成员)
- EmbedSlotSettings += imageFit/offsetX/offsetBottom/zIndex(cn PATCH 校验
  → slot-state 投影 → ov 公开回读;settings 为 Json 列免迁移,读侧
  normalizeStoredSettings 兜旧形状默认值)
- Flutter 模型同步(SignedUrl/PhotoPreview/AlbumPhotoScope,可空容错)

e2e:ui01-contracts 9 例(封面选取/字节取回/N+1 计数断言、preview 权限
矩阵、scope 过滤与 403/404/400)+ embed ⑨ 配置链路;全量 e2e 257 通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-21 14:09:35 +08:00
parent 3b5f8d2d77
commit 8f4ad11bd5
21 changed files with 1428 additions and 40 deletions
+93 -2
View File
@@ -949,6 +949,11 @@ paths:
相册照片管理列表(区别于公开列表):全部状态可见(unpublished/published/
hidden/deleted),条目含 uploader 脱敏信息。按 uploadedAt 与 ID 稳定倒序
游标分页。
scope(UI-01 可选扩展,缺省 all=既有行为不变):协作者显式查询自己
上传的照片——服务端过滤 uploaderId=当前用户,成员资格逐请求校验;
仅 owner/admin 可用 all(协作者 all → 403)。条目 preview 按请求者
权限逐张裁决(见 PhotoPreview)。
security:
- bearerAuth: []
parameters:
@@ -962,6 +967,17 @@ paths:
description: 分类 ID 过滤。
schema:
type: string
- name: scope
in: query
description: |
数据范围(UI-01):all=本相册全部照片(缺省,仅 owner/admin);
own=仅当前用户上传(任何有效成员,服务端过滤)。
schema:
type: string
enum:
- all
- own
default: all
- $ref: '#/components/parameters/CursorQuery'
- name: limit
in: query
@@ -987,7 +1003,9 @@ paths:
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 仅创建者或管理员可访问管理列表(AUTH_FORBIDDEN)。
description: |
仅创建者或管理员可访问全量管理列表(scope=all 或缺省时协作者
AUTH_FORBIDDEN);scope=own 需相册有效成员。
content:
application/json:
schema:
@@ -1915,6 +1933,8 @@ paths:
返回展示位绑定相册的近期已发布照片(≤20,按 publishedAt 与 ID 稳定
倒序,小尺寸缩略图省流量)与相册摘要。lang 语义:auto(缺省)或取值
不支持时按相册 defaultLocale 回退(不报 400,与公开相册一致)。
settings(UI-01)返回后台配置的归一化全集(嵌入端应用后台配置,
data-* 覆盖优先)。
- 防探测:slotToken 不存在、展示位停用(enabled=false)或未绑定相册
一律 404 NOT_FOUND(同码同文案,不区分原因,避免枚举探测)。
@@ -2797,6 +2817,16 @@ components:
统计副本最近同步时间(P2 A27:ov→cn 统计快照在本相册的应用时刻);
null 表示尚无回传应用(viewCountReplica 副本位仍为初始 0)。照片级
点赞副本(PhotoRecord.likeCount)的同步时间不经相册行表达。
cover:
description: |
相册封面摘要(UI-01,readOnly 可选扩展):当前生效封面资产
(album_assets kind=cover、state=effective 的最新一行)的短期
签名 URL;未配置封面为 null。列表端点按页批量组装(非逐相册
查询);旧客户端忽略本字段不受影响。
readOnly: true
anyOf:
- $ref: '#/components/schemas/SignedUrl'
- type: 'null'
AlbumList:
allOf:
@@ -3513,6 +3543,21 @@ components:
readOnly: true
description: 展示姓名(来自用户资料)。
PhotoPreview:
type: object
description: |
管理端照片预览(UI-01,readOnly):thumb/browse 短期签名 URL 与过期
时间。签发条件(服务端逐张裁决):请求者有该照片管理权(admin 全部 /
相册 owner 本相册全部 / 协作者仅自己上传)且照片有有效变体、未删除;
隐藏照片仅 admin/owner 签发。不满足条件时 AdminPhoto.preview 为 null
(PhotoRecord 本身仍如实返回)。URL 过期后经列表重取刷新。
required: [thumb, browse]
properties:
thumb:
$ref: '#/components/schemas/SignedUrl'
browse:
$ref: '#/components/schemas/SignedUrl'
AdminPhoto:
allOf:
- $ref: '#/components/schemas/PhotoRecord'
@@ -3521,6 +3566,14 @@ components:
properties:
uploader:
$ref: '#/components/schemas/AdminPhotoUploader'
preview:
description: |
受权限管理的照片预览(UI-01,readOnly 可选扩展;无变体/已删除/
无权签发时为 null)。旧客户端忽略本字段不受影响。
readOnly: true
anyOf:
- $ref: '#/components/schemas/PhotoPreview'
- type: 'null'
AdminPhotoList:
allOf:
@@ -4012,7 +4065,9 @@ components:
description: |
展示位展示设置。整体提交语义:创建时 settings 可整体缺省(各字段按
下列缺省值落库),提供时字段齐全;更新为整体替换(未单独提交
settings 则保持不变)。响应中字段恒齐全。
settings 则保持不变)。响应中字段恒齐全。UI-01 新增字段
(imageFit/offsetX/offsetBottom/zIndex)为可选扩展:提交时可缺省
(按默认值归一化落库),旧 JSON/旧客户端零影响;响应恒为归一化全集。
required: [position, size, intervalMs, lang, collapsedDefault]
properties:
position:
@@ -4041,6 +4096,33 @@ components:
type: boolean
description: 初始是否收起(收起后仅展示小把手,点击展开)。
default: false
imageFit:
type: string
description: |
轮播图片适配方式(UI-01):contain=完整展示于 4:3 舞台(中性底);
cover=裁切填满舞台。可缺省(默认 contain)。
enum:
- contain
- cover
default: contain
offsetX:
type: integer
minimum: 0
maximum: 120
description: 组件距宿主页面边缘的水平偏移(像素,UI-01)。可缺省。
default: 16
offsetBottom:
type: integer
minimum: 0
maximum: 240
description: 组件距宿主页面底部的垂直偏移(像素,UI-01)。可缺省。
default: 24
zIndex:
type: integer
minimum: 1
maximum: 2147483000
description: 组件在宿主页面的层级(UI-01)。可缺省。
default: 1000
EmbedSlot:
type: object
@@ -4374,6 +4456,8 @@ components:
照片,按 publishedAt 与 ID 稳定倒序,上限 20(空数组=相册暂无已发布
照片,嵌入端展示空态卡)。album 当前恒非 null(未绑定/停用的展示位
公开端一律 404 防探测);null 为契约前瞻保留,嵌入端按空态处理。
settings(UI-01 可选扩展)为后台配置的归一化全集(缺字段按默认值
补齐)——嵌入端据此应用后台配置,data-* 覆盖优先于它。
required: [slotToken, configVersion, album, photos]
properties:
slotToken:
@@ -4386,6 +4470,13 @@ components:
minimum: 1
readOnly: true
description: 配置版本(嵌入端变更检测:版本变化即整体重置渲染)。
settings:
description: |
展示设置(UI-01 可选扩展;ov 副本 JSON 按契约默认值归一化后返回,
恒为全集)。旧客户端忽略本字段不受影响。
readOnly: true
allOf:
- $ref: '#/components/schemas/EmbedSlotSettings'
album:
description: 绑定相册摘要;未绑定为 null(当前部署不会出现,见 schema 描述)。
anyOf:
+10 -1
View File
@@ -141,13 +141,22 @@ export interface SlotStateUpdatedPayload {
entityVersion: number;
}
/** 展示位设置载荷(契约 EmbedSlotSettings 的载荷形状;字段恒齐全)。 */
/**
* 展示位设置载荷(契约 EmbedSlotSettings 的载荷形状;字段恒齐全)。
* UI-01 新增字段为可选:cn 写入侧恒发归一化全集,但部署切替期间 outbox/
* sync_inbox 中在途事件仍是旧形状——消费侧(ov 投影/公开读)须对缺字段按
* 契约默认值归一化,不得假设恒在。
*/
export interface SlotSettingsPayload {
position: string; // left|right
size: string; // sm|md
intervalMs: number;
lang: string; // auto|zh-CN|en
collapsedDefault: boolean;
imageFit?: string; // contain|cover(默认 contain)
offsetX?: number; // 0..120(默认 16)
offsetBottom?: number; // 0..240(默认 24)
zIndex?: number; // 1..2147483000(默认 1000)
}
// 运行时校验用:全部合法事件类型字符串。satisfies 保证 数组 ⊆ 接口 union;
+71 -2
View File
@@ -418,6 +418,11 @@ export interface paths {
* @description 相册照片管理列表(区别于公开列表):全部状态可见(unpublished/published/
* hidden/deleted),条目含 uploader 脱敏信息。按 uploadedAt 与 ID 稳定倒序
* 游标分页。
*
* scope(UI-01 可选扩展,缺省 all=既有行为不变):协作者显式查询自己
* 上传的照片——服务端过滤 uploaderId=当前用户,成员资格逐请求校验;
* 仅 owner/admin 可用 all(协作者 all → 403)。条目 preview 按请求者
* 权限逐张裁决(见 PhotoPreview)。
*/
get: operations["listAlbumPhotos"];
put?: never;
@@ -905,6 +910,8 @@ export interface paths {
* 返回展示位绑定相册的近期已发布照片(≤20,按 publishedAt 与 ID 稳定
* 倒序,小尺寸缩略图省流量)与相册摘要。lang 语义:auto(缺省)或取值
* 不支持时按相册 defaultLocale 回退(不报 400,与公开相册一致)。
* settings(UI-01)返回后台配置的归一化全集(嵌入端应用后台配置,
* data-* 覆盖优先)。
*
* - 防探测:slotToken 不存在、展示位停用(enabled=false)或未绑定相册
* 一律 404 NOT_FOUND(同码同文案,不区分原因,避免枚举探测)。
@@ -1379,6 +1386,13 @@ export interface components {
* 点赞副本(PhotoRecord.likeCount)的同步时间不经相册行表达。
*/
readonly statsSyncedAt: string | null;
/**
* @description 相册封面摘要(UI-01,readOnly 可选扩展):当前生效封面资产
* (album_assets kind=cover、state=effective 的最新一行)的短期
* 签名 URL;未配置封面为 null。列表端点按页批量组装(非逐相册
* 查询);旧客户端忽略本字段不受影响。
*/
readonly cover?: components["schemas"]["SignedUrl"] | null;
};
AlbumList: components["schemas"]["CursorPageMeta"] & {
items: components["schemas"]["AlbumSummary"][];
@@ -1754,8 +1768,24 @@ export interface components {
/** @description 展示姓名(来自用户资料)。 */
readonly displayName: string;
};
/**
* @description 管理端照片预览(UI-01,readOnly):thumb/browse 短期签名 URL 与过期
* 时间。签发条件(服务端逐张裁决):请求者有该照片管理权(admin 全部 /
* 相册 owner 本相册全部 / 协作者仅自己上传)且照片有有效变体、未删除;
* 隐藏照片仅 admin/owner 签发。不满足条件时 AdminPhoto.preview 为 null
* (PhotoRecord 本身仍如实返回)。URL 过期后经列表重取刷新。
*/
PhotoPreview: {
thumb: components["schemas"]["SignedUrl"];
browse: components["schemas"]["SignedUrl"];
};
AdminPhoto: components["schemas"]["PhotoRecord"] & {
uploader: components["schemas"]["AdminPhotoUploader"];
/**
* @description 受权限管理的照片预览(UI-01,readOnly 可选扩展;无变体/已删除/
* 无权签发时为 null)。旧客户端忽略本字段不受影响。
*/
readonly preview?: components["schemas"]["PhotoPreview"] | null;
};
AdminPhotoList: components["schemas"]["CursorPageMeta"] & {
items: components["schemas"]["AdminPhoto"][];
@@ -2043,7 +2073,9 @@ export interface components {
/**
* @description 展示位展示设置。整体提交语义:创建时 settings 可整体缺省(各字段按
* 下列缺省值落库),提供时字段齐全;更新为整体替换(未单独提交
* settings 则保持不变)。响应中字段恒齐全。
* settings 则保持不变)。响应中字段恒齐全。UI-01 新增字段
* (imageFit/offsetX/offsetBottom/zIndex)为可选扩展:提交时可缺省
* (按默认值归一化落库),旧 JSON/旧客户端零影响;响应恒为归一化全集。
*/
EmbedSlotSettings: {
/**
@@ -2069,6 +2101,28 @@ export interface components {
* @default false
*/
collapsedDefault: boolean;
/**
* @description 轮播图片适配方式(UI-01):contain=完整展示于 4:3 舞台(中性底);
* cover=裁切填满舞台。可缺省(默认 contain)。
* @default contain
* @enum {string}
*/
imageFit: "contain" | "cover";
/**
* @description 组件距宿主页面边缘的水平偏移(像素,UI-01)。可缺省。
* @default 16
*/
offsetX: number;
/**
* @description 组件距宿主页面底部的垂直偏移(像素,UI-01)。可缺省。
* @default 24
*/
offsetBottom: number;
/**
* @description 组件在宿主页面的层级(UI-01)。可缺省。
* @default 1000
*/
zIndex: number;
};
/**
* @description 展示位条目(管理视角)。albumId/albumTitle 为当前绑定相册(null=未
@@ -2244,12 +2298,19 @@ export interface components {
* 照片,按 publishedAt 与 ID 稳定倒序,上限 20(空数组=相册暂无已发布
* 照片,嵌入端展示空态卡)。album 当前恒非 null(未绑定/停用的展示位
* 公开端一律 404 防探测);null 为契约前瞻保留,嵌入端按空态处理。
* settings(UI-01 可选扩展)为后台配置的归一化全集(缺字段按默认值
* 补齐)——嵌入端据此应用后台配置,data-* 覆盖优先于它。
*/
PublicEmbedSlot: {
/** @description 展示位公开令牌(回显请求路径)。 */
readonly slotToken: string;
/** @description 配置版本(嵌入端变更检测:版本变化即整体重置渲染)。 */
readonly configVersion: number;
/**
* @description 展示设置(UI-01 可选扩展;ov 副本 JSON 按契约默认值归一化后返回,
* 恒为全集)。旧客户端忽略本字段不受影响。
*/
readonly settings?: components["schemas"]["EmbedSlotSettings"];
/** @description 绑定相册摘要;未绑定为 null(当前部署不会出现,见 schema 描述)。 */
album: components["schemas"]["PublicEmbedAlbum"] | null;
/** @description 近期照片(publishedAt 与 ID 稳定倒序,小尺寸缩略图)。 */
@@ -3598,6 +3659,11 @@ export interface operations {
visibility?: components["schemas"]["PhotoVisibility"];
/** @description 分类 ID 过滤。 */
categoryId?: string;
/**
* @description 数据范围(UI-01):all=本相册全部照片(缺省,仅 owner/admin);
* own=仅当前用户上传(任何有效成员,服务端过滤)。
*/
scope?: "all" | "own";
/** @description 分页游标(上一页响应的 nextCursor)。 */
cursor?: components["parameters"]["CursorQuery"];
/** @description 每页条数。 */
@@ -3631,7 +3697,10 @@ export interface operations {
};
};
401: components["responses"]["Unauthorized"];
/** @description 仅创建者或管理员可访问管理列表(AUTH_FORBIDDEN)。 */
/**
* @description 仅创建者或管理员可访问全量管理列表(scope=all 或缺省时协作者
* AUTH_FORBIDDEN);scope=own 需相册有效成员。
*/
403: {
headers: {
[name: string]: unknown;