3395 lines
151 KiB
JavaScript
3395 lines
151 KiB
JavaScript
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 }
|
||
}
|
||
}
|