feat(intake): validate sku import skuCode spu prefix

standard sku workbook import now rejects rows whose skuCode does not start with the product spu in uppercase (lowercase codes are reported as needing uppercase); rows with an empty skuCode still pass and get auto-generated on save; the whole import is rejected with per-row line numbers and reasons. barcodes are skipped since they derive from the sku code.
This commit is contained in:
WuYongJie 2026-09-04 17:39:35 +08:00
parent e27e4fe67e
commit fccf9485b7
3 changed files with 103 additions and 2 deletions

View File

@ -4,7 +4,10 @@ import {
type SetStateAction,
} from "react";
import { parseNonIntegratedSkuWorkbook } from "@/lib/non-integrated-sku-export";
import {
findSkuImportSpuPrefixErrors,
parseNonIntegratedSkuWorkbook,
} from "@/lib/non-integrated-sku-export";
import type { NonIntegratedSkuItem } from "../../nonIntegratedStorage";
import type {
@ -154,6 +157,17 @@ export function useSkuWorkbookController({
if (rows.length === 0) {
throw new Error("上传资料中没有可导入的 SKU 行");
}
if (mode === "standard") {
const spuPrefixErrors = findSkuImportSpuPrefixErrors(
rows,
formValues.spu,
);
if (spuPrefixErrors.length > 0) {
throw new Error(
`导入已取消:货号/条码必须以 ${formValues.spu} 开头。${spuPrefixErrors.join(" ")}`,
);
}
}
if (mode === "fuzzy") {
const supplierErrors = getFuzzyImportSupplierErrors(
rows,

View File

@ -1,6 +1,7 @@
import * as XLSX from "xlsx";
import {
findSkuImportSpuPrefixErrors,
NON_INTEGRATED_SKU_TEMPLATE_COLUMNS,
parseNonIntegratedSkuWorkbook,
} from "@/lib/non-integrated-sku-export";
@ -109,3 +110,61 @@ describe("parseNonIntegratedSkuWorkbook", () => {
);
});
});
describe("findSkuImportSpuPrefixErrors", () => {
it("rejects non-empty sku codes that miss the spu prefix", () => {
const errors = findSkuImportSpuPrefixErrors(
[
{
skuCode: "LRSF009601",
barcode: "LRSF009601V01",
rowNumber: 2,
},
],
"LRSF0095",
);
expect(errors).toEqual([
"第2行:货号「LRSF009601」未以 LRSF0095 开头",
]);
});
it("ignores barcodes entirely since they are auto-derived from the sku code", () => {
const errors = findSkuImportSpuPrefixErrors(
[{ skuCode: "LRSF009501", barcode: "WHATEVER-BARCODE", rowNumber: 2 }],
"LRSF0095",
);
expect(errors).toEqual([]);
});
it("requires uppercase and rejects lowercase sku codes", () => {
const errors = findSkuImportSpuPrefixErrors(
[{ skuCode: "lrsf009501", barcode: "LRSF009501V01", rowNumber: 2 }],
"LRSF0095",
);
expect(errors).toEqual(["第2行:货号「lrsf009501」需为大写"]);
});
it("skips validation when the spu is empty", () => {
const errors = findSkuImportSpuPrefixErrors(
[{ skuCode: "ANY", barcode: "ANY01", rowNumber: 2 }],
"",
);
expect(errors).toEqual([]);
});
it("falls back to positional row labels when rowNumber is missing", () => {
const errors = findSkuImportSpuPrefixErrors(
[
{ skuCode: "", barcode: "" },
{ skuCode: "OTHER01", barcode: "" },
],
"LRSF0095",
);
expect(errors).toEqual(["第3行:货号「OTHER01」未以 LRSF0095 开头"]);
});
});

View File

@ -24,6 +24,33 @@ export type NonIntegratedSkuExportRow = {
batchNo: string
batchChangeType: string
dingTalkFlowNo: string
rowNumber?: number
}
export function findSkuImportSpuPrefixErrors(
rows: ReadonlyArray<
Pick<NonIntegratedSkuExportRow, 'skuCode'> & { rowNumber?: number }
>,
spu: string,
) {
const normalizedSpu = spu.trim().toUpperCase()
if (!normalizedSpu) return []
const errors: string[] = []
rows.forEach((row, index) => {
const rowLabel = row.rowNumber ?? index + 2
const skuCode = row.skuCode.trim()
if (!skuCode || skuCode.startsWith(normalizedSpu)) return
if (skuCode.toUpperCase().startsWith(normalizedSpu)) {
errors.push(`第${rowLabel}行:货号「${skuCode}」需为大写`)
} else {
errors.push(
`第${rowLabel}行:货号「${skuCode}」未以 ${normalizedSpu} 开头`,
)
}
})
return errors
}
export type NonIntegratedSkuFuzzyImportRow = {
@ -340,7 +367,7 @@ export async function parseNonIntegratedSkuWorkbook(
if (missingLabels.length === 0) {
const rows = sheetRows
.slice(headerRowIndex + 1)
.map((row) => {
.map((row, index) => {
const importRow = createEmptyImportRow()
for (const column of NON_INTEGRATED_SKU_TEMPLATE_COLUMNS) {
const cellIndex = headerIndexByLabel.get(column.label)
@ -349,6 +376,7 @@ export async function parseNonIntegratedSkuWorkbook(
? ''
: normalizeWorkbookCellValue(row[cellIndex])
}
importRow.rowNumber = headerRowIndex + index + 2
return importRow
})
.filter((row) =>