Files

1625 lines
56 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package 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: &regionOut[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
}