package utils import ( "errors" "fmt" "strconv" "strings" "github.com/xuri/excelize/v2" ) // CardInfo 卡信息(ICCID + MSISDN) type CardInfo struct { ICCID string MSISDN 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 } // DeviceRow 设备导入数据行 type DeviceRow struct { Line int DeviceNo string DeviceName string DeviceModel string DeviceType string MaxSimSlots int Manufacturer string ICCIDs []string } var ( // ErrExcelNoSheets Excel文件无工作表 ErrExcelNoSheets = errors.New("Excel文件无工作表") // ErrExcelNoData Excel文件无数据行 ErrExcelNoData = errors.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) ([]DeviceRow, int, error) { // 1. 打开Excel文件 f, err := excelize.OpenFile(filePath) if err != nil { return nil, 0, fmt.Errorf("打开Excel失败: %w", err) } defer func() { if err := f.Close(); err != nil { // 日志记录关闭错误,但不影响解析结果 } }() // 2. 选择sheet sheetName := selectSheet(f) if sheetName == "" { return nil, 0, ErrExcelNoSheets } // 3. 读取所有行 rows, err := f.GetRows(sheetName) if err != nil { return nil, 0, fmt.Errorf("读取sheet失败: %w", err) } if len(rows) < 2 { return nil, 0, ErrExcelNoData } // 4. 解析表头行,构建列索引 header := rows[0] colIndex := buildDeviceColumnIndex(header) // 5. 解析数据行 var deviceRows []DeviceRow for i := 1; i < len(rows); i++ { record := rows[i] lineNum := i + 1 // Excel行号从1开始,数据从第2行开始 row := DeviceRow{Line: lineNum} // 提取各字段 if idx := colIndex["device_no"]; idx >= 0 && idx < len(record) { row.DeviceNo = 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]) } // 提取ICCID (iccid_1 ~ iccid_4) 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) } } } // 跳过设备号为空的行 if row.DeviceNo == "" { continue } // 默认最大插槽数为4 if row.MaxSimSlots == 0 { row.MaxSimSlots = 4 } deviceRows = append(deviceRows, row) } return deviceRows, len(deviceRows), 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), } // 检测表头 (第1行) headerSkipped := false iccidCol, msisdnCol := -1, -1 if len(rows) > 0 { iccidCol, msisdnCol = findCardColumns(rows[0]) if iccidCol >= 0 && msisdnCol >= 0 { headerSkipped = true } } // 确定数据开始行 startLine := 0 if headerSkipped { startLine = 1 } // 解析数据行 for i := startLine; i < len(rows); i++ { row := rows[i] lineNum := i + 1 // Excel行号从1开始 // 跳过空行 if len(row) == 0 { continue } // 提取字段 iccid := "" msisdn := "" if iccidCol >= 0 { // 有表头,使用列索引 if iccidCol < len(row) { iccid = strings.TrimSpace(row[iccidCol]) } if msisdnCol < len(row) { msisdn = strings.TrimSpace(row[msisdnCol]) } } else { // 无表头,假设第一列ICCID,第二列MSISDN 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 } result.Cards = append(result.Cards, CardInfo{ ICCID: iccid, MSISDN: msisdn, }) } return result, nil } // findCardColumns 查找ICCID和MSISDN列索引 // 支持中英文列名识别 func findCardColumns(header []string) (iccidCol, msisdnCol int) { iccidCol, msisdnCol = -1, -1 for i, col := range header { colLower := strings.ToLower(strings.TrimSpace(col)) // 识别ICCID列 if colLower == "iccid" || colLower == "卡号" || colLower == "号码" { if iccidCol == -1 { // 只取第一个匹配 iccidCol = i } } // 识别MSISDN列 if colLower == "msisdn" || colLower == "接入号" || colLower == "手机号" || colLower == "电话" { if msisdnCol == -1 { // 只取第一个匹配 msisdnCol = i } } } return iccidCol, msisdnCol } // buildDeviceColumnIndex 构建设备导入列索引 // 识别表头中的列名,返回列名到列索引的映射 func buildDeviceColumnIndex(header []string) map[string]int { index := map[string]int{ "device_no": -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 { col = strings.ToLower(strings.TrimSpace(col)) if _, exists := index[col]; exists { index[col] = i } } return index }