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_TAC_SCENE, 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) }) const authPayload = (payload) => ({ clientId: runtimeConfig.clientId, tenantId: runtimeConfig.tenantId, ...payload }) // 已核对接口使用这一严格边界:只接受 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 (!globalThis.plus?.uploader?.createUpload) { 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 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 task?.abort?.() rejectOnce(createRequestCancelledError()) } const query = `uploadId=${encodeURIComponent(uploadId)}&chunkIndex=${encodeURIComponent(chunkIndex)}&chunkMd5=${encodeURIComponent(chunkMd5)}` try { task = globalThis.plus.uploader.createUpload( `${runtimeConfig.baseUrl}/genealogy/app/files/resumable/chunk?${query}`, { method: 'POST' }, (upload, status) => { if (status !== 200) { rejectOnce(createRequestError(`文件分片上传失败(HTTP ${status})`, 'HTTP_ERROR', { httpStatus: status })) return } try { parseStrictUploadResponse(upload.responseText, false) resolveOnce(null) } catch (error) { rejectOnce(error) } } ) task.setRequestHeader('clientid', runtimeConfig.clientId) task.setRequestHeader('tenantId', runtimeConfig.tenantId) const token = session.getToken() if (token) task.setRequestHeader('Authorization', `Bearer ${token}`) task.addFile(filePath, { key: 'file' }) if (requestController) requestController.bind(abort) task.start() } catch (error) { rejectOnce(error) } }) const requestNativeSingleFileUpload = ({ filePath }, { 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 task = null 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 task?.abort?.() rejectOnce(createRequestCancelledError()) } try { const token = session.getToken() task = uni.uploadFile({ url: `${runtimeConfig.baseUrl}/genealogy/app/files/upload`, filePath, name: 'file', header: { clientid: runtimeConfig.clientId, tenantId: runtimeConfig.tenantId, ...(token ? { Authorization: `Bearer ${token}` } : {}) }, success: ({ data, statusCode }) => { if (statusCode !== 200) { rejectOnce(createRequestError(`文件上传失败(HTTP ${statusCode})`, 'HTTP_ERROR', { httpStatus: statusCode })) return } try { resolveOnce(normalizeResumableCompleteResult(parseStrictUploadResponse(data, true))) } catch (error) { rejectOnce(error) } }, fail: (error) => { const message = error?.errMsg || '文件上传网络连接失败' rejectOnce(createRequestError(message, /timeout/i.test(message) ? 'REQUEST_TIMEOUT' : 'NETWORK_ERROR')) } }) if (requestController) requestController.bind(abort) } catch (error) { rejectOnce(error) } }) const requireRemoteAuth = () => { if (resolveRuntimeMode() !== 'remote') { const error = new Error('当前为本地预览模式,真实认证服务未启用') error.code = 'AUTH_REMOTE_REQUIRED' throw error } } const assertAuthScene = (sceneCode) => { if (!Object.values(AUTH_TAC_SCENE).includes(sceneCode)) { throw new TypeError('短信场景不属于当前认证合同') } return sceneCode } const assertValidToken = (validToken) => { if (typeof validToken !== 'string' || !validToken.trim()) { throw new TypeError('发送短信前必须取得有效的行为验证票据') } return validToken.trim() } 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 normalizeAppProfile = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { throw createRequestError('个人资料响应格式无效', 'PROFILE_RESPONSE_INVALID') } return { userId: normalizeProfileAvatar(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], ['sex', Infinity]]) { 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, 'birthday')) { const value = normalizeOptionalAuthText(payload.birthday, 'birthday') if (value) { if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})?$/.test(value)) { throw new TypeError('birthday 必须是 ISO 日期时间') } 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 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 uploadId = normalizeUploadId(payload.uploadId, '响应 uploadId') const ossId = payload.ossId === undefined || payload.ossId === null ? null : normalizeUploadOssId(payload.ossId, 'ossId') if (payload.instant && !ossId) { throw createRequestError('秒传响应缺少 ossId', 'FILE_RESPONSE_INVALID') } 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) 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 !== '') { const numericOssId = typeof payload.coverOssId === 'number' ? payload.coverOssId : Number(payload.coverOssId) if (!Number.isSafeInteger(numericOssId) || numericOssId <= 0) { throw new TypeError('创建家谱字段 coverOssId 必须是正整数') } normalized.coverOssId = numericOssId } 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 !== '') { const numericOssId = typeof payload.coverOssId === 'number' ? payload.coverOssId : Number(payload.coverOssId) if (!Number.isSafeInteger(numericOssId) || numericOssId <= 0) { throw new TypeError('更新家谱字段 coverOssId 必须是正整数') } normalized.coverOssId = numericOssId } 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 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 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}无效`) return normalized.slice(0, 10) } const normalizeLineageTree = (value) => { if (!Array.isArray(value)) throw lineageTreeError('世系树响应不是列表') const seen = new Set() const normalized = [] const appendNode = (node, parentId, relationOverride = '') => { 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 birthDate = lineageDatePart(node.birthDate, '出生日期') const deathDate = lineageDatePart(node.deathDate, '逝世日期') normalized.push({ id, parentId, name: normalizeLineageText(node.name, '人物姓名', { required: true }), relation: relationOverride || normalizeLineageText(node.relationName, '人物关系') || (parentId ? '后代' : '始祖'), generation: node.generation, branch: generationName ? (generationName.endsWith('字辈') ? generationName : `${generationName}字辈`) : '字辈待补', years: birthDate || deathDate ? `${birthDate}—${deathDate}` : '生卒待补', sex: normalizeLineageText(node.sex, '性别'), personStatus: normalizeLineageText(node.personStatus, '人物状态') }) return id } const walk = (node, parentId = null, depth = 0) => { if (depth > 64) throw lineageTreeError('世系树深度超出客户端上限') const id = appendNode(node, parentId) const spouses = node.spouses ?? [] const children = node.children ?? [] if (!Array.isArray(spouses) || !Array.isArray(children)) { throw lineageTreeError('世系树亲属集合无效') } spouses.forEach((spouse) => appendNode(spouse, parentId, '配偶')) children.forEach((child) => walk(child, id, 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 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.*)?$/.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 generationName = normalizeLineagePersonText(value.generationName, '字辈') const aliasName = normalizeLineagePersonText(value.aliasName, '别名或曾用名') 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 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, aliasName, generation: value.generation, generationName, 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, 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 ?? item.id, '动态标识', 'FEED_RESPONSE_INVALID') const content = normalizeOptionalGenealogyText(item.feedContent ?? item.content, 'feedContent') if (!id || !content) { throw createRequestError('家族动态响应缺少标识或内容', 'FEED_RESPONSE_INVALID') } return { id, genealogyId: expectedGenealogyId, content, publisher: normalizeOptionalGenealogyText(item.publisherNickName ?? item.author, 'publisherNickName') || '家族成员', type: normalizeOptionalGenealogyText(item.feedType ?? item.tag, 'feedType') || '文字动态', time: normalizeOptionalGenealogyText(item.createTime ?? item.time, 'createTime'), mediaOssIds: normalizeOptionalGenealogyText(item.mediaOssIds, 'mediaOssIds'), likeCount: normalizeOptionalNonnegativeInteger(item.likeCount, '动态点赞数', 'FEED_RESPONSE_INVALID'), commentCount: normalizeOptionalNonnegativeInteger(item.commentCount, '动态评论数', 'FEED_RESPONSE_INVALID'), likedByMe: item.likedByMe === true, pinned: item.pinned === true } }) 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', 'coverOssId', '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 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 } for (const field of ['coverOssId', '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 = normalizeOptionalSafeInteger(payload.ossId, 'ossId') if (!ossId || ossId < 1) throw new TypeError('相册照片需要真实上传回执中的 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 normalizeCeremonyPayload = (payload) => { const allowedFields = new Set(['ceremonyType', 'ceremonyTitle', 'ceremonyDesc', 'ceremonyTime', 'location', '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', 'status']) { const value = normalizeOptionalAuthText(payload[field], field) if (value) data[field] = value } for (const field of ['coverOssId', 'sortOrder']) { const value = normalizeOptionalSafeInteger(payload[field], field) if (value !== undefined) data[field] = value } 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 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 authPayload({ 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 normalizeFileReferencePayload = (payload) => { assertPlainPayload(payload, new Set(['bizType', 'bizName', 'bizTable', 'bizId', 'bizField', 'ossId', 'ossIds', 'usageScene', 'usageName']), '文件引用请求') const data = {} for (const field of ['bizType', 'bizName', 'bizTable', 'bizField', 'usageScene', 'usageName']) { const value = normalizeOptionalAuthText(payload[field], field) if (value) data[field] = value } for (const field of ['bizType', 'bizTable', 'bizField']) { if (!data[field]) throw new TypeError(`文件引用缺少 ${field}`) } data.bizId = Number(normalizeResourcePathId(payload.bizId, '业务标识')) if (payload.ossId !== undefined && payload.ossId !== null && payload.ossId !== '') { data.ossId = Number(normalizeResourcePathId(payload.ossId, '文件标识')) } const ossIds = normalizeOptionalOssIdList(payload.ossIds) if (ossIds) data.ossIds = ossIds if (!data.ossId && !data.ossIds) throw new TypeError('文件引用需要 ossId 或 ossIds') return data } const normalizeFileReferenceReleasePayload = (payload) => { assertPlainPayload(payload, new Set(['bizTable', 'bizId', 'bizField']), '文件引用释放请求') const bizTable = normalizeOptionalAuthText(payload.bizTable, 'bizTable') const bizField = normalizeOptionalAuthText(payload.bizField, 'bizField') if (!bizTable || !bizField) throw new TypeError('文件引用释放缺少 bizTable 或 bizField') return { bizTable, bizId: normalizeResourcePathId(payload.bizId, '业务标识'), bizField } } 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 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([ 'name', 'aliasName', 'generationName', 'birthDate', 'birthLunar', 'birthPlace', 'deathDate', 'deathLunar', 'deathPlace', 'burialPlace', 'biography', 'remark', 'relationName', 'generation' ]) 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 data = { name } 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 [ ['generationName', '字辈', 12], ['aliasName', '别名或曾用名', null], ['birthLunar', '出生农历', null], ['birthPlace', '出生地', null], ['deathLunar', '逝世农历', null], ['deathPlace', '逝世地', null], ['burialPlace', '安葬地', null], ['biography', '人物简介', 500], ['remark', '备注', null], ['relationName', '关系显示名称', 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('人物离世日期不能早于出生日期') } 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({ sceneCode, subject }, requestOptions = {}) { requireRemoteAuth() return requestAuth({ url: '/captcha/require', method: 'GET', data: authPayload({ sceneCode: assertAuthScene(sceneCode), subject }) }, requestOptions) }, async sendSmsCode({ sceneCode, phone, validToken }, requestOptions = {}) { requireRemoteAuth() return requestAuthVoid({ url: '/genealogy/app/auth/sms/code', method: 'POST', data: authPayload({ grantType: 'sms', sceneCode: assertAuthScene(sceneCode), phone, validToken: assertValidToken(validToken) }) }, requestOptions) }, async loginWithPassword({ phone, passwordHash }, requestOptions = {}) { requireRemoteAuth() const result = await requestAuth({ url: '/genealogy/app/auth/login', method: 'POST', data: authPayload({ grantType: 'password', phone, password: assertPasswordHash(passwordHash) }) }, requestOptions) return saveLogin(result) }, async loginWithSms({ phone, smsCode }, requestOptions = {}) { requireRemoteAuth() const result = await requestAuth({ url: '/genealogy/app/auth/login/sms', method: 'POST', data: authPayload({ 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: authPayload({ 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: authPayload({ 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: { clientId: runtimeConfig.clientId, 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 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 uploadSingleFile({ filePath }, requestOptions = {}) { if (typeof filePath !== 'string' || !filePath.trim()) { throw new TypeError('filePath必须是非空字符串') } requireRemoteResource('文件上传', '写入') return requestNativeSingleFileUpload({ filePath: filePath.trim() }, requestOptions) }, async createFileReference(payload, requestOptions = {}) { return writeRemoteVoid( '/genealogy/app/files/reference', 'POST', normalizeFileReferencePayload(payload), '文件引用', requestOptions ) }, async deleteFileReference(payload, requestOptions = {}) { const data = normalizeFileReferenceReleasePayload(payload) const query = `bizTable=${encodeURIComponent(data.bizTable)}&bizId=${encodeURIComponent(data.bizId)}&bizField=${encodeURIComponent(data.bizField)}` return writeRemoteVoid(`/genealogy/app/files/reference?${query}`, 'DELETE', undefined, '文件引用', requestOptions) }, 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 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 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') } return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}`, method: 'PUT', data: normalizeFeedCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) }, 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) return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds`, method: 'POST', data: normalizeFeedCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null }) } 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 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 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() return requestAuthVoid({ url: '/genealogy/app/auth/phone', method: 'PUT', data: normalizePhoneChangePayload(payload) }, requestOptions) }, 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 getContent(type, genealogyId, requestOptions = {}) { const loaders = { article: this.getArticles, album: this.getAlbums, ceremony: this.getCeremonies, record: this.getGrowthRecords } if (!loaders[type]) throw new Error('未知内容类型') return loaders[type].call(this, genealogyId, requestOptions) }, async getNotifications(requestOptions = {}) { if (!hasRemoteConfig()) return listNotificationFixtures() return readRemoteList('/genealogy/app/notifications', '消息通知', requestOptions) }, 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 } } }