import { currentUser, genealogies, publicGenealogies, notifications, joinApplications, listFamilyFeedFixtures, listFamilyArticleFixtures, listFamilyAlbumFixtures, listCeremonyFixtures, listGrowthRecordFixtures, listNotificationFixtures } from '@/data/mock.js' import { hasRemoteConfig, resolveRuntimeMode, runtimeConfig } from '@/utils/config.js' import { AUTH_VERIFICATION_OPERATION, assertSmsCode } from '@/utils/auth-verification.js' import { GENEALOGY_ACCESS_PRESET, fromApiGenealogyAccess } from '@/utils/genealogy-contracts.js' import { session } from '@/utils/session.js' const successCodes = [0, 200] const REQUEST_TIMEOUT_MS = 15000 const createRequestError = (message, code, details = {}) => { const error = new Error(message) error.code = code Object.assign(error, details) return error } const createRequestCancelledError = () => createRequestError('请求已取消', 'REQUEST_CANCELLED') // 页面只持有这个窄控制器,不直接依赖各端 RequestTask 的实现差异。 // request() 是唯一绑定/释放 owner;离页时 abort() 会同时拒绝等待中的 Promise, // 因而页面不会在已销毁后继续处理成功回调。 export const createRequestController = () => { let abortCurrent = null return { bind(abortRequest) { if (typeof abortRequest !== 'function') throw new TypeError('请求中止器必须是函数') if (abortCurrent) abortCurrent() abortCurrent = abortRequest }, release(abortRequest) { if (abortCurrent === abortRequest) abortCurrent = null }, abort() { const abortRequest = abortCurrent abortCurrent = null if (abortRequest) abortRequest() } } } export const isRequestCancelled = (error) => error?.code === 'REQUEST_CANCELLED' const isAuthenticatedSessionRejected = (error) => error?.code === 'BUSINESS_ERROR' && Number(error.businessCode) === 401 let authenticationRecoveryInFlight = false const recoverExpiredAuthenticatedSession = () => { if (authenticationRecoveryInFlight) return authenticationRecoveryInFlight = true session.clear() try { uni.reLaunch({ url: '/pages/auth/a01-entry', complete: () => { authenticationRecoveryInFlight = false } }) } catch { authenticationRecoveryInFlight = false } } export const unwrapResponse = (response) => { if (!response || typeof response !== 'object' || Array.isArray(response)) return response if (!Object.prototype.hasOwnProperty.call(response, 'code')) return response if (!successCodes.includes(Number(response.code))) { const numericCode = Number(response.code) throw createRequestError(response.msg || '请求未成功', 'BUSINESS_ERROR', { businessCode: Number.isNaN(numericCode) ? response.code : numericCode }) } return Object.prototype.hasOwnProperty.call(response, 'data') ? response.data : response } const request = (options, { authenticated = true, strictEnvelope = false, requireEnvelopeData = false, expectedStatus = null, requestController = null } = {}) => new Promise((resolve, reject) => { if (requestController !== null && ( typeof requestController.bind !== 'function' || typeof requestController.release !== 'function' )) { reject(new TypeError('请求控制器格式无效')) return } let settled = false let requestTask = null const release = () => requestController?.release(abortRequest) const resolveOnce = (value) => { if (settled) return settled = true release() resolve(value) } const rejectOnce = (error) => { if (settled) return settled = true release() reject(error) } const abortRequest = () => { if (settled) return const task = requestTask rejectOnce(createRequestCancelledError()) task?.abort?.() } const token = session.getToken() requestTask = uni.request({ ...options, url: `${runtimeConfig.baseUrl}${options.url}`, header: { clientid: runtimeConfig.clientId, tenantId: runtimeConfig.tenantId, ...(authenticated && token ? { Authorization: `Bearer ${token}` } : {}), ...(options.header || {}) }, success: ({ data, statusCode }) => { if (expectedStatus !== null && statusCode !== expectedStatus) { rejectOnce(createRequestError( `服务只接受 HTTP ${expectedStatus} 响应,实际为 ${statusCode}`, 'HTTP_ERROR', { httpStatus: statusCode } )) return } if (statusCode < 200 || statusCode >= 300) { rejectOnce(createRequestError( data?.msg || `请求失败(${statusCode})`, 'HTTP_ERROR', { httpStatus: statusCode } )) return } if ( strictEnvelope && (!data || typeof data !== 'object' || Array.isArray(data) || !Object.prototype.hasOwnProperty.call(data, 'code') || !Number.isInteger(data.code) || (requireEnvelopeData && !Object.prototype.hasOwnProperty.call(data, 'data'))) ) { rejectOnce(createRequestError('服务响应格式无效', 'RESPONSE_INVALID')) return } try { resolveOnce(unwrapResponse(data)) } catch (error) { // 业务 401 表示本地会话已经失效:只撤销本地令牌和家谱上下文, // 不调用退出接口、不重试写操作,也不影响未携带会话的登录/注册请求。 if (authenticated && isAuthenticatedSessionRejected(error)) { recoverExpiredAuthenticatedSession() } rejectOnce(error) } }, fail: (error) => { const message = error?.errMsg || '网络连接失败' rejectOnce(createRequestError( message, /timeout/i.test(message) ? 'REQUEST_TIMEOUT' : 'NETWORK_ERROR' )) } }) if (!settled && requestController) requestController.bind(abortRequest) }) // 已核对接口使用这一严格边界:只接受 HTTP 200 与完整 JSON envelope, // 并统一限制弱网等待时间。尚未逐页治理的旧业务读取仍沿用上方通用 request。 const requestStrict = (options, { authenticated = true, requireData = true, requestController = null } = {}) => request({ ...options, timeout: REQUEST_TIMEOUT_MS }, { authenticated, strictEnvelope: true, requireEnvelopeData: requireData, expectedStatus: 200, requestController }) const requestAuth = (options, { requestController = null } = {}) => requestStrict(options, { authenticated: false, requestController }) // RVoid 的 OpenAPI schema 没有把 data 声明为必填。空结果接口仍要求合法的 // HTTP 200 与整数 code,但统一向页面返回 null,避免把 envelope 本身泄漏成业务数据。 const requestAuthVoid = async (options, { requestController = null } = {}) => { await requestStrict(options, { authenticated: false, requireData: false, requestController }) return null } const parseStrictUploadResponse = (responseText, requireData) => { let data try { data = JSON.parse(responseText) } catch { throw createRequestError('文件服务响应格式无效', 'RESPONSE_INVALID') } if ( !data || typeof data !== 'object' || Array.isArray(data) || !Object.prototype.hasOwnProperty.call(data, 'code') || !Number.isInteger(data.code) || (requireData && !Object.prototype.hasOwnProperty.call(data, 'data')) ) { throw createRequestError('文件服务响应格式无效', 'RESPONSE_INVALID') } return unwrapResponse(data) } const requestNativeFileChunk = ({ uploadId, chunkIndex, chunkMd5, filePath }, { requestController = null } = {}) => new Promise((resolve, reject) => { if (typeof globalThis.uni?.uploadFile !== 'function') { reject(createRequestError('当前运行环境不支持文件上传', 'FILE_UPLOAD_UNAVAILABLE')) return } if (requestController !== null && ( typeof requestController.bind !== 'function' || typeof requestController.release !== 'function' )) { reject(new TypeError('文件上传控制器格式无效')) return } let settled = false let task = null let cancelled = false const release = () => requestController?.release(abort) const resolveOnce = (value) => { if (settled) return settled = true release() resolve(value) } const rejectOnce = (error) => { if (settled) return settled = true release() reject(error) } const abort = () => { if (settled) return cancelled = true task?.abort?.() rejectOnce(createRequestCancelledError()) } const token = session.getToken() try { task = globalThis.uni.uploadFile({ url: `${runtimeConfig.baseUrl}/genealogy/app/files/resumable/chunk`, filePath, name: 'file', formData: { uploadId, chunkIndex: String(chunkIndex), chunkMd5 }, header: { clientid: runtimeConfig.clientId, tenantId: runtimeConfig.tenantId, ...(token ? { Authorization: `Bearer ${token}` } : {}) }, timeout: REQUEST_TIMEOUT_MS, success: (upload) => { if (upload.statusCode !== 200) { rejectOnce(createRequestError(`文件分片上传失败(HTTP ${upload.statusCode})`, 'HTTP_ERROR', { httpStatus: upload.statusCode })) return } try { parseStrictUploadResponse(upload.data, false) resolveOnce(null) } catch (error) { rejectOnce(error) } }, fail: (error) => { if (settled) return if (cancelled) { rejectOnce(createRequestCancelledError()) return } const message = typeof error?.errMsg === 'string' && error.errMsg ? error.errMsg : '文件分片上传失败' const timeout = /timeout/i.test(message) rejectOnce(createRequestError( timeout ? '文件分片上传超时' : message, timeout ? 'REQUEST_TIMEOUT' : 'UPLOAD_FAILED' )) } }) if (requestController) requestController.bind(abort) } catch (error) { rejectOnce(error) } }) const requestBrowserFileChunk = ({ uploadId, chunkIndex, chunkMd5, file }, { requestController = null } = {}) => new Promise((resolve, reject) => { if (typeof globalThis.fetch !== 'function' || typeof globalThis.FormData !== 'function') { reject(createRequestError('当前运行环境不支持文件上传', 'FILE_UPLOAD_UNAVAILABLE')) return } if (requestController !== null && ( typeof requestController.bind !== 'function' || typeof requestController.release !== 'function' )) { reject(new TypeError('文件上传控制器格式无效')) return } let settled = false let timeoutId = null const abortController = new AbortController() const release = () => requestController?.release(abort) const clearRequestTimeout = () => { if (timeoutId === null) return clearTimeout(timeoutId) timeoutId = null } const resolveOnce = (value) => { if (settled) return settled = true clearRequestTimeout() release() resolve(value) } const rejectOnce = (error) => { if (settled) return settled = true clearRequestTimeout() release() reject(error) } const abort = () => { if (settled) return abortController.abort() rejectOnce(createRequestCancelledError()) } const token = session.getToken() const formData = new globalThis.FormData() formData.append('uploadId', uploadId) formData.append('chunkIndex', String(chunkIndex)) formData.append('chunkMd5', chunkMd5) formData.append('file', file, file.name || 'image') if (requestController) requestController.bind(abort) timeoutId = setTimeout(() => { abortController.abort() rejectOnce(createRequestError('文件分片上传超时', 'REQUEST_TIMEOUT')) }, REQUEST_TIMEOUT_MS) globalThis.fetch(`${runtimeConfig.baseUrl}/genealogy/app/files/resumable/chunk`, { method: 'POST', headers: { clientid: runtimeConfig.clientId, tenantId: runtimeConfig.tenantId, ...(token ? { Authorization: `Bearer ${token}` } : {}) }, body: formData, signal: abortController.signal }).then(async (response) => { if (response.status !== 200) { throw createRequestError(`文件分片上传失败(HTTP ${response.status})`, 'HTTP_ERROR', { httpStatus: response.status }) } parseStrictUploadResponse(await response.text(), false) resolveOnce(null) }).catch((error) => { if (settled) return rejectOnce(error?.name === 'AbortError' ? createRequestCancelledError() : error) }) }) const requireRemoteAuth = () => { if (resolveRuntimeMode() !== 'remote') { const error = new Error('当前为本地预览模式,真实认证服务未启用') error.code = 'AUTH_REMOTE_REQUIRED' throw error } } const assertAuthVerificationOperation = (operationCode) => { if (!Object.values(AUTH_VERIFICATION_OPERATION).includes(operationCode)) { throw new TypeError('认证动作不属于当前认证合同') } return operationCode } const assertValidToken = (validToken) => { if (typeof validToken !== 'string' || !validToken.trim()) { throw new TypeError('发送短信前必须取得有效的行为验证票据') } return validToken.trim() } const normalizeOptionalValidToken = (validToken) => validToken === undefined ? undefined : assertValidToken(validToken) const assertPasswordHash = (passwordHash) => { if (typeof passwordHash !== 'string' || !/^[a-f0-9]{32}$/.test(passwordHash)) { throw new TypeError('密码摘要必须是 32 位小写 MD5') } return passwordHash } const normalizeOptionalAuthText = (value, label) => { if (value === undefined || value === null) return undefined if (typeof value !== 'string') { throw new TypeError(`${label}必须是字符串`) } const normalized = value.trim() return normalized || undefined } const normalizeProfileResponseText = (value, field) => { if (value === undefined || value === null) return '' if (typeof value !== 'string') { throw createRequestError(`个人资料字段 ${field} 无效`, 'PROFILE_RESPONSE_INVALID') } return value.trim() } const normalizeProfileAvatar = (value) => { if (value === undefined || value === null || value === '') return null const numericValue = typeof value === 'string' && /^-?\d+$/.test(value) ? Number(value) : value if (Number.isSafeInteger(numericValue) && numericValue <= 0) return null return Number.isSafeInteger(numericValue) ? numericValue : null } const normalizeProfileUserId = (value) => { if (value === undefined || value === null || value === '') return '' 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('个人资料字段 userId 无效', 'PROFILE_RESPONSE_INVALID') } const normalizeAppProfile = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { throw createRequestError('个人资料响应格式无效', 'PROFILE_RESPONSE_INVALID') } return { userId: normalizeProfileUserId(payload.userId), tenantId: normalizeProfileResponseText(payload.tenantId, 'tenantId'), userNo: normalizeProfileResponseText(payload.userNo, 'userNo'), phone: normalizeProfileResponseText(payload.phone, 'phone'), nickName: normalizeProfileResponseText(payload.nickName, 'nickName'), realName: normalizeProfileResponseText(payload.realName, 'realName'), avatar: normalizeProfileAvatar(payload.avatar), sex: normalizeProfileResponseText(payload.sex, 'sex'), birthday: normalizeProfileResponseText(payload.birthday, 'birthday'), email: normalizeProfileResponseText(payload.email, 'email'), registerSource: normalizeProfileResponseText(payload.registerSource, 'registerSource'), status: normalizeProfileResponseText(payload.status, 'status') } } const normalizeProfileUpdatePayload = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) { throw new TypeError('个人资料请求必须是普通对象') } const allowedFields = new Set(['nickName', 'realName', 'avatar', 'sex', 'birthday', 'email']) const fieldNames = Object.getOwnPropertyNames(payload) const descriptors = Object.getOwnPropertyDescriptors(payload) if ( Object.getOwnPropertySymbols(payload).length > 0 || fieldNames.some((field) => !allowedFields.has(field)) || fieldNames.some((field) => !Object.prototype.hasOwnProperty.call(descriptors[field], 'value')) ) { throw new TypeError('个人资料请求包含未声明字段') } const normalized = {} for (const [field, limit] of [['nickName', 30], ['realName', 30]]) { if (!Object.prototype.hasOwnProperty.call(payload, field)) continue const value = normalizeOptionalAuthText(payload[field], field) if (!value) continue if (value.length > limit) throw new TypeError(`${field} 超出长度限制`) normalized[field] = value } if (Object.prototype.hasOwnProperty.call(payload, 'sex')) { const sex = normalizeOptionalAuthText(payload.sex, 'sex') if (sex) { if (!['0', '1', '2'].includes(sex)) throw new TypeError('sex 必须为 0、1 或 2') normalized.sex = sex } } if (Object.prototype.hasOwnProperty.call(payload, 'birthday')) { const value = normalizeOptionalAuthText(payload.birthday, 'birthday') if (value) { if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { throw new TypeError('birthday 必须是 yyyy-MM-dd 日期') } normalized.birthday = value } } if (Object.prototype.hasOwnProperty.call(payload, 'email')) { const value = normalizeOptionalAuthText(payload.email, 'email') if (value) { if (value.length > 100 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { throw new TypeError('email 格式无效') } normalized.email = value } } if (Object.prototype.hasOwnProperty.call(payload, 'avatar')) { const value = payload.avatar if (value !== undefined && value !== null && value !== '') { if (!Number.isSafeInteger(value) || value <= 0) { throw new TypeError('avatar 必须是安全整数文件标识') } normalized.avatar = value } } if (Object.keys(normalized).length === 0) { throw new TypeError('个人资料至少需要一个可更新字段') } return normalized } const assertPlainPayload = (payload, allowedFields, label) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) { throw new TypeError(`${label}必须是普通对象`) } const fieldNames = Object.getOwnPropertyNames(payload) const descriptors = Object.getOwnPropertyDescriptors(payload) if ( Object.getOwnPropertySymbols(payload).length > 0 || fieldNames.some((field) => !allowedFields.has(field)) || fieldNames.some((field) => !Object.prototype.hasOwnProperty.call(descriptors[field], 'value')) ) { throw new TypeError(`${label}包含未声明字段`) } } const normalizeUploadId = (value, field) => { if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${field}必须是非空字符串`) return value.trim() } const normalizeUploadFileName = (value) => { if (typeof value !== 'string' || !value.trim()) throw new TypeError('fileName必须是非空字符串') return value.trim() } const normalizeUploadMd5 = (value, field) => { if (typeof value !== 'string' || !/^[a-f0-9]{32}$/i.test(value)) { throw new TypeError(`${field}必须是 32 位十六进制 MD5`) } return value.toLowerCase() } const normalizeUploadSize = (value, field) => { if (!Number.isSafeInteger(value) || value <= 0) throw new TypeError(`${field}必须是正安全整数`) return value } const normalizeResumableInitPayload = (payload) => { const allowedFields = new Set(['uploadId', 'fileName', 'fileMd5', 'totalSize', 'totalChunks', 'chunkSize', 'contentType']) assertPlainPayload(payload, allowedFields, '文件初始化请求') if (!Number.isInteger(payload.totalChunks) || payload.totalChunks <= 0 || payload.totalChunks > 2147483647) { throw new TypeError('totalChunks必须是正 int32') } const data = { uploadId: normalizeUploadId(payload.uploadId, 'uploadId'), fileName: normalizeUploadFileName(payload.fileName), fileMd5: normalizeUploadMd5(payload.fileMd5, 'fileMd5'), totalSize: normalizeUploadSize(payload.totalSize, 'totalSize'), totalChunks: payload.totalChunks, chunkSize: normalizeUploadSize(payload.chunkSize, 'chunkSize') } const contentType = normalizeOptionalAuthText(payload.contentType, 'contentType') if (contentType) data.contentType = contentType return data } const normalizeResumableCompletePayload = (payload) => { const allowedFields = new Set(['uploadId', 'fileName', 'fileMd5', 'totalSize', 'totalChunks']) assertPlainPayload(payload, allowedFields, '文件完成请求') if (!Number.isInteger(payload.totalChunks) || payload.totalChunks <= 0 || payload.totalChunks > 2147483647) { throw new TypeError('totalChunks必须是正 int32') } return { uploadId: normalizeUploadId(payload.uploadId, 'uploadId'), fileName: normalizeUploadFileName(payload.fileName), fileMd5: normalizeUploadMd5(payload.fileMd5, 'fileMd5'), totalSize: normalizeUploadSize(payload.totalSize, 'totalSize'), totalChunks: payload.totalChunks } } const normalizeUploadOssId = (value, field) => { const id = typeof value === 'number' && Number.isSafeInteger(value) ? String(value) : value if (typeof id !== 'string' || !/^[1-9]\d*$/.test(id)) { throw createRequestError(`文件服务字段 ${field} 无效`, 'FILE_RESPONSE_INVALID') } return id } const normalizeOssIdString = (value, field) => { if (typeof value !== 'string' || !/^[1-9]\d*$/.test(value)) { throw new TypeError(`${field} 必须是正整数 OSS ID 字符串`) } return value } const normalizeResumableInitResult = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { throw createRequestError('文件初始化响应格式无效', 'FILE_RESPONSE_INVALID') } if (typeof payload.instant !== 'boolean') { throw createRequestError('文件初始化响应缺少 instant', 'FILE_RESPONSE_INVALID') } const ossId = payload.ossId === undefined || payload.ossId === null ? null : normalizeUploadOssId(payload.ossId, 'ossId') if (payload.instant && !ossId) { throw createRequestError('秒传响应缺少 ossId', 'FILE_RESPONSE_INVALID') } const uploadId = payload.instant && (payload.uploadId === undefined || payload.uploadId === null) ? null : normalizeUploadId(payload.uploadId, '响应 uploadId') return { uploadId, instant: payload.instant, ossId, url: normalizeProfileResponseText(payload.url, 'url'), fileName: normalizeProfileResponseText(payload.fileName, 'fileName') } } const normalizeResumableCompleteResult = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { throw createRequestError('文件完成响应格式无效', 'FILE_RESPONSE_INVALID') } return { ossId: normalizeUploadOssId(payload.ossId, 'ossId'), url: normalizeProfileResponseText(payload.url, 'url'), thumbnailUrl: normalizeProfileResponseText(payload.thumbnailUrl, 'thumbnailUrl'), fileName: normalizeProfileResponseText(payload.fileName, 'fileName') } } const normalizeFeedbackPayload = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) { throw new TypeError('反馈请求必须是普通对象') } const allowedFields = new Set(['feedbackType', 'feedbackContent', 'contactInfo']) const fieldNames = Object.getOwnPropertyNames(payload) if (Object.getOwnPropertySymbols(payload).length > 0 || fieldNames.some((field) => !allowedFields.has(field))) { throw new TypeError('反馈请求包含未声明字段') } const descriptors = Object.getOwnPropertyDescriptors(payload) if (fieldNames.some((field) => !Object.prototype.hasOwnProperty.call(descriptors[field], 'value'))) { throw new TypeError('反馈字段必须是普通数据属性') } if (typeof payload.feedbackContent !== 'string' || !payload.feedbackContent.trim()) { throw new TypeError('反馈内容必须是非空字符串') } const normalized = { feedbackContent: payload.feedbackContent.trim() } for (const optionalField of ['feedbackType', 'contactInfo']) { if (!Object.prototype.hasOwnProperty.call(payload, optionalField)) continue if (typeof payload[optionalField] !== 'string') { throw new TypeError(`反馈字段 ${optionalField} 必须是字符串`) } const value = payload[optionalField].trim() if (value) { if (optionalField === 'feedbackType' && !['advice', 'bug', 'complaint', 'other'].includes(value)) { throw new TypeError('feedbackType 必须为 advice、bug、complaint 或 other') } normalized[optionalField] = value } } return normalized } 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') } 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 } const normalizeOptionalGenealogyText = (value, label) => { if (value === undefined || value === null) return '' if (typeof value !== 'string') { throw createRequestError(`我的家谱响应字段 ${label} 无效`, 'GENEALOGY_RESPONSE_INVALID') } return value.trim() } const normalizeAppGenealogy = (item) => { if (!item || typeof item !== 'object' || Array.isArray(item)) { throw createRequestError('我的家谱响应包含无效条目', 'GENEALOGY_RESPONSE_INVALID') } const name = normalizeOptionalGenealogyText(item.genealogyName, 'genealogyName') if (!name) { throw createRequestError('我的家谱响应缺少家谱名称', 'GENEALOGY_RESPONSE_INVALID') } for (const field of ['canManage', 'canEditContent']) { if (typeof item[field] !== 'boolean') { throw createRequestError(`我的家谱响应字段 ${field} 无效`, 'GENEALOGY_RESPONSE_INVALID') } } if (item.canView === false) { throw createRequestError('当前账号无权查看该家谱', 'GENEALOGY_FORBIDDEN') } 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') } return { id: normalizeMyGenealogyId(item.genealogyId), name, surname: normalizeOptionalGenealogyText(item.surname, 'surname'), hall: normalizeOptionalGenealogyText(item.ancestralHall, 'ancestralHall'), location: normalizeOptionalGenealogyText(item.regionFullName, 'regionFullName') || normalizeOptionalGenealogyText(item.regionName, 'regionName') || normalizeOptionalGenealogyText(item.originPlace, 'originPlace') || normalizeOptionalGenealogyText(item.addressDetail, 'addressDetail') || '地区待补', memberCount: item.memberCount, personCount, accessPreset: fromApiGenealogyAccess({ visibility: normalizeOptionalGenealogyText(item.visibility, 'visibility'), joinMode: normalizeOptionalGenealogyText(item.joinMode, 'joinMode') }), accessRole: item.canManage ? 'owner' : 'member', canManage: item.canManage, canEditContent: item.canEditContent, intro: normalizeOptionalGenealogyText(item.intro, 'intro'), joinTime: normalizeOptionalGenealogyText(item.joinTime, 'joinTime') } } 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, accessRole: genealogy.accessRole, canManage: genealogy.canManage, canEditContent: genealogy.canEditContent } }) if (new Set(normalized.map((item) => item.id)).size !== normalized.length) { throw createRequestError('我的家谱响应包含重复标识', 'GENEALOGY_RESPONSE_INVALID') } return normalized } const isPlainDataObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype const normalizeGenealogyCreateText = (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 normalizeGenealogyCreatePayload = (payload) => { if (!isPlainDataObject(payload)) throw new TypeError('创建家谱请求必须是普通对象') const allowedFields = new Set([ 'genealogyName', 'surname', 'regionCode', 'ancestralHall', 'originPlace', 'addressDetail', 'intro', 'coverOssId', 'visibility', 'joinMode' ]) if ( Object.getOwnPropertySymbols(payload).length > 0 || Object.getOwnPropertyNames(payload).some((field) => !allowedFields.has(field)) ) { throw new TypeError('创建家谱请求包含未声明字段') } const normalized = { genealogyName: normalizeGenealogyCreateText(payload.genealogyName, '谱名', { required: true }), surname: normalizeGenealogyCreateText(payload.surname, '姓氏', { required: true }), regionCode: normalizeGenealogyCreateText(payload.regionCode, '地区代码', { required: true }) } for (const field of ['ancestralHall', 'originPlace', 'addressDetail', 'intro', 'visibility', 'joinMode']) { const value = normalizeGenealogyCreateText(payload[field], field) if (value) normalized[field] = value } if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') { normalized.coverOssId = normalizeOssIdString(payload.coverOssId, '创建家谱字段 coverOssId') } return normalized } const normalizeGenealogyUpdatePayload = (payload) => { if (!isPlainDataObject(payload)) throw new TypeError('更新家谱请求必须是普通对象') const allowedFields = new Set([ 'genealogyName', 'surname', 'regionCode', 'ancestralHall', 'originPlace', 'addressDetail', 'intro', 'coverOssId', 'visibility', 'joinMode' ]) if ( Object.getOwnPropertySymbols(payload).length > 0 || Object.getOwnPropertyNames(payload).some((field) => !allowedFields.has(field)) ) { throw new TypeError('更新家谱请求包含未声明字段') } const normalized = {} for (const field of [ 'genealogyName', 'surname', 'regionCode', 'ancestralHall', 'originPlace', 'addressDetail', 'intro', 'visibility', 'joinMode' ]) { if (!Object.prototype.hasOwnProperty.call(payload, field)) continue const value = normalizeGenealogyCreateText(payload[field], field) if (value) normalized[field] = value } if (Object.prototype.hasOwnProperty.call(payload, 'coverOssId') && payload.coverOssId !== null && payload.coverOssId !== '') { normalized.coverOssId = normalizeOssIdString(payload.coverOssId, '更新家谱字段 coverOssId') } if (!Object.keys(normalized).length) throw new TypeError('请至少填写一项需要更新的家谱信息') return normalized } const normalizeOptionalNonnegativeInteger = (value, label, code) => { if (value === undefined || value === null) return 0 if (!Number.isSafeInteger(value) || value < 0) { throw createRequestError(`${label}无效`, code) } return value } 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') } } const normalizeOptionalNumericId = (value, label, code) => { if (value === undefined || value === null || value === '') return null 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}无效`, code) } 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 = normalizeOptionalGenealogyText(value.genealogyName, 'genealogyName') const surname = normalizeOptionalGenealogyText(value.surname, 'surname') const regionCode = normalizeOptionalGenealogyText(value.regionCode, 'regionCode') if (!genealogyName || !surname || !regionCode) { throw createRequestError('家谱设置响应缺少名称、姓氏或地区', 'GENEALOGY_SETTINGS_RESPONSE_INVALID') } return { id, genealogyName, surname, ancestralHall: normalizeOptionalGenealogyText(value.ancestralHall, 'ancestralHall'), originPlace: normalizeOptionalGenealogyText(value.originPlace, 'originPlace'), regionCode, regionName: normalizeOptionalGenealogyText(value.regionName, 'regionName'), regionFullName: normalizeOptionalGenealogyText(value.regionFullName, 'regionFullName'), addressDetail: normalizeOptionalGenealogyText(value.addressDetail, 'addressDetail'), coverOssId: normalizeOptionalNumericId(value.coverOssId, 'coverOssId', 'GENEALOGY_SETTINGS_RESPONSE_INVALID'), intro: normalizeOptionalGenealogyText(value.intro, 'intro'), accessPreset: fromApiGenealogyAccess({ visibility: normalizeOptionalGenealogyText(value.visibility, 'visibility'), joinMode: normalizeOptionalGenealogyText(value.joinMode, 'joinMode') }), visibility: normalizeOptionalGenealogyText(value.visibility, 'visibility'), joinMode: normalizeOptionalGenealogyText(value.joinMode, 'joinMode'), memberCount: normalizeOptionalNonnegativeInteger(value.memberCount, '家谱成员数量', 'GENEALOGY_SETTINGS_RESPONSE_INVALID'), personCount: normalizeOptionalNonnegativeInteger(value.personCount, '家谱人物数量', 'GENEALOGY_SETTINGS_RESPONSE_INVALID'), canManage: value.canManage === true, canEditContent: value.canEditContent === true } } const normalizeRegionParentCode = (value) => { if (value === undefined || value === null || value === '') return '0' if (typeof value !== 'string' || !value.trim()) throw new TypeError('地区父级标识无效') return value.trim() } const normalizeRegionCode = (value) => { if (typeof value !== 'string' || !value.trim()) throw new TypeError('行政区划编码无效') return value.trim() } const normalizeRegionSearchPayload = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { throw new TypeError('行政区划搜索参数无效') } const keyword = typeof payload.keyword === 'string' ? payload.keyword.trim() : '' if (!keyword) throw new TypeError('行政区划搜索关键词无效') const data = { keyword } if (payload.level !== undefined && payload.level !== null && payload.level !== '') { if (!Number.isInteger(payload.level) || payload.level < 1 || payload.level > 5) { throw new TypeError('行政区划级别无效') } data.level = payload.level } if (payload.limit !== undefined && payload.limit !== null && payload.limit !== '') { if (!Number.isInteger(payload.limit)) throw new TypeError('行政区划搜索数量上限无效') data.limit = payload.limit } return data } const normalizeRegionSelectorItems = (value) => { if (!Array.isArray(value)) { throw createRequestError('地区选择响应不是列表', 'REGION_RESPONSE_INVALID') } return value.map((item) => { if (!item || typeof item !== 'object' || Array.isArray(item)) { throw createRequestError('地区选择响应包含无效条目', 'REGION_RESPONSE_INVALID') } const regionCode = normalizeOptionalGenealogyText(item.regionCode ?? item.value, 'regionCode') const label = normalizeOptionalGenealogyText(item.label ?? item.regionName, 'label') if (!regionCode || !label) { throw createRequestError('地区选择响应缺少地区名称或标识', 'REGION_RESPONSE_INVALID') } return { regionCode, label, parentCode: normalizeOptionalGenealogyText(item.parentCode, 'parentCode'), leaf: item.leaf === true, regionLevel: Number.isInteger(item.regionLevel) ? item.regionLevel : null } }) } 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') } const saveLogin = (loginResult) => { const token = loginResult?.access_token if (!token) throw new Error('登录响应未包含会话令牌') session.saveToken(token) return loginResult } const lineageTreeError = (message) => createRequestError(message, 'LINEAGE_TREE_RESPONSE_INVALID') const normalizeLineagePersonId = (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 lineageTreeError('世系树包含无效人物标识') } const normalizeLineageText = (value, label, { required = false } = {}) => { if (value === undefined || value === null) { if (required) throw lineageTreeError(`世系树缺少${label}`) return '' } if (typeof value !== 'string') throw lineageTreeError(`世系树${label}无效`) const normalized = value.trim() if (required && !normalized) throw lineageTreeError(`世系树缺少${label}`) return normalized } const lineageDatePart = (value, label) => { const normalized = normalizeLineageText(value, label) if (!normalized) return '' if (!/^\d{4}-\d{2}-\d{2}/.test(normalized)) { throw lineageTreeError(`世系树${label}无效`) } if (normalized.length === 10) return normalized const instant = new Date(normalized) if (Number.isNaN(instant.getTime())) throw lineageTreeError(`世系树${label}无效`) const chinaTime = new Date(instant.getTime() + 8 * 60 * 60 * 1000) const year = chinaTime.getUTCFullYear() const month = String(chinaTime.getUTCMonth() + 1).padStart(2, '0') const day = String(chinaTime.getUTCDate()).padStart(2, '0') return `${year}-${month}-${day}` } const normalizeLineageTree = (value) => { if (!Array.isArray(value)) throw lineageTreeError('世系树响应不是列表') const seen = new Set() const normalized = [] const normalizedById = new Map() const appendNode = (node, parentId, relationOverride = '', spouseOf = '') => { if (!node || typeof node !== 'object' || Array.isArray(node)) { throw lineageTreeError('世系树包含无效节点') } const id = normalizeLineagePersonId(node.personId) if (seen.has(id)) throw lineageTreeError('世系树包含重复人物标识') seen.add(id) if (seen.size > 5000) throw lineageTreeError('世系树节点数量超出客户端上限') if (!Number.isSafeInteger(node.generation) || node.generation < 1) { throw lineageTreeError('世系树人物世代无效') } const generationName = normalizeLineageText(node.generationName, '字辈') const relationName = normalizeLineageText(node.relationName, '人物关系') const birthDate = lineageDatePart(node.birthDate, '出生日期') const deathDate = lineageDatePart(node.deathDate, '逝世日期') const normalizedNode = { id, parentId, ...(spouseOf ? { spouseOf } : {}), name: normalizeLineageText(node.name, '人物姓名', { required: true }), relation: relationOverride || (parentId ? (relationName && relationName !== '配偶' ? relationName : '后代') : relationName || '始祖'), generation: node.generation, branch: generationName ? (generationName.endsWith('字辈') ? generationName : `${generationName}字辈`) : '字辈待补', years: birthDate || deathDate ? `${birthDate}—${deathDate}` : '生卒待补', sex: normalizeLineageText(node.sex, '性别'), personStatus: normalizeLineageText(node.personStatus, '人物状态') } normalized.push(normalizedNode) normalizedById.set(id, normalizedNode) return id } const walk = (node, parentId = null, depth = 0) => { if (depth > 64) throw lineageTreeError('世系树深度超出客户端上限') if (!node || typeof node !== 'object' || Array.isArray(node)) { throw lineageTreeError('世系树包含无效节点') } const spouses = node.spouses ?? [] const children = node.children ?? [] if (!Array.isArray(spouses) || !Array.isArray(children)) { throw lineageTreeError('世系树亲属集合无效') } const id = normalizeLineagePersonId(node.personId) const knownSpouseId = spouses .map((spouse) => normalizeLineagePersonId(spouse?.personId)) .find((spouseId) => normalizedById.has(spouseId)) || '' if (seen.has(id)) { if (knownSpouseId) return id throw lineageTreeError('世系树包含重复人物标识') } const knownSpouse = knownSpouseId ? normalizedById.get(knownSpouseId) : null const nodeParentId = knownSpouse ? knownSpouse.parentId : parentId const appendedId = appendNode(node, nodeParentId, knownSpouse ? '配偶' : '', knownSpouseId) spouses.forEach((spouse) => { const spouseId = normalizeLineagePersonId(spouse?.personId) if (seen.has(spouseId)) return appendNode(spouse, nodeParentId, '配偶', appendedId) }) children.forEach((child) => walk(child, knownSpouseId || appendedId, depth + 1)) } value.forEach((root) => walk(root)) return normalized } const lineagePersonError = (message) => createRequestError(message, 'LINEAGE_PERSON_RESPONSE_INVALID') const normalizeLineagePersonIdentity = (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 lineagePersonError(`成员详情${label}无效`) } const normalizeOptionalLineagePersonIdentity = (value, label) => { if (value === undefined || value === null || value === '') return '' return normalizeLineagePersonIdentity(value, label) } const normalizeLineagePersonText = (value, label, { required = false } = {}) => { if (value === undefined || value === null) { if (required) throw lineagePersonError(`成员详情缺少${label}`) return '' } if (typeof value !== 'string') throw lineagePersonError(`成员详情${label}无效`) const normalized = value.trim() if (required && !normalized) throw lineagePersonError(`成员详情缺少${label}`) return normalized } const normalizeLineagePersonDate = (value, label) => { const normalized = normalizeLineagePersonText(value, label) if (!normalized) return '' if (!/^\d{4}-\d{2}-\d{2}(?:[T\s].*)?$/.test(normalized)) { throw lineagePersonError(`成员详情${label}无效`) } const datePart = normalized.slice(0, 10) const parsed = new Date(`${datePart}T00:00:00Z`) if ( Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== datePart ) { throw lineagePersonError(`成员详情${label}无效`) } return datePart } const normalizeLineagePersonDetail = ( value, expectedGenealogyId, expectedPersonId, ) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw lineagePersonError('成员详情响应不是对象') } const genealogyId = normalizeLineagePersonIdentity(value.genealogyId, '家谱标识') const id = normalizeLineagePersonIdentity(value.personId, '人物标识') if (genealogyId !== expectedGenealogyId || id !== expectedPersonId) { throw lineagePersonError('成员详情响应标识与请求不匹配') } if (!Number.isSafeInteger(value.generation) || value.generation < 1) { throw lineagePersonError('成员详情世代无效') } const name = normalizeLineagePersonText(value.name, '姓名', { required: true }) const appUserId = normalizeOptionalLineagePersonIdentity(value.appUserId, '绑定用户标识') const bindingMode = value.bindingMode === undefined || value.bindingMode === null || value.bindingMode === '' ? (appUserId ? 'SPECIFIED' : 'NONE') : normalizeLineagePersonText(value.bindingMode, '身份认领方式') if (!['NONE', 'SELF', 'SPECIFIED'].includes(bindingMode)) { throw lineagePersonError('成员详情身份认领方式无效') } const personNo = normalizeLineagePersonText(value.personNo, '人物编号') const generationName = normalizeLineagePersonText(value.generationName, '字辈') const aliasName = normalizeLineagePersonText(value.aliasName, '别名或曾用名') const avatarOssId = normalizeOptionalLineagePersonIdentity(value.avatarOssId, '头像文件标识') const birthDate = normalizeLineagePersonDate(value.birthDate, '出生日期') const deathDate = normalizeLineagePersonDate(value.deathDate, '逝世日期') const birthLunar = normalizeLineagePersonText(value.birthLunar, '出生农历') const deathLunar = normalizeLineagePersonText(value.deathLunar, '逝世农历') const birthPlace = normalizeLineagePersonText(value.birthPlace, '出生地') const deathPlace = normalizeLineagePersonText(value.deathPlace, '逝世地') const burialPlace = normalizeLineagePersonText(value.burialPlace, '安葬地') const spouseNames = normalizeLineagePersonText(value.spouseNames, '配偶姓名') const personStatus = normalizeLineagePersonText(value.personStatus, '人物状态') const biography = normalizeLineagePersonText(value.biography, '生平') const remark = normalizeLineagePersonText(value.remark, '备注') const relationName = normalizeLineagePersonText(value.relationName, '关系显示名称') const sortOrder = value.sortOrder === undefined || value.sortOrder === null ? null : normalizeOptionalSafeInteger(value.sortOrder, '人物排序值') const relatives = [] for (const relation of [ { id: value.fatherId, name: value.fatherName, label: '父亲' }, { id: value.motherId, name: value.motherName, label: '母亲' } ]) { if (relation.id === undefined || relation.id === null) continue const relativeId = normalizeLineagePersonIdentity(relation.id, `${relation.label}标识`) if (relativeId === id || relatives.some((item) => item.id === relativeId)) { throw lineagePersonError('成员详情亲属标识冲突') } relatives.push({ id: relativeId, name: normalizeLineagePersonText(relation.name, `${relation.label}姓名`) || relation.label, relation: relation.label }) } return { id, genealogyId, genealogyName: normalizeLineagePersonText(value.genealogyName, '家谱名称') || '当前家谱', name, appUserId, bindingMode, personNo, aliasName, generation: value.generation, generationName, avatarOssId, relation: value.generation === 1 ? '始祖' : '家谱成员', branch: generationName ? (generationName.endsWith('字辈') ? generationName : `${generationName}字辈`) : '字辈待补', sex: normalizeLineagePersonText(value.sex, '性别'), birthDate, birthLunar, deathDate, deathLunar, years: birthDate || deathDate ? `${birthDate}—${deathDate}` : '生卒待补', birthplace: birthPlace, deathPlace, burialPlace, spouseNames, personStatus, biography, remark, relationName, sortOrder, status: ['DECEASED', 'DEAD'].includes(personStatus.toUpperCase()) ? 'deceased' : 'normal', relatives } } const normalizeLineagePersonPage = (value, expectedGenealogyId) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw lineagePersonError('成员分页响应不是对象') } if (!Array.isArray(value.rows)) { throw lineagePersonError('成员分页缺少 rows') } if (!Number.isSafeInteger(value.total) || value.total < 0) { throw lineagePersonError('成员分页 total 无效') } const rows = value.rows.map((item) => normalizeLineagePersonDetail( item, expectedGenealogyId, normalizeLineagePersonIdentity(item?.personId, '人物标识'), ), ) if (new Set(rows.map((item) => item.id)).size !== rows.length) { throw lineagePersonError('成员分页包含重复人物标识') } return { rows, total: value.total } } const normalizeFeedCommentId = (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 } const normalizeFeedComments = (value, expectedGenealogyId, expectedFeedId) => { 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 = normalizeFeedCommentId(item.genealogyId, '家谱标识') const feedId = normalizeFeedCommentId(item.feedId, '动态标识') if (genealogyId !== expectedGenealogyId || feedId !== expectedFeedId) { throw createRequestError('家族动态评论归属与请求不匹配', 'FEED_COMMENT_RESPONSE_INVALID') } return { id: normalizeFeedCommentId(item.commentId, '标识'), author: normalizeFeedCommentText(item.appUserNickName, '用户昵称') || '未署名成员', content: normalizeFeedCommentText(item.commentContent, '评论内容', { required: true }), time: normalizeFeedCommentText(item.createTime, '创建时间'), parentCommentId: item.parentCommentId === undefined || item.parentCommentId === null ? null : normalizeFeedCommentId(item.parentCommentId, '父评论标识'), replyCount: Number.isSafeInteger(item.replyCount) && item.replyCount >= 0 ? item.replyCount : 0, } }) if (new Set(comments.map((item) => item.id)).size !== comments.length) { throw createRequestError('家族动态评论包含重复标识', 'FEED_COMMENT_RESPONSE_INVALID') } return comments } const normalizeFeedCommentPayload = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) { throw new TypeError('家族动态评论请求必须是普通对象') } const fields = Object.keys(payload) if (fields.some((field) => !['parentCommentId', 'commentContent'].includes(field))) { throw new TypeError('家族动态评论请求包含未声明字段') } const commentContent = normalizeFeedCommentText(payload.commentContent, '评论内容', { required: true }) if (Array.from(commentContent).length > 1000) throw new TypeError('家族动态评论不能超过 1000 个字符') const data = { commentContent } if (payload.parentCommentId !== undefined && payload.parentCommentId !== null) { data.parentCommentId = normalizeFeedCommentId(payload.parentCommentId, '父评论标识') } return data } 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 data = { feedContent: payload.feedContent.trim() } for (const field of ['feedType', 'status']) { const value = normalizeOptionalAuthText(payload[field], field) if (value) data[field] = value } const mediaOssIds = normalizeOptionalOssIdList(payload.mediaOssIds) if (mediaOssIds) data.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 必须是安全整数') data.sortOrder = sortOrder } return data } 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 = normalizeOptionalGenealogyText(item.feedContent, 'feedContent') if (!id || !content) { throw createRequestError('家族动态响应缺少标识或内容', 'FEED_RESPONSE_INVALID') } return { id, genealogyId: expectedGenealogyId, content, publisher: normalizeOptionalGenealogyText(item.publisherNickName, 'publisherNickName') || '家族成员', type: normalizeOptionalGenealogyText(item.feedType, 'feedType') || '文字动态', time: normalizeOptionalGenealogyText(item.createTime, 'createTime'), mediaOssIds: normalizeOptionalGenealogyText(item.mediaOssIds, 'mediaOssIds'), likeCount: normalizeOptionalNonnegativeInteger(item.likeCount, '动态点赞数', 'FEED_RESPONSE_INVALID'), commentCount: normalizeOptionalNonnegativeInteger(item.commentCount, '动态评论数', 'FEED_RESPONSE_INVALID'), likedByMe: typeof item.likedByMe === 'boolean' ? item.likedByMe : null, pinned: normalizeOptionalGenealogyText(item.pinned, 'pinned') } }) if (new Set(rows.map((item) => item.id)).size !== rows.length) { throw createRequestError('家族动态响应包含重复标识', 'FEED_RESPONSE_INVALID') } return rows } const normalizeFamilyFeed = (value, expectedGenealogyId, expectedFeedId) => { const [feed] = normalizeFamilyFeedRows([value], expectedGenealogyId) if (feed.id !== expectedFeedId) { throw createRequestError('动态详情响应标识不匹配', 'FEED_RESPONSE_INVALID') } return feed } const normalizeFamilyArticleRows = (value, expectedGenealogyId) => { if (!Array.isArray(value)) { throw createRequestError('谱文响应不是列表', 'ARTICLE_RESPONSE_INVALID') } const rows = value.map((item) => { if (!item || typeof item !== 'object' || Array.isArray(item)) { throw createRequestError('谱文响应包含无效条目', 'ARTICLE_RESPONSE_INVALID') } const genealogyId = normalizeOptionalNumericId(item.genealogyId, '谱文家谱标识', 'ARTICLE_RESPONSE_INVALID') if (genealogyId && genealogyId !== expectedGenealogyId) { throw createRequestError('谱文归属与请求不匹配', 'ARTICLE_RESPONSE_INVALID') } const id = normalizeOptionalNumericId(item.articleId ?? item.id, '谱文标识', 'ARTICLE_RESPONSE_INVALID') const title = normalizeOptionalGenealogyText(item.articleTitle ?? item.title, 'articleTitle') const content = normalizeOptionalGenealogyText(item.articleContent, 'articleContent') if (!id || !title) { throw createRequestError('谱文响应缺少标识或标题', 'ARTICLE_RESPONSE_INVALID') } return { id, genealogyId: expectedGenealogyId, title, summary: normalizeOptionalGenealogyText(item.articleSummary ?? item.summary, 'articleSummary'), content, author: normalizeOptionalGenealogyText(item.authorName ?? item.author, 'authorName') || '家族成员', time: normalizeOptionalGenealogyText(item.publishTime ?? item.updatedAt, 'publishTime'), category: normalizeOptionalGenealogyText(item.categoryName ?? item.category, 'categoryName'), coverOssId: normalizeOptionalNumericId(item.coverOssId, 'coverOssId', 'ARTICLE_RESPONSE_INVALID'), viewCount: normalizeOptionalNonnegativeInteger(item.viewCount, '谱文阅读数', 'ARTICLE_RESPONSE_INVALID') } }) if (new Set(rows.map((item) => item.id)).size !== rows.length) { throw createRequestError('谱文响应包含重复标识', 'ARTICLE_RESPONSE_INVALID') } return rows } const normalizeFamilyArticle = (value, expectedGenealogyId, expectedArticleId) => { const [article] = normalizeFamilyArticleRows([value], expectedGenealogyId) if (article.id !== expectedArticleId) { throw createRequestError('谱文详情响应标识不匹配', 'ARTICLE_RESPONSE_INVALID') } return article } const normalizeArticleCreatePayload = (payload) => { const allowedFields = new Set([ 'categoryId', 'articleTitle', 'articleSummary', 'coverOssId', 'articleContent', 'authorName', 'sortOrder', 'status' ]) assertPlainPayload(payload, allowedFields, '谱文请求') const articleTitle = typeof payload.articleTitle === 'string' ? payload.articleTitle.trim() : '' const articleContent = typeof payload.articleContent === 'string' ? payload.articleContent.trim() : '' if (!articleTitle) throw new TypeError('谱文标题不能为空') if (!articleContent) throw new TypeError('谱文正文不能为空') const data = { articleTitle, articleContent } for (const field of ['articleSummary', 'authorName', 'status']) { const value = normalizeOptionalAuthText(payload[field], field) if (value) data[field] = value } for (const field of ['categoryId', 'sortOrder']) { if (payload[field] === undefined || payload[field] === null || payload[field] === '') continue const value = typeof payload[field] === 'number' ? payload[field] : Number(payload[field]) if (!Number.isSafeInteger(value)) throw new TypeError(`${field} 必须是安全整数`) data[field] = value } if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') { data.coverOssId = normalizeOssIdString(payload.coverOssId, 'coverOssId') } return data } 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 data = { albumName: payload.albumName.trim() } for (const field of ['albumDesc', 'status']) { const value = normalizeOptionalAuthText(payload[field], field) if (value) data[field] = value } if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') { data.coverOssId = normalizeOssIdString(payload.coverOssId, 'coverOssId') } for (const field of ['sortOrder']) { if (payload[field] === undefined || payload[field] === null || payload[field] === '') continue const value = typeof payload[field] === 'number' ? payload[field] : Number(payload[field]) if (!Number.isSafeInteger(value)) throw new TypeError(`${field} 必须是安全整数`) data[field] = value } return data } const normalizeAlbumPhotoCreatePayload = (payload) => { const allowedFields = new Set(['ossId', 'photoTitle', 'photoDesc', 'photographer', 'shootTime', 'sortOrder', 'status']) assertPlainPayload(payload, allowedFields, '相册照片请求') const ossId = normalizeOssIdString(payload.ossId, '相册照片 ossId') const data = { ossId } for (const field of ['photoTitle', 'photoDesc', 'photographer', 'shootTime', 'status']) { const value = normalizeOptionalAuthText(payload[field], field) if (value) data[field] = value } const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder') if (sortOrder !== undefined) data.sortOrder = sortOrder return data } const normalizeVideoPayload = (payload) => { const allowedFields = new Set(['videoTitle', 'videoDesc', 'coverOssId', 'videoOssId', 'durationSeconds', 'sortOrder', 'status']) assertPlainPayload(payload, allowedFields, '视频请求') const videoTitle = normalizeOptionalAuthText(payload.videoTitle, '视频标题') if (!videoTitle) throw new TypeError('视频标题不能为空') const videoOssId = normalizeOssIdString(payload.videoOssId, '视频文件 OSS ID') const data = { videoTitle, videoOssId } const videoDesc = normalizeOptionalAuthText(payload.videoDesc, '视频说明') if (videoDesc) data.videoDesc = videoDesc if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') { data.coverOssId = normalizeOssIdString(payload.coverOssId, '视频封面 OSS ID') } for (const field of ['durationSeconds', 'sortOrder']) { const value = normalizeOptionalSafeInteger(payload[field], field) if (value !== undefined) data[field] = value } if (Object.prototype.hasOwnProperty.call(payload, 'status')) { const status = normalizeOptionalAuthText(payload.status, 'status') if (status) { if (!['0', '1'].includes(status)) throw new TypeError('status 必须为 0 或 1') data.status = status } } return data } const normalizeCeremonyPayload = (payload) => { const allowedFields = new Set(['ceremonyType', 'ceremonyTitle', 'ceremonyDesc', 'ceremonyTime', 'location', 'locationAddress', 'longitude', 'latitude', 'coverOssId', 'sortOrder', 'status']) assertPlainPayload(payload, allowedFields, '礼仪活动请求') const ceremonyType = normalizeOptionalAuthText(payload.ceremonyType, 'ceremonyType') const ceremonyTitle = normalizeOptionalAuthText(payload.ceremonyTitle, 'ceremonyTitle') if (!ceremonyType || !ceremonyTitle) throw new TypeError('请填写活动类型和活动标题') const data = { ceremonyType, ceremonyTitle } for (const field of ['ceremonyDesc', 'ceremonyTime', 'location', 'locationAddress']) { const value = normalizeOptionalAuthText(payload[field], field) if (value) data[field] = value } if (Object.prototype.hasOwnProperty.call(payload, 'status')) { const status = normalizeOptionalAuthText(payload.status, 'status') if (status) { if (!['0', '1'].includes(status)) throw new TypeError('status 必须为 0 或 1') data.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 必须是有限数字') } data.longitude = longitude data.latitude = latitude } if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') { data.coverOssId = normalizeOssIdString(payload.coverOssId, 'coverOssId') } const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder') if (sortOrder !== undefined) { data.sortOrder = sortOrder } return data } 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 data = { giftAmount } for (const field of ['giverName', 'giftMessage']) { const value = normalizeOptionalAuthText(payload[field], field) if (value) data[field] = value } return data } const normalizeOptionalOssIdList = (value) => { if (value === undefined || value === null || value === '') return undefined if (typeof value !== 'string') throw new TypeError('mediaOssIds 必须是字符串') const normalized = value.trim() if (!/^[1-9]\d*(,[1-9]\d*)*$/.test(normalized)) { throw new TypeError('mediaOssIds 必须是以英文逗号分隔的正整数文件标识') } return normalized } const normalizeOptionalSafeInteger = (value, field) => { if (value === undefined || value === null || value === '') return undefined const numericValue = typeof value === 'number' ? value : Number(value) if (!Number.isSafeInteger(numericValue)) throw new TypeError(`${field} 必须是安全整数`) return numericValue } const normalizeRelativeRecordCreatePayload = (payload) => { const allowedFields = new Set(['relativeName', 'relationName', 'eventName', 'eventTime', 'giftAmount', 'recordContent', 'mediaOssIds', 'sortOrder', 'status']) assertPlainPayload(payload, allowedFields, '亲友往来请求') const relativeName = typeof payload.relativeName === 'string' ? payload.relativeName.trim() : '' if (!relativeName) throw new TypeError('亲友姓名不能为空') const data = { relativeName } for (const field of ['relationName', 'eventName', 'eventTime', 'recordContent', 'status']) { const value = normalizeOptionalAuthText(payload[field], field) if (value) data[field] = value } if (payload.giftAmount !== undefined && payload.giftAmount !== '' && payload.giftAmount !== null) { const giftAmount = typeof payload.giftAmount === 'number' ? payload.giftAmount : Number(payload.giftAmount) if (!Number.isFinite(giftAmount)) { throw new TypeError('亲友往来礼金金额必须是有限数字') } data.giftAmount = giftAmount } const mediaOssIds = normalizeOptionalOssIdList(payload.mediaOssIds) if (mediaOssIds) data.mediaOssIds = mediaOssIds const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder') if (sortOrder !== undefined) data.sortOrder = sortOrder return data } const normalizeGrowthRecordCreatePayload = (payload) => { const allowedFields = new Set(['lineagePersonId', 'recordType', 'recordTitle', 'recordContent', 'recordDate', 'remindTime', 'mediaOssIds', 'sortOrder', 'status']) assertPlainPayload(payload, allowedFields, '成长记录请求') const recordTitle = typeof payload.recordTitle === 'string' ? payload.recordTitle.trim() : '' if (!recordTitle) throw new TypeError('成长记录标题不能为空') const data = { recordTitle } for (const field of ['recordType', 'recordContent', 'recordDate', 'remindTime', 'status']) { const value = normalizeOptionalAuthText(payload[field], field) if (value) data[field] = value } const lineagePersonId = normalizeOptionalSafeInteger(payload.lineagePersonId, 'lineagePersonId') if (lineagePersonId !== undefined) data.lineagePersonId = lineagePersonId const mediaOssIds = normalizeOptionalOssIdList(payload.mediaOssIds) if (mediaOssIds) data.mediaOssIds = mediaOssIds const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder') if (sortOrder !== undefined) data.sortOrder = sortOrder return data } const normalizeMemoCreatePayload = (payload) => { const allowedFields = new Set(['memoTitle', 'memoContent', 'remindTime', 'completed', 'mediaOssIds', 'sortOrder', 'status']) assertPlainPayload(payload, allowedFields, '备忘请求') const memoTitle = typeof payload.memoTitle === 'string' ? payload.memoTitle.trim() : '' if (!memoTitle) throw new TypeError('备忘标题不能为空') const data = { memoTitle } for (const field of ['memoContent', 'remindTime', 'completed', 'status']) { const value = normalizeOptionalAuthText(payload[field], field) if (value) data[field] = value } const mediaOssIds = normalizeOptionalOssIdList(payload.mediaOssIds) if (mediaOssIds) data.mediaOssIds = mediaOssIds const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder') if (sortOrder !== undefined) data.sortOrder = sortOrder return data } const normalizeMeritRecordCreatePayload = (payload) => { const allowedFields = new Set(['donorName', 'meritTitle', 'meritType', 'meritContent', 'amount', 'meritTime', 'sortOrder', 'status']) assertPlainPayload(payload, allowedFields, '功德记录请求') const donorName = typeof payload.donorName === 'string' ? payload.donorName.trim() : '' const meritTitle = typeof payload.meritTitle === 'string' ? payload.meritTitle.trim() : '' if (!donorName || !meritTitle) throw new TypeError('功德记录捐赠人和标题不能为空') const data = { donorName, meritTitle } for (const field of ['meritType', 'meritContent', 'meritTime', 'status']) { const value = normalizeOptionalAuthText(payload[field], field) if (value) data[field] = value } if (payload.amount !== undefined && payload.amount !== '' && payload.amount !== null) { const amount = typeof payload.amount === 'number' ? payload.amount : Number(payload.amount) if (!Number.isFinite(amount)) throw new TypeError('功德金额必须是有限数字') data.amount = amount } const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder') if (sortOrder !== undefined) data.sortOrder = sortOrder return data } const normalizeResourcePathId = (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 new TypeError(`${label}必须是有效的正整数标识`) } const normalizeNotificationText = (value, field) => { if (value === undefined || value === null) return '' if (typeof value !== 'string') { throw createRequestError(`通知详情字段 ${field} 无效`, 'NOTIFICATION_DETAIL_RESPONSE_INVALID') } return value } const normalizeNotificationDetail = (value, expectedNotificationId) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw createRequestError('通知详情响应格式无效', 'NOTIFICATION_DETAIL_RESPONSE_INVALID') } const notificationId = normalizeOptionalNumericId(value.notificationId, '通知标识', 'NOTIFICATION_DETAIL_RESPONSE_INVALID') if (!notificationId || notificationId !== expectedNotificationId) { throw createRequestError('通知详情响应标识不匹配', 'NOTIFICATION_DETAIL_RESPONSE_INVALID') } return { notificationId, genealogyId: normalizeOptionalNumericId(value.genealogyId, '通知家谱标识', 'NOTIFICATION_DETAIL_RESPONSE_INVALID'), genealogyNo: normalizeNotificationText(value.genealogyNo, 'genealogyNo'), genealogyName: normalizeNotificationText(value.genealogyName, 'genealogyName'), senderUserId: normalizeOptionalNumericId(value.senderUserId, '通知发送人标识', 'NOTIFICATION_DETAIL_RESPONSE_INVALID'), senderNickName: normalizeNotificationText(value.senderNickName, 'senderNickName'), senderPhone: normalizeNotificationText(value.senderPhone, 'senderPhone'), noticeType: normalizeNotificationText(value.noticeType, 'noticeType'), noticeTitle: normalizeNotificationText(value.noticeTitle, 'noticeTitle'), noticeContent: normalizeNotificationText(value.noticeContent, 'noticeContent'), bizType: normalizeNotificationText(value.bizType, 'bizType'), bizId: normalizeOptionalNumericId(value.bizId, '通知关联业务标识', 'NOTIFICATION_DETAIL_RESPONSE_INVALID'), bizSummary: normalizeNotificationText(value.bizSummary, 'bizSummary'), publishTime: normalizeNotificationText(value.publishTime, 'publishTime'), readStatus: normalizeNotificationText(value.readStatus, 'readStatus'), readTime: normalizeNotificationText(value.readTime, 'readTime'), status: normalizeNotificationText(value.status, 'status'), remark: normalizeNotificationText(value.remark, 'remark') } } const requireRemoteResource = (label, operation) => { if (hasRemoteConfig()) return throw createRequestError(`${label}${operation}需要真实服务,当前本地预览不会伪造结果`, operation === '读取' ? 'REMOTE_READ_REQUIRED' : 'REMOTE_WRITE_REQUIRED') } const readRemoteList = async (url, label, requestOptions = {}) => { requireRemoteResource(label, '读取') const result = await requestStrict({ url, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) if (!Array.isArray(result)) { throw createRequestError(`${label}响应不是列表`, 'LIST_RESPONSE_INVALID') } return result } const readRemoteObject = async (url, label, requestOptions = {}) => { requireRemoteResource(label, '读取') const result = await requestStrict({ url, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) if (!result || typeof result !== 'object' || Array.isArray(result)) { throw createRequestError(`${label}响应不是对象`, 'OBJECT_RESPONSE_INVALID') } return result } const writeRemoteObject = (url, method, data, label, requestOptions = {}) => { requireRemoteResource(label, '写入') return requestStrict({ url, method, ...(data === undefined ? {} : { data }) }, { requestController: requestOptions.requestController ?? null }) } const writeRemoteVoid = async (url, method, data, label, requestOptions = {}) => { requireRemoteResource(label, '写入') await requestStrict({ url, method, ...(data === undefined ? {} : { data }) }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null } const normalizePhoneChangePayload = (payload) => { assertPlainPayload(payload, new Set(['phone', 'smsCode']), '换绑手机号请求') if (typeof payload.phone !== 'string' || !/^1\d{10}$/.test(payload.phone.trim())) { throw new TypeError('新手机号格式无效') } return { phone: payload.phone.trim(), smsCode: assertSmsCode(payload.smsCode) } } const normalizeMemberUpdatePayload = (payload) => { assertPlainPayload(payload, new Set(['memberName', 'relationName', 'roleType', 'lineagePersonId']), '成员更新请求') const data = {} for (const field of ['memberName', 'relationName', 'roleType']) { const value = normalizeOptionalAuthText(payload[field], field) if (value) data[field] = value } if (payload.lineagePersonId !== undefined && payload.lineagePersonId !== null && payload.lineagePersonId !== '') { data.lineagePersonId = Number(normalizeResourcePathId(payload.lineagePersonId, '世系人物标识')) } if (!Object.keys(data).length) throw new TypeError('请至少填写一项要更新的成员信息') return data } const normalizeVipOrderPayload = (payload) => { assertPlainPayload(payload, new Set(['packageId', 'genealogyId', 'payType']), 'VIP 订单请求') const data = { packageId: Number(normalizeResourcePathId(payload.packageId, '套餐标识')) } if (payload.genealogyId !== undefined && payload.genealogyId !== null && payload.genealogyId !== '') { data.genealogyId = Number(normalizeResourcePathId(payload.genealogyId, '家谱标识')) } const payType = normalizeOptionalAuthText(payload.payType, '支付方式') if (payType) data.payType = payType return data } const normalizeSiteArticleQuery = (payload = {}) => { assertPlainPayload(payload, new Set(['articleType', 'limit']), '官网文章查询') const data = {} const articleType = normalizeOptionalAuthText(payload.articleType, '官网文章类型') if (articleType) data.articleType = articleType if (payload.limit !== undefined && payload.limit !== null && payload.limit !== '') { const limit = typeof payload.limit === 'number' ? payload.limit : Number(payload.limit) if (!Number.isSafeInteger(limit) || limit < 1) throw new TypeError('官网文章数量上限必须是正整数') data.limit = limit } return data } const normalizeSitePageKey = (pageKey) => { const normalized = normalizeOptionalAuthText(pageKey, '官网页面标识') if (!normalized) throw new TypeError('官网页面标识不能为空') return normalized } const normalizeJoinApplicationPayload = (payload) => { assertPlainPayload(payload, new Set(['applicantName', 'phone', 'relationDesc', 'applyReason', 'inviterUserId']), '加入申请请求') const data = {} for (const field of ['applicantName', 'phone', 'relationDesc', 'applyReason']) { const value = normalizeOptionalAuthText(payload[field], field) if (value) data[field] = value } if (payload.inviterUserId !== undefined && payload.inviterUserId !== null && payload.inviterUserId !== '') { data.inviterUserId = Number(normalizeResourcePathId(payload.inviterUserId, '邀请人标识')) } return data } const normalizeJoinAuditPayload = (payload) => { assertPlainPayload(payload, new Set(['status', 'auditRemark']), '加入申请审核请求') const status = normalizeOptionalAuthText(payload.status, '审核状态') if (!status) throw new TypeError('加入申请审核缺少 status') const data = { status } const auditRemark = normalizeOptionalAuthText(payload.auditRemark, '审核备注') if (auditRemark) data.auditRemark = auditRemark return data } const normalizeCeremonyInviteePayload = (payload) => { assertPlainPayload(payload, new Set(['inviteeUserIds']), '活动受邀人请求') if (!Array.isArray(payload.inviteeUserIds)) { throw new TypeError('inviteeUserIds必须是数组') } const inviteeUserIds = payload.inviteeUserIds.map((value) => { const id = normalizeResourcePathId(value, '受邀业务用户标识') const numericId = Number(id) if (!Number.isSafeInteger(numericId)) { throw createRequestError('受邀业务用户标识超出 APP 安全整数范围,需后端统一字符串 ID 合同', 'CEREMONY_INVITEE_ID_UNSAFE') } return numericId }) if (new Set(inviteeUserIds).size !== inviteeUserIds.length) { throw new TypeError('inviteeUserIds不能包含重复标识') } return { inviteeUserIds } } const normalizeCeremonyInvitationResponsePayload = (payload) => { assertPlainPayload(payload, new Set(['inviteStatus']), '活动邀请响应请求') if (!['ACCEPTED', 'DECLINED'].includes(payload.inviteStatus)) { throw new TypeError('inviteStatus仅允许ACCEPTED或DECLINED') } return { inviteStatus: payload.inviteStatus } } const normalizeGenerationPoemWritePayload = (payload) => { assertPlainPayload(payload, new Set(['generationNo', 'generationText', 'description', 'sortOrder', 'status']), '字辈请求') const generationNo = Number(payload.generationNo) if (!Number.isSafeInteger(generationNo) || generationNo < 1 || generationNo > 2147483647) { throw new TypeError('世代序号必须是 1 到 2147483647 的整数') } const generationText = normalizeOptionalAuthText(payload.generationText, '字辈文字') if (!generationText || Array.from(generationText).length > 50) throw new TypeError('字辈文字不能为空且不能超过 50 个字符') const data = { generationNo, generationText } const description = normalizeOptionalAuthText(payload.description, '字辈说明') if (description) { if (Array.from(description).length > 500) throw new TypeError('字辈说明不能超过 500 个字符') data.description = description } if (payload.sortOrder !== undefined && payload.sortOrder !== null && payload.sortOrder !== '') { const sortOrder = Number(payload.sortOrder) if (!Number.isSafeInteger(sortOrder)) throw new TypeError('排序值必须是整数') data.sortOrder = sortOrder } const status = normalizeOptionalAuthText(payload.status, '状态') if (status) data.status = status return data } const normalizeGenerationPoemId = (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}无效`, 'GENERATION_POEM_RESPONSE_INVALID') } const normalizeGenerationPoemNumber = (value, label) => { if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return value if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) { const normalized = Number(value) if (Number.isSafeInteger(normalized)) return normalized } throw createRequestError(`字辈${label}无效`, 'GENERATION_POEM_RESPONSE_INVALID') } const normalizeGenerationPoemText = (value, label, { required = false, maxLength = null } = {}) => { if (value === undefined || value === null) { if (required) throw createRequestError(`字辈缺少${label}`, 'GENERATION_POEM_RESPONSE_INVALID') return '' } if (typeof value !== 'string') throw createRequestError(`字辈${label}无效`, 'GENERATION_POEM_RESPONSE_INVALID') const normalized = value.trim() if (required && !normalized) throw createRequestError(`字辈缺少${label}`, 'GENERATION_POEM_RESPONSE_INVALID') if (maxLength !== null && Array.from(normalized).length > maxLength) { throw createRequestError(`字辈${label}超出合同长度`, 'GENERATION_POEM_RESPONSE_INVALID') } return normalized } const normalizeGenerationPoemStatus = (value) => { if (value === '0' || value === '1') return value throw createRequestError('字辈状态无效', 'GENERATION_POEM_RESPONSE_INVALID') } const normalizeGenerationPoemRows = (value, expectedGenealogyId) => { if (!Array.isArray(value)) throw createRequestError('字辈响应不是列表', 'GENERATION_POEM_RESPONSE_INVALID') const rows = value.map((item) => { if (!item || typeof item !== 'object' || Array.isArray(item)) { throw createRequestError('字辈响应包含无效条目', 'GENERATION_POEM_RESPONSE_INVALID') } const genealogyId = normalizeGenerationPoemId(item.genealogyId, '家谱标识') if (genealogyId !== expectedGenealogyId) { throw createRequestError('字辈响应家谱标识与请求不匹配', 'GENERATION_POEM_RESPONSE_INVALID') } return { poemId: normalizeGenerationPoemId(item.poemId, '标识'), genealogyId, genealogyNo: normalizeGenerationPoemText(item.genealogyNo, '家谱编号'), genealogyName: normalizeGenerationPoemText(item.genealogyName, '家谱名称'), generationNo: normalizeGenerationPoemNumber(item.generationNo, '世代'), generationText: normalizeGenerationPoemText(item.generationText, '文字', { required: true, maxLength: 50 }), description: normalizeGenerationPoemText(item.description, '说明', { maxLength: 500 }), sortOrder: item.sortOrder, status: normalizeGenerationPoemStatus(item.status), } }) if (new Set(rows.map((item) => item.poemId)).size !== rows.length) { throw createRequestError('字辈响应包含重复标识', 'GENERATION_POEM_RESPONSE_INVALID') } if (new Set(rows.map((item) => item.generationNo)).size !== rows.length) { throw createRequestError('字辈响应包含重复世代', 'GENERATION_POEM_RESPONSE_INVALID') } return rows.sort((left, right) => left.generationNo - right.generationNo) } const normalizeGenerationPoemBatchPayload = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) { throw new TypeError('字辈批量请求必须是普通对象') } if (Object.keys(payload).some((field) => !['poemText', 'disableMissing'].includes(field))) { throw new TypeError('字辈批量请求包含未声明字段') } const poemText = normalizeGenerationPoemText(payload.poemText, '内容', { required: true, maxLength: 26000 }) const data = { poemText } if (payload.disableMissing !== undefined) { if (typeof payload.disableMissing !== 'boolean') throw new TypeError('字辈停用策略必须是布尔值') data.disableMissing = payload.disableMissing } return data } const normalizeGenerationPoemPreview = (value, expectedGenealogyId) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw createRequestError('字辈批量预览响应不是对象', 'GENERATION_POEM_RESPONSE_INVALID') } const genealogyId = normalizeGenerationPoemId(value.genealogyId, '预览家谱标识') if (genealogyId !== expectedGenealogyId) { throw createRequestError('字辈预览家谱标识与请求不匹配', 'GENERATION_POEM_RESPONSE_INVALID') } const counts = ['createCount', 'updateCount', 'keepCount', 'disableCount'] const preview = { genealogyId, items: Array.isArray(value.items) ? value.items : null } for (const field of counts) { if (!Number.isSafeInteger(value[field]) || value[field] < 0) { throw createRequestError(`字辈预览${field}无效`, 'GENERATION_POEM_RESPONSE_INVALID') } preview[field] = value[field] } if (preview.items === null) { throw createRequestError('字辈预览缺少明细列表', 'GENERATION_POEM_RESPONSE_INVALID') } return preview } const normalizeLineageWritePayload = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) { throw new TypeError('人物写入请求必须是普通对象') } const allowedFields = new Set([ 'bindingMode', 'appUserId', 'personNo', 'name', 'aliasName', 'sex', 'generationName', 'fatherId', 'motherId', 'avatarOssId', 'birthDate', 'birthLunar', 'birthPlace', 'deathDate', 'deathLunar', 'deathPlace', 'burialPlace', 'biography', 'remark', 'relationName', 'generation', 'personStatus', 'sortOrder' ]) for (const field of Object.keys(payload)) { if (!allowedFields.has(field)) throw new TypeError(`人物写入包含未声明字段:${field}`) } const name = normalizeLineagePersonText(payload.name, '姓名', { required: true }) if (name.length > 20) throw new TypeError('人物姓名长度超出当前页面合同') const bindingMode = normalizeLineagePersonText(payload.bindingMode, '身份认领方式') if (!['NONE', 'SELF', 'SPECIFIED'].includes(bindingMode)) { throw new TypeError('人物身份认领方式必须是 NONE、SELF 或 SPECIFIED') } const data = { bindingMode, name } if (bindingMode === 'SPECIFIED') { if (payload.appUserId === undefined || payload.appUserId === null || payload.appUserId === '') { throw new TypeError('指定业务用户绑定必须提供业务用户标识') } data.appUserId = normalizeLineagePersonIdentity(payload.appUserId, '绑定用户标识') } else if (payload.appUserId !== undefined && payload.appUserId !== null && payload.appUserId !== '') { throw new TypeError(`${bindingMode} 身份认领不能提供业务用户标识`) } for (const [field, label] of [ ['fatherId', '父亲标识'], ['motherId', '母亲标识'] ]) { if (payload[field] === undefined || payload[field] === null || payload[field] === '') continue data[field] = normalizeLineagePersonIdentity(payload[field], label) } if (payload.avatarOssId !== undefined && payload.avatarOssId !== null && payload.avatarOssId !== '') { data.avatarOssId = normalizeOssIdString(payload.avatarOssId, '头像文件标识') } if (payload.generation !== undefined) { if (!Number.isSafeInteger(payload.generation) || payload.generation < 1) { throw new TypeError('人物世代必须是正安全整数') } data.generation = payload.generation } for (const [field, label, maxLength] of [ ['personNo', '人物编号', null], ['sex', '性别', null], ['generationName', '字辈', 12], ['aliasName', '别名或曾用名', null], ['birthLunar', '出生农历', null], ['birthPlace', '出生地', null], ['deathLunar', '逝世农历', null], ['deathPlace', '逝世地', null], ['burialPlace', '安葬地', null], ['biography', '人物简介', 500], ['remark', '备注', null], ['relationName', '关系显示名称', null], ['personStatus', '人物状态', null] ]) { if (payload[field] === undefined) continue const value = normalizeLineagePersonText(payload[field], label) if (maxLength && value.length > maxLength) throw new TypeError(`人物${label}长度超出当前页面合同`) if (value) data[field] = value } for (const [field, label] of [ ['birthDate', '出生日期'], ['deathDate', '离世日期'] ]) { if (payload[field] === undefined) continue const value = normalizeLineagePersonDate(payload[field], label) if (value) data[field] = value } if (data.birthDate && data.deathDate && data.deathDate < data.birthDate) { throw new TypeError('人物离世日期不能早于出生日期') } 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('人物排序值必须是安全整数') data.sortOrder = sortOrder } return data } const lineageRelationPath = Object.freeze({ FATHER: 'parents', MOTHER: 'parents', SPOUSE: 'spouses', SIBLING: 'siblings', SON: 'children', DAUGHTER: 'children' }) const requireLineageWriteRequestController = (requestOptions) => { if (!requestOptions || typeof requestOptions !== 'object' || Array.isArray(requestOptions) || Object.getPrototypeOf(requestOptions) !== Object.prototype) { throw new TypeError('人物写入请求选项必须是普通对象') } const fields = Object.keys(requestOptions) if (fields.some((field) => field !== 'requestController')) { throw new TypeError('人物写入请求选项包含未声明字段') } return requestOptions.requestController ?? null } export const appApi = { async getCaptchaRequirement({ operationCode, subject }, requestOptions = {}) { requireRemoteAuth() return requestAuth({ url: `/genealogy/app/auth/verification/${encodeURIComponent(assertAuthVerificationOperation(operationCode))}/require`, method: 'GET', data: { tenantId: runtimeConfig.tenantId, subject } }, requestOptions) }, async sendSmsCode({ operationCode, phone, validToken }, requestOptions = {}) { requireRemoteAuth() const normalizedValidToken = normalizeOptionalValidToken(validToken) return requestAuthVoid({ url: `/genealogy/app/auth/sms/${encodeURIComponent(assertAuthVerificationOperation(operationCode))}/code`, method: 'POST', data: { tenantId: runtimeConfig.tenantId, grantType: 'sms', phone, ...(normalizedValidToken ? { validToken: normalizedValidToken } : {}) } }, requestOptions) }, async loginWithPassword({ phone, passwordHash, validToken }, requestOptions = {}) { requireRemoteAuth() const normalizedValidToken = normalizeOptionalValidToken(validToken) const result = await requestAuth({ url: '/genealogy/app/auth/login', method: 'POST', data: { tenantId: runtimeConfig.tenantId, grantType: 'password', phone, password: assertPasswordHash(passwordHash), ...(normalizedValidToken ? { validToken: normalizedValidToken } : {}) } }, requestOptions) return saveLogin(result) }, async loginWithSms({ phone, smsCode }, requestOptions = {}) { requireRemoteAuth() const result = await requestAuth({ url: '/genealogy/app/auth/login/sms', method: 'POST', data: { tenantId: runtimeConfig.tenantId, grantType: 'sms', phone, smsCode: assertSmsCode(smsCode) } }, requestOptions) return saveLogin(result) }, async registerWithPassword({ phone, passwordHash, smsCode, nickName }, requestOptions = {}) { requireRemoteAuth() const normalizedNickName = normalizeOptionalAuthText(nickName, '昵称') const result = await requestAuth({ url: '/genealogy/app/auth/register', method: 'POST', data: { tenantId: runtimeConfig.tenantId, grantType: 'password', phone, password: assertPasswordHash(passwordHash), smsCode: assertSmsCode(smsCode), ...(normalizedNickName ? { nickName: normalizedNickName } : {}) } }, requestOptions) return saveLogin(result) }, async resetPassword({ phone, passwordHash, smsCode }, requestOptions = {}) { requireRemoteAuth() return requestAuthVoid({ url: '/genealogy/app/auth/password/reset', method: 'PUT', data: { tenantId: runtimeConfig.tenantId, grantType: 'password', phone, newPassword: assertPasswordHash(passwordHash), smsCode: assertSmsCode(smsCode) } }, requestOptions) }, async changePassword({ oldPasswordHash, newPasswordHash }, requestOptions = {}) { requireRemoteAuth() await requestStrict({ url: '/genealogy/app/auth/password', method: 'PUT', data: { oldPassword: assertPasswordHash(oldPasswordHash), newPassword: assertPasswordHash(newPasswordHash) } }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async deactivateAccount({ smsCode }, requestOptions = {}) { requireRemoteAuth() await requestStrict({ url: '/genealogy/app/auth/account/deactivate', method: 'POST', data: { smsCode: assertSmsCode(smsCode) } }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async logout(requestOptions = {}) { if (resolveRuntimeMode() !== 'remote') return null await requestStrict({ url: '/genealogy/app/auth/logout', method: 'DELETE' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async submitFeedback(payload, requestOptions = {}) { const data = normalizeFeedbackPayload(payload) if (!requestOptions || typeof requestOptions !== 'object' || Array.isArray(requestOptions) || Object.getPrototypeOf(requestOptions) !== Object.prototype) { throw new TypeError('反馈请求选项必须是普通对象') } const optionNames = Object.getOwnPropertyNames(requestOptions) const optionDescriptors = Object.getOwnPropertyDescriptors(requestOptions) if ( Object.getOwnPropertySymbols(requestOptions).length > 0 || optionNames.some((field) => field !== 'requestController') || optionNames.some((field) => !Object.prototype.hasOwnProperty.call(optionDescriptors[field], 'value')) ) { throw new TypeError('反馈请求选项包含未声明字段') } const runtimeMode = resolveRuntimeMode() if (runtimeMode !== 'remote') { throw createRequestError('当前为本地预览模式,反馈未提交服务器', 'WRITE_UNAVAILABLE') } return requestStrict({ url: '/genealogy/app/feedback', method: 'POST', data }, { requireData: false, requestController: requestOptions.requestController ?? null }) }, async getProfile(requestOptions = {}) { if (!hasRemoteConfig()) { throw createRequestError('个人资料需要真实服务读取,当前本地预览不伪造资料', 'REMOTE_READ_REQUIRED') } const result = await requestStrict({ url: '/genealogy/app/auth/profile', method: 'GET' }, { requestController: requestOptions.requestController ?? null }) return normalizeAppProfile(result) }, async updateProfile(payload, requestOptions = {}) { const data = normalizeProfileUpdatePayload(payload) if (!hasRemoteConfig()) { throw createRequestError('个人资料更新需要真实服务,当前本地预览不伪造保存成功', 'REMOTE_WRITE_REQUIRED') } const result = await requestStrict({ url: '/genealogy/app/auth/profile', method: 'PUT', data }, { requestController: requestOptions.requestController ?? null }) return normalizeAppProfile(result) }, async initializeResumableUpload(payload, requestOptions = {}) { const data = normalizeResumableInitPayload(payload) if (!hasRemoteConfig()) { throw createRequestError('文件上传需要真实服务,当前本地预览不伪造回执', 'REMOTE_WRITE_REQUIRED') } const result = await requestStrict({ url: '/genealogy/app/files/resumable/init', method: 'POST', data }, { requestController: requestOptions.requestController ?? null }) return normalizeResumableInitResult(result) }, async uploadResumableChunk(payload, requestOptions = {}) { const allowedFields = new Set(['uploadId', 'chunkIndex', 'chunkMd5', 'filePath']) assertPlainPayload(payload, allowedFields, '文件分片请求') if (!Number.isInteger(payload.chunkIndex) || payload.chunkIndex < 0 || payload.chunkIndex > 2147483647) { throw new TypeError('chunkIndex必须是非负 int32') } if (typeof payload.filePath !== 'string' || !payload.filePath.trim()) { throw new TypeError('filePath必须是非空字符串') } if (!hasRemoteConfig()) { throw createRequestError('文件上传需要真实服务,当前本地预览不伪造回执', 'REMOTE_WRITE_REQUIRED') } return requestNativeFileChunk({ uploadId: normalizeUploadId(payload.uploadId, 'uploadId'), chunkIndex: payload.chunkIndex, chunkMd5: normalizeUploadMd5(payload.chunkMd5, 'chunkMd5'), filePath: payload.filePath.trim() }, requestOptions) }, async uploadBrowserResumableChunk(payload, file, requestOptions = {}) { const allowedFields = new Set(['uploadId', 'chunkIndex', 'chunkMd5']) assertPlainPayload(payload, allowedFields, '浏览器文件分片请求') if (!Number.isInteger(payload.chunkIndex) || payload.chunkIndex < 0 || payload.chunkIndex > 2147483647) { throw new TypeError('chunkIndex必须是非负 int32') } if (!file || typeof file !== 'object') { throw new TypeError('浏览器文件分片必须提供文件对象') } if (!hasRemoteConfig()) { throw createRequestError('文件上传需要真实服务,当前本地预览不伪造回执', 'REMOTE_WRITE_REQUIRED') } return requestBrowserFileChunk({ uploadId: normalizeUploadId(payload.uploadId, 'uploadId'), chunkIndex: payload.chunkIndex, chunkMd5: normalizeUploadMd5(payload.chunkMd5, 'chunkMd5'), file }, requestOptions) }, async completeResumableUpload(payload, requestOptions = {}) { const data = normalizeResumableCompletePayload(payload) if (!hasRemoteConfig()) { throw createRequestError('文件上传需要真实服务,当前本地预览不伪造回执', 'REMOTE_WRITE_REQUIRED') } const result = await requestStrict({ url: '/genealogy/app/files/resumable/complete', method: 'POST', data }, { requestController: requestOptions.requestController ?? null }) return normalizeResumableCompleteResult(result) }, async getRegionChildren(parentCode, requestOptions = {}) { if (!hasRemoteConfig()) { const error = new Error('地区选择需要真实读取服务,当前本地预览不会伪造地区列表') error.code = 'REMOTE_READ_REQUIRED' throw error } const result = await requestStrict({ url: '/genealogy/app/region/children', method: 'GET', data: { parentCode: normalizeRegionParentCode(parentCode) } }, { requestController: requestOptions.requestController ?? null }) return normalizeRegionSelectorItems(result) }, async getRegionPath(regionCode, requestOptions = {}) { requireRemoteResource('行政区划路径', '读取') const result = await requestStrict({ url: `/genealogy/app/region/path/${encodeURIComponent(normalizeRegionCode(regionCode))}`, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) if (!Array.isArray(result)) { throw createRequestError('行政区划路径响应不是列表', 'REGION_PATH_RESPONSE_INVALID') } return result }, async searchRegions(payload, requestOptions = {}) { requireRemoteResource('行政区划搜索', '读取') const result = await requestStrict({ url: '/genealogy/app/region/search', method: 'GET', data: normalizeRegionSearchPayload(payload) }, { requestController: requestOptions.requestController ?? null }) if (!Array.isArray(result)) { throw createRequestError('行政区划搜索响应不是列表', 'REGION_SEARCH_RESPONSE_INVALID') } return result }, async getRegion(regionCode, requestOptions = {}) { requireRemoteResource('行政区划详情', '读取') const result = await requestStrict({ url: `/genealogy/app/region/${encodeURIComponent(normalizeRegionCode(regionCode))}`, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) if (!result || typeof result !== 'object' || Array.isArray(result)) { throw createRequestError('行政区划详情响应不是对象', 'REGION_DETAIL_RESPONSE_INVALID') } return result }, async getMyGenealogies(requestOptions = {}) { if (!hasRemoteConfig()) { const error = new Error('我的家谱需要真实读取服务,当前本地预览不会伪造家谱列表') error.code = 'REMOTE_READ_REQUIRED' throw error } const result = await requestStrict({ url: '/genealogy/app/genealogies/mine', method: 'GET' }, { requestController: requestOptions.requestController ?? null }) return normalizeMyGenealogies(result) }, async getGenealogyQuota(requestOptions = {}) { requireRemoteResource('家谱配额', '读取') const result = await requestStrict({ url: '/genealogy/app/genealogies/quota', method: 'GET' }, { requestController: requestOptions.requestController ?? null }) return normalizeGenealogyQuota(result) }, async createGenealogy(payload, requestOptions = {}) { const data = normalizeGenealogyCreatePayload(payload) if (!hasRemoteConfig()) { throw createRequestError('创建家谱需要真实服务,当前本地预览不会伪造创建成功', 'REMOTE_WRITE_REQUIRED') } const result = await requestStrict({ url: '/genealogy/app/genealogies', method: 'POST', data }, { requestController: requestOptions.requestController ?? null }) return normalizeCreatedGenealogy(result) }, async getGenealogy(genealogyId) { return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}` }) : genealogies.find((item) => String(item.id) === String(genealogyId)) }, async getGenealogySettings(genealogyId, requestOptions = {}) { const normalizedId = normalizeGenealogyPathId(genealogyId) if (!hasRemoteConfig()) { const error = new Error('家谱设置需要真实读取服务,当前本地预览不会伪造可编辑数据') error.code = 'REMOTE_READ_REQUIRED' throw error } const result = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedId}`, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) return normalizeGenealogySettings(result, normalizedId) }, async updateGenealogy(genealogyId, payload, requestOptions = {}) { const normalizedId = normalizeGenealogyPathId(genealogyId) const data = normalizeGenealogyUpdatePayload(payload) if (!hasRemoteConfig()) { throw createRequestError('更新家谱需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED') } return requestStrict({ url: `/genealogy/app/genealogies/${normalizedId}`, method: 'PUT', data }, { requireData: false, requestController: requestOptions.requestController ?? null }) }, async getOverview(genealogyId, requestOptions = {}) { const normalizedId = normalizeGenealogyPathId(genealogyId) if (!hasRemoteConfig()) { const error = new Error('家谱概览需要真实读取服务,当前本地预览不会伪造概览数据') error.code = 'REMOTE_READ_REQUIRED' throw error } const result = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedId}/overview`, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) const overview = normalizeAppGenealogy(result) if (overview.id !== normalizedId) { throw createRequestError('家谱概览响应标识不匹配', 'GENEALOGY_RESPONSE_INVALID') } return overview }, async getTree(genealogyId, requestOptions = {}) { if (!hasRemoteConfig()) { const error = new Error('世系树需要真实读取服务,当前本地预览不会伪造人物节点') error.code = 'REMOTE_READ_REQUIRED' throw error } const normalizedId = normalizeGenealogyPathId(genealogyId) const tree = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedId}/lineage/tree`, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) return normalizeLineageTree(tree) }, async getPerson(genealogyId, personId, requestOptions = {}) { if (!hasRemoteConfig()) { const error = new Error('人物详情需要真实读取服务,当前本地预览不会伪造成员资料') error.code = 'REMOTE_READ_REQUIRED' throw error } const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识') const result = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/${normalizedPersonId}`, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) return normalizeLineagePersonDetail(result, normalizedGenealogyId, normalizedPersonId) }, async getPersonPage(genealogyId, { pageNum = 1, pageSize = 10, keyword = '' } = {}, requestOptions = {}) { if (!hasRemoteConfig()) { const error = new Error('成员目录需要真实读取服务,当前本地预览不会伪造目录数据') error.code = 'REMOTE_READ_REQUIRED' throw error } if (!Number.isSafeInteger(pageNum) || pageNum < 1) throw new TypeError('成员目录页码无效') if (!Number.isSafeInteger(pageSize) || pageSize < 1) throw new TypeError('成员目录页大小无效') if (typeof keyword !== 'string') throw new TypeError('成员目录关键词无效') const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const result = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/page`, method: 'GET', data: { pageNum, pageSize, ...(keyword.trim() ? { keyword: keyword.trim() } : {}) } }, { requireData: false, requestController: requestOptions.requestController ?? null }) return normalizeLineagePersonPage(result, normalizedGenealogyId) }, async getFeedComments(genealogyId, feedId, requestOptions = {}) { if (!hasRemoteConfig()) { const error = new Error('动态评论需要真实读取服务,当前本地预览不会伪造评论数据') error.code = 'REMOTE_READ_REQUIRED' throw error } const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedFeedId = normalizeFeedCommentId(feedId, '动态标识') const result = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}/comments`, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) return normalizeFeedComments(result, normalizedGenealogyId, normalizedFeedId) }, async createFeedComment(genealogyId, feedId, payload, requestOptions = {}) { if (!hasRemoteConfig()) { const error = new Error('动态评论需要真实服务,当前本地预览不会伪造提交成功') error.code = 'REMOTE_WRITE_REQUIRED' throw error } const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedFeedId = normalizeFeedCommentId(feedId, '动态标识') return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}/comments`, method: 'POST', data: normalizeFeedCommentPayload(payload) }, { requestController: requestOptions.requestController ?? null }) }, async getGenerationPoems(genealogyId, requestOptions = {}) { if (!hasRemoteConfig()) { const error = new Error('字辈列表需要真实读取服务,当前本地预览不会伪造字辈数据') error.code = 'REMOTE_READ_REQUIRED' throw error } const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const result = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems`, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) return normalizeGenerationPoemRows(result, normalizedGenealogyId) }, async getGenerationPoemManagement(genealogyId, requestOptions = {}) { if (!hasRemoteConfig()) { const error = new Error('字辈维护列表需要真实读取服务,当前本地预览不会伪造管理权限') error.code = 'REMOTE_READ_REQUIRED' throw error } const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const result = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems/management`, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) return normalizeGenerationPoemRows(result, normalizedGenealogyId) }, async previewGenerationPoemBatch(genealogyId, payload, requestOptions = {}) { if (!hasRemoteConfig()) { const error = new Error('字辈批量预览需要真实服务,当前本地预览不会伪造差异结果') error.code = 'REMOTE_READ_REQUIRED' throw error } const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const result = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems/batch/preview`, method: 'POST', data: normalizeGenerationPoemBatchPayload(payload) }, { requestController: requestOptions.requestController ?? null }) return normalizeGenerationPoemPreview(result, normalizedGenealogyId) }, async saveGenerationPoemBatch(genealogyId, payload, requestOptions = {}) { if (!hasRemoteConfig()) { const error = new Error('字辈批量保存需要真实服务,当前本地预览不会伪造保存成功') error.code = 'REMOTE_WRITE_REQUIRED' throw error } const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems/batch/save`, method: 'POST', data: normalizeGenerationPoemBatchPayload(payload) }, { requestController: requestOptions.requestController ?? null }) }, async createGenerationPoem(genealogyId, payload, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) return writeRemoteObject(`/genealogy/app/genealogies/${id}/generation-poems`, 'POST', normalizeGenerationPoemWritePayload(payload), '字辈', requestOptions) }, async updateGenerationPoem(genealogyId, poemId, payload, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) const normalizedPoemId = normalizeResourcePathId(poemId, '字辈标识') return writeRemoteObject(`/genealogy/app/genealogies/${id}/generation-poems/${normalizedPoemId}`, 'PUT', normalizeGenerationPoemWritePayload(payload), '字辈', requestOptions) }, async getLineagePersons(genealogyId, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) return readRemoteList(`/genealogy/app/genealogies/${id}/lineage/persons`, '世系人物', requestOptions) }, async createPerson(genealogyId, payload, requestOptions = {}) { if (hasRemoteConfig()) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons`, method: 'POST', data: normalizeLineageWritePayload(payload) }, { requestController: requireLineageWriteRequestController(requestOptions) }) } const error = new Error('人物创建接口在本地预览模式不可用,当前内容不会保存') error.code = 'WRITE_UNAVAILABLE' throw error }, async createRelatedPerson(genealogyId, personId, relationType, payload, requestOptions = {}) { if (!hasRemoteConfig()) { const error = new Error('人物关系写入接口在本地预览模式不可用,当前内容不会保存') error.code = 'WRITE_UNAVAILABLE' throw error } const relationPath = lineageRelationPath[relationType] if (!relationPath) throw new TypeError('人物关系类型不属于当前合同') const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识') return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/${normalizedPersonId}/${relationPath}`, method: 'POST', data: normalizeLineageWritePayload(payload) }, { requestController: requireLineageWriteRequestController(requestOptions) }) }, async updatePerson(genealogyId, personId, payload, requestOptions = {}) { if (!hasRemoteConfig()) { const error = new Error('人物编辑接口在本地预览模式不可用,当前内容不会保存') error.code = 'WRITE_UNAVAILABLE' throw error } const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识') return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/${normalizedPersonId}`, method: 'PUT', data: normalizeLineageWritePayload(payload) }, { requestController: requireLineageWriteRequestController(requestOptions) }) }, async deletePerson(genealogyId, personId, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) const normalizedPersonId = normalizeResourcePathId(personId, '人物标识') return writeRemoteVoid(`/genealogy/app/genealogies/${id}/lineage/persons/${normalizedPersonId}`, 'DELETE', undefined, '世系人物', requestOptions) }, async getFeeds(genealogyId, requestOptions = {}) { const normalizedId = normalizeGenealogyPathId(genealogyId) if (!hasRemoteConfig()) { return normalizeFamilyFeedRows(listFamilyFeedFixtures(normalizedId), normalizedId) } const result = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedId}/feeds`, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) return normalizeFamilyFeedRows(result, normalizedId) }, async getFeedDetail(genealogyId, feedId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedFeedId = normalizeFeedCommentId(feedId, '动态标识') if (!hasRemoteConfig()) { throw createRequestError('动态详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED') } const result = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}`, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) return normalizeFamilyFeed(result, normalizedGenealogyId, normalizedFeedId) }, async updateFeed(genealogyId, feedId, payload, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedFeedId = normalizeFeedCommentId(feedId, '动态标识') if (!hasRemoteConfig()) { throw createRequestError('修改动态需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED') } const result = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}`, method: 'PUT', data: normalizeFeedCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) return normalizeFamilyFeed(result, normalizedGenealogyId, normalizedFeedId) }, async deleteFeed(genealogyId, feedId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedFeedId = normalizeFeedCommentId(feedId, '动态标识') if (!hasRemoteConfig()) { throw createRequestError('删除动态需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED') } await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}`, method: 'DELETE' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async setFeedLike(genealogyId, feedId, liked, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedFeedId = normalizeFeedCommentId(feedId, '动态标识') if (typeof liked !== 'boolean') throw new TypeError('动态点赞状态必须是布尔值') if (!hasRemoteConfig()) { throw createRequestError('动态点赞需要真实服务,当前本地预览不会伪造操作成功', 'REMOTE_WRITE_REQUIRED') } await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}/likes`, method: liked ? 'POST' : 'DELETE' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async createFeed(genealogyId, payload, requestOptions = {}) { if (hasRemoteConfig()) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const result = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds`, method: 'POST', data: normalizeFeedCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) const [feed] = normalizeFamilyFeedRows([result], normalizedGenealogyId) return feed } const error = new Error('动态发布接口尚未接入,当前内容不会保存') error.code = 'WRITE_UNAVAILABLE' throw error }, async createArticle(genealogyId, payload, requestOptions = {}) { if (hasRemoteConfig()) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/articles`, method: 'POST', data: normalizeArticleCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) } const error = new Error('谱文创建需要真实服务,当前本地预览不会伪造创建成功') error.code = 'REMOTE_WRITE_REQUIRED' throw error }, async createAlbum(genealogyId, payload, requestOptions = {}) { if (hasRemoteConfig()) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/albums`, method: 'POST', data: normalizeAlbumCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) } const error = new Error('相册创建需要真实服务,当前本地预览不会伪造创建成功') error.code = 'REMOTE_WRITE_REQUIRED' throw error }, async updateAlbum(genealogyId, albumId, payload, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedAlbumId = normalizeFeedCommentId(albumId, '相册标识') if (!hasRemoteConfig()) { throw createRequestError('修改相册需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED') } return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/albums/${normalizedAlbumId}`, method: 'PUT', data: normalizeAlbumCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) }, async deleteAlbum(genealogyId, albumId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedAlbumId = normalizeFeedCommentId(albumId, '相册标识') if (!hasRemoteConfig()) { throw createRequestError('删除相册需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED') } await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/albums/${normalizedAlbumId}`, method: 'DELETE' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async createAlbumPhoto(genealogyId, albumId, payload, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedAlbumId = normalizeFeedCommentId(albumId, '相册标识') if (!hasRemoteConfig()) { throw createRequestError('上传相册照片需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED') } return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/albums/${normalizedAlbumId}/photos`, method: 'POST', data: normalizeAlbumPhotoCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) }, async deleteAlbumPhoto(genealogyId, albumId, photoId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedAlbumId = normalizeFeedCommentId(albumId, '相册标识') const normalizedPhotoId = normalizeFeedCommentId(photoId, '照片标识') if (!hasRemoteConfig()) { throw createRequestError('删除相册照片需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED') } await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/albums/${normalizedAlbumId}/photos/${normalizedPhotoId}`, method: 'DELETE' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async createCeremony(genealogyId, payload, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) if (!hasRemoteConfig()) { throw createRequestError('创建礼仪活动需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED') } return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies`, method: 'POST', data: normalizeCeremonyPayload(payload) }, { requestController: requestOptions.requestController ?? null }) }, async updateCeremony(genealogyId, ceremonyId, payload, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedCeremonyId = normalizeFeedCommentId(ceremonyId, '礼仪活动标识') if (!hasRemoteConfig()) { throw createRequestError('修改礼仪活动需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED') } return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}`, method: 'PUT', data: normalizeCeremonyPayload(payload) }, { requestController: requestOptions.requestController ?? null }) }, async deleteCeremony(genealogyId, ceremonyId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedCeremonyId = normalizeFeedCommentId(ceremonyId, '礼仪活动标识') if (!hasRemoteConfig()) { throw createRequestError('删除礼仪活动需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED') } await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}`, method: 'DELETE' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async createCeremonyGift(genealogyId, ceremonyId, payload, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedCeremonyId = normalizeFeedCommentId(ceremonyId, '礼仪活动标识') if (!hasRemoteConfig()) { throw createRequestError('新增礼仪献礼需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED') } return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/gifts`, method: 'POST', data: normalizeCeremonyGiftPayload(payload) }, { requestController: requestOptions.requestController ?? null }) }, async deleteCeremonyGift(genealogyId, ceremonyId, giftId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedCeremonyId = normalizeFeedCommentId(ceremonyId, '礼仪活动标识') const normalizedGiftId = normalizeFeedCommentId(giftId, '献礼标识') if (!hasRemoteConfig()) { throw createRequestError('删除礼仪献礼需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED') } await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/gifts/${normalizedGiftId}`, method: 'DELETE' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async createRelativeRecord(genealogyId, payload, requestOptions = {}) { if (hasRemoteConfig()) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/relative-records`, method: 'POST', data: normalizeRelativeRecordCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) } const error = new Error('亲友往来创建需要真实服务,当前本地预览不会伪造创建成功') error.code = 'REMOTE_WRITE_REQUIRED' throw error }, async createGrowthRecord(genealogyId, payload, requestOptions = {}) { if (hasRemoteConfig()) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records`, method: 'POST', data: normalizeGrowthRecordCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) } const error = new Error('成长记录创建需要真实服务,当前本地预览不会伪造创建成功') error.code = 'REMOTE_WRITE_REQUIRED' throw error }, async createMemo(genealogyId, payload, requestOptions = {}) { if (hasRemoteConfig()) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/memos`, method: 'POST', data: normalizeMemoCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) } const error = new Error('备忘创建需要真实服务,当前本地预览不会伪造创建成功') error.code = 'REMOTE_WRITE_REQUIRED' throw error }, async createMeritRecord(genealogyId, payload, requestOptions = {}) { if (hasRemoteConfig()) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/merit-records`, method: 'POST', data: normalizeMeritRecordCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) } const error = new Error('功德记录创建需要真实服务,当前本地预览不会伪造创建成功') error.code = 'REMOTE_WRITE_REQUIRED' throw error }, async updateGrowthRecord(genealogyId, recordId, payload, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedRecordId = normalizeResourcePathId(recordId, '成长记录标识') if (!hasRemoteConfig()) throw createRequestError('修改成长记录需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED') return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records/${normalizedRecordId}`, method: 'PUT', data: normalizeGrowthRecordCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) }, async deleteGrowthRecord(genealogyId, recordId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedRecordId = normalizeResourcePathId(recordId, '成长记录标识') if (!hasRemoteConfig()) throw createRequestError('删除成长记录需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED') await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records/${normalizedRecordId}`, method: 'DELETE' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async updateMemo(genealogyId, memoId, payload, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedMemoId = normalizeResourcePathId(memoId, '备忘标识') if (!hasRemoteConfig()) throw createRequestError('修改备忘需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED') return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/memos/${normalizedMemoId}`, method: 'PUT', data: normalizeMemoCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) }, async deleteMemo(genealogyId, memoId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedMemoId = normalizeResourcePathId(memoId, '备忘标识') if (!hasRemoteConfig()) throw createRequestError('删除备忘需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED') await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/memos/${normalizedMemoId}`, method: 'DELETE' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async updateRelativeRecord(genealogyId, relativeId, payload, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedRelativeId = normalizeResourcePathId(relativeId, '亲友记录标识') if (!hasRemoteConfig()) throw createRequestError('修改亲友记录需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED') return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/relative-records/${normalizedRelativeId}`, method: 'PUT', data: normalizeRelativeRecordCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) }, async deleteRelativeRecord(genealogyId, relativeId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedRelativeId = normalizeResourcePathId(relativeId, '亲友记录标识') if (!hasRemoteConfig()) throw createRequestError('删除亲友记录需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED') await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/relative-records/${normalizedRelativeId}`, method: 'DELETE' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async deleteMeritRecord(genealogyId, meritId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedMeritId = normalizeResourcePathId(meritId, '功德记录标识') if (!hasRemoteConfig()) throw createRequestError('删除功德记录需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED') await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/merit-records/${normalizedMeritId}`, method: 'DELETE' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async getArticles(genealogyId, requestOptions = {}) { const normalizedId = normalizeGenealogyPathId(genealogyId) if (!hasRemoteConfig()) { return normalizeFamilyArticleRows(listFamilyArticleFixtures(normalizedId), normalizedId) } const result = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedId}/articles`, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) return normalizeFamilyArticleRows(result, normalizedId) }, async getArticleDetail(genealogyId, articleId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedArticleId = normalizeFeedCommentId(articleId, '谱文标识') if (!hasRemoteConfig()) { throw createRequestError('谱文详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED') } const result = await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/articles/${normalizedArticleId}`, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) return normalizeFamilyArticle(result, normalizedGenealogyId, normalizedArticleId) }, async updateArticle(genealogyId, articleId, payload, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedArticleId = normalizeFeedCommentId(articleId, '谱文标识') if (!hasRemoteConfig()) { throw createRequestError('修改谱文需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED') } return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/articles/${normalizedArticleId}`, method: 'PUT', data: normalizeArticleCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) }, async deleteArticle(genealogyId, articleId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedArticleId = normalizeFeedCommentId(articleId, '谱文标识') if (!hasRemoteConfig()) { throw createRequestError('删除谱文需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED') } await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/articles/${normalizedArticleId}`, method: 'DELETE' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async getAlbums(genealogyId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) if (!hasRemoteConfig()) return listFamilyAlbumFixtures(normalizedGenealogyId) return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/albums`, '相册列表', requestOptions) }, async getAlbumPhotos(genealogyId, albumId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedAlbumId = normalizeFeedCommentId(albumId, '相册标识') if (!hasRemoteConfig()) { throw createRequestError('相册照片需要真实读取服务,当前本地预览不会伪造照片列表', 'REMOTE_READ_REQUIRED') } return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/albums/${normalizedAlbumId}/photos`, '相册照片', requestOptions) }, async getCeremonies(genealogyId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) if (!hasRemoteConfig()) return listCeremonyFixtures(normalizedGenealogyId) return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies`, '礼仪活动', requestOptions) }, async replaceCeremonyInvitees(genealogyId, ceremonyId, payload, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedCeremonyId = normalizeFeedCommentId(ceremonyId, '礼仪活动标识') return writeRemoteObject( `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/invitees`, 'PUT', normalizeCeremonyInviteePayload(payload), '活动受邀人', requestOptions ) }, async getCeremonyInvitations(genealogyId, ceremonyId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedCeremonyId = normalizeFeedCommentId(ceremonyId, '礼仪活动标识') return readRemoteList( `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/invitations`, '活动邀请名单', requestOptions ) }, async respondToCeremonyInvitation(genealogyId, ceremonyId, payload, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedCeremonyId = normalizeFeedCommentId(ceremonyId, '礼仪活动标识') return writeRemoteObject( `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/invitations/me`, 'PUT', normalizeCeremonyInvitationResponsePayload(payload), '活动邀请响应', requestOptions ) }, async getMyCeremonyInvitations(requestOptions = {}) { return readRemoteList('/genealogy/app/genealogies/ceremony-invitations/mine', '我的活动邀请', requestOptions) }, async getCeremonyDetail(genealogyId, ceremonyId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedCeremonyId = normalizeFeedCommentId(ceremonyId, '礼仪活动标识') if (!hasRemoteConfig()) { throw createRequestError('礼仪详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED') } return readRemoteObject(`/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}`, '礼仪活动详情', requestOptions) }, async getCeremonyGifts(genealogyId, ceremonyId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedCeremonyId = normalizeFeedCommentId(ceremonyId, '礼仪活动标识') if (!hasRemoteConfig()) { throw createRequestError('礼仪献礼需要真实读取服务,当前本地预览不会伪造列表', 'REMOTE_READ_REQUIRED') } return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/gifts`, '礼仪献礼', requestOptions) }, async getGrowthRecords(genealogyId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) if (!hasRemoteConfig()) return listGrowthRecordFixtures(normalizedGenealogyId) return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records`, '成长记录', requestOptions) }, async getGrowthRecordDetail(genealogyId, recordId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedRecordId = normalizeResourcePathId(recordId, '成长记录标识') if (!hasRemoteConfig()) throw createRequestError('成长记录详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED') return readRemoteObject(`/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records/${normalizedRecordId}`, '成长记录详情', requestOptions) }, async getMemos(genealogyId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) if (!hasRemoteConfig()) throw createRequestError('备忘列表需要真实读取服务,当前本地预览不会伪造列表', 'REMOTE_READ_REQUIRED') return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/memos`, '备忘录', requestOptions) }, async getMemoDetail(genealogyId, memoId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedMemoId = normalizeResourcePathId(memoId, '备忘标识') if (!hasRemoteConfig()) throw createRequestError('备忘详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED') return readRemoteObject(`/genealogy/app/genealogies/${normalizedGenealogyId}/memos/${normalizedMemoId}`, '备忘录详情', requestOptions) }, async getRelativeRecords(genealogyId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) if (!hasRemoteConfig()) throw createRequestError('亲友记录需要真实读取服务,当前本地预览不会伪造列表', 'REMOTE_READ_REQUIRED') return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/relative-records`, '亲友往来', requestOptions) }, async getRelativeRecordDetail(genealogyId, relativeId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedRelativeId = normalizeResourcePathId(relativeId, '亲友记录标识') if (!hasRemoteConfig()) throw createRequestError('亲友记录详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED') return readRemoteObject(`/genealogy/app/genealogies/${normalizedGenealogyId}/relative-records/${normalizedRelativeId}`, '亲友往来详情', requestOptions) }, async getMeritRecords(genealogyId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) if (!hasRemoteConfig()) throw createRequestError('功德记录需要真实读取服务,当前本地预览不会伪造列表', 'REMOTE_READ_REQUIRED') return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/merit-records`, '功德记录', requestOptions) }, async getFeedback(requestOptions = {}) { return readRemoteList('/genealogy/app/feedback', '我的反馈', requestOptions) }, async getVideos(genealogyId, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) return readRemoteList(`/genealogy/app/genealogies/${id}/videos`, '家族视频', requestOptions) }, async getVideoDetail(genealogyId, videoId, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) const normalizedVideoId = normalizeResourcePathId(videoId, '视频标识') return readRemoteObject(`/genealogy/app/genealogies/${id}/videos/${normalizedVideoId}`, '家族视频详情', requestOptions) }, async createVideo(genealogyId, payload, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) return writeRemoteObject(`/genealogy/app/genealogies/${id}/videos`, 'POST', normalizeVideoPayload(payload), '家族视频', requestOptions) }, async updateVideo(genealogyId, videoId, payload, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) const normalizedVideoId = normalizeResourcePathId(videoId, '视频标识') return writeRemoteObject(`/genealogy/app/genealogies/${id}/videos/${normalizedVideoId}`, 'PUT', normalizeVideoPayload(payload), '家族视频', requestOptions) }, async deleteVideo(genealogyId, videoId, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) const normalizedVideoId = normalizeResourcePathId(videoId, '视频标识') return writeRemoteVoid(`/genealogy/app/genealogies/${id}/videos/${normalizedVideoId}`, 'DELETE', undefined, '家族视频', requestOptions) }, async getArticleCategories(genealogyId, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) return readRemoteList(`/genealogy/app/genealogies/${id}/article-categories`, '谱文分类', requestOptions) }, async getFeedPage(genealogyId, { pageNum = 1, pageSize = 20 } = {}, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) if (!Number.isSafeInteger(pageNum) || pageNum < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1) throw new TypeError('动态分页参数无效') requireRemoteResource('家族动态', '读取') const result = await requestStrict({ url: `/genealogy/app/genealogies/${id}/feeds/page`, method: 'GET', data: { pageNum, pageSize } }, { requireData: false, requestController: requestOptions.requestController ?? null }) if (!result || typeof result !== 'object' || !Array.isArray(result.rows) || !Number.isSafeInteger(result.total)) { throw createRequestError('动态分页响应无效', 'PAGE_RESPONSE_INVALID') } return { rows: normalizeFamilyFeedRows(result.rows, id), total: result.total } }, async getFeedCommentPage(genealogyId, feedId, { pageNum = 1, pageSize = 20 } = {}, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) const normalizedFeedId = normalizeResourcePathId(feedId, '动态标识') if (!Number.isSafeInteger(pageNum) || pageNum < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1) throw new TypeError('评论分页参数无效') requireRemoteResource('动态评论', '读取') const result = await requestStrict({ url: `/genealogy/app/genealogies/${id}/feeds/${normalizedFeedId}/comments/page`, method: 'GET', data: { pageNum, pageSize } }, { requireData: false, requestController: requestOptions.requestController ?? null }) if (!result || typeof result !== 'object' || !Array.isArray(result.rows) || !Number.isSafeInteger(result.total)) { throw createRequestError('动态评论分页响应无效', 'PAGE_RESPONSE_INVALID') } return { rows: normalizeFeedComments(result.rows, id, normalizedFeedId), total: result.total } }, async getCommentReplies(genealogyId, feedId, commentId, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) const normalizedFeedId = normalizeResourcePathId(feedId, '动态标识') const normalizedCommentId = normalizeResourcePathId(commentId, '评论标识') return readRemoteList(`/genealogy/app/genealogies/${id}/feeds/${normalizedFeedId}/comments/${normalizedCommentId}/replies`, '评论回复', requestOptions) }, async getCommentReplyPage(genealogyId, feedId, commentId, { pageNum = 1, pageSize = 20 } = {}, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) const normalizedFeedId = normalizeResourcePathId(feedId, '动态标识') const normalizedCommentId = normalizeResourcePathId(commentId, '评论标识') if (!Number.isSafeInteger(pageNum) || pageNum < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1) throw new TypeError('回复分页参数无效') requireRemoteResource('评论回复', '读取') const result = await requestStrict({ url: `/genealogy/app/genealogies/${id}/feeds/${normalizedFeedId}/comments/${normalizedCommentId}/replies/page`, method: 'GET', data: { pageNum, pageSize } }, { requireData: false, requestController: requestOptions.requestController ?? null }) if (!result || typeof result !== 'object' || !Array.isArray(result.rows) || !Number.isSafeInteger(result.total)) { throw createRequestError('评论回复分页响应无效', 'PAGE_RESPONSE_INVALID') } return { rows: normalizeFeedComments(result.rows, id, normalizedFeedId), total: result.total } }, async deleteFeedComment(genealogyId, feedId, commentId, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) const normalizedFeedId = normalizeResourcePathId(feedId, '动态标识') const normalizedCommentId = normalizeResourcePathId(commentId, '评论标识') return writeRemoteVoid(`/genealogy/app/genealogies/${id}/feeds/${normalizedFeedId}/comments/${normalizedCommentId}`, 'DELETE', undefined, '动态评论', requestOptions) }, async changePhone(payload, requestOptions = {}) { requireRemoteAuth() await requestStrict({ url: '/genealogy/app/auth/phone', method: 'PUT', data: normalizePhoneChangePayload(payload) }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async getHelpArticles(requestOptions = {}) { return readRemoteList('/genealogy/app/help-articles', '帮助文章', requestOptions) }, async getHelpArticle(helpId, requestOptions = {}) { const id = normalizeResourcePathId(helpId, '帮助文章标识') return readRemoteObject(`/genealogy/app/help-articles/${id}`, '帮助文章详情', requestOptions) }, async getPromotions(requestOptions = {}) { return readRemoteList('/genealogy/app/promotions', '应用推广', requestOptions) }, async getVipPackages(requestOptions = {}) { return readRemoteList('/genealogy/app/vip/packages', 'VIP 套餐', requestOptions) }, async getVipOrders(requestOptions = {}) { return readRemoteList('/genealogy/app/vip/orders', 'VIP 订单', requestOptions) }, async createVipOrder(payload, requestOptions = {}) { return writeRemoteObject('/genealogy/app/vip/orders', 'POST', normalizeVipOrderPayload(payload), 'VIP 订单', requestOptions) }, async getGenealogyOptions(requestOptions = {}) { return readRemoteList('/genealogy/app/genealogies/options', '家谱选项', requestOptions) }, async getMembers(genealogyId, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) return readRemoteList(`/genealogy/app/genealogies/${id}/members`, '家谱成员', requestOptions) }, async getMemberOptions(genealogyId, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) return readRemoteList(`/genealogy/app/genealogies/${id}/members/options`, '成员选项', requestOptions) }, async updateMember(genealogyId, memberId, payload, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) const memberIdValue = normalizeResourcePathId(memberId, '成员标识') return writeRemoteObject(`/genealogy/app/genealogies/${id}/members/${memberIdValue}`, 'PUT', normalizeMemberUpdatePayload(payload), '家谱成员', requestOptions) }, async removeMember(genealogyId, memberId, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) const memberIdValue = normalizeResourcePathId(memberId, '成员标识') return writeRemoteVoid(`/genealogy/app/genealogies/${id}/members/${memberIdValue}`, 'DELETE', undefined, '家谱成员', requestOptions) }, async leaveGenealogy(genealogyId, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) return writeRemoteVoid(`/genealogy/app/genealogies/${id}/members/me`, 'DELETE', undefined, '退出家谱', requestOptions) }, async transferGenealogyOwner(genealogyId, targetMemberId, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) return writeRemoteObject(`/genealogy/app/genealogies/${id}/members/owner-transfer`, 'PUT', { targetMemberId: Number(normalizeResourcePathId(targetMemberId, '目标成员标识')) }, '家谱所有者转让', requestOptions) }, async getLineagePersonOptions(genealogyId, requestOptions = {}) { const id = normalizeGenealogyPathId(genealogyId) return readRemoteList(`/genealogy/app/genealogies/${id}/lineage/persons/options`, '世系人物选项', requestOptions) }, async getSiteArticles(payload = {}, requestOptions = {}) { const result = await requestStrict({ url: '/genealogy/app/site/articles', method: 'GET', data: normalizeSiteArticleQuery(payload) }, { authenticated: false, requestController: requestOptions.requestController ?? null }) if (!Array.isArray(result)) { throw createRequestError('官网文章列表响应不是数组', 'SITE_ARTICLE_LIST_RESPONSE_INVALID') } return result }, async getSitePage(pageKey, requestOptions = {}) { const result = await requestStrict({ url: `/genealogy/app/site/pages/${encodeURIComponent(normalizeSitePageKey(pageKey))}`, method: 'GET' }, { authenticated: false, requestController: requestOptions.requestController ?? null }) if (!result || typeof result !== 'object' || Array.isArray(result)) { throw createRequestError('官网单页响应不是对象', 'SITE_PAGE_RESPONSE_INVALID') } return result }, async getNotifications(requestOptions = {}) { if (!hasRemoteConfig()) return listNotificationFixtures() return readRemoteList('/genealogy/app/notifications', '消息通知', requestOptions) }, async getNotificationDetail(notificationId, requestOptions = {}) { const normalizedNotificationId = normalizeResourcePathId(notificationId, '通知标识') if (!hasRemoteConfig()) { throw createRequestError('通知详情需要真实服务读取,当前本地预览不伪造通知正文', 'REMOTE_READ_REQUIRED') } const result = await requestStrict({ url: `/genealogy/app/notifications/${normalizedNotificationId}`, method: 'GET' }, { requestController: requestOptions.requestController ?? null }) return normalizeNotificationDetail(result, normalizedNotificationId) }, async getUnreadNotificationCount(requestOptions = {}) { requireRemoteResource('未读通知数量', '读取') const value = await requestStrict({ url: '/genealogy/app/notifications/unread-count', method: 'GET' }, { requestController: requestOptions.requestController ?? null }) if (!Number.isSafeInteger(value) || value < 0) { throw createRequestError('未读通知数量响应无效', 'NOTIFICATION_COUNT_RESPONSE_INVALID') } return value }, async markNotificationRead(notificationId, requestOptions = {}) { const normalizedNotificationId = normalizeResourcePathId(notificationId, '通知标识') if (hasRemoteConfig()) { await requestStrict({ url: `/genealogy/app/notifications/${normalizedNotificationId}/read`, method: 'POST' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null } const notification = notifications.find((item) => String(item.id) === normalizedNotificationId) if (notification) notification.unread = false return { success: true } }, async markAllNotificationsRead(requestOptions = {}) { if (hasRemoteConfig()) { await requestStrict({ url: '/genealogy/app/notifications/read-all', method: 'POST' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null } notifications.forEach((item) => { item.unread = false }) return { success: true } }, async getPublicGenealogies(requestOptions = {}) { if (!hasRemoteConfig()) return publicGenealogies return readRemoteList('/genealogy/app/genealogies/public', '公开家谱', requestOptions) }, async applyToJoin(genealogyId, payload, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const data = normalizeJoinApplicationPayload(payload) if (hasRemoteConfig()) return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/join-applies`, method: 'POST', data }, { requestController: requestOptions.requestController ?? null }) joinApplications.unshift({ id: String(Date.now()), genealogyId: normalizedGenealogyId, name: payload.applicantName || currentUser.name, phone: payload.phone || currentUser.phone, relation: payload.relationDesc || '关系待补充', reason: payload.applyReason || '', appliedAt: '刚刚', status: 'PENDING' }) return { success: true } }, async getMyJoinApplications(requestOptions = {}) { if (!hasRemoteConfig()) return joinApplications return readRemoteList('/genealogy/app/genealogies/join-applies/mine', '我的加入申请', requestOptions) }, async withdrawJoinApplication(applicationId, requestOptions = {}) { const normalizedApplicationId = normalizeResourcePathId(applicationId, '加入申请标识') if (!hasRemoteConfig()) throw createRequestError('撤销加入申请需要真实服务,当前本地预览不会伪造撤销成功', 'REMOTE_WRITE_REQUIRED') await requestStrict({ url: `/genealogy/app/genealogies/join-applies/${normalizedApplicationId}`, method: 'DELETE' }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null }, async getPendingApplications(genealogyId, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) if (!hasRemoteConfig()) return joinApplications.filter((item) => String(item.genealogyId || 1001) === normalizedGenealogyId) return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/join-applies/pending`, '待审核申请', requestOptions) }, async auditApplication(genealogyId, applicationId, payload, requestOptions = {}) { const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId) const normalizedApplicationId = normalizeResourcePathId(applicationId, '加入申请标识') const data = normalizeJoinAuditPayload(payload) if (hasRemoteConfig()) { await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/join-applies/${normalizedApplicationId}/audit`, method: 'PUT', data }, { requireData: false, requestController: requestOptions.requestController ?? null }) return null } const application = joinApplications.find((item) => String(item.id) === normalizedApplicationId) if (application) Object.assign(application, data) return { success: true } } }