// 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"
"fmt"
"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/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"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
"io"
"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/gctx"
"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(dao.TSimChannels.Columns().ChanName+" 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
}
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) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TSimChannelsExportModel{})
if err != nil {
return
}
var (
fileName = "导出通道列表-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
exports []model.TSimChannelsExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// 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
}
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
},
}
}