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:
@@ -4,6 +4,8 @@
|
||||
// · name 1..64(VarChar(64) 列宽);
|
||||
// · settings 整体提交语义:创建可整体缺省(各字段按缺省值落库,服务层
|
||||
// 归一),提供时字段齐全——嵌套 DTO 无 @IsOptional,缺字段 400;
|
||||
// UI-01 新增字段(imageFit/offsetX/offsetBottom/zIndex)例外:全部
|
||||
// @IsOptional(旧客户端/旧 JSON 兼容),缺省由服务层按契约默认值归一;
|
||||
// · albumId [string,'null']:非 null 绑定/换绑(≤64,绑定存在性校验在
|
||||
// 服务层事务内),null 解绑。@IsOptional 对 undefined/null 均跳过校验
|
||||
// 且 null 值保留在 DTO 上——服务层以 `!== undefined` 区分「未提交」与
|
||||
@@ -35,6 +37,15 @@ const EMBED_SLOT_SIZES = ['sm', 'md'] as const;
|
||||
export const EMBED_INTERVAL_MS_MIN = 3000;
|
||||
export const EMBED_INTERVAL_MS_MAX = 30000;
|
||||
|
||||
/** UI-01 新增字段枚举与边界(全部可选:缺省由服务层归一化为默认值)。 */
|
||||
const EMBED_SLOT_IMAGE_FITS = ['contain', 'cover'] as const;
|
||||
export const EMBED_OFFSET_X_MIN = 0;
|
||||
export const EMBED_OFFSET_X_MAX = 120;
|
||||
export const EMBED_OFFSET_BOTTOM_MIN = 0;
|
||||
export const EMBED_OFFSET_BOTTOM_MAX = 240;
|
||||
export const EMBED_Z_INDEX_MIN = 1;
|
||||
export const EMBED_Z_INDEX_MAX = 2147483000;
|
||||
|
||||
export class EmbedSlotSettingsDto {
|
||||
@IsIn(EMBED_SLOT_POSITIONS)
|
||||
position!: (typeof EMBED_SLOT_POSITIONS)[number];
|
||||
@@ -52,6 +63,34 @@ export class EmbedSlotSettingsDto {
|
||||
|
||||
@IsBoolean()
|
||||
collapsedDefault!: boolean;
|
||||
|
||||
// —— UI-01 可选扩展(旧 JSON 兼容:缺省按默认值归一化落库)——
|
||||
|
||||
/** 图片适配方式;缺省 contain。 */
|
||||
@IsOptional()
|
||||
@IsIn(EMBED_SLOT_IMAGE_FITS)
|
||||
imageFit?: (typeof EMBED_SLOT_IMAGE_FITS)[number];
|
||||
|
||||
/** 距宿主边缘水平偏移(0..120px);缺省 16。 */
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(EMBED_OFFSET_X_MIN)
|
||||
@Max(EMBED_OFFSET_X_MAX)
|
||||
offsetX?: number;
|
||||
|
||||
/** 距宿主底部垂直偏移(0..240px);缺省 24。 */
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(EMBED_OFFSET_BOTTOM_MIN)
|
||||
@Max(EMBED_OFFSET_BOTTOM_MAX)
|
||||
offsetBottom?: number;
|
||||
|
||||
/** 宿主页面层级;缺省 1000。 */
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(EMBED_Z_INDEX_MIN)
|
||||
@Max(EMBED_Z_INDEX_MAX)
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
export class EmbedSlotCreateRequestDto {
|
||||
|
||||
@@ -52,13 +52,17 @@ export type EmbedSlotSettingsResponse =
|
||||
const PUBLIC_TOKEN_LENGTH = 24;
|
||||
const PUBLIC_TOKEN_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
/** 契约 EmbedSlotSettings 各字段缺省值(整体缺省时落库形态)。 */
|
||||
/** 契约 EmbedSlotSettings 各字段缺省值(整体缺省时落库形态;UI-01 新字段同列)。 */
|
||||
export const DEFAULT_EMBED_SLOT_SETTINGS: EmbedSlotSettingsResponse = {
|
||||
position: 'right',
|
||||
size: 'md',
|
||||
intervalMs: 5000,
|
||||
lang: 'auto',
|
||||
collapsedDefault: false,
|
||||
imageFit: 'contain',
|
||||
offsetX: 16,
|
||||
offsetBottom: 24,
|
||||
zIndex: 1000,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -82,7 +86,12 @@ function generatePublicToken(): string {
|
||||
return token;
|
||||
}
|
||||
|
||||
/** 入参 settings → 归一化存储值(null/undefined=整体缺省 → 契约缺省值)。 */
|
||||
/**
|
||||
* 入参 settings → 归一化存储值(null/undefined=整体缺省 → 契约缺省值)。
|
||||
* UI-01 新字段可缺省(旧客户端兼容):缺省按契约默认值归一,落库恒为全集
|
||||
* ——旧存储行(无新字段)的读取侧归一在 embed-ov 公开响应完成(cn 管理
|
||||
* 响应经本服务 toResponse 不做读归一,行由本函数写入即已全集)。
|
||||
*/
|
||||
function normalizeSettings(
|
||||
settings: EmbedSlotSettingsDto | null | undefined,
|
||||
): EmbedSlotSettingsResponse {
|
||||
@@ -95,6 +104,34 @@ function normalizeSettings(
|
||||
intervalMs: settings.intervalMs,
|
||||
lang: settings.lang,
|
||||
collapsedDefault: settings.collapsedDefault,
|
||||
imageFit: settings.imageFit ?? DEFAULT_EMBED_SLOT_SETTINGS.imageFit,
|
||||
offsetX: settings.offsetX ?? DEFAULT_EMBED_SLOT_SETTINGS.offsetX,
|
||||
offsetBottom:
|
||||
settings.offsetBottom ?? DEFAULT_EMBED_SLOT_SETTINGS.offsetBottom,
|
||||
zIndex: settings.zIndex ?? DEFAULT_EMBED_SLOT_SETTINGS.zIndex,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储行 settings JSON → 归一化全集(readOnly 读取侧,cn 管理响应与 ov 公开
|
||||
* 响应共用):UI-01 前落库的旧行无新字段,缺字段按契约默认值补齐——响应
|
||||
* 恒为全集(契约「响应中字段恒齐全」),不要求全量改写旧记录。
|
||||
*/
|
||||
export function normalizeStoredSettings(
|
||||
stored: Prisma.JsonValue,
|
||||
): EmbedSlotSettingsResponse {
|
||||
const row = (stored ?? {}) as Partial<EmbedSlotSettingsResponse>;
|
||||
return {
|
||||
position: row.position ?? DEFAULT_EMBED_SLOT_SETTINGS.position,
|
||||
size: row.size ?? DEFAULT_EMBED_SLOT_SETTINGS.size,
|
||||
intervalMs: row.intervalMs ?? DEFAULT_EMBED_SLOT_SETTINGS.intervalMs,
|
||||
lang: row.lang ?? DEFAULT_EMBED_SLOT_SETTINGS.lang,
|
||||
collapsedDefault:
|
||||
row.collapsedDefault ?? DEFAULT_EMBED_SLOT_SETTINGS.collapsedDefault,
|
||||
imageFit: row.imageFit ?? DEFAULT_EMBED_SLOT_SETTINGS.imageFit,
|
||||
offsetX: row.offsetX ?? DEFAULT_EMBED_SLOT_SETTINGS.offsetX,
|
||||
offsetBottom: row.offsetBottom ?? DEFAULT_EMBED_SLOT_SETTINGS.offsetBottom,
|
||||
zIndex: row.zIndex ?? DEFAULT_EMBED_SLOT_SETTINGS.zIndex,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -354,7 +391,8 @@ export class EmbedSlotsService {
|
||||
albumId: slot.albumId,
|
||||
albumTitle,
|
||||
configVersion: slot.configVersion,
|
||||
settings: slot.settings as EmbedSlotSettingsResponse,
|
||||
// 读取侧归一化:UI-01 前旧行缺新字段 → 契约默认值补齐(响应恒全集)。
|
||||
settings: normalizeStoredSettings(slot.settings),
|
||||
updatedAt: slot.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user