完成50%

This commit is contained in:
2026-07-23 08:23:59 +08:00
parent 9b0ad62df4
commit f1edc6b533
218 changed files with 24318 additions and 5514 deletions
+216
View File
@@ -0,0 +1,216 @@
// 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,
}))
}