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

360 lines
15 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 { normalizeOptionalSafeInteger } from './request-normalizers.js'
import { createRequestError } from './request-client.js'
import { normalizeOptionalNumericId } from './response-normalizers.js'
import { LINEAGE_PERSON_OPTIONS } from './lineage-person-options.js'
import { normalizeBusinessOptionProjection } from './business-dictionary-contract.js'
const optionLabels = (options) => Object.freeze(
Object.fromEntries(options.map(({ value, label }) => [value, label]))
)
const lineagePersonError = (message) =>
createRequestError(message, 'LINEAGE_PERSON_RESPONSE_INVALID')
const lineageRankError = (message) =>
createRequestError(message, 'LINEAGE_RANK_RESPONSE_INVALID')
const normalizeLineageRankText = (value, label) => {
if (typeof value !== 'string' || !value.trim()) {
throw lineageRankError(`世系排行选项缺少${label}`)
}
return value.trim()
}
export const normalizeLineagePersonIdentity = (value, label) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw lineagePersonError(`成员详情${label}无效`)
}
const normalizeOptionalLineagePersonIdentity = (value, label) => {
if (value === undefined || value === null || value === '') return ''
return normalizeLineagePersonIdentity(value, label)
}
export 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 normalizeLineageSpouseNames = (value) =>
normalizeLineagePersonText(value, '配偶姓名')
.replace(/\s*[(]P\d{15,}[)]/g, '')
.trim()
const LINEAGE_PERSON_SEX_LABELS = optionLabels(LINEAGE_PERSON_OPTIONS.sex)
const LINEAGE_PERSON_STATUS_LABELS = optionLabels(
LINEAGE_PERSON_OPTIONS.personStatus
)
const LINEAGE_BOOLEAN_LABELS = optionLabels(LINEAGE_PERSON_OPTIONS.lunar)
const lineageBindingModes = new Set(
LINEAGE_PERSON_OPTIONS.bindingMode.map(({ value }) => value)
)
const normalizeLineagePersonDictionaryLabel = (value, label, labels) => {
const normalized = normalizeLineagePersonText(value, label)
if (!normalized) return ''
const display = labels[normalized]
if (!display) throw lineagePersonError(`成员详情${label}字典值无效`)
return display
}
export const normalizeLineagePersonDate = (value, label) => {
const normalized = normalizeLineagePersonText(value, label)
if (!normalized) return ''
if (!/^\d{4}-\d{2}-\d{2}(?:[T\s].*)?$/.test(normalized)) {
throw lineagePersonError(`成员详情${label}无效`)
}
const datePart = normalized.slice(0, 10)
const parsed = new Date(`${datePart}T00:00:00Z`)
if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== datePart) {
throw lineagePersonError(`成员详情${label}无效`)
}
return datePart
}
export const normalizeLineagePersonDetail = (value, expectedGenealogyId, expectedPersonId) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw lineagePersonError('成员详情响应不是对象')
}
const genealogyId = normalizeLineagePersonIdentity(value.genealogyId, '家谱标识')
const id = normalizeLineagePersonIdentity(value.personId, '人物标识')
if (genealogyId !== expectedGenealogyId || id !== expectedPersonId) {
throw lineagePersonError('成员详情响应标识与请求不匹配')
}
if (!Number.isSafeInteger(value.generation) || value.generation < 1) {
throw lineagePersonError('成员详情世代无效')
}
const name = normalizeLineagePersonText(value.name, '姓名', { required: true })
const appUserId = normalizeOptionalLineagePersonIdentity(value.appUserId, '绑定用户标识')
const bindingMode = value.bindingMode === undefined || value.bindingMode === null || value.bindingMode === ''
? (appUserId ? 'SPECIFIED' : 'NONE')
: normalizeLineagePersonText(value.bindingMode, '身份认领方式')
if (!lineageBindingModes.has(bindingMode)) {
throw lineagePersonError('成员详情身份认领方式无效')
}
const personNo = normalizeLineagePersonText(value.personNo, '人物编号')
const generationName = normalizeLineagePersonText(value.generationName, '字辈')
const courtesyName = normalizeLineagePersonText(value.courtesyName, '表字')
const aliasName = normalizeLineagePersonText(value.aliasName, '别号或曾用名')
const avatarOssId = normalizeOptionalLineagePersonIdentity(value.avatarOssId, '头像文件标识')
const birthDate = normalizeLineagePersonDate(value.birthDate, '出生日期')
const deathDate = normalizeLineagePersonDate(value.deathDate, '逝世日期')
const birthLunar = normalizeLineagePersonText(value.birthLunar, '出生农历')
const deathLunar = normalizeLineagePersonText(value.deathLunar, '逝世农历')
const birthPlace = normalizeLineagePersonText(value.birthPlace, '出生地')
const zodiacOption = normalizeBusinessOptionProjection(
value.zodiacCode,
value.zodiacLabel,
value.zodiacOptionState,
'成员生肖',
'LINEAGE_PERSON_RESPONSE_INVALID'
)
const currentAddress = normalizeLineagePersonText(value.currentAddress, '现居地')
const mobile = normalizeLineagePersonText(value.mobile, '联系电话')
const email = normalizeLineagePersonText(value.email, '电子邮箱')
const educationOption = normalizeBusinessOptionProjection(
value.educationCode,
value.educationLabel,
value.educationOptionState,
'成员学历分类',
'LINEAGE_PERSON_RESPONSE_INVALID'
)
const occupation = normalizeLineagePersonText(value.occupation, '职业')
const deathAge = value.deathAge === undefined || value.deathAge === null
? null
: normalizeOptionalSafeInteger(value.deathAge, '人物享年')
if (deathAge !== null && (deathAge < 0 || deathAge > 200)) {
throw lineagePersonError('成员详情享年无效')
}
const deathPlace = normalizeLineagePersonText(value.deathPlace, '逝世地')
const deathExpressionOption = normalizeBusinessOptionProjection(
value.deathExpressionCode,
value.deathExpressionLabel,
value.deathExpressionOptionState,
'成员逝世表述',
'LINEAGE_PERSON_RESPONSE_INVALID'
)
const burialDate = normalizeLineagePersonDate(value.burialDate, '安葬日期')
const burialPlace = normalizeLineagePersonText(value.burialPlace, '安葬地')
const spouseNames = normalizeLineageSpouseNames(value.spouseNames)
const personStatus = normalizeLineagePersonText(value.personStatus, '人物状态')
const sex = normalizeLineagePersonText(value.sex, '性别')
const sexLabel = normalizeLineagePersonDictionaryLabel(sex, '性别', LINEAGE_PERSON_SEX_LABELS)
const personStatusLabel = normalizeLineagePersonDictionaryLabel(personStatus, '人物状态', LINEAGE_PERSON_STATUS_LABELS)
const birthLunarLabel = normalizeLineagePersonDictionaryLabel(birthLunar, '出生农历', LINEAGE_BOOLEAN_LABELS)
const deathLunarLabel = normalizeLineagePersonDictionaryLabel(deathLunar, '逝世农历', LINEAGE_BOOLEAN_LABELS)
const biography = normalizeLineagePersonText(value.biography, '生平')
const remark = normalizeLineagePersonText(value.remark, '备注')
const canManageSensitiveMedicalHistory = value.canManageSensitiveMedicalHistory === true
if (value.canManageSensitiveMedicalHistory !== undefined && typeof value.canManageSensitiveMedicalHistory !== 'boolean') {
throw lineagePersonError('成员详情敏感病史权限无效')
}
const relationName = normalizeLineagePersonText(value.relationName, '关系显示名称')
const rankId = normalizeOptionalNumericId(
value.rankId,
'成员排行标识',
'LINEAGE_PERSON_RESPONSE_INVALID'
)
const rankName = normalizeLineagePersonText(value.rankName, '排行名称')
for (const field of ['canDisable', 'canCreateDocument', 'canManageDocuments']) {
if (value[field] !== undefined && typeof value[field] !== 'boolean') {
throw lineagePersonError(`成员详情权限字段 ${field} 无效`)
}
}
const sortOrder = value.sortOrder === undefined || value.sortOrder === null
? null
: normalizeOptionalSafeInteger(value.sortOrder, '人物排序值')
const relatives = []
for (const relation of [
{ id: value.fatherId, name: value.fatherName, label: '父亲' },
{ id: value.motherId, name: value.motherName, label: '母亲' }
]) {
if (relation.id === undefined || relation.id === null) continue
const relativeId = normalizeLineagePersonIdentity(relation.id, `${relation.label}标识`)
if (relativeId === id || relatives.some((relative) => relative.id === relativeId)) {
throw lineagePersonError('成员详情亲属标识冲突')
}
relatives.push({
id: relativeId,
name: normalizeLineagePersonText(relation.name, `${relation.label}姓名`) || relation.label,
relation: relation.label
})
}
return {
id,
genealogyId,
genealogyName: normalizeLineagePersonText(value.genealogyName, '家谱名称') || '当前家谱',
name,
appUserId,
bindingMode,
personNo,
courtesyName,
aliasName,
generation: value.generation,
generationName,
avatarOssId,
relation: value.generation === 1 ? '始祖' : '家谱成员',
branch: generationName
? (generationName.endsWith('字辈') ? generationName : `${generationName}字辈`)
: '字辈待补',
sex,
sexLabel,
birthDate,
birthLunar,
birthLunarLabel,
deathDate,
deathLunar,
deathLunarLabel,
years: birthDate || deathDate ? `${birthDate}${deathDate}` : '生卒待补',
birthplace: birthPlace,
zodiac: zodiacOption.label,
zodiacCode: zodiacOption.value,
zodiacOptionState: zodiacOption.state,
currentAddress,
mobile,
email,
education: educationOption.label,
educationCode: educationOption.value,
educationOptionState: educationOption.state,
occupation,
deathAge,
deathPlace,
deathType: deathExpressionOption.label,
deathExpressionCode: deathExpressionOption.value,
deathExpressionOptionState: deathExpressionOption.state,
burialDate,
burialPlace,
spouseNames,
personStatus,
personStatusLabel,
biography,
remark,
canManageSensitiveMedicalHistory,
relationName,
rankId,
rankName,
sortOrder,
status: personStatus === '1' ? 'deceased' : 'normal',
canDisable: value.canDisable === true,
disabledReason: normalizeLineagePersonText(value.disabledReason, '不可停用原因'),
canCreateDocument: value.canCreateDocument === true,
canManageDocuments: value.canManageDocuments === true,
relatives
}
}
export const normalizeLineageRankOptions = (value) => {
if (!Array.isArray(value)) throw lineageRankError('世系排行选项响应不是列表')
const rankIds = new Set()
return value.map((option) => {
if (!option || typeof option !== 'object' || Array.isArray(option)) {
throw lineageRankError('世系排行选项包含无效记录')
}
const rankId = normalizeOptionalNumericId(
option.rankId,
'世系排行标识',
'LINEAGE_RANK_RESPONSE_INVALID'
)
if (!rankId || rankIds.has(rankId)) {
throw lineageRankError(rankId ? '世系排行选项包含重复标识' : '世系排行选项缺少标识')
}
rankIds.add(rankId)
const rankCode = normalizeLineageRankText(option.rankCode, '排行编码')
const rankName = normalizeLineageRankText(option.rankName, '排行名称')
const rankType = normalizeLineageRankText(option.rankType, '排行类型')
const genderScope = normalizeLineageRankText(option.genderScope, '排行适用性别')
if (!['ANCESTOR', 'GENERATION'].includes(rankType)) {
throw lineageRankError('世系排行选项类型无效')
}
if (!['0', '1', '2'].includes(genderScope)) {
throw lineageRankError('世系排行选项适用性别无效')
}
const rankOrder = normalizeOptionalSafeInteger(option.rankOrder, '排行顺序')
if (rankOrder !== undefined && rankOrder < 0) {
throw lineageRankError('世系排行选项顺序无效')
}
return { rankId, rankCode, rankName, rankType, genderScope, rankOrder }
})
}
export const normalizeLineageSensitiveProfile = (value, expectedPersonId) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw lineagePersonError('成员敏感健康资料响应不是对象')
}
const personId = normalizeLineagePersonIdentity(value.personId, '敏感健康资料人物标识')
if (personId !== expectedPersonId) {
throw lineagePersonError('成员敏感健康资料响应标识与请求不匹配')
}
if (typeof value.present !== 'boolean' || typeof value.canManage !== 'boolean') {
throw lineagePersonError('成员敏感健康资料状态无效')
}
const hereditaryMedicalHistory = normalizeLineagePersonText(
value.hereditaryMedicalHistory,
'遗传病史'
)
if (!value.present && hereditaryMedicalHistory) {
throw lineagePersonError('成员敏感健康资料状态与正文不一致')
}
return {
personId,
hereditaryMedicalHistory,
present: value.present,
canManage: value.canManage,
updatedAt: normalizeLineagePersonText(value.updatedAt, '敏感健康资料更新时间')
}
}
export 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((person) => person.id)).size !== rows.length) {
throw lineagePersonError('成员分页包含重复人物标识')
}
return { rows, total: value.total }
}
export const normalizeLineagePersonOptions = (value, expectedGenealogyId) => {
if (!Array.isArray(value)) throw lineagePersonError('世系人物选项响应不是列表')
const options = value.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw lineagePersonError('世系人物选项包含无效条目')
}
const id = normalizeLineagePersonIdentity(item.personId, '人物标识')
const genealogyId = normalizeLineagePersonIdentity(item.genealogyId, '家谱标识')
if (genealogyId !== expectedGenealogyId) {
throw lineagePersonError('世系人物选项归属与请求不匹配')
}
return {
id,
name: normalizeLineagePersonText(item.name, '姓名', { required: true }),
personNo: normalizeLineagePersonText(item.personNo, '人物编号'),
generation: Number.isSafeInteger(item.generation) && item.generation > 0
? item.generation
: null,
generationName: normalizeLineagePersonText(item.generationName, '字辈')
}
})
if (new Set(options.map((person) => person.id)).size !== options.length) {
throw lineagePersonError('世系人物选项包含重复人物标识')
}
return options
}