Files
iotlcm/worker-svr/internal/service/inbound/executor.go
T

1590 lines
49 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}