package service import ( "bytes" "errors" "testing" "github.com/xuri/excelize/v2" ) func TestParseMaterialImportFileReadsRowsWithExcelRowNumbers(t *testing.T) { data := buildTestWorkbook(t, [][]string{ {"内六角螺栓", "M8x30", "个", "机械结构类", "标准件"}, {"", "", "", "", ""}, // 整行空,应被跳过 {"急停按钮", "XB2", "个", "电气元器件类", "成品件"}, }) rows, err := ParseMaterialImportFile(bytes.NewReader(data)) if err != nil { t.Fatalf("parse: %v", err) } if len(rows) != 2 { t.Fatalf("rows = %d, want 2", len(rows)) } if rows[0].Row != 2 || rows[0].Name != "内六角螺栓" || rows[0].MinorName != "标准件" { t.Fatalf("first row = %+v", rows[0]) } // 第 3 行是空行被跳过,所以第二条数据是 Excel 第 4 行 if rows[1].Row != 4 || rows[1].Name != "急停按钮" { t.Fatalf("second row = %+v", rows[1]) } } func TestParseMaterialImportFileKeepsRowsWithAnyContent(t *testing.T) { // 名称填了、其余为空:不是空行,必须保留交给校验去报错 data := buildTestWorkbook(t, [][]string{ {"只有名称", "", "", "", ""}, }) rows, err := ParseMaterialImportFile(bytes.NewReader(data)) if err != nil { t.Fatalf("parse: %v", err) } if len(rows) != 1 || rows[0].Name != "只有名称" { t.Fatalf("rows = %+v", rows) } } func TestParseMaterialImportFileRejectsMissingRequiredColumn(t *testing.T) { // 只有「物料名称」和「规格」,缺必填的大类、小类 data := buildTestWorkbookWithHeaders(t, []string{"物料名称", "规格"}, [][]string{ {"内六角螺栓", "M8x30"}, }) _, err := ParseMaterialImportFile(bytes.NewReader(data)) if !errors.Is(err, ErrImportTemplateInvalid) { t.Fatalf("err = %v, want ErrImportTemplateInvalid", err) } } func TestParseMaterialImportFileAcceptsMissingOptionalColumnsAndReorderedColumns(t *testing.T) { // 删掉「规格」,并把「单位」挪到「物料名称」前面 headers := []string{"单位", "物料名称", "大类", "小类"} data := buildTestWorkbookWithHeaders(t, headers, [][]string{ {"个", "内六角螺栓", "机械结构类", "标准件"}, }) rows, err := ParseMaterialImportFile(bytes.NewReader(data)) if err != nil { t.Fatalf("parse: %v", err) } if len(rows) != 1 { t.Fatalf("rows = %d, want 1", len(rows)) } if rows[0].Name != "内六角螺栓" || rows[0].Unit != "个" || rows[0].MinorName != "标准件" { t.Fatalf("row = %+v", rows[0]) } if rows[0].Spec != "" { t.Fatalf("缺失的规格列应解析为空,得到 %q", rows[0].Spec) } } func TestParseMaterialImportFileIgnoresUnknownColumns(t *testing.T) { headers := []string{"物料名称", "备注", "大类", "小类"} data := buildTestWorkbookWithHeaders(t, headers, [][]string{ {"内六角螺栓", "与导入无关", "机械结构类", "标准件"}, }) rows, err := ParseMaterialImportFile(bytes.NewReader(data)) if err != nil { t.Fatalf("parse: %v", err) } if len(rows) != 1 || rows[0].Name != "内六角螺栓" { t.Fatalf("rows = %+v", rows) } } func TestParseMaterialImportFileRejectsTooManyRows(t *testing.T) { rows := make([][]string, 0, MaterialImportMaxRows+1) for i := 0; i < MaterialImportMaxRows+1; i++ { rows = append(rows, []string{"物料", "", "", "机械结构类", "标准件"}) } data := buildTestWorkbook(t, rows) _, err := ParseMaterialImportFile(bytes.NewReader(data)) if !errors.Is(err, ErrImportTooManyRows) { t.Fatalf("err = %v, want ErrImportTooManyRows", err) } } func TestBuildMaterialImportTemplateRoundTrips(t *testing.T) { data, err := BuildMaterialImportTemplate() if err != nil { t.Fatalf("build template: %v", err) } f, err := excelize.OpenReader(bytes.NewReader(data)) if err != nil { t.Fatalf("open template: %v", err) } defer f.Close() if idx, _ := f.GetSheetIndex(MaterialImportSheetData); idx < 0 { t.Fatal("模板缺少「物料数据」工作表") } if idx, _ := f.GetSheetIndex(MaterialImportSheetGuide); idx < 0 { t.Fatal("模板缺少「填写说明」工作表") } headers, err := f.GetRows(MaterialImportSheetData) if err != nil { t.Fatalf("read rows: %v", err) } if len(headers) == 0 || headers[0][0] != "物料名称" || headers[0][4] != "小类" { t.Fatalf("表头 = %v", headers) } // 模板必须能被自己的解析器接受(表头一致) if _, err := ParseMaterialImportFile(bytes.NewReader(data)); err != nil { t.Fatalf("模板无法被解析器接受: %v", err) } } // buildTestWorkbook 用标准表头 + 给定数据行构造一个 xlsx 字节流。 func buildTestWorkbook(t *testing.T, dataRows [][]string) []byte { t.Helper() return buildTestWorkbookWithHeaders(t, materialImportHeaders, dataRows) } // buildTestWorkbookWithHeaders 允许指定表头,用来覆盖缺列、乱序、多余列的解析分支。 func buildTestWorkbookWithHeaders(t *testing.T, headers []string, dataRows [][]string) []byte { t.Helper() f := excelize.NewFile() defer f.Close() f.SetSheetName("Sheet1", MaterialImportSheetData) for i, header := range headers { cell, _ := excelize.CoordinatesToCellName(i+1, 1) f.SetCellValue(MaterialImportSheetData, cell, header) } for r, row := range dataRows { for c, value := range row { cell, _ := excelize.CoordinatesToCellName(c+1, r+2) f.SetCellValue(MaterialImportSheetData, cell, value) } } buf, err := f.WriteToBuffer() if err != nil { t.Fatalf("write workbook: %v", err) } return buf.Bytes() }