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) =>