package upstreamapi import ( "context" "errors" "strings" "time" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/service" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" ) const ( defaultChannelTestTimeout = 2 * time.Minute defaultChannelTestItemTimeout = 10 * time.Second ) type channelTestOperation struct { key string name string execute func(context.Context, model.SimCardChannelI, *model.SimKey, time.Time) (any, int, string, bool, error) } func (u *sUpstreamAPI) TestChannel(ctx context.Context, input *model.ChannelTestInput) (*model.ChannelTestOutput, error) { if input == nil { return nil, gerror.New("channel test input is required") } if input.ChannelID <= 0 || strings.TrimSpace(input.TenantID) == "" { return nil, gerror.New("channel is not accessible") } operations, err := selectChannelTestOperations(input.Items) if err != nil { return nil, err } totalTimeout := g.Cfg().MustGet(ctx, "upstreamapi.channel_test.timeout", defaultChannelTestTimeout).Duration() if totalTimeout <= 0 { totalTimeout = defaultChannelTestTimeout } itemTimeout := g.Cfg().MustGet(ctx, "upstreamapi.channel_test.item_timeout", defaultChannelTestItemTimeout).Duration() if itemTimeout <= 0 { itemTimeout = defaultChannelTestItemTimeout } testCtx, cancel := context.WithTimeout(ctx, totalTimeout) defer cancel() config, err := service.ChannelConfig().LoadChannelForTest(testCtx, input.ChannelID, input.TenantID) if err != nil { if errors.Is(err, model.ErrChannelNotAccessible) { return nil, model.ErrChannelNotAccessible } return nil, gerror.Wrap(err, "load channel test configuration") } instance, err := u.newTestChannelInstance(testCtx, config) if err != nil { return nil, gerror.Wrap(err, "channel test configuration is invalid") } completionCtx, completionCancel := context.WithTimeout(testCtx, itemTimeout) simKey, err := resolveChannelTestSimKey(completionCtx, instance, config.Channel.ChanKey, input) completionCancel() if err != nil { return nil, err } results := make([]*model.ChannelTestItem, 0, len(operations)) now := time.Now() for _, operation := range operations { if testCtx.Err() != nil { results = append(results, channelTestTimeoutResult(operation)) continue } results = append(results, executeChannelTestOperation(testCtx, itemTimeout, operation, instance, simKey, now)) } return &model.ChannelTestOutput{Items: results}, nil } func resolveChannelTestSimKey(ctx context.Context, instance model.SimCardChannelI, chanKey string, input *model.ChannelTestInput) (*model.SimKey, error) { mode := strings.ToLower(strings.TrimSpace(input.InputMode)) if mode == "" { mode = model.ChannelTestInputModeComplete } cardType := strings.ToLower(strings.TrimSpace(input.CardValueType)) cardValue := strings.TrimSpace(input.CardValue) if mode == model.ChannelTestInputModeComplete { if strings.TrimSpace(input.ICCID) == "" || strings.TrimSpace(input.IMSI) == "" || strings.TrimSpace(input.MSISDN) == "" { return nil, gerror.New("complete mode requires iccid, imsi and msisdn") } return &model.SimKey{ChanKey: chanKey, ICCID: input.ICCID, IMSI: input.IMSI, MSISDN: input.MSISDN}, nil } if mode != model.ChannelTestInputModeSingle && mode != model.ChannelTestInputModeDirectSingle { return nil, gerror.New("unsupported channel test input mode") } if cardValue == "" || !validChannelTestCardType(cardType) { return nil, gerror.New("card value type and value are required") } key := &model.SimKey{ChanKey: chanKey} setChannelTestCardValue(key, cardType, cardValue) if mode == model.ChannelTestInputModeDirectSingle { return key, nil } // Card completion uses the same request-scoped instance as the selected // checks; it never falls back to chanMap or creates a second instance. basic, err := instance.QuerySimBasicInfo(ctx, &model.SimBasicInfoInput{SimKeys: []*model.SimKey{key}}) if err != nil { return nil, gerror.Wrap(err, "complete card identifiers") } if len(basic) == 0 || basic[0] == nil || basic[0].Code != consts.ApiStatusSuccess { if len(basic) > 0 && basic[0] != nil && basic[0].Message != "" { return nil, gerror.New(basic[0].Message) } return nil, gerror.New("carrier did not return card identifiers") } completed := basic[0].ResData if strings.TrimSpace(completed.ICCID) == "" || strings.TrimSpace(completed.IMSI) == "" || strings.TrimSpace(completed.MSISDN) == "" { return nil, gerror.New("carrier did not return complete card identifiers") } return &model.SimKey{ChanKey: chanKey, ICCID: completed.ICCID, IMSI: completed.IMSI, MSISDN: completed.MSISDN}, nil } func validChannelTestCardType(valueType string) bool { return valueType == "iccid" || valueType == "imsi" || valueType == "msisdn" } func setChannelTestCardValue(key *model.SimKey, valueType, value string) { switch valueType { case "iccid": key.ICCID = value case "imsi": key.IMSI = value case "msisdn": key.MSISDN = value } } func selectChannelTestOperations(requested []string) ([]channelTestOperation, error) { all := newChannelTestOperations() if len(requested) == 0 { return all, nil } wanted := make(map[string]struct{}, len(requested)) for _, key := range requested { key = strings.TrimSpace(key) if _, ok := model.ChannelTestOperationNames[key]; !ok { return nil, gerror.Newf("unsupported channel test item: %s", key) } wanted[key] = struct{}{} } selected := make([]channelTestOperation, 0, len(wanted)) for _, operation := range all { if _, ok := wanted[operation.key]; ok { selected = append(selected, operation) } } return selected, nil } func executeChannelTestOperation(parent context.Context, itemTimeout time.Duration, operation channelTestOperation, instance model.SimCardChannelI, simKey *model.SimKey, now time.Time) *model.ChannelTestItem { started := time.Now() callCtx, cancel := context.WithTimeout(parent, itemTimeout) defer cancel() data, errorCode, message, success, err := operation.execute(callCtx, instance, simKey, now) result := &model.ChannelTestItem{Key: operation.key, Name: operation.name, Status: "failed", Data: data, ErrorCode: errorCode, DurationMs: time.Since(started).Milliseconds()} if callCtx.Err() != nil { result.Status = "timeout" result.Message = "channel test item timed out" return result } if err != nil { result.Message = err.Error() return result } if !success { result.Message = message return result } result.Status = "success" result.Message = message return result } func channelTestTimeoutResult(operation channelTestOperation) *model.ChannelTestItem { return &model.ChannelTestItem{Key: operation.key, Name: operation.name, Status: "timeout", Message: "channel test request timed out"} } func newChannelTestOperations() []channelTestOperation { testType := 0 return []channelTestOperation{ newChannelTestOperation("simBasicInfo", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimBasicInfoItem], error) { return instance.QuerySimBasicInfo(ctx, &model.SimBasicInfoInput{SimKeys: []*model.SimKey{key}}) }), newChannelTestOperation("simSession", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimSessionItem], error) { return instance.QuerySimSession(ctx, &model.SimSessionInput{SimKeys: []*model.SimKey{key}}) }), newChannelTestOperation("simStatus", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimCardStatusItem], error) { return instance.QuerySimCardStatus(ctx, &model.SimCardStatusInput{SimKeys: []*model.SimKey{key}}) }), newChannelTestOperation("simStopReason", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimStopReasonItem], error) { return instance.QuerySimStopReason(ctx, &model.SimStopReasonInput{SimKeys: []*model.SimKey{key}}) }), newChannelTestOperation("simFrozenState", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimFrozenStateItem], error) { return instance.QuerySimFrozenState(ctx, &model.SimFrozenStateInput{SimKeys: []*model.SimKey{key}}) }), newChannelTestOperation("simCardBindStatus", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimCardBindStatusItem], error) { return instance.QuerySimCardBindStatus(ctx, &model.SimCardBindStatusInput{SimKeys: []*model.SimKey{key}, ReqData: model.SimCardBindStatusInputItem{TestType: &testType}}) }), newChannelTestOperation("simDeviceStatus", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimDeviceStatusItem], error) { return instance.QuerySimDeviceStatus(ctx, &model.SimDeviceStatusInput{SimKeys: []*model.SimKey{key}}) }), newChannelTestOperation("simRegionLimitStatus", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimRegionLimitStatusItem], error) { return instance.QuerySimRegionLimitStatus(ctx, &model.SimRegionLimitStatusInput{SimKeys: []*model.SimKey{key}}) }), newChannelTestOperation("simImei", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimImeiItem], error) { return instance.QuerySimImei(ctx, &model.SimImeiInput{SimKeys: []*model.SimKey{key}}) }), newChannelTestOperation("simDataUsage", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimDataUsageItem], error) { return instance.QuerySimDataUsage(ctx, &model.SimDataUsageInput{SimKeys: []*model.SimKey{key}}) }), newChannelTestOperation("simDataUsageMonthly", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, now time.Time) ([]*model.SimBaseRes[model.SimDataUsageMonthlyOutputItem], error) { last := now.AddDate(0, -1, 0) return instance.QuerySimDataUsageMonthly(ctx, &model.SimDataUsageMonthlyInput{SimKeys: []*model.SimKey{key}, ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: last.Format("200601")}}) }), newChannelTestOperation("simDataUsageDaily", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, now time.Time) ([]*model.SimBaseRes[model.SimDataUsageDailyOutputItem], error) { yesterday := now.AddDate(0, 0, -1) return instance.QuerySimDataUsageDaily(ctx, &model.SimDataUsageDailyInput{SimKeys: []*model.SimKey{key}, ReqData: model.SimDataUsageDailyInputItem{QueryDate: yesterday.Format("20060102")}}) }), newChannelTestOperation("simApnInfo", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimApnInfoItemList], error) { return instance.QuerySimApnInfo(ctx, &model.SimApnInfoInput{SimKeys: []*model.SimKey{key}}) }), } } func newChannelTestOperation[T any](key string, query func(context.Context, model.SimCardChannelI, *model.SimKey, time.Time) ([]*model.SimBaseRes[T], error)) channelTestOperation { return channelTestOperation{key: key, name: model.ChannelTestOperationNames[key], execute: func(ctx context.Context, instance model.SimCardChannelI, simKey *model.SimKey, now time.Time) (any, int, string, bool, error) { output, err := query(ctx, instance, simKey, now) if err != nil { return nil, 0, "", false, err } if len(output) == 0 || output[0] == nil { return nil, 0, "carrier returned no result", false, nil } first := output[0] if first.Code != consts.ApiStatusSuccess { return first.ResData, first.Code, first.Message, false, nil } return first.ResData, 0, first.Message, true, nil }} }