chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)
This commit is contained in:
@@ -0,0 +1,852 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @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
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user