Files
jiapuapp/utils/api.js
T
2026-07-24 07:56:33 +08:00

1452 lines
61 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
currentUser,
genealogies,
publicGenealogies,
notifications,
joinApplications,
listFamilyFeedFixtures,
listFamilyArticleFixtures,
listFamilyAlbumFixtures,
listCeremonyFixtures,
listGrowthRecordFixtures,
listNotificationFixtures
} from '@/data/mock.js'
import { hasRemoteConfig, resolveRuntimeMode, runtimeConfig } from '@/utils/config.js'
import { AUTH_TAC_SCENE, assertSmsCode } from '@/utils/auth-verification.js'
import { GENEALOGY_ACCESS_PRESET, fromApiGenealogyAccess } from '@/utils/genealogy-contracts.js'
import { session } from '@/utils/session.js'
const successCodes = [0, 200]
const REQUEST_TIMEOUT_MS = 15000
const createRequestError = (message, code, details = {}) => {
const error = new Error(message)
error.code = code
Object.assign(error, details)
return error
}
const createRequestCancelledError = () => createRequestError('请求已取消', 'REQUEST_CANCELLED')
// 页面只持有这个窄控制器,不直接依赖各端 RequestTask 的实现差异。
// request() 是唯一绑定/释放 owner;离页时 abort() 会同时拒绝等待中的 Promise,
// 因而页面不会在已销毁后继续处理成功回调。
export const createRequestController = () => {
let abortCurrent = null
return {
bind(abortRequest) {
if (typeof abortRequest !== 'function') throw new TypeError('请求中止器必须是函数')
if (abortCurrent) abortCurrent()
abortCurrent = abortRequest
},
release(abortRequest) {
if (abortCurrent === abortRequest) abortCurrent = null
},
abort() {
const abortRequest = abortCurrent
abortCurrent = null
if (abortRequest) abortRequest()
}
}
}
export const isRequestCancelled = (error) => error?.code === 'REQUEST_CANCELLED'
export const unwrapResponse = (response) => {
if (!response || typeof response !== 'object' || Array.isArray(response)) return response
if (!Object.prototype.hasOwnProperty.call(response, 'code')) return response
if (!successCodes.includes(Number(response.code))) {
const numericCode = Number(response.code)
throw createRequestError(response.msg || '请求未成功', 'BUSINESS_ERROR', {
businessCode: Number.isNaN(numericCode) ? response.code : numericCode
})
}
return Object.prototype.hasOwnProperty.call(response, 'data') ? response.data : response
}
const request = (options, {
authenticated = true,
strictEnvelope = false,
requireEnvelopeData = false,
expectedStatus = null,
requestController = null
} = {}) => new Promise((resolve, reject) => {
if (requestController !== null && (
typeof requestController.bind !== 'function' ||
typeof requestController.release !== 'function'
)) {
reject(new TypeError('请求控制器格式无效'))
return
}
let settled = false
let requestTask = null
const release = () => requestController?.release(abortRequest)
const resolveOnce = (value) => {
if (settled) return
settled = true
release()
resolve(value)
}
const rejectOnce = (error) => {
if (settled) return
settled = true
release()
reject(error)
}
const abortRequest = () => {
if (settled) return
const task = requestTask
rejectOnce(createRequestCancelledError())
task?.abort?.()
}
const token = session.getToken()
requestTask = uni.request({
...options,
url: `${runtimeConfig.baseUrl}${options.url}`,
header: {
clientid: runtimeConfig.clientId,
...(authenticated && token ? { Authorization: `Bearer ${token}` } : {}),
...(options.header || {})
},
success: ({ data, statusCode }) => {
if (expectedStatus !== null && statusCode !== expectedStatus) {
rejectOnce(createRequestError(
`服务只接受 HTTP ${expectedStatus} 响应,实际为 ${statusCode}`,
'HTTP_ERROR',
{ httpStatus: statusCode }
))
return
}
if (statusCode < 200 || statusCode >= 300) {
rejectOnce(createRequestError(
data?.msg || `请求失败(${statusCode}`,
'HTTP_ERROR',
{ httpStatus: statusCode }
))
return
}
if (
strictEnvelope &&
(!data ||
typeof data !== 'object' ||
Array.isArray(data) ||
!Object.prototype.hasOwnProperty.call(data, 'code') ||
!Number.isInteger(data.code) ||
(requireEnvelopeData && !Object.prototype.hasOwnProperty.call(data, 'data')))
) {
rejectOnce(createRequestError('服务响应格式无效', 'RESPONSE_INVALID'))
return
}
try {
resolveOnce(unwrapResponse(data))
} catch (error) {
rejectOnce(error)
}
},
fail: (error) => {
const message = error?.errMsg || '网络连接失败'
rejectOnce(createRequestError(
message,
/timeout/i.test(message) ? 'REQUEST_TIMEOUT' : 'NETWORK_ERROR'
))
}
})
if (!settled && requestController) requestController.bind(abortRequest)
})
const authPayload = (payload) => ({
clientId: runtimeConfig.clientId,
tenantId: runtimeConfig.tenantId,
...payload
})
// 已核对接口使用这一严格边界:只接受 HTTP 200 与完整 JSON envelope
// 并统一限制弱网等待时间。尚未逐页治理的旧业务读取仍沿用上方通用 request。
const requestStrict = (options, {
authenticated = true,
requireData = true,
requestController = null
} = {}) => request({
...options,
timeout: REQUEST_TIMEOUT_MS
}, {
authenticated,
strictEnvelope: true,
requireEnvelopeData: requireData,
expectedStatus: 200,
requestController
})
const requestAuth = (options, { requestController = null } = {}) => requestStrict(options, {
authenticated: false,
requestController
})
// RVoid 的 OpenAPI schema 没有把 data 声明为必填。空结果接口仍要求合法的
// HTTP 200 与整数 code,但统一向页面返回 null,避免把 envelope 本身泄漏成业务数据。
const requestAuthVoid = async (options, { requestController = null } = {}) => {
await requestStrict(options, {
authenticated: false,
requireData: false,
requestController
})
return null
}
const requireRemoteAuth = () => {
if (resolveRuntimeMode() !== 'remote') {
const error = new Error('当前为本地预览模式,真实认证服务未启用')
error.code = 'AUTH_REMOTE_REQUIRED'
throw error
}
}
const assertAuthScene = (sceneCode) => {
if (!Object.values(AUTH_TAC_SCENE).includes(sceneCode)) {
throw new TypeError('短信场景不属于当前认证合同')
}
return sceneCode
}
const assertValidToken = (validToken) => {
if (typeof validToken !== 'string' || !validToken.trim()) {
throw new TypeError('发送短信前必须取得有效的行为验证票据')
}
return validToken.trim()
}
const assertPasswordHash = (passwordHash) => {
if (typeof passwordHash !== 'string' || !/^[a-f0-9]{32}$/.test(passwordHash)) {
throw new TypeError('密码摘要必须是 32 位小写 MD5')
}
return passwordHash
}
const normalizeFeedbackPayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) {
throw new TypeError('反馈请求必须是普通对象')
}
const allowedFields = new Set(['feedbackType', 'feedbackContent', 'contactInfo'])
const fieldNames = Object.getOwnPropertyNames(payload)
if (Object.getOwnPropertySymbols(payload).length > 0 || fieldNames.some((field) => !allowedFields.has(field))) {
throw new TypeError('反馈请求包含未声明字段')
}
const descriptors = Object.getOwnPropertyDescriptors(payload)
if (fieldNames.some((field) => !Object.prototype.hasOwnProperty.call(descriptors[field], 'value'))) {
throw new TypeError('反馈字段必须是普通数据属性')
}
if (typeof payload.feedbackContent !== 'string' || !payload.feedbackContent.trim()) {
throw new TypeError('反馈内容必须是非空字符串')
}
const normalized = { feedbackContent: payload.feedbackContent.trim() }
for (const optionalField of ['feedbackType', 'contactInfo']) {
if (!Object.prototype.hasOwnProperty.call(payload, optionalField)) continue
if (typeof payload[optionalField] !== 'string') {
throw new TypeError(`反馈字段 ${optionalField} 必须是字符串`)
}
const value = payload[optionalField].trim()
if (value) normalized[optionalField] = value
}
return normalized
}
const normalizeMyGenealogyId = (value) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw createRequestError('我的家谱响应包含无效标识', 'GENEALOGY_RESPONSE_INVALID')
}
const normalizeGenealogyPathId = (value) => {
const id = typeof value === 'number' && Number.isSafeInteger(value) ? String(value) : value
if (typeof id !== 'string' || !/^[1-9]\d*$/.test(id)) {
throw createRequestError('家谱标识无效', 'GENEALOGY_ID_INVALID')
}
return id
}
const normalizeOptionalGenealogyText = (value, label) => {
if (value === undefined || value === null) return ''
if (typeof value !== 'string') {
throw createRequestError(`我的家谱响应字段 ${label} 无效`, 'GENEALOGY_RESPONSE_INVALID')
}
return value.trim()
}
const normalizeAppGenealogy = (item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('我的家谱响应包含无效条目', 'GENEALOGY_RESPONSE_INVALID')
}
const name = normalizeOptionalGenealogyText(item.genealogyName, 'genealogyName')
if (!name) {
throw createRequestError('我的家谱响应缺少家谱名称', 'GENEALOGY_RESPONSE_INVALID')
}
for (const field of ['canManage', 'canEditContent']) {
if (typeof item[field] !== 'boolean') {
throw createRequestError(`我的家谱响应字段 ${field} 无效`, 'GENEALOGY_RESPONSE_INVALID')
}
}
if (item.canView === false) {
throw createRequestError('当前账号无权查看该家谱', 'GENEALOGY_FORBIDDEN')
}
if (!Number.isSafeInteger(item.memberCount) || item.memberCount < 0) {
throw createRequestError('我的家谱响应成员数量无效', 'GENEALOGY_RESPONSE_INVALID')
}
const personCount = item.personCount ?? null
if (personCount !== null && (!Number.isSafeInteger(personCount) || personCount < 0)) {
throw createRequestError('我的家谱响应世系人数无效', 'GENEALOGY_RESPONSE_INVALID')
}
return {
id: normalizeMyGenealogyId(item.genealogyId),
name,
surname: normalizeOptionalGenealogyText(item.surname, 'surname'),
hall: normalizeOptionalGenealogyText(item.ancestralHall, 'ancestralHall'),
location:
normalizeOptionalGenealogyText(item.regionFullName, 'regionFullName') ||
normalizeOptionalGenealogyText(item.regionName, 'regionName') ||
normalizeOptionalGenealogyText(item.originPlace, 'originPlace') ||
normalizeOptionalGenealogyText(item.addressDetail, 'addressDetail') ||
'地区待补',
memberCount: item.memberCount,
personCount,
accessPreset: fromApiGenealogyAccess({
visibility: normalizeOptionalGenealogyText(item.visibility, 'visibility'),
joinMode: normalizeOptionalGenealogyText(item.joinMode, 'joinMode')
}),
accessRole: item.canManage ? 'owner' : 'member',
canManage: item.canManage,
canEditContent: item.canEditContent,
intro: normalizeOptionalGenealogyText(item.intro, 'intro'),
joinTime: normalizeOptionalGenealogyText(item.joinTime, 'joinTime')
}
}
const normalizeMyGenealogies = (value) => {
if (!Array.isArray(value)) {
throw createRequestError('我的家谱响应不是列表', 'GENEALOGY_RESPONSE_INVALID')
}
const normalized = value.map((item) => {
const genealogy = normalizeAppGenealogy(item)
return {
id: genealogy.id,
name: genealogy.name,
surname: genealogy.surname,
hall: genealogy.hall,
location: genealogy.location,
memberCount: genealogy.memberCount,
accessRole: genealogy.accessRole,
canManage: genealogy.canManage,
canEditContent: genealogy.canEditContent
}
})
if (new Set(normalized.map((item) => item.id)).size !== normalized.length) {
throw createRequestError('我的家谱响应包含重复标识', 'GENEALOGY_RESPONSE_INVALID')
}
return normalized
}
const saveLogin = (loginResult) => {
const token = loginResult?.access_token
if (!token) throw new Error('登录响应未包含会话令牌')
session.saveToken(token)
return loginResult
}
const lineageTreeError = (message) =>
createRequestError(message, 'LINEAGE_TREE_RESPONSE_INVALID')
const normalizeLineagePersonId = (value) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw lineageTreeError('世系树包含无效人物标识')
}
const normalizeLineageText = (value, label, { required = false } = {}) => {
if (value === undefined || value === null) {
if (required) throw lineageTreeError(`世系树缺少${label}`)
return ''
}
if (typeof value !== 'string') throw lineageTreeError(`世系树${label}无效`)
const normalized = value.trim()
if (required && !normalized) throw lineageTreeError(`世系树缺少${label}`)
return normalized
}
const lineageDatePart = (value, label) => {
const normalized = normalizeLineageText(value, label)
if (!normalized) return ''
if (!/^\d{4}-\d{2}-\d{2}/.test(normalized)) throw lineageTreeError(`世系树${label}无效`)
return normalized.slice(0, 10)
}
const normalizeLineageTree = (value) => {
if (!Array.isArray(value)) throw lineageTreeError('世系树响应不是列表')
const seen = new Set()
const normalized = []
const appendNode = (node, parentId, relationOverride = '') => {
if (!node || typeof node !== 'object' || Array.isArray(node)) {
throw lineageTreeError('世系树包含无效节点')
}
const id = normalizeLineagePersonId(node.personId)
if (seen.has(id)) throw lineageTreeError('世系树包含重复人物标识')
seen.add(id)
if (seen.size > 5000) throw lineageTreeError('世系树节点数量超出客户端上限')
if (!Number.isSafeInteger(node.generation) || node.generation < 1) {
throw lineageTreeError('世系树人物世代无效')
}
const generationName = normalizeLineageText(node.generationName, '字辈')
const birthDate = lineageDatePart(node.birthDate, '出生日期')
const deathDate = lineageDatePart(node.deathDate, '逝世日期')
normalized.push({
id,
parentId,
name: normalizeLineageText(node.name, '人物姓名', { required: true }),
relation:
relationOverride ||
normalizeLineageText(node.relationName, '人物关系') ||
(parentId ? '后代' : '始祖'),
generation: node.generation,
branch: generationName
? (generationName.endsWith('字辈') ? generationName : `${generationName}字辈`)
: '字辈待补',
years:
birthDate || deathDate
? `${birthDate}${deathDate}`
: '生卒待补',
sex: normalizeLineageText(node.sex, '性别'),
personStatus: normalizeLineageText(node.personStatus, '人物状态')
})
return id
}
const walk = (node, parentId = null, depth = 0) => {
if (depth > 64) throw lineageTreeError('世系树深度超出客户端上限')
const id = appendNode(node, parentId)
const spouses = node.spouses ?? []
const children = node.children ?? []
if (!Array.isArray(spouses) || !Array.isArray(children)) {
throw lineageTreeError('世系树亲属集合无效')
}
spouses.forEach((spouse) => appendNode(spouse, parentId, '配偶'))
children.forEach((child) => walk(child, id, depth + 1))
}
value.forEach((root) => walk(root))
return normalized
}
const lineagePersonError = (message) =>
createRequestError(message, 'LINEAGE_PERSON_RESPONSE_INVALID')
const normalizeLineagePersonIdentity = (value, label) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw lineagePersonError(`成员详情${label}无效`)
}
const normalizeLineagePersonText = (value, label, { required = false } = {}) => {
if (value === undefined || value === null) {
if (required) throw lineagePersonError(`成员详情缺少${label}`)
return ''
}
if (typeof value !== 'string') throw lineagePersonError(`成员详情${label}无效`)
const normalized = value.trim()
if (required && !normalized) throw lineagePersonError(`成员详情缺少${label}`)
return normalized
}
const normalizeLineagePersonDate = (value, label) => {
const normalized = normalizeLineagePersonText(value, label)
if (!normalized) return ''
if (!/^\d{4}-\d{2}-\d{2}(?:T.*)?$/.test(normalized)) {
throw lineagePersonError(`成员详情${label}无效`)
}
const datePart = normalized.slice(0, 10)
const parsed = new Date(`${datePart}T00:00:00Z`)
if (
Number.isNaN(parsed.getTime()) ||
parsed.toISOString().slice(0, 10) !== datePart
) {
throw lineagePersonError(`成员详情${label}无效`)
}
return datePart
}
const normalizeLineagePersonDetail = (
value,
expectedGenealogyId,
expectedPersonId,
) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw lineagePersonError('成员详情响应不是对象')
}
const genealogyId = normalizeLineagePersonIdentity(value.genealogyId, '家谱标识')
const id = normalizeLineagePersonIdentity(value.personId, '人物标识')
if (genealogyId !== expectedGenealogyId || id !== expectedPersonId) {
throw lineagePersonError('成员详情响应标识与请求不匹配')
}
if (!Number.isSafeInteger(value.generation) || value.generation < 1) {
throw lineagePersonError('成员详情世代无效')
}
const name = normalizeLineagePersonText(value.name, '姓名', { required: true })
const generationName = normalizeLineagePersonText(value.generationName, '字辈')
const aliasName = normalizeLineagePersonText(value.aliasName, '别名或曾用名')
const birthDate = normalizeLineagePersonDate(value.birthDate, '出生日期')
const deathDate = normalizeLineagePersonDate(value.deathDate, '逝世日期')
const birthLunar = normalizeLineagePersonText(value.birthLunar, '出生农历')
const deathLunar = normalizeLineagePersonText(value.deathLunar, '逝世农历')
const birthPlace = normalizeLineagePersonText(value.birthPlace, '出生地')
const deathPlace = normalizeLineagePersonText(value.deathPlace, '逝世地')
const burialPlace = normalizeLineagePersonText(value.burialPlace, '安葬地')
const spouseNames = normalizeLineagePersonText(value.spouseNames, '配偶姓名')
const personStatus = normalizeLineagePersonText(value.personStatus, '人物状态')
const biography = normalizeLineagePersonText(value.biography, '生平')
const remark = normalizeLineagePersonText(value.remark, '备注')
const relatives = []
for (const relation of [
{ id: value.fatherId, name: value.fatherName, label: '父亲' },
{ id: value.motherId, name: value.motherName, label: '母亲' }
]) {
if (relation.id === undefined || relation.id === null) continue
const relativeId = normalizeLineagePersonIdentity(relation.id, `${relation.label}标识`)
if (relativeId === id || relatives.some((item) => item.id === relativeId)) {
throw lineagePersonError('成员详情亲属标识冲突')
}
relatives.push({
id: relativeId,
name: normalizeLineagePersonText(relation.name, `${relation.label}姓名`) || relation.label,
relation: relation.label
})
}
return {
id,
genealogyId,
genealogyName: normalizeLineagePersonText(value.genealogyName, '家谱名称') || '当前家谱',
name,
aliasName,
generation: value.generation,
generationName,
relation: value.generation === 1 ? '始祖' : '家谱成员',
branch: generationName
? (generationName.endsWith('字辈') ? generationName : `${generationName}字辈`)
: '字辈待补',
sex: normalizeLineagePersonText(value.sex, '性别'),
birthDate,
birthLunar,
deathDate,
deathLunar,
years: birthDate || deathDate ? `${birthDate}${deathDate}` : '生卒待补',
birthplace: birthPlace,
deathPlace,
burialPlace,
spouseNames,
personStatus,
biography,
remark,
status: ['DECEASED', 'DEAD'].includes(personStatus.toUpperCase()) ? 'deceased' : 'normal',
relatives
}
}
const normalizeLineagePersonPage = (value, expectedGenealogyId) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw lineagePersonError('成员分页响应不是对象')
}
if (!Array.isArray(value.rows)) {
throw lineagePersonError('成员分页缺少 rows')
}
if (!Number.isSafeInteger(value.total) || value.total < 0) {
throw lineagePersonError('成员分页 total 无效')
}
const rows = value.rows.map((item) =>
normalizeLineagePersonDetail(
item,
expectedGenealogyId,
normalizeLineagePersonIdentity(item?.personId, '人物标识'),
),
)
if (new Set(rows.map((item) => item.id)).size !== rows.length) {
throw lineagePersonError('成员分页包含重复人物标识')
}
return { rows, total: value.total }
}
const normalizeFeedCommentId = (value, label) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw createRequestError(`家族动态评论${label}无效`, 'FEED_COMMENT_RESPONSE_INVALID')
}
const normalizeFeedCommentText = (value, label, { required = false } = {}) => {
if (value === undefined || value === null) {
if (required) throw createRequestError(`家族动态评论缺少${label}`, 'FEED_COMMENT_RESPONSE_INVALID')
return ''
}
if (typeof value !== 'string') {
throw createRequestError(`家族动态评论${label}无效`, 'FEED_COMMENT_RESPONSE_INVALID')
}
const normalized = value.trim()
if (required && !normalized) {
throw createRequestError(`家族动态评论缺少${label}`, 'FEED_COMMENT_RESPONSE_INVALID')
}
return normalized
}
const normalizeFeedComments = (value, expectedGenealogyId, expectedFeedId) => {
if (!Array.isArray(value)) {
throw createRequestError('家族动态评论响应不是列表', 'FEED_COMMENT_RESPONSE_INVALID')
}
const comments = value.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('家族动态评论包含无效条目', 'FEED_COMMENT_RESPONSE_INVALID')
}
const genealogyId = normalizeFeedCommentId(item.genealogyId, '家谱标识')
const feedId = normalizeFeedCommentId(item.feedId, '动态标识')
if (genealogyId !== expectedGenealogyId || feedId !== expectedFeedId) {
throw createRequestError('家族动态评论归属与请求不匹配', 'FEED_COMMENT_RESPONSE_INVALID')
}
return {
id: normalizeFeedCommentId(item.commentId, '标识'),
author: normalizeFeedCommentText(item.appUserNickName, '用户昵称') || '未署名成员',
content: normalizeFeedCommentText(item.commentContent, '评论内容', { required: true }),
time: normalizeFeedCommentText(item.createTime, '创建时间'),
parentCommentId: item.parentCommentId === undefined || item.parentCommentId === null
? null
: normalizeFeedCommentId(item.parentCommentId, '父评论标识'),
replyCount: Number.isSafeInteger(item.replyCount) && item.replyCount >= 0 ? item.replyCount : 0,
}
})
if (new Set(comments.map((item) => item.id)).size !== comments.length) {
throw createRequestError('家族动态评论包含重复标识', 'FEED_COMMENT_RESPONSE_INVALID')
}
return comments
}
const normalizeFeedCommentPayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) {
throw new TypeError('家族动态评论请求必须是普通对象')
}
const fields = Object.keys(payload)
if (fields.some((field) => !['parentCommentId', 'commentContent'].includes(field))) {
throw new TypeError('家族动态评论请求包含未声明字段')
}
const commentContent = normalizeFeedCommentText(payload.commentContent, '评论内容', { required: true })
if (Array.from(commentContent).length > 1000) throw new TypeError('家族动态评论不能超过 1000 个字符')
const data = { commentContent }
if (payload.parentCommentId !== undefined && payload.parentCommentId !== null) {
data.parentCommentId = normalizeFeedCommentId(payload.parentCommentId, '父评论标识')
}
return data
}
const normalizeFeedCreatePayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) {
throw new TypeError('家族动态请求必须是普通对象')
}
if (Object.keys(payload).some((field) => field !== 'feedContent')) {
throw new TypeError('家族动态请求包含未声明字段')
}
if (typeof payload.feedContent !== 'string' || !payload.feedContent.trim()) {
throw new TypeError('家族动态内容不能为空')
}
return { feedContent: payload.feedContent.trim() }
}
const normalizeArticleCreatePayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) {
throw new TypeError('谱文请求必须是普通对象')
}
if (Object.keys(payload).some((field) => !['articleTitle', 'articleContent'].includes(field))) {
throw new TypeError('谱文请求包含未声明字段')
}
const articleTitle = typeof payload.articleTitle === 'string' ? payload.articleTitle.trim() : ''
const articleContent = typeof payload.articleContent === 'string' ? payload.articleContent.trim() : ''
if (!articleTitle) throw new TypeError('谱文标题不能为空')
if (!articleContent) throw new TypeError('谱文正文不能为空')
return { articleTitle, articleContent }
}
const normalizeAlbumCreatePayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) {
throw new TypeError('相册请求必须是普通对象')
}
if (Object.keys(payload).some((field) => field !== 'albumName')) {
throw new TypeError('相册请求包含未声明字段')
}
if (typeof payload.albumName !== 'string' || !payload.albumName.trim()) {
throw new TypeError('相册名称不能为空')
}
return { albumName: payload.albumName.trim() }
}
const normalizeRelativeRecordCreatePayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) {
throw new TypeError('亲友往来请求必须是普通对象')
}
const allowedFields = ['relativeName', 'relationName', 'eventName', 'eventTime', 'giftAmount', 'recordContent']
if (Object.keys(payload).some((field) => !allowedFields.includes(field))) {
throw new TypeError('亲友往来请求包含未声明字段')
}
const relativeName = typeof payload.relativeName === 'string' ? payload.relativeName.trim() : ''
if (!relativeName) throw new TypeError('亲友姓名不能为空')
const data = { relativeName }
for (const field of ['relationName', 'eventName', 'eventTime', 'recordContent']) {
if (payload[field] === undefined) continue
if (typeof payload[field] !== 'string') throw new TypeError(`亲友往来${field}必须是字符串`)
const value = payload[field].trim()
if (value) data[field] = value
}
if (payload.giftAmount !== undefined && payload.giftAmount !== '' && payload.giftAmount !== null) {
if (typeof payload.giftAmount !== 'number' || !Number.isFinite(payload.giftAmount)) {
throw new TypeError('亲友往来礼金金额必须是有限数字')
}
data.giftAmount = payload.giftAmount
}
return data
}
const normalizeGrowthRecordCreatePayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) throw new TypeError('成长记录请求必须是普通对象')
if (Object.keys(payload).some((field) => !['recordTitle', 'recordContent', 'recordDate'].includes(field))) throw new TypeError('成长记录请求包含未声明字段')
const recordTitle = typeof payload.recordTitle === 'string' ? payload.recordTitle.trim() : ''
if (!recordTitle) throw new TypeError('成长记录标题不能为空')
const data = { recordTitle }
for (const field of ['recordContent', 'recordDate']) {
if (payload[field] === undefined) continue
if (typeof payload[field] !== 'string') throw new TypeError(`成长记录${field}必须是字符串`)
const value = payload[field].trim()
if (value) data[field] = value
}
return data
}
const normalizeMemoCreatePayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) throw new TypeError('备忘请求必须是普通对象')
if (Object.keys(payload).some((field) => !['memoTitle', 'memoContent', 'remindTime'].includes(field))) throw new TypeError('备忘请求包含未声明字段')
const memoTitle = typeof payload.memoTitle === 'string' ? payload.memoTitle.trim() : ''
if (!memoTitle) throw new TypeError('备忘标题不能为空')
const data = { memoTitle }
for (const field of ['memoContent', 'remindTime']) {
if (payload[field] === undefined) continue
if (typeof payload[field] !== 'string') throw new TypeError(`备忘${field}必须是字符串`)
const value = payload[field].trim()
if (value) data[field] = value
}
return data
}
const normalizeMeritRecordCreatePayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) throw new TypeError('功德记录请求必须是普通对象')
const allowedFields = ['donorName', 'meritTitle', 'meritType', 'meritContent', 'amount', 'meritTime']
if (Object.keys(payload).some((field) => !allowedFields.includes(field))) throw new TypeError('功德记录请求包含未声明字段')
const donorName = typeof payload.donorName === 'string' ? payload.donorName.trim() : ''
const meritTitle = typeof payload.meritTitle === 'string' ? payload.meritTitle.trim() : ''
if (!donorName || !meritTitle) throw new TypeError('功德记录捐赠人和标题不能为空')
const data = { donorName, meritTitle }
for (const field of ['meritType', 'meritContent', 'meritTime']) {
if (payload[field] === undefined) continue
if (typeof payload[field] !== 'string') throw new TypeError(`功德记录${field}必须是字符串`)
const value = payload[field].trim()
if (value) data[field] = value
}
if (payload.amount !== undefined && payload.amount !== '' && payload.amount !== null) {
if (typeof payload.amount !== 'number' || !Number.isFinite(payload.amount)) throw new TypeError('功德金额必须是有限数字')
data.amount = payload.amount
}
return data
}
const normalizeGenerationPoemId = (value, label) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw createRequestError(`字辈${label}无效`, 'GENERATION_POEM_RESPONSE_INVALID')
}
const normalizeGenerationPoemNumber = (value, label) => {
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return value
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) {
const normalized = Number(value)
if (Number.isSafeInteger(normalized)) return normalized
}
throw createRequestError(`字辈${label}无效`, 'GENERATION_POEM_RESPONSE_INVALID')
}
const normalizeGenerationPoemText = (value, label, { required = false, maxLength = null } = {}) => {
if (value === undefined || value === null) {
if (required) throw createRequestError(`字辈缺少${label}`, 'GENERATION_POEM_RESPONSE_INVALID')
return ''
}
if (typeof value !== 'string') throw createRequestError(`字辈${label}无效`, 'GENERATION_POEM_RESPONSE_INVALID')
const normalized = value.trim()
if (required && !normalized) throw createRequestError(`字辈缺少${label}`, 'GENERATION_POEM_RESPONSE_INVALID')
if (maxLength !== null && Array.from(normalized).length > maxLength) {
throw createRequestError(`字辈${label}超出合同长度`, 'GENERATION_POEM_RESPONSE_INVALID')
}
return normalized
}
const normalizeGenerationPoemStatus = (value) => {
if (value === '0' || value === '1') return value
throw createRequestError('字辈状态无效', 'GENERATION_POEM_RESPONSE_INVALID')
}
const normalizeGenerationPoemRows = (value, expectedGenealogyId) => {
if (!Array.isArray(value)) throw createRequestError('字辈响应不是列表', 'GENERATION_POEM_RESPONSE_INVALID')
const rows = value.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('字辈响应包含无效条目', 'GENERATION_POEM_RESPONSE_INVALID')
}
const genealogyId = normalizeGenerationPoemId(item.genealogyId, '家谱标识')
if (genealogyId !== expectedGenealogyId) {
throw createRequestError('字辈响应家谱标识与请求不匹配', 'GENERATION_POEM_RESPONSE_INVALID')
}
return {
poemId: normalizeGenerationPoemId(item.poemId, '标识'),
genealogyId,
genealogyNo: normalizeGenerationPoemText(item.genealogyNo, '家谱编号'),
genealogyName: normalizeGenerationPoemText(item.genealogyName, '家谱名称'),
generationNo: normalizeGenerationPoemNumber(item.generationNo, '世代'),
generationText: normalizeGenerationPoemText(item.generationText, '文字', { required: true, maxLength: 50 }),
description: normalizeGenerationPoemText(item.description, '说明', { maxLength: 500 }),
sortOrder: item.sortOrder,
status: normalizeGenerationPoemStatus(item.status),
}
})
if (new Set(rows.map((item) => item.poemId)).size !== rows.length) {
throw createRequestError('字辈响应包含重复标识', 'GENERATION_POEM_RESPONSE_INVALID')
}
if (new Set(rows.map((item) => item.generationNo)).size !== rows.length) {
throw createRequestError('字辈响应包含重复世代', 'GENERATION_POEM_RESPONSE_INVALID')
}
return rows.sort((left, right) => left.generationNo - right.generationNo)
}
const normalizeGenerationPoemBatchPayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) {
throw new TypeError('字辈批量请求必须是普通对象')
}
if (Object.keys(payload).some((field) => !['poemText', 'disableMissing'].includes(field))) {
throw new TypeError('字辈批量请求包含未声明字段')
}
const poemText = normalizeGenerationPoemText(payload.poemText, '内容', { required: true, maxLength: 26000 })
const data = { poemText }
if (payload.disableMissing !== undefined) {
if (typeof payload.disableMissing !== 'boolean') throw new TypeError('字辈停用策略必须是布尔值')
data.disableMissing = payload.disableMissing
}
return data
}
const normalizeGenerationPoemPreview = (value, expectedGenealogyId) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('字辈批量预览响应不是对象', 'GENERATION_POEM_RESPONSE_INVALID')
}
const genealogyId = normalizeGenerationPoemId(value.genealogyId, '预览家谱标识')
if (genealogyId !== expectedGenealogyId) {
throw createRequestError('字辈预览家谱标识与请求不匹配', 'GENERATION_POEM_RESPONSE_INVALID')
}
const counts = ['createCount', 'updateCount', 'keepCount', 'disableCount']
const preview = { genealogyId, items: Array.isArray(value.items) ? value.items : null }
for (const field of counts) {
if (!Number.isSafeInteger(value[field]) || value[field] < 0) {
throw createRequestError(`字辈预览${field}无效`, 'GENERATION_POEM_RESPONSE_INVALID')
}
preview[field] = value[field]
}
if (preview.items === null) {
throw createRequestError('字辈预览缺少明细列表', 'GENERATION_POEM_RESPONSE_INVALID')
}
return preview
}
const normalizeLineageWritePayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) {
throw new TypeError('人物写入请求必须是普通对象')
}
const allowedFields = new Set([
'name',
'aliasName',
'generationName',
'birthDate',
'birthLunar',
'birthPlace',
'deathDate',
'deathLunar',
'deathPlace',
'burialPlace',
'biography',
'remark',
'relationName'
])
for (const field of Object.keys(payload)) {
if (!allowedFields.has(field)) throw new TypeError(`人物写入包含未声明字段:${field}`)
}
const name = normalizeLineagePersonText(payload.name, '姓名', { required: true })
if (name.length > 20) throw new TypeError('人物姓名长度超出当前页面合同')
const data = { name }
for (const [field, label, maxLength] of [
['generationName', '字辈', 12],
['aliasName', '别名或曾用名', null],
['birthLunar', '出生农历', null],
['birthPlace', '出生地', null],
['deathLunar', '逝世农历', null],
['deathPlace', '逝世地', null],
['burialPlace', '安葬地', null],
['biography', '人物简介', 500],
['remark', '备注', null],
['relationName', '关系显示名称', null]
]) {
if (payload[field] === undefined) continue
const value = normalizeLineagePersonText(payload[field], label)
if (maxLength && value.length > maxLength) throw new TypeError(`人物${label}长度超出当前页面合同`)
if (value) data[field] = value
}
for (const [field, label] of [
['birthDate', '出生日期'],
['deathDate', '离世日期']
]) {
if (payload[field] === undefined) continue
const value = normalizeLineagePersonDate(payload[field], label)
if (value) data[field] = value
}
if (data.birthDate && data.deathDate && data.deathDate < data.birthDate) {
throw new TypeError('人物离世日期不能早于出生日期')
}
return data
}
const lineageRelationPath = Object.freeze({
FATHER: 'parents',
MOTHER: 'parents',
SPOUSE: 'spouses',
SIBLING: 'siblings',
SON: 'children',
DAUGHTER: 'children'
})
const requireLineageWriteRequestController = (requestOptions) => {
if (!requestOptions || typeof requestOptions !== 'object' || Array.isArray(requestOptions) || Object.getPrototypeOf(requestOptions) !== Object.prototype) {
throw new TypeError('人物写入请求选项必须是普通对象')
}
const fields = Object.keys(requestOptions)
if (fields.some((field) => field !== 'requestController')) {
throw new TypeError('人物写入请求选项包含未声明字段')
}
return requestOptions.requestController ?? null
}
export const appApi = {
async getCaptchaRequirement({ sceneCode, subject }, requestOptions = {}) {
requireRemoteAuth()
return requestAuth({
url: '/captcha/require',
method: 'GET',
data: authPayload({ sceneCode: assertAuthScene(sceneCode), subject })
}, requestOptions)
},
async sendSmsCode({ sceneCode, phone, validToken }, requestOptions = {}) {
requireRemoteAuth()
return requestAuthVoid({
url: '/genealogy/app/auth/sms/code',
method: 'POST',
data: authPayload({
grantType: 'sms',
sceneCode: assertAuthScene(sceneCode),
phone,
validToken: assertValidToken(validToken)
})
}, requestOptions)
},
async loginWithPassword({ phone, passwordHash }, requestOptions = {}) {
requireRemoteAuth()
const result = await requestAuth({
url: '/genealogy/app/auth/login',
method: 'POST',
data: authPayload({ grantType: 'password', phone, password: assertPasswordHash(passwordHash) })
}, requestOptions)
return saveLogin(result)
},
async loginWithSms({ phone, smsCode }, requestOptions = {}) {
requireRemoteAuth()
const result = await requestAuth({
url: '/genealogy/app/auth/login/sms',
method: 'POST',
data: authPayload({ grantType: 'sms', phone, smsCode: assertSmsCode(smsCode) })
}, requestOptions)
return saveLogin(result)
},
async registerWithPassword({ phone, passwordHash, smsCode }, requestOptions = {}) {
requireRemoteAuth()
const result = await requestAuth({
url: '/genealogy/app/auth/register',
method: 'POST',
data: authPayload({
grantType: 'password',
phone,
password: assertPasswordHash(passwordHash),
smsCode: assertSmsCode(smsCode)
})
}, requestOptions)
return saveLogin(result)
},
async resetPassword({ phone, passwordHash, smsCode }, requestOptions = {}) {
requireRemoteAuth()
return requestAuthVoid({
url: '/genealogy/app/auth/password/reset',
method: 'PUT',
data: authPayload({
grantType: 'password',
phone,
newPassword: assertPasswordHash(passwordHash),
smsCode: assertSmsCode(smsCode)
})
}, requestOptions)
},
async changePassword({ oldPasswordHash, newPasswordHash }, requestOptions = {}) {
requireRemoteAuth()
await requestStrict({
url: '/genealogy/app/auth/password',
method: 'PUT',
data: {
oldPassword: assertPasswordHash(oldPasswordHash),
newPassword: assertPasswordHash(newPasswordHash)
}
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return null
},
async logout(requestOptions = {}) {
if (resolveRuntimeMode() !== 'remote') return null
await requestStrict({
url: '/genealogy/app/auth/logout',
method: 'DELETE'
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return null
},
async submitFeedback(payload, requestOptions = {}) {
const data = normalizeFeedbackPayload(payload)
if (!requestOptions || typeof requestOptions !== 'object' || Array.isArray(requestOptions) || Object.getPrototypeOf(requestOptions) !== Object.prototype) {
throw new TypeError('反馈请求选项必须是普通对象')
}
const optionNames = Object.getOwnPropertyNames(requestOptions)
const optionDescriptors = Object.getOwnPropertyDescriptors(requestOptions)
if (
Object.getOwnPropertySymbols(requestOptions).length > 0 ||
optionNames.some((field) => field !== 'requestController') ||
optionNames.some((field) => !Object.prototype.hasOwnProperty.call(optionDescriptors[field], 'value'))
) {
throw new TypeError('反馈请求选项包含未声明字段')
}
const runtimeMode = resolveRuntimeMode()
if (runtimeMode !== 'remote') {
throw createRequestError('当前为本地预览模式,反馈未提交服务器', 'WRITE_UNAVAILABLE')
}
return requestStrict({
url: '/genealogy/app/feedback',
method: 'POST',
data
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
},
async getProfile() {
return hasRemoteConfig() ? request({ url: '/genealogy/app/auth/profile' }) : currentUser
},
async getMyGenealogies(requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('我的家谱需要真实读取服务,当前本地预览不会伪造家谱列表')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const result = await requestStrict({
url: '/genealogy/app/genealogies/mine',
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeMyGenealogies(result)
},
async createGenealogy(payload) {
if (hasRemoteConfig()) return request({ url: '/genealogy/app/genealogies', method: 'POST', data: payload })
const accessPreset = Object.values(GENEALOGY_ACCESS_PRESET).includes(payload.accessPreset)
? payload.accessPreset
: GENEALOGY_ACCESS_PRESET.MEMBER_ONLY
const created = { ...payload, id: String(Date.now()), memberCount: 0, activeCount: 0, motto: '敦亲睦族,敬祖传家。', accessPreset, membership: 'created' }
genealogies.unshift(created)
return created
},
async getGenealogy(genealogyId) {
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}` }) : genealogies.find((item) => String(item.id) === String(genealogyId))
},
async getOverview(genealogyId, requestOptions = {}) {
const normalizedId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) {
const error = new Error('家谱概览需要真实读取服务,当前本地预览不会伪造概览数据')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedId}/overview`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
const overview = normalizeAppGenealogy(result)
if (overview.id !== normalizedId) {
throw createRequestError('家谱概览响应标识不匹配', 'GENEALOGY_RESPONSE_INVALID')
}
return overview
},
async getTree(genealogyId, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('世系树需要真实读取服务,当前本地预览不会伪造人物节点')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const normalizedId = normalizeGenealogyPathId(genealogyId)
const tree = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedId}/lineage/tree`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeLineageTree(tree)
},
async getPerson(genealogyId, personId, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('人物详情需要真实读取服务,当前本地预览不会伪造成员资料')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识')
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/${normalizedPersonId}`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeLineagePersonDetail(result, normalizedGenealogyId, normalizedPersonId)
},
async getPersonPage(genealogyId, { pageNum = 1, pageSize = 10, keyword = '' } = {}, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('成员目录需要真实读取服务,当前本地预览不会伪造目录数据')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
if (!Number.isSafeInteger(pageNum) || pageNum < 1) throw new TypeError('成员目录页码无效')
if (!Number.isSafeInteger(pageSize) || pageSize < 1) throw new TypeError('成员目录页大小无效')
if (typeof keyword !== 'string') throw new TypeError('成员目录关键词无效')
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/page`,
method: 'GET',
data: { pageNum, pageSize, ...(keyword.trim() ? { keyword: keyword.trim() } : {}) }
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeLineagePersonPage(result, normalizedGenealogyId)
},
async getFeedComments(genealogyId, feedId, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('动态评论需要真实读取服务,当前本地预览不会伪造评论数据')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedFeedId = normalizeFeedCommentId(feedId, '动态标识')
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}/comments`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeFeedComments(result, normalizedGenealogyId, normalizedFeedId)
},
async createFeedComment(genealogyId, feedId, payload, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('动态评论需要真实服务,当前本地预览不会伪造提交成功')
error.code = 'REMOTE_WRITE_REQUIRED'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedFeedId = normalizeFeedCommentId(feedId, '动态标识')
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}/comments`,
method: 'POST',
data: normalizeFeedCommentPayload(payload)
}, {
requestController: requestOptions.requestController ?? null
})
},
async getGenerationPoems(genealogyId, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('字辈列表需要真实读取服务,当前本地预览不会伪造字辈数据')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeGenerationPoemRows(result, normalizedGenealogyId)
},
async getGenerationPoemManagement(genealogyId, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('字辈维护列表需要真实读取服务,当前本地预览不会伪造管理权限')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems/management`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeGenerationPoemRows(result, normalizedGenealogyId)
},
async previewGenerationPoemBatch(genealogyId, payload, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('字辈批量预览需要真实服务,当前本地预览不会伪造差异结果')
error.code = 'REMOTE_READ_REQUIRED'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems/batch/preview`,
method: 'POST',
data: normalizeGenerationPoemBatchPayload(payload)
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeGenerationPoemPreview(result, normalizedGenealogyId)
},
async saveGenerationPoemBatch(genealogyId, payload, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('字辈批量保存需要真实服务,当前本地预览不会伪造保存成功')
error.code = 'REMOTE_WRITE_REQUIRED'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems/batch/save`,
method: 'POST',
data: normalizeGenerationPoemBatchPayload(payload)
}, {
requestController: requestOptions.requestController ?? null
})
},
async createPerson(genealogyId, payload, requestOptions = {}) {
if (hasRemoteConfig()) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons`,
method: 'POST',
data: normalizeLineageWritePayload(payload)
}, {
requestController: requireLineageWriteRequestController(requestOptions)
})
}
const error = new Error('人物创建接口在本地预览模式不可用,当前内容不会保存')
error.code = 'WRITE_UNAVAILABLE'
throw error
},
async createRelatedPerson(genealogyId, personId, relationType, payload, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('人物关系写入接口在本地预览模式不可用,当前内容不会保存')
error.code = 'WRITE_UNAVAILABLE'
throw error
}
const relationPath = lineageRelationPath[relationType]
if (!relationPath) throw new TypeError('人物关系类型不属于当前合同')
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识')
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/${normalizedPersonId}/${relationPath}`,
method: 'POST',
data: normalizeLineageWritePayload(payload)
}, {
requestController: requireLineageWriteRequestController(requestOptions)
})
},
async updatePerson(genealogyId, personId, payload, requestOptions = {}) {
if (!hasRemoteConfig()) {
const error = new Error('人物编辑接口在本地预览模式不可用,当前内容不会保存')
error.code = 'WRITE_UNAVAILABLE'
throw error
}
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识')
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/${normalizedPersonId}`,
method: 'PUT',
data: normalizeLineageWritePayload(payload)
}, {
requestController: requireLineageWriteRequestController(requestOptions)
})
},
async getFeeds(genealogyId) {
return hasRemoteConfig()
? request({ url: `/genealogy/app/genealogies/${genealogyId}/feeds` })
: listFamilyFeedFixtures(genealogyId)
},
async createFeed(genealogyId, payload, requestOptions = {}) {
if (hasRemoteConfig()) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds`,
method: 'POST',
data: normalizeFeedCreatePayload(payload)
}, {
requestController: requestOptions.requestController ?? null
})
}
const error = new Error('动态发布接口尚未接入,当前内容不会保存')
error.code = 'WRITE_UNAVAILABLE'
throw error
},
async createArticle(genealogyId, payload, requestOptions = {}) {
if (hasRemoteConfig()) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/articles`,
method: 'POST',
data: normalizeArticleCreatePayload(payload)
}, {
requestController: requestOptions.requestController ?? null
})
}
const error = new Error('谱文创建需要真实服务,当前本地预览不会伪造创建成功')
error.code = 'REMOTE_WRITE_REQUIRED'
throw error
},
async createAlbum(genealogyId, payload, requestOptions = {}) {
if (hasRemoteConfig()) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/albums`,
method: 'POST',
data: normalizeAlbumCreatePayload(payload)
}, {
requestController: requestOptions.requestController ?? null
})
}
const error = new Error('相册创建需要真实服务,当前本地预览不会伪造创建成功')
error.code = 'REMOTE_WRITE_REQUIRED'
throw error
},
async createRelativeRecord(genealogyId, payload, requestOptions = {}) {
if (hasRemoteConfig()) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/relative-records`,
method: 'POST',
data: normalizeRelativeRecordCreatePayload(payload)
}, {
requestController: requestOptions.requestController ?? null
})
}
const error = new Error('亲友往来创建需要真实服务,当前本地预览不会伪造创建成功')
error.code = 'REMOTE_WRITE_REQUIRED'
throw error
},
async createGrowthRecord(genealogyId, payload, requestOptions = {}) {
if (hasRemoteConfig()) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records`, method: 'POST', data: normalizeGrowthRecordCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null })
}
const error = new Error('成长记录创建需要真实服务,当前本地预览不会伪造创建成功')
error.code = 'REMOTE_WRITE_REQUIRED'
throw error
},
async createMemo(genealogyId, payload, requestOptions = {}) {
if (hasRemoteConfig()) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/memos`, method: 'POST', data: normalizeMemoCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null })
}
const error = new Error('备忘创建需要真实服务,当前本地预览不会伪造创建成功')
error.code = 'REMOTE_WRITE_REQUIRED'
throw error
},
async createMeritRecord(genealogyId, payload, requestOptions = {}) {
if (hasRemoteConfig()) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({ url: `/genealogy/app/genealogies/${normalizedGenealogyId}/merit-records`, method: 'POST', data: normalizeMeritRecordCreatePayload(payload) }, { requestController: requestOptions.requestController ?? null })
}
const error = new Error('功德记录创建需要真实服务,当前本地预览不会伪造创建成功')
error.code = 'REMOTE_WRITE_REQUIRED'
throw error
},
async getArticles(genealogyId) {
return hasRemoteConfig()
? request({ url: `/genealogy/app/genealogies/${genealogyId}/articles` })
: listFamilyArticleFixtures(genealogyId)
},
async getAlbums(genealogyId) {
return hasRemoteConfig()
? request({ url: `/genealogy/app/genealogies/${genealogyId}/albums` })
: listFamilyAlbumFixtures(genealogyId)
},
async getCeremonies(genealogyId) {
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/ceremonies` }) : listCeremonyFixtures(genealogyId)
},
async getGrowthRecords(genealogyId) {
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/growth-records` }) : listGrowthRecordFixtures(genealogyId)
},
async getContent(type, genealogyId) {
const loaders = { article: this.getArticles, album: this.getAlbums, ceremony: this.getCeremonies, record: this.getGrowthRecords }
if (!loaders[type]) throw new Error('未知内容类型')
return loaders[type].call(this, genealogyId)
},
async getNotifications() {
return hasRemoteConfig() ? request({ url: '/genealogy/app/notifications' }) : listNotificationFixtures()
},
async markNotificationRead(notificationId) {
if (hasRemoteConfig()) return request({ url: `/genealogy/app/notifications/${notificationId}/read`, method: 'POST' })
const notification = notifications.find((item) => String(item.id) === String(notificationId))
if (notification) notification.unread = false
return { success: true }
},
async markAllNotificationsRead() {
if (hasRemoteConfig()) return request({ url: '/genealogy/app/notifications/read-all', method: 'POST' })
notifications.forEach((item) => { item.unread = false })
return { success: true }
},
async getPublicGenealogies() {
return hasRemoteConfig() ? request({ url: '/genealogy/app/genealogies/public' }) : publicGenealogies
},
async applyToJoin(genealogyId, payload) {
if (hasRemoteConfig()) return request({ url: `/genealogy/app/genealogies/${genealogyId}/join-applies`, method: 'POST', data: payload })
joinApplications.unshift({
id: String(Date.now()),
genealogyId,
name: payload.applicantName || currentUser.name,
phone: payload.phone || currentUser.phone,
relation: payload.relationDesc || '关系待补充',
reason: payload.applyReason || '',
appliedAt: '刚刚',
status: 'PENDING'
})
return { success: true }
},
async getPendingApplications(genealogyId) {
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/join-applies/pending` }) : joinApplications.filter((item) => String(item.genealogyId || 1001) === String(genealogyId))
},
async auditApplication(genealogyId, applicationId, { status, auditRemark = '' }) {
if (typeof status !== 'string' || !/^[12]$/.test(status)) throw new TypeError('审核状态只能是 1 或 2')
if (typeof auditRemark !== 'string') throw new TypeError('审核备注必须是字符串')
if (Array.from(auditRemark).length > 500) throw new RangeError('审核备注不能超过 500 个字符')
if (hasRemoteConfig()) return request({ url: `/genealogy/app/genealogies/${genealogyId}/join-applies/${applicationId}/audit`, method: 'PUT', data: { status, auditRemark } })
const application = joinApplications.find((item) => String(item.id) === String(applicationId))
if (application) Object.assign(application, { status, auditRemark })
return { success: true }
}
}