Files
jiapuapp/utils/generation-poem.js
T
2026-07-23 08:24:07 +08:00

217 lines
7.8 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.
// 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,
}))
}