import { createRequestError } from './request-client.js' import { normalizeNormalDisableResponseStatus, normalizeOptionalNonnegativeInteger, normalizeOptionalNumericId, normalizeResponseText } from './response-normalizers.js' import { assertPlainPayload, normalizeOptionalNormalDisableStatus, normalizeOptionalSafeInteger, normalizeOptionalText, normalizeOssIdString } from './request-normalizers.js' import { normalizeBusinessFileAccess, normalizeResourceCapabilities } from './business-file-contract.js' export const PLATFORM_VIDEO_PLACEMENT = Object.freeze({ HOME_FEATURED: 'home_featured', VIDEO_CENTER: 'video_center', PROFILE_FEATURED: 'profile_featured' }) const platformVideoPlacements = new Set(Object.values(PLATFORM_VIDEO_PLACEMENT)) export const normalizePlatformVideoPlacement = (value) => { if (typeof value !== 'string' || !platformVideoPlacements.has(value.trim())) { throw new TypeError('平台视频投放位无效') } return value.trim() } const normalizePlatformVideo = (value, expectedPlacement) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw createRequestError('平台视频响应无效', 'PLATFORM_VIDEO_RESPONSE_INVALID') } const id = normalizeOptionalNumericId( value.platformVideoId, '平台视频标识', 'PLATFORM_VIDEO_RESPONSE_INVALID' ) const title = normalizeResponseText(value.videoTitle, 'videoTitle') if ( !id || !title || value.placement !== expectedPlacement || (value.platform !== 'app' && value.platform !== 'all') ) { throw createRequestError('平台视频响应缺少稳定字段', 'PLATFORM_VIDEO_RESPONSE_INVALID') } if (typeof value.likedByCurrentUser !== 'boolean') { throw createRequestError('平台视频点赞状态无效', 'PLATFORM_VIDEO_RESPONSE_INVALID') } return { id, platform: value.platform, placement: expectedPlacement, title, description: normalizeResponseText(value.videoDesc, 'videoDesc'), startAt: normalizeResponseText(value.startAt, 'startAt'), endAt: normalizeResponseText(value.endAt, 'endAt'), coverFile: normalizeBusinessFileAccess( value.coverFile, '平台视频封面', 'PLATFORM_VIDEO_RESPONSE_INVALID' ), videoFile: normalizeBusinessFileAccess( value.videoFile, '平台视频文件', 'PLATFORM_VIDEO_RESPONSE_INVALID', { required: true } ), durationSeconds: normalizeOptionalNonnegativeInteger( value.durationSeconds, '平台视频时长', 'PLATFORM_VIDEO_RESPONSE_INVALID' ), viewCount: normalizeOptionalNonnegativeInteger( value.viewCount, '平台视频播放量', 'PLATFORM_VIDEO_RESPONSE_INVALID' ), likeCount: normalizeOptionalNonnegativeInteger( value.likeCount, '平台视频点赞量', 'PLATFORM_VIDEO_RESPONSE_INVALID' ), commentCount: normalizeOptionalNonnegativeInteger( value.commentCount, '平台视频评论量', 'PLATFORM_VIDEO_RESPONSE_INVALID' ), likedByCurrentUser: value.likedByCurrentUser } } export const normalizePlatformVideos = (value, expectedPlacement) => { if (!Array.isArray(value)) { throw createRequestError('平台视频响应不是列表', 'PLATFORM_VIDEO_RESPONSE_INVALID') } const videos = value.map((item) => normalizePlatformVideo(item, expectedPlacement)) if (new Set(videos.map((item) => item.id)).size !== videos.length) { throw createRequestError('平台视频响应包含重复标识', 'PLATFORM_VIDEO_RESPONSE_INVALID') } return videos } export const normalizeAppVideo = (value, expectedGenealogyId, expectedVideoId = '') => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw createRequestError('家族视频响应无效', 'VIDEO_RESPONSE_INVALID') } const id = normalizeOptionalNumericId(value.videoId, '视频标识', 'VIDEO_RESPONSE_INVALID') const genealogyId = normalizeOptionalNumericId(value.genealogyId, '视频家谱标识', 'VIDEO_RESPONSE_INVALID') const title = normalizeResponseText(value.videoTitle, 'videoTitle') if (!id || genealogyId !== expectedGenealogyId || !title || (expectedVideoId && id !== expectedVideoId)) { throw createRequestError('家族视频响应缺少稳定字段', 'VIDEO_RESPONSE_INVALID') } if ( typeof value.canEdit !== 'boolean' || typeof value.canDelete !== 'boolean' || typeof value.likedByCurrentUser !== 'boolean' ) { throw createRequestError('家族视频响应缺少状态或权限字段', 'VIDEO_RESPONSE_INVALID') } return { id, genealogyId, title, description: normalizeResponseText(value.videoDesc, 'videoDesc'), coverFile: normalizeBusinessFileAccess(value.coverFile, '视频封面', 'VIDEO_RESPONSE_INVALID'), videoFile: normalizeBusinessFileAccess(value.videoFile, '视频文件', 'VIDEO_RESPONSE_INVALID', { required: true }), durationSeconds: normalizeOptionalNonnegativeInteger(value.durationSeconds, '视频时长', 'VIDEO_RESPONSE_INVALID'), publishTime: normalizeResponseText(value.publishTime, 'publishTime'), viewCount: normalizeOptionalNonnegativeInteger(value.viewCount, '视频播放量', 'VIDEO_RESPONSE_INVALID'), likeCount: normalizeOptionalNonnegativeInteger(value.likeCount, '视频点赞量', 'VIDEO_RESPONSE_INVALID'), commentCount: normalizeOptionalNonnegativeInteger(value.commentCount, '视频评论量', 'VIDEO_RESPONSE_INVALID'), likedByCurrentUser: value.likedByCurrentUser, sortOrder: normalizeOptionalNonnegativeInteger(value.sortOrder, '视频排序值', 'VIDEO_RESPONSE_INVALID'), status: normalizeNormalDisableResponseStatus(value.status, '视频状态', 'VIDEO_RESPONSE_INVALID'), canEdit: value.canEdit, canDelete: value.canDelete } } export const normalizeAppVideos = (value, expectedGenealogyId) => { if (!Array.isArray(value)) throw createRequestError('家族视频响应不是列表', 'VIDEO_RESPONSE_INVALID') const videos = value.map((item) => normalizeAppVideo(item, expectedGenealogyId)) if (new Set(videos.map((item) => item.id)).size !== videos.length) { throw createRequestError('家族视频响应包含重复标识', 'VIDEO_RESPONSE_INVALID') } return videos } export const normalizeAppAlbum = (value, expectedGenealogyId) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw createRequestError('相册响应无效', 'ALBUM_RESPONSE_INVALID') } const id = normalizeOptionalNumericId(value.albumId, '相册标识', 'ALBUM_RESPONSE_INVALID') const genealogyId = normalizeOptionalNumericId(value.genealogyId, '相册家谱标识', 'ALBUM_RESPONSE_INVALID') if (!id || genealogyId !== expectedGenealogyId) { throw createRequestError('相册响应缺少稳定归属字段', 'ALBUM_RESPONSE_INVALID') } return { id, genealogyId, name: normalizeResponseText(value.albumName, 'albumName') || '未命名相册', description: normalizeResponseText(value.albumDesc, 'albumDesc'), coverFile: normalizeBusinessFileAccess(value.coverFile, '相册封面', 'ALBUM_RESPONSE_INVALID'), photoCount: normalizeOptionalNonnegativeInteger(value.photoCount, '相册照片数量', 'ALBUM_RESPONSE_INVALID'), createTime: normalizeResponseText(value.createTime, 'createTime'), sortOrder: normalizeOptionalNonnegativeInteger(value.sortOrder, '相册排序值', 'ALBUM_RESPONSE_INVALID'), status: normalizeNormalDisableResponseStatus(value.status, '相册状态', 'ALBUM_RESPONSE_INVALID'), ...normalizeResourceCapabilities(value, '相册', 'ALBUM_RESPONSE_INVALID') } } export const normalizeAppAlbums = (value, expectedGenealogyId) => { if (!Array.isArray(value)) throw createRequestError('相册响应不是列表', 'ALBUM_RESPONSE_INVALID') const albums = value.map((item) => normalizeAppAlbum(item, expectedGenealogyId)) if (new Set(albums.map((item) => item.id)).size !== albums.length) { throw createRequestError('相册响应包含重复标识', 'ALBUM_RESPONSE_INVALID') } return albums } const normalizeAppAlbumPhoto = (value, expectedGenealogyId, expectedAlbumId) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw createRequestError('相册照片响应无效', 'ALBUM_PHOTO_RESPONSE_INVALID') } const id = normalizeOptionalNumericId(value.photoId, '照片标识', 'ALBUM_PHOTO_RESPONSE_INVALID') const genealogyId = normalizeOptionalNumericId(value.genealogyId, '照片家谱标识', 'ALBUM_PHOTO_RESPONSE_INVALID') const albumId = normalizeOptionalNumericId(value.albumId, '照片相册标识', 'ALBUM_PHOTO_RESPONSE_INVALID') if (!id || genealogyId !== expectedGenealogyId || albumId !== expectedAlbumId) { throw createRequestError('相册照片响应缺少稳定归属字段', 'ALBUM_PHOTO_RESPONSE_INVALID') } return { id, genealogyId, albumId, title: normalizeResponseText(value.photoTitle, 'photoTitle') || '未命名照片', description: normalizeResponseText(value.photoDesc, 'photoDesc'), photographer: normalizeResponseText(value.photographer, 'photographer'), shootTime: normalizeResponseText(value.shootTime, 'shootTime'), photoFile: normalizeBusinessFileAccess(value.photoFile, '相册照片', 'ALBUM_PHOTO_RESPONSE_INVALID', { required: true }), ...normalizeResourceCapabilities(value, '相册照片', 'ALBUM_PHOTO_RESPONSE_INVALID') } } export const normalizeAppAlbumPhotos = (value, expectedGenealogyId, expectedAlbumId) => { if (!Array.isArray(value)) throw createRequestError('相册照片响应不是列表', 'ALBUM_PHOTO_RESPONSE_INVALID') const photos = value.map((item) => normalizeAppAlbumPhoto(item, expectedGenealogyId, expectedAlbumId)) if (new Set(photos.map((item) => item.id)).size !== photos.length) { throw createRequestError('相册照片响应包含重复标识', 'ALBUM_PHOTO_RESPONSE_INVALID') } return photos } export const normalizeAlbumCreatePayload = (payload) => { const allowedFields = new Set(['albumName', 'albumDesc', 'coverOssId', 'sortOrder', 'status']) assertPlainPayload(payload, allowedFields, '相册请求') if (typeof payload.albumName !== 'string' || !payload.albumName.trim()) { throw new TypeError('相册名称不能为空') } const normalizedPayload = { albumName: payload.albumName.trim() } const albumDesc = normalizeOptionalText(payload.albumDesc, 'albumDesc') if (albumDesc) normalizedPayload.albumDesc = albumDesc const status = normalizeOptionalNormalDisableStatus(payload.status, '相册状态') if (status) normalizedPayload.status = status if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') { normalizedPayload.coverOssId = normalizeOssIdString(payload.coverOssId, 'coverOssId') } if (payload.sortOrder !== undefined && payload.sortOrder !== null && payload.sortOrder !== '') { const sortOrder = typeof payload.sortOrder === 'number' ? payload.sortOrder : Number(payload.sortOrder) if (!Number.isSafeInteger(sortOrder)) throw new TypeError('sortOrder 必须是安全整数') normalizedPayload.sortOrder = sortOrder } return normalizedPayload } export const normalizeAlbumPhotoCreatePayload = (payload) => { const allowedFields = new Set(['ossId', 'photoTitle', 'photoDesc', 'photographer', 'shootTime', 'sortOrder', 'status']) assertPlainPayload(payload, allowedFields, '相册照片请求') const ossId = normalizeOssIdString(payload.ossId, '相册照片 ossId') const normalizedPayload = { ossId } for (const field of ['photoTitle', 'photoDesc', 'photographer', 'shootTime']) { const normalizedText = normalizeOptionalText(payload[field], field) if (normalizedText) normalizedPayload[field] = normalizedText } const status = normalizeOptionalNormalDisableStatus(payload.status, '照片状态') if (status) normalizedPayload.status = status const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder') if (sortOrder !== undefined) normalizedPayload.sortOrder = sortOrder return normalizedPayload } export const normalizeVideoPayload = (payload) => { const allowedFields = new Set(['videoTitle', 'videoDesc', 'coverOssId', 'videoOssId', 'durationSeconds', 'sortOrder', 'status']) assertPlainPayload(payload, allowedFields, '视频请求') const videoTitle = normalizeOptionalText(payload.videoTitle, '视频标题') if (!videoTitle) throw new TypeError('视频标题不能为空') const videoOssId = normalizeOssIdString(payload.videoOssId, '视频文件 OSS ID') const normalizedPayload = { videoTitle, videoOssId } const videoDesc = normalizeOptionalText(payload.videoDesc, '视频说明') if (videoDesc) normalizedPayload.videoDesc = videoDesc if (Object.prototype.hasOwnProperty.call(payload, 'coverOssId')) { if (payload.coverOssId === null) normalizedPayload.coverOssId = null else if (payload.coverOssId !== '') normalizedPayload.coverOssId = normalizeOssIdString(payload.coverOssId, '视频封面 OSS ID') } for (const field of ['durationSeconds', 'sortOrder']) { const normalizedInteger = normalizeOptionalSafeInteger(payload[field], field) if (normalizedInteger !== undefined) normalizedPayload[field] = normalizedInteger } const status = normalizeOptionalNormalDisableStatus(payload.status, '视频状态') if (status) normalizedPayload.status = status return normalizedPayload }