package utils import ( stderrors "errors" "fmt" "strconv" "strings" "github.com/xuri/excelize/v2" pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors" ) // 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 } header := rows[0] colIndex := buildDeviceColumnIndex(header) parseResult := &DeviceParseResult{ Rows: make([]DeviceRow, 0), ParseErrors: make([]CSVParseError, 0), } for i := 1; i < len(rows); i++ { record := rows[i] lineNum := i + 1 row := DeviceRow{Line: lineNum} if idx := colIndex["virtual_no"]; idx >= 0 && idx < len(record) { row.VirtualNo = strings.TrimSpace(record[idx]) } if idx := colIndex["imei"]; idx >= 0 && idx < len(record) { row.IMEI = strings.TrimSpace(record[idx]) } if idx := colIndex["device_name"]; idx >= 0 && idx < len(record) { row.DeviceName = strings.TrimSpace(record[idx]) } if idx := colIndex["device_model"]; idx >= 0 && idx < len(record) { row.DeviceModel = strings.TrimSpace(record[idx]) } if idx := colIndex["device_type"]; idx >= 0 && idx < len(record) { row.DeviceType = strings.TrimSpace(record[idx]) } if idx := colIndex["max_sim_slots"]; idx >= 0 && idx < len(record) { if n, err := strconv.Atoi(strings.TrimSpace(record[idx])); err == nil { row.MaxSimSlots = n } } if idx := colIndex["manufacturer"]; idx >= 0 && idx < len(record) { row.Manufacturer = strings.TrimSpace(record[idx]) } row.ICCIDs = make([]string, 0, 4) for j := 1; j <= 4; j++ { colName := "iccid_" + strconv.Itoa(j) if idx := colIndex[colName]; idx >= 0 && idx < len(record) { iccid := strings.TrimSpace(record[idx]) if iccid != "" { row.ICCIDs = append(row.ICCIDs, iccid) } } } parseResult.TotalCount++ if row.VirtualNo == "" { parseResult.ParseErrors = append(parseResult.ParseErrors, CSVParseError{ Line: lineNum, Reason: "设备虚拟号(virtual_no)不能为空", }) continue } if row.MaxSimSlots == 0 { row.MaxSimSlots = 4 } 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 解析卡数据行 // 自动检测表头并提取ICCID和MSISDN列 func parseCardRows(rows [][]string) (*CSVParseResult, error) { result := &CSVParseResult{ Cards: make([]CardInfo, 0), ParseErrors: make([]CSVParseError, 0), } headerSkipped := false iccidCol, msisdnCol, virtualNoCol := -1, -1, -1 if len(rows) > 0 { iccidCol, msisdnCol, virtualNoCol = findCardColumns(rows[0]) if iccidCol >= 0 && msisdnCol >= 0 { headerSkipped = true } } if virtualNoCol == -1 { return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "Excel 文件缺少 virtual_no 列,请使用最新模板") } startLine := 0 if headerSkipped { startLine = 1 } for i := startLine; i < len(rows); i++ { row := rows[i] lineNum := i + 1 if len(row) == 0 { continue } iccid := "" msisdn := "" virtualNo := "" if iccidCol >= 0 { if iccidCol < len(row) { iccid = strings.TrimSpace(row[iccidCol]) } if msisdnCol < len(row) { msisdn = strings.TrimSpace(row[msisdnCol]) } if virtualNoCol >= 0 && virtualNoCol < len(row) { virtualNo = strings.TrimSpace(row[virtualNoCol]) } } else { if len(row) >= 1 { iccid = strings.TrimSpace(row[0]) } if len(row) >= 2 { msisdn = strings.TrimSpace(row[1]) } } result.TotalCount++ if iccid == "" && msisdn == "" { continue } 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 } if virtualNo == "" { result.ParseErrors = append(result.ParseErrors, CSVParseError{ Line: lineNum, ICCID: iccid, MSISDN: msisdn, Reason: "虚拟号(virtual_no)不能为空", }) continue } result.Cards = append(result.Cards, CardInfo{ ICCID: iccid, MSISDN: msisdn, VirtualNo: virtualNo, }) } return result, nil } // findCardColumns 查找ICCID、MSISDN和VirtualNo列索引 // 支持中英文列名识别 func findCardColumns(header []string) (iccidCol, msisdnCol, virtualNoCol int) { iccidCol, msisdnCol, virtualNoCol = -1, -1, -1 for i, col := range header { colLower := strings.ToLower(strings.TrimSpace(col)) if colLower == "iccid" || colLower == "卡号" || colLower == "号码" { if iccidCol == -1 { iccidCol = i } } if colLower == "msisdn" || colLower == "接入号" || colLower == "手机号" || colLower == "电话" { if msisdnCol == -1 { msisdnCol = i } } if colLower == "virtual_no" || colLower == "virtualno" || colLower == "虚拟号" || colLower == "设备号" { if virtualNoCol == -1 { virtualNoCol = i } } } return iccidCol, msisdnCol, virtualNoCol } // buildDeviceColumnIndex 构建设备导入列索引 // 识别表头中的列名,返回列名到列索引的映射 // IMEI 列支持多种列名格式:imei/IMEI/设备IMEI/IMEI号 func buildDeviceColumnIndex(header []string) map[string]int { index := map[string]int{ "virtual_no": -1, "imei": -1, "device_name": -1, "device_model": -1, "device_type": -1, "max_sim_slots": -1, "manufacturer": -1, "iccid_1": -1, "iccid_2": -1, "iccid_3": -1, "iccid_4": -1, } for i, col := range header { normalized := strings.ToLower(strings.TrimSpace(col)) // 支持 IMEI 多种列名格式 switch normalized { case "imei", "设备imei", "imei号": if index["imei"] == -1 { index["imei"] = i } default: if _, exists := index[normalized]; exists { index[normalized] = i } } } return index }