import { createRequestError } from './request-client.js' import { normalizeNormalDisableResponseStatus, normalizeOptionalCurrencyAmount, normalizeOptionalNonnegativeInteger, normalizeOptionalNumericId, normalizeResponseText } from './response-normalizers.js' import { assertPlainPayload, normalizeOptionalNormalDisableStatus, normalizeOptionalSafeInteger, normalizeOptionalText, normalizeOssIdString, normalizeResourcePathId } from './request-normalizers.js' import { normalizeBusinessFileAccess, normalizeResourceCapabilities } from './business-file-contract.js' import { normalizeGenealogyPathId } from './genealogy-contract.js' const normalizeAppCeremonyCoordinate = (value, field) => { if (value === undefined || value === null || value === '') return null const coordinate = typeof value === 'number' ? value : Number(value) if (!Number.isFinite(coordinate)) { throw createRequestError(`礼仪活动响应字段 ${field} 无效`, 'CEREMONY_RESPONSE_INVALID') } return coordinate } const CEREMONY_TYPE_LABELS = Object.freeze({ ancestor: '祭祖', memorial: '纪念', funeral: '丧礼', wedding: '婚礼', birth: '出生宴', birthday: '生日宴', banquet: '宴席', other: '其他' }) const ceremonyTypes = new Set(Object.keys(CEREMONY_TYPE_LABELS)) export const CEREMONY_INVITATION_STATUS = Object.freeze({ PENDING: 'PENDING', ACCEPTED: 'ACCEPTED', DECLINED: 'DECLINED', CANCELED: 'CANCELED' }) export const CEREMONY_INVITATION_STATUS_LABELS = Object.freeze({ [CEREMONY_INVITATION_STATUS.PENDING]: '等待回应', [CEREMONY_INVITATION_STATUS.ACCEPTED]: '已接受', [CEREMONY_INVITATION_STATUS.DECLINED]: '已拒绝', [CEREMONY_INVITATION_STATUS.CANCELED]: '已取消' }) const ceremonyInvitationStatuses = new Set( Object.values(CEREMONY_INVITATION_STATUS) ) const ceremonyInvitationResponseStatuses = new Set([ CEREMONY_INVITATION_STATUS.ACCEPTED, CEREMONY_INVITATION_STATUS.DECLINED ]) export const normalizeAppCeremony = (value, expectedGenealogyId, expectedCeremonyId = '') => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw createRequestError('礼仪活动响应无效', 'CEREMONY_RESPONSE_INVALID') } const id = normalizeOptionalNumericId(value.ceremonyId, '礼仪活动标识', 'CEREMONY_RESPONSE_INVALID') const genealogyId = normalizeOptionalNumericId(value.genealogyId, '礼仪活动家谱标识', 'CEREMONY_RESPONSE_INVALID') if (!id || genealogyId !== expectedGenealogyId || (expectedCeremonyId && id !== expectedCeremonyId)) { throw createRequestError('礼仪活动响应缺少稳定归属字段', 'CEREMONY_RESPONSE_INVALID') } const location = normalizeResponseText(value.location, 'location') const locationAddress = normalizeResponseText(value.locationAddress, 'locationAddress') const longitude = normalizeAppCeremonyCoordinate(value.longitude, 'longitude') const latitude = normalizeAppCeremonyCoordinate(value.latitude, 'latitude') if ((longitude === null) !== (latitude === null)) { throw createRequestError('礼仪活动响应的经纬度必须同时存在或同时为空', 'CEREMONY_RESPONSE_INVALID') } return { id, genealogyId, title: normalizeResponseText(value.ceremonyTitle, 'ceremonyTitle') || '未命名活动', type: normalizeResponseText(value.ceremonyType, 'ceremonyType'), time: normalizeResponseText(value.ceremonyTime, 'ceremonyTime'), location, locationAddress, longitude, latitude, description: normalizeResponseText(value.ceremonyDesc, 'ceremonyDesc'), coverFile: normalizeBusinessFileAccess(value.coverFile, '礼仪活动封面', 'CEREMONY_RESPONSE_INVALID'), giftCount: normalizeOptionalNonnegativeInteger(value.giftCount, '礼仪献礼数量', 'CEREMONY_RESPONSE_INVALID'), sortOrder: normalizeOptionalNonnegativeInteger(value.sortOrder, '礼仪活动排序值', 'CEREMONY_RESPONSE_INVALID'), status: normalizeNormalDisableResponseStatus(value.status, '礼仪活动状态', 'CEREMONY_RESPONSE_INVALID'), ...normalizeResourceCapabilities(value, '礼仪活动', 'CEREMONY_RESPONSE_INVALID') } } export const normalizeAppCeremonyWithTypeLabel = (ceremony) => { const typeLabel = CEREMONY_TYPE_LABELS[ceremony.type] if (!ceremony.type || !typeLabel) { throw createRequestError('礼仪活动响应包含未公开的活动类型', 'CEREMONY_RESPONSE_INVALID') } return { ...ceremony, typeLabel } } export const normalizeAppCeremonies = (value, expectedGenealogyId) => { if (!Array.isArray(value)) throw createRequestError('礼仪活动响应不是列表', 'CEREMONY_RESPONSE_INVALID') const ceremonies = value.map((item) => normalizeAppCeremony(item, expectedGenealogyId)) if (new Set(ceremonies.map((item) => item.id)).size !== ceremonies.length) { throw createRequestError('礼仪活动响应包含重复标识', 'CEREMONY_RESPONSE_INVALID') } return ceremonies.map((item) => normalizeAppCeremonyWithTypeLabel(item)) } export const normalizeAppCeremonyGift = (value, expectedGenealogyId, expectedCeremonyId) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw createRequestError('礼仪献礼响应无效', 'CEREMONY_GIFT_RESPONSE_INVALID') } const id = normalizeOptionalNumericId(value.giftId, '献礼标识', 'CEREMONY_GIFT_RESPONSE_INVALID') const genealogyId = normalizeOptionalNumericId(value.genealogyId, '献礼家谱标识', 'CEREMONY_GIFT_RESPONSE_INVALID') const ceremonyId = normalizeOptionalNumericId(value.ceremonyId, '献礼活动标识', 'CEREMONY_GIFT_RESPONSE_INVALID') if (!id || genealogyId !== expectedGenealogyId || ceremonyId !== expectedCeremonyId) { throw createRequestError('礼仪献礼响应缺少稳定归属字段', 'CEREMONY_GIFT_RESPONSE_INVALID') } return { id, genealogyId, ceremonyId, giverName: normalizeResponseText(value.giverName, 'giverName'), giverNickName: normalizeResponseText(value.giverNickName, 'giverNickName'), amount: normalizeOptionalCurrencyAmount(value.giftAmount, '献礼金额', 'CEREMONY_GIFT_RESPONSE_INVALID', { required: true }), message: normalizeResponseText(value.giftMessage, 'giftMessage'), time: normalizeResponseText(value.giftTime, 'giftTime'), ...normalizeResourceCapabilities(value, '礼仪献礼', 'CEREMONY_GIFT_RESPONSE_INVALID') } } export const normalizeAppCeremonyGifts = (value, expectedGenealogyId, expectedCeremonyId) => { if (!Array.isArray(value)) throw createRequestError('礼仪献礼响应不是列表', 'CEREMONY_GIFT_RESPONSE_INVALID') const gifts = value.map((item) => normalizeAppCeremonyGift(item, expectedGenealogyId, expectedCeremonyId)) if (new Set(gifts.map((item) => item.id)).size !== gifts.length) { throw createRequestError('礼仪献礼响应包含重复标识', 'CEREMONY_GIFT_RESPONSE_INVALID') } return gifts } export const projectPreviewCeremonies = (value, expectedGenealogyId) => value.map((item) => ({ id: String(item.ceremonyId), genealogyId: expectedGenealogyId, title: String(item.ceremonyTitle || '未命名活动'), type: String(item.ceremonyType || ''), typeLabel: CEREMONY_TYPE_LABELS[String(item.ceremonyType || '')] || '', time: String(item.ceremonyTime || ''), location: String(item.location || ''), description: String(item.ceremonyDesc || ''), coverFile: null, giftCount: Number.isSafeInteger(item.giftCount) ? item.giftCount : 0, canEdit: false, canDelete: false })) export const normalizeCeremonyPayload = (payload) => { const allowedFields = new Set(['ceremonyType', 'ceremonyTitle', 'ceremonyDesc', 'ceremonyTime', 'location', 'locationAddress', 'longitude', 'latitude', 'coverOssId', 'sortOrder', 'status']) assertPlainPayload(payload, allowedFields, '礼仪活动请求') const ceremonyType = normalizeOptionalText(payload.ceremonyType, 'ceremonyType') const ceremonyTitle = normalizeOptionalText(payload.ceremonyTitle, 'ceremonyTitle') if (!ceremonyType || !ceremonyTitle) throw new TypeError('请填写活动类型和活动标题') if (!ceremonyTypes.has(ceremonyType)) throw new TypeError('请选择有效的活动类型') const normalizedPayload = { ceremonyType, ceremonyTitle } for (const field of ['ceremonyDesc', 'ceremonyTime', 'location', 'locationAddress']) { const normalizedText = normalizeOptionalText(payload[field], field) if (normalizedText) normalizedPayload[field] = normalizedText } const status = normalizeOptionalNormalDisableStatus(payload.status, '礼仪活动状态') if (status) normalizedPayload.status = status const hasLongitude = payload.longitude !== undefined && payload.longitude !== null && payload.longitude !== '' const hasLatitude = payload.latitude !== undefined && payload.latitude !== null && payload.latitude !== '' if (hasLongitude !== hasLatitude) throw new TypeError('longitude 和 latitude 必须同时提供') if (hasLongitude) { const longitude = typeof payload.longitude === 'number' ? payload.longitude : Number(payload.longitude) const latitude = typeof payload.latitude === 'number' ? payload.latitude : Number(payload.latitude) if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) { throw new TypeError('longitude 和 latitude 必须是有限数字') } normalizedPayload.longitude = longitude normalizedPayload.latitude = latitude } if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') { normalizedPayload.coverOssId = normalizeOssIdString(payload.coverOssId, 'coverOssId') } const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder') if (sortOrder !== undefined) { normalizedPayload.sortOrder = sortOrder } return normalizedPayload } export const normalizeCeremonyGiftPayload = (payload) => { const allowedFields = new Set(['giverName', 'giftAmount', 'giftMessage']) assertPlainPayload(payload, allowedFields, '礼仪献礼请求') if (payload.giftAmount === undefined || payload.giftAmount === null || payload.giftAmount === '') { throw new TypeError('请填写献礼金额') } const giftAmount = typeof payload.giftAmount === 'number' ? payload.giftAmount : Number(payload.giftAmount) if (!Number.isFinite(giftAmount)) throw new TypeError('献礼金额必须是有限数字') const normalizedPayload = { giftAmount } for (const field of ['giverName', 'giftMessage']) { const normalizedText = normalizeOptionalText(payload[field], field) if (normalizedText) normalizedPayload[field] = normalizedText } return normalizedPayload } export const normalizeCeremonyInviteePayload = (payload) => { assertPlainPayload(payload, new Set(['inviteeUserIds']), '活动受邀人请求') if (!Array.isArray(payload.inviteeUserIds)) { throw new TypeError('inviteeUserIds必须是数组') } const inviteeUserIds = payload.inviteeUserIds.map((value) => normalizeResourcePathId(value, '受邀业务用户标识') ) if (new Set(inviteeUserIds).size !== inviteeUserIds.length) { throw new TypeError('inviteeUserIds不能包含重复标识') } return { inviteeUserIds } } export const normalizeCeremonyInvitationResponsePayload = (payload) => { assertPlainPayload(payload, new Set(['inviteStatus']), '活动邀请响应请求') if (!ceremonyInvitationResponseStatuses.has(payload.inviteStatus)) { throw new TypeError('inviteStatus仅允许ACCEPTED或DECLINED') } return { inviteStatus: payload.inviteStatus } } const normalizeCeremonyInvitationText = (value, field) => { if (value === undefined || value === null) return '' if (typeof value !== 'string') { throw createRequestError(`活动邀请字段 ${field} 无效`, 'CEREMONY_INVITATION_RESPONSE_INVALID') } return value.trim() } export const normalizeCeremonyInvitationRows = (value, expectedGenealogyId = '') => { if (!Array.isArray(value)) { throw createRequestError('活动邀请响应不是数组', 'CEREMONY_INVITATION_RESPONSE_INVALID') } const seenIds = new Set() return value.map((item) => { if (!item || typeof item !== 'object' || Array.isArray(item)) { throw createRequestError('活动邀请包含无效条目', 'CEREMONY_INVITATION_RESPONSE_INVALID') } const id = normalizeResourcePathId(item.invitationId, '活动邀请标识') if (seenIds.has(id)) { throw createRequestError('活动邀请包含重复标识', 'CEREMONY_INVITATION_RESPONSE_INVALID') } seenIds.add(id) const genealogyId = normalizeGenealogyPathId(item.genealogyId) if (expectedGenealogyId && genealogyId !== expectedGenealogyId) { throw createRequestError('活动邀请家谱归属与请求不匹配', 'CEREMONY_INVITATION_RESPONSE_INVALID') } const ceremonyId = normalizeResourcePathId(item.ceremonyId, '礼仪活动标识') const userKey = normalizeResourcePathId(item.inviteeUserId, '受邀业务用户标识') const inviteStatus = normalizeCeremonyInvitationText(item.inviteStatus, 'inviteStatus') if (!ceremonyInvitationStatuses.has(inviteStatus)) { throw createRequestError('活动邀请状态无效', 'CEREMONY_INVITATION_RESPONSE_INVALID') } const inviteVersion = item.inviteVersion if (inviteVersion !== undefined && (!Number.isSafeInteger(inviteVersion) || inviteVersion < 1)) { throw createRequestError('活动邀请版本无效', 'CEREMONY_INVITATION_RESPONSE_INVALID') } return { id, genealogyId, ceremonyId, userKey, inviteStatus, inviteVersion: inviteVersion === undefined ? null : inviteVersion, deliveredTime: normalizeCeremonyInvitationText(item.deliveredTime, 'deliveredTime'), readTime: normalizeCeremonyInvitationText(item.readTime, 'readTime'), responseTime: normalizeCeremonyInvitationText(item.responseTime, 'responseTime'), ceremonyTitle: normalizeCeremonyInvitationText(item.ceremonyTitle, 'ceremonyTitle'), ceremonyTime: normalizeCeremonyInvitationText(item.ceremonyTime, 'ceremonyTime'), location: normalizeCeremonyInvitationText(item.location, 'location'), locationAddress: normalizeCeremonyInvitationText(item.locationAddress, 'locationAddress') } }) } export const normalizeCeremonyInviteeOptions = (value) => { if (!Array.isArray(value)) { throw createRequestError('活动受邀候选响应不是数组', 'CEREMONY_INVITEE_OPTIONS_RESPONSE_INVALID') } const seenUserIds = new Set() return value.map((item) => { if (!item || typeof item !== 'object' || Array.isArray(item)) { throw createRequestError('活动受邀候选包含无效条目', 'CEREMONY_INVITEE_OPTIONS_RESPONSE_INVALID') } const memberId = normalizeResourcePathId(item.memberId, '受邀成员标识') const appUserId = normalizeResourcePathId(item.appUserId, '受邀业务用户标识') if (seenUserIds.has(appUserId)) { throw createRequestError('活动受邀候选包含重复业务用户', 'CEREMONY_INVITEE_OPTIONS_RESPONSE_INVALID') } seenUserIds.add(appUserId) const displayName = normalizeCeremonyInvitationText(item.displayName, 'displayName') if (!displayName) { throw createRequestError('活动受邀候选缺少显示名称', 'CEREMONY_INVITEE_OPTIONS_RESPONSE_INVALID') } if (typeof item.eligible !== 'boolean') { throw createRequestError('活动受邀候选 eligible 无效', 'CEREMONY_INVITEE_OPTIONS_RESPONSE_INVALID') } return { memberId, appUserId, displayName, memberRole: normalizeCeremonyInvitationText(item.memberRole, 'memberRole'), eligible: item.eligible, disabledReason: normalizeCeremonyInvitationText(item.disabledReason, 'disabledReason') } }) }