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' 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, ...(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) { 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 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 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 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) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) { throw new TypeError('家族动态请求必须是普通对象') } if (Object.keys(payload).some((field) => field !== 'feedContent')) { throw new TypeError('家族动态请求包含未声明字段') } if (typeof payload.feedContent !== 'string' || !payload.feedContent.trim()) { throw new TypeError('家族动态内容不能为空') } return { feedContent: payload.feedContent.trim() } } const normalizeArticleCreatePayload = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) { throw new TypeError('谱文请求必须是普通对象') } if (Object.keys(payload).some((field) => !['articleTitle', 'articleContent'].includes(field))) { throw new TypeError('谱文请求包含未声明字段') } 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('谱文正文不能为空') return { articleTitle, articleContent } } const normalizeAlbumCreatePayload = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) { throw new TypeError('相册请求必须是普通对象') } if (Object.keys(payload).some((field) => field !== 'albumName')) { throw new TypeError('相册请求包含未声明字段') } if (typeof payload.albumName !== 'string' || !payload.albumName.trim()) { throw new TypeError('相册名称不能为空') } return { albumName: payload.albumName.trim() } } const normalizeRelativeRecordCreatePayload = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) { throw new TypeError('亲友往来请求必须是普通对象') } const allowedFields = ['relativeName', 'relationName', 'eventName', 'eventTime', 'giftAmount', 'recordContent'] if (Object.keys(payload).some((field) => !allowedFields.includes(field))) { throw new TypeError('亲友往来请求包含未声明字段') } 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']) { if (payload[field] === undefined) continue if (typeof payload[field] !== 'string') throw new TypeError(`亲友往来${field}必须是字符串`) const value = payload[field].trim() if (value) data[field] = value } if (payload.giftAmount !== undefined && payload.giftAmount !== '' && payload.giftAmount !== null) { if (typeof payload.giftAmount !== 'number' || !Number.isFinite(payload.giftAmount)) { throw new TypeError('亲友往来礼金金额必须是有限数字') } data.giftAmount = payload.giftAmount } return data } const normalizeGrowthRecordCreatePayload = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) throw new TypeError('成长记录请求必须是普通对象') if (Object.keys(payload).some((field) => !['recordTitle', 'recordContent', 'recordDate'].includes(field))) throw new TypeError('成长记录请求包含未声明字段') const recordTitle = typeof payload.recordTitle === 'string' ? payload.recordTitle.trim() : '' if (!recordTitle) throw new TypeError('成长记录标题不能为空') const data = { recordTitle } for (const field of ['recordContent', 'recordDate']) { if (payload[field] === undefined) continue if (typeof payload[field] !== 'string') throw new TypeError(`成长记录${field}必须是字符串`) const value = payload[field].trim() if (value) data[field] = value } return data } const normalizeMemoCreatePayload = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) throw new TypeError('备忘请求必须是普通对象') if (Object.keys(payload).some((field) => !['memoTitle', 'memoContent', 'remindTime'].includes(field))) throw new TypeError('备忘请求包含未声明字段') const memoTitle = typeof payload.memoTitle === 'string' ? payload.memoTitle.trim() : '' if (!memoTitle) throw new TypeError('备忘标题不能为空') const data = { memoTitle } for (const field of ['memoContent', 'remindTime']) { if (payload[field] === undefined) continue if (typeof payload[field] !== 'string') throw new TypeError(`备忘${field}必须是字符串`) const value = payload[field].trim() if (value) data[field] = value } return data } const normalizeMeritRecordCreatePayload = (payload) => { if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) throw new TypeError('功德记录请求必须是普通对象') const allowedFields = ['donorName', 'meritTitle', 'meritType', 'meritContent', 'amount', 'meritTime'] if (Object.keys(payload).some((field) => !allowedFields.includes(field))) throw new TypeError('功德记录请求包含未声明字段') 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']) { if (payload[field] === undefined) continue if (typeof payload[field] !== 'string') throw new TypeError(`功德记录${field}必须是字符串`) const value = payload[field].trim() if (value) data[field] = value } if (payload.amount !== undefined && payload.amount !== '' && payload.amount !== null) { if (typeof payload.amount !== 'number' || !Number.isFinite(payload.amount)) throw new TypeError('功德金额必须是有限数字') data.amount = payload.amount } 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' ]) 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 } 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 }, requestOptions = {}) { requireRemoteAuth() const result = await requestAuth({ url: '/genealogy/app/auth/register', method: 'POST', data: authPayload({ grantType: 'password', phone, password: assertPasswordHash(passwordHash), smsCode: assertSmsCode(smsCode) }) }, 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 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() { return hasRemoteConfig() ? request({ url: '/genealogy/app/auth/profile' }) : currentUser }, 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) { if (hasRemoteConfig()) return request({ url: '/genealogy/app/genealogies', method: 'POST', data: payload }) const accessPreset = Object.values(GENEALOGY_ACCESS_PRESET).includes(payload.accessPreset) ? payload.accessPreset : GENEALOGY_ACCESS_PRESET.MEMBER_ONLY const created = { ...payload, id: String(Date.now()), memberCount: 0, activeCount: 0, motto: '敦亲睦族,敬祖传家。', accessPreset, membership: 'created' } genealogies.unshift(created) return created }, async getGenealogy(genealogyId) { return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}` }) : genealogies.find((item) => String(item.id) === String(genealogyId)) }, 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() } : {}) } }, { 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 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 getFeeds(genealogyId) { return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/feeds` }) : listFamilyFeedFixtures(genealogyId) }, 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 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 getArticles(genealogyId) { return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/articles` }) : listFamilyArticleFixtures(genealogyId) }, async getAlbums(genealogyId) { return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/albums` }) : listFamilyAlbumFixtures(genealogyId) }, async getCeremonies(genealogyId) { return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/ceremonies` }) : listCeremonyFixtures(genealogyId) }, async getGrowthRecords(genealogyId) { return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/growth-records` }) : listGrowthRecordFixtures(genealogyId) }, async getContent(type, genealogyId) { 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) }, async getNotifications() { return hasRemoteConfig() ? request({ url: '/genealogy/app/notifications' }) : listNotificationFixtures() }, async markNotificationRead(notificationId) { if (hasRemoteConfig()) return request({ url: `/genealogy/app/notifications/${notificationId}/read`, method: 'POST' }) const notification = notifications.find((item) => String(item.id) === String(notificationId)) if (notification) notification.unread = false return { success: true } }, async markAllNotificationsRead() { if (hasRemoteConfig()) return request({ url: '/genealogy/app/notifications/read-all', method: 'POST' }) notifications.forEach((item) => { item.unread = false }) return { success: true } }, async getPublicGenealogies() { return hasRemoteConfig() ? request({ url: '/genealogy/app/genealogies/public' }) : publicGenealogies }, async applyToJoin(genealogyId, payload) { if (hasRemoteConfig()) return request({ url: `/genealogy/app/genealogies/${genealogyId}/join-applies`, method: 'POST', data: payload }) joinApplications.unshift({ id: String(Date.now()), genealogyId, name: payload.applicantName || currentUser.name, phone: payload.phone || currentUser.phone, relation: payload.relationDesc || '关系待补充', reason: payload.applyReason || '', appliedAt: '刚刚', status: 'PENDING' }) return { success: true } }, async getPendingApplications(genealogyId) { return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/join-applies/pending` }) : joinApplications.filter((item) => String(item.genealogyId || 1001) === String(genealogyId)) }, async auditApplication(genealogyId, applicationId, { status, auditRemark = '' }) { if (typeof status !== 'string' || !/^[12]$/.test(status)) throw new TypeError('审核状态只能是 1 或 2') if (typeof auditRemark !== 'string') throw new TypeError('审核备注必须是字符串') if (Array.from(auditRemark).length > 500) throw new RangeError('审核备注不能超过 500 个字符') if (hasRemoteConfig()) return request({ url: `/genealogy/app/genealogies/${genealogyId}/join-applies/${applicationId}/audit`, method: 'PUT', data: { status, auditRemark } }) const application = joinApplications.find((item) => String(item.id) === String(applicationId)) if (application) Object.assign(application, { status, auditRemark }) return { success: true } } }