// generationNo 来自 int64。当前页面必须做加法与排序,因此只接受 JavaScript // 可以精确表达的正安全整数;超出范围时失败关闭,不能把两个世代舍入成同一值。 export const MAX_GENERATION_NO = Number.MAX_SAFE_INTEGER // 三个限制逐一对应 GenerationPoemBatchBody:输入总长 26000、单代文字 // 最多 50 字符、单批最多 500 个世代,不能再合并成一个含义模糊的“长度”。 export const MAX_GENERATION_COUNT = 500 export const MAX_GENERATION_TEXT_LENGTH = 50 export const MAX_GENERATION_POEM_INPUT_LENGTH = 26000 export const GENERATION_POEM_STATUS = Object.freeze({ ACTIVE: '0', DISABLED: '1', }) const normalizePositiveInteger = (value) => { if (typeof value === 'number') { return Number.isSafeInteger(value) && value > 0 ? value : null } if (typeof value !== 'string' || !/^[1-9]\d*$/.test(value)) return null const number = Number(value) return Number.isSafeInteger(number) ? number : null } // 路由查询始终先经过这里:缺省值可以回落,显式传入的非法值必须失败关闭, // 不能用 Number(...) || fallback 把 0、小数或科学计数法悄悄改成另一代。 export const normalizeGenerationNumber = (value, fallback) => { const candidate = value === undefined || value === null || value === '' ? fallback : value const number = normalizePositiveInteger(candidate) return number !== null && number <= MAX_GENERATION_NO ? number : null } const separatorPattern = /[\s,,;;、/|]/u const separatorRunPattern = /[\s,,;;、/|]+/u const forbiddenControlPattern = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/u const codePointLength = (value) => Array.from(value).length const normalizeGenerationText = (value) => { const text = String(value ?? '').trim() if ( !text || codePointLength(text) > MAX_GENERATION_TEXT_LENGTH || forbiddenControlPattern.test(text) ) { return null } return text } // 按 OpenAPI 的批量输入规则解析:没有分隔符时每个 Unicode code point 对应 // 一代;存在空格、逗号、分号、顿号、斜杠或竖线时,每个分段可包含多字。 export const validateGenerationPoemText = (value) => { const input = String(value ?? '') if (!input.trim()) { return Object.freeze({ valid: false, generations: [], message: '请录入字辈内容' }) } if (codePointLength(input) > MAX_GENERATION_POEM_INPUT_LENGTH) { return Object.freeze({ valid: false, generations: [], message: `字辈输入最多 ${MAX_GENERATION_POEM_INPUT_LENGTH} 个字符`, }) } if (forbiddenControlPattern.test(input)) { return Object.freeze({ valid: false, generations: [], message: '字辈内容包含不支持的控制字符', }) } const generations = separatorPattern.test(input) ? input.split(separatorRunPattern).map((item) => item.trim()).filter(Boolean) : Array.from(input) if (!generations.length) { return Object.freeze({ valid: false, generations, message: '请录入字辈内容' }) } if (generations.length > MAX_GENERATION_COUNT) { return Object.freeze({ valid: false, generations, message: `一次最多录入 ${MAX_GENERATION_COUNT} 个世代`, }) } if (generations.some((item) => normalizeGenerationText(item) === null)) { return Object.freeze({ valid: false, generations, message: `每代字辈文字不能为空且最多 ${MAX_GENERATION_TEXT_LENGTH} 个字符`, }) } return Object.freeze({ valid: true, generations, message: '' }) } const normalizeExistingRow = (row) => { const generationNo = normalizeGenerationNumber(row?.generationNo) const generationText = normalizeGenerationText(row?.generationText) if ( generationNo === null || generationText === null || !Object.values(GENERATION_POEM_STATUS).includes(row?.status) ) { throw new TypeError('已有字辈记录不符合 GenerationPoemView 合同') } return { ...row, generationNo, generationText, status: row.status, current: false, } } // 只检查已建立字辈的起点到本批起始代之间,避免把家谱尚未录入的更早世代 // 当成缺口;算法只遍历已有记录,能够安全处理几十代、几百代甚至稀疏数据。 export const findFirstGenerationGap = ( rows, startGeneration, sequenceStartGeneration, ) => { const start = normalizeGenerationNumber(startGeneration) if (start === null) return null const generations = [...new Set( (Array.isArray(rows) ? rows : []) .filter((row) => row?.status !== GENERATION_POEM_STATUS.DISABLED) .map((row) => normalizeGenerationNumber(row?.generationNo)) .filter((generationNo) => generationNo !== null && generationNo < start), )].sort((left, right) => left - right) if (!generations.length) return null const explicitSequenceStart = sequenceStartGeneration === undefined ? null : normalizeGenerationNumber(sequenceStartGeneration) if (sequenceStartGeneration !== undefined && explicitSequenceStart === null) { throw new TypeError('字辈序列起点无效') } let expected = explicitSequenceStart ?? generations[0] for (const generationNo of generations) { if (generationNo > expected) return expected expected = generationNo + 1 } return expected < start ? expected : null } // 批量维护只替换本次覆盖区间。disableMissing=true 时,后续遗漏记录会被 // 标成停用但仍保留,确保历史数据不会因一次编辑被物理删除。 export const mergeGenerationPoemRows = ({ existingRows, generationTexts, startGeneration, currentGeneration, disableMissing, }) => { const start = normalizeGenerationNumber(startGeneration) const current = normalizeGenerationNumber(currentGeneration) if (typeof disableMissing !== 'boolean') { throw new TypeError('disableMissing 必须是布尔值') } if (!Array.isArray(generationTexts) || !generationTexts.length) { throw new TypeError('字辈合并参数无效') } if (generationTexts.length > MAX_GENERATION_COUNT) { throw new RangeError(`一次最多 ${MAX_GENERATION_COUNT} 个世代`) } const nextGenerationTexts = generationTexts.map(normalizeGenerationText) if (nextGenerationTexts.some((item) => item === null)) { throw new TypeError('单代字辈文字无效') } if (start === null || current === null) throw new TypeError('字辈合并参数无效') if (start > MAX_GENERATION_NO - nextGenerationTexts.length + 1) { throw new RangeError('字辈世代超出支持范围') } const end = start + nextGenerationTexts.length - 1 const rowsByGeneration = new Map() for (const item of Array.isArray(existingRows) ? existingRows : []) { const row = normalizeExistingRow(item) if (rowsByGeneration.has(row.generationNo)) { throw new TypeError('已有字辈包含重复的世代记录') } rowsByGeneration.set(row.generationNo, row) } for (let index = 0; index < nextGenerationTexts.length; index += 1) { const generationNo = start + index const existing = rowsByGeneration.get(generationNo) rowsByGeneration.set(generationNo, { ...existing, generationNo, generationText: nextGenerationTexts[index], status: GENERATION_POEM_STATUS.ACTIVE, current: false, }) } if (disableMissing) { for (const [generationNo, row] of rowsByGeneration) { if (generationNo > end) { rowsByGeneration.set(generationNo, { ...row, status: GENERATION_POEM_STATUS.DISABLED, current: false, }) } } } return [...rowsByGeneration.values()] .sort((left, right) => left.generationNo - right.generationNo) .map((row) => ({ ...row, current: row.status === GENERATION_POEM_STATUS.ACTIVE && row.generationNo === current, })) }