From fccf9485b7bc54e855ea578faaabca9bdcd417b9 Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Fri, 4 Sep 2026 17:39:35 +0800 Subject: [PATCH] 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. --- .../basic-info/useSkuWorkbookController.ts | 16 ++++- .../src/lib/non-integrated-sku-export.test.ts | 59 +++++++++++++++++++ .../src/lib/non-integrated-sku-export.ts | 30 +++++++++- 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/useSkuWorkbookController.ts b/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/useSkuWorkbookController.ts index ddaaf4b3..322bcb79 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/useSkuWorkbookController.ts +++ b/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/useSkuWorkbookController.ts @@ -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, diff --git a/furniture-3d-system/src/lib/non-integrated-sku-export.test.ts b/furniture-3d-system/src/lib/non-integrated-sku-export.test.ts index 5ae6f039..70444eb2 100644 --- a/furniture-3d-system/src/lib/non-integrated-sku-export.test.ts +++ b/furniture-3d-system/src/lib/non-integrated-sku-export.test.ts @@ -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 开头"]); + }); +}); diff --git a/furniture-3d-system/src/lib/non-integrated-sku-export.ts b/furniture-3d-system/src/lib/non-integrated-sku-export.ts index 67f72316..1361955e 100644 --- a/furniture-3d-system/src/lib/non-integrated-sku-export.ts +++ b/furniture-3d-system/src/lib/non-integrated-sku-export.ts @@ -24,6 +24,33 @@ export type NonIntegratedSkuExportRow = { batchNo: string batchChangeType: string dingTalkFlowNo: string + rowNumber?: number +} + +export function findSkuImportSpuPrefixErrors( + rows: ReadonlyArray< + Pick & { 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) =>