// Package gen // @Link https://github.com/xiujiecn/xiu-admin // @Copyright Copyright (c) 2026 LiXiujie // @Author Lxj // @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE // @AutoGenerate Version package iot import ( "context" "crypto/rand" "io" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rabbitmq" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook" 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/model/do" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert" "strings" "time" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" ) const ( // sim_yd_v5 is the official mobile channel. Its upstream app key is kept as-is. platformAppKeyPrefix = "YZT" platformAppKeyRandomLength = 20 platformAppKeyAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" sharedTokenTemplateModel = "shared_token" mobileOfficialTemplateKey = "sim_yd_v5" platformAppKeyMaxAttempts = 5 ) type sIotTSimChannels struct{} var publishChannelRefresh = rabbitmq.Publish func NewIotTSimChannels() *sIotTSimChannels { return &sIotTSimChannels{} } func init() { service.RegisterIotTSimChannels(NewIotTSimChannels()) } // Model 通道列表ORM模型 func (s *sIotTSimChannels) Model(ctx context.Context, option ...*handler.Option) *gdb.Model { if len(option) == 0 { option = append(option, &handler.Option{ FilterTenant: true, FilterAuth: true, }) } return handler.Model(dao.TSimChannels.Ctx(ctx), option...) } // List 获取通道列表列表 func (s *sIotTSimChannels) List(ctx context.Context, in *model.TSimChannelsListParam) (list []*model.TSimChannelsListModel, totalCount int, err error) { mod := s.Model(ctx) channelTemplate := dao.TSimChannelTemplate.Table() simChannels := dao.TSimChannels.Table() mod = mod.LeftJoin(channelTemplate, channelTemplate+"."+dao.TSimChannelTemplate.Columns().TempKey+"="+simChannels+"."+dao.TSimChannels.Columns().TempKey) // 字段过滤 mod = mod.FieldsPrefix(dao.TSimChannels.Table(), model.TSimChannelsListModel{}) mod = mod.Fields(channelTemplate + ".temp_name as tempName") // 查询ID if in.Id > 0 { mod = mod.Where(dao.TSimChannels.Columns().Id, in.Id) } // 查询通道编号 if in.ChanKey != "" { mod = mod.Where(dao.TSimChannels.Columns().ChanKey+" LIKE ?", "%"+in.ChanKey+"%") } // 查询通道名称 if in.ChanName != "" { mod = mod.Where(channelDisplayNameSQL(ctx, simChannels)+" LIKE ?", "%"+in.ChanName+"%") } // 查询通道别名 if in.ChanAlias != "" { mod = mod.Where(dao.TSimChannels.Columns().ChanAlias+" LIKE ?", "%"+in.ChanAlias+"%") } if in.Operator != nil { mod = mod.Where(dao.TSimChannels.Columns().Operator, *in.Operator) } if in.ShareEnabled != nil { mod = mod.Where(dao.TSimChannels.Columns().ShareEnabled, *in.ShareEnabled) } if in.IsNormal != nil { mod = mod.Where(dao.TSimChannels.Columns().IsNormal, *in.IsNormal) } if in.DockingStatus != nil { mod = mod.Where(dao.TSimChannels.Columns().DockingStatus, *in.DockingStatus) } // 查询通道状态(0未启用,1启用) if in.ChanStatus != nil { mod = mod.Where(dao.TSimChannels.Columns().ChanStatus, *in.ChanStatus) } // 查询创建时间 if len(in.CreatedAt) == 2 { in.CreatedAt[1] = in.CreatedAt[1].EndOfDay() } if len(in.CreatedAt) == 2 { mod = mod.WhereBetween(dao.TSimChannels.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) } // 分页 mod = mod.Page(in.Page, in.PageSize) // 排序 mod = mod.OrderDesc(dao.TSimChannels.Columns().Id) // 操作人摘要信息 mod = mod.Hook(hook.MemberSummary) // 查询数据 if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { err = gerror.Wrap(err, "获取通道列表列表失败,请稍后重试!") return } fullName := canViewChannelFullName(ctx) for _, item := range list { if item != nil { item.ChanName = channelDisplayName(fullName, item.ChanName, item.ChanAlias) } } return } type channelCardCounts struct { ChanKey string `orm:"chan_key"` Count int64 `orm:"card_count"` } // RefreshCardCounts rebuilds the three persisted channel counters from lifecycle tables. func (s *sIotTSimChannels) RefreshCardCounts(ctx context.Context) error { tenantID := contexts.GetTenantId(ctx) counts := make(map[string][3]int64) load := func(table string, index int) error { var rows []channelCardCounts if err := g.DB().Ctx(ctx).Model(table). Fields("chan_key, COUNT(*) AS card_count"). Where("tenant_id", tenantID). WhereNot("chan_key", ""). Group("chan_key").Scan(&rows); err != nil { return gerror.Wrapf(err, "统计通道卡数量失败: %s", table) } for _, row := range rows { values := counts[row.ChanKey] values[index] = row.Count counts[row.ChanKey] = values } return nil } if err := load(dao.TSimCardStock.Table(), 0); err != nil { return err } if err := load(dao.TSimCardInfo.Table(), 1); err != nil { return err } if err := load(dao.TSimCardCancellation.Table(), 2); err != nil { return err } var channels []gdb.Record err := s.Model(ctx).Fields(dao.TSimChannels.Columns().ChanKey).Scan(&channels) if err != nil { return gerror.Wrap(err, "查询通道失败") } return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { for _, channel := range channels { key := gconv.String(channel[dao.TSimChannels.Columns().ChanKey]) values := counts[key] if _, err := tx.Ctx(ctx).Model(dao.TSimChannels.Table()). Where(dao.TSimChannels.Columns().TenantId, tenantID). Where(dao.TSimChannels.Columns().ChanKey, key). Data(do.TSimChannels{StockCardCount: values[0], CardInfoCount: values[1], CancellationCardCount: values[2]}).Update(); err != nil { return gerror.Wrap(err, "更新通道卡数量失败") } } return nil }) } // Export 导出通道管理(异步:提交任务后到下载中心取文件) func (s *sIotTSimChannels) Export(ctx context.Context, in *model.TSimChannelsListParam) (err error) { _, err = exportcenter.Submit(ctx, &exportcenter.Request{ BizKey: "tSimChannels", BizName: "通道管理", PermCode: "cpm:iot:tSimChannels:export", QueryParam: in, Tags: func() ([]string, error) { return convert.GetEntityDescTags(model.TSimChannelsListModel{}) }, Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) { query := *in query.Page = page query.PageSize = pageSize list, count, err := s.List(ctx, &query) if err != nil { return nil, 0, err } rows = make([]interface{}, 0, len(list)) for _, item := range list { rows = append(rows, item) } return rows, count, nil }, }) return err } // shouldGeneratePlatformAppKey reports whether a channel uses the shared-token // protocol without the official mobile channel's own app key. func shouldGeneratePlatformAppKey(tempKey, tempModel string) bool { return strings.EqualFold(strings.TrimSpace(tempModel), sharedTokenTemplateModel) && !strings.EqualFold(strings.TrimSpace(tempKey), mobileOfficialTemplateKey) } // isMissingAppKey recognizes the legacy slash placeholder used for an // unconfigured upstream app key. It must not prevent allocation of a platform // identifier for a shared-token channel. func isMissingAppKey(appKey string) bool { value := strings.TrimSpace(appKey) return value == "" || value == "/" } // generatePlatformAppKey creates the stable platform-side identifier used by // shared-token third-party channels. The random suffix uses a 32-character // alphabet, so every byte can be mapped without modulo bias. func generatePlatformAppKey() (string, error) { randomPart := make([]byte, platformAppKeyRandomLength) if _, err := io.ReadFull(rand.Reader, randomPart); err != nil { return "", gerror.Wrap(err, "generate platform app key") } for index, value := range randomPart { randomPart[index] = platformAppKeyAlphabet[int(value)%len(platformAppKeyAlphabet)] } return platformAppKeyPrefix + string(randomPart), nil } // loadChannelTemplateModel resolves the template model in the same request // transaction as the channel update. A missing template keeps the existing // behavior and does not trigger platform app key generation. func (s *sIotTSimChannels) loadChannelTemplateModel(ctx context.Context, tempKey string) (string, error) { tempKey = strings.TrimSpace(tempKey) if tempKey == "" { return "", nil } var template struct { TempModel string `json:"tempModel"` } if err := handler.Model(dao.TSimChannelTemplate.Ctx(ctx), &handler.Option{FilterAuth: false, FilterTenant: true}). Fields(dao.TSimChannelTemplate.Columns().TempModel). Where(dao.TSimChannelTemplate.Columns().TempKey, tempKey). Where(dao.TSimChannelTemplate.Columns().TenantId, contexts.GetTenantId(ctx)). Limit(1). Scan(&template); err != nil { return "", gerror.Wrap(err, "query channel template model") } return strings.TrimSpace(template.TempModel), nil } // generateUniquePlatformAppKey checks existing channel identifiers before // returning a candidate. The app_key column is shared with upstream app keys, // so the check is global and does not alter the existing uniqueness semantics. func (s *sIotTSimChannels) generateUniquePlatformAppKey(ctx context.Context) (string, error) { for attempt := 0; attempt < platformAppKeyMaxAttempts; attempt++ { candidate, err := generatePlatformAppKey() if err != nil { return "", err } count, err := s.Model(ctx, &handler.Option{FilterAuth: false, FilterTenant: false}). Where(dao.TSimChannels.Columns().AppKey, candidate). Count() if err != nil { return "", gerror.Wrap(err, "check platform app key uniqueness") } if count == 0 { return candidate, nil } } return "", gerror.New("unable to allocate a unique platform app key") } // Edit 修改/新增通道列表 func (s *sIotTSimChannels) Edit(ctx context.Context, in *model.TSimChannelsEditParam) (err error) { if err = in.Filter(ctx); err != nil { return err } var ( refreshMessage model.ChannelRefreshMessage shouldPublish bool ) err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) { // 修改 if in.Id > 0 { var existing struct { Id int64 `json:"id"` ChanKey string `json:"chanKey"` Operator int `json:"operator"` TempKey string `json:"tempKey"` AppKey string `json:"appKey"` ShareEnabled int `json:"shareEnabled"` TenantId string `json:"tenantId"` } channelTenantID := strings.TrimSpace(contexts.GetTenantId(ctx)) channelModel := tx.Ctx(ctx).Model(dao.TSimChannels.Table()) if channelTenantID != "" { channelModel = channelModel.Where(dao.TSimChannels.Columns().TenantId, channelTenantID) } if err = handler.Model(channelModel). Fields( dao.TSimChannels.Columns().Id, dao.TSimChannels.Columns().ChanKey, dao.TSimChannels.Columns().Operator, dao.TSimChannels.Columns().TempKey, dao.TSimChannels.Columns().AppKey, dao.TSimChannels.Columns().ShareEnabled, dao.TSimChannels.Columns().TenantId, ). WherePri(in.Id). LockUpdate(). Scan(&existing); err != nil { return gerror.Wrap(err, "查询原通道编号失败") } if existing.Id == 0 { return gerror.New("channel does not exist or is not accessible") } if strings.TrimSpace(existing.TenantId) != "" { channelTenantID = strings.TrimSpace(existing.TenantId) } oldChanKey, newChanKey, chanKeyChanged, keyErr := resolveChannelKeyChange(existing.ChanKey, existing.ShareEnabled, in.ChanKey) if keyErr != nil { return keyErr } if in.ChanKey != nil { keyModel := tx.Ctx(ctx).Model(dao.TSimChannels.Table()) if channelTenantID != "" { keyModel = keyModel.Where(dao.TSimChannels.Columns().TenantId, channelTenantID) } count, countErr := keyModel. Where(dao.TSimChannels.Columns().ChanKey, newChanKey). Where(dao.TSimChannels.Columns().Id+" <> ?", in.Id). LockUpdate().Count() if countErr != nil { return gerror.Wrap(countErr, "校验通道编号唯一性失败") } if count > 0 { return gerror.New("通道编号已存在") } in.ChanKey = &newChanKey } // The web form sends appKey together with the share toggle. Keep an // existing identifier when that form value is blank so disabling and // re-enabling sharing remains stable. appKey := strings.TrimSpace(existing.AppKey) if in.AppKey != nil { requestedAppKey := strings.TrimSpace(*in.AppKey) if requestedAppKey != "" || isMissingAppKey(existing.AppKey) { appKey = requestedAppKey } } shareEnabled := existing.ShareEnabled if in.ShareEnabled != nil { shareEnabled = *in.ShareEnabled } tempKey := strings.TrimSpace(existing.TempKey) if in.TempKey != nil { tempKey = strings.TrimSpace(*in.TempKey) } tempModel, templateErr := s.loadChannelTemplateModel(ctx, tempKey) if templateErr != nil { return templateErr } if shareEnabled == 1 && isMissingAppKey(appKey) && shouldGeneratePlatformAppKey(tempKey, tempModel) { appKey, err = s.generateUniquePlatformAppKey(ctx) if err != nil { return err } in.AppKey = &appKey } if in.AppKey != nil { normalizedAppKey := appKey in.AppKey = &normalizedAppKey } updateBy := contexts.GetUserId(ctx) in.UpdatedBy = &updateBy in.UpdatedAt = gtime.Now() if chanKeyChanged { // Renaming a channel only rebuilds its runtime instance. Card channel // references are historical/business data and must be migrated manually. refreshMessage = model.ChannelRefreshMessage{Type: "channel_rename", OldChanKey: oldChanKey, NewChanKey: newChanKey} } else { refreshMessage = model.ChannelRefreshMessage{Type: "channel", ChanKey: newChanKey} } shouldPublish = newChanKey != "" channelModel = tx.Ctx(ctx).Model(dao.TSimChannels.Table()) if channelTenantID != "" { channelModel = channelModel.Where(dao.TSimChannels.Columns().TenantId, channelTenantID) } if _, err = handler.Model(channelModel). Fields(model.TSimChannelsUpdateFields{}). WherePri(in.Id).Data(in).Update(); err != nil { err = gerror.Wrap(err, "修改通道列表失败,请稍后重试!") } return } // 新增 if err = validateNewChannelKey(ctx, tx, in); err != nil { return err } tenantId := contexts.GetTenantId(ctx) in.TenantId = &tenantId deptId := contexts.GetDeptId(ctx) in.CreatedDept = &deptId userid := contexts.GetUserId(ctx) in.CreatedBy = &userid in.CreatedAt = gtime.Now() if in.TempKey != nil && in.ShareEnabled != nil && *in.ShareEnabled == 1 && (in.AppKey == nil || isMissingAppKey(*in.AppKey)) { tempKey := strings.TrimSpace(*in.TempKey) tempModel, templateErr := s.loadChannelTemplateModel(ctx, tempKey) if templateErr != nil { return templateErr } if shouldGeneratePlatformAppKey(tempKey, tempModel) { appKey, generateErr := s.generateUniquePlatformAppKey(ctx) if generateErr != nil { return generateErr } in.AppKey = &appKey } } if _, err = handler.Model(tx.Ctx(ctx).Model(dao.TSimChannels.Table()), &handler.Option{FilterAuth: false}). Fields(model.TSimChannelsInsertFields{}). Data(in).Insert(); err != nil { err = gerror.Wrap(err, "新增通道列表失败,请稍后重试!") } return }) if err != nil { return err } if shouldPublish || (in.Id <= 0 && s.resolveSavedChannelKey(ctx, in) != "") { publishCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if !shouldPublish { refreshMessage = model.ChannelRefreshMessage{Type: "channel", ChanKey: s.resolveSavedChannelKey(ctx, in)} } if err = publishChannelRefresh(publishCtx, consts.QueueChannelRefresh, refreshMessage); err != nil { return gerror.Wrap(err, "通道已保存,但刷新任务投递失败") } } return nil } func validateNewChannelKey(ctx context.Context, tx gdb.TX, in *model.TSimChannelsEditParam) error { if in == nil || in.ChanKey == nil { return gerror.New("通道编号不能为空") } chanKey := strings.TrimSpace(*in.ChanKey) if chanKey == "" { return gerror.New("通道编号不能为空") } keyModel := tx.Ctx(ctx).Model(dao.TSimChannels.Table()) if tenantID := strings.TrimSpace(contexts.GetTenantId(ctx)); tenantID != "" { keyModel = keyModel.Where(dao.TSimChannels.Columns().TenantId, tenantID) } count, err := keyModel. Where(dao.TSimChannels.Columns().ChanKey, chanKey). LockUpdate().Count() if err != nil { return gerror.Wrap(err, "校验通道编号唯一性失败") } if count > 0 { return gerror.New("通道编号已存在") } *in.ChanKey = chanKey return nil } func resolveChannelKeyChange(existingChanKey string, shareEnabled int, requested *string) (oldChanKey, newChanKey string, changed bool, err error) { oldChanKey = strings.TrimSpace(existingChanKey) if oldChanKey == "" { return "", "", false, gerror.New("通道不存在") } newChanKey = oldChanKey if requested == nil { return oldChanKey, newChanKey, false, nil } newChanKey = strings.TrimSpace(*requested) changed = newChanKey != oldChanKey if !changed { return oldChanKey, newChanKey, false, nil } if shareEnabled == 1 { return oldChanKey, newChanKey, true, gerror.New("已开启共享Token的通道不允许修改通道编号") } if newChanKey == "" { return oldChanKey, newChanKey, true, gerror.New("通道编号不能为空") } return oldChanKey, newChanKey, true, nil } func (s *sIotTSimChannels) resolveSavedChannelKey(ctx context.Context, in *model.TSimChannelsEditParam) string { if in == nil { return "" } if in.ChanKey != nil && strings.TrimSpace(*in.ChanKey) != "" { return strings.TrimSpace(*in.ChanKey) } if in.Id <= 0 { return "" } var row struct { ChanKey string `json:"chanKey"` } if err := s.Model(ctx). Fields(dao.TSimChannels.Columns().ChanKey). WherePri(in.Id). Limit(1). Scan(&row); err != nil { g.Log().Warningf(ctx, "resolve saved channel key failed, id=%d err=%+v", in.Id, err) return "" } return strings.TrimSpace(row.ChanKey) } // Delete 删除通道列表 func (s *sIotTSimChannels) Delete(ctx context.Context, in *model.TSimChannelsDeleteParam) (err error) { if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil { err = gerror.Wrap(err, "删除通道列表失败,请稍后重试!") return } return } // View 获取通道列表指定信息 func (s *sIotTSimChannels) View(ctx context.Context, in *model.TSimChannelsViewParam) (res *model.TSimChannelsViewModel, err error) { mod := s.Model(ctx) channelTemplate := dao.TSimChannelTemplate.Table() simChannels := dao.TSimChannels.Table() mod = mod.LeftJoin(channelTemplate, channelTemplate+"."+dao.TSimChannelTemplate.Columns().TempKey+"="+simChannels+"."+dao.TSimChannels.Columns().TempKey) // 字段过滤 mod = mod.FieldsPrefix(dao.TSimChannels.Table(), model.TSimChannelsListModel{}) mod = mod.Fields( channelTemplate+".temp_name as tempName", channelTemplate+".temp_model as tempModel", ) if err = mod.WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil { err = gerror.Wrap(err, "获取通道列表信息,请稍后重试!") return } if res != nil { res.ChanName = channelDisplayName(canViewChannelFullName(ctx), res.ChanName, res.ChanAlias) } return } type tSimChannelTestChannel struct { ChanKey string `json:"chanKey"` } type tSimChannelTestOperation struct { key string name string execute func(context.Context, *model.SimKey, time.Time) (data any, errorCode int, message string, success bool, err error) } // Test executes card queries or a shared-token probe against an accessible channel. func (s *sIotTSimChannels) Test(ctx context.Context, in *model.TSimChannelsTestParam) (*model.TSimChannelsTestOutput, error) { if in == nil { return nil, gerror.New("测试参数不能为空") } if err := in.Filter(ctx); err != nil { return nil, err } items, err := model.NormalizeTSimChannelTestItems(in.Items) if err != nil { return nil, err } var accessible struct { Id int64 `json:"id"` ChanKey string `json:"chanKey"` } if err := s.Model(ctx). Fields(dao.TSimChannels.Columns().Id, dao.TSimChannels.Columns().ChanKey). WherePri(in.ChannelId). Limit(1). Scan(&accessible); err != nil { return nil, gerror.Wrap(err, "查询通道失败") } if accessible.Id == 0 { return nil, gerror.New("通道不存在或无权访问") } return service.UpstreamChan().TestChannel(ctx, &model.UpstreamChannelTestRequest{ ChannelID: in.ChannelId, TenantID: strings.TrimSpace(contexts.GetTenantId(ctx)), InputMode: in.InputMode, CardValueType: in.CardValueType, CardValue: in.CardValue, ICCID: in.Iccid, IMSI: in.Imsi, MSISDN: in.Msisdn, Items: items, }) } func (s *sIotTSimChannels) resolveTSimChannelTestSimKey(ctx context.Context, chanKey string, in *model.TSimChannelsTestParam) (*model.SimKey, error) { switch in.InputMode { case model.TSimChannelsTestInputModeSingle: return s.queryTSimChannelTestCard(ctx, chanKey, in.CardValueType, in.CardValue) case model.TSimChannelsTestInputModeDirectSingle: return buildTSimChannelTestSingleSimKey(chanKey, in.CardValueType, in.CardValue) default: return &model.SimKey{ ChanKey: chanKey, ICCID: in.Iccid, IMSI: in.Imsi, MSISDN: in.Msisdn, }, nil } } func buildTSimChannelTestSingleSimKey(chanKey, cardValueType, cardValue string) (*model.SimKey, error) { simKey := &model.SimKey{ChanKey: chanKey} switch cardValueType { case "iccid": simKey.ICCID = cardValue case "imsi": simKey.IMSI = cardValue case "msisdn": simKey.MSISDN = cardValue default: return nil, gerror.New("card value type must be iccid, imsi, or msisdn") } return simKey, nil } // resolveSimKeyThreeCodes 调用运营商接口,用单码(iccid/imsi/msisdn)补全三码。 // 走 gateway 配置,保证单码即可查询(与通道测试三码补全逻辑一致,供诊断流程复用)。 func resolveSimKeyThreeCodes(ctx context.Context, chanKey, cardValueType, cardValue string) (*model.SimKey, error) { queryCtx := upstreamlogic.WithPreservedUpstreamResults(upstreamlogic.WithGatewayUpstreamConfig(ctx)) return resolveSimKeyThreeCodesFromUpstream(ctx, queryCtx, chanKey, cardValueType, cardValue) } func resolveSimKeyThreeCodesFromUpstream(ctx context.Context, queryCtx context.Context, chanKey, cardValueType, cardValue string) (*model.SimKey, error) { simKey := &model.SimKey{ChanKey: chanKey} switch cardValueType { case "iccid": simKey.ICCID = cardValue case "imsi": simKey.IMSI = cardValue case "msisdn": simKey.MSISDN = cardValue default: return nil, gerror.New("单码补全模式下卡值类型只能是iccid、imsi或msisdn") } // 调用运营商API补全三码 output, err := service.UpstreamChan().QuerySimBasicInfo(queryCtx, &model.SimBasicInfoInput{ SimKeys: []*model.SimKey{simKey}, }) if err != nil { return nil, gerror.Wrap(err, "调用运营商API补全三码失败") } if len(output) == 0 { return nil, gerror.New("运营商API未返回有效结果") } first := output[0] if first.Code != 0 { return nil, gerror.New(first.Message) } if strings.TrimSpace(first.ResData.ICCID) == "" || strings.TrimSpace(first.ResData.IMSI) == "" || strings.TrimSpace(first.ResData.MSISDN) == "" { return nil, gerror.New("运营商API未返回完整的卡信息") } // 返回补全后的三码 return &model.SimKey{ ChanKey: chanKey, ICCID: first.ResData.ICCID, IMSI: first.ResData.IMSI, MSISDN: first.ResData.MSISDN, }, nil } func (s *sIotTSimChannels) queryTSimChannelTestCard(ctx context.Context, chanKey, cardValueType, cardValue string) (*model.SimKey, error) { return resolveSimKeyThreeCodes(ctx, chanKey, cardValueType, cardValue) } func selectTSimChannelTestOperations(all []tSimChannelTestOperation, requested []string) []tSimChannelTestOperation { if len(requested) == 0 { return all } requestedSet := make(map[string]struct{}, len(requested)) for _, key := range requested { requestedSet[key] = struct{}{} } selected := make([]tSimChannelTestOperation, 0, len(requested)) for _, operation := range all { if _, ok := requestedSet[operation.key]; ok { selected = append(selected, operation) } } return selected } func executeTSimChannelTestOperation(ctx context.Context, operation tSimChannelTestOperation, simKey *model.SimKey, now time.Time) *model.TSimChannelsTestItem { startedAt := time.Now() callCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() data, errorCode, message, success, err := operation.execute(callCtx, simKey, now) result := &model.TSimChannelsTestItem{ Key: operation.key, Name: operation.name, Status: "failed", DurationMs: time.Since(startedAt).Milliseconds(), Data: data, ErrorCode: errorCode, } if callCtx.Err() == context.DeadlineExceeded || isTSimChannelTestTimeout(err) { result.Status = "timeout" result.Message = "请求网关超时" return result } if err != nil { result.Message = err.Error() return result } if !success { // 通道测试是运维排查工具,保留上游原始错误码便于定位; // 但消息统一加上「上游」归属,避免把上游繁忙误读成本平台故障。 result.Message = formatUpstreamTestMessage(errorCode, message) return result } result.Status = "success" result.Message = message return result } // formatUpstreamTestMessage 给上游业务错误的原始文案加上归属说明。 // 上游繁忙类状态(如移动 OneLink 的 13000「系统忙,请稍后再试」)会随机出现, // 原文案容易被误认为本平台出错,因此统一改写为「上游系统服务忙,请稍后再试」。 func formatUpstreamTestMessage(code int, message string) string { message = strings.TrimSpace(message) if display := upstreamStatusDisplayMessage(code, message); display != "" { return display } if message == "" { return "上游未返回结果" } return message } func isTSimChannelTestTimeout(err error) bool { if err == nil { return false } message := strings.ToLower(err.Error()) return strings.Contains(message, "timeout") || strings.Contains(message, "deadline exceeded") || strings.Contains(message, "超时") } func newTSimChannelTestOperations() []tSimChannelTestOperation { return []tSimChannelTestOperation{ newTSimChannelTestOperation("simBasicInfo", "基础信息", func(ctx context.Context, simKey *model.SimKey, _ time.Time) ([]model.SimBaseRes[model.SimBasicInfoItem], error) { return service.UpstreamChan().QuerySimBasicInfo(ctx, &model.SimBasicInfoInput{SimKeys: []*model.SimKey{simKey}}) }), newTSimChannelTestOperation("simSession", "实时会话", func(ctx context.Context, simKey *model.SimKey, _ time.Time) ([]model.SimBaseRes[model.SimSessionItem], error) { return service.UpstreamChan().QuerySimSession(ctx, &model.SimSessionInput{SimKeys: []*model.SimKey{simKey}}) }), newTSimChannelTestOperation("simStatus", "卡状态", func(ctx context.Context, simKey *model.SimKey, _ time.Time) ([]model.SimBaseRes[model.SimCardStatusItem], error) { return service.UpstreamChan().QuerySimCardStatus(ctx, &model.SimCardStatusInput{SimKeys: []*model.SimKey{simKey}}) }), newTSimChannelTestOperation("simStopReason", "停机原因", func(ctx context.Context, simKey *model.SimKey, _ time.Time) ([]model.SimBaseRes[model.SimStopReasonItem], error) { return service.UpstreamChan().QuerySimStopReason(ctx, &model.SimStopReasonInput{SimKeys: []*model.SimKey{simKey}}) }), newTSimChannelTestOperation("simFrozenState", "冻结状态", func(ctx context.Context, simKey *model.SimKey, _ time.Time) ([]model.SimBaseRes[model.SimFrozenStateItem], error) { return service.UpstreamChan().QuerySimFrozenState(ctx, &model.SimFrozenStateInput{SimKeys: []*model.SimKey{simKey}}) }), newTSimChannelTestOperation("simCardBindStatus", "机卡绑定", func(ctx context.Context, simKey *model.SimKey, _ time.Time) ([]model.SimBaseRes[model.SimCardBindStatusItem], error) { return service.UpstreamChan().QuerySimCardBindStatus(ctx, &model.SimCardBindStatusInput{SimKeys: []*model.SimKey{simKey}, ReqData: model.SimCardBindStatusInputItem{TestType: 0}}) }), newTSimChannelTestOperation("simDeviceStatus", "设备状态", func(ctx context.Context, simKey *model.SimKey, _ time.Time) ([]model.SimBaseRes[model.SimDeviceStatusItem], error) { return service.UpstreamChan().QuerySimDeviceStatus(ctx, &model.SimDeviceStatusInput{SimKeys: []*model.SimKey{simKey}}) }), newTSimChannelTestOperation("simRegionLimitStatus", "区域限制", func(ctx context.Context, simKey *model.SimKey, _ time.Time) ([]model.SimBaseRes[model.SimRegionLimitStatusItem], error) { return service.UpstreamChan().QuerySimRegionLimitStatus(ctx, &model.SimRegionLimitStatusInput{SimKeys: []*model.SimKey{simKey}}) }), newTSimChannelTestOperation("simImei", "IMEI", func(ctx context.Context, simKey *model.SimKey, _ time.Time) ([]model.SimBaseRes[model.SimImeiItem], error) { return service.UpstreamChan().QuerySimImei(ctx, &model.SimImeiInput{SimKeys: []*model.SimKey{simKey}}) }), newTSimChannelTestOperation("simDataUsage", "实时流量", func(ctx context.Context, simKey *model.SimKey, _ time.Time) ([]model.SimBaseRes[model.SimDataUsageItem], error) { return service.UpstreamChan().QuerySimDatausage(ctx, &model.SimDataUsageInput{SimKeys: []*model.SimKey{simKey}}) }), newTSimChannelTestOperation("simDataUsageMonthly", "月流量", func(ctx context.Context, simKey *model.SimKey, now time.Time) ([]model.SimBaseRes[model.SimDataUsageMonthlyOutputItem], error) { lastMonth := now.AddDate(0, -1, 0) queryDate := time.Date(lastMonth.Year(), lastMonth.Month(), 1, 0, 0, 0, 0, time.Local).Format("200601") return service.UpstreamChan().QuerySimDatausageMonthly(ctx, &model.SimDataUsageMonthlyInput{SimKeys: []*model.SimKey{simKey}, ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: queryDate}}) }), newTSimChannelTestOperation("simDataUsageDaily", "日流量", func(ctx context.Context, simKey *model.SimKey, now time.Time) ([]model.SimBaseRes[model.SimDataUsageDailyOutputItem], error) { yesterday := now.AddDate(0, 0, -1) queryDate := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 0, 0, 0, 0, time.Local).Format("20060102") return service.UpstreamChan().QuerySimDatausageDaily(ctx, &model.SimDataUsageDailyInput{SimKeys: []*model.SimKey{simKey}, ReqData: model.SimDataUsageDailyInputItem{QueryDate: queryDate}}) }), newTSimChannelTestOperation("simApnInfo", "APN信息", func(ctx context.Context, simKey *model.SimKey, _ time.Time) ([]model.SimBaseRes[model.SimApnInfoItemList], error) { return service.UpstreamChan().QuerySimApnInfo(ctx, &model.SimApnInfoInput{SimKeys: []*model.SimKey{simKey}}) }), } } func newTSimChannelTestOperation[T any](key string, name string, query func(context.Context, *model.SimKey, time.Time) ([]model.SimBaseRes[T], error)) tSimChannelTestOperation { return tSimChannelTestOperation{ key: key, name: name, execute: func(ctx context.Context, simKey *model.SimKey, now time.Time) (any, int, string, bool, error) { output, err := query(ctx, simKey, now) if err != nil { return nil, 0, "", false, err } if len(output) == 0 { return nil, 0, "网关未返回结果", false, nil } first := output[0] if first.Code != 0 { return first.ResData, first.Code, first.Message, false, nil } return first.ResData, 0, first.Message, true, nil }, } }