package excel import ( "context" "io" "os" "strings" "github.com/gogf/gf/v2/errors/gerror" "github.com/xuri/excelize/v2" ) type ParseOptions struct { RequiredHeaders []string SingleSheet bool // MaxDataRows 限制单次导入的有效数据行数(不含表头、不含空行)。 // 0 表示不限制(向后兼容已有批量功能)。超过该值立即停止读取并返回错误, // 避免超大 Excel 在解析阶段占用过多内存或解析完成后才报错。 MaxDataRows int } func ParseFile(ctx context.Context, filePath string, options ParseOptions) ([]ParsedRow, error) { file, err := os.Open(filePath) if err != nil { return nil, gerror.Wrap(err, "open Excel file failed") } defer file.Close() return ParseReader(ctx, file, options) } // StreamFile parses the first data sheet row by row and keeps only the current // row in memory. func StreamFile(ctx context.Context, filePath string, options ParseOptions, handler func(ParsedRow) error) (int, error) { file, err := os.Open(filePath) if err != nil { return 0, gerror.Wrap(err, "open Excel file failed") } defer file.Close() return StreamReader(ctx, file, options, handler) } func StreamReader(ctx context.Context, reader io.Reader, options ParseOptions, handler func(ParsedRow) error) (int, error) { if handler == nil { return 0, gerror.New("Excel row handler cannot be nil") } f, err := excelize.OpenReader(reader) if err != nil { return 0, gerror.Wrap(err, "open Excel file failed") } defer func() { _ = f.Close() }() sheetList := f.GetSheetList() if len(sheetList) == 0 { return 0, gerror.New("Excel file has no worksheet") } if options.SingleSheet { if err = ensureSingleDataSheet(f, sheetList); err != nil { return 0, err } } rows, err := f.Rows(sheetList[0]) if err != nil { return 0, gerror.Wrap(err, "read Excel rows failed") } defer rows.Close() if !rows.Next() { if err = rows.Error(); err != nil { return 0, gerror.Wrap(err, "read Excel header failed") } return 0, gerror.New("Excel file has no header") } headers, err := rows.Columns() if err != nil { return 0, gerror.Wrap(err, "read Excel header failed") } colIndex, err := buildColumnIndex(headers) if err != nil { return 0, err } for _, col := range options.RequiredHeaders { if _, ok := colIndex[normalizeHeader(col)]; !ok { return 0, gerror.Newf("Excel file is missing required column: %s", col) } } rowNo := 1 parsedCount := 0 for rows.Next() { rowNo++ if err = ctx.Err(); err != nil { return parsedCount, err } values, columnsErr := rows.Columns() if columnsErr != nil { return parsedCount, gerror.Wrapf(columnsErr, "read Excel row %d failed", rowNo) } if isEmptyRow(values) { continue } fields := make(map[string]string, len(colIndex)) for colName, idx := range colIndex { fields[colName] = cellValue(values, idx) } if err = handler(ParsedRow{RowNo: rowNo, Fields: fields}); err != nil { return parsedCount, err } parsedCount++ } if err = rows.Error(); err != nil { return parsedCount, gerror.Wrap(err, "read Excel rows failed") } if parsedCount == 0 { return 0, gerror.New("Excel file has no data rows") } return parsedCount, nil } func ParseReader(ctx context.Context, reader io.Reader, options ParseOptions) ([]ParsedRow, error) { f, err := excelize.OpenReader(reader) if err != nil { return nil, gerror.Wrap(err, "open Excel file failed") } defer func() { _ = f.Close() }() sheetList := f.GetSheetList() if len(sheetList) == 0 { return nil, gerror.New("Excel file has no worksheet") } if options.SingleSheet { if err = ensureSingleDataSheet(f, sheetList); err != nil { return nil, err } } // 使用流式迭代器逐行读取,避免在超过 MaxDataRows 时仍把整张表读入内存。 rowsIter, err := f.Rows(sheetList[0]) if err != nil { return nil, gerror.Wrap(err, "read Excel rows failed") } defer func() { _ = rowsIter.Close() }() if !rowsIter.Next() { return nil, gerror.New("Excel文件缺少表头,请检查文件内容") } header, err := rowsIter.Columns() if err != nil { return nil, gerror.Wrap(err, "读取Excel表头失败,请检查文件内容") } colIndex, err := buildColumnIndex(header) if err != nil { return nil, err } for _, col := range options.RequiredHeaders { if _, ok := colIndex[normalizeHeader(col)]; !ok { return nil, gerror.Newf("Excel file is missing required column: %s", col) } } initCap := options.MaxDataRows if initCap <= 0 || initCap > 100000 { initCap = 1024 } parsedRows := make([]ParsedRow, 0, initCap) lineNo := 2 dataCount := 0 for rowsIter.Next() { if err = ctx.Err(); err != nil { return nil, err } row, err := rowsIter.Columns() if err != nil { return nil, gerror.Wrap(err, "读取Excel数据失败,请检查文件内容") } if isEmptyRow(row) { lineNo++ continue } dataCount++ if options.MaxDataRows > 0 && dataCount > options.MaxDataRows { return nil, gerror.Newf("单次导入最多支持%d张卡,请拆分后重试", options.MaxDataRows) } fields := make(map[string]string, len(colIndex)) for colName, idx := range colIndex { fields[colName] = cellValue(row, idx) } parsedRows = append(parsedRows, ParsedRow{ RowNo: lineNo, Fields: fields, }) lineNo++ } if len(parsedRows) == 0 { return nil, gerror.New("Excel file has no data rows") } return parsedRows, nil } func buildColumnIndex(headers []string) (map[string]int, error) { colIndex := make(map[string]int) for i, col := range headers { header := NormalizeHeader(col) if header == "" { continue } if _, exists := colIndex[header]; exists { return nil, gerror.Newf("Excel file has duplicate column: %s", strings.TrimSpace(col)) } colIndex[header] = i } return colIndex, nil } func ensureSingleDataSheet(f *excelize.File, sheetList []string) error { for _, sheetName := range sheetList[1:] { rows, err := f.GetRows(sheetName) if err != nil { return gerror.Wrap(err, "read Excel worksheet failed") } for _, row := range rows { if !isEmptyRow(row) { return gerror.New("only one non-empty worksheet is supported") } } } return nil } func cellValue(row []string, index int) string { if index < 0 || index >= len(row) { return "" } return strings.TrimSpace(row[index]) } func isEmptyRow(row []string) bool { for _, col := range row { if strings.TrimSpace(col) != "" { return false } } return true } func normalizeHeader(header string) string { return NormalizeHeader(header) }