1452 lines
61 KiB
JavaScript
1452 lines
61 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_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 }
|
||
}
|
||
}
|