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 } 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, 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 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') } 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 } }) if (new Set(genealogies.map((genealogy) => genealogy.id)).size !== genealogies.length) { throw createRequestError('公开家谱响应包含重复标识', 'PUBLIC_GENEALOGY_RESPONSE_INVALID') } return genealogies } export const projectPreviewPublicGenealogies = (value) => value.map((item) => ({ id: String(item.id), name: String(item.name || '未命名家谱'), surname: String(item.surname || ''), location: String(item.location || ''), intro: String(item.publicDescription || ''), memberCount: Number.isSafeInteger(item.memberCount) ? item.memberCount : 0, canManage: false })) 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' ]) const assertGenealogyWritePayload = (payload, operation) => { assertPlainPayload(payload, GENEALOGY_WRITE_FIELDS, `${operation}家谱请求`) } export const normalizeGenealogyCreatePayload = (payload) => { assertGenealogyWritePayload(payload, '创建') const normalizedPayload = { genealogyName: normalizeGenealogyWriteText(payload.genealogyName, '谱名', { required: true }), surname: normalizeGenealogyWriteText(payload.surname, '姓氏', { required: true }), regionCode: normalizeGenealogyWriteText(payload.regionCode, '地区代码', { required: 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) => { assertGenealogyWritePayload(payload, '更新') 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') && payload.coverOssId !== null && 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') } }