Files
jiapuapp/utils/api.js
T
2026-07-27 06:50:23 +08:00

3395 lines
151 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
currentUser,
genealogies,
publicGenealogies,
notifications,
joinApplications,
listFamilyFeedFixtures,
listFamilyArticleFixtures,
listFamilyAlbumFixtures,
listCeremonyFixtures,
listGrowthRecordFixtures,
listNotificationFixtures
} from '@/data/mock.js'
import { hasRemoteConfig, resolveRuntimeMode, runtimeConfig } from '@/utils/config.js'
import { AUTH_VERIFICATION_OPERATION, assertSmsCode } from '@/utils/auth-verification.js'
import { GENEALOGY_ACCESS_PRESET, fromApiGenealogyAccess } from '@/utils/genealogy-contracts.js'
import { session } from '@/utils/session.js'
const successCodes = [0, 200]
const REQUEST_TIMEOUT_MS = 15000
const createRequestError = (message, code, details = {}) => {
const error = new Error(message)
error.code = code
Object.assign(error, details)
return error
}
const createRequestCancelledError = () => createRequestError('请求已取消', 'REQUEST_CANCELLED')
// 页面只持有这个窄控制器,不直接依赖各端 RequestTask 的实现差异。
// request() 是唯一绑定/释放 owner;离页时 abort() 会同时拒绝等待中的 Promise,
// 因而页面不会在已销毁后继续处理成功回调。
export const createRequestController = () => {
let abortCurrent = null
return {
bind(abortRequest) {
if (typeof abortRequest !== 'function') throw new TypeError('请求中止器必须是函数')
if (abortCurrent) abortCurrent()
abortCurrent = abortRequest
},
release(abortRequest) {
if (abortCurrent === abortRequest) abortCurrent = null
},
abort() {
const abortRequest = abortCurrent
abortCurrent = null
if (abortRequest) abortRequest()
}
}
}
export const isRequestCancelled = (error) => error?.code === 'REQUEST_CANCELLED'
const isAuthenticatedSessionRejected = (error) =>
error?.code === 'BUSINESS_ERROR' && Number(error.businessCode) === 401
let authenticationRecoveryInFlight = false
const recoverExpiredAuthenticatedSession = () => {
if (authenticationRecoveryInFlight) return
authenticationRecoveryInFlight = true
session.clear()
try {
uni.reLaunch({
url: '/pages/auth/a01-entry',
complete: () => {
authenticationRecoveryInFlight = false
}
})
} catch {
authenticationRecoveryInFlight = false
}
}
export const unwrapResponse = (response) => {
if (!response || typeof response !== 'object' || Array.isArray(response)) return response
if (!Object.prototype.hasOwnProperty.call(response, 'code')) return response
if (!successCodes.includes(Number(response.code))) {
const numericCode = Number(response.code)
throw createRequestError(response.msg || '请求未成功', 'BUSINESS_ERROR', {
businessCode: Number.isNaN(numericCode) ? response.code : numericCode
})
}
return Object.prototype.hasOwnProperty.call(response, 'data') ? response.data : response
}
const request = (options, {
authenticated = true,
strictEnvelope = false,
requireEnvelopeData = false,
expectedStatus = null,
requestController = null
} = {}) => new Promise((resolve, reject) => {
if (requestController !== null && (
typeof requestController.bind !== 'function' ||
typeof requestController.release !== 'function'
)) {
reject(new TypeError('请求控制器格式无效'))
return
}
let settled = false
let requestTask = null
const release = () => requestController?.release(abortRequest)
const resolveOnce = (value) => {
if (settled) return
settled = true
release()
resolve(value)
}
const rejectOnce = (error) => {
if (settled) return
settled = true
release()
reject(error)
}
const abortRequest = () => {
if (settled) return
const task = requestTask
rejectOnce(createRequestCancelledError())
task?.abort?.()
}
const token = session.getToken()
requestTask = uni.request({
...options,
url: `${runtimeConfig.baseUrl}${options.url}`,
header: {
clientid: runtimeConfig.clientId,
tenantId: runtimeConfig.tenantId,
...(authenticated && token ? { Authorization: `Bearer ${token}` } : {}),
...(options.header || {})
},
success: ({ data, statusCode }) => {
if (expectedStatus !== null && statusCode !== expectedStatus) {
rejectOnce(createRequestError(
`服务只接受 HTTP ${expectedStatus} 响应,实际为 ${statusCode}`,
'HTTP_ERROR',
{ httpStatus: statusCode }
))
return
}
if (statusCode < 200 || statusCode >= 300) {
rejectOnce(createRequestError(
data?.msg || `请求失败(${statusCode}`,
'HTTP_ERROR',
{ httpStatus: statusCode }
))
return
}
if (
strictEnvelope &&
(!data ||
typeof data !== 'object' ||
Array.isArray(data) ||
!Object.prototype.hasOwnProperty.call(data, 'code') ||
!Number.isInteger(data.code) ||
(requireEnvelopeData && !Object.prototype.hasOwnProperty.call(data, 'data')))
) {
rejectOnce(createRequestError('服务响应格式无效', 'RESPONSE_INVALID'))
return
}
try {
resolveOnce(unwrapResponse(data))
} catch (error) {
// 业务 401 表示本地会话已经失效:只撤销本地令牌和家谱上下文,
// 不调用退出接口、不重试写操作,也不影响未携带会话的登录/注册请求。
if (authenticated && isAuthenticatedSessionRejected(error)) {
recoverExpiredAuthenticatedSession()
}
rejectOnce(error)
}
},
fail: (error) => {
const message = error?.errMsg || '网络连接失败'
rejectOnce(createRequestError(
message,
/timeout/i.test(message) ? 'REQUEST_TIMEOUT' : 'NETWORK_ERROR'
))
}
})
if (!settled && requestController) requestController.bind(abortRequest)
})
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 requestBrowserFileChunk = ({ uploadId, chunkIndex, chunkMd5, file }, { requestController = null } = {}) => new Promise((resolve, reject) => {
if (typeof globalThis.fetch !== 'function' || typeof globalThis.FormData !== 'function') {
reject(createRequestError('当前运行环境不支持文件上传', 'FILE_UPLOAD_UNAVAILABLE'))
return
}
if (requestController !== null && (
typeof requestController.bind !== 'function' ||
typeof requestController.release !== 'function'
)) {
reject(new TypeError('文件上传控制器格式无效'))
return
}
let settled = false
const abortController = new AbortController()
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
abortController.abort()
rejectOnce(createRequestCancelledError())
}
const query = `uploadId=${encodeURIComponent(uploadId)}&chunkIndex=${encodeURIComponent(chunkIndex)}&chunkMd5=${encodeURIComponent(chunkMd5)}`
const token = session.getToken()
const formData = new globalThis.FormData()
formData.append('file', file, file.name || 'image')
if (requestController) requestController.bind(abort)
globalThis.fetch(`${runtimeConfig.baseUrl}/genealogy/app/files/resumable/chunk?${query}`, {
method: 'POST',
headers: {
clientid: runtimeConfig.clientId,
tenantId: runtimeConfig.tenantId,
...(token ? { Authorization: `Bearer ${token}` } : {})
},
body: formData,
signal: abortController.signal
}).then(async (response) => {
if (response.status !== 200) {
throw createRequestError(`文件分片上传失败(HTTP ${response.status}`, 'HTTP_ERROR', { httpStatus: response.status })
}
parseStrictUploadResponse(await response.text(), false)
resolveOnce(null)
}).catch((error) => {
if (settled) return
rejectOnce(error?.name === 'AbortError' ? createRequestCancelledError() : error)
})
})
const 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 assertAuthVerificationOperation = (operationCode) => {
if (!Object.values(AUTH_VERIFICATION_OPERATION).includes(operationCode)) {
throw new TypeError('认证动作不属于当前认证合同')
}
return operationCode
}
const assertValidToken = (validToken) => {
if (typeof validToken !== 'string' || !validToken.trim()) {
throw new TypeError('发送短信前必须取得有效的行为验证票据')
}
return validToken.trim()
}
const normalizeOptionalValidToken = (validToken) =>
validToken === undefined ? undefined : assertValidToken(validToken)
const assertPasswordHash = (passwordHash) => {
if (typeof passwordHash !== 'string' || !/^[a-f0-9]{32}$/.test(passwordHash)) {
throw new TypeError('密码摘要必须是 32 位小写 MD5')
}
return passwordHash
}
const normalizeOptionalAuthText = (value, label) => {
if (value === undefined || value === null) return undefined
if (typeof value !== 'string') {
throw new TypeError(`${label}必须是字符串`)
}
const normalized = value.trim()
return normalized || undefined
}
const normalizeProfileResponseText = (value, field) => {
if (value === undefined || value === null) return ''
if (typeof value !== 'string') {
throw createRequestError(`个人资料字段 ${field} 无效`, 'PROFILE_RESPONSE_INVALID')
}
return value.trim()
}
const normalizeProfileAvatar = (value) => {
if (value === undefined || value === null || value === '') return null
const numericValue = typeof value === 'string' && /^-?\d+$/.test(value)
? Number(value)
: value
if (Number.isSafeInteger(numericValue) && numericValue <= 0) return null
return Number.isSafeInteger(numericValue) ? numericValue : null
}
const 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 ossId = payload.ossId === undefined || payload.ossId === null ? null : normalizeUploadOssId(payload.ossId, 'ossId')
if (payload.instant && !ossId) {
throw createRequestError('秒传响应缺少 ossId', 'FILE_RESPONSE_INVALID')
}
const uploadId = payload.instant && (payload.uploadId === undefined || payload.uploadId === null)
? null
: normalizeUploadId(payload.uploadId, '响应 uploadId')
return {
uploadId,
instant: payload.instant,
ossId,
url: normalizeProfileResponseText(payload.url, 'url'),
fileName: normalizeProfileResponseText(payload.fileName, 'fileName')
}
}
const normalizeResumableCompleteResult = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw createRequestError('文件完成响应格式无效', 'FILE_RESPONSE_INVALID')
}
return {
ossId: normalizeUploadOssId(payload.ossId, 'ossId'),
url: normalizeProfileResponseText(payload.url, 'url'),
thumbnailUrl: normalizeProfileResponseText(payload.thumbnailUrl, 'thumbnailUrl'),
fileName: normalizeProfileResponseText(payload.fileName, 'fileName')
}
}
const normalizeFeedbackPayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) {
throw new TypeError('反馈请求必须是普通对象')
}
const allowedFields = new Set(['feedbackType', 'feedbackContent', 'contactInfo'])
const fieldNames = Object.getOwnPropertyNames(payload)
if (Object.getOwnPropertySymbols(payload).length > 0 || fieldNames.some((field) => !allowedFields.has(field))) {
throw new TypeError('反馈请求包含未声明字段')
}
const descriptors = Object.getOwnPropertyDescriptors(payload)
if (fieldNames.some((field) => !Object.prototype.hasOwnProperty.call(descriptors[field], 'value'))) {
throw new TypeError('反馈字段必须是普通数据属性')
}
if (typeof payload.feedbackContent !== 'string' || !payload.feedbackContent.trim()) {
throw new TypeError('反馈内容必须是非空字符串')
}
const normalized = { feedbackContent: payload.feedbackContent.trim() }
for (const optionalField of ['feedbackType', 'contactInfo']) {
if (!Object.prototype.hasOwnProperty.call(payload, optionalField)) continue
if (typeof payload[optionalField] !== 'string') {
throw new TypeError(`反馈字段 ${optionalField} 必须是字符串`)
}
const value = payload[optionalField].trim()
if (value) 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 normalizeGenealogyQuota = (value) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('家谱配额响应无效', 'GENEALOGY_QUOTA_RESPONSE_INVALID')
}
const normalizeUsage = (field) => {
if (!Number.isSafeInteger(value[field]) || value[field] < 0) {
throw createRequestError(`家谱配额字段 ${field} 无效`, 'GENEALOGY_QUOTA_RESPONSE_INVALID')
}
return value[field]
}
const normalizeLimit = (field) => {
if (!Number.isSafeInteger(value[field]) || value[field] < -1) {
throw createRequestError(`家谱配额字段 ${field} 无效`, 'GENEALOGY_QUOTA_RESPONSE_INVALID')
}
return value[field]
}
const normalizePermission = (field) => {
if (typeof value[field] !== 'boolean') {
throw createRequestError(`家谱配额字段 ${field} 无效`, 'GENEALOGY_QUOTA_RESPONSE_INVALID')
}
return value[field]
}
return {
createUsed: normalizeUsage('createUsed'),
createLimit: normalizeLimit('createLimit'),
createRemaining: normalizeLimit('createRemaining'),
canCreate: normalizePermission('canCreate'),
joinUsed: normalizeUsage('joinUsed'),
joinLimit: normalizeLimit('joinLimit'),
joinRemaining: normalizeLimit('joinRemaining'),
canJoin: normalizePermission('canJoin')
}
}
const normalizeOptionalNumericId = (value, label, code) => {
if (value === undefined || value === null || value === '') return null
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw createRequestError(`${label}无效`, code)
}
const normalizeGenealogySettings = (value, expectedGenealogyId) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('家谱设置响应无效', 'GENEALOGY_SETTINGS_RESPONSE_INVALID')
}
const id = normalizeMyGenealogyId(value.genealogyId)
if (id !== expectedGenealogyId) {
throw createRequestError('家谱设置响应标识不匹配', 'GENEALOGY_SETTINGS_RESPONSE_INVALID')
}
const genealogyName = normalizeOptionalGenealogyText(value.genealogyName, 'genealogyName')
const surname = normalizeOptionalGenealogyText(value.surname, 'surname')
const regionCode = normalizeOptionalGenealogyText(value.regionCode, 'regionCode')
if (!genealogyName || !surname || !regionCode) {
throw createRequestError('家谱设置响应缺少名称、姓氏或地区', 'GENEALOGY_SETTINGS_RESPONSE_INVALID')
}
return {
id,
genealogyName,
surname,
ancestralHall: normalizeOptionalGenealogyText(value.ancestralHall, 'ancestralHall'),
originPlace: normalizeOptionalGenealogyText(value.originPlace, 'originPlace'),
regionCode,
regionName: normalizeOptionalGenealogyText(value.regionName, 'regionName'),
regionFullName: normalizeOptionalGenealogyText(value.regionFullName, 'regionFullName'),
addressDetail: normalizeOptionalGenealogyText(value.addressDetail, 'addressDetail'),
coverOssId: normalizeOptionalNumericId(value.coverOssId, 'coverOssId', 'GENEALOGY_SETTINGS_RESPONSE_INVALID'),
intro: normalizeOptionalGenealogyText(value.intro, 'intro'),
accessPreset: fromApiGenealogyAccess({
visibility: normalizeOptionalGenealogyText(value.visibility, 'visibility'),
joinMode: normalizeOptionalGenealogyText(value.joinMode, 'joinMode')
}),
visibility: normalizeOptionalGenealogyText(value.visibility, 'visibility'),
joinMode: normalizeOptionalGenealogyText(value.joinMode, 'joinMode'),
memberCount: normalizeOptionalNonnegativeInteger(value.memberCount, '家谱成员数量', 'GENEALOGY_SETTINGS_RESPONSE_INVALID'),
personCount: normalizeOptionalNonnegativeInteger(value.personCount, '家谱人物数量', 'GENEALOGY_SETTINGS_RESPONSE_INVALID'),
canManage: value.canManage === true,
canEditContent: value.canEditContent === true
}
}
const normalizeRegionParentCode = (value) => {
if (value === undefined || value === null || value === '') return '0'
if (typeof value !== 'string' || !value.trim()) throw new TypeError('地区父级标识无效')
return value.trim()
}
const normalizeRegionCode = (value) => {
if (typeof value !== 'string' || !value.trim()) throw new TypeError('行政区划编码无效')
return value.trim()
}
const normalizeRegionSearchPayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new TypeError('行政区划搜索参数无效')
}
const keyword = typeof payload.keyword === 'string' ? payload.keyword.trim() : ''
if (!keyword) throw new TypeError('行政区划搜索关键词无效')
const data = { keyword }
if (payload.level !== undefined && payload.level !== null && payload.level !== '') {
if (!Number.isInteger(payload.level) || payload.level < 1 || payload.level > 5) {
throw new TypeError('行政区划级别无效')
}
data.level = payload.level
}
if (payload.limit !== undefined && payload.limit !== null && payload.limit !== '') {
if (!Number.isInteger(payload.limit)) throw new TypeError('行政区划搜索数量上限无效')
data.limit = payload.limit
}
return data
}
const normalizeRegionSelectorItems = (value) => {
if (!Array.isArray(value)) {
throw createRequestError('地区选择响应不是列表', 'REGION_RESPONSE_INVALID')
}
return value.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('地区选择响应包含无效条目', 'REGION_RESPONSE_INVALID')
}
const regionCode = normalizeOptionalGenealogyText(item.regionCode ?? item.value, 'regionCode')
const label = normalizeOptionalGenealogyText(item.label ?? item.regionName, 'label')
if (!regionCode || !label) {
throw createRequestError('地区选择响应缺少地区名称或标识', 'REGION_RESPONSE_INVALID')
}
return {
regionCode,
label,
parentCode: normalizeOptionalGenealogyText(item.parentCode, 'parentCode'),
leaf: item.leaf === true,
regionLevel: Number.isInteger(item.regionLevel) ? item.regionLevel : null
}
})
}
const normalizeCreatedGenealogy = (value) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('创建家谱响应无效', 'GENEALOGY_RESPONSE_INVALID')
}
const id = value.genealogyId
if (typeof id === 'string' && /^[1-9]\d*$/.test(id)) return { id }
if (typeof id === 'number' && Number.isSafeInteger(id) && id > 0) return { id: String(id) }
throw createRequestError('创建家谱响应缺少有效标识', 'GENEALOGY_RESPONSE_INVALID')
}
const saveLogin = (loginResult) => {
const token = loginResult?.access_token
if (!token) throw new Error('登录响应未包含会话令牌')
session.saveToken(token)
return loginResult
}
const lineageTreeError = (message) =>
createRequestError(message, 'LINEAGE_TREE_RESPONSE_INVALID')
const normalizeLineagePersonId = (value) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw lineageTreeError('世系树包含无效人物标识')
}
const normalizeLineageText = (value, label, { required = false } = {}) => {
if (value === undefined || value === null) {
if (required) throw lineageTreeError(`世系树缺少${label}`)
return ''
}
if (typeof value !== 'string') throw lineageTreeError(`世系树${label}无效`)
const normalized = value.trim()
if (required && !normalized) throw lineageTreeError(`世系树缺少${label}`)
return normalized
}
const lineageDatePart = (value, label) => {
const normalized = normalizeLineageText(value, label)
if (!normalized) return ''
if (!/^\d{4}-\d{2}-\d{2}/.test(normalized)) throw lineageTreeError(`世系树${label}无效`)
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: typeof item.likedByMe === 'boolean' ? item.likedByMe : null,
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 normalizeCeremonyInviteePayload = (payload) => {
assertPlainPayload(payload, new Set(['inviteeUserIds']), '活动受邀人请求')
if (!Array.isArray(payload.inviteeUserIds)) {
throw new TypeError('inviteeUserIds必须是数组')
}
const inviteeUserIds = payload.inviteeUserIds.map((value) => {
const id = normalizeResourcePathId(value, '受邀业务用户标识')
const numericId = Number(id)
if (!Number.isSafeInteger(numericId)) {
throw createRequestError('受邀业务用户标识超出 APP 安全整数范围,需后端统一字符串 ID 合同', 'CEREMONY_INVITEE_ID_UNSAFE')
}
return numericId
})
if (new Set(inviteeUserIds).size !== inviteeUserIds.length) {
throw new TypeError('inviteeUserIds不能包含重复标识')
}
return { inviteeUserIds }
}
const normalizeCeremonyInvitationResponsePayload = (payload) => {
assertPlainPayload(payload, new Set(['inviteStatus']), '活动邀请响应请求')
if (!['ACCEPTED', 'DECLINED'].includes(payload.inviteStatus)) {
throw new TypeError('inviteStatus仅允许ACCEPTED或DECLINED')
}
return { inviteStatus: payload.inviteStatus }
}
const normalizeGenerationPoemWritePayload = (payload) => {
assertPlainPayload(payload, new Set(['generationNo', 'generationText', 'description', 'sortOrder', 'status']), '字辈请求')
const generationNo = Number(payload.generationNo)
if (!Number.isSafeInteger(generationNo) || generationNo < 1 || generationNo > 2147483647) {
throw new TypeError('世代序号必须是 1 到 2147483647 的整数')
}
const generationText = normalizeOptionalAuthText(payload.generationText, '字辈文字')
if (!generationText || Array.from(generationText).length > 50) throw new TypeError('字辈文字不能为空且不能超过 50 个字符')
const data = { generationNo, generationText }
const description = normalizeOptionalAuthText(payload.description, '字辈说明')
if (description) {
if (Array.from(description).length > 500) throw new TypeError('字辈说明不能超过 500 个字符')
data.description = description
}
if (payload.sortOrder !== undefined && payload.sortOrder !== null && payload.sortOrder !== '') {
const sortOrder = Number(payload.sortOrder)
if (!Number.isSafeInteger(sortOrder)) throw new TypeError('排序值必须是整数')
data.sortOrder = sortOrder
}
const status = normalizeOptionalAuthText(payload.status, '状态')
if (status) data.status = status
return data
}
const normalizeGenerationPoemId = (value, label) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw createRequestError(`字辈${label}无效`, 'GENERATION_POEM_RESPONSE_INVALID')
}
const normalizeGenerationPoemNumber = (value, label) => {
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return value
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) {
const normalized = Number(value)
if (Number.isSafeInteger(normalized)) return normalized
}
throw createRequestError(`字辈${label}无效`, 'GENERATION_POEM_RESPONSE_INVALID')
}
const normalizeGenerationPoemText = (value, label, { required = false, maxLength = null } = {}) => {
if (value === undefined || value === null) {
if (required) throw createRequestError(`字辈缺少${label}`, 'GENERATION_POEM_RESPONSE_INVALID')
return ''
}
if (typeof value !== 'string') throw createRequestError(`字辈${label}无效`, 'GENERATION_POEM_RESPONSE_INVALID')
const normalized = value.trim()
if (required && !normalized) throw createRequestError(`字辈缺少${label}`, 'GENERATION_POEM_RESPONSE_INVALID')
if (maxLength !== null && Array.from(normalized).length > maxLength) {
throw createRequestError(`字辈${label}超出合同长度`, 'GENERATION_POEM_RESPONSE_INVALID')
}
return normalized
}
const normalizeGenerationPoemStatus = (value) => {
if (value === '0' || value === '1') return value
throw createRequestError('字辈状态无效', 'GENERATION_POEM_RESPONSE_INVALID')
}
const normalizeGenerationPoemRows = (value, expectedGenealogyId) => {
if (!Array.isArray(value)) throw createRequestError('字辈响应不是列表', 'GENERATION_POEM_RESPONSE_INVALID')
const rows = value.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('字辈响应包含无效条目', 'GENERATION_POEM_RESPONSE_INVALID')
}
const genealogyId = normalizeGenerationPoemId(item.genealogyId, '家谱标识')
if (genealogyId !== expectedGenealogyId) {
throw createRequestError('字辈响应家谱标识与请求不匹配', 'GENERATION_POEM_RESPONSE_INVALID')
}
return {
poemId: normalizeGenerationPoemId(item.poemId, '标识'),
genealogyId,
genealogyNo: normalizeGenerationPoemText(item.genealogyNo, '家谱编号'),
genealogyName: normalizeGenerationPoemText(item.genealogyName, '家谱名称'),
generationNo: normalizeGenerationPoemNumber(item.generationNo, '世代'),
generationText: normalizeGenerationPoemText(item.generationText, '文字', { required: true, maxLength: 50 }),
description: normalizeGenerationPoemText(item.description, '说明', { maxLength: 500 }),
sortOrder: item.sortOrder,
status: normalizeGenerationPoemStatus(item.status),
}
})
if (new Set(rows.map((item) => item.poemId)).size !== rows.length) {
throw createRequestError('字辈响应包含重复标识', 'GENERATION_POEM_RESPONSE_INVALID')
}
if (new Set(rows.map((item) => item.generationNo)).size !== rows.length) {
throw createRequestError('字辈响应包含重复世代', 'GENERATION_POEM_RESPONSE_INVALID')
}
return rows.sort((left, right) => left.generationNo - right.generationNo)
}
const normalizeGenerationPoemBatchPayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) {
throw new TypeError('字辈批量请求必须是普通对象')
}
if (Object.keys(payload).some((field) => !['poemText', 'disableMissing'].includes(field))) {
throw new TypeError('字辈批量请求包含未声明字段')
}
const poemText = normalizeGenerationPoemText(payload.poemText, '内容', { required: true, maxLength: 26000 })
const data = { poemText }
if (payload.disableMissing !== undefined) {
if (typeof payload.disableMissing !== 'boolean') throw new TypeError('字辈停用策略必须是布尔值')
data.disableMissing = payload.disableMissing
}
return data
}
const normalizeGenerationPoemPreview = (value, expectedGenealogyId) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('字辈批量预览响应不是对象', 'GENERATION_POEM_RESPONSE_INVALID')
}
const genealogyId = normalizeGenerationPoemId(value.genealogyId, '预览家谱标识')
if (genealogyId !== expectedGenealogyId) {
throw createRequestError('字辈预览家谱标识与请求不匹配', 'GENERATION_POEM_RESPONSE_INVALID')
}
const counts = ['createCount', 'updateCount', 'keepCount', 'disableCount']
const preview = { genealogyId, items: Array.isArray(value.items) ? value.items : null }
for (const field of counts) {
if (!Number.isSafeInteger(value[field]) || value[field] < 0) {
throw createRequestError(`字辈预览${field}无效`, 'GENERATION_POEM_RESPONSE_INVALID')
}
preview[field] = value[field]
}
if (preview.items === null) {
throw createRequestError('字辈预览缺少明细列表', 'GENERATION_POEM_RESPONSE_INVALID')
}
return preview
}
const normalizeLineageWritePayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) {
throw new TypeError('人物写入请求必须是普通对象')
}
const allowedFields = new Set([
'appUserId',
'personNo',
'name',
'aliasName',
'sex',
'generationName',
'fatherId',
'motherId',
'avatarOssId',
'birthDate',
'birthLunar',
'birthPlace',
'deathDate',
'deathLunar',
'deathPlace',
'burialPlace',
'biography',
'remark',
'relationName',
'generation',
'personStatus',
'sortOrder'
])
for (const field of Object.keys(payload)) {
if (!allowedFields.has(field)) throw new TypeError(`人物写入包含未声明字段:${field}`)
}
const name = normalizeLineagePersonText(payload.name, '姓名', { required: true })
if (name.length > 20) throw new TypeError('人物姓名长度超出当前页面合同')
const data = { name }
for (const [field, label] of [
['appUserId', '绑定用户标识'],
['fatherId', '父亲标识'],
['motherId', '母亲标识'],
['avatarOssId', '头像文件标识']
]) {
if (payload[field] === undefined || payload[field] === null || payload[field] === '') continue
data[field] = normalizeLineagePersonIdentity(payload[field], label)
}
if (payload.generation !== undefined) {
if (!Number.isSafeInteger(payload.generation) || payload.generation < 1) {
throw new TypeError('人物世代必须是正安全整数')
}
data.generation = payload.generation
}
for (const [field, label, maxLength] of [
['personNo', '人物编号', null],
['sex', '性别', null],
['generationName', '字辈', 12],
['aliasName', '别名或曾用名', null],
['birthLunar', '出生农历', null],
['birthPlace', '出生地', null],
['deathLunar', '逝世农历', null],
['deathPlace', '逝世地', null],
['burialPlace', '安葬地', null],
['biography', '人物简介', 500],
['remark', '备注', null],
['relationName', '关系显示名称', null],
['personStatus', '人物状态', null]
]) {
if (payload[field] === undefined) continue
const value = normalizeLineagePersonText(payload[field], label)
if (maxLength && value.length > maxLength) throw new TypeError(`人物${label}长度超出当前页面合同`)
if (value) data[field] = value
}
for (const [field, label] of [
['birthDate', '出生日期'],
['deathDate', '离世日期']
]) {
if (payload[field] === undefined) continue
const value = normalizeLineagePersonDate(payload[field], label)
if (value) data[field] = value
}
if (data.birthDate && data.deathDate && data.deathDate < data.birthDate) {
throw new TypeError('人物离世日期不能早于出生日期')
}
if (payload.sortOrder !== undefined && payload.sortOrder !== null && payload.sortOrder !== '') {
const sortOrder = typeof payload.sortOrder === 'number' ? payload.sortOrder : Number(payload.sortOrder)
if (!Number.isSafeInteger(sortOrder)) throw new TypeError('人物排序值必须是安全整数')
data.sortOrder = sortOrder
}
return data
}
const lineageRelationPath = Object.freeze({
FATHER: 'parents',
MOTHER: 'parents',
SPOUSE: 'spouses',
SIBLING: 'siblings',
SON: 'children',
DAUGHTER: 'children'
})
const requireLineageWriteRequestController = (requestOptions) => {
if (!requestOptions || typeof requestOptions !== 'object' || Array.isArray(requestOptions) || Object.getPrototypeOf(requestOptions) !== Object.prototype) {
throw new TypeError('人物写入请求选项必须是普通对象')
}
const fields = Object.keys(requestOptions)
if (fields.some((field) => field !== 'requestController')) {
throw new TypeError('人物写入请求选项包含未声明字段')
}
return requestOptions.requestController ?? null
}
export const appApi = {
async getCaptchaRequirement({ operationCode, subject }, requestOptions = {}) {
requireRemoteAuth()
return requestAuth({
url: `/genealogy/app/auth/verification/${encodeURIComponent(assertAuthVerificationOperation(operationCode))}/require`,
method: 'GET',
data: { tenantId: runtimeConfig.tenantId, subject }
}, requestOptions)
},
async sendSmsCode({ operationCode, phone, validToken }, requestOptions = {}) {
requireRemoteAuth()
const normalizedValidToken = normalizeOptionalValidToken(validToken)
return requestAuthVoid({
url: `/genealogy/app/auth/sms/${encodeURIComponent(assertAuthVerificationOperation(operationCode))}/code`,
method: 'POST',
data: {
tenantId: runtimeConfig.tenantId,
grantType: 'sms',
phone,
...(normalizedValidToken ? { validToken: normalizedValidToken } : {})
}
}, requestOptions)
},
async sendLegacySmsCode({ phone, validToken }, requestOptions = {}) {
requireRemoteAuth()
const normalizedValidToken = normalizeOptionalValidToken(validToken)
return requestAuthVoid({
url: '/genealogy/app/auth/sms/code',
method: 'POST',
data: {
tenantId: runtimeConfig.tenantId,
grantType: 'sms',
phone,
...(normalizedValidToken ? { validToken: normalizedValidToken } : {})
}
}, requestOptions)
},
async loginWithPassword({ phone, passwordHash, validToken }, requestOptions = {}) {
requireRemoteAuth()
const normalizedValidToken = normalizeOptionalValidToken(validToken)
const result = await requestAuth({
url: '/genealogy/app/auth/login',
method: 'POST',
data: {
tenantId: runtimeConfig.tenantId,
grantType: 'password',
phone,
password: assertPasswordHash(passwordHash),
...(normalizedValidToken ? { validToken: normalizedValidToken } : {})
}
}, requestOptions)
return saveLogin(result)
},
async loginWithSms({ phone, smsCode }, requestOptions = {}) {
requireRemoteAuth()
const result = await requestAuth({
url: '/genealogy/app/auth/login/sms',
method: 'POST',
data: 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 uploadBrowserResumableChunk(payload, file, requestOptions = {}) {
const allowedFields = new Set(['uploadId', 'chunkIndex', 'chunkMd5'])
assertPlainPayload(payload, allowedFields, '浏览器文件分片请求')
if (!Number.isInteger(payload.chunkIndex) || payload.chunkIndex < 0 || payload.chunkIndex > 2147483647) {
throw new TypeError('chunkIndex必须是非负 int32')
}
if (!file || typeof file !== 'object') {
throw new TypeError('浏览器文件分片必须提供文件对象')
}
if (!hasRemoteConfig()) {
throw createRequestError('文件上传需要真实服务,当前本地预览不伪造回执', 'REMOTE_WRITE_REQUIRED')
}
return requestBrowserFileChunk({
uploadId: normalizeUploadId(payload.uploadId, 'uploadId'),
chunkIndex: payload.chunkIndex,
chunkMd5: normalizeUploadMd5(payload.chunkMd5, 'chunkMd5'),
file
}, requestOptions)
},
async completeResumableUpload(payload, requestOptions = {}) {
const data = normalizeResumableCompletePayload(payload)
if (!hasRemoteConfig()) {
throw createRequestError('文件上传需要真实服务,当前本地预览不伪造回执', 'REMOTE_WRITE_REQUIRED')
}
const result = await requestStrict({
url: '/genealogy/app/files/resumable/complete',
method: 'POST',
data
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeResumableCompleteResult(result)
},
async 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/region/children',
method: 'GET',
data: { parentCode: normalizeRegionParentCode(parentCode) }
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeRegionSelectorItems(result)
},
async getRegionPath(regionCode, requestOptions = {}) {
requireRemoteResource('行政区划路径', '读取')
const result = await requestStrict({
url: `/genealogy/region/path/${encodeURIComponent(normalizeRegionCode(regionCode))}`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
if (!Array.isArray(result)) {
throw createRequestError('行政区划路径响应不是列表', 'REGION_PATH_RESPONSE_INVALID')
}
return result
},
async searchRegions(payload, requestOptions = {}) {
requireRemoteResource('行政区划搜索', '读取')
const result = await requestStrict({
url: '/genealogy/region/search',
method: 'GET',
data: normalizeRegionSearchPayload(payload)
}, {
requestController: requestOptions.requestController ?? null
})
if (!Array.isArray(result)) {
throw createRequestError('行政区划搜索响应不是列表', 'REGION_SEARCH_RESPONSE_INVALID')
}
return result
},
async getRegion(regionCode, requestOptions = {}) {
requireRemoteResource('行政区划详情', '读取')
const result = await requestStrict({
url: `/genealogy/region/${encodeURIComponent(normalizeRegionCode(regionCode))}`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
if (!result || typeof result !== 'object' || Array.isArray(result)) {
throw createRequestError('行政区划详情响应不是对象', 'REGION_DETAIL_RESPONSE_INVALID')
}
return result
},
async getMyGenealogies(requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('我的家谱需要真实读取服务,当前本地预览不会伪造家谱列表')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const result = await requestStrict({
url: '/genealogy/app/genealogies/mine',
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeMyGenealogies(result)
},
async getGenealogyQuota(requestOptions = {}) {
requireRemoteResource('家谱配额', '读取')
const result = await requestStrict({
url: '/genealogy/app/genealogies/quota',
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeGenealogyQuota(result)
},
async createGenealogy(payload, requestOptions = {}) {
const data = normalizeGenealogyCreatePayload(payload)
if (!hasRemoteConfig()) {
throw createRequestError('创建家谱需要真实服务,当前本地预览不会伪造创建成功', 'REMOTE_WRITE_REQUIRED')
}
const result = await requestStrict({
url: '/genealogy/app/genealogies',
method: 'POST',
data
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeCreatedGenealogy(result)
},
async getGenealogy(genealogyId) {
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}` }) : genealogies.find((item) => String(item.id) === String(genealogyId))
},
async getGenealogySettings(genealogyId, requestOptions = {}) {
const normalizedId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) {
const error = new Error('家谱设置需要真实读取服务,当前本地预览不会伪造可编辑数据')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedId}`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeGenealogySettings(result, normalizedId)
},
async updateGenealogy(genealogyId, payload, requestOptions = {}) {
const normalizedId = normalizeGenealogyPathId(genealogyId)
const data = normalizeGenealogyUpdatePayload(payload)
if (!hasRemoteConfig()) {
throw createRequestError('更新家谱需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED')
}
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedId}`,
method: 'PUT',
data
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
},
async getOverview(genealogyId, requestOptions = {}) {
const normalizedId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) {
const error = new Error('家谱概览需要真实读取服务,当前本地预览不会伪造概览数据')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedId}/overview`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
const overview = normalizeAppGenealogy(result)
if (overview.id !== normalizedId) {
throw createRequestError('家谱概览响应标识不匹配', 'GENEALOGY_RESPONSE_INVALID')
}
return overview
},
async getTree(genealogyId, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('世系树需要真实读取服务,当前本地预览不会伪造人物节点')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const normalizedId = normalizeGenealogyPathId(genealogyId)
const tree = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedId}/lineage/tree`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeLineageTree(tree)
},
async getPerson(genealogyId, personId, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('人物详情需要真实读取服务,当前本地预览不会伪造成员资料')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识')
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/${normalizedPersonId}`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeLineagePersonDetail(result, normalizedGenealogyId, normalizedPersonId)
},
async getPersonPage(genealogyId, { pageNum = 1, pageSize = 10, keyword = '' } = {}, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('成员目录需要真实读取服务,当前本地预览不会伪造目录数据')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
if (!Number.isSafeInteger(pageNum) || pageNum < 1) throw new TypeError('成员目录页码无效')
if (!Number.isSafeInteger(pageSize) || pageSize < 1) throw new TypeError('成员目录页大小无效')
if (typeof keyword !== 'string') throw new TypeError('成员目录关键词无效')
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/page`,
method: 'GET',
data: { pageNum, pageSize, ...(keyword.trim() ? { keyword: keyword.trim() } : {}) }
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return normalizeLineagePersonPage(result, normalizedGenealogyId)
},
async getFeedComments(genealogyId, feedId, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('动态评论需要真实读取服务,当前本地预览不会伪造评论数据')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedFeedId = normalizeFeedCommentId(feedId, '动态标识')
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}/comments`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeFeedComments(result, normalizedGenealogyId, normalizedFeedId)
},
async createFeedComment(genealogyId, feedId, payload, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('动态评论需要真实服务,当前本地预览不会伪造提交成功')
error.code = 'REMOTE_WRITE_REQUIRED'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedFeedId = normalizeFeedCommentId(feedId, '动态标识')
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}/comments`,
method: 'POST',
data: normalizeFeedCommentPayload(payload)
}, {
requestController: requestOptions.requestController ?? null
})
},
async getGenerationPoems(genealogyId, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('字辈列表需要真实读取服务,当前本地预览不会伪造字辈数据')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeGenerationPoemRows(result, normalizedGenealogyId)
},
async getGenerationPoemManagement(genealogyId, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('字辈维护列表需要真实读取服务,当前本地预览不会伪造管理权限')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems/management`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeGenerationPoemRows(result, normalizedGenealogyId)
},
async previewGenerationPoemBatch(genealogyId, payload, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('字辈批量预览需要真实服务,当前本地预览不会伪造差异结果')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems/batch/preview`,
method: 'POST',
data: normalizeGenerationPoemBatchPayload(payload)
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeGenerationPoemPreview(result, normalizedGenealogyId)
},
async saveGenerationPoemBatch(genealogyId, payload, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('字辈批量保存需要真实服务,当前本地预览不会伪造保存成功')
error.code = 'REMOTE_WRITE_REQUIRED'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems/batch/save`,
method: 'POST',
data: normalizeGenerationPoemBatchPayload(payload)
}, {
requestController: requestOptions.requestController ?? null
})
},
async createGenerationPoem(genealogyId, payload, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId)
return writeRemoteObject(`/genealogy/app/genealogies/${id}/generation-poems`, 'POST', normalizeGenerationPoemWritePayload(payload), '字辈', requestOptions)
},
async updateGenerationPoem(genealogyId, poemId, payload, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId)
const normalizedPoemId = normalizeResourcePathId(poemId, '字辈标识')
return writeRemoteObject(`/genealogy/app/genealogies/${id}/generation-poems/${normalizedPoemId}`, 'PUT', normalizeGenerationPoemWritePayload(payload), '字辈', requestOptions)
},
async getLineagePersons(genealogyId, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId)
return readRemoteList(`/genealogy/app/genealogies/${id}/lineage/persons`, '世系人物', requestOptions)
},
async createPerson(genealogyId, payload, requestOptions = {}) {
if (hasRemoteConfig()) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons`,
method: 'POST',
data: normalizeLineageWritePayload(payload)
}, {
requestController: requireLineageWriteRequestController(requestOptions)
})
}
const error = new Error('人物创建接口在本地预览模式不可用,当前内容不会保存')
error.code = 'WRITE_UNAVAILABLE'
throw error
},
async createRelatedPerson(genealogyId, personId, relationType, payload, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('人物关系写入接口在本地预览模式不可用,当前内容不会保存')
error.code = 'WRITE_UNAVAILABLE'
throw error
}
const relationPath = lineageRelationPath[relationType]
if (!relationPath) throw new TypeError('人物关系类型不属于当前合同')
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识')
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/${normalizedPersonId}/${relationPath}`,
method: 'POST',
data: normalizeLineageWritePayload(payload)
}, {
requestController: requireLineageWriteRequestController(requestOptions)
})
},
async updatePerson(genealogyId, personId, payload, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('人物编辑接口在本地预览模式不可用,当前内容不会保存')
error.code = 'WRITE_UNAVAILABLE'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识')
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/${normalizedPersonId}`,
method: 'PUT',
data: normalizeLineageWritePayload(payload)
}, {
requestController: requireLineageWriteRequestController(requestOptions)
})
},
async deletePerson(genealogyId, personId, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeResourcePathId(personId, '人物标识')
return writeRemoteVoid(`/genealogy/app/genealogies/${id}/lineage/persons/${normalizedPersonId}`, 'DELETE', undefined, '世系人物', requestOptions)
},
async getFeeds(genealogyId, requestOptions = {}) {
const normalizedId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) {
return normalizeFamilyFeedRows(listFamilyFeedFixtures(normalizedId), normalizedId)
}
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedId}/feeds`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeFamilyFeedRows(result, normalizedId)
},
async getFeedDetail(genealogyId, feedId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedFeedId = normalizeFeedCommentId(feedId, '动态标识')
if (!hasRemoteConfig()) {
throw createRequestError('动态详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED')
}
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeFamilyFeed(result, normalizedGenealogyId, normalizedFeedId)
},
async updateFeed(genealogyId, feedId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedFeedId = normalizeFeedCommentId(feedId, '动态标识')
if (!hasRemoteConfig()) {
throw createRequestError('修改动态需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED')
}
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 replaceCeremonyInvitees(genealogyId, ceremonyId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeFeedCommentId(ceremonyId, '礼仪活动标识')
return writeRemoteObject(
`/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/invitees`,
'PUT',
normalizeCeremonyInviteePayload(payload),
'活动受邀人',
requestOptions
)
},
async getCeremonyInvitations(genealogyId, ceremonyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeFeedCommentId(ceremonyId, '礼仪活动标识')
return readRemoteList(
`/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/invitations`,
'活动邀请名单',
requestOptions
)
},
async respondToCeremonyInvitation(genealogyId, ceremonyId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeFeedCommentId(ceremonyId, '礼仪活动标识')
return writeRemoteObject(
`/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/invitations/me`,
'PUT',
normalizeCeremonyInvitationResponsePayload(payload),
'活动邀请响应',
requestOptions
)
},
async getMyCeremonyInvitations(requestOptions = {}) {
return readRemoteList('/genealogy/app/genealogies/ceremony-invitations/mine', '我的活动邀请', requestOptions)
},
async getCeremonyDetail(genealogyId, ceremonyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeFeedCommentId(ceremonyId, '礼仪活动标识')
if (!hasRemoteConfig()) {
throw createRequestError('礼仪详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED')
}
return readRemoteObject(`/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}`, '礼仪活动详情', requestOptions)
},
async getCeremonyGifts(genealogyId, ceremonyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeFeedCommentId(ceremonyId, '礼仪活动标识')
if (!hasRemoteConfig()) {
throw createRequestError('礼仪献礼需要真实读取服务,当前本地预览不会伪造列表', 'REMOTE_READ_REQUIRED')
}
return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/gifts`, '礼仪献礼', requestOptions)
},
async getGrowthRecords(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) return listGrowthRecordFixtures(normalizedGenealogyId)
return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records`, '成长记录', requestOptions)
},
async getGrowthRecordDetail(genealogyId, recordId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedRecordId = normalizeResourcePathId(recordId, '成长记录标识')
if (!hasRemoteConfig()) throw createRequestError('成长记录详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED')
return readRemoteObject(`/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records/${normalizedRecordId}`, '成长记录详情', requestOptions)
},
async getMemos(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) throw createRequestError('备忘列表需要真实读取服务,当前本地预览不会伪造列表', 'REMOTE_READ_REQUIRED')
return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/memos`, '备忘录', requestOptions)
},
async getMemoDetail(genealogyId, memoId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedMemoId = normalizeResourcePathId(memoId, '备忘标识')
if (!hasRemoteConfig()) throw createRequestError('备忘详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED')
return readRemoteObject(`/genealogy/app/genealogies/${normalizedGenealogyId}/memos/${normalizedMemoId}`, '备忘录详情', requestOptions)
},
async getRelativeRecords(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) throw createRequestError('亲友记录需要真实读取服务,当前本地预览不会伪造列表', 'REMOTE_READ_REQUIRED')
return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/relative-records`, '亲友往来', requestOptions)
},
async getRelativeRecordDetail(genealogyId, relativeId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedRelativeId = normalizeResourcePathId(relativeId, '亲友记录标识')
if (!hasRemoteConfig()) throw createRequestError('亲友记录详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED')
return readRemoteObject(`/genealogy/app/genealogies/${normalizedGenealogyId}/relative-records/${normalizedRelativeId}`, '亲友往来详情', requestOptions)
},
async getMeritRecords(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) throw createRequestError('功德记录需要真实读取服务,当前本地预览不会伪造列表', 'REMOTE_READ_REQUIRED')
return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/merit-records`, '功德记录', requestOptions)
},
async getFeedback(requestOptions = {}) {
return readRemoteList('/genealogy/app/feedback', '我的反馈', requestOptions)
},
async 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 getUnreadNotificationCount(requestOptions = {}) {
requireRemoteResource('未读通知数量', '读取')
const value = await requestStrict({
url: '/genealogy/app/notifications/unread-count',
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
if (!Number.isSafeInteger(value) || value < 0) {
throw createRequestError('未读通知数量响应无效', 'NOTIFICATION_COUNT_RESPONSE_INVALID')
}
return value
},
async markNotificationRead(notificationId, requestOptions = {}) {
const normalizedNotificationId = normalizeResourcePathId(notificationId, '通知标识')
if (hasRemoteConfig()) {
await requestStrict({ url: `/genealogy/app/notifications/${normalizedNotificationId}/read`, method: 'POST' }, { requireData: false, requestController: requestOptions.requestController ?? null })
return null
}
const notification = notifications.find((item) => String(item.id) === normalizedNotificationId)
if (notification) notification.unread = false
return { success: true }
},
async markAllNotificationsRead(requestOptions = {}) {
if (hasRemoteConfig()) {
await requestStrict({ url: '/genealogy/app/notifications/read-all', method: 'POST' }, { requireData: false, requestController: requestOptions.requestController ?? null })
return null
}
notifications.forEach((item) => { item.unread = false })
return { success: true }
},
async getPublicGenealogies(requestOptions = {}) {
if (!hasRemoteConfig()) return publicGenealogies
return readRemoteList('/genealogy/app/genealogies/public', '公开家谱', requestOptions)
},
async applyToJoin(genealogyId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const data = normalizeJoinApplicationPayload(payload)
if (hasRemoteConfig()) return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/join-applies`, method: 'POST', data }, { requestController: requestOptions.requestController ?? null })
joinApplications.unshift({
id: String(Date.now()),
genealogyId: normalizedGenealogyId,
name: payload.applicantName || currentUser.name,
phone: payload.phone || currentUser.phone,
relation: payload.relationDesc || '关系待补充',
reason: payload.applyReason || '',
appliedAt: '刚刚',
status: 'PENDING'
})
return { success: true }
},
async getMyJoinApplications(requestOptions = {}) {
if (!hasRemoteConfig()) return joinApplications
return readRemoteList('/genealogy/app/genealogies/join-applies/mine', '我的加入申请', requestOptions)
},
async withdrawJoinApplication(applicationId, requestOptions = {}) {
const normalizedApplicationId = normalizeResourcePathId(applicationId, '加入申请标识')
if (!hasRemoteConfig()) throw createRequestError('撤销加入申请需要真实服务,当前本地预览不会伪造撤销成功', 'REMOTE_WRITE_REQUIRED')
await requestStrict({ url: `/genealogy/app/genealogies/join-applies/${normalizedApplicationId}`, method: 'DELETE' }, { requireData: false, requestController: requestOptions.requestController ?? null })
return null
},
async getPendingApplications(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) return joinApplications.filter((item) => String(item.genealogyId || 1001) === normalizedGenealogyId)
return readRemoteList(`/genealogy/app/genealogies/${normalizedGenealogyId}/join-applies/pending`, '待审核申请', requestOptions)
},
async auditApplication(genealogyId, applicationId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedApplicationId = normalizeResourcePathId(applicationId, '加入申请标识')
const data = normalizeJoinAuditPayload(payload)
if (hasRemoteConfig()) {
await requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/join-applies/${normalizedApplicationId}/audit`, method: 'PUT', data }, { requireData: false, requestController: requestOptions.requestController ?? null })
return null
}
const application = joinApplications.find((item) => String(item.id) === normalizedApplicationId)
if (application) Object.assign(application, data)
return { success: true }
}
}