package inbound import ( "bytes" "context" "fmt" "math/big" "path/filepath" "regexp" "strings" "sync" "time" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/xuri/excelize/v2" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/batch" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/batchresult" ) const ( cascadeFailureReason = "校验通过,但本批次存在失败明细,整批未执行入库" maxIdentifierLength = 32 minICCIDLength = 19 minIMSILength = 15 minMSISDNLength = 11 ) const ( inboundUpstreamBatchSizeConfigKey = "batch.inbound.upstreamBatchSize" inboundUpstreamConcurrencyKey = "batch.inbound.upstreamConcurrency" defaultInboundUpstreamBatchSize = 1 defaultInboundUpstreamConcurrency = 8 ) var tokenSplitter = regexp.MustCompile(`[,\t,\s]+`) type executor struct { repository Repository upstream Upstream rateLimiter upstreamBasicInfoRateLimiter } func NewExecutor(repository Repository) *executor { return &executor{repository: repository} } func NewExecutorWithUpstream(repository Repository, upstream Upstream) *executor { return &executor{ repository: repository, upstream: upstream, rateLimiter: newUpstreamBasicInfoRateLimiter(), } } func (e *executor) Execute(ctx context.Context, batchNo string) (err error) { totalStarted := time.Now() var task *Task defer func() { logInboundTotal(ctx, batchNo, task, totalStarted, err) }() stageStarted := time.Now() task, err = e.repository.Load(ctx, batchNo) if err != nil { logInboundStageByBatchNo(ctx, batchNo, "load_batch", stageStarted, 0, 0, 0, err) return gerror.Wrap(err, "load inbound batch failed") } logInboundStage(ctx, task, "load_batch", stageStarted, nil) stageStarted = time.Now() if err = e.populateRows(ctx, task); err != nil { logInboundStage(ctx, task, "populate_rows", stageStarted, err) return err } logInboundStage(ctx, task, "populate_rows", stageStarted, nil) stageStarted = time.Now() if err = e.validate(ctx, task); err != nil { logInboundStage(ctx, task, "validate", stageStarted, err) return err } logInboundStage(ctx, task, "validate", stageStarted, nil) if len(task.Rows) == 0 { stageStarted = time.Now() if err = e.repository.SaveValidationFailure(ctx, task); err != nil { logInboundStage(ctx, task, "save_validation_failure", stageStarted, err) return gerror.Wrap(err, "save empty inbound failure failed") } logInboundStage(ctx, task, "save_validation_failure", stageStarted, nil) _ = e.saveResultFile(ctx, task) return nil } if hasFailedRows(task.Rows) { applyCascadeFailure(task) stageStarted = time.Now() if err = e.repository.SaveValidationFailure(ctx, task); err != nil { logInboundStage(ctx, task, "save_validation_failure", stageStarted, err) return gerror.Wrap(err, "save inbound validation failure failed") } logInboundStage(ctx, task, "save_validation_failure", stageStarted, nil) _ = e.saveResultFile(ctx, task) return nil } stageStarted = time.Now() if err = e.repository.SaveSuccess(ctx, task); err != nil { logInboundStage(ctx, task, "save_success", stageStarted, err) g.Log().Errorf(ctx, "inbound batch DB write failed, batchNo:%s, err:%+v", task.Batch.BatchNo, err) stageStarted = time.Now() if saveErr := e.repository.SaveBusinessFailure(ctx, task, "数据库写入失败,本次整批未入库"); saveErr != nil { logInboundStage(ctx, task, "save_business_failure", stageStarted, saveErr) return gerror.Wrap(saveErr, "save inbound DB failure failed") } logInboundStage(ctx, task, "save_business_failure", stageStarted, nil) _ = e.saveResultFile(ctx, task) return nil } logInboundStage(ctx, task, "save_success", stageStarted, nil) _ = e.saveResultFile(ctx, task) return nil } func (e *executor) saveResultFile(ctx context.Context, task *Task) error { stageStarted := time.Now() if err := e.repository.SaveResultFile(ctx, task, resultRequest(task)); err != nil { g.Log().Errorf(ctx, "save inbound result file status failed, batchNo:%s, err:%+v", task.Batch.BatchNo, err) logInboundStage(ctx, task, "result_file", stageStarted, err) return err } logInboundStage(ctx, task, "result_file", stageStarted, nil) return nil } func logInboundStage(ctx context.Context, task *Task, stage string, started time.Time, err error) { batchNo := "" rows, valid, failed := 0, 0, 0 if task != nil { batchNo = task.Batch.BatchNo rows = len(task.Rows) valid = countValidRows(task.Rows) failed = countFailedRows(task.Rows) } logInboundStageByBatchNo(ctx, batchNo, stage, started, rows, valid, failed, err) } func logInboundStageByBatchNo(ctx context.Context, batchNo, stage string, started time.Time, rows, valid, failed int, err error) { durationMs := time.Since(started).Milliseconds() if err != nil { g.Log().Warningf(ctx, "inbound batch stage failed, batchNo:%s, stage:%s, rows:%d, valid:%d, failed:%d, durationMs:%d, err:%+v", batchNo, stage, rows, valid, failed, durationMs, err) return } g.Log().Infof(ctx, "inbound batch stage completed, batchNo:%s, stage:%s, rows:%d, valid:%d, failed:%d, durationMs:%d", batchNo, stage, rows, valid, failed, durationMs) } func logInboundTotal(ctx context.Context, batchNo string, task *Task, started time.Time, err error) { rows, valid, failed := 0, 0, 0 outcome := "success" if task != nil { if task.Batch.BatchNo != "" { batchNo = task.Batch.BatchNo } rows = len(task.Rows) valid = countValidRows(task.Rows) failed = countFailedRows(task.Rows) if task.FailureReason != "" || failed > 0 { outcome = "business_failed" } } if err != nil { outcome = "error" } duration := time.Since(started) rowsPerSecond := 0.0 if duration > 0 && rows > 0 { rowsPerSecond = float64(rows) / duration.Seconds() } if err != nil { g.Log().Warningf(ctx, "inbound batch finished, batchNo:%s, outcome:%s, rows:%d, valid:%d, failed:%d, durationMs:%d, rowsPerSecond:%.2f, err:%+v", batchNo, outcome, rows, valid, failed, duration.Milliseconds(), rowsPerSecond, err) return } g.Log().Infof(ctx, "inbound batch finished, batchNo:%s, outcome:%s, rows:%d, valid:%d, failed:%d, durationMs:%d, rowsPerSecond:%.2f", batchNo, outcome, rows, valid, failed, duration.Milliseconds(), rowsPerSecond) } func (e *executor) populateRows(ctx context.Context, task *Task) error { switch task.Batch.SourceType { case consts.BatchSourceTypeFile: return e.populateExcelRows(ctx, task) case consts.BatchSourceTypeBatchInput: return e.populateInputRows(ctx, task) case consts.BatchSourceTypeRange: return e.populateRangeRows(task) default: return e.populateRowsBySourceShape(ctx, task) } } func (e *executor) populateRowsBySourceShape(ctx context.Context, task *Task) error { switch { case task.Source.ImportFileOssID > 0: return e.populateExcelRows(ctx, task) case task.Source.InputText != "" || task.Source.InputFileOssID > 0: return e.populateInputRows(ctx, task) case task.Source.Range.StartICCID != "" || task.Source.Range.EndICCID != "": return e.populateRangeRows(task) default: return gerror.Newf("unsupported inbound source type %d", task.Batch.SourceType) } } func (e *executor) populateExcelRows(ctx context.Context, task *Task) error { if task.Source.ImportFileOssID <= 0 { return gerror.New("inbound Excel source importFileOssId is empty") } content, name, err := e.repository.ReadOssFile(ctx, task.Source.ImportFileOssID) if err != nil { return batch.Retryable(gerror.Wrap(err, "read inbound Excel source failed")) } workbook, err := excelize.OpenReader(bytes.NewReader(content)) if err != nil { return gerror.Wrapf(err, "open inbound Excel source %s failed", name) } defer func() { _ = workbook.Close() }() sheets := workbook.GetSheetList() if len(sheets) == 0 { return gerror.New("inbound Excel source has no sheets") } lines, err := workbook.GetRows(sheets[0]) if err != nil { return gerror.Wrap(err, "read inbound Excel rows failed") } if len(lines) == 0 || !hasInboundIdentifierHeader(headerIndex(lines[0])) { return gerror.New("入库文件表头无效:缺少 ICCID、MSISDN 或 IMSI 列") } task.Rows = parseTableRows(lines) return nil } func (e *executor) populateInputRows(ctx context.Context, task *Task) error { text := task.Source.InputText if strings.TrimSpace(text) == "" && task.Source.InputFileOssID > 0 { content, name, err := e.repository.ReadOssFile(ctx, task.Source.InputFileOssID) if err != nil { return batch.Retryable(gerror.Wrap(err, "read inbound input source failed")) } if strings.ToLower(filepath.Ext(name)) != ".txt" { g.Log().Warningf(ctx, "inbound input source file is not .txt, batchNo:%s, file:%s", task.Batch.BatchNo, name) } text = string(content) } task.Rows = parseInputText(text) return nil } func (e *executor) populateRangeRows(task *Task) error { rows, err := expandRange(task.Source.Range) if err != nil { return err } task.Rows = rows return nil } func (e *executor) validate(ctx context.Context, task *Task) error { if len(task.Rows) == 0 { task.FailureReason = "未解析到有效入库数据" return nil } useOptionFallback := shouldUseInboundOptionFallback(task) if !useOptionFallback { task.Options = Options{ValidateUpstreamBasicInfo: task.Options.ValidateUpstreamBasicInfo} } for _, row := range task.Rows { normalizeRow(row, task.Options, useOptionFallback) validateRow(row, task.Source.Range.RangeType) } syncInboundOptionsFromRows(task) markSameBatchDuplicates(task.Rows) if hasFailedRows(task.Rows) { markIncompleteRows(task.Rows) task.ValidCount = countValidRows(task.Rows) return nil } if err := e.markStockDuplicates(ctx, task); err != nil { return err } if hasFailedRows(task.Rows) { markIncompleteRows(task.Rows) task.ValidCount = countValidRows(task.Rows) return nil } task.Options.ValidateUpstreamBasicInfo = normalizeUpstreamBasicInfoValidationOption(task.Options.ValidateUpstreamBasicInfo) if e.upstream != nil && task.Options.ValidateUpstreamBasicInfo == "1" { if err := e.validateUpstreamBasicInfo(ctx, task); err != nil { return err } } markIncompleteRows(task.Rows) markSameBatchDuplicates(task.Rows) if err := e.markStockDuplicates(ctx, task); err != nil { return err } task.ValidCount = countValidRows(task.Rows) return nil } func (e *executor) markStockDuplicates(ctx context.Context, task *Task) error { iccids, msisdns, imsis := rowKeys(task.Rows) stocks, err := e.repository.FindExistingStocks(ctx, task.Batch.TenantID, iccids, msisdns, imsis) if err != nil { return gerror.Wrap(err, "query inbound stock duplicates failed") } markStockDuplicates(task.Rows, stocks) return nil } func parseInputText(text string) []*Row { lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n") rows := make([]*Row, 0, len(lines)) for index, line := range lines { fields := splitFields(line) if len(fields) == 0 { continue } if len(fields) == 1 { rows = append(rows, rowFromSingleIdentifier(index+1, fields[0])) continue } rows = append(rows, rowFromFields(index+1, fields, nil)) } return rows } func parseTableRows(lines [][]string) []*Row { if len(lines) == 0 { return nil } header := headerIndex(lines[0]) start := 0 if len(header) > 0 { start = 1 } rows := make([]*Row, 0, len(lines)-start) for index := start; index < len(lines); index++ { fields := trimRow(lines[index]) if rowEmpty(fields) { continue } if len(header) > 0 { rows = append(rows, rowFromHeader(index+1, fields, header)) continue } rows = append(rows, rowFromFields(index+1, fields, nil)) } return rows } func hasInboundIdentifierHeader(header map[string]int) bool { for _, key := range []string{"iccid", "msisdn", "imsi"} { if _, ok := header[key]; ok { return true } } return false } func rowFromHeader(rowNo int, fields []string, header map[string]int) *Row { raw := make(map[string]string, len(header)) for key, index := range header { if index >= 0 && index < len(fields) { raw[key] = fields[index] } } return &Row{ RowNo: rowNo, ICCID: valueForHeader(fields, header, "iccid"), MSISDN: valueForHeader(fields, header, "msisdn"), IMSI: valueForHeader(fields, header, "imsi"), IMEI: valueForHeader(fields, header, "imei"), ChanKey: firstNonEmpty( valueForHeader(fields, header, "chanKey"), valueForHeader(fields, header, "chan_key"), ), CardType: valueForHeader(fields, header, "cardType"), UpPkgCode: valueForHeader(fields, header, "upPkgCode"), SuppliersCode: valueForHeader(fields, header, "suppliersCode"), UpstreamActivationTime: valueForHeader(fields, header, "upstreamActivationTime"), InsertToPlugEnabled: valueForHeader(fields, header, "insertToPlugEnabled"), CarrierTrafficCutoffEnabled: valueForHeader(fields, header, "carrierTrafficCutoffEnabled"), AgreementExpiryDate: valueForHeader(fields, header, "agreementExpiryDate"), HasTestPeriod: valueForHeader(fields, header, "hasTestPeriod"), TestPeriodDays: valueForHeader(fields, header, "testPeriodDays"), TestPeriodLatestExpiryDate: valueForHeader(fields, header, "testPeriodLatestExpiryDate"), HasSilentPeriod: valueForHeader(fields, header, "hasSilentPeriod"), SilentPeriodDays: valueForHeader(fields, header, "silentPeriodDays"), SilentPeriodLatestExpiryDate: valueForHeader(fields, header, "silentPeriodLatestExpiryDate"), Raw: raw, } } func rowFromFields(rowNo int, fields []string, raw map[string]string) *Row { return &Row{ RowNo: rowNo, InputCode: valueAt(fields, 0), ICCID: valueAt(fields, 0), MSISDN: valueAt(fields, 1), IMSI: valueAt(fields, 2), IMEI: valueAt(fields, 3), ChanKey: valueAt(fields, 4), Raw: raw, } } func rowFromSingleIdentifier(rowNo int, code string) *Row { code = strings.TrimSpace(code) row := &Row{ RowNo: rowNo, InputCode: code, Raw: map[string]string{"inputCode": code}, } switch inferIdentifierType(code) { case "iccid": row.ICCID = code case "imsi": row.IMSI = code case "msisdn": row.MSISDN = code } return row } func inferIdentifierType(code string) string { code = strings.TrimSpace(code) switch { case code == "": return "" case len(code) >= minICCIDLength && len(code) <= maxIdentifierLength && allAlphaNumeric(code): return "iccid" case len(code) >= minIMSILength && len(code) < minICCIDLength && allDigits(code): return "imsi" case len(code) >= minMSISDNLength && len(code) < minIMSILength && allDigits(code): return "msisdn" default: return "" } } func headerIndex(fields []string) map[string]int { indexes := make(map[string]int) for index, field := range fields { key := normalizeHeader(field) switch key { case "iccid", "msisdn", "imsi", "imei", "chankey", "chan_key", "cardType", "upPkgCode", "suppliersCode", "upstreamActivationTime", "insertToPlugEnabled", "carrierTrafficCutoffEnabled", "agreementExpiryDate", "hasTestPeriod", "testPeriodDays", "testPeriodLatestExpiryDate", "hasSilentPeriod", "silentPeriodDays", "silentPeriodLatestExpiryDate": if key == "chankey" { key = "chanKey" } indexes[key] = index } } if !hasInboundIdentifierHeader(indexes) { return nil } return indexes } func normalizeHeader(value string) string { value = strings.TrimSpace(value) value = strings.Trim(value, "\ufeff") value = strings.TrimRight(value, "**") value = strings.TrimSpace(value) value = strings.ToLower(value) value = strings.NewReplacer( " ", "", "_", "", "-", "", "/", "", "(", "", ")", "", "(", "", ")", "", ":", "", ":", "", ).Replace(value) switch value { case "iccid", "iccid号", "集成电路卡识别码": return "iccid" case "msisdn", "卡号", "手机号", "接入号", "号码": return "msisdn" case "imsi", "国际移动用户识别码": return "imsi" case "imei", "国际移动设备识别码": return "imei" case "chankey", "通道", "通道编号", "通道编码", "通道标识": return "chankey" case "cardtype", "卡类型", "卡片类型": return "cardType" case "uppkgcode", "上游套餐", "上游套餐编码", "上游套餐编号": return "upPkgCode" case "supplierscode", "suppliercode", "供应商", "供应商编码", "供应商编号": return "suppliersCode" case "upstreamactivationtime", "上游激活时间", "上游给出激活时间", "上游开户时间": return "upstreamActivationTime" case "inserttoplugenabled", "是否贴片转插拔", "贴片转插拔": return "insertToPlugEnabled" case "carriertrafficcutoffenabled", "是否运营商达量断网", "运营商达量断网": return "carrierTrafficCutoffEnabled" case "agreementexpirydate", "协议到期时间", "协议期到期时间": return "agreementExpiryDate" case "hastestperiod", "是否有测试期", "有测试期": return "hasTestPeriod" case "testperioddays", "测试期天数": return "testPeriodDays" case "testperiodlatestexpirydate", "测试期最晚过期日期", "测试期最晚到期日期": return "testPeriodLatestExpiryDate" case "hassilentperiod", "是否有沉默期", "有沉默期": return "hasSilentPeriod" case "silentperioddays", "沉默期天数": return "silentPeriodDays" case "silentperiodlatestexpirydate", "沉默期最晚过期日期", "沉默期最晚到期日期": return "silentPeriodLatestExpiryDate" default: return value } } func splitFields(line string) []string { line = strings.TrimSpace(line) if line == "" { return nil } parts := tokenSplitter.Split(line, -1) fields := make([]string, 0, len(parts)) for _, part := range parts { if value := strings.TrimSpace(part); value != "" { fields = append(fields, value) } } return fields } func trimRow(values []string) []string { fields := make([]string, len(values)) for index, value := range values { fields[index] = strings.TrimSpace(value) } return fields } func rowEmpty(fields []string) bool { for _, field := range fields { if strings.TrimSpace(field) != "" { return false } } return true } func expandRange(input Range) ([]*Row, error) { rangeType := strings.ToLower(strings.TrimSpace(input.RangeType)) if rangeType == "" { rangeType = "iccid" } if rangeType != "iccid" && rangeType != "msisdn" && rangeType != "imsi" { return nil, gerror.New("rangeType must be iccid, msisdn or imsi") } if rangeType == "msisdn" { return expandSelectedRange(input.StartMSISDN, input.EndMSISDN, "msisdn") } if rangeType == "imsi" { return expandSelectedRange(input.StartIMSI, input.EndIMSI, "imsi") } iccidValues, err := iccidRange(input.StartICCID, input.EndICCID, true) if err != nil { return nil, gerror.Wrap(err, "invalid ICCID range") } msisdnValues, err := numericRange(input.StartMSISDN, input.EndMSISDN, false) if err != nil { return nil, gerror.Wrap(err, "invalid MSISDN range") } imsiValues, err := numericRange(input.StartIMSI, input.EndIMSI, false) if err != nil { return nil, gerror.Wrap(err, "invalid IMSI range") } if err = ensureRangeLength(len(iccidValues), len(msisdnValues), "MSISDN"); err != nil { return nil, err } if err = ensureRangeLength(len(iccidValues), len(imsiValues), "IMSI"); err != nil { return nil, err } rows := make([]*Row, 0, len(iccidValues)) for index, iccid := range iccidValues { row := &Row{RowNo: index + 1, ICCID: iccid} if len(msisdnValues) > 0 { row.MSISDN = msisdnValues[index] } if len(imsiValues) > 0 { row.IMSI = imsiValues[index] } rows = append(rows, row) } return rows, nil } func expandSelectedRange(start, end, rangeType string) ([]*Row, error) { values, err := numericRange(start, end, true) if err != nil { return nil, gerror.Wrapf(err, "invalid %s range", strings.ToUpper(rangeType)) } rows := make([]*Row, 0, len(values)) for index, value := range values { row := &Row{RowNo: index + 1} switch rangeType { case "msisdn": row.MSISDN = value case "imsi": row.IMSI = value } rows = append(rows, row) } return rows, nil } func iccidRange(start, end string, required bool) ([]string, error) { start = strings.TrimSpace(start) end = strings.TrimSpace(end) if start == "" && end == "" && !required { return nil, nil } if start == "" || end == "" { return nil, gerror.New("start and end are required together") } if !allAlphaNumeric(start) || !allAlphaNumeric(end) { return nil, gerror.New("range values must be alphanumeric") } if len(start) != len(end) { return nil, gerror.New("range values must have the same length") } if start == end { return []string{start}, nil } startPrefix, startTail := splitTrailingDigits(start) endPrefix, endTail := splitTrailingDigits(end) if startTail == "" || endTail == "" { return nil, gerror.New("range values must contain incrementable numeric suffixes") } if startPrefix != endPrefix { return nil, gerror.New("range value prefixes must match") } tails, err := numericRange(startTail, endTail, true) if err != nil { return nil, err } values := make([]string, 0, len(tails)) for _, tail := range tails { values = append(values, startPrefix+tail) } return values, nil } func numericRange(start, end string, required bool) ([]string, error) { start = strings.TrimSpace(start) end = strings.TrimSpace(end) if start == "" && end == "" && !required { return nil, nil } if start == "" || end == "" { return nil, gerror.New("start and end are required together") } if !allDigits(start) || !allDigits(end) { return nil, gerror.New("range values must be numeric") } startInt, ok := new(big.Int).SetString(start, 10) if !ok { return nil, gerror.New("invalid start value") } endInt, ok := new(big.Int).SetString(end, 10) if !ok { return nil, gerror.New("invalid end value") } if startInt.Cmp(endInt) > 0 { return nil, gerror.New("start must be less than or equal to end") } count := new(big.Int).Sub(endInt, startInt) count.Add(count, big.NewInt(1)) if !count.IsInt64() || count.Int64() > 100000 { return nil, gerror.New("range is too large") } width := len(start) if len(end) > width { width = len(end) } values := make([]string, 0, int(count.Int64())) current := new(big.Int).Set(startInt) for current.Cmp(endInt) <= 0 { value := current.String() if len(value) < width { value = strings.Repeat("0", width-len(value)) + value } values = append(values, value) current.Add(current, big.NewInt(1)) } return values, nil } func ensureRangeLength(iccidLength, otherLength int, name string) error { if otherLength > 0 && otherLength != iccidLength { return gerror.Newf("%s range count must match ICCID range count", name) } return nil } func normalizeRow(row *Row, options Options, useOptionFallback bool) { if row == nil { return } row.ICCID = strings.TrimSpace(row.ICCID) row.MSISDN = strings.TrimSpace(row.MSISDN) row.IMSI = strings.TrimSpace(row.IMSI) row.IMEI = strings.TrimSpace(row.IMEI) row.ChanKey = strings.TrimSpace(firstNonEmpty(row.ChanKey, fallbackOption(useOptionFallback, options.ChanKey))) row.CardType = normalizeCardType(firstNonEmpty(row.CardType, fallbackOption(useOptionFallback, options.CardType))) row.UpPkgCode = strings.TrimSpace(firstNonEmpty(row.UpPkgCode, fallbackOption(useOptionFallback, options.UpPkgCode))) row.SuppliersCode = strings.TrimSpace(firstNonEmpty(row.SuppliersCode, fallbackOption(useOptionFallback, options.SuppliersCode))) row.UpstreamActivationTime = strings.TrimSpace(firstNonEmpty(row.UpstreamActivationTime, fallbackOption(useOptionFallback, options.UpstreamActivationTime))) row.InsertToPlugEnabled = normalizeYesNo(firstNonEmpty(row.InsertToPlugEnabled, fallbackOption(useOptionFallback, options.InsertToPlugEnabled))) row.CarrierTrafficCutoffEnabled = normalizeYesNo(firstNonEmpty(row.CarrierTrafficCutoffEnabled, fallbackOption(useOptionFallback, options.CarrierTrafficCutoffEnabled))) row.AgreementExpiryDate = strings.TrimSpace(firstNonEmpty(row.AgreementExpiryDate, fallbackOption(useOptionFallback, options.AgreementExpiryDate))) row.HasTestPeriod = normalizeYesNo(firstNonEmpty(row.HasTestPeriod, fallbackOption(useOptionFallback, options.HasTestPeriod))) row.TestPeriodDays = strings.TrimSpace(firstNonEmpty(row.TestPeriodDays, fallbackOption(useOptionFallback, options.TestPeriodDays))) row.TestPeriodLatestExpiryDate = strings.TrimSpace(firstNonEmpty(row.TestPeriodLatestExpiryDate, fallbackOption(useOptionFallback, options.TestPeriodLatestExpiryDate))) row.HasSilentPeriod = normalizeYesNo(firstNonEmpty(row.HasSilentPeriod, fallbackOption(useOptionFallback, options.HasSilentPeriod))) row.SilentPeriodDays = strings.TrimSpace(firstNonEmpty(row.SilentPeriodDays, fallbackOption(useOptionFallback, options.SilentPeriodDays))) row.SilentPeriodLatestExpiryDate = strings.TrimSpace(firstNonEmpty(row.SilentPeriodLatestExpiryDate, fallbackOption(useOptionFallback, options.SilentPeriodLatestExpiryDate))) } func fallbackOption(enabled bool, value string) string { if !enabled { return "" } return value } func shouldUseInboundOptionFallback(task *Task) bool { if task == nil { return false } return task.Batch.SourceType != consts.BatchSourceTypeFile && task.Source.ImportFileOssID <= 0 } func validateRow(row *Row, rangeType string) { if row == nil { return } var reasons []string if row.ICCID == "" && row.MSISDN == "" && row.IMSI == "" { reasons = append(reasons, "ICCID/MSISDN/IMSI至少填写一个") } if row.ICCID != "" && (len(row.ICCID) < minICCIDLength || len(row.ICCID) > maxIdentifierLength || !allAlphaNumeric(row.ICCID)) { reasons = append(reasons, "ICCID格式不正确") } if row.MSISDN != "" && (len(row.MSISDN) < minMSISDNLength || len(row.MSISDN) > maxIdentifierLength || !allDigits(row.MSISDN)) { reasons = append(reasons, "MSISDN格式不正确") } if row.IMSI != "" && (len(row.IMSI) < minIMSILength || len(row.IMSI) > maxIdentifierLength || !allDigits(row.IMSI)) { reasons = append(reasons, "IMSI格式不正确") } if row.IMEI != "" && (len(row.IMEI) > maxIdentifierLength || !allDigits(row.IMEI)) { reasons = append(reasons, "IMEI格式不正确") } if row.ChanKey == "" { reasons = append(reasons, "chanKey不能为空") } if len(reasons) > 0 { row.Reason = strings.Join(reasons, ";") } } func markSameBatchDuplicates(rows []*Row) { seenICCID := make(map[string]struct{}, len(rows)) seenMSISDN := make(map[string]struct{}, len(rows)) seenIMSI := make(map[string]struct{}, len(rows)) for _, row := range rows { if row == nil || row.Reason != "" { continue } if row.ICCID != "" { if _, ok := seenICCID[row.ICCID]; ok { appendReason(row, "同批重复ICCID") } else { seenICCID[row.ICCID] = struct{}{} } } if row.MSISDN != "" { if _, ok := seenMSISDN[row.MSISDN]; ok { appendReason(row, "同批重复MSISDN") } else { seenMSISDN[row.MSISDN] = struct{}{} } } if row.IMSI != "" { if _, ok := seenIMSI[row.IMSI]; ok { appendReason(row, "同批重复IMSI") } else { seenIMSI[row.IMSI] = struct{}{} } } } } func markStockDuplicates(rows []*Row, stocks []Stock) { existingICCID := make(map[string]struct{}, len(stocks)) existingMSISDN := make(map[string]struct{}, len(stocks)) existingIMSI := make(map[string]struct{}, len(stocks)) for _, stock := range stocks { if stock.ICCID != "" { existingICCID[strings.TrimSpace(stock.ICCID)] = struct{}{} } if stock.MSISDN != "" { existingMSISDN[strings.TrimSpace(stock.MSISDN)] = struct{}{} } if stock.IMSI != "" { existingIMSI[strings.TrimSpace(stock.IMSI)] = struct{}{} } } for _, row := range rows { if row == nil || row.Reason != "" { continue } if _, ok := existingICCID[row.ICCID]; ok && row.ICCID != "" { appendReason(row, "ICCID已存在库存") } if _, ok := existingMSISDN[row.MSISDN]; ok && row.MSISDN != "" { appendReason(row, "MSISDN已存在库存") } if _, ok := existingIMSI[row.IMSI]; ok && row.IMSI != "" { appendReason(row, "IMSI已存在库存") } } } func markIncompleteRows(rows []*Row) { for _, row := range rows { if row == nil || row.Reason != "" { continue } missing := make([]string, 0, 3) if strings.TrimSpace(row.ICCID) == "" { missing = append(missing, "ICCID") } if strings.TrimSpace(row.MSISDN) == "" { missing = append(missing, "MSISDN") } if strings.TrimSpace(row.IMSI) == "" { missing = append(missing, "IMSI") } if len(missing) > 0 { appendReason(row, "三码信息不完整: "+strings.Join(missing, ",")) } } } func normalizeUpstreamBasicInfoValidationOption(value string) string { normalized := normalizeYesNo(value) if normalized == "1" { return "1" } return "0" } func syncInboundOptionsFromRows(task *Task) { if task == nil { return } checks := []struct { name string get func(*Row) string assign func(string) }{ {name: "通道编号", get: func(row *Row) string { return row.ChanKey }, assign: func(value string) { task.Options.ChanKey = value }}, {name: "卡类型", get: func(row *Row) string { return row.CardType }, assign: func(value string) { task.Options.CardType = value }}, {name: "上游套餐编码", get: func(row *Row) string { return row.UpPkgCode }, assign: func(value string) { task.Options.UpPkgCode = value }}, {name: "供应商编码", get: func(row *Row) string { return row.SuppliersCode }, assign: func(value string) { task.Options.SuppliersCode = value }}, {name: "上游激活时间", get: func(row *Row) string { return row.UpstreamActivationTime }, assign: func(value string) { task.Options.UpstreamActivationTime = value }}, {name: "是否贴片转插拔", get: func(row *Row) string { return row.InsertToPlugEnabled }, assign: func(value string) { task.Options.InsertToPlugEnabled = value }}, {name: "是否运营商达量断网", get: func(row *Row) string { return row.CarrierTrafficCutoffEnabled }, assign: func(value string) { task.Options.CarrierTrafficCutoffEnabled = value }}, {name: "协议到期时间", get: func(row *Row) string { return row.AgreementExpiryDate }, assign: func(value string) { task.Options.AgreementExpiryDate = value }}, {name: "是否有测试期", get: func(row *Row) string { return row.HasTestPeriod }, assign: func(value string) { task.Options.HasTestPeriod = value }}, {name: "测试期天数", get: func(row *Row) string { return row.TestPeriodDays }, assign: func(value string) { task.Options.TestPeriodDays = value }}, {name: "测试期最晚过期日期", get: func(row *Row) string { return row.TestPeriodLatestExpiryDate }, assign: func(value string) { task.Options.TestPeriodLatestExpiryDate = value }}, {name: "是否有沉默期", get: func(row *Row) string { return row.HasSilentPeriod }, assign: func(value string) { task.Options.HasSilentPeriod = value }}, {name: "沉默期天数", get: func(row *Row) string { return row.SilentPeriodDays }, assign: func(value string) { task.Options.SilentPeriodDays = value }}, {name: "沉默期最晚过期日期", get: func(row *Row) string { return row.SilentPeriodLatestExpiryDate }, assign: func(value string) { task.Options.SilentPeriodLatestExpiryDate = value }}, } for _, check := range checks { value, ok := sameBatchOptionValue(task.Rows, check.get) if !ok { markRowsWithReason(task.Rows, check.name+"必须同批一致") if task.FailureReason == "" { task.FailureReason = "同批入库配置必须一致" } continue } if value != "" { check.assign(value) } } fillRowOptionsFromTask(task.Rows, task.Options) } func sameBatchOptionValue(rows []*Row, get func(*Row) string) (string, bool) { value := "" for _, row := range rows { if row == nil { continue } current := strings.TrimSpace(get(row)) if current == "" { continue } if value == "" { value = current continue } if value != current { return "", false } } return value, true } func markRowsWithReason(rows []*Row, reason string) { for _, row := range rows { if row != nil { appendReason(row, reason) } } } func fillRowOptionsFromTask(rows []*Row, options Options) { for _, row := range rows { if row == nil { continue } row.ChanKey = firstNonEmpty(row.ChanKey, options.ChanKey) row.CardType = firstNonEmpty(row.CardType, options.CardType) row.UpPkgCode = firstNonEmpty(row.UpPkgCode, options.UpPkgCode) row.SuppliersCode = firstNonEmpty(row.SuppliersCode, options.SuppliersCode) row.UpstreamActivationTime = firstNonEmpty(row.UpstreamActivationTime, options.UpstreamActivationTime) row.InsertToPlugEnabled = firstNonEmpty(row.InsertToPlugEnabled, options.InsertToPlugEnabled) row.CarrierTrafficCutoffEnabled = firstNonEmpty(row.CarrierTrafficCutoffEnabled, options.CarrierTrafficCutoffEnabled) row.AgreementExpiryDate = firstNonEmpty(row.AgreementExpiryDate, options.AgreementExpiryDate) row.HasTestPeriod = firstNonEmpty(row.HasTestPeriod, options.HasTestPeriod) row.TestPeriodDays = firstNonEmpty(row.TestPeriodDays, options.TestPeriodDays) row.TestPeriodLatestExpiryDate = firstNonEmpty(row.TestPeriodLatestExpiryDate, options.TestPeriodLatestExpiryDate) row.HasSilentPeriod = firstNonEmpty(row.HasSilentPeriod, options.HasSilentPeriod) row.SilentPeriodDays = firstNonEmpty(row.SilentPeriodDays, options.SilentPeriodDays) row.SilentPeriodLatestExpiryDate = firstNonEmpty(row.SilentPeriodLatestExpiryDate, options.SilentPeriodLatestExpiryDate) } } func (e *executor) validateUpstreamBasicInfo(ctx context.Context, task *Task) error { started := time.Now() groups := groupRowsForUpstream(task.Rows) batchSize := inboundUpstreamBatchSize(ctx) jobs := make([]upstreamBasicInfoJob, 0, len(groups)) upstreamRows := 0 for chanKey, rows := range groups { upstreamRows += len(rows) for _, chunk := range chunkRows(rows, batchSize) { if len(chunk) == 0 { continue } jobs = append(jobs, upstreamBasicInfoJob{ chanKey: chanKey, rows: chunk, }) } } if len(jobs) == 0 { return nil } workers := inboundUpstreamConcurrency(ctx) if workers < 1 { workers = 1 } if workers > len(jobs) { workers = len(jobs) } g.Log().Infof(ctx, "inbound upstream basic-info started, batchNo:%s, rows:%d, groups:%d, jobs:%d, batchSize:%d, concurrency:%d", task.Batch.BatchNo, upstreamRows, len(groups), len(jobs), batchSize, workers) jobCh := make(chan upstreamBasicInfoJob) var wg sync.WaitGroup for i := 0; i < workers; i++ { wg.Add(1) go func() { defer wg.Done() for job := range jobCh { e.validateUpstreamBasicInfoChunk(ctx, task.Batch.BatchNo, job.chanKey, job.rows) } }() } for _, job := range jobs { jobCh <- job } close(jobCh) wg.Wait() g.Log().Infof(ctx, "inbound upstream basic-info completed, batchNo:%s, rows:%d, groups:%d, jobs:%d, batchSize:%d, concurrency:%d, failed:%d, durationMs:%d", task.Batch.BatchNo, upstreamRows, len(groups), len(jobs), batchSize, workers, countFailedRows(task.Rows), time.Since(started).Milliseconds()) return nil } func (e *executor) validateUpstreamBasicInfoChunk(ctx context.Context, batchNo, chanKey string, rows []*Row) { if len(rows) == 0 { return } started := time.Now() simKeys := make([]*model.SimKey, 0, len(rows)) for _, row := range rows { simKeys = append(simKeys, simKeyFromRow(row, chanKey)) } if e.rateLimiter != nil { // The limiter controls upstream HTTP requests. One chunk produces one // request, regardless of how many SIM keys it contains. if err := e.rateLimiter.Wait(ctx, chanKey, 1); err != nil { for _, row := range rows { appendReason(row, "上游三码校验限流失败: "+err.Error()) } g.Log().Warningf(ctx, "inbound upstream basic-info rate limit failed, batchNo:%s, chanKey:%s, rows:%d, durationMs:%d, err:%+v", batchNo, chanKey, len(rows), time.Since(started).Milliseconds(), err) return } } response, err := e.upstream.QuerySimBasicInfo(ctx, &model.SimBasicInfoInput{ SimKeys: simKeys, ReqData: model.SimInputReqNullDate{}, }) if err != nil { for _, row := range rows { appendReason(row, "上游三码校验失败: "+err.Error()) } g.Log().Warningf(ctx, "inbound upstream basic-info chunk failed, batchNo:%s, chanKey:%s, rows:%d, durationMs:%d, err:%+v", batchNo, chanKey, len(rows), time.Since(started).Milliseconds(), err) return } applyUpstreamBasicInfoResponse(rows, response) if duration := time.Since(started); duration >= time.Second { g.Log().Infof(ctx, "inbound upstream basic-info chunk completed, batchNo:%s, chanKey:%s, rows:%d, response:%d, failed:%d, durationMs:%d", batchNo, chanKey, len(rows), len(response), countFailedRows(rows), duration.Milliseconds()) } } func simKeyFromRow(row *Row, chanKey string) *model.SimKey { if row == nil { return &model.SimKey{ChanKey: chanKey} } if row.ICCID != "" { return &model.SimKey{ ChanKey: chanKey, ICCID: row.ICCID, } } return &model.SimKey{ ChanKey: chanKey, MSISDN: row.MSISDN, IMSI: row.IMSI, } } func groupRowsForUpstream(rows []*Row) map[string][]*Row { groups := make(map[string][]*Row) for _, row := range rows { if row == nil || row.Reason != "" || row.ChanKey == "" { continue } groups[row.ChanKey] = append(groups[row.ChanKey], row) } return groups } type upstreamBasicInfoJob struct { chanKey string rows []*Row } func chunkRows(rows []*Row, size int) [][]*Row { if size <= 0 { size = defaultInboundUpstreamBatchSize } if len(rows) == 0 { return nil } chunks := make([][]*Row, 0, (len(rows)+size-1)/size) for start := 0; start < len(rows); start += size { end := start + size if end > len(rows) { end = len(rows) } chunks = append(chunks, rows[start:end]) } return chunks } func inboundUpstreamBatchSize(ctx context.Context) int { value := g.Cfg().MustGet(ctx, inboundUpstreamBatchSizeConfigKey, defaultInboundUpstreamBatchSize).Int() if value <= 0 { return defaultInboundUpstreamBatchSize } return value } func inboundUpstreamConcurrency(ctx context.Context) int { value := g.Cfg().MustGet(ctx, inboundUpstreamConcurrencyKey, defaultInboundUpstreamConcurrency).Int() if value <= 0 { return defaultInboundUpstreamConcurrency } return value } func applyUpstreamBasicInfoResponse(rows []*Row, response []model.SimBaseRes[model.SimBasicInfoItem]) { if len(response) == 0 { for _, row := range rows { appendReason(row, "上游未返回SIM卡信息") } return } index := newBasicInfoResponseIndex(response) for _, row := range rows { item, ok := index.take(row) if !ok { appendReason(row, "上游未返回SIM卡信息") continue } if item.Code != 0 { reason := strings.TrimSpace(item.Message) if reason == "" { reason = "上游返回失败" } appendReason(row, "上游三码校验失败: "+reason) continue } info := normalizeBasicInfo(item.ResData) if reason := upstreamBasicInfoEmptyReason(item, info); reason != "" { appendReason(row, reason) continue } if reason := compareBasicInfo(row, info); reason != "" { appendReason(row, reason) continue } if reason := completeBasicInfoReason(info); reason != "" { appendReason(row, reason) continue } applyBasicInfo(row, info) } } type basicInfoResponseIndex struct { response []model.SimBaseRes[model.SimBasicInfoItem] infos []model.SimBasicInfoItem used []bool byICCID map[string][]int byMSISDN map[string][]int byIMSI map[string][]int } func newBasicInfoResponseIndex(response []model.SimBaseRes[model.SimBasicInfoItem]) *basicInfoResponseIndex { index := &basicInfoResponseIndex{ response: response, infos: make([]model.SimBasicInfoItem, len(response)), used: make([]bool, len(response)), byICCID: make(map[string][]int, len(response)), byMSISDN: make(map[string][]int, len(response)), byIMSI: make(map[string][]int, len(response)), } for i := range response { info := normalizeBasicInfo(response[i].ResData) index.infos[i] = info index.addInfoKeys(i, info) if response[i].SimKey != nil { index.addSimKeyKeys(i, response[i].SimKey) } } return index } func (i *basicInfoResponseIndex) addInfoKeys(index int, info model.SimBasicInfoItem) { appendResponseIndex(i.byICCID, iccidIndexKey(info.ICCID), index) appendResponseIndex(i.byMSISDN, identifierIndexKey(info.MSISDN), index) appendResponseIndex(i.byIMSI, identifierIndexKey(info.IMSI), index) } func (i *basicInfoResponseIndex) addSimKeyKeys(index int, simKey *model.SimKey) { if simKey == nil { return } appendResponseIndex(i.byICCID, iccidIndexKey(simKey.ICCID), index) appendResponseIndex(i.byMSISDN, identifierIndexKey(simKey.MSISDN), index) appendResponseIndex(i.byIMSI, identifierIndexKey(simKey.IMSI), index) } func appendResponseIndex(values map[string][]int, key string, index int) { if key == "" { return } values[key] = append(values[key], index) } func (i *basicInfoResponseIndex) take(row *Row) (model.SimBaseRes[model.SimBasicInfoItem], bool) { if i == nil || row == nil { return model.SimBaseRes[model.SimBasicInfoItem]{}, false } for _, candidates := range [][]int{ i.byICCID[iccidIndexKey(row.ICCID)], i.byMSISDN[identifierIndexKey(row.MSISDN)], i.byIMSI[identifierIndexKey(row.IMSI)], } { for _, candidate := range candidates { if candidate < 0 || candidate >= len(i.response) || i.used[candidate] { continue } if rowMatchesBasicInfo(row, i.infos[candidate]) || rowMatchesSimKey(row, i.response[candidate].SimKey) { i.used[candidate] = true return i.response[candidate], true } } } return model.SimBaseRes[model.SimBasicInfoItem]{}, false } func iccidIndexKey(value string) string { return strings.ToUpper(strings.TrimSpace(value)) } func identifierIndexKey(value string) string { return strings.TrimSpace(value) } func compareBasicInfo(row *Row, info model.SimBasicInfoItem) string { missing := make([]string, 0, 3) mismatches := make([]string, 0, 3) if row.ICCID != "" { actual := strings.TrimSpace(info.ICCID) if actual == "" { missing = append(missing, "ICCID") } else if !sameICCID(actual, row.ICCID) { mismatches = append(mismatches, "ICCID") } } if row.MSISDN != "" { actual := strings.TrimSpace(info.MSISDN) if actual == "" { missing = append(missing, "MSISDN") } else if actual != row.MSISDN { mismatches = append(mismatches, "MSISDN") } } if row.IMSI != "" { actual := strings.TrimSpace(info.IMSI) if actual == "" { missing = append(missing, "IMSI") } else if actual != row.IMSI { mismatches = append(mismatches, "IMSI") } } if len(missing) > 0 { return "上游未返回三码信息: " + strings.Join(missing, ",") } if len(mismatches) == 0 { return "" } return "上游三码校验不一致: " + strings.Join(mismatches, ",") } func normalizeBasicInfo(info model.SimBasicInfoItem) model.SimBasicInfoItem { info.ICCID = strings.TrimSpace(info.ICCID) info.MSISDN = strings.TrimSpace(info.MSISDN) info.IMSI = strings.TrimSpace(info.IMSI) info.ActiveDate = strings.TrimSpace(info.ActiveDate) info.OpenDate = strings.TrimSpace(info.OpenDate) info.Remark = strings.TrimSpace(info.Remark) return info } func upstreamBasicInfoEmptyReason(item model.SimBaseRes[model.SimBasicInfoItem], info model.SimBasicInfoItem) string { if info.ICCID != "" || info.MSISDN != "" || info.IMSI != "" { return "" } message := strings.TrimSpace(item.Message) if message == "" { return "" } if strings.Contains(message, "未查询") || strings.Contains(message, "不存在") { return "上游三码校验失败: " + message } return "" } func completeBasicInfoReason(info model.SimBasicInfoItem) string { missing := make([]string, 0, 3) if strings.TrimSpace(info.ICCID) == "" { missing = append(missing, "ICCID") } if strings.TrimSpace(info.MSISDN) == "" { missing = append(missing, "MSISDN") } if strings.TrimSpace(info.IMSI) == "" { missing = append(missing, "IMSI") } if len(missing) == 0 { return "" } return "上游未返回完整三码信息: " + strings.Join(missing, ",") } func applyBasicInfo(row *Row, info model.SimBasicInfoItem) { if row == nil { return } row.ICCID = strings.TrimSpace(info.ICCID) row.MSISDN = strings.TrimSpace(info.MSISDN) row.IMSI = strings.TrimSpace(info.IMSI) } func rowMatchesBasicInfo(row *Row, info model.SimBasicInfoItem) bool { if row == nil { return false } if row.ICCID != "" && info.ICCID != "" && sameICCID(info.ICCID, row.ICCID) { return true } if row.MSISDN != "" && info.MSISDN != "" && sameIdentifier(info.MSISDN, row.MSISDN) { return true } if row.IMSI != "" && info.IMSI != "" && sameIdentifier(info.IMSI, row.IMSI) { return true } return false } func rowMatchesSimKey(row *Row, simKey *model.SimKey) bool { if row == nil || simKey == nil { return false } return rowMatchesBasicInfo(row, model.SimBasicInfoItem{ ICCID: simKey.ICCID, MSISDN: simKey.MSISDN, IMSI: simKey.IMSI, }) } func sameICCID(a, b string) bool { return strings.EqualFold(strings.TrimSpace(a), strings.TrimSpace(b)) } func sameIdentifier(a, b string) bool { return strings.TrimSpace(a) == strings.TrimSpace(b) } func applyCascadeFailure(task *Task) { for _, row := range task.Rows { if row != nil && row.Reason == "" { row.Reason = cascadeFailureReason } } if task.FailureReason == "" { task.FailureReason = "同批存在校验失败,本次整批未入库" } } func hasFailedRows(rows []*Row) bool { for _, row := range rows { if row != nil && row.Reason != "" { return true } } return false } func countValidRows(rows []*Row) int { count := 0 for _, row := range rows { if row != nil && row.Reason == "" { count++ } } return count } func countFailedRows(rows []*Row) int { count := 0 for _, row := range rows { if row != nil && row.Reason != "" && row.Reason != cascadeFailureReason { count++ } } return count } func inboundRowStatus(row *Row) string { if IsCascadeFailure(row) { return "未执行" } if row.Reason != "" { return "失败" } return "成功" } func IsCascadeFailure(row *Row) bool { return row != nil && row.Reason == cascadeFailureReason } func rowKeys(rows []*Row) ([]string, []string, []string) { iccidSet := make(map[string]struct{}, len(rows)) msisdnSet := make(map[string]struct{}, len(rows)) imsiSet := make(map[string]struct{}, len(rows)) for _, row := range rows { if row == nil || row.Reason != "" { continue } if row.ICCID != "" { iccidSet[row.ICCID] = struct{}{} } if row.MSISDN != "" { msisdnSet[row.MSISDN] = struct{}{} } if row.IMSI != "" { imsiSet[row.IMSI] = struct{}{} } } return setKeys(iccidSet), setKeys(msisdnSet), setKeys(imsiSet) } func setKeys(values map[string]struct{}) []string { keys := make([]string, 0, len(values)) for key := range values { keys = append(keys, key) } return keys } func appendReason(row *Row, reason string) { if row.Reason == "" { row.Reason = reason return } row.Reason += ";" + reason } func allDigits(value string) bool { if value == "" { return false } for _, r := range value { if r < '0' || r > '9' { return false } } return true } func splitTrailingDigits(value string) (prefix, digits string) { index := len(value) for index > 0 { ch := value[index-1] if ch < '0' || ch > '9' { break } index-- } return value[:index], value[index:] } func allAlphaNumeric(value string) bool { if value == "" { return false } for _, r := range value { if (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { continue } return false } return true } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } func normalizeYesNo(value string) string { value = strings.TrimSpace(strings.ToLower(value)) if value == "" { return "" } value = strings.NewReplacer(" ", "", " ", "").Replace(value) switch value { case "0", "是", "true", "y", "yes", "启用", "开启", "有": return "0" case "1", "否", "false", "n", "no", "禁用", "关闭", "无": return "1" default: return value } } func normalizeCardType(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } compact := strings.NewReplacer(" ", "", " ", "", "-", "", "_", "", "‑", "", "-", "", "×", "x", "X", "x").Replace(strings.ToLower(value)) switch compact { case "1", "通用插拔sim卡", "通用插拔卡", "插拔卡": return "1" case "2", "贴片m2m卡5x6mm", "m2m5x6", "贴片卡5x6": return "2" case "3", "贴片m2m卡2x2mm", "m2m2x2", "贴片卡2x2": return "3" case "4", "mp1消费级插拔物联卡", "mp1": return "4" case "5", "mp2工业级插拔物联卡", "mp2": return "5" case "6", "ms0消费级贴片物联卡", "ms0": return "6" case "7", "ms1工业级贴片物联卡", "ms1": return "7" default: return value } } func valueAt(values []string, index int) string { if index < 0 || index >= len(values) { return "" } return strings.TrimSpace(values[index]) } func valueForHeader(values []string, header map[string]int, key string) string { index, ok := header[key] if !ok { return "" } return valueAt(values, index) } func resultRequest(task *Task) batchresult.Request { rows := make([]batchresult.Row, 0, len(task.Rows)) for _, row := range task.Rows { if row == nil { continue } rows = append(rows, batchresult.Row{ RowNo: row.RowNo, ICCID: row.ICCID, MSISDN: row.MSISDN, IMSI: row.IMSI, Success: row.Reason == "", Status: inboundRowStatus(row), Reason: row.Reason, }) } return batchresult.Request{BatchNo: task.Batch.BatchNo, BatchType: task.Batch.BatchType, TenantID: task.Batch.TenantID, Rows: rows} } func (r Range) String() string { return fmt.Sprintf("%s-%s", r.StartICCID, r.EndICCID) }