execl问题修复v2
This commit is contained in:
1 parent
07b23fce9e
commit
a4d55a5c55
3 files changed
+291
-7
No files matched your search
@@ -13,7 +13,7 @@ const ABSOLUTE_MAX_DATA_ROWS = 5_000;
|
||||
const MAX_PASSENGER_SEQUENCE = 5_000;
|
||||
const MAX_HEADER_ROW = 100;
|
||||
|
||||
export const PASSENGER_ROSTER_WORKBOOK_VERSION = 'ltjt-passenger-roster-workbook-v1.4.0';
|
||||
export const PASSENGER_ROSTER_WORKBOOK_VERSION = 'ltjt-passenger-roster-workbook-v1.5.0';
|
||||
|
||||
const REQUIRED_SOURCE_FIELDS = [
|
||||
'序号', '姓名', '英文姓名', '性别', '出生日期', '出生地', '护照号码',
|
||||
@@ -70,6 +70,7 @@ export type PassengerRosterWorkbookErrorCode =
|
||||
| 'roster_workbook_formula_not_allowed'
|
||||
| 'roster_workbook_formula_result_missing'
|
||||
| 'roster_workbook_non_contiguous_rows'
|
||||
| 'roster_workbook_incomplete_row'
|
||||
| 'roster_workbook_row_limit'
|
||||
| 'roster_workbook_no_data'
|
||||
| 'roster_workbook_invalid_sequence'
|
||||
@@ -96,6 +97,7 @@ const ERROR_MESSAGES: Record<PassengerRosterWorkbookErrorCode, string> = {
|
||||
roster_workbook_formula_not_allowed: '名单必需字段包含不允许的公式',
|
||||
roster_workbook_formula_result_missing: '名单必需字段公式没有可验证的缓存结果',
|
||||
roster_workbook_non_contiguous_rows: '名单数据行不连续',
|
||||
roster_workbook_incomplete_row: '名单中间存在未填写旅客资料的模板行',
|
||||
roster_workbook_row_limit: '名单数据行数超过技术限制',
|
||||
roster_workbook_no_data: '名单工作簿没有数据行',
|
||||
roster_workbook_invalid_sequence: '名单序号必须是正整数',
|
||||
@@ -390,10 +392,63 @@ function rowHasData(row: ExcelJS.Row, sourceColumns: readonly number[]): boolean
|
||||
return false;
|
||||
}
|
||||
|
||||
// These two legacy template formulas derive gender/birth date from a deleted
|
||||
// identity-card column. Recognize the exact broken templates, not arbitrary
|
||||
// formulas or their cached results, when deciding whether a tail row is unused.
|
||||
const BROKEN_TEMPLATE_FORMULAS: Partial<Record<RequiredSourceField, string>> = {
|
||||
性别: 'IF(OR(LEN(#REF!)=15,LEN(#REF!)=18),IF(MOD(MID(#REF!,15,3)*1,2),"男","女"),NA())',
|
||||
出生日期: 'DATE(MID(#REF!,7,VLOOKUP(LEN(#REF!),{15,2;18,4},2,0)),MID(#REF!,VLOOKUP(LEN(#REF!),{15,9;18,11},2,0),2),MID(#REF!,VLOOKUP(LEN(#REF!),{15,11;18,13},2,0),2))'
|
||||
};
|
||||
|
||||
function unusedTemplateRow(row: ExcelJS.Row, headerRow: number, sourceColumns: SourceColumnMap): boolean {
|
||||
const sequenceCell = row.getCell(sourceColumns['序号']);
|
||||
if (sequenceCell.type === ExcelJS.ValueType.Formula) return false;
|
||||
const sequence = normalizedText(scalarValue(sequenceCell));
|
||||
if (sequence && (!/^[1-9]\d*$/.test(sequence) || Number(sequence) > MAX_PASSENGER_SEQUENCE)) return false;
|
||||
for (const field of REQUIRED_SOURCE_FIELDS) {
|
||||
if (field === '序号') continue;
|
||||
const cell = row.getCell(sourceColumns[field]);
|
||||
if (cell.type === ExcelJS.ValueType.Formula) {
|
||||
const template = BROKEN_TEMPLATE_FORMULAS[field];
|
||||
if (!formulaAllowed(cell, headerRow, sourceColumns)
|
||||
&& (!template || normalizedFormula(cell.formula) !== normalizedFormula(template))) return false;
|
||||
} else if (normalizedText(scalarValue(cell)) !== '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function mergedTemplateNote(row: ExcelJS.Row, headerRow: number, sourceColumns: SourceColumnMap): boolean {
|
||||
const columns = Object.values(sourceColumns);
|
||||
const master = row.getCell(Math.min(...columns)).master;
|
||||
if (!master.isMerged || cellRow(master) <= headerRow
|
||||
|| !columns.every(column => row.getCell(column).master.address === master.address)) return false;
|
||||
// A footer must span every imported column and be explicitly labelled. Any
|
||||
// separate populated cell keeps the row in the roster for normal validation.
|
||||
let onlyNote = true;
|
||||
row.eachCell({ includeEmpty: false }, cell => {
|
||||
if (cell.master.address !== master.address && cellHasData(cell)) onlyNote = false;
|
||||
});
|
||||
return onlyNote && /^(?:备注|说明|注意事项|温馨提示)[::]/.test(headerText(master));
|
||||
}
|
||||
|
||||
function lastRosterRow(sheet: ExcelJS.Worksheet, headerRow: number, sourceColumns: SourceColumnMap): number {
|
||||
let lastRow = sheet.rowCount;
|
||||
while (lastRow > headerRow) {
|
||||
const row = sheet.getRow(lastRow);
|
||||
if (!mergedTemplateNote(row, headerRow, sourceColumns)
|
||||
&& !unusedTemplateRow(row, headerRow, sourceColumns)) break;
|
||||
lastRow--;
|
||||
}
|
||||
return lastRow;
|
||||
}
|
||||
|
||||
function validateFormulasAndHiddenData(
|
||||
sheet: ExcelJS.Worksheet,
|
||||
headerRow: number,
|
||||
sourceColumns: SourceColumnMap
|
||||
sourceColumns: SourceColumnMap,
|
||||
lastDataRow: number
|
||||
): void {
|
||||
const selectedColumns = new Set(Object.values(sourceColumns));
|
||||
sheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
|
||||
@@ -405,7 +460,7 @@ function validateFormulasAndHiddenData(
|
||||
if (containsExternalFormulaReference(formula)) {
|
||||
fail('roster_workbook_unsafe_content', { row: rowNumber, column: columnNumber });
|
||||
}
|
||||
if (selectedColumns.has(columnNumber) && !formulaAllowed(cell, headerRow, sourceColumns)) {
|
||||
if (rowNumber <= lastDataRow && selectedColumns.has(columnNumber) && !formulaAllowed(cell, headerRow, sourceColumns)) {
|
||||
fail('roster_workbook_formula_not_allowed', { row: rowNumber, column: columnNumber });
|
||||
}
|
||||
}
|
||||
@@ -547,12 +602,13 @@ function dataRows(
|
||||
sheet: ExcelJS.Worksheet,
|
||||
headerRow: number,
|
||||
sourceColumns: SourceColumnMap,
|
||||
maxDataRows: number
|
||||
maxDataRows: number,
|
||||
lastDataRow: number
|
||||
): ExcelJS.Row[] {
|
||||
const rows: ExcelJS.Row[] = [];
|
||||
let gapAfterData = false;
|
||||
const selectedColumns = Object.values(sourceColumns);
|
||||
for (let rowNumber = headerRow + 1; rowNumber <= sheet.rowCount; rowNumber += 1) {
|
||||
for (let rowNumber = headerRow + 1; rowNumber <= lastDataRow; rowNumber += 1) {
|
||||
const row = sheet.getRow(rowNumber);
|
||||
const hasData = rowHasData(row, selectedColumns);
|
||||
if (!hasData) {
|
||||
@@ -560,6 +616,9 @@ function dataRows(
|
||||
continue;
|
||||
}
|
||||
if (gapAfterData) fail('roster_workbook_non_contiguous_rows', { row: rowNumber });
|
||||
if (unusedTemplateRow(row, headerRow, sourceColumns)) {
|
||||
fail('roster_workbook_incomplete_row', { row: rowNumber, column: sourceColumns['姓名'] });
|
||||
}
|
||||
rows.push(row);
|
||||
if (rows.length > maxDataRows) fail('roster_workbook_row_limit');
|
||||
}
|
||||
@@ -654,8 +713,11 @@ export async function normalizePassengerRosterWorkbook(
|
||||
if (sheet.state !== 'visible') fail('roster_workbook_hidden_data');
|
||||
const header = findHeaderLayout(sheet);
|
||||
const headerRow = header.row;
|
||||
validateFormulasAndHiddenData(sheet, headerRow, header.columns);
|
||||
const rows = dataRows(sheet, headerRow, header.columns, maxDataRows);
|
||||
const lastDataRow = lastRosterRow(sheet, headerRow, header.columns);
|
||||
// Content safety and hidden-data checks still cover the entire workbook,
|
||||
// including discarded placeholders and footnotes.
|
||||
validateFormulasAndHiddenData(sheet, headerRow, header.columns, lastDataRow);
|
||||
const rows = dataRows(sheet, headerRow, header.columns, maxDataRows, lastDataRow);
|
||||
const date1904 = workbook.properties.date1904 === true;
|
||||
const seenSequences = new Set<number>();
|
||||
const canonicalRows = rows.map((row) => canonicalRow(row, date1904, seenSequences, header.columns));
|
||||
|
||||
Reference in new issue
Block a user