package utils import ( "encoding/csv" "errors" "io" "strings" ) // CardInfo 卡信息(ICCID + MSISDN) type CardInfo struct { ICCID string MSISDN string } // CSVParseResult CSV 解析结果 type CSVParseResult struct { Cards []CardInfo TotalCount int ParseErrors []CSVParseError } // CSVParseError CSV 解析错误 type CSVParseError struct { Line int ICCID string MSISDN string Reason string } // ErrInvalidCSVFormat CSV 格式错误 var ErrInvalidCSVFormat = errors.New("CSV 文件格式错误:缺少 MSISDN 列,文件必须包含 ICCID 和 MSISDN 两列") // ParseCardCSV 解析包含 ICCID 和 MSISDN 两列的 CSV 文件 func ParseCardCSV(reader io.Reader) (*CSVParseResult, error) { csvReader := csv.NewReader(reader) csvReader.FieldsPerRecord = -1 csvReader.TrimLeadingSpace = true result := &CSVParseResult{ Cards: make([]CardInfo, 0), ParseErrors: make([]CSVParseError, 0), } lineNum := 0 headerSkipped := false for { record, err := csvReader.Read() if err == io.EOF { break } lineNum++ if err != nil { result.ParseErrors = append(result.ParseErrors, CSVParseError{ Line: lineNum, Reason: "CSV 解析错误: " + err.Error(), }) continue } if len(record) == 0 { continue } if len(record) < 2 { if lineNum == 1 && !headerSkipped { firstCol := strings.TrimSpace(record[0]) if isICCIDHeader(firstCol) { return nil, ErrInvalidCSVFormat } } result.ParseErrors = append(result.ParseErrors, CSVParseError{ Line: lineNum, ICCID: strings.TrimSpace(record[0]), Reason: "列数不足:缺少 MSISDN 列", }) result.TotalCount++ continue } iccid := strings.TrimSpace(record[0]) msisdn := strings.TrimSpace(record[1]) if lineNum == 1 && !headerSkipped && isHeader(iccid, msisdn) { headerSkipped = true 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, }) } return result, nil } func isICCIDHeader(value string) bool { lower := strings.ToLower(value) return lower == "iccid" || lower == "卡号" || lower == "号码" } func isMSISDNHeader(value string) bool { lower := strings.ToLower(value) return lower == "msisdn" || lower == "接入号" || lower == "手机号" || lower == "电话" || lower == "号码" } func isHeader(col1, col2 string) bool { return isICCIDHeader(col1) && isMSISDNHeader(col2) }