Files
jiapuapp/utils/api.js
T
2026-07-23 17:21:33 +08:00

807 lines
31 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,
treeMembers,
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 toTreeNode = (person, index = 0) => {
const generation = Number(person.generationNo || person.generation || 1)
return {
...person,
relatives: Array.isArray(person.relatives)
? person.relatives.map((relative) => ({ ...relative }))
: [],
id: person.id || person.personId,
name: person.personName || person.name || '未命名族人',
relation: person.relation || (generation === 1 ? '始祖' : '族人'),
generation,
years: person.years || [person.birthDate, person.deathDate].filter(Boolean).join('—') || '生卒待补',
branch: person.branch || '主支',
x: person.x ?? (20 + (index % 4) * 20),
y: person.y ?? (generation * 31 - 24)
}
}
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 birthDate = normalizeLineagePersonDate(value.birthDate, '出生日期')
const deathDate = normalizeLineagePersonDate(value.deathDate, '逝世日期')
const personStatus = normalizeLineagePersonText(value.personStatus, '人物状态').toUpperCase()
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,
generation: value.generation,
generationName,
relation: value.generation === 1 ? '始祖' : '家谱成员',
branch: generationName
? (generationName.endsWith('字辈') ? generationName : `${generationName}字辈`)
: '字辈待补',
sex: normalizeLineagePersonText(value.sex, '性别'),
birthDate,
deathDate,
years: birthDate || deathDate ? `${birthDate}${deathDate}` : '生卒待补',
birthplace: normalizeLineagePersonText(value.birthPlace, '出生地'),
biography: normalizeLineagePersonText(value.biography, '生平'),
status: ['DECEASED', 'DEAD'].includes(personStatus) ? 'deceased' : 'normal',
relatives
}
}
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 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()) {
return genealogies.map((item) => ({
...item,
accessRole: item.membership === 'created' ? 'owner' : 'member'
}))
}
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()) return this.getGenealogy(normalizedId)
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 normalizedId = normalizeGenealogyPathId(genealogyId)
const tree = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedId}/lineage/tree`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeLineageTree(tree)
}
return treeMembers
.filter((item) => String(item.genealogyId) === String(genealogyId))
.map(toTreeNode)
},
async getPerson(genealogyId, personId, requestOptions = {}) {
if (hasRemoteConfig()) {
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)
}
const person = treeMembers.find(
(item) => String(item.id) === String(personId) &&
String(item.genealogyId) === String(genealogyId)
)
return person ? toTreeNode(person) : null
},
async createPerson(genealogyId, payload) {
if (hasRemoteConfig()) {
const person = await request({ url: `/genealogy/app/genealogies/${genealogyId}/lineage/persons`, method: 'POST', data: payload })
return toTreeNode(person)
}
const error = new Error('人物创建接口在本地预览模式不可用,当前内容不会保存')
error.code = 'WRITE_UNAVAILABLE'
throw error
},
async getFeeds(genealogyId) {
return hasRemoteConfig()
? request({ url: `/genealogy/app/genealogies/${genealogyId}/feeds` })
: listFamilyFeedFixtures(genealogyId)
},
async createFeed(genealogyId, payload) {
if (hasRemoteConfig()) return request({ url: `/genealogy/app/genealogies/${genealogyId}/feeds`, method: 'POST', data: payload })
const error = new Error('动态发布接口尚未接入,当前内容不会保存')
error.code = 'WRITE_UNAVAILABLE'
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 }
}
}