package iot import ( "context" "encoding/json" "fmt" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao" upstreamlogic "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/upstream" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/uuid32" "sort" "strconv" "strings" "sync" "time" "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 sIotTSimDiagnose struct{} type tSimDiagnoseCardInfo struct { Iccid string `json:"iccid"` Imsi string `json:"imsi"` Msisdn string `json:"msisdn"` ChanKey string `json:"chanKey"` TempKey string `json:"tempKey"` BossCardStatus int `json:"bossCardStatus"` DataServiceStatus int `json:"dataServiceStatus"` PackageStatus int `json:"packageStatus"` PackageTotalFlowMb float64 `json:"packageTotalFlowMb"` UsedFlowMb float64 `json:"usedFlowMb"` FlowPkgBeginAt *gtime.Time `json:"flowPkgBeginAt"` FlowPkgEndAt *gtime.Time `json:"flowPkgEndAt"` FlowSyncTime *gtime.Time `json:"flowSyncTime"` PackageName string `json:"packageName"` PackageType string `json:"packageType"` HasLocalCard bool `json:"hasLocalCard"` } const ( simDiagnoseRegionCacheTTL = time.Hour simDiagnoseRegionLockTTL = 15 * time.Second simDiagnoseRegionLockWait = 12 * time.Second simDiagnoseRegionLockPoll = 100 * time.Millisecond ) var simDiagnoseRegionFlight singleflight.Group type simDiagnoseRegionCachePayload struct { Item model.SimBaseRes[model.SimRegionLimitStatusItem] `json:"item"` CachedAt string `json:"cachedAt"` } type simDiagnoseRegionQueryResult struct { Item *model.SimBaseRes[model.SimRegionLimitStatusItem] CachedAt string Cached bool } func NewIotTSimDiagnose() *sIotTSimDiagnose { return &sIotTSimDiagnose{} } func init() { service.RegisterIotTSimDiagnose(NewIotTSimDiagnose()) } // resolveDiagnoseCardInfo 查询诊断用卡信息,并优先使用卡通道关联表修正上游通道。 func (s *sIotTSimDiagnose) resolveDiagnoseCardInfo(ctx context.Context, queryCode string, operation string) (*tSimDiagnoseCardInfo, error) { queryCode = strings.TrimSpace(queryCode) if queryCode == "" { return nil, gerror.New(operation + "失败,请确认查询码是否正确") } assoc, err := resolveCardChannelAssocByIccid(ctx, queryCode) if err != nil { return nil, gerror.Wrap(err, operation+"失败,请稍后重试") } var assocDisplay cardChannelDisplayRecord var assocDisplayErr error hasAssoc := false if assoc != nil { assocDisplay, hasAssoc, assocDisplayErr = resolveDiagnoseAssocChannelDisplay(ctx, assoc, operation) if assocDisplayErr != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.resolveDiagnoseCardInfo, resolve assoc channel failed, queryCode %s, channelKey %s, err %+v", queryCode, assoc.ChannelKey, assocDisplayErr) } } cardInfoTable := dao.TSimCardInfo.Table() channelTable := dao.TSimChannels.Table() flowPackageTable := dao.TSimFlowPackageInfo.Table() cardValueCondition := "(" + cardInfoTable + "." + dao.TSimCardInfo.Columns().Msisdn + " = ? OR " + cardInfoTable + "." + dao.TSimCardInfo.Columns().Iccid + " = ? OR " + cardInfoTable + "." + dao.TSimCardInfo.Columns().Imsi + " = ?)" var cardInfo *tSimDiagnoseCardInfo err = service.IotTSimCardInfo(). Model(ctx). LeftJoin(channelTable, channelTable+"."+dao.TSimChannels.Columns().ChanKey+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().ChanKey+" AND "+channelTable+"."+dao.TSimChannels.Columns().TenantId+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().TenantId). LeftJoin(flowPackageTable, flowPackageTable+"."+dao.TSimFlowPackageInfo.Columns().PackageCode+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().CurrentPackageCode+" AND "+flowPackageTable+"."+dao.TSimFlowPackageInfo.Columns().TenantId+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().TenantId). Where(cardValueCondition, queryCode, queryCode, queryCode). Fields( cardInfoTable+"."+dao.TSimCardInfo.Columns().Iccid+" as iccid", cardInfoTable+"."+dao.TSimCardInfo.Columns().Imsi+" as imsi", cardInfoTable+"."+dao.TSimCardInfo.Columns().Msisdn+" as msisdn", cardInfoTable+"."+dao.TSimCardInfo.Columns().ChanKey+" as chanKey", channelTable+"."+dao.TSimChannels.Columns().TempKey+" as tempKey", cardInfoTable+"."+dao.TSimCardInfo.Columns().BossCardStatus+" as bossCardStatus", cardInfoTable+"."+dao.TSimCardInfo.Columns().DataServiceStatus+" as dataServiceStatus", cardInfoTable+"."+dao.TSimCardInfo.Columns().PackageStatus+" as packageStatus", cardInfoTable+"."+dao.TSimCardInfo.Columns().PackageTotalFlowMb+" as packageTotalFlowMb", cardInfoTable+"."+dao.TSimCardInfo.Columns().UsedFlowMb+" as usedFlowMb", cardInfoTable+"."+dao.TSimCardInfo.Columns().FlowPkgBeginAt+" as flowPkgBeginAt", cardInfoTable+"."+dao.TSimCardInfo.Columns().FlowPkgEndAt+" as flowPkgEndAt", cardInfoTable+"."+dao.TSimCardInfo.Columns().FlowSyncTime+" as flowSyncTime", "COALESCE(NULLIF("+flowPackageTable+"."+dao.TSimFlowPackageInfo.Columns().PackageName+", ''), "+cardInfoTable+"."+dao.TSimCardInfo.Columns().CurrentPackageCode+") as packageName", flowPackageTable+"."+dao.TSimFlowPackageInfo.Columns().PackageType+" as packageType", ). Limit(1). Scan(&cardInfo) if err != nil { return nil, gerror.Wrap(err, operation+"失败,请稍后重试") } if cardInfo == nil { if hasAssoc { cardInfo = &tSimDiagnoseCardInfo{ Iccid: queryCode, ChanKey: assocDisplay.ChanKey, TempKey: assocDisplay.TempKey, } } else { if assocDisplayErr != nil { return nil, assocDisplayErr } cardInfo, err = resolveLegacyDiagnoseCardInfo(ctx, queryCode) if err != nil { return nil, gerror.Wrap(err, operation+"失败") } if cardInfo == nil { return nil, gerror.New(operation + "失败,请确认查询码是否正确") } } } else { cardInfo.HasLocalCard = true if !hasAssoc && strings.TrimSpace(cardInfo.Iccid) != "" { localAssoc, resolveErr := resolveCardChannelAssocByIccid(ctx, cardInfo.Iccid) if resolveErr != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.resolveDiagnoseCardInfo, resolve card channel failed, iccid %s, err %+v", cardInfo.Iccid, resolveErr) } else if display, ok, displayErr := resolveDiagnoseAssocChannelDisplay(ctx, localAssoc, operation); displayErr != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.resolveDiagnoseCardInfo, resolve local assoc channel failed, iccid %s, err %+v", cardInfo.Iccid, displayErr) if assocDisplayErr == nil { assocDisplayErr = displayErr } } else if ok { assocDisplay = display hasAssoc = true } } if hasAssoc { cardInfo.ChanKey = assocDisplay.ChanKey cardInfo.TempKey = assocDisplay.TempKey } } if strings.TrimSpace(cardInfo.ChanKey) == "" { if assocDisplayErr != nil { return nil, assocDisplayErr } return nil, gerror.New(operation + "失败,未找到卡对应的上游通道") } return cardInfo, nil } func resolveDiagnoseAssocChannelDisplay(ctx context.Context, assoc *cardChannelAssocRecord, operation string) (cardChannelDisplayRecord, bool, error) { if assoc == nil { return cardChannelDisplayRecord{}, false, nil } display, ok, err := resolveCardChannelDisplayByAssoc(ctx, assoc, nil) if err != nil { return cardChannelDisplayRecord{}, false, gerror.Wrap(err, operation+"失败,请稍后重试") } if !ok || strings.TrimSpace(display.ChanKey) == "" { return cardChannelDisplayRecord{}, false, gerror.New(operation + "失败,卡通道关联已匹配,但未找到对应通道配置") } return display, true, nil } // Info 查询SIM卡诊断信息 func (s *sIotTSimDiagnose) Info(ctx context.Context, in *model.TSimDiagnoseInfoReq) (res *model.TSimDiagnoseInfoOutput, err error) { cardInfo, err := s.resolveDiagnoseCardInfo(ctx, in.QueryCode, "查询SIM卡诊断信息") if err != nil { return nil, err } if in.OnlyFlow { return s.buildDiagnosisCurrentFlow(ctx, cardInfo), nil } return s.buildDiagnosisInfo(ctx, cardInfo) } func (s *sIotTSimDiagnose) buildDiagnosisCurrentFlow(ctx context.Context, cardInfo *tSimDiagnoseCardInfo) *model.TSimDiagnoseInfoOutput { res := &model.TSimDiagnoseInfoOutput{ Info: &model.TSimDiagnoseInfoItem{ Iccid: emptyStringPtr(cardInfo.Iccid), Imsi: emptyStringPtr(cardInfo.Imsi), Msisdn: emptyStringPtr(cardInfo.Msisdn), }, Modules: make(map[string]*model.TSimDiagnoseModuleItem), } diagCtx := upstreamlogic.WithPreservedUpstreamResults(upstreamlogic.WithDiagnoseUpstreamConfig(ctx)) if strings.TrimSpace(cardInfo.Imsi) == "" || strings.TrimSpace(cardInfo.Msisdn) == "" { threeCodes, threeErr := resolveSimKeyThreeCodesFromUpstream(ctx, diagCtx, cardInfo.ChanKey, "iccid", cardInfo.Iccid) if threeErr != nil { setDiagnoseModule(res, "flowUsage", "error", threeErr.Error(), false, "") return res } if threeCodes != nil { cardInfo.Imsi = strings.TrimSpace(threeCodes.IMSI) cardInfo.Msisdn = strings.TrimSpace(threeCodes.MSISDN) res.Info.Imsi = emptyStringPtr(cardInfo.Imsi) res.Info.Msisdn = emptyStringPtr(cardInfo.Msisdn) } } s.applyDiagnoseCurrentFlowUsage(ctx, diagCtx, res, cardInfo.ChanKey) return res } func (s *sIotTSimDiagnose) buildDiagnosisInfo(ctx context.Context, cardInfo *tSimDiagnoseCardInfo) (res *model.TSimDiagnoseInfoOutput, err error) { res = &model.TSimDiagnoseInfoOutput{ Info: &model.TSimDiagnoseInfoItem{ Iccid: emptyStringPtr(cardInfo.Iccid), Imsi: emptyStringPtr(cardInfo.Imsi), Msisdn: emptyStringPtr(cardInfo.Msisdn), Status: nil, DataStatus: nil, UsedFlowMb: nil, FlowSyncTime: nil, }, Session: &model.TSimDiagnoseSessionItem{}, Modules: make(map[string]*model.TSimDiagnoseModuleItem), } res.Info.DataServiceSource = "apn" if isTelecomDiagnoseChannel(cardInfo.TempKey, cardInfo.ChanKey) { res.Info.DataServiceSource = "network_block" } applyDiagnoseRemainFlow(res.Info) setDiagnoseModule(res, "card", "success", "", false, "") if isMobileDiagnoseChannel(cardInfo.TempKey, cardInfo.ChanKey) { res.Info.HumanLimitStatus = nil } if isTelecomDiagnoseChannel(cardInfo.TempKey, cardInfo.ChanKey) { res.Info.RateLimitStatus = nil res.Info.RateLimitReason = nil } simKey := &model.SimKey{ ChanKey: cardInfo.ChanKey, ICCID: cardInfo.Iccid, IMSI: cardInfo.Imsi, MSISDN: cardInfo.Msisdn, } diagCtx := upstreamlogic.WithPreservedUpstreamResults(upstreamlogic.WithDiagnoseUpstreamConfig(ctx)) var mu sync.Mutex updateDiagnose := func(fn func()) { mu.Lock() defer mu.Unlock() fn() } setModule := func(key string, status string, message string, cached bool, updatedAt string) { updateDiagnose(func() { setDiagnoseModule(res, key, status, message, cached, updatedAt) if key == "cardStatus" && res.Info.DataServiceSource == "network_block" && status != "success" && status != "cached" { setDiagnoseModule(res, "dataService", status, message, cached, updatedAt) } }) } setUpstreamSuccess := func(key string, cached bool, updatedAt string) { status, message := diagnoseCachedModule(cached) setModule(key, status, message, cached, updatedAt) } // 本地卡信息缺失 MSISDN/IMSI(如仅通过号段关联命中通道、未录入本地卡表)时, // 先用 ICCID 调运营商接口补全三码,避免后续诊断因缺码而全部返回“码号信息错误”。 if strings.TrimSpace(simKey.MSISDN) == "" || strings.TrimSpace(simKey.IMSI) == "" { cardValueType, cardValue := diagnoseLookupCardValue(simKey) if cardValueType == "" { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, no valid card identifier for three-code lookup, chanKey %s", cardInfo.ChanKey) } else if threeCodes, threeErr := resolveSimKeyThreeCodesFromUpstream(ctx, diagCtx, cardInfo.ChanKey, cardValueType, cardValue); threeErr != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, resolve three codes failed, iccid %s, chanKey %s, err %+v", cardInfo.Iccid, cardInfo.ChanKey, threeErr) } else if threeCodes != nil { simKey.ICCID = strings.TrimSpace(threeCodes.ICCID) simKey.IMSI = strings.TrimSpace(threeCodes.IMSI) simKey.MSISDN = strings.TrimSpace(threeCodes.MSISDN) updateDiagnose(func() { if value := emptyStringPtr(simKey.ICCID); value != nil { res.Info.Iccid = value } if value := emptyStringPtr(simKey.IMSI); value != nil { res.Info.Imsi = value } if value := emptyStringPtr(simKey.MSISDN); value != nil { res.Info.Msisdn = value } }) if cardInfo != nil { cardInfo.Iccid = simKey.ICCID cardInfo.Imsi = simKey.IMSI cardInfo.Msisdn = simKey.MSISDN } } } runDiagnose := func(wg *sync.WaitGroup, fn func()) { wg.Add(1) go func() { defer wg.Done() fn() }() } var wg sync.WaitGroup runDiagnose(&wg, func() { if res.Info.DataServiceSource == "network_block" { return } apnOut, queryErr := service.UpstreamChan().QuerySimApnInfo(diagCtx, &model.SimApnInfoInput{ SimKeys: []*model.SimKey{simKey}, ReqData: model.SimInputReqNullDate{}, }) if queryErr != nil { setModule("dataService", "error", queryErr.Error(), false, "") return } if len(apnOut) == 0 { setModule("dataService", "error", "上游数据服务未返回有效结果", false, "") return } for _, item := range apnOut { if item.Code != 0 { continue } if dataStatus := diagnoseDataServiceStatusForChannel(item.ResData.ApnList, cardInfo.TempKey, cardInfo.ChanKey); dataStatus != nil { updateDiagnose(func() { res.Info.DataStatus = dataStatus }) setUpstreamSuccess("dataService", item.Cached, item.UpdatedAt) return } } if apnOut[0].Code == 5000 { setModule("dataService", "skipped", apnOut[0].Message, false, "") return } if apnOut[0].Code != 0 { status, message := diagnoseUpstreamFailure("上游数据服务查询失败", apnOut[0].Code, apnOut[0].Message) setModule("dataService", status, message, false, "") return } setModule("dataService", "skipped", "上游数据服务未返回有效状态", false, "") }) runDiagnose(&wg, func() { if basicOut, err := service.UpstreamChan().QuerySimBasicInfo(diagCtx, &model.SimBasicInfoInput{ SimKeys: []*model.SimKey{simKey}, ReqData: model.SimInputReqNullDate{}, }); err != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, query sim basic info failed, simKey %+v, err %+v", simKey, err) setModule("basicInfo", "error", err.Error(), false, "") } else if len(basicOut) > 0 { item := basicOut[0] if item.Code == 0 { data := item.ResData updateDiagnose(func() { if value := emptyStringPtr(data.ICCID); value != nil { res.Info.Iccid = value } if value := emptyStringPtr(data.IMSI); value != nil { res.Info.Imsi = value } if value := emptyStringPtr(data.MSISDN); value != nil { res.Info.Msisdn = value } if res.Info.RegionLimitType == nil { res.Info.RegionLimitType = emptyStringPtr(data.AreaLimitType) } if res.Info.RegionLimitArea == nil { res.Info.RegionLimitArea = emptyStringPtr(data.AreaLimitInfo) } humanLimitStatus := mapHumanLimitStatus(data.IsHumanLimit) if humanLimitStatus != nil && *humanLimitStatus != "2" { res.Info.HumanLimitStatus = humanLimitStatus } else if !isMobileDiagnoseChannel(cardInfo.TempKey, cardInfo.ChanKey) { res.Info.HumanLimitStatus = humanLimitStatus } }) setUpstreamSuccess("basicInfo", item.Cached, item.UpdatedAt) } else if item.Code == 5000 { setModule("basicInfo", "skipped", item.Message, false, "") } else { setModule("basicInfo", "error", diagnoseUpstreamMessage("上游基础信息查询失败", item.Code, item.Message), false, "") } } else { setModule("basicInfo", "error", "上游基础信息未返回有效结果", false, "") } }) runDiagnose(&wg, func() { if statusOut, err := service.UpstreamChan().QuerySimCardStatus(diagCtx, &model.SimCardStatusInput{ SimKeys: []*model.SimKey{simKey}, }); err != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, query sim card status failed, simKey %+v, err %+v", simKey, err) setModule("cardStatus", "error", err.Error(), false, "") } else if len(statusOut) > 0 { item := statusOut[0] if item.Code == 0 { updateDiagnose(func() { res.Info.Status = diagnoseCardStatusPtr(item.ResData.CardStatus) if res.Info.DataServiceSource == "network_block" { if applyDiagnoseNetworkBlock(res.Info, item.ResData) { dataStatus, dataMessage := diagnoseCachedModule(item.Cached) setDiagnoseModule(res, "dataService", dataStatus, dataMessage, item.Cached, item.UpdatedAt) } else { setDiagnoseModule(res, "dataService", "skipped", "电信未返回有效断网状态", false, "") } } status, message := diagnoseCachedModule(item.Cached) setDiagnoseModule(res, "cardStatus", status, message, item.Cached, item.UpdatedAt) }) } else if item.Code == 5000 { setModule("cardStatus", "skipped", item.Message, false, "") } else { status, message := diagnoseUpstreamFailure("上游卡状态查询失败", item.Code, item.Message) setModule("cardStatus", status, message, false, "") } } else { setModule("cardStatus", "error", "上游卡状态未返回有效结果", false, "") } }) runDiagnose(&wg, func() { if sessionOut, err := service.UpstreamChan().QuerySimSession(diagCtx, &model.SimSessionInput{ SimKeys: []*model.SimKey{simKey}, }); err != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, query sim session failed, simKey %+v, err %+v", simKey, err) setModule("session", "error", err.Error(), false, "") } else if len(sessionOut) > 0 { item := sessionOut[0] if item.Code == 0 { data := item.ResData if data.Status == "" && data.ApnId == "" && data.Ipv4 == "" && data.Ipv6 == "" && data.Rat == "" && data.CreateTime == "" { setModule("session", "skipped", "上游会话信息未返回有效结果", false, "") return } sessionStatus := diagnoseSessionStatus(data.Status) updateDiagnose(func() { res.Info.OnlineStatus = mapOnlineStatus(sessionStatus) res.Session.ApnId = emptyStringPtr(data.ApnId) res.Session.Rat = emptyStringPtr(data.Rat) res.Session.Ipv4 = emptyStringPtr(data.Ipv4) res.Session.Ipv6 = emptyStringPtr(data.Ipv6) res.Session.Ipv6Prefix = emptyStringPtr(data.Ipv6Prefix) res.Session.Status = emptyStringPtr(sessionStatus) res.Session.CreateTime = emptyStringPtr(data.CreateTime) }) setUpstreamSuccess("session", item.Cached, item.UpdatedAt) } else if item.Code == 5000 { setModule("session", "skipped", item.Message, false, "") } else { setModule("session", "error", diagnoseUpstreamMessage("上游会话信息查询失败", item.Code, item.Message), false, "") } } else { setModule("session", "error", "上游会话信息未返回有效结果", false, "") } }) runDiagnose(&wg, func() { items, queryErr := service.UpstreamChan().QuerySimImei(diagCtx, &model.SimImeiInput{ SimKeys: []*model.SimKey{simKey}, }) if queryErr != nil { setModule("imei", "error", queryErr.Error(), false, "") return } if len(items) == 0 { setModule("imei", "skipped", "上游IMEI未返回有效结果", false, "") return } item := items[0] if item.Code != 0 { status, message := diagnoseUpstreamFailure("上游IMEI查询失败", item.Code, item.Message) setModule("imei", status, message, false, "") return } imei := emptyStringPtr(item.ResData.Imei) if imei == nil { setModule("imei", "skipped", "上游IMEI未返回有效结果", false, "") return } updateDiagnose(func() { res.Session.Imei = imei }) setUpstreamSuccess("imei", item.Cached, item.UpdatedAt) }) runDiagnose(&wg, func() { if deviceOut, err := service.UpstreamChan().QuerySimDeviceStatus(diagCtx, &model.SimDeviceStatusInput{ SimKeys: []*model.SimKey{simKey}, }); err != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, query sim device status failed, simKey %+v, err %+v", simKey, err) setModule("device", "error", err.Error(), false, "") } else if len(deviceOut) > 0 { if deviceOut[0].Code == 5000 { updateDiagnose(func() { res.Info.DeviceStatus = nil }) setModule("device", "skipped", deviceOut[0].Message, false, "") } else if deviceOut[0].Code == 0 { if mapDeviceStatus(deviceOut[0].ResData.DeviceStatus) == nil && mapOperatingStatus(deviceOut[0].ResData.OperatingStatus) == nil { setModule("device", "skipped", "上游设备状态未返回有效结果", false, "") return } updateDiagnose(func() { res.Info.DeviceStatus = mapDeviceStatus(deviceOut[0].ResData.DeviceStatus) res.Info.OperatingStatus = mapOperatingStatus(deviceOut[0].ResData.OperatingStatus) }) setUpstreamSuccess("device", deviceOut[0].Cached, deviceOut[0].UpdatedAt) } else { status, message := diagnoseUpstreamFailure("上游设备状态查询失败", deviceOut[0].Code, deviceOut[0].Message) setModule("device", status, message, false, "") } } else { setModule("device", "error", "上游设备状态未返回有效结果", false, "") } }) runDiagnose(&wg, func() { if bindOut, err := service.UpstreamChan().QuerySimCardBindStatus(diagCtx, &model.SimCardBindStatusInput{ SimKeys: []*model.SimKey{simKey}, ReqData: model.SimCardBindStatusInputItem{TestType: 0}, }); err != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, query sim card bind status failed, simKey %+v, err %+v", simKey, err) setModule("cardBind", "error", err.Error(), false, "") } else if len(bindOut) > 0 { if bindOut[0].Code == 5000 { updateDiagnose(func() { res.Info.ImeiStatus = nil }) setModule("cardBind", "skipped", bindOut[0].Message, false, "") } else if bindOut[0].Code == 0 { bindStatus := strings.TrimSpace(bindOut[0].ResData.BindStatus) if bindOut[0].ResData.OrderStatus == "1" { updateDiagnose(func() { res.Info.ImeiStatus = nil }) setModule("cardBind", "skipped", "该卡未订购机卡绑定服务", false, "") } else if bindStatus == "2" || bindStatus == "-" { message := strings.TrimSpace(bindOut[0].ResData.ErrorDes) if message == "" { message = strings.TrimSpace(bindOut[0].ResData.ErrorCode) } if message == "" { message = "上游机卡绑定网络查询失败" } updateDiagnose(func() { res.Info.ImeiStatus = nil }) setModule("cardBind", "error", message, false, "") } else { updateDiagnose(func() { res.Info.ImeiStatus = mapImeiStatusByBindResult(bindStatus) }) setUpstreamSuccess("cardBind", bindOut[0].Cached, bindOut[0].UpdatedAt) } } else { status, message := diagnoseUpstreamFailure("上游机卡绑定状态查询失败", bindOut[0].Code, bindOut[0].Message) setModule("cardBind", status, message, false, "") } } else { setModule("cardBind", "error", "上游机卡绑定状态未返回有效结果", false, "") } }) runDiagnose(&wg, func() { regionResult, err := queryDiagnoseRegionLimit(ctx, diagCtx, simKey, shouldCacheDiagnoseRegionLimit(cardInfo.TempKey, cardInfo.ChanKey)) if err != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, query sim region limit status failed, simKey %+v, err %+v", simKey, err) setModule("region", "error", err.Error(), false, "") } else if regionResult != nil && regionResult.Item != nil { updateDiagnose(func() { applyDiagnoseRegionLimit(res, regionResult.Item) }) status, message, updatedAt := diagnoseRegionModule(regionResult) setModule("region", status, message, regionResult.Cached, updatedAt) } else { setModule("region", "error", "区域限制未返回有效数据,请稍后重试", false, "") } }) runDiagnose(&wg, func() { if rateLimitOut, err := service.UpstreamChan().QuerySimDataDiagnosis(diagCtx, &model.SimDataDiagnosisInput{ SimKeys: []*model.SimKey{simKey}, }); err != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, query sim data diagnosis failed, simKey %+v, err %+v", simKey, err) setModule("rateLimit", "error", err.Error(), false, "") } else if len(rateLimitOut) > 0 { updateDiagnose(func() { applyDiagnoseRateLimit(res, &rateLimitOut[0]) }) if rateLimitOut[0].Code == 5000 { setModule("rateLimit", "skipped", rateLimitOut[0].Message, false, "") } else if rateLimitOut[0].Code == 0 { setUpstreamSuccess("rateLimit", rateLimitOut[0].Cached, rateLimitOut[0].UpdatedAt) } else { status, message := diagnoseUpstreamFailure("上游达量限速查询失败", rateLimitOut[0].Code, rateLimitOut[0].Message) setModule("rateLimit", status, message, false, "") } } else { setModule("rateLimit", "error", "上游达量限速未返回有效结果", false, "") } }) runDiagnose(&wg, func() { if stopOut, err := service.UpstreamChan().QuerySimStopReason(diagCtx, &model.SimStopReasonInput{ SimKeys: []*model.SimKey{simKey}, }); err != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, query sim stop reason failed, simKey %+v, err %+v", simKey, err) setModule("stopReason", "error", err.Error(), false, "") } else if len(stopOut) > 0 { if stopOut[0].Code == 0 { updateDiagnose(func() { res.Info.StopReason = emptyStringPtr(normalizeDiagnoseStopReason(stopOut[0].ResData.StopReason)) }) setUpstreamSuccess("stopReason", stopOut[0].Cached, stopOut[0].UpdatedAt) } else if stopOut[0].Code == 5000 { setModule("stopReason", "skipped", stopOut[0].Message, false, "") } else { status, message := diagnoseUpstreamFailure("上游停机原因查询失败", stopOut[0].Code, stopOut[0].Message) setModule("stopReason", status, message, false, "") } } else { setModule("stopReason", "error", "上游停机原因未返回有效结果", false, "") } }) runDiagnose(&wg, func() { if frozenOut, err := service.UpstreamChan().QuerySimFrozenState(diagCtx, &model.SimFrozenStateInput{ SimKeys: []*model.SimKey{simKey}, }); err != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, query sim frozen state failed, simKey %+v, err %+v", simKey, err) setModule("frozen", "error", err.Error(), false, "") } else if len(frozenOut) > 0 { if frozenOut[0].Code == 0 { if strings.TrimSpace(frozenOut[0].ResData.FrozenStatus) == "" { setModule("frozen", "skipped", "上游冻结状态未返回有效结果", false, "") return } updateDiagnose(func() { res.Info.FrozenStatus = emptyStringPtr(frozenOut[0].ResData.FrozenStatus) res.Info.FrozenReason = emptyStringPtr(frozenOut[0].ResData.FrozenReason) res.Info.FrozenOperTime = emptyStringPtr(frozenOut[0].ResData.OperTime) }) setUpstreamSuccess("frozen", frozenOut[0].Cached, frozenOut[0].UpdatedAt) } else if frozenOut[0].Code == 5000 { setModule("frozen", "skipped", frozenOut[0].Message, false, "") } else { status, message := diagnoseUpstreamFailure("上游冻结状态查询失败", frozenOut[0].Code, frozenOut[0].Message) setModule("frozen", status, message, false, "") } } else { setModule("frozen", "error", "上游冻结状态未返回有效结果", false, "") } }) wg.Wait() s.applyDiagnoseCurrentFlowUsage(ctx, diagCtx, res, cardInfo.ChanKey) return res, nil } // Flow 查询SIM卡使用流量 func (s *sIotTSimDiagnose) Flow(ctx context.Context, in *model.TSimDiagnoseFlowReq) (res *model.TSimDiagnoseFlowOutput, err error) { cardInfo, err := s.resolveDiagnoseCardInfo(ctx, in.QueryCode, "查询SIM卡使用流量") if err != nil { return nil, err } switch in.QueryType { case "7d", "30d": return s.queryDailyFlow(ctx, cardInfo.Iccid, in.QueryType) case "6M": return s.queryMonthlyFlow(ctx, cardInfo.Iccid) default: return nil, gerror.New("查询SIM卡使用流量失败,查询类型不正确") } } // emptyStringPtr 返回非空字符串的指针,空字符串返回nil func emptyStringPtr(value string) *string { if strings.TrimSpace(value) == "" { return nil } return &value } func diagnoseFlowFloatPtr(ok bool, value float64) *float64 { if !ok { return nil } if value < 0 { value = 0 } return &value } func applyDiagnoseRemainFlow(info *model.TSimDiagnoseInfoItem) { if info == nil || info.PackageTotalFlowMb == nil || info.UsedFlowMb == nil { return } remain := *info.PackageTotalFlowMb - *info.UsedFlowMb if remain < 0 { remain = 0 } info.RemainFlowMb = &remain } func diagnoseLookupCardValue(simKey *model.SimKey) (string, string) { if simKey == nil { return "", "" } for _, item := range []struct { cardValueType string cardValue string }{ {cardValueType: "iccid", cardValue: simKey.ICCID}, {cardValueType: "imsi", cardValue: simKey.IMSI}, {cardValueType: "msisdn", cardValue: simKey.MSISDN}, } { if value := strings.TrimSpace(item.cardValue); value != "" { return item.cardValueType, value } } return "", "" } func (s *sIotTSimDiagnose) applyDiagnoseCurrentFlowUsage(ctx context.Context, diagCtx context.Context, res *model.TSimDiagnoseInfoOutput, chanKey string) { if res == nil || res.Info == nil { return } simKey := &model.SimKey{ChanKey: strings.TrimSpace(chanKey)} if res.Info.Iccid != nil { simKey.ICCID = strings.TrimSpace(*res.Info.Iccid) } if res.Info.Imsi != nil { simKey.IMSI = strings.TrimSpace(*res.Info.Imsi) } if res.Info.Msisdn != nil { simKey.MSISDN = strings.TrimSpace(*res.Info.Msisdn) } if simKey.ChanKey == "" { setDiagnoseModule(res, "flowUsage", "error", "未找到卡对应的上游通道", false, "") return } out, err := service.UpstreamChan().QuerySimDatausage(diagCtx, &model.SimDataUsageInput{ SimKeys: []*model.SimKey{simKey}, ReqData: model.SimInputReqNullDate{}, }) if err != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, query current flow usage failed, simKey %+v, err %+v", simKey, err) setDiagnoseModule(res, "flowUsage", "error", err.Error(), false, "") return } if len(out) == 0 { setDiagnoseModule(res, "flowUsage", "error", "上游实时流量未返回有效结果", false, "") return } item := out[0] if item.Code == 5000 || isDiagnoseUnsupportedMessage(item.Message) { setDiagnoseModule(res, "flowUsage", "skipped", item.Message, false, "") return } if item.Code != 0 { status, message := diagnoseUpstreamFailure("上游实时流量查询失败", item.Code, item.Message) setDiagnoseModule(res, "flowUsage", status, message, false, "") return } res.Info.UsedFlowMb = diagnoseFlowFloatPtr(true, item.ResData.DataUsage) syncTime := gtime.Now().Format("Y-m-d H:i:s") if item.Cached && strings.TrimSpace(item.UpdatedAt) != "" { syncTime = strings.TrimSpace(item.UpdatedAt) } res.Info.FlowSyncTime = &syncTime applyDiagnoseRemainFlow(res.Info) status, message := diagnoseCachedModule(item.Cached) setDiagnoseModule(res, "flowUsage", status, message, item.Cached, item.UpdatedAt) } func diagnoseDataServiceStatus(items []model.SimApnInfoItem) *string { return diagnoseDataServiceStatusForChannel(items, "sim_yd", "") } func diagnoseDataServiceStatusForChannel(items []model.SimApnInfoItem, tempKey string, chanKey string) *string { mobile := diagnoseIsMobileChannel(tempKey, chanKey) var selected *string for _, item := range items { if mobile && !strings.EqualFold(strings.TrimSpace(item.ApnName), consts.SimCardDataApnName) { continue } status := strings.TrimSpace(item.Status) if strings.TrimSpace(item.ApnName) == "" || (status != "0" && status != "1") { return nil } if selected != nil && *selected != status { return nil } selected = emptyStringPtr(status) } return selected } func diagnoseIsMobileChannel(tempKey string, chanKey string) bool { value := strings.ToLower(strings.TrimSpace(tempKey + " " + chanKey)) return strings.Contains(value, "sim_yd") || strings.Contains(value, "yd_mock") } func diagnoseCardStatusPtr(status int) *string { switch status { case 0, 1, 2, 4, 6, 7, 8, 9: default: return nil } value := strconv.Itoa(status) return &value } func mapDiagnoseCardStatus(status string) *string { switch strings.TrimSpace(status) { case "": return nil case "0", "未知": return diagnoseCardStatusPtr(0) case "1", "待激活": return diagnoseCardStatusPtr(1) case "2", "已激活", "正常", "在用": return diagnoseCardStatusPtr(2) case "4", "停机", "已停机": return diagnoseCardStatusPtr(4) case "6", "可测试", "测试期": return diagnoseCardStatusPtr(6) case "7", "库存": return diagnoseCardStatusPtr(7) case "8", "预销户": return diagnoseCardStatusPtr(8) case "9", "已销户", "销户": return diagnoseCardStatusPtr(9) default: return nil } } func isMobileDiagnoseChannel(tempKey string, chanKey string) bool { value := strings.ToLower(strings.TrimSpace(tempKey + " " + chanKey)) return strings.Contains(value, "sim_yd") || strings.Contains(value, "yd_mock") } func isTelecomDiagnoseChannel(tempKey string, chanKey string) bool { value := strings.ToLower(strings.TrimSpace(tempKey + " " + chanKey)) return strings.Contains(value, "sim_dx") || strings.Contains(value, "dx_") } func shouldCacheDiagnoseRegionLimit(tempKey string, chanKey string) bool { value := strings.ToLower(strings.TrimSpace(tempKey + " " + chanKey)) return isMobileDiagnoseChannel(tempKey, chanKey) || strings.Contains(value, "onelink") || strings.Contains(value, "shared_token") } func normalizeDiagnoseStopReason(stopReason string) string { switch strings.TrimSpace(stopReason) { case "", "000000000000": return "未停机或系统暂无停机原因" default: return strings.TrimSpace(stopReason) } } func diagnoseUpstreamMessage(prefix string, code int, message string) string { message = strings.TrimSpace(message) if message != "" { if isDiagnoseUnsupportedMessage(message) { return "" } if normalized := normalizeDiagnoseCustomerMessage(message); normalized != "" { return normalized } } prefix = strings.TrimSpace(prefix) if prefix == "" { prefix = "上游查询失败" } if isDiagnoseUnsupportedMessage(prefix) { return "" } return normalizeDiagnoseCustomerMessage(prefix) } func diagnoseUpstreamFailure(prefix string, code int, message string) (status string, displayMessage string) { if code == 5000 || isDiagnoseUnsupportedMessage(message) { return "skipped", message } return "error", diagnoseUpstreamMessage(prefix, code, message) } const simDiagnoseRetryMessage = "查询暂时未完成,请稍后重试" func isDiagnoseUnsupportedMessage(message string) bool { message = strings.TrimSpace(message) if message == "" { return false } return strings.Contains(message, "暂不提供") || strings.Contains(message, "暂未提供") || strings.Contains(message, "未提供独立") || strings.Contains(message, "不支持查询") || strings.Contains(message, "运营商暂不支持") || strings.Contains(message, "暂未提供当前达量限速查询能力") || strings.Contains(message, "不提供查询能力") } func normalizeDiagnoseCustomerMessage(message string) string { message = strings.TrimSpace(message) if message == "" { return "" } lower := strings.ToLower(message) switch { case strings.Contains(message, "同一卡号60分钟"): return "区域限制查询受运营商频率限制,暂未取得有效结果,请在上次查询60分钟后再试" case 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, "token"), strings.Contains(lower, "http://"), strings.Contains(lower, "https://"): return simDiagnoseRetryMessage case strings.Contains(message, "上游"), strings.Contains(message, "缓存"), strings.Contains(message, "60分钟"), strings.Contains(message, "未返回有效结果"), strings.Contains(message, "通道未实例化"): return simDiagnoseRetryMessage case isDiagnoseUnsupportedMessage(message): return "" default: return message } } func setDiagnoseModule(res *model.TSimDiagnoseInfoOutput, key string, status string, message string, cached bool, updatedAt string) { if res == nil { return } if res.Modules == nil { res.Modules = make(map[string]*model.TSimDiagnoseModuleItem) } reason := diagnoseModuleReason(status, message) message = normalizeDiagnoseCustomerMessage(message) res.Modules[key] = &model.TSimDiagnoseModuleItem{ Reason: reason, Status: status, Message: strings.TrimSpace(message), Cached: cached, UpdatedAt: updatedAt, } } func diagnoseModuleReason(status string, message string) string { switch { case status == "error" && (strings.Contains(message, "60分钟") || strings.Contains(message, "频率限制")): return "rate_limited" case status == "error": return "query_error" case strings.Contains(message, "未订购"), strings.Contains(message, "未开通"), strings.Contains(message, "通道未申请"): return "not_subscribed" case status == "skipped" && (strings.Contains(message, "未返回") || strings.Contains(message, "未查询到") || strings.Contains(message, "无数据")): return "no_data" case status == "skipped": return "unsupported" default: return "" } } func diagnoseCachedModule(cached bool) (status string, message string) { if cached { return "cached", "上游查询存在频率限制,当前展示最近一次缓存结果。" } return "success", "" } func diagnoseRegionCacheKey(simKey *model.SimKey) string { if simKey == nil { return "iotlcm:sim-diagnose:region:unknown" } parts := []string{ safeDiagnoseCachePart(simKey.ChanKey), safeDiagnoseCachePart(simKey.ICCID), safeDiagnoseCachePart(simKey.IMSI), safeDiagnoseCachePart(simKey.MSISDN), } return "iotlcm:sim-diagnose:region:" + strings.Join(parts, ":") } func diagnoseRegionLockKey(simKey *model.SimKey) string { return diagnoseRegionCacheKey(simKey) + ":lock" } func queryDiagnoseRegionLimit(ctx context.Context, diagCtx context.Context, simKey *model.SimKey, useCache bool) (*simDiagnoseRegionQueryResult, error) { if !useCache { return fetchDiagnoseRegionLimit(diagCtx, simKey) } if cached, cachedAt, ok := getCachedDiagnoseRegionLimit(ctx, simKey); ok { return &simDiagnoseRegionQueryResult{Item: cached, CachedAt: cachedAt, Cached: true}, nil } value, err, _ := simDiagnoseRegionFlight.Do(diagnoseRegionCacheKey(simKey), func() (any, error) { if cached, cachedAt, ok := getCachedDiagnoseRegionLimit(ctx, simKey); ok { return &simDiagnoseRegionQueryResult{Item: cached, CachedAt: cachedAt, Cached: true}, nil } return queryDiagnoseRegionLimitWithLock(ctx, diagCtx, simKey) }) if err != nil { return nil, err } result, ok := value.(*simDiagnoseRegionQueryResult) if !ok || result == nil { return nil, gerror.New("区域限制查询未返回有效结果") } return result, nil } func queryDiagnoseRegionLimitWithLock(ctx context.Context, diagCtx context.Context, simKey *model.SimKey) (*simDiagnoseRegionQueryResult, error) { lockKey := diagnoseRegionLockKey(simKey) lockValue := uuid32.S() acquired, err := acquireDiagnoseRegionLock(ctx, lockKey, lockValue) if err != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, acquire region cache lock failed, key %s, err %+v", lockKey, err) return fetchAndCacheDiagnoseRegionLimit(ctx, diagCtx, simKey) } if acquired { defer releaseDiagnoseRegionLock(lockKey, lockValue) if cached, cachedAt, ok := getCachedDiagnoseRegionLimit(ctx, simKey); ok { return &simDiagnoseRegionQueryResult{Item: cached, CachedAt: cachedAt, Cached: true}, nil } return fetchAndCacheDiagnoseRegionLimit(ctx, diagCtx, simKey) } deadline := time.NewTimer(simDiagnoseRegionLockWait) ticker := time.NewTicker(simDiagnoseRegionLockPoll) 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 := getCachedDiagnoseRegionLimit(ctx, simKey); ok { return &simDiagnoseRegionQueryResult{Item: cached, CachedAt: cachedAt, Cached: true}, nil } acquired, lockErr := acquireDiagnoseRegionLock(ctx, lockKey, lockValue) if lockErr != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, retry region cache lock failed, key %s, err %+v", lockKey, lockErr) continue } if acquired { defer releaseDiagnoseRegionLock(lockKey, lockValue) return fetchAndCacheDiagnoseRegionLimit(ctx, diagCtx, simKey) } } } } func fetchDiagnoseRegionLimit(ctx context.Context, simKey *model.SimKey) (*simDiagnoseRegionQueryResult, error) { regionOut, err := service.UpstreamChan().QuerySimRegionLimitStatus(ctx, &model.SimRegionLimitStatusInput{ SimKeys: []*model.SimKey{simKey}, }) if err != nil { return nil, err } if len(regionOut) == 0 { return nil, nil } return &simDiagnoseRegionQueryResult{Item: ®ionOut[0], CachedAt: gtime.Now().Format("Y-m-d H:i:s")}, nil } func fetchAndCacheDiagnoseRegionLimit(ctx context.Context, diagCtx context.Context, simKey *model.SimKey) (*simDiagnoseRegionQueryResult, error) { result, err := fetchDiagnoseRegionLimit(diagCtx, simKey) if err != nil || result == nil || result.Item == nil { return result, err } if isDiagnoseRegionLimitCacheable(result.Item) { setCachedDiagnoseRegionLimit(ctx, simKey, *result.Item, result.CachedAt) } return result, nil } func diagnoseRegionModule(result *simDiagnoseRegionQueryResult) (status string, message string, updatedAt string) { if result == nil || result.Item == nil { return "error", "区域限制未返回有效数据,请稍后重试", "" } if result.Cached && isDiagnoseRegionLimitCacheable(result.Item) { updatedAt, hint := describeDiagnoseRegionCache(result.CachedAt) return "cached", "运营商一小时内只能查询一次,当前展示最近一次查询结果。" + hint, updatedAt } if isDiagnoseRegionLimitCacheable(result.Item) { updatedAt, hint := describeDiagnoseRegionCache(result.CachedAt) return "success", hint, updatedAt } if result.Item.Code == 5000 { return "skipped", result.Item.Message, "" } rawMessage := strings.TrimSpace(result.Item.Message) message = normalizeDiagnoseCustomerMessage(rawMessage) if message == "" { message = "区域限制未返回有效数据,请稍后重试" } return "error", message, "" } func acquireDiagnoseRegionLock(ctx context.Context, key string, value string) (bool, error) { res, err := g.Redis().Do(ctx, "SET", key, value, "NX", "PX", simDiagnoseRegionLockTTL.Milliseconds()) if err != nil { return false, err } return strings.EqualFold(strings.TrimSpace(res.String()), "OK"), nil } func releaseDiagnoseRegionLock(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, "sIotTSimDiagnose.Info, release region cache lock failed, key %s, err %+v", key, err) } } func safeDiagnoseCachePart(value string) string { value = strings.TrimSpace(value) if value == "" { return "-" } replacer := strings.NewReplacer(":", "_", "\n", "_", "\r", "_", "\t", "_", " ", "_") return replacer.Replace(value) } func getCachedDiagnoseRegionLimit(ctx context.Context, simKey *model.SimKey) (*model.SimBaseRes[model.SimRegionLimitStatusItem], string, bool) { cacheKey := diagnoseRegionCacheKey(simKey) value, err := g.Redis().Get(ctx, cacheKey) if err != nil || value == nil || strings.TrimSpace(value.String()) == "" { return nil, "", false } var payload simDiagnoseRegionCachePayload if err = json.Unmarshal([]byte(value.String()), &payload); err != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, unmarshal region cache failed, key %s, err %+v", cacheKey, err) return nil, "", false } if !isDiagnoseRegionLimitCacheable(&payload.Item) { _, _ = g.Redis().Do(ctx, "DEL", cacheKey) return nil, "", false } return &payload.Item, payload.CachedAt, true } func setCachedDiagnoseRegionLimit(ctx context.Context, simKey *model.SimKey, item model.SimBaseRes[model.SimRegionLimitStatusItem], cachedAt string) { if !isDiagnoseRegionLimitCacheable(&item) { return } if strings.TrimSpace(cachedAt) == "" { cachedAt = gtime.Now().Format("Y-m-d H:i:s") } payload := simDiagnoseRegionCachePayload{ Item: item, CachedAt: cachedAt, } raw, err := json.Marshal(payload) if err != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, marshal region cache failed, simKey %+v, err %+v", simKey, err) return } if _, err = g.Redis().Do(ctx, "SETEX", diagnoseRegionCacheKey(simKey), int(simDiagnoseRegionCacheTTL.Seconds()), string(raw)); err != nil { g.Log().Warningf(ctx, "sIotTSimDiagnose.Info, set region cache failed, simKey %+v, err %+v", simKey, err) } } func hasDiagnoseRegionLimitData(item *model.SimBaseRes[model.SimRegionLimitStatusItem]) bool { if item == nil { return false } data := item.ResData return strings.TrimSpace(data.RegionLimitStatus) != "" || strings.TrimSpace(data.OrderStatus) != "" || strings.TrimSpace(data.LimitStatus) != "" || strings.TrimSpace(data.LimitType) != "" || strings.TrimSpace(data.LimitArea) != "" } func isDiagnoseRegionLimitCacheable(item *model.SimBaseRes[model.SimRegionLimitStatusItem]) bool { if item == nil || item.Code != 0 { return false } data := item.ResData return isKnownDiagnoseRegionStatus(data.RegionLimitStatus) || isKnownDiagnoseRegionStatus(data.OrderStatus) || isKnownDiagnoseRegionStatus(data.LimitStatus) } func isKnownDiagnoseRegionStatus(value string) bool { value = strings.TrimSpace(value) return value == "0" || value == "1" } func cachedDiagnoseRegionModule(item *model.SimBaseRes[model.SimRegionLimitStatusItem]) (status string, message string) { if item == nil { return "error", "区域限制暂无有效缓存数据,请稍后重试" } if isDiagnoseRegionLimitCacheable(item) { return "success", "" } if item.Code == 5000 { return "error", "区域限制暂无有效缓存数据,请稍后重试" } return "error", normalizeDiagnoseCustomerMessage(item.Message) } func describeDiagnoseRegionCache(cachedAt string) (updatedAt string, hint string) { cachedAt = strings.TrimSpace(cachedAt) if cachedAt == "" { return "", "" } parsed, err := time.ParseInLocation("2006-01-02 15:04:05", cachedAt, time.Local) if err != nil { return cachedAt, "" } now := time.Now() updatedAt = parsed.Format("15:04") if sameDayTime(parsed, now) { updatedAt += fmt.Sprintf("(%s)", regionCacheAgoText(now.Sub(parsed))) } else { updatedAt = parsed.Format("01-02 15:04") + fmt.Sprintf("(%s)", regionCacheAgoText(now.Sub(parsed))) } nextCheckAt := parsed.Add(simDiagnoseRegionCacheTTL) if sameDayTime(parsed, nextCheckAt) { hint = nextCheckAt.Format("15:04") + "后可再次查看" } else { hint = nextCheckAt.Format("01-02 15:04") + "后可再次查看" } return updatedAt, hint } func regionCacheAgoText(d time.Duration) string { if d < time.Minute { return "刚刚" } if d < time.Hour { minutes := int(d.Minutes()) if minutes <= 0 { minutes = 1 } return fmt.Sprintf("%d分钟前", minutes) } hours := int(d.Hours()) if hours < 24 { return fmt.Sprintf("%d小时前", hours) } return fmt.Sprintf("%d天前", hours/24) } func sameDayTime(left time.Time, right time.Time) bool { leftYear, leftMonth, leftDay := left.Date() rightYear, rightMonth, rightDay := right.Date() return leftYear == rightYear && leftMonth == rightMonth && leftDay == rightDay } func applyDiagnoseRegionLimit(res *model.TSimDiagnoseInfoOutput, item *model.SimBaseRes[model.SimRegionLimitStatusItem]) { if res == nil || res.Info == nil || item == nil { return } if !hasDiagnoseRegionLimitData(item) { return } if item.Code != 0 { return } data := item.ResData orderStatus := strings.TrimSpace(data.OrderStatus) if orderStatus == "1" { res.Info.RegionLimitStatus = emptyStringPtr("2") res.Info.RegionLimitOrderStatus = emptyStringPtr("1") res.Info.RegionLimitTriggerStatus = nil res.Info.RegionLimitType = nil res.Info.RegionLimitArea = nil return } if orderStatus == "0" { res.Info.RegionLimitOrderStatus = emptyStringPtr("0") switch strings.TrimSpace(data.LimitStatus) { case "0": res.Info.RegionLimitStatus = emptyStringPtr("1") res.Info.RegionLimitTriggerStatus = emptyStringPtr("0") case "1": res.Info.RegionLimitStatus = emptyStringPtr("0") res.Info.RegionLimitTriggerStatus = emptyStringPtr("1") case "-": res.Info.RegionLimitStatus = emptyStringPtr("2") res.Info.RegionLimitTriggerStatus = emptyStringPtr("-") default: res.Info.RegionLimitStatus = emptyStringPtr("2") res.Info.RegionLimitTriggerStatus = nil } switch strings.TrimSpace(data.LimitType) { case "1", "2", "3", "4": res.Info.RegionLimitType = emptyStringPtr(data.LimitType) default: res.Info.RegionLimitType = nil } res.Info.RegionLimitArea = emptyStringPtr(data.LimitArea) return } res.Info.RegionLimitStatus = mapRegionLimitStatus(data.RegionLimitStatus) res.Info.RegionLimitOrderStatus = emptyStringPtr(data.OrderStatus) res.Info.RegionLimitTriggerStatus = emptyStringPtr(data.LimitStatus) res.Info.RegionLimitType = emptyStringPtr(data.LimitType) res.Info.RegionLimitArea = emptyStringPtr(data.LimitArea) } func applyDiagnoseRateLimit(res *model.TSimDiagnoseInfoOutput, item *model.SimBaseRes[model.SimDataDiagnosisItem]) { if res == nil || res.Info == nil || item == nil { return } if item.Code == 5000 { res.Info.RateLimitStatus = nil res.Info.RateLimitReason = nil return } if item.Code != 0 { return } res.Info.RateLimitStatus = mapRateLimitStatus(item.ResData.RateLimitStatus) res.Info.RateLimitReason = emptyStringPtr(item.ResData.RateLimitReason) res.Info.RateLimitDetail = emptyStringPtr(item.ResData.RateLimitDetail) res.Info.RateLimitApnDetails = make([]model.TSimDiagnoseRateLimitApnItem, 0, len(item.ResData.ApnDetails)) for _, apn := range item.ResData.ApnDetails { pccDetails := make([]model.TSimDiagnoseRateLimitPccItem, 0, len(apn.PccDetails)) for _, pcc := range apn.PccDetails { pccDetails = append(pccDetails, model.TSimDiagnoseRateLimitPccItem{ PccServiceCode: pcc.PccServiceCode, PccName: pcc.PccName, PccPolicyStatus: pcc.PccPolicyStatus, PccServiceUsageState: pcc.PccServiceUsageState, ModifyTime: pcc.ModifyTime, }) } res.Info.RateLimitApnDetails = append(res.Info.RateLimitApnDetails, model.TSimDiagnoseRateLimitApnItem{ ApnName: apn.ApnName, ApnStatus: apn.ApnStatus, ModifyTime: apn.ModifyTime, OverUseDetails: apn.OverUseDetails, ApnServiceUsageState: apn.ApnServiceUsageState, PccDetails: pccDetails, }) } } // timeStringPtr 格式化时间并返回字符串指针 func timeStringPtr(value *gtime.Time) *string { if value == nil || value.IsZero() { return nil } text := value.Format("Y-m-d H:i:s") return &text } // durationMonthsPtr 计算两个时间之间的月数 func durationMonthsPtr(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 } // mapOnlineStatus 映射在线状态 func diagnoseSessionStatus(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 "" } } func mapOnlineStatus(status string) *string { switch diagnoseSessionStatus(status) { case "0": return emptyStringPtr("1") case "-1": return emptyStringPtr("0") case "-2": return emptyStringPtr("2") default: return nil } } // mapOperatingStatus 映射运营商终端开关机状态 func mapOperatingStatus(status string) *string { switch strings.TrimSpace(status) { case "0": value := "0" return &value case "1": value := "1" return &value default: return nil } } // mapDeviceStatus 映射设备状态 func mapDeviceStatus(status string) *string { var value string switch strings.TrimSpace(status) { case "0": value = "0" case "1": value = "1" default: return nil } return &value } // mapImeiStatus 映射机卡状态 func mapImeiStatusByBindResult(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 } // mapRegionLimitStatus 映射地域限制状态 func mapRegionLimitStatus(status string) *string { switch strings.TrimSpace(status) { case "0": value := "0" return &value case "1": value := "1" return &value case "2": value := "2" return &value default: return nil } } // mapHumanLimitStatus 映射人联网限制状态 func mapHumanLimitStatus(status string) *string { var value string switch strings.TrimSpace(status) { case "1": value = "0" case "0": value = "1" default: return nil } return &value } // mapRateLimitStatus 映射达量限速状态 func mapRateLimitStatus(status string) *string { var value string switch strings.TrimSpace(status) { case "0": value = "0" case "1": value = "1" default: return nil } return &value } // mapPackageEffectiveStatus 映射套餐生效状态 func mapPackageEffectiveStatus(status int) *string { var value string switch status { case 1: value = "0" case 2: value = "1" case 3: value = "2" default: return nil } return &value } func (s *sIotTSimDiagnose) queryDailyFlow(ctx context.Context, iccid string, queryType string) (res *model.TSimDiagnoseFlowOutput, err error) { days := 7 if queryType == "30d" { days = 30 } now := time.Now() startTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -(days - 1)) endTime := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location()) list, _, err := service.TrafficUsage().QueryDaily(ctx, model.TrafficUsageQueryInput{ Iccid: iccid, StartTime: startTime, EndTime: endTime, Asc: true, }) if err != nil { return nil, gerror.Wrap(err, "查询SIM卡使用流量失败,请稍后重试!") } usageMap := make(map[string]float64, len(list)) for _, item := range list { usageMap[item.Ts.Format("2006-01-02")] = item.UsageMb } res = &model.TSimDiagnoseFlowOutput{ Times: make([]string, 0, days), Flow: make([]float64, 0, days), FlowAvailable: make([]bool, 0, days), Unit: "MB", } for i := 0; i < days; i++ { current := startTime.AddDate(0, 0, i) key := current.Format("2006-01-02") res.Times = append(res.Times, current.Format("01-02")) usage, ok := usageMap[key] res.Flow = append(res.Flow, usage) res.FlowAvailable = append(res.FlowAvailable, ok) } return res, nil } func (s *sIotTSimDiagnose) queryMonthlyFlow(ctx context.Context, iccid string) (res *model.TSimDiagnoseFlowOutput, err error) { now := time.Now() currentMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) startMonth := currentMonth.AddDate(0, -5, 0) endTime := time.Date(now.Year(), now.Month()+1, 0, 23, 59, 59, 0, now.Location()) list, _, err := service.TrafficUsage().QueryMonthly(ctx, model.TrafficUsageQueryInput{ Iccid: iccid, StartTime: startMonth, EndTime: endTime, Asc: true, }) if err != nil { return nil, gerror.Wrap(err, "查询SIM卡使用流量失败,请稍后重试!") } sort.Slice(list, func(i, j int) bool { return list[i].Ts.Before(list[j].Ts) }) usageMap := make(map[string]float64, len(list)) for _, item := range list { usageMap[item.Ts.Format("2006-01")] = item.UsageMb } res = &model.TSimDiagnoseFlowOutput{ Times: make([]string, 0, 6), Flow: make([]float64, 0, 6), FlowAvailable: make([]bool, 0, 6), Unit: "MB", } for i := 0; i < 6; i++ { current := startMonth.AddDate(0, i, 0) key := current.Format("2006-01") res.Times = append(res.Times, key) usage, ok := usageMap[key] res.Flow = append(res.Flow, usage) res.FlowAvailable = append(res.FlowAvailable, ok) } return res, nil }