package app import ( "context" "encoding/json" "fmt" "math" "strconv" "strings" "sync" "time" "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" "git.5iot.com/iotlcm-public/iotlcm-app/utility" "git.5iot.com/iotlcm-public/iotlcm-app/utility/uuid32" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "golang.org/x/sync/singleflight" ) type sAppDiagnosis struct{} const ( appDiagnosisRegionCacheTTL = time.Hour appDiagnosisRegionLockTTL = 15 * time.Second appDiagnosisRegionLockWait = 12 * time.Second appDiagnosisRegionLockPoll = 100 * time.Millisecond appOpenDiagnosisRateWindow = time.Minute appOpenDiagnosisRateLimit = 20 appDiagnosisNotIncludedMessage = "该卡暂未纳入服务" ) var appDiagnosisRegionFlight singleflight.Group type appDiagnosisUpdateFunc func(func()) type appDiagnosisSetModuleFunc func(key string, status string, message string, cached bool, updatedAt string) type appDiagnosisRegionCachePayload struct { Item appUpstreamSimQueryItem `json:"item"` CachedAt string `json:"cachedAt"` } type appDiagnosisRegionQueryResult struct { Item *appUpstreamSimQueryItem CachedAt string Cached bool } func NewAppDiagnosis() *sAppDiagnosis { return &sAppDiagnosis{} } func init() { service.RegisterAppDiagnosis(NewAppDiagnosis()) } // Info 查询登录用户可见的SIM卡诊断信息,并保留完整会话字段。 func (s *sAppDiagnosis) Info(ctx context.Context, param *model.AppDiagnosisInfoParam) (res *model.AppDiagnosisInfoModel, err error) { if param == nil { param = &model.AppDiagnosisInfoParam{} } user, cardLogic, row, err := s.resolveDiagnosisCard(ctx, param.QueryCode) if err != nil { return nil, err } cardModel := cardLogic.cardRowToModel(ctx, row, user, cardLogic.ownerType(ctx, user), true) res, err = s.buildDiagnosisInfo(ctx, row, cardModel) if err != nil { return nil, err } if row.Id == 0 { return sanitizeOpenDiagnosisInfo(res), nil } return res, nil } func (s *sAppDiagnosis) OpenInfo(ctx context.Context, param *model.AppOpenDiagnosisInfoParam) (res *model.AppDiagnosisInfoModel, err error) { if param == nil { param = &model.AppOpenDiagnosisInfoParam{} } if err = appEnsureOpenDiagnosisRateLimit(ctx); err != nil { return nil, err } appAuditOpenDiagnosis(ctx, "info", param.QueryCode, "") cardLogic, row, err := s.resolveOpenDiagnosisCard(ctx, param.TenantId, param.QueryCode) if err != nil { return nil, err } res, err = s.buildDiagnosisInfo(ctx, row, cardLogic.publicCardRowToModel(ctx, row, param.QueryCode)) if err != nil { return nil, err } return sanitizeOpenDiagnosisInfo(res), nil } func (s *sAppDiagnosis) buildDiagnosisInfo(ctx context.Context, row *appCardQueryRow, cardModel *model.AppCardModel) (res *model.AppDiagnosisInfoModel, err error) { if row == nil { return nil, gerror.New("卡片信息为空,不能诊断") } ctx = context.WithValue(ctx, appDiagnosisProxyContextKey{}, true) simKey, err := s.prepareDiagnosisSimKey(ctx, row) if err != nil { return nil, err } if cardModel != nil { if strings.TrimSpace(row.Iccid) != "" { cardModel.Iccid = strings.TrimSpace(row.Iccid) } if strings.TrimSpace(row.Imsi) != "" { cardModel.Imsi = strings.TrimSpace(row.Imsi) } if strings.TrimSpace(row.Msisdn) != "" { cardModel.Msisdn = strings.TrimSpace(row.Msisdn) } cardModel.UsedFlowMb = 0 cardModel.RemainFlowMb = 0 cardModel.UsedFlowPercent = 0 } res = &model.AppDiagnosisInfoModel{ Info: &model.AppDiagnosisInfoItem{ Iccid: appDiagnosisStringPtr(row.Iccid), Imsi: appDiagnosisStringPtr(row.Imsi), Msisdn: appDiagnosisStringPtr(row.Msisdn), }, Session: &model.AppDiagnosisSessionItem{}, Modules: make(map[string]*model.AppDiagnosisModuleItem), Card: cardModel, } res.Info.DataServiceSource = "apn" if appDiagnosisIsTelecomChannel(row.TempKey, row.ChanKey) { res.Info.DataServiceSource = "network_block" } appSetDiagnosisModule(res, "card", "success", "", false, "") var mu sync.Mutex updateDiagnosis := func(fn func()) { mu.Lock() defer mu.Unlock() fn() } setModule := func(key string, status string, message string, cached bool, updatedAt string) { updateDiagnosis(func() { appSetDiagnosisModule(res, key, status, message, cached, updatedAt) if key == "cardStatus" && res.Info.DataServiceSource == "network_block" && res.Info.NetBlockStatus == nil && status != "success" && status != "cached" { appSetDiagnosisModule(res, "dataService", status, message, cached, updatedAt) } }) } runDiagnose := func(wg *sync.WaitGroup, fn func()) { wg.Add(1) go func() { defer wg.Done() fn() }() } var wg sync.WaitGroup runDiagnose(&wg, func() { s.attachUpstreamDataService(ctx, simKey, row, res, updateDiagnosis, setModule) }) runDiagnose(&wg, func() { s.attachUpstreamBasicInfo(ctx, simKey, row, res, updateDiagnosis, setModule) }) runDiagnose(&wg, func() { s.attachUpstreamCardStatus(ctx, simKey, res, updateDiagnosis, setModule) }) runDiagnose(&wg, func() { s.attachUpstreamSession(ctx, simKey, row, res, updateDiagnosis, setModule) }) runDiagnose(&wg, func() { s.attachUpstreamLatestImei(ctx, simKey, res, updateDiagnosis, setModule) }) runDiagnose(&wg, func() { s.attachUpstreamDeviceStatus(ctx, simKey, row, res, updateDiagnosis, setModule) }) runDiagnose(&wg, func() { s.attachUpstreamBindStatus(ctx, simKey, res, updateDiagnosis, setModule) }) runDiagnose(&wg, func() { s.attachUpstreamRegionLimitStatus(ctx, simKey, row.TempKey, row.ChanKey, res, updateDiagnosis, setModule) }) runDiagnose(&wg, func() { s.attachUpstreamRateLimitStatus(ctx, simKey, res, updateDiagnosis, setModule) }) runDiagnose(&wg, func() { s.attachUpstreamStopReason(ctx, simKey, res, updateDiagnosis, setModule) }) runDiagnose(&wg, func() { s.attachUpstreamFrozenState(ctx, simKey, res, updateDiagnosis, setModule) }) runDiagnose(&wg, func() { s.attachUpstreamCurrentFlowUsage(ctx, simKey, row, res, updateDiagnosis, setModule) }) wg.Wait() // Online status is determined solely by the realtime session. The data service // status is independent and must not make an online session appear offline. sessionStatus := appDiagnosisSessionStatus(appDiagnosisPtrValue(res.Session.Status)) if res.Session.Status == nil { res.Info.OnlineStatus = nil if res.Card != nil { res.Card.OnlineStatus = "" } } else { res.Info.OnlineStatus = appDiagnosisOnlineStatus(sessionStatus) if res.Card != nil { // AppCardModel uses the upstream session status convention: // 0 means online and -1 means offline. res.Card.OnlineStatus = sessionStatus } } return res, nil } // Flow 查询登录用户可见SIM卡的流量趋势。 func (s *sAppDiagnosis) Flow(ctx context.Context, param *model.AppDiagnosisFlowParam) (res *model.AppDiagnosisFlowModel, err error) { if param == nil { param = &model.AppDiagnosisFlowParam{} } _, _, row, err := s.resolveDiagnosisCard(ctx, param.QueryCode) if err != nil { return nil, err } return s.queryDiagnosisFlowByCard(ctx, row, param.QueryType) } func (s *sAppDiagnosis) OpenFlow(ctx context.Context, param *model.AppOpenDiagnosisFlowParam) (res *model.AppDiagnosisFlowModel, err error) { if param == nil { param = &model.AppOpenDiagnosisFlowParam{} } if err = appEnsureOpenDiagnosisRateLimit(ctx); err != nil { return nil, err } appAuditOpenDiagnosis(ctx, "flow", param.QueryCode, param.QueryType) _, row, err := s.resolveOpenDiagnosisCard(ctx, param.TenantId, param.QueryCode) if err != nil { return nil, err } return s.queryDiagnosisFlowByCard(ctx, row, param.QueryType) } func appEnsureOpenDiagnosisRateLimit(ctx context.Context) error { clientIp := strings.TrimSpace(utility.GetClientIp(ctx)) if clientIp == "" { clientIp = "unknown" } key := fmt.Sprintf("iotlcm:app:open-diagnosis:rate:%s", appDiagnosisSafeCachePart(clientIp)) value, err := g.Redis().Do(ctx, "INCR", key) if err != nil { g.Log().Warningf(ctx, "open diagnosis rate limit unavailable, key:%s, err:%+v", key, err) return nil } count := value.Int() if count == 1 { if _, err = g.Redis().Do(ctx, "EXPIRE", key, int(appOpenDiagnosisRateWindow.Seconds())); err != nil { g.Log().Warningf(ctx, "open diagnosis rate limit expiry failed, key:%s, err:%+v", key, err) } } if count > appOpenDiagnosisRateLimit { return gerror.New("公开诊断请求过于频繁,请稍后再试") } return nil } func appAuditOpenDiagnosis(ctx context.Context, kind string, queryCode string, queryType string) { g.Log().Infof(ctx, "open diagnosis query, kind:%s, queryType:%s, query:%s, clientIp:%s", kind, queryType, appDiagnosisMaskIdentifier(queryCode), utility.GetClientIp(ctx)) } func appDiagnosisMaskIdentifier(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } if len(value) <= 4 { return "****" } if len(value) <= 8 { return value[:2] + "****" + value[len(value)-2:] } return value[:4] + "****" + value[len(value)-4:] } func sanitizeOpenDiagnosisInfo(res *model.AppDiagnosisInfoModel) *model.AppDiagnosisInfoModel { if res == nil { return nil } if res.Info != nil { res.Info.Iccid = appDiagnosisMaskedStringPtr(res.Info.Iccid) res.Info.Imsi = nil res.Info.Msisdn = appDiagnosisMaskedStringPtr(res.Info.Msisdn) res.Info.RateLimitApnDetails = nil } if res.Session != nil { res.Session.ApnId = nil res.Session.Ipv4 = nil res.Session.Ipv6 = nil res.Session.Ipv6Prefix = nil } if res.Card != nil { res.Card.Iccid = appDiagnosisMaskIdentifier(res.Card.Iccid) res.Card.Msisdn = appDiagnosisMaskIdentifier(res.Card.Msisdn) res.Card.Imsi = "" res.Card.ApnName = "" res.Card.ApnId = "" res.Card.Ipv4 = "" res.Card.Ipv6 = "" res.Card.Ipv6Prefix = "" res.Card.Remark = "" } return res } func appDiagnosisMaskedStringPtr(value *string) *string { if value == nil { return nil } return appDiagnosisStringPtr(appDiagnosisMaskIdentifier(*value)) } func (s *sAppDiagnosis) queryDiagnosisFlowByCard(ctx context.Context, row *appCardQueryRow, queryType string) (*model.AppDiagnosisFlowModel, error) { switch strings.TrimSpace(queryType) { case "7d": return s.queryDiagnosisDailyFlow(ctx, row.Iccid, 7) case "30d": return s.queryDiagnosisDailyFlow(ctx, row.Iccid, 30) case "3M": return s.queryDiagnosisMonthlyFlow(ctx, row.Iccid, 3) case "6M": return s.queryDiagnosisMonthlyFlow(ctx, row.Iccid, 6) default: return nil, gerror.New("查询类型只能为7d、30d、3M或6M") } } func (s *sAppDiagnosis) resolveDiagnosisCard(ctx context.Context, queryCode string) (*entity.SysUser, *sAppCard, *appCardQueryRow, error) { queryCode = strings.TrimSpace(queryCode) if queryCode == "" { return nil, nil, nil, gerror.New("请输入ICCID、MSISDN、IMSI或卡号") } user, err := appCurrentUser(ctx) if err != nil { return nil, nil, nil, err } cardLogic := NewAppCard() row, err := cardLogic.findDiagnosisCard(ctx, user, queryCode) if err != nil { return nil, nil, nil, gerror.Wrap(err, "查询诊断卡片失败") } if row == nil || row.Id == 0 { openRow, openErr := cardLogic.findOpenCard(ctx, "", queryCode) if openErr != nil { return nil, nil, nil, gerror.Wrap(openErr, "查询诊断卡片失败") } if openRow != nil && openRow.Id > 0 { return nil, nil, nil, gerror.New(appDiagnosisNotIncludedMessage) } row, err = s.resolveAssocOnlyDiagnosisCard(ctx, cardLogic, user.TenantId, queryCode, "查询诊断卡片") if err != nil { return nil, nil, nil, err } } if row == nil { return nil, nil, nil, gerror.New(appDiagnosisNotIncludedMessage) } if err = s.resolveDiagnosisCardChannel(ctx, cardLogic, row, queryCode, "查询诊断卡片"); err != nil { return nil, nil, nil, err } return user, cardLogic, row, nil } func (s *sAppDiagnosis) resolveOpenDiagnosisCard(ctx context.Context, tenantId string, queryCode string) (*sAppCard, *appCardQueryRow, error) { queryCode = strings.TrimSpace(queryCode) if queryCode == "" { return nil, nil, gerror.New("请输入ICCID、MSISDN、IMSI或卡号") } cardLogic := NewAppCard() tenantId = strings.TrimSpace(tenantId) row, err := cardLogic.findOpenCard(ctx, tenantId, queryCode) if err != nil { return nil, nil, gerror.Wrap(err, "查询公开诊断卡片失败") } if row == nil || row.Id == 0 { row, err = s.resolveAssocOnlyDiagnosisCard(ctx, cardLogic, tenantId, queryCode, "查询公开诊断卡片") if err != nil { return nil, nil, err } } if row == nil { return nil, nil, gerror.New(appDiagnosisNotIncludedMessage) } if err = s.resolveDiagnosisCardChannel(ctx, cardLogic, row, queryCode, "查询公开诊断卡片"); err != nil { return nil, nil, err } return cardLogic, row, nil } func (s *sAppDiagnosis) resolveAssocOnlyDiagnosisCard(ctx context.Context, cardLogic *sAppCard, tenantId string, queryCode string, operation string) (*appCardQueryRow, error) { info, err := cardLogic.findDiagnosisAssocChannel(ctx, tenantId, queryCode) if err != nil { return nil, gerror.Wrap(err, operation+"失败") } if info != nil { if err = validateDiagnosisCardChannelInfo(info); err != nil { return nil, err } return &appCardQueryRow{ TSimCardInfo: entity.TSimCardInfo{TenantId: tenantId, Iccid: queryCode, ChanKey: info.ChanKey}, ChanName: info.ChanName, ChanAlias: info.ChanAlias, TempKey: info.TempKey, }, nil } card, err := queryLegacyDiagnosisCard(ctx, queryCode) if err != nil { return nil, gerror.Wrap(err, operation+"失败") } if card == nil { return nil, nil } return &appCardQueryRow{ TSimCardInfo: entity.TSimCardInfo{ TenantId: tenantId, Iccid: card.ICCID, Imsi: card.IMSI, Msisdn: card.MSISDN, }, }, nil } func (s *sAppDiagnosis) resolveDiagnosisCardChannel(ctx context.Context, cardLogic *sAppCard, row *appCardQueryRow, queryCode string, operation string) error { if cardLogic == nil || row == nil { return gerror.New(appDiagnosisNotIncludedMessage) } assocInfo, assocErr := cardLogic.findDiagnosisAssocChannel(ctx, row.TenantId, row.Iccid) if assocErr != nil { return gerror.Wrap(assocErr, operation+"失败") } if assocInfo != nil { if err := validateDiagnosisCardChannelInfo(assocInfo); err != nil { return err } row.ChanKey = assocInfo.ChanKey row.ChanName = assocInfo.ChanName row.ChanAlias = assocInfo.ChanAlias row.TempKey = assocInfo.TempKey return nil } if row.Id > 0 && strings.TrimSpace(row.ChanKey) != "" { info, err := cardLogic.findLegacyCardChannelInfo(ctx, row.TenantId, row.ChanKey) if err != nil { return gerror.Wrap(err, operation+"失败") } if validateDiagnosisCardChannelInfo(info) == nil { row.ChanKey = info.ChanKey row.ChanName = info.ChanName row.ChanAlias = info.ChanAlias row.TempKey = info.TempKey return nil } } lookupCode := strings.TrimSpace(row.Iccid) if lookupCode == "" { lookupCode = strings.TrimSpace(queryCode) } card, err := queryLegacyDiagnosisCard(ctx, lookupCode) if err != nil { return gerror.Wrap(err, operation+"失败") } if card == nil || (row.Iccid != "" && card.ICCID != row.Iccid) { return gerror.New(appDiagnosisNotIncludedMessage) } info, err := cardLogic.findLegacyCardChannelInfo(ctx, row.TenantId, card.ChannelKey) if err != nil { return gerror.Wrap(err, operation+"失败") } if err = validateDiagnosisCardChannelInfo(info); err != nil { return err } row.Iccid = card.ICCID row.Imsi = card.IMSI row.Msisdn = card.MSISDN row.ChanKey = info.ChanKey row.ChanName = info.ChanName row.ChanAlias = info.ChanAlias row.TempKey = info.TempKey return nil } func validateDiagnosisCardChannelInfo(info *appCardChannelInfo) error { if info == nil || strings.TrimSpace(info.ChanKey) == "" || info.ChanStatus != 0 { return gerror.New(appDiagnosisNotIncludedMessage) } return nil } func (s *sAppDiagnosis) prepareDiagnosisSimKey(ctx context.Context, row *appCardQueryRow) (*appUpstreamSimKey, error) { simKey, err := appUpstreamSimKeyFromRow(row) if err != nil { return nil, err } if row.Id > 0 && strings.TrimSpace(simKey.MSISDN) != "" && strings.TrimSpace(simKey.IMSI) != "" { return simKey, nil } threeCodes, err := s.resolveDiagnosisThreeCodes(ctx, simKey) if err != nil { g.Log().Warningf(ctx, "app diagnosis resolve three codes failed, iccid:%s, chanKey:%s, err:%+v", simKey.ICCID, simKey.ChanKey, err) if row.Id == 0 { return nil, err } return simKey, nil } if threeCodes == nil { return simKey, nil } if value := strings.TrimSpace(threeCodes.ICCID); value != "" { simKey.ICCID = value row.Iccid = value } if value := strings.TrimSpace(threeCodes.IMSI); value != "" { simKey.IMSI = value row.Imsi = value } if value := strings.TrimSpace(threeCodes.MSISDN); value != "" { simKey.MSISDN = value row.Msisdn = value } return simKey, nil } func (s *sAppDiagnosis) resolveDiagnosisThreeCodes(ctx context.Context, simKey *appUpstreamSimKey) (*appUpstreamSimKey, error) { items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-basic-info", map[string]any{}) if err != nil { return nil, err } item := appDiagnosisFirstAvailableItem(items) if item == nil || item.Code == 5000 || appDiagnosisIsUnsupportedMessage(item.Message) { return nil, nil } if item.Code != 0 { return nil, gerror.New(appDiagnosisModuleMessage(item, "上游基础信息查询失败")) } threeCodes := &appUpstreamSimKey{ChanKey: strings.TrimSpace(simKey.ChanKey)} if item.SimKey != nil { threeCodes.ICCID = strings.TrimSpace(item.SimKey.ICCID) threeCodes.IMSI = strings.TrimSpace(item.SimKey.IMSI) threeCodes.MSISDN = strings.TrimSpace(item.SimKey.MSISDN) } if value := appDiagnosisRawString(item.ResData, "iccid", "ICCID"); value != "" { threeCodes.ICCID = value } if value := appDiagnosisRawString(item.ResData, "imsi", "IMSI"); value != "" { threeCodes.IMSI = value } if value := appDiagnosisRawString(item.ResData, "msisdn", "MSISDN"); value != "" { threeCodes.MSISDN = value } if threeCodes.ICCID == "" && threeCodes.IMSI == "" && threeCodes.MSISDN == "" { return nil, nil } return threeCodes, nil } func (s *sAppDiagnosis) attachUpstreamBasicInfo(ctx context.Context, simKey *appUpstreamSimKey, row *appCardQueryRow, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) { items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-basic-info", map[string]any{}) if err != nil { g.Log().Warningf(ctx, "app diagnosis query sim basic info failed, iccid:%s, err:%+v", row.Iccid, err) setModule("basicInfo", "error", err.Error(), false, "") return } item := appDiagnosisFirstAvailableItem(items) if item == nil { setModule("basicInfo", "skipped", "上游基础信息未返回有效结果", false, "") return } if item.Code != 0 { if item.Code == 5000 { setModule("basicInfo", "skipped", appDiagnosisModuleMessage(item, "上游基础信息暂不支持"), false, "") } else { status, message := appDiagnosisUpstreamFailure("上游基础信息查询失败", item.Code, item.Message) setModule("basicInfo", status, message, false, "") } return } update(func() { if value := appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "iccid", "ICCID")); value != nil { res.Info.Iccid = value } if value := appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "imsi", "IMSI")); value != nil { res.Info.Imsi = value } if value := appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "msisdn", "MSISDN")); value != nil { res.Info.Msisdn = value } if value := appDiagnosisRawString(item.ResData, "imei", "IMEI"); value != "" && res.Card != nil { res.Card.Imei = value } if res.Info.RegionLimitType == nil { res.Info.RegionLimitType = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "areaLimitType", "area_limit_type")) } if res.Info.RegionLimitArea == nil { res.Info.RegionLimitArea = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "areaLimitInfo", "area_limit_info")) } rawHumanLimitStatus := appDiagnosisRawString(item.ResData, "isHumanLimit", "is_human_limit") if rawHumanLimitStatus != "" { humanLimitStatus := appDiagnosisHumanLimitStatus(rawHumanLimitStatus) if humanLimitStatus != nil && *humanLimitStatus != "3" { res.Info.HumanLimitStatus = humanLimitStatus } } }) setModule("basicInfo", "success", "", false, "") } func (s *sAppDiagnosis) attachUpstreamDataService(ctx context.Context, simKey *appUpstreamSimKey, row *appCardQueryRow, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) { if appDiagnosisIsTelecomChannel(row.TempKey, row.ChanKey) { return } items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-apn-info", map[string]any{}) if err != nil { g.Log().Warningf(ctx, "app diagnosis query sim data service failed, iccid:%s, err:%v", simKey.ICCID, err) setModule("dataService", "error", err.Error(), false, "") return } dataStatus := appDiagnosisDataServiceStatusFromItemsForChannel(items, row.TempKey, row.ChanKey) if dataStatus != nil { update(func() { res.Info.DataStatus = dataStatus if res.Card != nil { if value, err := strconv.Atoi(*dataStatus); err == nil { res.Card.DataServiceStatus = value } } }) setModule("dataService", "success", "", false, "") return } item := appDiagnosisFirstAvailableItem(items) if item == nil { setModule("dataService", "skipped", "上游数据服务未返回有效结果", false, "") return } if item.Code == 5000 { setModule("dataService", "skipped", appDiagnosisModuleMessage(item, "上游数据服务暂不支持"), false, "") return } if item.Code != 0 { status, message := appDiagnosisUpstreamFailure("上游数据服务查询失败", item.Code, item.Message) setModule("dataService", status, message, false, "") return } setModule("dataService", "skipped", "上游数据服务未返回有效状态", false, "") } func (s *sAppDiagnosis) attachUpstreamCardStatus(ctx context.Context, simKey *appUpstreamSimKey, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) { items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-status", map[string]any{}) if err != nil { g.Log().Warningf(ctx, "app diagnosis query sim card status failed, iccid:%s, err:%+v", simKey.ICCID, err) setModule("cardStatus", "error", err.Error(), false, "") return } item := appDiagnosisFirstAvailableItem(items) if item == nil { setModule("cardStatus", "skipped", "上游卡状态未返回有效结果", false, "") return } if item.Code == 5000 { setModule("cardStatus", "skipped", appDiagnosisModuleMessage(item, "上游卡状态暂不支持"), false, "") return } if item.Code != 0 { status, message := appDiagnosisUpstreamFailure("上游卡状态查询失败", item.Code, item.Message) setModule("cardStatus", status, message, false, "") return } if res.Info.DataServiceSource == "network_block" { update(func() { if appDiagnosisApplyNetworkBlock(res.Info, item.ResData) { appSetDiagnosisModule(res, "dataService", "success", "", false, "") } else { appSetDiagnosisModule(res, "dataService", "skipped", "电信未返回有效断网状态", false, "") } }) } status := appDiagnosisCardStatus(appDiagnosisRawString(item.ResData, "cardStatus", "card_status", "simStatus", "sim_status", "status")) if status == nil { setModule("cardStatus", "skipped", "上游卡状态未返回有效结果", false, "") return } update(func() { res.Info.Status = status if res.Card != nil { if value, err := strconv.Atoi(*status); err == nil { res.Card.BossCardStatus = value } } appSetDiagnosisModule(res, "cardStatus", "success", "", false, "") }) } func (s *sAppDiagnosis) attachUpstreamSession(ctx context.Context, simKey *appUpstreamSimKey, row *appCardQueryRow, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) { items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-session", nil) if err != nil { setModule("session", "error", err.Error(), false, "") return } item := appDiagnosisFirstAvailableItem(items) if item == nil { setModule("session", "skipped", "上游会话信息未返回有效结果", false, "") return } if item.Code != 0 { status, message := appDiagnosisUpstreamFailure("上游会话查询失败", item.Code, item.Message) setModule("session", status, message, false, "") return } session := &appOneLinkMockSimSessionItem{} if err := json.Unmarshal(appDiagnosisUnwrapResData(item.ResData), session); err != nil { setModule("session", "error", "会话响应格式无效", false, "") return } appNormalizeUpstreamSession(session, item.ResData) if session.isZero() { setModule("session", "skipped", "上游会话信息未返回有效结果", false, "") return } update(func() { sessionStatus := appDiagnosisSessionStatus(session.Status) res.Info.OnlineStatus = appDiagnosisOnlineStatus(sessionStatus) res.Session.ApnId = appDiagnosisStringPtr(session.ApnId) res.Session.Rat = appDiagnosisStringPtr(session.Rat) res.Session.CreateTime = appDiagnosisStringPtr(session.CreateTime) res.Session.Ipv4 = appDiagnosisStringPtr(session.Ipv4) res.Session.Ipv6 = appDiagnosisStringPtr(session.Ipv6) res.Session.Ipv6Prefix = appDiagnosisStringPtr(session.Ipv6Prefix) res.Session.Status = appDiagnosisStringPtr(sessionStatus) }) setModule("session", "success", "", false, "") } func (s *sAppDiagnosis) attachUpstreamLatestImei(ctx context.Context, simKey *appUpstreamSimKey, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) { items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-imei", nil) if err != nil { setModule("imei", "error", err.Error(), false, "") return } item := appDiagnosisFirstAvailableItem(items) if item == nil { setModule("imei", "skipped", "上游IMEI未返回有效结果", false, "") return } if item.Code != 0 { status, message := appDiagnosisUpstreamFailure("上游IMEI查询失败", item.Code, item.Message) setModule("imei", status, message, false, "") return } imei := appDiagnosisLatestImei(items) if imei == "" { setModule("imei", "skipped", "上游IMEI未返回有效结果", false, "") return } update(func() { res.Session.Imei = appDiagnosisStringPtr(imei) }) setModule("imei", "success", "", false, "") } func appDiagnosisLatestImei(items []appUpstreamSimQueryItem) string { for _, item := range items { if item.Code != 0 { continue } if imei := appDiagnosisRawString(item.ResData, "imei", "IMEI"); imei != "" { return imei } } return "" } func (s *sAppDiagnosis) attachUpstreamDeviceStatus(ctx context.Context, simKey *appUpstreamSimKey, row *appCardQueryRow, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) { items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-device-status", map[string]any{}) if err != nil { g.Log().Warningf(ctx, "app diagnosis query sim device status failed, iccid:%s, err:%+v", row.Iccid, err) setModule("device", "error", err.Error(), false, "") return } item := appDiagnosisFirstAvailableItem(items) if item == nil { setModule("device", "skipped", "上游设备状态未返回有效结果", false, "") return } if item.Code == 5000 { update(func() { res.Info.DeviceStatus = nil }) setModule("device", "skipped", appDiagnosisModuleMessage(item, "上游设备状态暂不支持"), false, "") return } if item.Code != 0 { status, message := appDiagnosisUpstreamFailure("上游设备状态查询失败", item.Code, item.Message) setModule("device", status, message, false, "") return } if appDiagnosisDeviceStatus(appDiagnosisRawString(item.ResData, "deviceStatus", "device_status")) == nil && appDiagnosisOperatingStatus(appDiagnosisRawString(item.ResData, "operatingStatus", "operating_status")) == nil { setModule("device", "skipped", "上游设备状态未返回有效结果", false, "") return } update(func() { res.Info.DeviceStatus = appDiagnosisDeviceStatus(appDiagnosisRawString(item.ResData, "deviceStatus", "device_status")) res.Info.OperatingStatus = appDiagnosisOperatingStatus(appDiagnosisRawString(item.ResData, "operatingStatus", "operating_status")) }) setModule("device", "success", "", false, "") } func (s *sAppDiagnosis) attachUpstreamBindStatus(ctx context.Context, simKey *appUpstreamSimKey, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) { items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-card-bind-status", map[string]any{"testType": 0}) if err != nil { g.Log().Warningf(ctx, "app diagnosis query sim bind status failed, iccid:%s, err:%+v", simKey.ICCID, err) setModule("cardBind", "error", err.Error(), false, "") return } item := appDiagnosisFirstAvailableItem(items) if item == nil { setModule("cardBind", "skipped", "上游机卡绑定状态未返回有效结果", false, "") return } if item.Code == 5000 { update(func() { res.Info.ImeiStatus = nil }) setModule("cardBind", "skipped", appDiagnosisModuleMessage(item, "上游机卡绑定状态暂不支持"), false, "") return } if item.Code != 0 { g.Log().Warningf(ctx, "app diagnosis sim bind status returned error, iccid:%s, code:%d, message:%s", simKey.ICCID, item.Code, item.Message) status, message := appDiagnosisUpstreamFailure("上游机卡绑定状态查询失败", item.Code, item.Message) setModule("cardBind", status, message, false, "") return } if appDiagnosisRawString(item.ResData, "orderStatus", "order_status") == "1" { setModule("cardBind", "skipped", "该卡未订购机卡绑定服务", false, "") return } if appDiagnosisRawString(item.ResData, "bindStatus", "bind_status", "cardBindStatus") == "2" { message := appDiagnosisRawString(item.ResData, "errorDes", "error_des", "errorCode", "error_code") if message == "" { message = "上游机卡绑定状态查询失败" } setModule("cardBind", "error", message, false, "") return } if appDiagnosisRawString(item.ResData, "bindStatus", "bind_status", "cardBindStatus") == "-" { setModule("cardBind", "error", "上游机卡绑定网络查询失败", false, "") return } if appDiagnosisRawString(item.ResData, "bindStatus", "bind_status", "cardBindStatus", "cardBindingType", "card_binding_type") == "" { setModule("cardBind", "skipped", "上游机卡绑定状态未返回有效结果", false, "") return } bindStatus := appDiagnosisBindStatus( appDiagnosisRawString(item.ResData, "bindStatus", "bind_status", "cardBindStatus"), appDiagnosisRawString(item.ResData, "cardBindingType", "card_binding_type"), ) update(func() { res.Info.ImeiStatus = appDiagnosisImeiStatus(bindStatus) }) setModule("cardBind", "success", "", false, "") } func (s *sAppDiagnosis) attachUpstreamRegionLimitStatus(ctx context.Context, simKey *appUpstreamSimKey, tempKey string, chanKey string, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) { result, err := appQueryDiagnosisRegionLimit(ctx, simKey, appDiagnosisShouldCacheRegionLimit(tempKey, chanKey)) if err != nil { g.Log().Warningf(ctx, "app diagnosis query sim region limit status failed, iccid:%s, err:%+v", simKey.ICCID, err) setModule("region", "error", err.Error(), false, "") return } if result == nil { setModule("region", "skipped", "上游区域限制未返回有效结果", false, "") return } item := result.Item if item == nil { setModule("region", "skipped", "上游区域限制未返回有效结果", result.Cached, result.CachedAt) return } update(func() { appDiagnosisApplyRegionLimit(res, item) }) if result.Cached { status, message := appCachedDiagnosisRegionModule(item) setModule("region", status, message, true, result.CachedAt) return } if appDiagnosisRegionLimitHasData(item) { message := "" if item.Code != 0 { message = appDiagnosisModuleMessage(item, "区域限制查询已返回有效结果") } setModule("region", "success", message, false, result.CachedAt) return } if item.Code == 5000 { setModule("region", "skipped", appDiagnosisModuleMessage(item, "上游区域限制暂不支持"), false, "") return } if item.Code != 0 { status, message := appDiagnosisUpstreamFailure("上游区域限制查询失败", item.Code, item.Message) setModule("region", status, message, false, "") return } setModule("region", "error", "上游区域限制未返回有效数据", false, "") } func (s *sAppDiagnosis) attachUpstreamRateLimitStatus(ctx context.Context, simKey *appUpstreamSimKey, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) { items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-data-diagnosis", map[string]any{}) if err != nil { g.Log().Warningf(ctx, "app diagnosis query sim data diagnosis failed, iccid:%s, err:%+v", simKey.ICCID, err) setModule("rateLimit", "error", err.Error(), false, "") return } item := appDiagnosisFirstAvailableItem(items) if item == nil { setModule("rateLimit", "skipped", "上游达量限速未返回有效结果", false, "") return } if item.Code == 0 { item.ResData = appDiagnosisNormalizeRateLimitBody(item.ResData) } if item.Code == 0 && appDiagnosisRawString(item.ResData, "rateLimitStatus", "rate_limit_status", "rateLimitReason", "rateLimitDetail") == "" && len(appDiagnosisRateLimitApnDetails(item.ResData)) == 0 { setModule("rateLimit", "skipped", "上游达量限速未返回有效结果", false, "") return } update(func() { appDiagnosisApplyRateLimit(res, item) }) if item.Code == 5000 { setModule("rateLimit", "skipped", appDiagnosisModuleMessage(item, "上游达量限速暂不支持"), false, "") return } if item.Code != 0 { status, message := appDiagnosisUpstreamFailure("上游达量限速查询失败", item.Code, item.Message) setModule("rateLimit", status, message, false, "") return } setModule("rateLimit", "success", "", false, "") } func (s *sAppDiagnosis) attachUpstreamStopReason(ctx context.Context, simKey *appUpstreamSimKey, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) { items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-stop-reason", map[string]any{}) if err != nil { g.Log().Warningf(ctx, "app diagnosis query sim stop reason failed, iccid:%s, err:%+v", simKey.ICCID, err) setModule("stopReason", "error", err.Error(), false, "") return } item := appDiagnosisFirstAvailableItem(items) if item == nil { setModule("stopReason", "skipped", "上游停机原因未返回有效结果", false, "") return } if item.Code == 5000 { setModule("stopReason", "skipped", appDiagnosisModuleMessage(item, "上游停机原因暂不支持"), false, "") return } if item.Code != 0 { status, message := appDiagnosisUpstreamFailure("上游停机原因查询失败", item.Code, item.Message) setModule("stopReason", status, message, false, "") return } update(func() { res.Info.StopReason = appDiagnosisStringPtr(appDiagnosisNormalizeStopReason(appDiagnosisRawString(item.ResData, "stopReason", "stop_reason"))) }) setModule("stopReason", "success", "", false, "") } func (s *sAppDiagnosis) attachUpstreamFrozenState(ctx context.Context, simKey *appUpstreamSimKey, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) { items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-frozen-state", map[string]any{}) if err != nil { g.Log().Warningf(ctx, "app diagnosis query sim frozen state failed, iccid:%s, err:%+v", simKey.ICCID, err) setModule("frozen", "error", err.Error(), false, "") return } item := appDiagnosisFirstAvailableItem(items) if item == nil { setModule("frozen", "skipped", "上游冻结状态未返回有效结果", false, "") return } if item.Code == 5000 { setModule("frozen", "skipped", appDiagnosisModuleMessage(item, "上游冻结状态暂不支持"), false, "") return } if item.Code != 0 { status, message := appDiagnosisUpstreamFailure("上游冻结状态查询失败", item.Code, item.Message) setModule("frozen", status, message, false, "") return } if appDiagnosisRawString(item.ResData, "frozenStatus", "frozen_status") == "" { setModule("frozen", "skipped", "上游冻结状态未返回有效结果", false, "") return } update(func() { res.Info.FrozenStatus = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "frozenStatus", "frozen_status")) res.Info.FrozenReason = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "frozenReason", "frozen_reason")) res.Info.FrozenOperTime = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "operTime", "oper_time")) }) setModule("frozen", "success", "", false, "") } func (s *sAppDiagnosis) attachUpstreamCurrentFlowUsage(ctx context.Context, simKey *appUpstreamSimKey, row *appCardQueryRow, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) { items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-datausage", map[string]any{}) if err != nil { g.Log().Warningf(ctx, "app diagnosis query current flow usage failed, iccid:%s, err:%+v", simKey.ICCID, err) setModule("flowUsage", "error", "", false, "") return } item := appDiagnosisFirstAvailableItem(items) if item == nil { setModule("flowUsage", "skipped", "上游实时流量未返回有效结果", false, "") return } if item.Code == 5000 { setModule("flowUsage", "skipped", appDiagnosisModuleMessage(item, "上游实时流量暂不支持"), false, "") return } if item.Code != 0 { status, message := appDiagnosisUpstreamFailure("上游实时流量查询失败", item.Code, item.Message) setModule("flowUsage", status, message, false, "") return } usedFlow, ok := appDiagnosisRawFloat(item.ResData, "dataUsage", "data_usage") if !ok { setModule("flowUsage", "skipped", "上游实时流量未返回有效结果", false, "") return } if usedFlow < 0 || math.IsNaN(usedFlow) || math.IsInf(usedFlow, 0) { setModule("flowUsage", "error", "实时流量未返回有效数值", false, "") return } update(func() { now := gtime.Now() nowText := now.Format("Y-m-d H:i:s") totalFlow := float64(0) if res.Package != nil && res.Package.PackageTotalFlowMb != nil { totalFlow = *res.Package.PackageTotalFlowMb } remainFlow := totalFlow - usedFlow if remainFlow < 0 { remainFlow = 0 } if res.Package != nil { res.Package.UsedFlowMb = &usedFlow if totalFlow > 0 { res.Package.RemainFlowMb = &remainFlow } else { res.Package.RemainFlowMb = nil } } if res.Info != nil { res.Info.UsedFlowMb = &usedFlow if totalFlow > 0 { res.Info.RemainFlowMb = &remainFlow } else { res.Info.RemainFlowMb = nil } res.Info.FlowSyncTime = &nowText } if res.Card != nil { res.Card.UsedFlowMb = usedFlow res.Card.RemainFlowMb = remainFlow if totalFlow > 0 { res.Card.UsedFlowPercent = usedFlow / totalFlow * 100 } else { res.Card.UsedFlowPercent = 0 } res.Card.FlowSyncTime = now } }) setModule("flowUsage", "success", "", false, "") } func appSetDiagnosisModule(res *model.AppDiagnosisInfoModel, key string, status string, message string, cached bool, updatedAt string) { if res == nil { return } if res.Modules == nil { res.Modules = make(map[string]*model.AppDiagnosisModuleItem) } reason := appDiagnosisModuleReason(status, message) message = appDiagnosisNormalizeCustomerMessage(message) res.Modules[key] = &model.AppDiagnosisModuleItem{ Status: strings.TrimSpace(status), Reason: reason, Message: strings.TrimSpace(message), Cached: cached, UpdatedAt: updatedAt, } } func appDiagnosisModuleReason(status string, message string) string { status = strings.TrimSpace(status) message = strings.TrimSpace(message) switch { case status == "error" && (strings.Contains(message, "60分钟") || strings.Contains(message, "频率限制")): return "rate_limited" case status == "error": return "query_error" case appDiagnosisIsUnsubscribedMessage(message): return "not_subscribed" case appDiagnosisIsUnsupportedCapabilityMessage(message): return "unsupported" case status == "skipped" && appDiagnosisIsNoDataMessage(message): return "no_data" case status == "skipped": return "unsupported" default: return "" } } func appDiagnosisIsUnsubscribedMessage(message string) bool { message = strings.TrimSpace(message) return strings.Contains(message, "未订购") || strings.Contains(message, "接口未开通") || strings.Contains(message, "能力未开通") || strings.Contains(message, "服务未开通") || strings.Contains(message, "未开通该接口") || strings.Contains(message, "通道未申请") } func appDiagnosisIsUnsupportedCapabilityMessage(message string) bool { message = strings.TrimSpace(message) return strings.Contains(message, "暂不提供") || strings.Contains(message, "暂未提供") || strings.Contains(message, "未提供独立") || strings.Contains(message, "不支持查询") || strings.Contains(message, "运营商暂不支持") || strings.Contains(message, "暂未提供当前达量限速查询能力") || strings.Contains(message, "不提供查询能力") } func appDiagnosisIsNoDataMessage(message string) bool { message = strings.TrimSpace(message) return strings.Contains(message, "未返回") || strings.Contains(message, "未查询到") || strings.Contains(message, "无数据") || strings.Contains(message, "暂无数据") } func appDiagnosisModuleMessage(item *appUpstreamSimQueryItem, fallback string) string { if item == nil || strings.TrimSpace(item.Message) == "" { return fallback } return strings.TrimSpace(item.Message) } func appDiagnosisRegionCacheKey(simKey *appUpstreamSimKey) string { if simKey == nil { return "iotlcm:sim-diagnose:region:unknown" } parts := []string{ appDiagnosisSafeCachePart(simKey.ChanKey), appDiagnosisSafeCachePart(simKey.ICCID), appDiagnosisSafeCachePart(simKey.IMSI), appDiagnosisSafeCachePart(simKey.MSISDN), } return "iotlcm:sim-diagnose:region:" + strings.Join(parts, ":") } func appDiagnosisSafeCachePart(value string) string { value = strings.TrimSpace(value) if value == "" { return "-" } replacer := strings.NewReplacer(":", "_", "\n", "_", "\r", "_", "\t", "_", " ", "_") return replacer.Replace(value) } func appDiagnosisShouldCacheRegionLimit(tempKey string, chanKey string) bool { value := strings.ToLower(strings.TrimSpace(tempKey + " " + chanKey)) return strings.Contains(value, "sim_yd") || strings.Contains(value, "yd_mock") || strings.Contains(value, "onelink") || strings.Contains(value, "shared_token") } func appQueryDiagnosisRegionLimit(ctx context.Context, simKey *appUpstreamSimKey, useCache bool) (*appDiagnosisRegionQueryResult, error) { if !useCache { return appFetchDiagnosisRegionLimit(ctx, simKey) } if cached, cachedAt, ok := appGetCachedDiagnosisRegionLimit(ctx, simKey); ok { return &appDiagnosisRegionQueryResult{Item: cached, CachedAt: cachedAt, Cached: true}, nil } value, err, _ := appDiagnosisRegionFlight.Do(appDiagnosisRegionCacheKey(simKey), func() (any, error) { if cached, cachedAt, ok := appGetCachedDiagnosisRegionLimit(ctx, simKey); ok { return &appDiagnosisRegionQueryResult{Item: cached, CachedAt: cachedAt, Cached: true}, nil } return appQueryDiagnosisRegionLimitWithLock(ctx, simKey) }) if err != nil { return nil, err } result, ok := value.(*appDiagnosisRegionQueryResult) if !ok || result == nil { return nil, gerror.New("区域限制查询未返回有效结果") } return result, nil } func appQueryDiagnosisRegionLimitWithLock(ctx context.Context, simKey *appUpstreamSimKey) (*appDiagnosisRegionQueryResult, error) { lockKey := appDiagnosisRegionCacheKey(simKey) + ":lock" lockValue := uuid32.S() acquired, err := appAcquireDiagnosisRegionLock(ctx, lockKey, lockValue) if err != nil { g.Log().Warningf(ctx, "app diagnosis acquire region cache lock failed, key:%s, err:%+v", lockKey, err) return appFetchAndCacheDiagnosisRegionLimit(ctx, simKey) } if acquired { defer appReleaseDiagnosisRegionLock(lockKey, lockValue) if cached, cachedAt, ok := appGetCachedDiagnosisRegionLimit(ctx, simKey); ok { return &appDiagnosisRegionQueryResult{Item: cached, CachedAt: cachedAt, Cached: true}, nil } return appFetchAndCacheDiagnosisRegionLimit(ctx, simKey) } deadline := time.NewTimer(appDiagnosisRegionLockWait) ticker := time.NewTicker(appDiagnosisRegionLockPoll) defer deadline.Stop() defer ticker.Stop() for { select { case <-ctx.Done(): return nil, ctx.Err() case <-deadline.C: return nil, gerror.New("区域限制查询正在处理中,请稍后重试") case <-ticker.C: if cached, cachedAt, ok := appGetCachedDiagnosisRegionLimit(ctx, simKey); ok { return &appDiagnosisRegionQueryResult{Item: cached, CachedAt: cachedAt, Cached: true}, nil } acquired, lockErr := appAcquireDiagnosisRegionLock(ctx, lockKey, lockValue) if lockErr != nil { g.Log().Warningf(ctx, "app diagnosis retry region cache lock failed, key:%s, err:%+v", lockKey, lockErr) continue } if acquired { defer appReleaseDiagnosisRegionLock(lockKey, lockValue) return appFetchAndCacheDiagnosisRegionLimit(ctx, simKey) } } } } func appFetchDiagnosisRegionLimit(ctx context.Context, simKey *appUpstreamSimKey) (*appDiagnosisRegionQueryResult, error) { items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-region-limit-status", map[string]any{}) if err != nil { return nil, err } item := appDiagnosisFirstAvailableItem(items) if item == nil { return nil, nil } return &appDiagnosisRegionQueryResult{ Item: item, CachedAt: gtime.Now().Format("Y-m-d H:i:s"), }, nil } func appFetchAndCacheDiagnosisRegionLimit(ctx context.Context, simKey *appUpstreamSimKey) (*appDiagnosisRegionQueryResult, error) { result, err := appFetchDiagnosisRegionLimit(ctx, simKey) if err != nil || result == nil || result.Item == nil { return result, err } if appDiagnosisRegionLimitHasData(result.Item) { appSetCachedDiagnosisRegionLimit(ctx, simKey, *result.Item, result.CachedAt) } return result, nil } func appAcquireDiagnosisRegionLock(ctx context.Context, key string, value string) (bool, error) { res, err := g.Redis().Do(ctx, "SET", key, value, "NX", "PX", appDiagnosisRegionLockTTL.Milliseconds()) if err != nil { return false, err } return strings.EqualFold(strings.TrimSpace(res.String()), "OK"), nil } func appReleaseDiagnosisRegionLock(key string, value string) { const script = `if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end` releaseCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if _, err := g.Redis().Do(releaseCtx, "EVAL", script, 1, key, value); err != nil { g.Log().Warningf(releaseCtx, "app diagnosis release region cache lock failed, key:%s, err:%+v", key, err) } } func appGetCachedDiagnosisRegionLimit(ctx context.Context, simKey *appUpstreamSimKey) (*appUpstreamSimQueryItem, string, bool) { cacheKey := appDiagnosisRegionCacheKey(simKey) value, err := g.Redis().Get(ctx, cacheKey) if err != nil || value == nil || strings.TrimSpace(value.String()) == "" { return nil, "", false } var payload appDiagnosisRegionCachePayload if err = json.Unmarshal([]byte(value.String()), &payload); err != nil { g.Log().Warningf(ctx, "app diagnosis unmarshal region cache failed, key:%s, err:%+v", cacheKey, err) return nil, "", false } if !appDiagnosisRegionLimitHasData(&payload.Item) { if _, err = g.Redis().Do(ctx, "DEL", cacheKey); err != nil { g.Log().Warningf(ctx, "app diagnosis delete invalid region cache failed, key:%s, err:%+v", cacheKey, err) } return nil, "", false } return &payload.Item, payload.CachedAt, true } func appSetCachedDiagnosisRegionLimit(ctx context.Context, simKey *appUpstreamSimKey, item appUpstreamSimQueryItem, cachedAt string) { if strings.TrimSpace(cachedAt) == "" { cachedAt = gtime.Now().Format("Y-m-d H:i:s") } payload := appDiagnosisRegionCachePayload{ Item: item, CachedAt: cachedAt, } raw, err := json.Marshal(payload) if err != nil { g.Log().Warningf(ctx, "app diagnosis marshal region cache failed, iccid:%s, err:%+v", appDiagnosisSimKeyLogIccid(simKey), err) return } if _, err = g.Redis().Do(ctx, "SETEX", appDiagnosisRegionCacheKey(simKey), int(appDiagnosisRegionCacheTTL.Seconds()), string(raw)); err != nil { g.Log().Warningf(ctx, "app diagnosis set region cache failed, iccid:%s, err:%+v", appDiagnosisSimKeyLogIccid(simKey), err) } } func appCachedDiagnosisRegionModule(item *appUpstreamSimQueryItem) (status string, message string) { if item == nil { return "error", "区域限制结果来自缓存,但缓存内容为空" } message = strings.TrimSpace(item.Message) if message == "" { message = "区域限制结果来自缓存" } else { message = "区域限制结果来自缓存:" + message } if appDiagnosisRegionLimitHasData(item) { return "cached", message } switch item.Code { case 0: return "error", "区域限制结果来自缓存,但缓存内容为空" case 5000: return "skipped", message default: return "error", message } } func appDiagnosisApplyRegionLimit(res *model.AppDiagnosisInfoModel, item *appUpstreamSimQueryItem) { if res == nil || res.Info == nil || item == nil { return } hasData := appDiagnosisRegionLimitHasData(item) if item.Code == 5000 && !hasData { res.Info.RegionLimitStatus = nil return } if item.Code != 0 && !hasData { return } res.Info.RegionLimitStatus = appDiagnosisRegionLimitStatus(appDiagnosisRawString(item.ResData, "regionLimitStatus", "region_limit_status")) res.Info.RegionLimitOrderStatus = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "orderStatus", "order_status")) res.Info.RegionLimitTriggerStatus = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "limitStatus", "limit_status")) res.Info.RegionLimitType = appDiagnosisFirstStringPtr( appDiagnosisRawString(item.ResData, "limitType", "limit_type"), appDiagnosisPtrValue(res.Info.RegionLimitType), ) res.Info.RegionLimitArea = appDiagnosisFirstStringPtr( appDiagnosisRawString(item.ResData, "limitArea", "limit_area"), appDiagnosisPtrValue(res.Info.RegionLimitArea), ) } func appDiagnosisRegionLimitHasData(item *appUpstreamSimQueryItem) bool { if item == nil { return false } for _, value := range []string{ appDiagnosisRawString(item.ResData, "regionLimitStatus", "region_limit_status"), appDiagnosisRawString(item.ResData, "orderStatus", "order_status"), appDiagnosisRawString(item.ResData, "limitStatus", "limit_status"), appDiagnosisRawString(item.ResData, "limitType", "limit_type"), appDiagnosisRawString(item.ResData, "limitArea", "limit_area"), } { if strings.TrimSpace(value) != "" { return true } } return false } func appDiagnosisApplyRateLimit(res *model.AppDiagnosisInfoModel, item *appUpstreamSimQueryItem) { if res == nil || res.Info == nil || item == nil { return } if item.Code == 5000 { res.Info.RateLimitStatus = nil res.Info.RateLimitReason = nil res.Info.RateLimitDetail = nil res.Info.RateLimitApnDetails = nil return } if item.Code != 0 { return } item.ResData = appDiagnosisNormalizeRateLimitBody(item.ResData) res.Info.RateLimitStatus = appDiagnosisRateLimitStatus(appDiagnosisRawString(item.ResData, "rateLimitStatus", "rate_limit_status")) res.Info.RateLimitReason = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "rateLimitReason", "rate_limit_reason")) res.Info.RateLimitDetail = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "rateLimitDetail", "rate_limit_detail")) res.Info.RateLimitApnDetails = appDiagnosisRateLimitApnDetails(item.ResData) } func appDiagnosisIsMobileChannel(tempKey string, chanKey string) bool { value := strings.ToLower(strings.TrimSpace(tempKey + " " + chanKey)) return strings.Contains(value, "sim_yd") || strings.Contains(value, "yd_mock") } func appDiagnosisIsTelecomChannel(tempKey string, chanKey string) bool { value := strings.ToLower(strings.TrimSpace(tempKey + " " + chanKey)) return strings.Contains(value, "sim_dx") || strings.Contains(value, "dx_") } func appDiagnosisNormalizeStopReason(stopReason string) string { switch strings.TrimSpace(stopReason) { case "", "000000000000": return "未停机或系统暂无停机原因" default: return strings.TrimSpace(stopReason) } } func appDiagnosisFirstAvailableItem(items []appUpstreamSimQueryItem) *appUpstreamSimQueryItem { if len(items) == 0 { return nil } return &items[0] } func appDiagnosisRateLimitApnDetails(raw json.RawMessage) []model.AppDiagnosisRateLimitApnItem { raw = appDiagnosisUnwrapResData(raw) if len(raw) == 0 || string(raw) == "null" { return nil } payload := struct { ApnDetails []model.AppDiagnosisRateLimitApnItem `json:"apnDetails"` }{} if err := json.Unmarshal(raw, &payload); err != nil || len(payload.ApnDetails) == 0 { return nil } details := make([]model.AppDiagnosisRateLimitApnItem, 0, len(payload.ApnDetails)) for _, apn := range payload.ApnDetails { item := model.AppDiagnosisRateLimitApnItem{ ApnName: strings.TrimSpace(apn.ApnName), ApnStatus: strings.TrimSpace(apn.ApnStatus), ModifyTime: strings.TrimSpace(apn.ModifyTime), OverUseDetails: strings.TrimSpace(apn.OverUseDetails), ApnServiceUsageState: strings.TrimSpace(apn.ApnServiceUsageState), } for _, pcc := range apn.PccDetails { pccItem := model.AppDiagnosisRateLimitPccItem{ PccServiceCode: strings.TrimSpace(pcc.PccServiceCode), PccName: strings.TrimSpace(pcc.PccName), PccPolicyStatus: strings.TrimSpace(pcc.PccPolicyStatus), PccServiceUsageState: strings.TrimSpace(pcc.PccServiceUsageState), ModifyTime: strings.TrimSpace(pcc.ModifyTime), } if appDiagnosisHasRateLimitPccDetail(pccItem) { item.PccDetails = append(item.PccDetails, pccItem) } } if appDiagnosisHasRateLimitApnDetail(item) { details = append(details, item) } } return details } func appDiagnosisUnwrapResData(raw json.RawMessage) json.RawMessage { if len(raw) == 0 || string(raw) == "null" { return nil } wrapper := struct { ResData json.RawMessage `json:"resData"` }{} if err := json.Unmarshal(raw, &wrapper); err == nil && len(wrapper.ResData) > 0 && string(wrapper.ResData) != "null" { return wrapper.ResData } return raw } func appDiagnosisHasRateLimitApnDetail(item model.AppDiagnosisRateLimitApnItem) bool { return appDiagnosisFirstNonEmptyString( item.ApnName, item.ApnStatus, item.ModifyTime, item.OverUseDetails, item.ApnServiceUsageState, ) != "" || len(item.PccDetails) > 0 } func appDiagnosisHasRateLimitPccDetail(item model.AppDiagnosisRateLimitPccItem) bool { return appDiagnosisFirstNonEmptyString( item.PccServiceCode, item.PccName, item.PccPolicyStatus, item.PccServiceUsageState, item.ModifyTime, ) != "" } func appDiagnosisRawString(raw json.RawMessage, keys ...string) string { if len(raw) == 0 || string(raw) == "null" { return "" } var payload map[string]any if err := json.Unmarshal(raw, &payload); err != nil || len(payload) == 0 { return "" } if nested, ok := payload["resData"]; ok && nested != nil { if nestedRaw, err := json.Marshal(nested); err == nil { if value := appDiagnosisRawString(nestedRaw, keys...); value != "" { return value } } } for _, key := range keys { if value, ok := payload[key]; ok && value != nil { text := strings.TrimSpace(fmt.Sprint(value)) if text != "" && text != "" { return text } } } return "" } func appDiagnosisRawFloat(raw json.RawMessage, keys ...string) (float64, bool) { if len(raw) == 0 || string(raw) == "null" { return 0, false } var payload map[string]any if err := json.Unmarshal(raw, &payload); err != nil || len(payload) == 0 { return 0, false } if nested, ok := payload["resData"]; ok && nested != nil { if nestedRaw, err := json.Marshal(nested); err == nil { if value, ok := appDiagnosisRawFloat(nestedRaw, keys...); ok { return value, true } } } for _, key := range keys { value, ok := payload[key] if !ok || value == nil { continue } switch typed := value.(type) { case float64: return typed, true case float32: return float64(typed), true case int: return float64(typed), true case int64: return float64(typed), true case json.Number: parsed, err := typed.Float64() return parsed, err == nil default: text := strings.TrimSpace(fmt.Sprint(value)) if text == "" || text == "" { continue } parsed, err := strconv.ParseFloat(text, 64) return parsed, err == nil } } return 0, false } func appDiagnosisFirstNonEmptyString(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } func appDiagnosisSimKeyLogIccid(simKey *appUpstreamSimKey) string { if simKey == nil { return "" } return strings.TrimSpace(simKey.ICCID) } func appDiagnosisStringPtr(value string) *string { value = strings.TrimSpace(value) if value == "" { return nil } return &value } func appDiagnosisFirstStringPtr(values ...string) *string { for _, value := range values { if ptr := appDiagnosisStringPtr(value); ptr != nil { return ptr } } return nil } func appDiagnosisPtrValue(value *string) string { if value == nil { return "" } return *value } func appDiagnosisTimeString(value *gtime.Time) *string { if value == nil || value.IsZero() { return nil } text := value.Format("Y-m-d H:i:s") return &text } func appDiagnosisDurationMonths(begin, end *gtime.Time) *int { if begin == nil || begin.IsZero() || end == nil || end.IsZero() { return nil } beginTime := begin.Time endTime := end.Time months := (endTime.Year()-beginTime.Year())*12 + int(endTime.Month()-beginTime.Month()) if endTime.Day() >= beginTime.Day()-1 { months++ } if months < 0 { return nil } return &months } func appDiagnosisDataServiceStatus(status string) *string { var value string switch strings.ToUpper(strings.TrimSpace(status)) { case "0", "OFF", "CLOSED", "DISABLED": value = "0" case "1", "ON", "OPEN", "ENABLED": value = "1" case "-1", "2", "UNKNOWN": value = "-1" default: return nil } return &value } func appDiagnosisDataServiceStatusFromItems(items []appUpstreamSimQueryItem) *string { for _, item := range items { if item.Code == 0 { if status := appDiagnosisDataServiceStatusFromApn(item.ResData); status != nil { return status } } } return nil } func appDiagnosisDataServiceStatusFromItemsForChannel(items []appUpstreamSimQueryItem, tempKey string, chanKey string) *string { mobile := appDiagnosisIsMobileChannel(tempKey, chanKey) for _, item := range items { if item.Code == 0 { if status := appDiagnosisDataServiceStatusFromApnForChannel(item.ResData, mobile); status != nil { return status } } } return nil } func appDiagnosisDataServiceStatusFromApn(raw json.RawMessage) *string { return appDiagnosisDataServiceStatusFromApnForChannel(raw, true) } func appDiagnosisDataServiceStatusFromApnForChannel(raw json.RawMessage, mobile bool) *string { if mobile { return appDiagnosisDataServiceStatusFromApnTarget(raw) } return appDiagnosisDataServiceStatusFromApnAny(raw) } func appDiagnosisDataServiceStatusFromApnTarget(raw json.RawMessage) *string { return appDiagnosisDataServiceStatusFromApnFiltered(raw, true) } func appDiagnosisDataServiceStatusFromApnAny(raw json.RawMessage) *string { return appDiagnosisDataServiceStatusFromApnFiltered(raw, false) } func appDiagnosisDataServiceStatusFromApnFiltered(raw json.RawMessage, targetOnly bool) *string { if len(raw) == 0 || string(raw) == "null" { return nil } var payload struct { ApnList []struct { ApnName string `json:"apnName"` Status string `json:"status"` } `json:"apnList"` } if err := json.Unmarshal(appDiagnosisUnwrapResData(raw), &payload); err != nil { return nil } var selected *string for _, apn := range payload.ApnList { isTarget := strings.EqualFold(strings.TrimSpace(apn.ApnName), consts.SimCardDataApnName) if targetOnly && !isTarget { continue } status := strings.TrimSpace(apn.Status) if strings.TrimSpace(apn.ApnName) == "" || (status != "0" && status != "1") { return nil } if selected != nil && *selected != status { return nil } selected = appDiagnosisStringPtr(status) } return selected } func appDiagnosisOnlineStatus(status string) *string { var value string switch strings.ToUpper(strings.TrimSpace(status)) { case "0", "01", "ONLINE": value = "1" case "-1", "00", "OFFLINE": value = "0" case "-2", "02", "2", "UNKNOWN": value = "2" default: value = "2" } return &value } func appDiagnosisSessionStatus(status string) string { switch strings.ToUpper(strings.TrimSpace(status)) { case "0", "01", "ONLINE": return "0" case "-1", "00", "OFFLINE": return "-1" case "-2", "02", "2", "UNKNOWN": return "-2" default: return "-2" } } func appDiagnosisDeviceStatus(status string) *string { var value string switch strings.TrimSpace(status) { case "0": value = "0" case "1": value = "1" default: return nil } return &value } func appDiagnosisOperatingStatus(status string) *string { switch strings.TrimSpace(status) { case "0": value := "0" return &value case "1": value := "1" return &value default: return nil } } func appDiagnosisImeiStatus(status string) *string { var value string switch strings.TrimSpace(status) { case "0": value = "0" case "1": value = "1" case "2": value = "2" default: value = "2" } return &value } func appDiagnosisBindStatus(bindStatus string, cardBindingType string) string { switch strings.TrimSpace(bindStatus) { case "0", "1", "2": return strings.TrimSpace(bindStatus) } switch strings.TrimSpace(cardBindingType) { case "1", "2", "3", "4", "5": return "1" case "", "-1": return "2" default: return "2" } } func appDiagnosisCardStatus(status string) *string { switch strings.TrimSpace(status) { case "": return nil case "0", "未知": return appDiagnosisCardStatusPtr(0) case "1", "待激活": return appDiagnosisCardStatusPtr(1) case "2", "已激活", "正常", "在用": return appDiagnosisCardStatusPtr(2) case "4", "停机", "已停机": return appDiagnosisCardStatusPtr(4) case "6", "可测试", "测试期": return appDiagnosisCardStatusPtr(6) case "7", "库存": return appDiagnosisCardStatusPtr(7) case "8", "预销户": return appDiagnosisCardStatusPtr(8) case "9", "已销户", "销户": return appDiagnosisCardStatusPtr(9) default: return nil } } func appDiagnosisCardStatusPtr(status int) *string { if status < 0 { return nil } value := strconv.Itoa(status) return &value } func appDiagnosisRegionLimitStatus(status string) *string { var value string switch strings.TrimSpace(status) { case "0": value = "0" case "1": value = "1" default: value = "2" } return &value } func appDiagnosisUpstreamFailure(prefix string, code int, message string) (status string, displayMessage string) { if code == 5000 || appDiagnosisIsUnsupportedMessage(message) { return "skipped", message } if appDiagnosisIsConnectivityMessage(message) { return "error", "" } return "error", appDiagnosisRetryMessage } func appDiagnosisUpstreamMessage(prefix string, code int, message string) string { message = strings.TrimSpace(message) if message != "" { if appDiagnosisIsUnsupportedMessage(message) { return "" } if normalized := appDiagnosisNormalizeCustomerMessage(message); normalized != "" { return normalized } } prefix = strings.TrimSpace(prefix) if prefix == "" { prefix = "上游查询失败" } if appDiagnosisIsUnsupportedMessage(prefix) { return "" } return appDiagnosisNormalizeCustomerMessage(prefix) } const appDiagnosisRetryMessage = "服务繁忙,请重试" const appDiagnosisMissingMSISDNMessage = "机卡绑定信息暂不可查" func appDiagnosisIsUnsupportedMessage(message string) bool { return appDiagnosisIsUnsubscribedMessage(message) || appDiagnosisIsUnsupportedCapabilityMessage(message) } func appDiagnosisNormalizeCustomerMessage(message string) string { if strings.Contains(message, "同一卡号60分钟") { return "区域限制查询受运营商频率限制,暂未取得有效结果,请在上次查询60分钟后再试" } message = strings.TrimSpace(message) if message == "" { return "" } lower := strings.ToLower(message) switch { case appDiagnosisIsUnsupportedMessage(message): return "" case strings.Contains(lower, "missing msisdn"): return appDiagnosisMissingMSISDNMessage case appDiagnosisIsConnectivityMessage(message): return appDiagnosisRetryMessage case strings.Contains(message, "上游"), strings.Contains(message, "缓存"), strings.Contains(message, "60分钟"), strings.Contains(message, "未返回有效结果"), strings.Contains(message, "通道未实例化"): return appDiagnosisRetryMessage default: return message } } func appDiagnosisIsConnectivityMessage(message string) bool { lower := strings.ToLower(strings.TrimSpace(message)) return strings.Contains(lower, "context deadline exceeded") || strings.Contains(lower, "client.timeout") || strings.Contains(lower, "dial tcp") || strings.Contains(lower, "connection refused") || strings.Contains(lower, "request failed") || strings.Contains(lower, "timeout") || strings.Contains(lower, "no such host") || strings.Contains(lower, "network is unreachable") || strings.Contains(lower, "connection reset") || strings.Contains(lower, "http://") || strings.Contains(lower, "https://") } func appDiagnosisHumanLimitStatus(status string) *string { var value string switch strings.TrimSpace(status) { case "1": value = "0" case "0": value = "1" case "2": value = "2" case "3": value = "3" default: value = "2" } return &value } func appDiagnosisRateLimitStatus(status string) *string { var value string switch strings.TrimSpace(status) { case "0": value = "0" case "1": value = "1" case "3": value = "3" default: value = "2" } return &value } func appDiagnosisPackageEffectiveStatus(status int) *string { var value string switch status { case consts.PackageStatusPending: value = "0" case consts.PackageStatusEffective: value = "1" case consts.PackageStatusExpired: value = "2" default: return nil } return &value }