package utils import ( stderrors "errors" "fmt" "strconv" "strings" "github.com/xuri/excelize/v2" ) // CardInfo 卡信息(ICCID + MSISDN + VirtualNo) type CardInfo struct { ICCID string MSISDN string VirtualNo string } // CSVParseResult Excel/CSV 解析结果 type CSVParseResult struct { Cards []CardInfo TotalCount int ParseErrors []CSVParseError } // CSVParseError Excel/CSV 解析错误 type CSVParseError struct { Line int ICCID string MSISDN string Reason string } // DeviceParseResult 设备导入 Excel 解析结果 type DeviceParseResult struct { Rows []DeviceRow TotalCount int ParseErrors []CSVParseError } // DeviceRow 设备导入数据行 type DeviceRow struct { Line int VirtualNo string IMEI string // 设备IMEI,对应 Excel IMEI 列,用于 Gateway API 调用 DeviceName string DeviceModel string DeviceType string MaxSimSlots int Manufacturer string ICCIDs []string } var ( // ErrExcelNoSheets Excel文件无工作表 ErrExcelNoSheets = stderrors.New("Excel文件无工作表") // ErrExcelNoData Excel文件无数据行 ErrExcelNoData = stderrors.New("Excel文件无数据行(至少需要表头+1行数据)") ) // ParseCardExcel 解析包含 ICCID 和 MSISDN 两列的 Excel 文件 // filePath: Excel文件路径(.xlsx格式) // 返回: 解析结果 (与CSV解析器返回相同的数据结构) func ParseCardExcel(filePath string) (*CSVParseResult, error) { // 1. 打开Excel文件 f, err := excelize.OpenFile(filePath) if err != nil { return nil, fmt.Errorf("打开Excel失败: %w", err) } defer func() { if err := f.Close(); err != nil { // 日志记录关闭错误,但不影响解析结果 } }() // 2. 选择sheet (优先"导入数据",否则第一个) sheetName := selectSheet(f) if sheetName == "" { return nil, ErrExcelNoSheets } // 3. 读取所有行 rows, err := f.GetRows(sheetName) if err != nil { return nil, fmt.Errorf("读取sheet失败: %w", err) } if len(rows) < 2 { return nil, ErrExcelNoData } // 4. 解析表头 + 数据行 return parseCardRows(rows) } // ParseDeviceExcel 解析设备导入 Excel 文件 // filePath: Excel文件路径(.xlsx格式) // 返回: 解析结果(包含有效行和失败行)、错误 func ParseDeviceExcel(filePath string) (*DeviceParseResult, error) { f, err := excelize.OpenFile(filePath) if err != nil { return nil, fmt.Errorf("打开Excel失败: %w", err) } defer func() { if err := f.Close(); err != nil { } }() sheetName := selectSheet(f) if sheetName == "" { return nil, ErrExcelNoSheets } rows, err := f.GetRows(sheetName) if err != nil { return nil, fmt.Errorf("读取sheet失败: %w", err) } if len(rows) < 2 { return nil, ErrExcelNoData } parseResult := &DeviceParseResult{ Rows: make([]DeviceRow, 0), ParseErrors: make([]CSVParseError, 0), } // 按固定列位置读取,第1行为表头跳过: // col0=虚拟号 col1=设备名称 col2=设备型号 col3=设备类型 col4=IMEI // col5=制造商 col6=最大SIM槽数 col7~10=卡1~4 ICCID for i := 1; i < len(rows); i++ { record := rows[i] lineNum := i + 1 row := DeviceRow{Line: lineNum} getCol := func(idx int) string { if idx < len(record) { return strings.TrimSpace(record[idx]) } return "" } row.VirtualNo = getCol(0) row.DeviceName = getCol(1) row.DeviceModel = getCol(2) row.DeviceType = getCol(3) row.IMEI = getCol(4) row.Manufacturer = getCol(5) maxSimSlotsStr := getCol(6) row.ICCIDs = make([]string, 0, 4) for j := 7; j <= 10; j++ { iccid := getCol(j) if iccid != "" { row.ICCIDs = append(row.ICCIDs, iccid) } } // 整行目标列皆为空,视为空行跳过,不计入 total if row.VirtualNo == "" && row.DeviceName == "" && row.DeviceModel == "" && row.DeviceType == "" && row.IMEI == "" && row.Manufacturer == "" && maxSimSlotsStr == "" && len(row.ICCIDs) == 0 { continue } parseResult.TotalCount++ if row.VirtualNo == "" { parseResult.ParseErrors = append(parseResult.ParseErrors, CSVParseError{ Line: lineNum, Reason: "设备虚拟号(virtual_no)不能为空", }) continue } // 解析最大SIM槽数 if maxSimSlotsStr != "" { if n, err := strconv.Atoi(maxSimSlotsStr); err == nil { row.MaxSimSlots = n } } // 范围校验:不在 [1,4] 则失败;为空/0 时回填默认值 4 if row.MaxSimSlots == 0 { row.MaxSimSlots = 4 } else if row.MaxSimSlots < 1 || row.MaxSimSlots > 4 { parseResult.ParseErrors = append(parseResult.ParseErrors, CSVParseError{ Line: lineNum, Reason: "最大SIM槽数必须在1-4之间", }) continue } parseResult.Rows = append(parseResult.Rows, row) } return parseResult, nil } // selectSheet 选择要读取的sheet // 优先返回名为"导入数据"的sheet,否则返回第一个sheet func selectSheet(f *excelize.File) string { sheets := f.GetSheetList() if len(sheets) == 0 { return "" } // 优先查找"导入数据"sheet for _, sheet := range sheets { if sheet == "导入数据" { return sheet } } // 返回第一个sheet return sheets[0] } // parseCardRows 解析卡数据行 // 第1行固定为表头跳过,按列位置取值:col0=ICCID、col1=MSISDN、col2=VirtualNo func parseCardRows(rows [][]string) (*CSVParseResult, error) { result := &CSVParseResult{ Cards: make([]CardInfo, 0), ParseErrors: make([]CSVParseError, 0), } // 从第2行(索引1)开始,跳过第1行表头 for i := 1; i < len(rows); i++ { row := rows[i] lineNum := i + 1 iccid := "" msisdn := "" virtualNo := "" if len(row) >= 1 { iccid = strings.TrimSpace(row[0]) } if len(row) >= 2 { msisdn = strings.TrimSpace(row[1]) } if len(row) >= 3 { virtualNo = strings.TrimSpace(row[2]) } // 三列皆为空视为空行,跳过且不计入 total if iccid == "" && msisdn == "" && virtualNo == "" { continue } result.TotalCount++ if iccid == "" { result.ParseErrors = append(result.ParseErrors, CSVParseError{ Line: lineNum, MSISDN: msisdn, Reason: "ICCID 不能为空", }) continue } if msisdn == "" { result.ParseErrors = append(result.ParseErrors, CSVParseError{ Line: lineNum, ICCID: iccid, Reason: "MSISDN 不能为空", }) continue } result.Cards = append(result.Cards, CardInfo{ ICCID: iccid, MSISDN: msisdn, VirtualNo: virtualNo, }) } return result, nil }