279 lines
13 KiB
JavaScript
279 lines
13 KiB
JavaScript
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
|
|
}
|