导入表头修复
This commit is contained in:
1 parent
8a0f02c9be
commit
a3505f3921
28 files changed
+281
-45
No files matched your search
@@ -80,7 +80,7 @@ export function effectiveParserModeForRoute(route: BusinessRouteDefinition, conf
|
||||
}
|
||||
|
||||
const PROGRAM_VERSION = 'ltjt-program-parser-v1.0.12';
|
||||
export const BUSINESS_INPUT_CONTRACT_VERSION = 'business-input-templates-0.5.134';
|
||||
export const BUSINESS_INPUT_CONTRACT_VERSION = 'business-input-templates-0.5.135';
|
||||
|
||||
const lookupFields = ({ customerRequired = true } = {}): ProgramFieldDefinition[] => [
|
||||
{ key: 'order_no', label: '单号', aliases: ['团号', '订单号'], kind: 'text' },
|
||||
|
||||
@@ -13,22 +13,23 @@ 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.6.0';
|
||||
export const PASSENGER_ROSTER_WORKBOOK_VERSION = 'ltjt-passenger-roster-workbook-v1.7.0';
|
||||
|
||||
const REQUIRED_SOURCE_FIELDS = [
|
||||
'序号', '姓名', '英文姓名', '性别', '出生日期', '出生地', '护照号码',
|
||||
'签发地', '签发日期', '有效期', '电话', '备注'
|
||||
'签发地', '签发日期', '有效期', '备注'
|
||||
] as const;
|
||||
|
||||
type RequiredSourceField = typeof REQUIRED_SOURCE_FIELDS[number];
|
||||
type SourceColumnMap = Record<RequiredSourceField, number>;
|
||||
type SourceField = RequiredSourceField | '电话';
|
||||
type SourceColumnMap = Record<RequiredSourceField, number> & { 电话?: number };
|
||||
|
||||
/**
|
||||
* Header recognition is intentionally exact and finite. It accepts the
|
||||
* required customer-facing labels, canonical ERP labels, and approved aliases.
|
||||
* Non-import columns are deliberately absent from this map and are ignored.
|
||||
*/
|
||||
const HEADER_FIELD_ENTRIES: ReadonlyArray<readonly [string, RequiredSourceField]> = [
|
||||
const HEADER_FIELD_ENTRIES: ReadonlyArray<readonly [string, SourceField]> = [
|
||||
['序号', '序号'],
|
||||
['姓名', '姓名'],
|
||||
['英文姓名', '英文姓名'],
|
||||
@@ -48,7 +49,7 @@ const HEADER_FIELD_ENTRIES: ReadonlyArray<readonly [string, RequiredSourceField]
|
||||
['备注', '备注']
|
||||
];
|
||||
|
||||
const SOURCE_FIELD_BY_HEADER = new Map<string, RequiredSourceField>(HEADER_FIELD_ENTRIES);
|
||||
const SOURCE_FIELD_BY_HEADER = new Map<string, SourceField>(HEADER_FIELD_ENTRIES);
|
||||
|
||||
export const CANONICAL_PASSENGER_HEADERS = [
|
||||
'序号', '姓名', 'NAME', '性别', '出生日期', '出生地', '证件类型', '证件号码',
|
||||
@@ -402,7 +403,7 @@ function rowHasData(row: ExcelJS.Row, sourceColumns: readonly number[]): boolean
|
||||
// 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>> = {
|
||||
const BROKEN_TEMPLATE_FORMULAS: Partial<Record<SourceField, 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))'
|
||||
};
|
||||
@@ -434,11 +435,11 @@ function unusedTemplateRow(row: ExcelJS.Row, headerRow: number, sourceColumns: S
|
||||
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) {
|
||||
for (const [field, column] of Object.entries(sourceColumns)) {
|
||||
if (field === '序号') continue;
|
||||
const cell = row.getCell(sourceColumns[field]);
|
||||
const cell = row.getCell(column);
|
||||
if (cell.type === ExcelJS.ValueType.Formula) {
|
||||
const template = BROKEN_TEMPLATE_FORMULAS[field];
|
||||
const template = BROKEN_TEMPLATE_FORMULAS[field as SourceField];
|
||||
if (!formulaAllowed(cell, headerRow, sourceColumns, identityColumn)
|
||||
&& (!template || normalizedFormula(cell.formula) !== normalizedFormula(template))) return false;
|
||||
} else if (normalizedText(scalarValue(cell)) !== '') {
|
||||
@@ -516,7 +517,7 @@ function validateFormulasAndHiddenData(
|
||||
}
|
||||
|
||||
function headerColumnsForRow(sheet: ExcelJS.Worksheet, rowNumber: number): SourceColumnMap | null {
|
||||
const columns = new Map<RequiredSourceField, number>();
|
||||
const columns = new Map<SourceField, number>();
|
||||
const row = sheet.getRow(rowNumber);
|
||||
for (let columnNumber = 1; columnNumber <= sheet.columnCount; columnNumber += 1) {
|
||||
const cell = row.getCell(columnNumber);
|
||||
@@ -530,11 +531,9 @@ function headerColumnsForRow(sheet: ExcelJS.Worksheet, rowNumber: number): Sourc
|
||||
}
|
||||
if (!REQUIRED_SOURCE_FIELDS.every((field) => columns.has(field))) return null;
|
||||
|
||||
const result: Partial<SourceColumnMap> = {};
|
||||
for (const field of REQUIRED_SOURCE_FIELDS) {
|
||||
result[field] = columns.get(field);
|
||||
}
|
||||
return result as SourceColumnMap;
|
||||
// Keep absent optional fields out of the map: all downstream row/formula
|
||||
// checks operate on actual selected columns, never a guessed neighbour.
|
||||
return Object.fromEntries(columns) as SourceColumnMap;
|
||||
}
|
||||
|
||||
function findHeaderLayout(sheet: ExcelJS.Worksheet): { row: number; columns: SourceColumnMap } {
|
||||
@@ -739,7 +738,7 @@ function canonicalRow(
|
||||
canonicalTextCell(cell('签发地')),
|
||||
canonicalDate(issueDateCell, date1904, issueDateCell.type === ExcelJS.ValueType.Formula),
|
||||
canonicalDate(expiryCell, date1904, expiryCell.type === ExcelJS.ValueType.Formula),
|
||||
canonicalIdentifier(cell('电话')),
|
||||
sourceColumns['电话'] === undefined ? '' : canonicalIdentifier(row.getCell(sourceColumns['电话'])),
|
||||
canonicalTextCell(cell('备注'))
|
||||
];
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user