Files
jiapuapp/services/api/lineage-tree-contract.js
2026-09-13 17:45:52 +08:00

129 lines
5.5 KiB
JavaScript

import { createRequestError } from './request-client.js'
import { normalizeBusinessFileAccess } from './business-file-contract.js'
const lineageTreeError = (message) =>
createRequestError(message, 'LINEAGE_TREE_RESPONSE_INVALID')
const normalizeLineagePersonId = (value) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw lineageTreeError('世系树包含无效人物标识')
}
const normalizeLineageText = (value, label, { required = false } = {}) => {
if (value === undefined || value === null) {
if (required) throw lineageTreeError(`世系树缺少${label}`)
return ''
}
if (typeof value !== 'string') throw lineageTreeError(`世系树${label}无效`)
const normalized = value.trim()
if (required && !normalized) throw lineageTreeError(`世系树缺少${label}`)
return normalized
}
const lineageDatePart = (value, label) => {
const normalized = normalizeLineageText(value, label)
if (!normalized) return ''
if (!/^\d{4}-\d{2}-\d{2}/.test(normalized)) {
throw lineageTreeError(`世系树${label}无效`)
}
if (normalized.length === 10) return normalized
const instant = new Date(normalized)
if (Number.isNaN(instant.getTime())) throw lineageTreeError(`世系树${label}无效`)
const chinaTime = new Date(instant.getTime() + 8 * 60 * 60 * 1000)
const year = chinaTime.getUTCFullYear()
const month = String(chinaTime.getUTCMonth() + 1).padStart(2, '0')
const day = String(chinaTime.getUTCDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
const compactLineageYears = (birthDate, deathDate) => {
const birthYear = birthDate ? birthDate.slice(0, 4) : ''
const deathYear = deathDate ? deathDate.slice(0, 4) : ''
return birthYear || deathYear ? `${birthYear}${deathYear}` : '生卒待补'
}
export const normalizeLineageTree = (value) => {
if (!Array.isArray(value)) throw lineageTreeError('世系树响应不是列表')
const seen = new Set()
const normalized = []
const normalizedById = new Map()
const appendNode = (node, parentId, relationOverride = '', spouseOf = '') => {
if (!node || typeof node !== 'object' || Array.isArray(node)) {
throw lineageTreeError('世系树包含无效节点')
}
const id = normalizeLineagePersonId(node.personId)
if (seen.has(id)) throw lineageTreeError('世系树包含重复人物标识')
seen.add(id)
if (seen.size > 5000) throw lineageTreeError('世系树节点数量超出客户端上限')
if (!Number.isSafeInteger(node.generation) || node.generation < 1) {
throw lineageTreeError('世系树人物世代无效')
}
const generationName = normalizeLineageText(node.generationName, '字辈')
const rankName = normalizeLineageText(node.rankName, '排行名称')
const relationName = normalizeLineageText(node.relationName, '人物关系')
const birthDate = lineageDatePart(node.birthDate, '出生日期')
const deathDate = lineageDatePart(node.deathDate, '逝世日期')
const normalizedNode = {
id,
parentId,
...(spouseOf ? { spouseOf } : {}),
name: normalizeLineageText(node.name, '人物姓名', { required: true }),
relation:
relationOverride ||
rankName ||
(parentId
? (relationName && relationName !== '配偶' ? relationName : '排行待补')
: relationName || '始祖'),
rankName,
generation: node.generation,
branch: generationName
? (generationName.endsWith('字辈') ? generationName : `${generationName}字辈`)
: '字辈待补',
years: birthDate || deathDate ? `${birthDate}${deathDate}` : '生卒待补',
treeYears: compactLineageYears(birthDate, deathDate),
avatarFile: normalizeBusinessFileAccess(
node.avatarFile,
'世系人物头像',
'LINEAGE_TREE_RESPONSE_INVALID'
),
sex: normalizeLineageText(node.sex, '性别'),
personStatus: normalizeLineageText(node.personStatus, '人物状态')
}
normalized.push(normalizedNode)
normalizedById.set(id, normalizedNode)
return id
}
const walk = (node, parentId = null, depth = 0) => {
if (depth > 64) throw lineageTreeError('世系树深度超出客户端上限')
if (!node || typeof node !== 'object' || Array.isArray(node)) {
throw lineageTreeError('世系树包含无效节点')
}
const spouses = node.spouses ?? []
const children = node.children ?? []
if (!Array.isArray(spouses) || !Array.isArray(children)) {
throw lineageTreeError('世系树亲属集合无效')
}
const id = normalizeLineagePersonId(node.personId)
const knownSpouseId = spouses
.map((spouse) => normalizeLineagePersonId(spouse?.personId))
.find((spouseId) => normalizedById.has(spouseId)) || ''
if (seen.has(id)) {
if (knownSpouseId) return id
throw lineageTreeError('世系树包含重复人物标识')
}
const knownSpouse = knownSpouseId ? normalizedById.get(knownSpouseId) : null
const nodeParentId = knownSpouse ? knownSpouse.parentId : parentId
const appendedId = appendNode(node, nodeParentId, knownSpouse ? '配偶' : '', knownSpouseId)
spouses.forEach((spouse) => {
const spouseId = normalizeLineagePersonId(spouse?.personId)
if (seen.has(spouseId)) return
appendNode(spouse, nodeParentId, '配偶', appendedId)
})
children.forEach((child) => walk(child, knownSpouseId || appendedId, depth + 1))
}
value.forEach((root) => walk(root))
return normalized
}