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