import { fromApiGenealogyAccess, isGenealogyJoinMode, isGenealogyVisibility } from '@/utils/genealogy/access-policy.js' import { normalizeBusinessFileAccess } from './business-file-contract.js' import { createRequestError } from './request-client.js' import { assertPlainPayload, normalizeOssIdString } from './request-normalizers.js' import { normalizeOptionalNonnegativeInteger, normalizeResponseText } from './response-normalizers.js' const normalizeGenealogyResponseText = (value, label) => normalizeResponseText(value, label, { code: 'GENEALOGY_RESPONSE_INVALID', subject: '我的家谱响应' }) const normalizeGenealogySettingsResponseText = (value, label) => normalizeResponseText(value, label, { code: 'GENEALOGY_SETTINGS_RESPONSE_INVALID', subject: '家谱设置响应' }) export const GENEALOGY_LIFECYCLE_STATUS = Object.freeze({ NORMAL: 'normal', ARCHIVED: 'archived', DELETE_PENDING: 'delete_pending' }) const genealogyLifecycleStatuses = new Set( Object.values(GENEALOGY_LIFECYCLE_STATUS) ) const normalizeGenealogyLifecycleStatus = (value, code) => { const status = normalizeResponseText(value, 'lifecycleStatus', { code, subject: '家谱响应' }) if (!genealogyLifecycleStatuses.has(status)) { throw createRequestError('家谱生命周期状态无效', code) } return status } const normalizeGenealogyAccessValue = (value, field, isValid, code) => { const normalized = normalizeResponseText(value, field, { code, subject: '家谱响应' }) if (normalized && !isValid(normalized)) { throw createRequestError(`家谱字段 ${field} 无效`, code) } return normalized } const normalizeGenealogyCapabilities = (value, code) => { const capabilities = {} for (const field of ['canManage', 'canEditContent', 'canArchive', 'canRestore']) { if (typeof value[field] !== 'boolean') { throw createRequestError(`家谱权限字段 ${field} 无效`, code) } capabilities[field] = value[field] } return capabilities } export const normalizeGenealogyPermanentDeletionCapability = (value) => { if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.canDeletePermanently !== 'boolean') { throw createRequestError('家谱永久删除能力响应无效', 'GENEALOGY_DELETION_RESPONSE_INVALID') } if (!Array.isArray(value.disabledReasons) || value.disabledReasons.some((reason) => typeof reason !== 'string')) { throw createRequestError('家谱永久删除阻断原因无效', 'GENEALOGY_DELETION_RESPONSE_INVALID') } const phoneMasked = normalizeResponseText(value.verifiedMobileMasked, 'verifiedMobileMasked', { code: 'GENEALOGY_DELETION_RESPONSE_INVALID', subject: '家谱永久删除能力响应' }) if (value.canDeletePermanently && !phoneMasked) { throw createRequestError('家谱永久删除能力缺少验证手机号', 'GENEALOGY_DELETION_RESPONSE_INVALID') } return { canDeletePermanently: value.canDeletePermanently, phoneMasked, disabledReasons: value.disabledReasons.map((reason) => reason.trim()).filter(Boolean) } } export const normalizeGenealogyPermanentDeletionPayload = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { throw new TypeError('家谱永久删除请求无效') } if (typeof payload.confirmationName !== 'string' || !payload.confirmationName.trim() || payload.confirmationName.trim().length > 24) { throw new TypeError('家谱永久删除确认名称无效') } if (typeof payload.smsCode !== 'string' || !/^\d{4}$/.test(payload.smsCode)) { throw new TypeError('家谱永久删除验证码必须是4位数字') } return { genealogyName: payload.confirmationName.trim(), smsCode: payload.smsCode } } export const normalizeGenealogyDeletionTask = (value, expectedGenealogyId) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw createRequestError('家谱删除任务响应无效', 'GENEALOGY_DELETION_RESPONSE_INVALID') } const taskId = normalizeResponseText(String(value.taskId ?? ''), 'taskId', { code: 'GENEALOGY_DELETION_RESPONSE_INVALID', subject: '家谱删除任务' }) const genealogyId = String(value.genealogyId ?? '') if (!/^[1-9]\d*$/.test(taskId) || genealogyId !== expectedGenealogyId) { throw createRequestError('家谱删除任务缺少稳定归属字段', 'GENEALOGY_DELETION_RESPONSE_INVALID') } return { taskId, genealogyId, status: normalizeResponseText(value.status, 'status') } } const normalizeMyGenealogyId = (value) => { 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('我的家谱响应包含无效标识', 'GENEALOGY_RESPONSE_INVALID') } export const normalizeGenealogyPathId = (value) => { const id = typeof value === 'number' && Number.isSafeInteger(value) ? String(value) : value if (typeof id !== 'string' || !/^[1-9]\d*$/.test(id)) { throw createRequestError('家谱标识无效', 'GENEALOGY_ID_INVALID') } return id } export const normalizeCreatedGenealogy = (value) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw createRequestError('创建家谱响应无效', 'GENEALOGY_RESPONSE_INVALID') } const id = value.genealogyId if (typeof id === 'string' && /^[1-9]\d*$/.test(id)) return { id } if (typeof id === 'number' && Number.isSafeInteger(id) && id > 0) return { id: String(id) } throw createRequestError('创建家谱响应缺少有效标识', 'GENEALOGY_RESPONSE_INVALID') } export const normalizeGenealogySettings = (value, expectedGenealogyId) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw createRequestError('家谱设置响应无效', 'GENEALOGY_SETTINGS_RESPONSE_INVALID') } const id = normalizeMyGenealogyId(value.genealogyId) if (id !== expectedGenealogyId) { throw createRequestError('家谱设置响应标识不匹配', 'GENEALOGY_SETTINGS_RESPONSE_INVALID') } const genealogyName = normalizeGenealogySettingsResponseText(value.genealogyName, 'genealogyName') const surname = normalizeGenealogySettingsResponseText(value.surname, 'surname') const regionCode = normalizeGenealogySettingsResponseText(value.regionCode, 'regionCode') if (!genealogyName || !surname || !regionCode) { throw createRequestError('家谱设置响应缺少名称、姓氏或地区', 'GENEALOGY_SETTINGS_RESPONSE_INVALID') } const visibility = normalizeGenealogyAccessValue( value.visibility, 'visibility', isGenealogyVisibility, 'GENEALOGY_SETTINGS_RESPONSE_INVALID' ) const joinMode = normalizeGenealogyAccessValue( value.joinMode, 'joinMode', isGenealogyJoinMode, 'GENEALOGY_SETTINGS_RESPONSE_INVALID' ) const capabilities = normalizeGenealogyCapabilities( value, 'GENEALOGY_SETTINGS_RESPONSE_INVALID' ) return { id, genealogyName, surname, ancestralHall: normalizeGenealogySettingsResponseText(value.ancestralHall, 'ancestralHall'), originPlace: normalizeGenealogySettingsResponseText(value.originPlace, 'originPlace'), regionCode, regionName: normalizeGenealogySettingsResponseText(value.regionName, 'regionName'), regionFullName: normalizeGenealogySettingsResponseText(value.regionFullName, 'regionFullName'), addressDetail: normalizeGenealogySettingsResponseText(value.addressDetail, 'addressDetail'), coverFile: normalizeBusinessFileAccess( value.coverFile, '家谱封面', 'GENEALOGY_SETTINGS_RESPONSE_INVALID' ), intro: normalizeGenealogySettingsResponseText(value.intro, 'intro'), accessPreset: fromApiGenealogyAccess({ visibility, joinMode }), visibility, joinMode, memberCount: normalizeOptionalNonnegativeInteger(value.memberCount, '家谱成员数量', 'GENEALOGY_SETTINGS_RESPONSE_INVALID'), personCount: normalizeOptionalNonnegativeInteger(value.personCount, '家谱人物数量', 'GENEALOGY_SETTINGS_RESPONSE_INVALID'), ...capabilities, lifecycleStatus: normalizeGenealogyLifecycleStatus( value.lifecycleStatus, 'GENEALOGY_SETTINGS_RESPONSE_INVALID' ), archivedAt: normalizeGenealogySettingsResponseText(value.archivedAt, 'archivedAt') } } export const normalizeAppGenealogy = (item) => { if (!item || typeof item !== 'object' || Array.isArray(item)) { throw createRequestError('我的家谱响应包含无效条目', 'GENEALOGY_RESPONSE_INVALID') } const name = normalizeGenealogyResponseText(item.genealogyName, 'genealogyName') if (!name) { throw createRequestError('我的家谱响应缺少家谱名称', 'GENEALOGY_RESPONSE_INVALID') } const capabilities = normalizeGenealogyCapabilities(item, 'GENEALOGY_RESPONSE_INVALID') if (!Number.isSafeInteger(item.memberCount) || item.memberCount < 0) { throw createRequestError('我的家谱响应成员数量无效', 'GENEALOGY_RESPONSE_INVALID') } const personCount = item.personCount ?? null if (personCount !== null && (!Number.isSafeInteger(personCount) || personCount < 0)) { throw createRequestError('我的家谱响应世系人数无效', 'GENEALOGY_RESPONSE_INVALID') } const visibility = normalizeGenealogyAccessValue( item.visibility, 'visibility', isGenealogyVisibility, 'GENEALOGY_RESPONSE_INVALID' ) const joinMode = normalizeGenealogyAccessValue( item.joinMode, 'joinMode', isGenealogyJoinMode, 'GENEALOGY_RESPONSE_INVALID' ) return { id: normalizeMyGenealogyId(item.genealogyId), name, firstAncestorName: normalizeGenealogyResponseText(item.firstAncestorName, 'firstAncestorName'), rootPersonId: normalizeOptionalNumericId(item.rootPersonId, '始迁祖人物标识', 'GENEALOGY_RESPONSE_INVALID'), surname: normalizeGenealogyResponseText(item.surname, 'surname'), hall: normalizeGenealogyResponseText(item.ancestralHall, 'ancestralHall'), location: normalizeGenealogyResponseText(item.regionFullName, 'regionFullName') || normalizeGenealogyResponseText(item.regionName, 'regionName') || normalizeGenealogyResponseText(item.originPlace, 'originPlace') || normalizeGenealogyResponseText(item.addressDetail, 'addressDetail') || '地区待补', memberCount: item.memberCount, personCount, accessPreset: fromApiGenealogyAccess({ visibility, joinMode }), ...capabilities, lifecycleStatus: normalizeGenealogyLifecycleStatus( item.lifecycleStatus, 'GENEALOGY_RESPONSE_INVALID' ), archivedAt: normalizeGenealogyResponseText(item.archivedAt, 'archivedAt'), intro: normalizeGenealogyResponseText(item.intro, 'intro'), joinTime: normalizeGenealogyResponseText(item.joinTime, 'joinTime') } } export const normalizeMyGenealogies = (value) => { if (!Array.isArray(value)) { throw createRequestError('我的家谱响应不是列表', 'GENEALOGY_RESPONSE_INVALID') } const normalized = value.map((item) => { const genealogy = normalizeAppGenealogy(item) return { id: genealogy.id, name: genealogy.name, surname: genealogy.surname, hall: genealogy.hall, location: genealogy.location, memberCount: genealogy.memberCount, canManage: genealogy.canManage, canEditContent: genealogy.canEditContent, lifecycleStatus: genealogy.lifecycleStatus, archivedAt: genealogy.archivedAt, canArchive: genealogy.canArchive, canRestore: genealogy.canRestore } }) if (new Set(normalized.map((genealogy) => genealogy.id)).size !== normalized.length) { throw createRequestError('我的家谱响应包含重复标识', 'GENEALOGY_RESPONSE_INVALID') } return normalized } export const normalizeGenealogyOrderIds = (value) => { if (!Array.isArray(value)) { throw createRequestError('家谱排序必须是列表', 'GENEALOGY_ORDER_INVALID') } const ids = value.map((item) => normalizeMyGenealogyId(item)) if (new Set(ids).size !== ids.length) { throw createRequestError('家谱排序不能包含重复家谱', 'GENEALOGY_ORDER_INVALID') } return ids } export const normalizePublicGenealogyQuery = (query = {}) => { assertPlainPayload(query, new Set(['keyword']), '公开家谱查询') if (query.keyword === undefined || query.keyword === null) return {} if (typeof query.keyword !== 'string') throw new TypeError('公开家谱关键词必须是字符串') const keyword = query.keyword.trim() if (keyword.length > 50) throw new TypeError('公开家谱关键词不能超过50个字符') return keyword ? { keyword } : {} } export const normalizePublicGenealogies = (value) => { if (!Array.isArray(value)) { throw createRequestError('公开家谱响应不是列表', 'PUBLIC_GENEALOGY_RESPONSE_INVALID') } const genealogies = value.map((item) => { if (!item || typeof item !== 'object' || Array.isArray(item)) { throw createRequestError('公开家谱响应包含无效条目', 'PUBLIC_GENEALOGY_RESPONSE_INVALID') } const id = normalizeMyGenealogyId(item.genealogyId) const name = normalizeGenealogyResponseText(item.genealogyName, 'genealogyName') if (!name) { throw createRequestError('公开家谱响应缺少家谱名称', 'PUBLIC_GENEALOGY_RESPONSE_INVALID') } const memberCount = normalizeOptionalNonnegativeInteger( item.memberCount, '公开家谱成员数量', 'PUBLIC_GENEALOGY_RESPONSE_INVALID' ) if (item.canManage !== undefined && typeof item.canManage !== 'boolean') { throw createRequestError('公开家谱管理权限无效', 'PUBLIC_GENEALOGY_RESPONSE_INVALID') } const memberStatus = normalizeGenealogyResponseText(item.memberStatus, 'memberStatus') return { id, name, surname: normalizeGenealogyResponseText(item.surname, 'surname'), location: normalizeGenealogyResponseText(item.regionFullName, 'regionFullName') || normalizeGenealogyResponseText(item.regionName, 'regionName') || normalizeGenealogyResponseText(item.originPlace, 'originPlace') || normalizeGenealogyResponseText(item.addressDetail, 'addressDetail'), intro: normalizeGenealogyResponseText(item.intro, 'intro'), memberCount, canManage: item.canManage === true, memberStatus, hasMembership: item.canManage === true || Boolean(memberStatus) } }) if (new Set(genealogies.map((genealogy) => genealogy.id)).size !== genealogies.length) { throw createRequestError('公开家谱响应包含重复标识', 'PUBLIC_GENEALOGY_RESPONSE_INVALID') } return genealogies } const normalizeGenealogyWriteText = (value, field, { required = false } = {}) => { if (value === undefined || value === null) { if (required) throw new TypeError(`创建家谱缺少 ${field}`) return '' } if (typeof value !== 'string') throw new TypeError(`创建家谱字段 ${field} 必须是字符串`) const normalized = value.trim() if (required && !normalized) throw new TypeError(`创建家谱缺少 ${field}`) return normalized } const GENEALOGY_WRITE_FIELDS = new Set([ 'genealogyName', 'surname', 'regionCode', 'ancestralHall', 'originPlace', 'addressDetail', 'intro', 'coverOssId', 'visibility', 'joinMode' ]) export const normalizeGenealogyCreatePayload = (payload) => { assertPlainPayload( payload, new Set([...GENEALOGY_WRITE_FIELDS, 'firstAncestorName', 'requestId', 'ownerIsFirstAncestor']), '创建家谱请求' ) const requestId = normalizeGenealogyWriteText(payload.requestId, 'requestId', { required: true }) if (requestId.length > 64) throw new TypeError('创建家谱请求号不能超过64个字符') const firstAncestorName = normalizeGenealogyWriteText(payload.firstAncestorName, '始迁祖姓名', { required: true }) if (firstAncestorName.length > 30) throw new TypeError('始迁祖姓名不能超过30个字符') if (payload.ownerIsFirstAncestor !== undefined && typeof payload.ownerIsFirstAncestor !== 'boolean') { throw new TypeError('谱主是否为始迁祖必须是布尔值') } const normalizedPayload = { genealogyName: normalizeGenealogyWriteText(payload.genealogyName, '谱名', { required: true }), surname: normalizeGenealogyWriteText(payload.surname, '姓氏', { required: true }), regionCode: normalizeGenealogyWriteText(payload.regionCode, '地区代码', { required: true }), firstAncestorName, requestId, ownerIsFirstAncestor: payload.ownerIsFirstAncestor === true } for (const field of ['ancestralHall', 'originPlace', 'addressDetail', 'intro']) { const normalizedText = normalizeGenealogyWriteText(payload[field], field) if (normalizedText) normalizedPayload[field] = normalizedText } const visibility = normalizeGenealogyWriteText(payload.visibility, 'visibility') if (visibility) { if (!isGenealogyVisibility(visibility)) throw new TypeError('创建家谱字段 visibility 无效') normalizedPayload.visibility = visibility } const joinMode = normalizeGenealogyWriteText(payload.joinMode, 'joinMode') if (joinMode) { if (!isGenealogyJoinMode(joinMode)) throw new TypeError('创建家谱字段 joinMode 无效') normalizedPayload.joinMode = joinMode } if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') { normalizedPayload.coverOssId = normalizeOssIdString(payload.coverOssId, '创建家谱字段 coverOssId') } return normalizedPayload } export const normalizeGenealogyUpdatePayload = (payload) => { assertPlainPayload(payload, GENEALOGY_WRITE_FIELDS, '更新家谱请求') const normalizedPayload = {} for (const field of [ 'genealogyName', 'surname', 'regionCode', 'ancestralHall', 'originPlace', 'addressDetail', 'intro' ]) { if (!Object.prototype.hasOwnProperty.call(payload, field)) continue const normalizedText = normalizeGenealogyWriteText(payload[field], field) if (normalizedText) normalizedPayload[field] = normalizedText } if (Object.prototype.hasOwnProperty.call(payload, 'visibility')) { const visibility = normalizeGenealogyWriteText(payload.visibility, 'visibility') if (!isGenealogyVisibility(visibility)) throw new TypeError('更新家谱字段 visibility 无效') normalizedPayload.visibility = visibility } if (Object.prototype.hasOwnProperty.call(payload, 'joinMode')) { const joinMode = normalizeGenealogyWriteText(payload.joinMode, 'joinMode') if (!isGenealogyJoinMode(joinMode)) throw new TypeError('更新家谱字段 joinMode 无效') normalizedPayload.joinMode = joinMode } if (Object.prototype.hasOwnProperty.call(payload, 'coverOssId')) { if (payload.coverOssId === null) normalizedPayload.coverOssId = null else if (payload.coverOssId !== '') { normalizedPayload.coverOssId = normalizeOssIdString(payload.coverOssId, '更新家谱字段 coverOssId') } } if (!Object.keys(normalizedPayload).length) throw new TypeError('请至少填写一项需要更新的家谱信息') return normalizedPayload } export const normalizeGenealogyQuota = (value) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw createRequestError('家谱配额响应无效', 'GENEALOGY_QUOTA_RESPONSE_INVALID') } const normalizeUsage = (field) => { if (!Number.isSafeInteger(value[field]) || value[field] < 0) { throw createRequestError(`家谱配额字段 ${field} 无效`, 'GENEALOGY_QUOTA_RESPONSE_INVALID') } return value[field] } const normalizeLimit = (field) => { if (!Number.isSafeInteger(value[field]) || value[field] < -1) { throw createRequestError(`家谱配额字段 ${field} 无效`, 'GENEALOGY_QUOTA_RESPONSE_INVALID') } return value[field] } const normalizePermission = (field) => { if (typeof value[field] !== 'boolean') { throw createRequestError(`家谱配额字段 ${field} 无效`, 'GENEALOGY_QUOTA_RESPONSE_INVALID') } return value[field] } return { createUsed: normalizeUsage('createUsed'), createLimit: normalizeLimit('createLimit'), createRemaining: normalizeLimit('createRemaining'), canCreate: normalizePermission('canCreate'), joinUsed: normalizeUsage('joinUsed'), joinLimit: normalizeLimit('joinLimit'), joinRemaining: normalizeLimit('joinRemaining'), canJoin: normalizePermission('canJoin') } }