feat: 完成前端业务闭环与后端联调

This commit is contained in:
2026-08-24 23:37:56 +08:00
parent c59e36f933
commit 9ad572907b
175 changed files with 10711 additions and 1740 deletions
+46 -43
View File
@@ -9,6 +9,7 @@ import {
import {
assertPlainPayload,
normalizeOptionalNormalDisableStatus,
normalizeOptionalCurrencyNumber,
normalizeOptionalOssIdList,
normalizeOptionalSafeInteger,
normalizeOptionalText,
@@ -20,6 +21,7 @@ import {
normalizeContentProtectionCapabilities,
normalizeResourceCapabilities
} from './business-file-contract.js'
import { normalizeBusinessOptionProjection } from './business-dictionary-contract.js'
const freezeOptions = (options) =>
Object.freeze(options.map((option) => Object.freeze(option)))
@@ -49,19 +51,12 @@ export const LIFE_EVENT_DATE_PRECISION_OPTIONS = freezeOptions([
{ value: 'DAY', label: '填到日期' }
])
const GROWTH_RECORD_FALLBACK_LABELS = Object.freeze({
birth: '出生',
preschool: '学龄前',
school: '求学',
'first-step': '第一次成长',
marriage: '婚姻',
career: '事业',
other: '其他'
export const MEMO_TYPE = Object.freeze({
GENERAL: 'general',
BENEFACTOR: 'benefactor'
})
const MERIT_TYPE_LABELS = Object.freeze(
Object.fromEntries(MERIT_TYPE_OPTIONS.map(({ value, label }) => [value, label]))
)
const memoTypes = new Set(Object.values(MEMO_TYPE))
export const normalizeAppGrowthRecord = (value, expectedGenealogyId, expectedRecordId = '') => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -72,19 +67,27 @@ export const normalizeAppGrowthRecord = (value, expectedGenealogyId, expectedRec
if (!id || genealogyId !== expectedGenealogyId || (expectedRecordId && id !== expectedRecordId)) {
throw createRequestError('成长记录响应缺少稳定归属字段', 'GROWTH_RECORD_RESPONSE_INVALID')
}
const type = normalizeResponseText(value.recordType, 'recordType')
const recordType = normalizeBusinessOptionProjection(
value.recordType,
value.recordTypeLabel,
value.recordTypeOptionState,
'成长记录类型',
'GROWTH_RECORD_RESPONSE_INVALID'
)
return {
id,
genealogyId,
lineagePersonId: normalizeOptionalNumericId(value.lineagePersonId, '成长记录人物标识', 'GROWTH_RECORD_RESPONSE_INVALID'),
personName: normalizeResponseText(value.lineagePersonName, 'lineagePersonName') || '未关联人物',
type,
typeLabel: GROWTH_RECORD_FALLBACK_LABELS[type] || '',
type: recordType.value,
typeLabel: recordType.label,
typeOptionState: recordType.state,
title: normalizeResponseText(value.recordTitle, 'recordTitle') || '未命名记录',
content: normalizeResponseText(value.recordContent, 'recordContent'),
recordDate: normalizeResponseText(value.recordDate, 'recordDate'),
remindTime: normalizeResponseText(value.remindTime, 'remindTime'),
date: normalizeResponseText(value.recordDate, 'recordDate') || normalizeResponseText(value.remindTime, 'remindTime'),
createTime: normalizeResponseText(value.createTime, 'createTime'),
mediaFiles: normalizeBusinessFileAccessRows(value.mediaFiles, '成长记录媒体', 'GROWTH_RECORD_RESPONSE_INVALID'),
sortOrder: normalizeOptionalNonnegativeInteger(value.sortOrder, '成长记录排序值', 'GROWTH_RECORD_RESPONSE_INVALID'),
status: normalizeNormalDisableResponseStatus(value.status, '成长记录状态', 'GROWTH_RECORD_RESPONSE_INVALID'),
@@ -102,21 +105,6 @@ export const normalizeAppGrowthRecords = (value, expectedGenealogyId) => {
return records
}
export const projectPreviewGrowthRecords = (value, expectedGenealogyId) =>
value.map((item) => ({
id: String(item.recordId),
genealogyId: expectedGenealogyId,
lineagePersonId: item.lineagePersonId ? String(item.lineagePersonId) : null,
personName: String(item.lineagePersonName || '未关联人物'),
type: String(item.recordType || ''),
typeLabel: GROWTH_RECORD_FALLBACK_LABELS[String(item.recordType || '')] || '',
title: String(item.recordTitle || '未命名记录'),
content: String(item.recordContent || ''),
date: String(item.recordDate || item.remindTime || ''),
canEdit: false,
canDelete: false
}))
const lifeEventTypes = new Set(LIFE_EVENT_TYPE_OPTIONS.map(({ value }) => value))
const lifeEventDatePrecisions = new Set(
LIFE_EVENT_DATE_PRECISION_OPTIONS.map(({ value }) => value)
@@ -198,6 +186,7 @@ export const normalizeAppRelativeRecord = (value, expectedGenealogyId, expectedR
event: normalizeResponseText(value.eventName, 'eventName'),
time: normalizeResponseText(value.eventTime, 'eventTime'),
eventTime: normalizeResponseText(value.eventTime, 'eventTime'),
createTime: normalizeResponseText(value.createTime, 'createTime'),
amount: normalizeOptionalCurrencyAmount(value.giftAmount, '亲友礼金金额', 'RELATIVE_RECORD_RESPONSE_INVALID'),
content: normalizeResponseText(value.recordContent, 'recordContent'),
mediaFiles: normalizeBusinessFileAccessRows(value.mediaFiles, '亲友往来媒体', 'RELATIVE_RECORD_RESPONSE_INVALID'),
@@ -225,13 +214,19 @@ export const normalizeAppMemo = (value, expectedGenealogyId, expectedMemoId = ''
if (!id || genealogyId !== expectedGenealogyId || (expectedMemoId && id !== expectedMemoId)) {
throw createRequestError('备忘录响应缺少稳定归属字段', 'MEMO_RESPONSE_INVALID')
}
const memoType = normalizeResponseText(value.memoType, 'memoType')
if (!memoTypes.has(memoType)) {
throw createRequestError('备忘录响应类型无效', 'MEMO_RESPONSE_INVALID')
}
return {
id,
genealogyId,
memoType,
title: normalizeResponseText(value.memoTitle, 'memoTitle') || '未命名备忘',
remindTime: normalizeResponseText(value.remindTime, 'remindTime'),
content: normalizeResponseText(value.memoContent, 'memoContent'),
completed: normalizeResponseText(value.completed, 'completed'),
createTime: normalizeResponseText(value.createTime, 'createTime'),
mediaFiles: normalizeBusinessFileAccessRows(value.mediaFiles, '备忘媒体', 'MEMO_RESPONSE_INVALID'),
sortOrder: normalizeOptionalNonnegativeInteger(value.sortOrder, '备忘排序值', 'MEMO_RESPONSE_INVALID'),
status: normalizeNormalDisableResponseStatus(value.status, '备忘状态', 'MEMO_RESPONSE_INVALID'),
@@ -257,18 +252,26 @@ export const normalizeAppMeritRecord = (value, expectedGenealogyId, expectedMeri
if (!id || genealogyId !== expectedGenealogyId || (expectedMeritId && id !== expectedMeritId)) {
throw createRequestError('功德记录响应缺少稳定归属字段', 'MERIT_RECORD_RESPONSE_INVALID')
}
const type = normalizeResponseText(value.meritType, 'meritType')
const typeLabel = type ? (MERIT_TYPE_LABELS[type] || type) : ''
const meritType = normalizeBusinessOptionProjection(
value.meritType,
value.meritTypeLabel,
value.meritTypeOptionState,
'功德类型',
'MERIT_RECORD_RESPONSE_INVALID'
)
return {
id,
genealogyId,
donor: normalizeResponseText(value.donorName, 'donorName') || '未署名',
title: normalizeResponseText(value.meritTitle, 'meritTitle') || '未命名功德',
type,
typeLabel,
type: meritType.value,
typeLabel: meritType.label,
typeOptionState: meritType.state,
amount: normalizeOptionalCurrencyAmount(value.amount, '功德金额', 'MERIT_RECORD_RESPONSE_INVALID', { required: true }),
time: normalizeResponseText(value.meritTime, 'meritTime'),
createTime: normalizeResponseText(value.createTime, 'createTime'),
content: normalizeResponseText(value.meritContent, 'meritContent'),
mediaFiles: normalizeBusinessFileAccessRows(value.mediaFiles, '功德记录图片', 'MERIT_RECORD_RESPONSE_INVALID'),
sortOrder: normalizeOptionalNonnegativeInteger(value.sortOrder, '功德记录排序值', 'MERIT_RECORD_RESPONSE_INVALID'),
status: normalizeNormalDisableResponseStatus(value.status, '功德记录状态', 'MERIT_RECORD_RESPONSE_INVALID'),
...normalizeResourceCapabilities(value, '功德记录', 'MERIT_RECORD_RESPONSE_INVALID')
@@ -297,10 +300,7 @@ export const normalizeRelativeRecordCreatePayload = (payload) => {
const status = normalizeOptionalNormalDisableStatus(payload.status, '亲友往来状态')
if (status) normalizedPayload.status = status
if (payload.giftAmount !== undefined && payload.giftAmount !== '' && payload.giftAmount !== null) {
const giftAmount = typeof payload.giftAmount === 'number' ? payload.giftAmount : Number(payload.giftAmount)
if (!Number.isFinite(giftAmount)) {
throw new TypeError('亲友往来礼金金额必须是有限数字')
}
const giftAmount = normalizeOptionalCurrencyNumber(payload.giftAmount, '亲友往来礼金金额')
normalizedPayload.giftAmount = giftAmount
}
const mediaOssIds = normalizeOptionalOssIdList(payload.mediaOssIds)
@@ -326,7 +326,7 @@ export const normalizeGrowthRecordCreatePayload = (payload) => {
normalizedPayload.lineagePersonId = normalizeResourcePathId(payload.lineagePersonId, '关联人物标识')
}
const mediaOssIds = normalizeOptionalOssIdList(payload.mediaOssIds)
if (mediaOssIds) normalizedPayload.mediaOssIds = mediaOssIds
if (mediaOssIds || payload.mediaOssIds === '') normalizedPayload.mediaOssIds = mediaOssIds || ''
const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder')
if (sortOrder !== undefined) normalizedPayload.sortOrder = sortOrder
return normalizedPayload
@@ -357,11 +357,13 @@ export const normalizeLifeEventPayload = (payload) => {
}
export const normalizeMemoCreatePayload = (payload) => {
const allowedFields = new Set(['memoTitle', 'memoContent', 'remindTime', 'completed', 'mediaOssIds', 'sortOrder', 'status'])
const allowedFields = new Set(['memoType', 'memoTitle', 'memoContent', 'remindTime', 'completed', 'mediaOssIds', 'sortOrder', 'status'])
assertPlainPayload(payload, allowedFields, '备忘请求')
const memoTitle = typeof payload.memoTitle === 'string' ? payload.memoTitle.trim() : ''
if (!memoTitle) throw new TypeError('备忘标题不能为空')
const normalizedPayload = { memoTitle }
const memoType = normalizeOptionalText(payload.memoType, 'memoType') || MEMO_TYPE.GENERAL
if (!memoTypes.has(memoType)) throw new TypeError('备忘类型无效')
const normalizedPayload = { memoType, memoTitle }
for (const field of ['memoContent', 'remindTime', 'completed']) {
const normalizedText = normalizeOptionalText(payload[field], field)
if (normalizedText) normalizedPayload[field] = normalizedText
@@ -376,7 +378,7 @@ export const normalizeMemoCreatePayload = (payload) => {
}
export const normalizeMeritRecordCreatePayload = (payload) => {
const allowedFields = new Set(['donorName', 'meritTitle', 'meritType', 'meritContent', 'amount', 'meritTime', 'sortOrder', 'status'])
const allowedFields = new Set(['donorName', 'meritTitle', 'meritType', 'meritContent', 'amount', 'meritTime', 'mediaOssIds', 'sortOrder', 'status'])
assertPlainPayload(payload, allowedFields, '功德记录请求')
const donorName = typeof payload.donorName === 'string' ? payload.donorName.trim() : ''
const meritTitle = typeof payload.meritTitle === 'string' ? payload.meritTitle.trim() : ''
@@ -389,10 +391,11 @@ export const normalizeMeritRecordCreatePayload = (payload) => {
const status = normalizeOptionalNormalDisableStatus(payload.status, '功德记录状态')
if (status) normalizedPayload.status = status
if (payload.amount !== undefined && payload.amount !== '' && payload.amount !== null) {
const amount = typeof payload.amount === 'number' ? payload.amount : Number(payload.amount)
if (!Number.isFinite(amount)) throw new TypeError('功德金额必须是有限数字')
const amount = normalizeOptionalCurrencyNumber(payload.amount, '功德金额')
normalizedPayload.amount = amount
}
const mediaOssIds = normalizeOptionalOssIdList(payload.mediaOssIds)
if (mediaOssIds || payload.mediaOssIds === '') normalizedPayload.mediaOssIds = mediaOssIds || ''
const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder')
if (sortOrder !== undefined) normalizedPayload.sortOrder = sortOrder
return normalizedPayload