Files
iotlcm/internal/logic/upstreamapi/channel_test_runtime.go
T
MiFaZhan 96b1edff45 chore: 初始开源发布
iotlcm-upstream-chan:运营商上游通道适配服务(统一 /sim/* 协议)。

- 内联原私有依赖:yunze-public → internal/pub/(10 包 18 文件),
  yunze-pb/pb → internal/pb/(9 文件);Consul 注册改用官方
  gogf/gf/contrib/registry/consul/v2(API 兼容)
- Go module:yz-svr → git.5iot.com/iotlcm-public/iotlcm-upstream-chan
- 修复 mingqian 包测试:实现重构后 `adapter.operations` 字段已移除,
  测试断言按当前实现语义重写(不改产品行为)
- 凭据与内部标识脱敏:MySQL/Redis/Consul 凭据、私网与生产 IP、
  内网域名、真实手机号、注释里的真实 token/transid
- 移除内部资产:CI 触发器、生产通道种子 SQL、真实凭据测试资源文档
- Dockerfile 改用公共基础镜像,移除写死的 registry 凭据
- 补 Apache-2.0 LICENSE,重写 README,LF 行尾 + .gitattributes
2026-10-22 16:08:06 +08:00

252 lines
12 KiB
Go

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
}}
}