Files
iotlcm/server/internal/logic/app/app_diagnosis.go
T
MiFaZhan 2ed5dfa6a1 chore: 初始开源发布
iotlcm-app:小程序端 + 用户端服务。

- app/:uni-app 小程序前端(基于 unibest,MIT)
  - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID
  - 内网后端地址 → admin.iotlcm.example.com
  - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明)
- server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0)
  - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件)
  - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化
  - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位)
  - 依赖号码尾部的断言同步更新
  - 补 server/LICENSE(Apache-2.0)
- 移除内部资产:CI 触发器
- 补根 README,LF 行尾 + .gitattributes

已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
2026-10-26 14:01:25 +08:00

1539 lines
50 KiB
Go

package app
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"sync"
"time"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
type sAppDiagnosis struct{}
const (
appDiagnosisRegionCacheTTL = time.Hour
appOpenDiagnosisRateWindow = time.Minute
appOpenDiagnosisRateLimit = 20
)
type appDiagnosisUpdateFunc func(func())
type appDiagnosisSetModuleFunc func(key string, status string, message string, cached bool, updatedAt string)
type appDiagnosisRegionCachePayload struct {
Item appUpstreamSimQueryItem `json:"item"`
CachedAt string `json:"cachedAt"`
}
func NewAppDiagnosis() *sAppDiagnosis {
return &sAppDiagnosis{}
}
func init() {
service.RegisterAppDiagnosis(NewAppDiagnosis())
}
// Info 查询登录用户权限范围内的SIM卡诊断信息。
func (s *sAppDiagnosis) Info(ctx context.Context, param *model.AppDiagnosisInfoParam) (res *model.AppDiagnosisInfoModel, err error) {
if param == nil {
param = &model.AppDiagnosisInfoParam{}
}
if err = appEnsureFeatureAccess(ctx, "card:diagnosis"); err != nil {
return nil, err
}
_, cardLogic, row, err := s.resolveDiagnosisCard(ctx, param.QueryCode)
if err != nil {
return nil, err
}
return s.buildDiagnosisInfo(ctx, row, cardLogic.publicCardRowToModel(ctx, row, param.QueryCode))
}
func (s *sAppDiagnosis) OpenInfo(ctx context.Context, param *model.AppOpenDiagnosisInfoParam) (res *model.AppDiagnosisInfoModel, err error) {
if param == nil {
param = &model.AppOpenDiagnosisInfoParam{}
}
if err = appEnsureOpenDiagnosisRateLimit(ctx); err != nil {
return nil, err
}
appAuditOpenDiagnosis(ctx, "info", param.QueryCode, "")
cardLogic, row, err := s.resolveOpenDiagnosisCard(ctx, param.TenantId, param.QueryCode)
if err != nil {
return nil, err
}
res, err = s.buildDiagnosisInfo(ctx, row, cardLogic.publicCardRowToModel(ctx, row, param.QueryCode))
if err != nil {
return nil, err
}
return sanitizeOpenDiagnosisInfo(res), nil
}
func (s *sAppDiagnosis) buildDiagnosisInfo(ctx context.Context, row *appCardQueryRow, cardModel *model.AppCardModel) (res *model.AppDiagnosisInfoModel, err error) {
if row == nil {
return nil, gerror.New("卡片信息为空,不能诊断")
}
simKey, err := s.prepareDiagnosisSimKey(ctx, row)
if err != nil {
return nil, err
}
if cardModel != nil {
if strings.TrimSpace(row.Iccid) != "" {
cardModel.Iccid = strings.TrimSpace(row.Iccid)
}
if strings.TrimSpace(row.Imsi) != "" {
cardModel.Imsi = strings.TrimSpace(row.Imsi)
}
if strings.TrimSpace(row.Msisdn) != "" {
cardModel.Msisdn = strings.TrimSpace(row.Msisdn)
}
}
totalFlow := row.PackageTotalFlowMb
usedFlow := row.UsedFlowMb
remainFlow := totalFlow - usedFlow
if remainFlow < 0 {
remainFlow = 0
}
packageType := fmt.Sprintf("%d", appCardPackageType(row))
hasLocalCard := row.Id > 0
var status *string
var dataStatus *string
var packageTotalFlowMb *float64
var infoUsedFlowMb *float64
var infoRemainFlowMb *float64
if hasLocalCard {
statusValue := fmt.Sprintf("%d", row.BossCardStatus)
dataStatusValue := fmt.Sprintf("%d", row.DataServiceStatus)
status = &statusValue
dataStatus = &dataStatusValue
packageTotalFlowMb = &totalFlow
infoUsedFlowMb = &usedFlow
infoRemainFlowMb = &remainFlow
}
var packageInfo *model.AppDiagnosisPackageItem
if hasLocalCard {
packageInfo = &model.AppDiagnosisPackageItem{
PackageName: appDiagnosisStringPtr(appCardPackageName(row)),
PackageType: appDiagnosisStringPtr(packageType),
EffectiveStatus: appDiagnosisPackageEffectiveStatus(row.PackageStatus),
PackageTotalFlowMb: &totalFlow,
UsedFlowMb: &usedFlow,
RemainFlowMb: &remainFlow,
DurationMonths: appDiagnosisDurationMonths(row.FlowPkgBeginAt, row.FlowPkgEndAt),
ExpectedExpireTime: appDiagnosisTimeString(row.FlowPkgEndAt),
}
}
res = &model.AppDiagnosisInfoModel{
Info: &model.AppDiagnosisInfoItem{
Iccid: appDiagnosisStringPtr(row.Iccid),
Imsi: appDiagnosisStringPtr(row.Imsi),
Msisdn: appDiagnosisStringPtr(row.Msisdn),
Status: status,
DataStatus: dataStatus,
PackageTotalFlowMb: packageTotalFlowMb,
UsedFlowMb: infoUsedFlowMb,
RemainFlowMb: infoRemainFlowMb,
},
Session: &model.AppDiagnosisSessionItem{},
Package: packageInfo,
Modules: make(map[string]*model.AppDiagnosisModuleItem),
Card: cardModel,
}
appSetDiagnosisModule(res, "card", "success", "", false, "")
if hasLocalCard {
appSetDiagnosisModule(res, "package", "success", "", false, "")
} else {
appSetDiagnosisModule(res, "package", "skipped", "本地卡表暂无套餐信息", false, "")
}
if appDiagnosisIsMobileChannel(row.TempKey, row.ChanKey) {
res.Info.HumanLimitStatus = appDiagnosisStringPtr("3")
}
var mu sync.Mutex
updateDiagnosis := func(fn func()) {
mu.Lock()
defer mu.Unlock()
fn()
}
setModule := func(key string, status string, message string, cached bool, updatedAt string) {
updateDiagnosis(func() {
appSetDiagnosisModule(res, key, status, message, cached, updatedAt)
})
}
runDiagnose := func(wg *sync.WaitGroup, fn func()) {
wg.Add(1)
go func() {
defer wg.Done()
fn()
}()
}
var wg sync.WaitGroup
runDiagnose(&wg, func() { s.attachUpstreamBasicInfo(ctx, simKey, row, res, updateDiagnosis, setModule) })
runDiagnose(&wg, func() { s.attachUpstreamCardStatus(ctx, simKey, res, updateDiagnosis, setModule) })
runDiagnose(&wg, func() { s.attachUpstreamSession(ctx, simKey, row, res, updateDiagnosis, setModule) })
runDiagnose(&wg, func() { s.attachUpstreamDeviceStatus(ctx, simKey, row, res, updateDiagnosis, setModule) })
runDiagnose(&wg, func() { s.attachUpstreamBindStatus(ctx, simKey, res, updateDiagnosis, setModule) })
runDiagnose(&wg, func() { s.attachUpstreamRegionLimitStatus(ctx, simKey, res, updateDiagnosis, setModule) })
runDiagnose(&wg, func() { s.attachUpstreamRateLimitStatus(ctx, simKey, res, updateDiagnosis, setModule) })
runDiagnose(&wg, func() { s.attachUpstreamStopReason(ctx, simKey, res, updateDiagnosis, setModule) })
runDiagnose(&wg, func() { s.attachUpstreamFrozenState(ctx, simKey, res, updateDiagnosis, setModule) })
runDiagnose(&wg, func() { s.attachUpstreamCurrentFlowUsage(ctx, simKey, res, updateDiagnosis, setModule) })
wg.Wait()
return res, nil
}
// Flow 查询SIM卡使用流量。小程序诊断完全开放,不校验登录用户和卡片归属。
func (s *sAppDiagnosis) Flow(ctx context.Context, param *model.AppDiagnosisFlowParam) (res *model.AppDiagnosisFlowModel, err error) {
if param == nil {
param = &model.AppDiagnosisFlowParam{}
}
return s.OpenFlow(ctx, &model.AppOpenDiagnosisFlowParam{
QueryCode: param.QueryCode,
QueryType: param.QueryType,
})
}
func (s *sAppDiagnosis) OpenFlow(ctx context.Context, param *model.AppOpenDiagnosisFlowParam) (res *model.AppDiagnosisFlowModel, err error) {
if param == nil {
param = &model.AppOpenDiagnosisFlowParam{}
}
if err = appEnsureOpenDiagnosisRateLimit(ctx); err != nil {
return nil, err
}
appAuditOpenDiagnosis(ctx, "flow", param.QueryCode, param.QueryType)
_, row, err := s.resolveOpenDiagnosisCard(ctx, param.TenantId, param.QueryCode)
if err != nil {
return nil, err
}
return s.queryDiagnosisFlowByCard(ctx, row, param.QueryType)
}
func appEnsureOpenDiagnosisRateLimit(ctx context.Context) error {
clientIp := strings.TrimSpace(utility.GetClientIp(ctx))
if clientIp == "" {
clientIp = "unknown"
}
key := fmt.Sprintf("iotlcm:app:open-diagnosis:rate:%s", appDiagnosisSafeCachePart(clientIp))
value, err := g.Redis().Do(ctx, "INCR", key)
if err != nil {
g.Log().Warningf(ctx, "open diagnosis rate limit unavailable, key:%s, err:%+v", key, err)
return nil
}
count := value.Int()
if count == 1 {
if _, err = g.Redis().Do(ctx, "EXPIRE", key, int(appOpenDiagnosisRateWindow.Seconds())); err != nil {
g.Log().Warningf(ctx, "open diagnosis rate limit expiry failed, key:%s, err:%+v", key, err)
}
}
if count > appOpenDiagnosisRateLimit {
return gerror.New("公开诊断请求过于频繁,请稍后再试")
}
return nil
}
func appAuditOpenDiagnosis(ctx context.Context, kind string, queryCode string, queryType string) {
g.Log().Infof(ctx, "open diagnosis query, kind:%s, queryType:%s, query:%s, clientIp:%s", kind, queryType, appDiagnosisMaskIdentifier(queryCode), utility.GetClientIp(ctx))
}
func appDiagnosisMaskIdentifier(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
if len(value) <= 4 {
return "****"
}
if len(value) <= 8 {
return value[:2] + "****" + value[len(value)-2:]
}
return value[:4] + "****" + value[len(value)-4:]
}
func sanitizeOpenDiagnosisInfo(res *model.AppDiagnosisInfoModel) *model.AppDiagnosisInfoModel {
if res == nil {
return nil
}
if res.Info != nil {
res.Info.Iccid = appDiagnosisMaskedStringPtr(res.Info.Iccid)
res.Info.Imsi = nil
res.Info.Msisdn = appDiagnosisMaskedStringPtr(res.Info.Msisdn)
res.Info.RateLimitApnDetails = nil
}
if res.Session != nil {
res.Session.ApnId = nil
res.Session.Imei = nil
res.Session.Ipv4 = nil
res.Session.Ipv6 = nil
res.Session.Ipv6Prefix = nil
}
if res.Card != nil {
res.Card.Iccid = appDiagnosisMaskIdentifier(res.Card.Iccid)
res.Card.Msisdn = appDiagnosisMaskIdentifier(res.Card.Msisdn)
res.Card.Imsi = ""
res.Card.Imei = ""
res.Card.ApnName = ""
res.Card.ApnId = ""
res.Card.Ipv4 = ""
res.Card.Ipv6 = ""
res.Card.Ipv6Prefix = ""
res.Card.Remark = ""
}
return res
}
func appDiagnosisMaskedStringPtr(value *string) *string {
if value == nil {
return nil
}
return appDiagnosisStringPtr(appDiagnosisMaskIdentifier(*value))
}
func (s *sAppDiagnosis) queryDiagnosisFlowByCard(ctx context.Context, row *appCardQueryRow, queryType string) (*model.AppDiagnosisFlowModel, error) {
switch strings.TrimSpace(queryType) {
case "7d":
return s.queryDiagnosisDailyFlow(ctx, row.Iccid, 7)
case "30d":
return s.queryDiagnosisDailyFlow(ctx, row.Iccid, 30)
case "3M":
return s.queryDiagnosisMonthlyFlow(ctx, row.Iccid, 3)
case "6M":
return s.queryDiagnosisMonthlyFlow(ctx, row.Iccid, 6)
default:
return nil, gerror.New("查询类型只能为7d、30d、3M或6M")
}
}
func (s *sAppDiagnosis) resolveDiagnosisCard(ctx context.Context, queryCode string) (*entity.SysUser, *sAppCard, *appCardQueryRow, error) {
queryCode = strings.TrimSpace(queryCode)
if queryCode == "" {
return nil, nil, nil, gerror.New("请输入ICCID、MSISDN、IMSI或卡号")
}
user, err := appCurrentUser(ctx)
if err != nil {
return nil, nil, nil, err
}
cardLogic := NewAppCard()
row, err := cardLogic.findDiagnosisCard(ctx, user, queryCode)
if err != nil {
return nil, nil, nil, gerror.Wrap(err, "查询诊断卡片失败")
}
if row == nil || row.Id == 0 {
openRow, openErr := cardLogic.findOpenCard(ctx, "", queryCode)
if openErr != nil {
return nil, nil, nil, gerror.Wrap(openErr, "查询诊断卡片失败")
}
if openRow != nil && openRow.Id > 0 {
return nil, nil, nil, gerror.New("卡片不存在")
}
row, err = s.resolveAssocOnlyDiagnosisCard(ctx, cardLogic, user.TenantId, queryCode, "查询诊断卡片")
if err != nil {
return nil, nil, nil, err
}
}
if row == nil {
return nil, nil, nil, gerror.New("卡片不存在")
}
if err = s.resolveDiagnosisCardChannel(ctx, cardLogic, row, queryCode, "查询诊断卡片"); err != nil {
return nil, nil, nil, err
}
return user, cardLogic, row, nil
}
func (s *sAppDiagnosis) resolveOpenDiagnosisCard(ctx context.Context, tenantId string, queryCode string) (*sAppCard, *appCardQueryRow, error) {
queryCode = strings.TrimSpace(queryCode)
if queryCode == "" {
return nil, nil, gerror.New("请输入ICCID、MSISDN、IMSI或卡号")
}
cardLogic := NewAppCard()
tenantId = strings.TrimSpace(tenantId)
row, err := cardLogic.findOpenCard(ctx, tenantId, queryCode)
if err != nil {
return nil, nil, gerror.Wrap(err, "查询公开诊断卡片失败")
}
if row == nil || row.Id == 0 {
row, err = s.resolveAssocOnlyDiagnosisCard(ctx, cardLogic, tenantId, queryCode, "查询公开诊断卡片")
if err != nil {
return nil, nil, err
}
}
if row == nil {
return nil, nil, gerror.New("卡片不存在")
}
if err = s.resolveDiagnosisCardChannel(ctx, cardLogic, row, queryCode, "查询公开诊断卡片"); err != nil {
return nil, nil, err
}
return cardLogic, row, nil
}
func (s *sAppDiagnosis) resolveAssocOnlyDiagnosisCard(ctx context.Context, cardLogic *sAppCard, tenantId string, queryCode string, operation string) (*appCardQueryRow, error) {
if cardLogic == nil {
return nil, nil
}
queryCode = strings.TrimSpace(queryCode)
if queryCode == "" {
return nil, nil
}
info, err := cardLogic.findAssocCardChannelInfo(ctx, tenantId, queryCode)
if err != nil {
return nil, gerror.Wrap(err, operation+"失败")
}
if info == nil || strings.TrimSpace(info.ChanKey) == "" {
return nil, nil
}
return &appCardQueryRow{
TSimCardInfo: entity.TSimCardInfo{
TenantId: tenantId,
Iccid: queryCode,
ChanKey: info.ChanKey,
},
ChanName: info.ChanName,
ChanAlias: info.ChanAlias,
TempKey: info.TempKey,
}, nil
}
func (s *sAppDiagnosis) resolveDiagnosisCardChannel(ctx context.Context, cardLogic *sAppCard, row *appCardQueryRow, queryCode string, operation string) error {
if cardLogic == nil || row == nil {
return nil
}
applyInfo := func(info *appCardChannelInfo) bool {
if info == nil || strings.TrimSpace(info.ChanKey) == "" {
return false
}
row.ChanKey = info.ChanKey
row.ChanName = info.ChanName
row.ChanAlias = info.ChanAlias
row.TempKey = info.TempKey
return true
}
var assocErr error
for _, value := range []string{queryCode, row.Iccid} {
value = strings.TrimSpace(value)
if value == "" {
continue
}
info, err := cardLogic.findAssocCardChannelInfo(ctx, row.TenantId, value)
if err != nil {
g.Log().Warningf(ctx, "app diagnosis resolve assoc channel failed, value:%s, err:%+v", appDiagnosisMaskIdentifier(value), err)
if assocErr == nil {
assocErr = err
}
continue
}
if applyInfo(info) {
return nil
}
}
if strings.TrimSpace(row.ChanKey) != "" {
return nil
}
if assocErr != nil {
return gerror.Wrap(assocErr, operation+"失败")
}
return cardLogic.attachDiagnosisCardChannelInfo(ctx, row)
}
func (s *sAppDiagnosis) prepareDiagnosisSimKey(ctx context.Context, row *appCardQueryRow) (*appUpstreamSimKey, error) {
simKey, err := appUpstreamSimKeyFromRow(row)
if err != nil {
return nil, err
}
if strings.TrimSpace(simKey.MSISDN) != "" && strings.TrimSpace(simKey.IMSI) != "" {
return simKey, nil
}
threeCodes, err := s.resolveDiagnosisThreeCodes(ctx, simKey)
if err != nil {
g.Log().Warningf(ctx, "app diagnosis resolve three codes failed, iccid:%s, chanKey:%s, err:%+v", simKey.ICCID, simKey.ChanKey, err)
return simKey, nil
}
if threeCodes == nil {
return simKey, nil
}
if value := strings.TrimSpace(threeCodes.ICCID); value != "" {
simKey.ICCID = value
row.Iccid = value
}
if value := strings.TrimSpace(threeCodes.IMSI); value != "" {
simKey.IMSI = value
row.Imsi = value
}
if value := strings.TrimSpace(threeCodes.MSISDN); value != "" {
simKey.MSISDN = value
row.Msisdn = value
}
return simKey, nil
}
func (s *sAppDiagnosis) resolveDiagnosisThreeCodes(ctx context.Context, simKey *appUpstreamSimKey) (*appUpstreamSimKey, error) {
items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-basic-info", map[string]any{})
if err != nil {
return nil, err
}
item := appDiagnosisFirstAvailableItem(items)
if item == nil || item.Code == 5000 || appDiagnosisIsUnsupportedMessage(item.Message) {
return nil, nil
}
if item.Code != 0 {
return nil, gerror.New(appDiagnosisModuleMessage(item, "上游基础信息查询失败"))
}
threeCodes := &appUpstreamSimKey{ChanKey: strings.TrimSpace(simKey.ChanKey)}
if item.SimKey != nil {
threeCodes.ICCID = strings.TrimSpace(item.SimKey.ICCID)
threeCodes.IMSI = strings.TrimSpace(item.SimKey.IMSI)
threeCodes.MSISDN = strings.TrimSpace(item.SimKey.MSISDN)
}
if value := appDiagnosisRawString(item.ResData, "iccid", "ICCID"); value != "" {
threeCodes.ICCID = value
}
if value := appDiagnosisRawString(item.ResData, "imsi", "IMSI"); value != "" {
threeCodes.IMSI = value
}
if value := appDiagnosisRawString(item.ResData, "msisdn", "MSISDN"); value != "" {
threeCodes.MSISDN = value
}
if threeCodes.ICCID == "" && threeCodes.IMSI == "" && threeCodes.MSISDN == "" {
return nil, nil
}
return threeCodes, nil
}
func (s *sAppDiagnosis) attachUpstreamBasicInfo(ctx context.Context, simKey *appUpstreamSimKey, row *appCardQueryRow, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) {
items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-basic-info", map[string]any{})
if err != nil {
g.Log().Warningf(ctx, "app diagnosis query sim basic info failed, iccid:%s, err:%+v", row.Iccid, err)
setModule("basicInfo", "error", err.Error(), false, "")
return
}
item := appDiagnosisFirstAvailableItem(items)
if item == nil {
setModule("basicInfo", "skipped", "上游基础信息未返回有效结果", false, "")
return
}
if item.Code != 0 {
if item.Code == 5000 {
setModule("basicInfo", "skipped", appDiagnosisModuleMessage(item, "上游基础信息暂不支持"), false, "")
} else {
status, message := appDiagnosisUpstreamFailure("上游基础信息查询失败", item.Code, item.Message)
setModule("basicInfo", status, message, false, "")
}
return
}
update(func() {
if value := appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "iccid", "ICCID")); value != nil {
res.Info.Iccid = value
}
if value := appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "imsi", "IMSI")); value != nil {
res.Info.Imsi = value
}
if value := appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "msisdn", "MSISDN")); value != nil {
res.Info.Msisdn = value
}
if status := appDiagnosisCardStatus(appDiagnosisRawString(item.ResData, "simStatus", "sim_status", "cardStatus", "card_status", "status")); status != nil {
res.Info.Status = status
}
if res.Info.RegionLimitType == nil {
res.Info.RegionLimitType = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "areaLimitType", "area_limit_type"))
}
if res.Info.RegionLimitArea == nil {
res.Info.RegionLimitArea = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "areaLimitInfo", "area_limit_info"))
}
rawHumanLimitStatus := appDiagnosisRawString(item.ResData, "isHumanLimit", "is_human_limit")
if rawHumanLimitStatus != "" {
humanLimitStatus := appDiagnosisHumanLimitStatus(rawHumanLimitStatus)
if humanLimitStatus != nil && *humanLimitStatus != "3" {
res.Info.HumanLimitStatus = humanLimitStatus
}
}
})
setModule("basicInfo", "success", "", false, "")
}
func (s *sAppDiagnosis) attachUpstreamCardStatus(ctx context.Context, simKey *appUpstreamSimKey, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) {
items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-status", map[string]any{})
if err != nil {
g.Log().Warningf(ctx, "app diagnosis query sim card status failed, iccid:%s, err:%+v", simKey.ICCID, err)
setModule("cardStatus", "error", err.Error(), false, "")
return
}
item := appDiagnosisFirstAvailableItem(items)
if item == nil {
setModule("cardStatus", "skipped", "上游卡状态未返回有效结果", false, "")
return
}
if item.Code == 5000 {
setModule("cardStatus", "skipped", appDiagnosisModuleMessage(item, "上游卡状态暂不支持"), false, "")
return
}
if item.Code != 0 {
status, message := appDiagnosisUpstreamFailure("上游卡状态查询失败", item.Code, item.Message)
setModule("cardStatus", status, message, false, "")
return
}
status := appDiagnosisCardStatus(appDiagnosisRawString(item.ResData, "cardStatus", "card_status", "simStatus", "sim_status", "status"))
if status == nil {
setModule("cardStatus", "skipped", "上游卡状态未返回有效结果", false, "")
return
}
update(func() {
res.Info.Status = status
if res.Card != nil {
if value, err := strconv.Atoi(*status); err == nil {
res.Card.BossCardStatus = value
}
}
})
setModule("cardStatus", "success", "", false, "")
}
func (s *sAppDiagnosis) attachUpstreamSession(ctx context.Context, simKey *appUpstreamSimKey, row *appCardQueryRow, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) {
session, source, err := queryAppCardRealtimeSessionWithKey(ctx, row, simKey)
if err != nil {
g.Log().Warningf(ctx, "app diagnosis query sim session failed, iccid:%s, err:%+v", row.Iccid, err)
setModule("session", "error", err.Error(), false, "")
return
}
if source == "onelink_mock" {
g.Log().Debugf(ctx, "app diagnosis sim session loaded from onelink mock, iccid:%s", row.Iccid)
}
if session == nil || session.isZero() {
setModule("session", "skipped", "上游会话信息未返回有效结果", false, "")
return
}
update(func() {
sessionStatus := appDiagnosisSessionStatus(session.Status)
res.Info.OnlineStatus = appDiagnosisOnlineStatus(sessionStatus)
res.Session.ApnId = appDiagnosisStringPtr(session.ApnId)
res.Session.Rat = appDiagnosisStringPtr(session.Rat)
res.Session.Imei = appDiagnosisStringPtr(session.Imei)
res.Session.CreateTime = appDiagnosisStringPtr(session.CreateTime)
res.Session.Ipv4 = appDiagnosisStringPtr(session.Ipv4)
res.Session.Ipv6 = appDiagnosisStringPtr(session.Ipv6)
res.Session.Ipv6Prefix = appDiagnosisStringPtr(session.Ipv6Prefix)
res.Session.Status = appDiagnosisStringPtr(sessionStatus)
})
message := ""
if source == "onelink_mock" {
message = "已使用OneLink挡板会话结果"
}
setModule("session", "success", message, false, "")
}
func (s *sAppDiagnosis) attachUpstreamDeviceStatus(ctx context.Context, simKey *appUpstreamSimKey, row *appCardQueryRow, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) {
items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-device-status", map[string]any{})
if err != nil {
g.Log().Warningf(ctx, "app diagnosis query sim device status failed, iccid:%s, err:%+v", row.Iccid, err)
setModule("device", "error", err.Error(), false, "")
return
}
item := appDiagnosisFirstAvailableItem(items)
if item == nil {
setModule("device", "skipped", "上游设备状态未返回有效结果", false, "")
return
}
if item.Code == 5000 {
update(func() {
res.Info.DeviceStatus = nil
})
setModule("device", "skipped", appDiagnosisModuleMessage(item, "上游设备状态暂不支持"), false, "")
return
}
if item.Code != 0 {
status, message := appDiagnosisUpstreamFailure("上游设备状态查询失败", item.Code, item.Message)
setModule("device", status, message, false, "")
return
}
update(func() {
res.Info.DeviceStatus = appDiagnosisDeviceStatus(appDiagnosisRawString(item.ResData, "deviceStatus", "device_status"))
res.Info.OperatingStatus = appDiagnosisOperatingStatus(appDiagnosisRawString(item.ResData, "operatingStatus", "operating_status"))
})
setModule("device", "success", "", false, "")
}
func (s *sAppDiagnosis) attachUpstreamBindStatus(ctx context.Context, simKey *appUpstreamSimKey, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) {
items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-card-bind-status", map[string]any{"testType": 0})
if err != nil {
g.Log().Warningf(ctx, "app diagnosis query sim bind status failed, iccid:%s, err:%+v", simKey.ICCID, err)
setModule("cardBind", "error", err.Error(), false, "")
return
}
item := appDiagnosisFirstAvailableItem(items)
if item == nil {
setModule("cardBind", "skipped", "上游机卡绑定状态未返回有效结果", false, "")
return
}
if item.Code == 5000 {
update(func() {
res.Info.ImeiStatus = nil
})
setModule("cardBind", "skipped", appDiagnosisModuleMessage(item, "上游机卡绑定状态暂不支持"), false, "")
return
}
if item.Code != 0 {
status, message := appDiagnosisUpstreamFailure("上游机卡绑定状态查询失败", item.Code, item.Message)
setModule("cardBind", status, message, false, "")
return
}
bindStatus := appDiagnosisBindStatus(
appDiagnosisRawString(item.ResData, "bindStatus", "bind_status", "cardBindStatus"),
appDiagnosisRawString(item.ResData, "cardBindingType", "card_binding_type"),
)
update(func() {
res.Info.ImeiStatus = appDiagnosisImeiStatus(bindStatus)
})
setModule("cardBind", "success", "", false, "")
}
func (s *sAppDiagnosis) attachUpstreamRegionLimitStatus(ctx context.Context, simKey *appUpstreamSimKey, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) {
if cachedRegion, cachedAt, ok := appGetCachedDiagnosisRegionLimit(ctx, simKey); ok {
update(func() {
appDiagnosisApplyRegionLimit(res, cachedRegion)
})
status, message := appCachedDiagnosisRegionModule(cachedRegion)
setModule("region", status, message, true, cachedAt)
return
}
items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-region-limit-status", map[string]any{})
if err != nil {
g.Log().Warningf(ctx, "app diagnosis query sim region limit status failed, iccid:%s, err:%+v", simKey.ICCID, err)
setModule("region", "error", err.Error(), false, "")
return
}
item := appDiagnosisFirstAvailableItem(items)
if item == nil {
setModule("region", "skipped", "上游区域限制未返回有效结果", false, "")
return
}
update(func() {
appDiagnosisApplyRegionLimit(res, item)
})
appSetCachedDiagnosisRegionLimit(ctx, simKey, *item)
if appDiagnosisRegionLimitHasData(item) {
message := ""
if item.Code != 0 {
message = appDiagnosisModuleMessage(item, "区域限制查询已返回有效结果")
}
setModule("region", "success", message, false, "")
return
}
if item.Code == 5000 {
setModule("region", "skipped", appDiagnosisModuleMessage(item, "上游区域限制暂不支持"), false, "")
return
}
if item.Code != 0 {
status, message := appDiagnosisUpstreamFailure("上游区域限制查询失败", item.Code, item.Message)
setModule("region", status, message, false, "")
return
}
setModule("region", "error", "上游区域限制未返回有效数据", false, "")
}
func (s *sAppDiagnosis) attachUpstreamRateLimitStatus(ctx context.Context, simKey *appUpstreamSimKey, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) {
items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-data-diagnosis", map[string]any{})
if err != nil {
g.Log().Warningf(ctx, "app diagnosis query sim data diagnosis failed, iccid:%s, err:%+v", simKey.ICCID, err)
setModule("rateLimit", "error", err.Error(), false, "")
return
}
item := appDiagnosisFirstAvailableItem(items)
if item == nil {
setModule("rateLimit", "skipped", "上游达量限速未返回有效结果", false, "")
return
}
update(func() {
appDiagnosisApplyRateLimit(res, item)
})
if item.Code == 5000 {
setModule("rateLimit", "skipped", appDiagnosisModuleMessage(item, "上游达量限速暂不支持"), false, "")
return
}
if item.Code != 0 {
status, message := appDiagnosisUpstreamFailure("上游达量限速查询失败", item.Code, item.Message)
setModule("rateLimit", status, message, false, "")
return
}
setModule("rateLimit", "success", "", false, "")
}
func (s *sAppDiagnosis) attachUpstreamStopReason(ctx context.Context, simKey *appUpstreamSimKey, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) {
items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-stop-reason", map[string]any{})
if err != nil {
g.Log().Warningf(ctx, "app diagnosis query sim stop reason failed, iccid:%s, err:%+v", simKey.ICCID, err)
setModule("stopReason", "error", err.Error(), false, "")
return
}
item := appDiagnosisFirstAvailableItem(items)
if item == nil {
setModule("stopReason", "skipped", "上游停机原因未返回有效结果", false, "")
return
}
if item.Code == 5000 {
setModule("stopReason", "skipped", appDiagnosisModuleMessage(item, "上游停机原因暂不支持"), false, "")
return
}
if item.Code != 0 {
status, message := appDiagnosisUpstreamFailure("上游停机原因查询失败", item.Code, item.Message)
setModule("stopReason", status, message, false, "")
return
}
update(func() {
res.Info.StopReason = appDiagnosisStringPtr(appDiagnosisNormalizeStopReason(appDiagnosisRawString(item.ResData, "stopReason", "stop_reason")))
})
setModule("stopReason", "success", "", false, "")
}
func (s *sAppDiagnosis) attachUpstreamFrozenState(ctx context.Context, simKey *appUpstreamSimKey, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) {
items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-frozen-state", map[string]any{})
if err != nil {
g.Log().Warningf(ctx, "app diagnosis query sim frozen state failed, iccid:%s, err:%+v", simKey.ICCID, err)
setModule("frozen", "error", err.Error(), false, "")
return
}
item := appDiagnosisFirstAvailableItem(items)
if item == nil {
setModule("frozen", "skipped", "上游冻结状态未返回有效结果", false, "")
return
}
if item.Code == 5000 {
setModule("frozen", "skipped", appDiagnosisModuleMessage(item, "上游冻结状态暂不支持"), false, "")
return
}
if item.Code != 0 {
status, message := appDiagnosisUpstreamFailure("上游冻结状态查询失败", item.Code, item.Message)
setModule("frozen", status, message, false, "")
return
}
update(func() {
res.Info.FrozenStatus = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "frozenStatus", "frozen_status"))
res.Info.FrozenReason = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "frozenReason", "frozen_reason"))
res.Info.FrozenOperTime = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "operTime", "oper_time"))
})
setModule("frozen", "success", "", false, "")
}
func (s *sAppDiagnosis) attachUpstreamCurrentFlowUsage(ctx context.Context, simKey *appUpstreamSimKey, res *model.AppDiagnosisInfoModel, update appDiagnosisUpdateFunc, setModule appDiagnosisSetModuleFunc) {
items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-datausage", map[string]any{})
if err != nil {
g.Log().Warningf(ctx, "app diagnosis query current flow usage failed, iccid:%s, err:%+v", simKey.ICCID, err)
setModule("flowUsage", "error", err.Error(), false, "")
return
}
item := appDiagnosisFirstAvailableItem(items)
if item == nil {
setModule("flowUsage", "skipped", "上游实时流量未返回有效结果", false, "")
return
}
if item.Code == 5000 {
setModule("flowUsage", "skipped", appDiagnosisModuleMessage(item, "上游实时流量暂不支持"), false, "")
return
}
if item.Code != 0 {
status, message := appDiagnosisUpstreamFailure("上游实时流量查询失败", item.Code, item.Message)
setModule("flowUsage", status, message, false, "")
return
}
usedFlow, ok := appDiagnosisRawFloat(item.ResData, "dataUsage", "data_usage")
if !ok {
setModule("flowUsage", "skipped", "上游实时流量未返回有效结果", false, "")
return
}
if usedFlow < 0 {
usedFlow = 0
}
update(func() {
now := gtime.Now()
nowText := now.Format("Y-m-d H:i:s")
totalFlow := float64(0)
if res.Package != nil && res.Package.PackageTotalFlowMb != nil {
totalFlow = *res.Package.PackageTotalFlowMb
}
remainFlow := totalFlow - usedFlow
if remainFlow < 0 {
remainFlow = 0
}
if res.Package != nil {
res.Package.UsedFlowMb = &usedFlow
if totalFlow > 0 {
res.Package.RemainFlowMb = &remainFlow
} else {
res.Package.RemainFlowMb = nil
}
}
if res.Info != nil {
res.Info.UsedFlowMb = &usedFlow
if totalFlow > 0 {
res.Info.RemainFlowMb = &remainFlow
} else {
res.Info.RemainFlowMb = nil
}
res.Info.FlowSyncTime = &nowText
}
if res.Card != nil {
res.Card.UsedFlowMb = usedFlow
res.Card.RemainFlowMb = remainFlow
if totalFlow > 0 {
res.Card.UsedFlowPercent = usedFlow / totalFlow * 100
} else {
res.Card.UsedFlowPercent = 0
}
res.Card.FlowSyncTime = now
}
})
setModule("flowUsage", "success", "", false, "")
}
func appSetDiagnosisModule(res *model.AppDiagnosisInfoModel, key string, status string, message string, cached bool, updatedAt string) {
if res == nil {
return
}
if res.Modules == nil {
res.Modules = make(map[string]*model.AppDiagnosisModuleItem)
}
message = appDiagnosisNormalizeCustomerMessage(message)
res.Modules[key] = &model.AppDiagnosisModuleItem{
Status: strings.TrimSpace(status),
Message: strings.TrimSpace(message),
Cached: cached,
UpdatedAt: updatedAt,
}
}
func appDiagnosisModuleMessage(item *appUpstreamSimQueryItem, fallback string) string {
if item == nil || strings.TrimSpace(item.Message) == "" {
return fallback
}
return strings.TrimSpace(item.Message)
}
func appDiagnosisRegionCacheKey(simKey *appUpstreamSimKey) string {
if simKey == nil {
return "iotlcm:sim-diagnose:region:unknown"
}
parts := []string{
appDiagnosisSafeCachePart(simKey.ChanKey),
appDiagnosisSafeCachePart(simKey.ICCID),
appDiagnosisSafeCachePart(simKey.IMSI),
appDiagnosisSafeCachePart(simKey.MSISDN),
}
return "iotlcm:sim-diagnose:region:" + strings.Join(parts, ":")
}
func appDiagnosisSafeCachePart(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return "-"
}
replacer := strings.NewReplacer(":", "_", "\n", "_", "\r", "_", "\t", "_", " ", "_")
return replacer.Replace(value)
}
func appGetCachedDiagnosisRegionLimit(ctx context.Context, simKey *appUpstreamSimKey) (*appUpstreamSimQueryItem, string, bool) {
cacheKey := appDiagnosisRegionCacheKey(simKey)
value, err := g.Redis().Get(ctx, cacheKey)
if err != nil || value == nil || strings.TrimSpace(value.String()) == "" {
return nil, "", false
}
var payload appDiagnosisRegionCachePayload
if err = json.Unmarshal([]byte(value.String()), &payload); err != nil {
g.Log().Warningf(ctx, "app diagnosis unmarshal region cache failed, key:%s, err:%+v", cacheKey, err)
return nil, "", false
}
return &payload.Item, payload.CachedAt, true
}
func appSetCachedDiagnosisRegionLimit(ctx context.Context, simKey *appUpstreamSimKey, item appUpstreamSimQueryItem) {
payload := appDiagnosisRegionCachePayload{
Item: item,
CachedAt: gtime.Now().Format("Y-m-d H:i:s"),
}
raw, err := json.Marshal(payload)
if err != nil {
g.Log().Warningf(ctx, "app diagnosis marshal region cache failed, iccid:%s, err:%+v", appDiagnosisSimKeyLogIccid(simKey), err)
return
}
if _, err = g.Redis().Do(ctx, "SETEX", appDiagnosisRegionCacheKey(simKey), int(appDiagnosisRegionCacheTTL.Seconds()), string(raw)); err != nil {
g.Log().Warningf(ctx, "app diagnosis set region cache failed, iccid:%s, err:%+v", appDiagnosisSimKeyLogIccid(simKey), err)
}
}
func appCachedDiagnosisRegionModule(item *appUpstreamSimQueryItem) (status string, message string) {
if item == nil {
return "error", "区域限制结果来自缓存,但缓存内容为空"
}
message = strings.TrimSpace(item.Message)
if message == "" {
message = "区域限制结果来自缓存"
} else {
message = "区域限制结果来自缓存:" + message
}
if appDiagnosisRegionLimitHasData(item) {
return "cached", message
}
switch item.Code {
case 0:
return "error", "区域限制结果来自缓存,但缓存内容为空"
case 5000:
return "skipped", message
default:
return "error", message
}
}
func appDiagnosisApplyRegionLimit(res *model.AppDiagnosisInfoModel, item *appUpstreamSimQueryItem) {
if res == nil || res.Info == nil || item == nil {
return
}
hasData := appDiagnosisRegionLimitHasData(item)
if item.Code == 5000 && !hasData {
res.Info.RegionLimitStatus = nil
return
}
if item.Code != 0 && !hasData {
return
}
res.Info.RegionLimitStatus = appDiagnosisRegionLimitStatus(appDiagnosisRawString(item.ResData, "regionLimitStatus", "region_limit_status"))
res.Info.RegionLimitOrderStatus = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "orderStatus", "order_status"))
res.Info.RegionLimitTriggerStatus = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "limitStatus", "limit_status"))
res.Info.RegionLimitType = appDiagnosisFirstStringPtr(
appDiagnosisRawString(item.ResData, "limitType", "limit_type"),
appDiagnosisPtrValue(res.Info.RegionLimitType),
)
res.Info.RegionLimitArea = appDiagnosisFirstStringPtr(
appDiagnosisRawString(item.ResData, "limitArea", "limit_area"),
appDiagnosisPtrValue(res.Info.RegionLimitArea),
)
}
func appDiagnosisRegionLimitHasData(item *appUpstreamSimQueryItem) bool {
if item == nil {
return false
}
for _, value := range []string{
appDiagnosisRawString(item.ResData, "regionLimitStatus", "region_limit_status"),
appDiagnosisRawString(item.ResData, "orderStatus", "order_status"),
appDiagnosisRawString(item.ResData, "limitStatus", "limit_status"),
appDiagnosisRawString(item.ResData, "limitType", "limit_type"),
appDiagnosisRawString(item.ResData, "limitArea", "limit_area"),
} {
if strings.TrimSpace(value) != "" {
return true
}
}
return false
}
func appDiagnosisApplyRateLimit(res *model.AppDiagnosisInfoModel, item *appUpstreamSimQueryItem) {
if res == nil || res.Info == nil || item == nil {
return
}
if item.Code == 5000 {
res.Info.RateLimitStatus = nil
res.Info.RateLimitReason = nil
res.Info.RateLimitDetail = nil
res.Info.RateLimitApnDetails = nil
return
}
if item.Code != 0 {
return
}
res.Info.RateLimitStatus = appDiagnosisRateLimitStatus(appDiagnosisRawString(item.ResData, "rateLimitStatus", "rate_limit_status"))
res.Info.RateLimitReason = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "rateLimitReason", "rate_limit_reason"))
res.Info.RateLimitDetail = appDiagnosisStringPtr(appDiagnosisRawString(item.ResData, "rateLimitDetail", "rate_limit_detail"))
res.Info.RateLimitApnDetails = appDiagnosisRateLimitApnDetails(item.ResData)
}
func appDiagnosisIsMobileChannel(tempKey string, chanKey string) bool {
value := strings.ToLower(strings.TrimSpace(tempKey + " " + chanKey))
return strings.Contains(value, "sim_yd") || strings.Contains(value, "yd_mock")
}
func appDiagnosisIsTelecomChannel(tempKey string, chanKey string) bool {
value := strings.ToLower(strings.TrimSpace(tempKey + " " + chanKey))
return strings.Contains(value, "sim_dx") || strings.Contains(value, "dx_")
}
func appDiagnosisNormalizeStopReason(stopReason string) string {
switch strings.TrimSpace(stopReason) {
case "", "000000000000":
return "未停机或系统暂无停机原因"
default:
return strings.TrimSpace(stopReason)
}
}
func appDiagnosisFirstAvailableItem(items []appUpstreamSimQueryItem) *appUpstreamSimQueryItem {
if len(items) == 0 {
return nil
}
return &items[0]
}
func appDiagnosisRateLimitApnDetails(raw json.RawMessage) []model.AppDiagnosisRateLimitApnItem {
raw = appDiagnosisUnwrapResData(raw)
if len(raw) == 0 || string(raw) == "null" {
return nil
}
payload := struct {
ApnDetails []model.AppDiagnosisRateLimitApnItem `json:"apnDetails"`
}{}
if err := json.Unmarshal(raw, &payload); err != nil || len(payload.ApnDetails) == 0 {
return nil
}
details := make([]model.AppDiagnosisRateLimitApnItem, 0, len(payload.ApnDetails))
for _, apn := range payload.ApnDetails {
item := model.AppDiagnosisRateLimitApnItem{
ApnName: strings.TrimSpace(apn.ApnName),
ApnStatus: strings.TrimSpace(apn.ApnStatus),
ModifyTime: strings.TrimSpace(apn.ModifyTime),
OverUseDetails: strings.TrimSpace(apn.OverUseDetails),
ApnServiceUsageState: strings.TrimSpace(apn.ApnServiceUsageState),
}
for _, pcc := range apn.PccDetails {
pccItem := model.AppDiagnosisRateLimitPccItem{
PccServiceCode: strings.TrimSpace(pcc.PccServiceCode),
PccName: strings.TrimSpace(pcc.PccName),
PccPolicyStatus: strings.TrimSpace(pcc.PccPolicyStatus),
PccServiceUsageState: strings.TrimSpace(pcc.PccServiceUsageState),
ModifyTime: strings.TrimSpace(pcc.ModifyTime),
}
if appDiagnosisHasRateLimitPccDetail(pccItem) {
item.PccDetails = append(item.PccDetails, pccItem)
}
}
if appDiagnosisHasRateLimitApnDetail(item) {
details = append(details, item)
}
}
return details
}
func appDiagnosisUnwrapResData(raw json.RawMessage) json.RawMessage {
if len(raw) == 0 || string(raw) == "null" {
return nil
}
wrapper := struct {
ResData json.RawMessage `json:"resData"`
}{}
if err := json.Unmarshal(raw, &wrapper); err == nil && len(wrapper.ResData) > 0 && string(wrapper.ResData) != "null" {
return wrapper.ResData
}
return raw
}
func appDiagnosisHasRateLimitApnDetail(item model.AppDiagnosisRateLimitApnItem) bool {
return appDiagnosisFirstNonEmptyString(
item.ApnName,
item.ApnStatus,
item.ModifyTime,
item.OverUseDetails,
item.ApnServiceUsageState,
) != "" || len(item.PccDetails) > 0
}
func appDiagnosisHasRateLimitPccDetail(item model.AppDiagnosisRateLimitPccItem) bool {
return appDiagnosisFirstNonEmptyString(
item.PccServiceCode,
item.PccName,
item.PccPolicyStatus,
item.PccServiceUsageState,
item.ModifyTime,
) != ""
}
func appDiagnosisRawString(raw json.RawMessage, keys ...string) string {
if len(raw) == 0 || string(raw) == "null" {
return ""
}
var payload map[string]any
if err := json.Unmarshal(raw, &payload); err != nil || len(payload) == 0 {
return ""
}
if nested, ok := payload["resData"]; ok && nested != nil {
if nestedRaw, err := json.Marshal(nested); err == nil {
if value := appDiagnosisRawString(nestedRaw, keys...); value != "" {
return value
}
}
}
for _, key := range keys {
if value, ok := payload[key]; ok && value != nil {
text := strings.TrimSpace(fmt.Sprint(value))
if text != "" && text != "<nil>" {
return text
}
}
}
return ""
}
func appDiagnosisRawFloat(raw json.RawMessage, keys ...string) (float64, bool) {
if len(raw) == 0 || string(raw) == "null" {
return 0, false
}
var payload map[string]any
if err := json.Unmarshal(raw, &payload); err != nil || len(payload) == 0 {
return 0, false
}
if nested, ok := payload["resData"]; ok && nested != nil {
if nestedRaw, err := json.Marshal(nested); err == nil {
if value, ok := appDiagnosisRawFloat(nestedRaw, keys...); ok {
return value, true
}
}
}
for _, key := range keys {
value, ok := payload[key]
if !ok || value == nil {
continue
}
switch typed := value.(type) {
case float64:
return typed, true
case float32:
return float64(typed), true
case int:
return float64(typed), true
case int64:
return float64(typed), true
case json.Number:
parsed, err := typed.Float64()
return parsed, err == nil
default:
text := strings.TrimSpace(fmt.Sprint(value))
if text == "" || text == "<nil>" {
continue
}
parsed, err := strconv.ParseFloat(text, 64)
return parsed, err == nil
}
}
return 0, false
}
func appDiagnosisFirstNonEmptyString(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func appDiagnosisSimKeyLogIccid(simKey *appUpstreamSimKey) string {
if simKey == nil {
return ""
}
return strings.TrimSpace(simKey.ICCID)
}
func appDiagnosisStringPtr(value string) *string {
value = strings.TrimSpace(value)
if value == "" {
return nil
}
return &value
}
func appDiagnosisFirstStringPtr(values ...string) *string {
for _, value := range values {
if ptr := appDiagnosisStringPtr(value); ptr != nil {
return ptr
}
}
return nil
}
func appDiagnosisPtrValue(value *string) string {
if value == nil {
return ""
}
return *value
}
func appDiagnosisTimeString(value *gtime.Time) *string {
if value == nil || value.IsZero() {
return nil
}
text := value.Format("Y-m-d H:i:s")
return &text
}
func appDiagnosisDurationMonths(begin, end *gtime.Time) *int {
if begin == nil || begin.IsZero() || end == nil || end.IsZero() {
return nil
}
beginTime := begin.Time
endTime := end.Time
months := (endTime.Year()-beginTime.Year())*12 + int(endTime.Month()-beginTime.Month())
if endTime.Day() >= beginTime.Day()-1 {
months++
}
if months < 0 {
return nil
}
return &months
}
func appDiagnosisOnlineStatus(status string) *string {
var value string
switch strings.ToUpper(strings.TrimSpace(status)) {
case "0", "01", "ONLINE":
value = "1"
case "-1", "00", "OFFLINE":
value = "0"
case "-2", "02", "2", "UNKNOWN":
value = "2"
default:
value = "2"
}
return &value
}
func appDiagnosisSessionStatus(status string) string {
switch strings.ToUpper(strings.TrimSpace(status)) {
case "0", "01", "ONLINE":
return "0"
case "-1", "00", "OFFLINE":
return "-1"
case "-2", "02", "2", "UNKNOWN", "":
return "-2"
default:
return "-2"
}
}
func appDiagnosisDeviceStatus(status string) *string {
var value string
switch strings.TrimSpace(status) {
case "0":
value = "0"
case "1":
value = "1"
default:
return nil
}
return &value
}
func appDiagnosisOperatingStatus(status string) *string {
switch strings.TrimSpace(status) {
case "0":
value := "0"
return &value
case "1":
value := "1"
return &value
default:
return nil
}
}
func appDiagnosisImeiStatus(status string) *string {
var value string
switch strings.TrimSpace(status) {
case "0":
value = "0"
case "1":
value = "1"
case "2":
value = "2"
default:
value = "2"
}
return &value
}
func appDiagnosisBindStatus(bindStatus string, cardBindingType string) string {
switch strings.TrimSpace(bindStatus) {
case "0", "1", "2":
return strings.TrimSpace(bindStatus)
}
switch strings.TrimSpace(cardBindingType) {
case "1", "2", "3", "4", "5":
return "1"
case "", "-1":
return "2"
default:
return "2"
}
}
func appDiagnosisCardStatus(status string) *string {
switch strings.TrimSpace(status) {
case "":
return nil
case "0", "未知":
return appDiagnosisCardStatusPtr(0)
case "1", "待激活":
return appDiagnosisCardStatusPtr(1)
case "2", "已激活", "正常", "在用":
return appDiagnosisCardStatusPtr(2)
case "4", "停机", "已停机":
return appDiagnosisCardStatusPtr(4)
case "6", "可测试", "测试期":
return appDiagnosisCardStatusPtr(6)
case "7", "库存":
return appDiagnosisCardStatusPtr(7)
case "8", "预销户":
return appDiagnosisCardStatusPtr(8)
case "9", "已销户", "销户":
return appDiagnosisCardStatusPtr(9)
default:
return nil
}
}
func appDiagnosisCardStatusPtr(status int) *string {
if status < 0 {
return nil
}
value := strconv.Itoa(status)
return &value
}
func appDiagnosisRegionLimitStatus(status string) *string {
var value string
switch strings.TrimSpace(status) {
case "0":
value = "0"
case "1":
value = "1"
default:
value = "2"
}
return &value
}
func appDiagnosisUpstreamFailure(prefix string, code int, message string) (status string, displayMessage string) {
if code == 5000 || appDiagnosisIsUnsupportedMessage(message) {
return "skipped", ""
}
return "error", appDiagnosisUpstreamMessage(prefix, code, message)
}
func appDiagnosisUpstreamMessage(prefix string, code int, message string) string {
message = strings.TrimSpace(message)
if message != "" {
if appDiagnosisIsUnsupportedMessage(message) {
return ""
}
if normalized := appDiagnosisNormalizeCustomerMessage(message); normalized != "" {
return normalized
}
}
prefix = strings.TrimSpace(prefix)
if prefix == "" {
prefix = "上游查询失败"
}
if appDiagnosisIsUnsupportedMessage(prefix) {
return ""
}
return appDiagnosisNormalizeCustomerMessage(prefix)
}
const appDiagnosisRetryMessage = "查询暂时未完成,请稍后重试"
func appDiagnosisIsUnsupportedMessage(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 appDiagnosisNormalizeCustomerMessage(message string) string {
message = strings.TrimSpace(message)
if message == "" {
return ""
}
lower := strings.ToLower(message)
switch {
case appDiagnosisIsUnsupportedMessage(message):
return ""
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 appDiagnosisRetryMessage
case strings.Contains(message, "上游"),
strings.Contains(message, "缓存"),
strings.Contains(message, "60分钟"),
strings.Contains(message, "未返回有效结果"),
strings.Contains(message, "通道未实例化"):
return appDiagnosisRetryMessage
default:
return message
}
}
func appDiagnosisHumanLimitStatus(status string) *string {
var value string
switch strings.TrimSpace(status) {
case "1":
value = "0"
case "0":
value = "1"
case "2":
value = "2"
case "3":
value = "3"
default:
value = "2"
}
return &value
}
func appDiagnosisRateLimitStatus(status string) *string {
var value string
switch strings.TrimSpace(status) {
case "0":
value = "0"
case "1":
value = "1"
case "3":
value = "3"
default:
value = "2"
}
return &value
}
func appDiagnosisPackageEffectiveStatus(status int) *string {
var value string
switch status {
case consts.PackageStatusPending:
value = "0"
case consts.PackageStatusEffective:
value = "1"
case consts.PackageStatusExpired:
value = "2"
default:
return nil
}
return &value
}