Files
jiapuapp/services/api/family-feed-contract.js

253 lines
11 KiB
JavaScript

import { createRequestError } from './request-client.js'
import {
normalizeNormalDisableResponseStatus,
normalizeOptionalNonnegativeInteger,
normalizeOptionalNumericId,
normalizeResponseText
} from './response-normalizers.js'
import {
assertPlainPayload,
normalizeOptionalNormalDisableStatus,
normalizeOptionalOssIdList,
normalizeOptionalText
} from './request-normalizers.js'
import {
normalizeBusinessFileAccessRows,
normalizeResourceCapabilities
} from './business-file-contract.js'
export const normalizeFeedResourceId = (value, label) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw createRequestError(`家族动态评论${label}无效`, 'FEED_COMMENT_RESPONSE_INVALID')
}
const normalizeFeedCommentText = (value, label, { required = false } = {}) => {
if (value === undefined || value === null) {
if (required) throw createRequestError(`家族动态评论缺少${label}`, 'FEED_COMMENT_RESPONSE_INVALID')
return ''
}
if (typeof value !== 'string') {
throw createRequestError(`家族动态评论${label}无效`, 'FEED_COMMENT_RESPONSE_INVALID')
}
const normalized = value.trim()
if (required && !normalized) {
throw createRequestError(`家族动态评论缺少${label}`, 'FEED_COMMENT_RESPONSE_INVALID')
}
return normalized
}
export const FEED_COMMENT_LEVEL = Object.freeze({
ROOT: 'root',
REPLY: 'reply'
})
const feedCommentLevels = new Set(Object.values(FEED_COMMENT_LEVEL))
const normalizeFeedParentCommentId = (value) => {
if (value === undefined || value === null || value === 0) return null
return normalizeFeedResourceId(value, '父评论标识')
}
export const normalizeFeedComments = (
value,
expectedGenealogyId,
expectedFeedId,
expectedLevel = ''
) => {
if (expectedLevel && !feedCommentLevels.has(expectedLevel)) {
throw new TypeError('预期评论层级无效')
}
if (!Array.isArray(value)) {
throw createRequestError('家族动态评论响应不是列表', 'FEED_COMMENT_RESPONSE_INVALID')
}
const comments = value.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('家族动态评论包含无效条目', 'FEED_COMMENT_RESPONSE_INVALID')
}
const genealogyId = normalizeFeedResourceId(item.genealogyId, '家谱标识')
const feedId = normalizeFeedResourceId(item.feedId, '动态标识')
if (genealogyId !== expectedGenealogyId || feedId !== expectedFeedId) {
throw createRequestError('家族动态评论归属与请求不匹配', 'FEED_COMMENT_RESPONSE_INVALID')
}
const level = normalizeFeedCommentText(item.commentLevel, '评论层级', { required: true })
if (!feedCommentLevels.has(level) || (expectedLevel && level !== expectedLevel)) {
throw createRequestError('家族动态评论层级无效', 'FEED_COMMENT_RESPONSE_INVALID')
}
const parentCommentId = normalizeFeedParentCommentId(item.parentCommentId)
if (
(level === FEED_COMMENT_LEVEL.ROOT && parentCommentId !== null) ||
(level === FEED_COMMENT_LEVEL.REPLY && parentCommentId === null)
) {
throw createRequestError('家族动态评论父级与层级不匹配', 'FEED_COMMENT_RESPONSE_INVALID')
}
const deletedStatus = item.userDeleted === undefined || item.userDeleted === null
? '0'
: item.userDeleted
if (deletedStatus !== '0' && deletedStatus !== '1') {
throw createRequestError('家族动态评论删除状态无效', 'FEED_COMMENT_RESPONSE_INVALID')
}
const userDeleted = deletedStatus === '1'
if (item.canDelete !== undefined && typeof item.canDelete !== 'boolean') {
throw createRequestError('家族动态评论删除权限无效', 'FEED_COMMENT_RESPONSE_INVALID')
}
return {
id: normalizeFeedResourceId(item.commentId, '标识'),
author: normalizeFeedCommentText(item.appUserNickName, '用户昵称') || '未署名成员',
content: userDeleted
? '该评论已删除'
: normalizeFeedCommentText(item.commentContent, '评论内容', { required: true }),
time: normalizeFeedCommentText(item.createTime, '创建时间'),
parentCommentId,
parentAuthor: normalizeFeedCommentText(item.parentAppUserNickName, '被回复用户昵称'),
level,
status: normalizeNormalDisableResponseStatus(
item.status,
'评论状态',
'FEED_COMMENT_RESPONSE_INVALID'
),
userDeleted,
canDelete: item.canDelete === true,
replyCount: normalizeOptionalNonnegativeInteger(
item.replyCount,
'评论回复数',
'FEED_COMMENT_RESPONSE_INVALID'
)
}
})
if (new Set(comments.map((item) => item.id)).size !== comments.length) {
throw createRequestError('家族动态评论包含重复标识', 'FEED_COMMENT_RESPONSE_INVALID')
}
return comments
}
export const normalizeDirectFeedReplies = (value, expectedGenealogyId, expectedFeedId, expectedParentCommentId) => {
const replies = normalizeFeedComments(
value,
expectedGenealogyId,
expectedFeedId,
FEED_COMMENT_LEVEL.REPLY
)
if (replies.some((item) => item.parentCommentId !== expectedParentCommentId)) {
throw createRequestError('评论回复父级与请求不匹配', 'FEED_COMMENT_RESPONSE_INVALID')
}
return replies
}
export const normalizeFeedCommentPayload = (payload) => {
assertPlainPayload(payload, new Set(['parentCommentId', 'commentContent']), '家族动态评论请求')
const commentContent = normalizeFeedCommentText(payload.commentContent, '评论内容', { required: true })
if (Array.from(commentContent).length > 1000) throw new TypeError('家族动态评论不能超过 1000 个字符')
const normalizedPayload = { commentContent }
if (payload.parentCommentId !== undefined && payload.parentCommentId !== null) {
normalizedPayload.parentCommentId = normalizeFeedResourceId(payload.parentCommentId, '父评论标识')
}
return normalizedPayload
}
export const normalizeFeedCreatePayload = (payload) => {
const allowedFields = new Set(['feedType', 'feedContent', 'mediaOssIds', 'sortOrder', 'status'])
assertPlainPayload(payload, allowedFields, '家族动态请求')
if (typeof payload.feedContent !== 'string' || !payload.feedContent.trim()) {
throw new TypeError('家族动态内容不能为空')
}
const normalizedPayload = { feedContent: payload.feedContent.trim() }
const feedType = normalizeOptionalText(payload.feedType, 'feedType')
if (feedType) normalizedPayload.feedType = feedType
const status = normalizeOptionalNormalDisableStatus(payload.status, '动态状态')
if (status) normalizedPayload.status = status
const mediaOssIds = normalizeOptionalOssIdList(payload.mediaOssIds)
if (mediaOssIds) normalizedPayload.mediaOssIds = mediaOssIds
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 normalizeFamilyFeedRows = (value, expectedGenealogyId) => {
if (!Array.isArray(value)) {
throw createRequestError('家族动态响应不是列表', 'FEED_RESPONSE_INVALID')
}
const rows = value.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('家族动态响应包含无效条目', 'FEED_RESPONSE_INVALID')
}
const genealogyId = normalizeOptionalNumericId(item.genealogyId, '动态家谱标识', 'FEED_RESPONSE_INVALID')
if (genealogyId && genealogyId !== expectedGenealogyId) {
throw createRequestError('家族动态归属与请求不匹配', 'FEED_RESPONSE_INVALID')
}
const id = normalizeOptionalNumericId(item.feedId, '动态标识', 'FEED_RESPONSE_INVALID')
const content = normalizeResponseText(item.feedContent, 'feedContent')
if (!id || !content) {
throw createRequestError('家族动态响应缺少标识或内容', 'FEED_RESPONSE_INVALID')
}
if (
item.likedByMe !== undefined &&
item.likedByMe !== null &&
typeof item.likedByMe !== 'boolean'
) {
throw createRequestError('动态点赞状态无效', 'FEED_RESPONSE_INVALID')
}
const pinned = normalizeResponseText(item.pinned, 'pinned')
if (pinned && pinned !== '0' && pinned !== '1') {
throw createRequestError('动态置顶状态无效', 'FEED_RESPONSE_INVALID')
}
return {
id,
genealogyId: expectedGenealogyId,
content,
publisher: normalizeResponseText(item.publisherNickName, 'publisherNickName') || '家族成员',
type: normalizeResponseText(item.feedType, 'feedType') || '文字动态',
time: normalizeResponseText(item.createTime, 'createTime'),
mediaFiles: normalizeBusinessFileAccessRows(item.mediaFiles, '动态媒体', 'FEED_RESPONSE_INVALID'),
likeCount: normalizeOptionalNonnegativeInteger(item.likeCount, '动态点赞数', 'FEED_RESPONSE_INVALID'),
commentCount: normalizeOptionalNonnegativeInteger(item.commentCount, '动态评论数', 'FEED_RESPONSE_INVALID'),
likedByMe: item.likedByMe ?? null,
pinned,
sortOrder: normalizeOptionalNonnegativeInteger(item.sortOrder, '动态排序值', 'FEED_RESPONSE_INVALID'),
status: normalizeNormalDisableResponseStatus(item.status, '动态状态', 'FEED_RESPONSE_INVALID'),
...normalizeResourceCapabilities(item, '家族动态', 'FEED_RESPONSE_INVALID')
}
})
if (new Set(rows.map((item) => item.id)).size !== rows.length) {
throw createRequestError('家族动态响应包含重复标识', 'FEED_RESPONSE_INVALID')
}
return rows
}
export const normalizeFamilyFeed = (value, expectedGenealogyId, expectedFeedId) => {
const [feed] = normalizeFamilyFeedRows([value], expectedGenealogyId)
if (feed.id !== expectedFeedId) {
throw createRequestError('动态详情响应标识不匹配', 'FEED_RESPONSE_INVALID')
}
return feed
}
export const normalizeFamilyFeedRecommendations = (value, expectedGenealogyId) => {
if (!Array.isArray(value)) {
throw createRequestError('动态推荐响应不是列表', 'FEED_RECOMMENDATION_RESPONSE_INVALID')
}
const reasonCodes = new Set([
'MANUAL_PRIORITY',
'FOLLOWED_INTERACTION',
'POPULAR_IN_GENEALOGY',
'RECENT_IN_GENEALOGY'
])
const feeds = value.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item) || !reasonCodes.has(item.reasonCode)) {
throw createRequestError('动态推荐响应包含无效条目', 'FEED_RECOMMENDATION_RESPONSE_INVALID')
}
if (typeof item.reasonText !== 'string' || !item.reasonText.trim()) {
throw createRequestError('动态推荐响应缺少说明', 'FEED_RECOMMENDATION_RESPONSE_INVALID')
}
const [feed] = normalizeFamilyFeedRows([item.feed], expectedGenealogyId)
return { ...feed, recommendationReason: item.reasonText.trim() }
})
if (new Set(feeds.map((item) => item.id)).size !== feeds.length) {
throw createRequestError('动态推荐响应包含重复动态', 'FEED_RECOMMENDATION_RESPONSE_INVALID')
}
return feeds
}