chore: 并入 iotlcm-upstream-proxy 历史到 upstream-proxy/(原提交 44209ec)
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
package channelconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
|
||||
"sync"
|
||||
"time"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/dao"
|
||||
|
||||
"github.com/gogf/gf/v2/crypto/gmd5"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// channelConfigRow extends the generated entity without losing SQL NULL values.
|
||||
type channelConfigRow struct {
|
||||
entity.TSimChannels
|
||||
IsNormal *int `json:"isNormal" orm:"is_normal"`
|
||||
}
|
||||
|
||||
type sChannelConfig struct {
|
||||
sync.RWMutex
|
||||
channelCache map[string]*model.SimChannelConfigItemCache
|
||||
loadVersion uint8
|
||||
chgLogMaxId int64
|
||||
running bool
|
||||
}
|
||||
|
||||
func NewChannelConfig() *sChannelConfig {
|
||||
return &sChannelConfig{
|
||||
channelCache: make(map[string]*model.SimChannelConfigItemCache),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterChannelConfig(NewChannelConfig())
|
||||
}
|
||||
|
||||
func (s *sChannelConfig) Start(ctx context.Context) error {
|
||||
err := s.LoadAllChannelConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "ChannelConfig Start err %+v", err)
|
||||
return err
|
||||
}
|
||||
s.running = true
|
||||
err = s.Loop(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "ChannelConfig Start err %+v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sChannelConfig) Stop(ctx context.Context) error {
|
||||
s.running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sChannelConfig) Loop(ctx context.Context) error {
|
||||
g.Log().Infof(ctx, "ChannelConfig loop begin.")
|
||||
lastLoadAllChannelConfigTime := int64(0)
|
||||
go func() {
|
||||
for s.running {
|
||||
now := time.Now().Unix()
|
||||
loadAllInterval := g.Cfg().MustGet(ctx, "channel.load_all_interval", 2*60*60).Int64()
|
||||
if now-lastLoadAllChannelConfigTime >= loadAllInterval {
|
||||
lastLoadAllChannelConfigTime = now
|
||||
s.LoadAllChannelConfig(ctx)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}()
|
||||
g.Log().Infof(ctx, "ChannelConfig loop background goroutine started.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sChannelConfig) LoadAllChannelConfig(ctx context.Context) error {
|
||||
s.loadVersion++
|
||||
const warnRowThreshold = 1000
|
||||
|
||||
dbSimChannels := make([]*channelConfigRow, 0)
|
||||
err := dao.TSimChannels.Ctx(ctx).
|
||||
OrderAsc(dao.TSimChannels.Columns().Id).
|
||||
Scan(&dbSimChannels)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
if len(dbSimChannels) >= warnRowThreshold {
|
||||
g.Log().Warningf(ctx, "LoadAllChannelConfig TSimChannels row count %d >= %d, confirm small-table assumption or add pagination",
|
||||
len(dbSimChannels), warnRowThreshold)
|
||||
}
|
||||
|
||||
dbSimChannelTemplates := make([]*entity.TSimChannelTemplate, 0)
|
||||
err = dao.TSimChannelTemplate.Ctx(ctx).
|
||||
OrderAsc(dao.TSimChannelTemplate.Columns().Id).
|
||||
Scan(&dbSimChannelTemplates)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
if len(dbSimChannelTemplates) >= warnRowThreshold {
|
||||
g.Log().Warningf(ctx, "LoadAllChannelConfig TSimChannelTemplate row count %d >= %d, confirm small-table assumption or add pagination",
|
||||
len(dbSimChannelTemplates), warnRowThreshold)
|
||||
}
|
||||
|
||||
templateByKey := make(map[string]*entity.TSimChannelTemplate, len(dbSimChannelTemplates))
|
||||
for _, tpl := range dbSimChannelTemplates {
|
||||
if tpl != nil {
|
||||
templateByKey[tpl.TempKey] = tpl
|
||||
}
|
||||
}
|
||||
|
||||
s.Lock()
|
||||
for _, dbSimChannel := range dbSimChannels {
|
||||
if dbSimChannel == nil {
|
||||
continue
|
||||
}
|
||||
cache := &model.SimChannelConfigItemCache{
|
||||
Channel: &dbSimChannel.TSimChannels,
|
||||
IsNormal: dbSimChannel.IsNormal,
|
||||
Version: s.getChanVersion(ctx, dbSimChannel),
|
||||
}
|
||||
oldChannelCache, _ := s.channelCache[dbSimChannel.ChanKey]
|
||||
if cache.CanCreateInstance() {
|
||||
template, ok := templateByKey[dbSimChannel.TempKey]
|
||||
if !ok {
|
||||
g.Log().Warningf(ctx, "LoadAllChannelConfig no template for tempKey=%s chanKey=%s", dbSimChannel.TempKey, dbSimChannel.ChanKey)
|
||||
continue
|
||||
}
|
||||
cache.Template = template
|
||||
cache.Version += s.getTemplateVersion(ctx, template)
|
||||
}
|
||||
|
||||
if oldChannelCache != nil && oldChannelCache.Version == cache.Version {
|
||||
continue
|
||||
}
|
||||
s.channelCache[dbSimChannel.ChanKey] = cache
|
||||
}
|
||||
s.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sChannelConfig) RefreshChannel(ctx context.Context, chanKey string) error {
|
||||
if chanKey == "" {
|
||||
return gerror.New("chanKey is required")
|
||||
}
|
||||
channel := new(channelConfigRow)
|
||||
if err := dao.TSimChannels.Ctx(ctx).Where(dao.TSimChannels.Columns().ChanKey, chanKey).Scan(channel); err != nil {
|
||||
return err
|
||||
}
|
||||
if channel.ChanKey == "" {
|
||||
return gerror.Newf("channel not found: %s", chanKey)
|
||||
}
|
||||
cache := &model.SimChannelConfigItemCache{
|
||||
Channel: &channel.TSimChannels,
|
||||
IsNormal: channel.IsNormal,
|
||||
Version: s.getChanVersion(ctx, channel),
|
||||
}
|
||||
// Ineligible channels refresh successfully even while their template is incomplete.
|
||||
if cache.CanCreateInstance() {
|
||||
template := new(entity.TSimChannelTemplate)
|
||||
if err := dao.TSimChannelTemplate.Ctx(ctx).Where(dao.TSimChannelTemplate.Columns().TempKey, channel.TempKey).Scan(template); err != nil {
|
||||
return err
|
||||
}
|
||||
if template.TempKey == "" {
|
||||
return gerror.Newf("channel template not found: %s", channel.TempKey)
|
||||
}
|
||||
cache.Template = template
|
||||
cache.Version += s.getTemplateVersion(ctx, template)
|
||||
}
|
||||
s.Lock()
|
||||
s.channelCache[chanKey] = cache
|
||||
s.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveChannel removes a channel configuration cache. Missing keys are
|
||||
// intentionally treated as success so rename retries remain idempotent.
|
||||
func (s *sChannelConfig) RemoveChannel(ctx context.Context, chanKey string) error {
|
||||
chanKey = strings.TrimSpace(chanKey)
|
||||
if chanKey == "" {
|
||||
return gerror.New("chanKey is required")
|
||||
}
|
||||
s.Lock()
|
||||
delete(s.channelCache, chanKey)
|
||||
s.Unlock()
|
||||
g.Log().Infof(ctx, "channel config removed, chanKey: %s", chanKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// get alarm cache by id
|
||||
func (s *sChannelConfig) GetChannel(chanKey string) (*model.SimChannelConfigItemCache, bool) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
channelCache, ok := s.channelCache[chanKey]
|
||||
return channelCache, ok
|
||||
}
|
||||
|
||||
func (s *sChannelConfig) GetAllChannel(ctx context.Context) ([]*model.SimChannelConfigItemCache, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
channels := make([]*model.SimChannelConfigItemCache, 0)
|
||||
for _, channelCache := range s.channelCache {
|
||||
channels = append(channels, channelCache)
|
||||
}
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (s *sChannelConfig) getChanVersion(ctx context.Context, channel *channelConfigRow) string {
|
||||
chanKey := channel.ChanKey
|
||||
|
||||
channelNew := &channelConfigRow{
|
||||
IsNormal: channel.IsNormal,
|
||||
TSimChannels: entity.TSimChannels{
|
||||
TempKey: channel.TempKey,
|
||||
ChanKey: channel.ChanKey,
|
||||
ChanName: channel.ChanName,
|
||||
ChanConfig: channel.ChanConfig,
|
||||
ChanStatus: channel.ChanStatus,
|
||||
ChanAlias: channel.ChanAlias,
|
||||
RegionCode: channel.RegionCode,
|
||||
AppKey: channel.AppKey,
|
||||
AppSecret: channel.AppSecret,
|
||||
AccessToken: channel.AccessToken,
|
||||
RefreshToken: channel.RefreshToken,
|
||||
},
|
||||
}
|
||||
chanVersion, err := gmd5.EncryptString(gjson.MustEncodeString(channelNew))
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sChannelConfig.getChanVersion encrypt error %+v, chanKey %s", err, chanKey)
|
||||
return ""
|
||||
}
|
||||
g.Log().Debugf(ctx, "sChannelConfig.getChanVersion chanVersion=%s chanKey=%s", chanVersion, chanKey)
|
||||
return chanVersion
|
||||
}
|
||||
|
||||
func (s *sChannelConfig) getTemplateVersion(ctx context.Context, template *entity.TSimChannelTemplate) string {
|
||||
templateKey := template.TempKey
|
||||
templateNew := &entity.TSimChannelTemplate{
|
||||
TempKey: template.TempKey,
|
||||
TempName: template.TempName,
|
||||
TempModel: template.TempModel,
|
||||
TempConfig: template.TempConfig,
|
||||
TempStatus: template.TempStatus,
|
||||
}
|
||||
templateVersion, err := gmd5.EncryptString(gjson.MustEncodeString(templateNew))
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sChannelConfig.getTemplateVersion encrypt error %+v, templateKey %s, templateNew %+v", err, templateKey, templateNew)
|
||||
return ""
|
||||
}
|
||||
g.Log().Debugf(ctx, "sChannelConfig.getTemplateVersion templateVersion=%s templateKey=%s", templateVersion, templateKey)
|
||||
return templateVersion
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package channelconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model/entity"
|
||||
)
|
||||
|
||||
func TestChannelVersionDetectsCredentialChanges(t *testing.T) {
|
||||
normal := 0
|
||||
row := &channelConfigRow{IsNormal: &normal, TSimChannels: entity.TSimChannels{
|
||||
ChanKey: "telecom-test", TempKey: "sim_dx_v1", AppKey: "key", AppSecret: "secret",
|
||||
AccessToken: "token", RefreshToken: "refresh",
|
||||
}}
|
||||
config := NewChannelConfig()
|
||||
before := config.getChanVersion(context.Background(), row)
|
||||
for _, field := range []string{"appKey", "appSecret", "accessToken", "refreshToken", "tempKey"} {
|
||||
t.Run(field, func(t *testing.T) {
|
||||
changed := *row
|
||||
switch field {
|
||||
case "appKey":
|
||||
changed.AppKey = "new-key"
|
||||
case "appSecret":
|
||||
changed.AppSecret = "new-secret"
|
||||
case "accessToken":
|
||||
changed.AccessToken = "new-token"
|
||||
case "refreshToken":
|
||||
changed.RefreshToken = "new-refresh"
|
||||
case "tempKey":
|
||||
changed.TempKey = "sim_dx_v3"
|
||||
}
|
||||
if got := config.getChanVersion(context.Background(), &changed); got == before {
|
||||
t.Fatalf("%s change did not invalidate runtime version", field)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package channelconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sChannelUpdate struct{}
|
||||
|
||||
func NewChannelUpdate() *sChannelUpdate {
|
||||
return &sChannelUpdate{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterChannelUpdate(NewChannelUpdate())
|
||||
}
|
||||
|
||||
func (s *sChannelUpdate) UpdateAccessToken(ctx context.Context, input *model.UpdateAccessTokenInput) error {
|
||||
_, err := dao.TSimChannels.Ctx(ctx).Where(dao.TSimChannels.Columns().ChanKey, input.ChanKey).Update(g.Map{
|
||||
dao.TSimChannels.Columns().AccessToken: input.AccessToken,
|
||||
dao.TSimChannels.Columns().AccessTokenCreated: input.AccessTokenCreated,
|
||||
dao.TSimChannels.Columns().AccessTokenExpire: input.AccessTokenExpire,
|
||||
dao.TSimChannels.Columns().RefreshToken: input.RefreshToken,
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// ==========================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// ==========================================================================
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/logic/channelconfig"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/logic/simKeyFill"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/logic/upstreamapi"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/logic/upstreamimpl"
|
||||
)
|
||||
@@ -0,0 +1,193 @@
|
||||
package simKeyFill
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sSimKeyFill struct {
|
||||
}
|
||||
|
||||
func NewSimKeyFill() *sSimKeyFill {
|
||||
return &sSimKeyFill{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSimKeyFill(NewSimKeyFill())
|
||||
}
|
||||
|
||||
func (s *sSimKeyFill) SimKeyFill(ctx context.Context, input *model.SimBaseReqJson) (output model.SimBaseResJson, err error) {
|
||||
var operation struct {
|
||||
OperationID string `json:"operationId"`
|
||||
}
|
||||
request := g.RequestFromCtx(ctx)
|
||||
if request != nil && (request.URL.Path == "/sim/change/sim-apn" || request.URL.Path == "/sim/query/sim-apn-capability") && json.Unmarshal(input.ReqData, &operation) == nil && operation.OperationID != "" {
|
||||
if len(input.SimKeys) == 0 || len(input.SimKeys) > 100 {
|
||||
return nil, gerror.New("异步子任务卡数必须在1至100之间")
|
||||
}
|
||||
for _, key := range input.SimKeys {
|
||||
if key == nil || key.ChanKey == "" || key.ICCID == "" {
|
||||
return nil, gerror.New("异步子任务必须保留chanKey和ICCID快照")
|
||||
}
|
||||
}
|
||||
return service.UpstreamAPI().SimBase(ctx, input)
|
||||
}
|
||||
simKeys, err := s.batchGetSimKey(ctx, input.SimKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(simKeys) == 0 {
|
||||
g.Log().Debug(ctx, "SimKeyFill.batchGetSimKey, simKeys is empty, input %+v", input)
|
||||
return nil, gerror.New("simKeys is empty")
|
||||
}
|
||||
input.SimKeys = simKeys
|
||||
output, err = service.UpstreamAPI().SimBase(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSimKeyFill) batchGetSimKey(ctx context.Context, inputs []*model.SimKey) ([]*model.SimKey, error) {
|
||||
queryICCIDKey := "sim:code:iccid:"
|
||||
queryMSISDNKey := "sim:code:msisdn:"
|
||||
queryIMSIKey := "sim:code:imsi:"
|
||||
if len(inputs) == 0 {
|
||||
g.Log().Debugf(ctx, "SimKeyFill.batchGetSimKey, The parameter is empty, inputs %+v", inputs)
|
||||
return nil, gerror.New("参数不能为空")
|
||||
}
|
||||
|
||||
var redisKeys []string
|
||||
var dbKeys []string
|
||||
simKeys := []*model.SimKey{}
|
||||
|
||||
// 设置Redis查询Key
|
||||
for _, in := range inputs {
|
||||
switch {
|
||||
case in.ICCID != "":
|
||||
key := queryICCIDKey + in.ICCID
|
||||
redisKeys = append(redisKeys, key)
|
||||
case in.MSISDN != "":
|
||||
key := queryMSISDNKey + in.MSISDN
|
||||
redisKeys = append(redisKeys, key)
|
||||
case in.IMSI != "":
|
||||
key := queryIMSIKey + in.IMSI
|
||||
redisKeys = append(redisKeys, key)
|
||||
default:
|
||||
g.Log().Debugf(ctx, "SimKeyFill.batchGetSimKey, The parameter is empty, inputs %+v", inputs)
|
||||
return nil, gerror.New("iccid/msisdn/imsi至少一个不能为空")
|
||||
}
|
||||
}
|
||||
|
||||
// 查询Redis数据
|
||||
g.Log().Info(ctx, "SimKeyFill.batchGetSimKey, query redis")
|
||||
vals, err := g.Redis().MGet(ctx, redisKeys...)
|
||||
if err != nil {
|
||||
g.Log().Debug(ctx, "SimKeyFill.batchGetSimKey, query redis error %+v, redisKeys %+v, inputs %+v", err, redisKeys, inputs)
|
||||
} else {
|
||||
// 解析Redis数据
|
||||
for _, key := range redisKeys {
|
||||
simKey := new(model.SimKey)
|
||||
err := vals[key].Struct(&simKey)
|
||||
if err != nil {
|
||||
g.Log().Debug(ctx, "SimKeyFill.batchGetSimKey, unmarshalValue error %+v, key %s, inputs %+v", err, key, inputs)
|
||||
dbKeys = append(dbKeys, key)
|
||||
continue
|
||||
}
|
||||
simKeys = append(simKeys, simKey)
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Info(ctx, "SimKeyFill.batchGetSimKey, query db")
|
||||
if len(dbKeys) > 0 {
|
||||
// 解析数据库查询Key
|
||||
var dbIccid []string
|
||||
var dbMsisdn []string
|
||||
var dbImsi []string
|
||||
for _, key := range dbKeys {
|
||||
keySplit := strings.Split(key, ":")
|
||||
if len(keySplit) != 4 {
|
||||
g.Log().Debug(ctx, "SimKeyFill.batchGetSimKey, Error in splicing data, key %s, keySplit %v", key, keySplit)
|
||||
continue
|
||||
}
|
||||
switch keySplit[2] {
|
||||
case "iccid":
|
||||
dbIccid = append(dbIccid, keySplit[3])
|
||||
case "msisdn":
|
||||
dbMsisdn = append(dbMsisdn, keySplit[3])
|
||||
case "imsi":
|
||||
dbImsi = append(dbImsi, keySplit[3])
|
||||
}
|
||||
}
|
||||
|
||||
mod := dao.TSimCardInfo.Ctx(ctx)
|
||||
|
||||
if len(dbIccid) > 0 {
|
||||
mod = mod.WhereOrIn(dao.TSimCardInfo.Columns().Iccid, dbIccid)
|
||||
}
|
||||
if len(dbMsisdn) > 0 {
|
||||
mod = mod.WhereOrIn(dao.TSimCardInfo.Columns().Msisdn, dbMsisdn)
|
||||
}
|
||||
if len(dbImsi) > 0 {
|
||||
mod = mod.WhereOrIn(dao.TSimCardInfo.Columns().Imsi, dbImsi)
|
||||
}
|
||||
|
||||
// 查询数据库数据
|
||||
var simInfos []*entity.TSimCardInfo
|
||||
err = mod.Scan(&simInfos)
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, "SimKeyFill.batchGetSimKey, query db error %+v, dbKeys %+v, inputs %+v", err, dbKeys, inputs)
|
||||
return nil, gerror.New("查询数据库数据失败")
|
||||
}
|
||||
if len(simInfos) == 0 {
|
||||
g.Log().Debug(ctx, "SimKeyFill.batchGetSimKey, simInfos is empty, dbKeys %+v, inputs %+v", dbKeys, inputs)
|
||||
return nil, gerror.New("卡信息不存在")
|
||||
}
|
||||
|
||||
// 整理卡信息
|
||||
simInfoMap := make(map[string]*model.SimKey)
|
||||
for _, SimInfo := range simInfos {
|
||||
simInfoMap[queryICCIDKey+SimInfo.Iccid] = &model.SimKey{
|
||||
ICCID: SimInfo.Iccid,
|
||||
MSISDN: SimInfo.Msisdn,
|
||||
IMSI: SimInfo.Imsi,
|
||||
}
|
||||
simInfoMap[queryMSISDNKey+SimInfo.Msisdn] = &model.SimKey{
|
||||
MSISDN: SimInfo.Msisdn,
|
||||
IMSI: SimInfo.Imsi,
|
||||
ICCID: SimInfo.Iccid,
|
||||
}
|
||||
simInfoMap[queryIMSIKey+SimInfo.Imsi] = &model.SimKey{
|
||||
IMSI: SimInfo.Imsi,
|
||||
ICCID: SimInfo.Iccid,
|
||||
MSISDN: SimInfo.Msisdn,
|
||||
}
|
||||
}
|
||||
|
||||
// 新建一个线程,进行Redis数据存储
|
||||
go s.SetRedis(ctx, simInfoMap)
|
||||
// 解析数据库数据
|
||||
for _, info := range dbKeys {
|
||||
simKeys = append(simKeys, simInfoMap[info])
|
||||
}
|
||||
}
|
||||
return simKeys, nil
|
||||
}
|
||||
|
||||
func (s *sSimKeyFill) SetRedis(ctx context.Context, simInfoMap map[string]*model.SimKey) {
|
||||
for key, simKey := range simInfoMap {
|
||||
_, err := g.Redis().Set(ctx, key, *simKey)
|
||||
if err != nil {
|
||||
g.Log().Debugf(ctx, "SimKeyFill.batchGetSimKey, Set error %+v, key %s, simKey %+v", err, key, simKey)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package simKeyFill
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
)
|
||||
|
||||
type snapshotTestAPI struct {
|
||||
service.IUpstreamAPI
|
||||
input *model.SimBaseReqJson
|
||||
}
|
||||
|
||||
func (a *snapshotTestAPI) SimBase(_ context.Context, in *model.SimBaseReqJson) (model.SimBaseResJson, error) {
|
||||
a.input = in
|
||||
return nil, nil
|
||||
}
|
||||
func TestAsyncOperationPreservesCardSnapshot(t *testing.T) {
|
||||
api := &snapshotTestAPI{}
|
||||
service.RegisterUpstreamAPI(api)
|
||||
defer service.RegisterUpstreamAPI(nil)
|
||||
in := &model.SimBaseReqJson{SimKeys: []*model.SimKey{{ChanKey: "test", ICCID: "test-card", MSISDN: "snapshot-number"}}, ReqData: json.RawMessage(`{"operationId":"stable-id","submitOnly":true}`)}
|
||||
_, err := NewSimKeyFill().SimKeyFill((&ghttp.Request{Request: httptest.NewRequest("POST", "/sim/change/sim-apn", nil)}).Context(), in)
|
||||
if err != nil || api.input != in || api.input.SimKeys[0].MSISDN != "snapshot-number" {
|
||||
t.Fatalf("异步请求不应重新填充或改写卡快照:%v", err)
|
||||
}
|
||||
}
|
||||
func TestAsyncOperationRejectsIncompleteSnapshot(t *testing.T) {
|
||||
_, err := NewSimKeyFill().SimKeyFill((&ghttp.Request{Request: httptest.NewRequest("POST", "/sim/change/sim-apn", nil)}).Context(), &model.SimBaseReqJson{SimKeys: []*model.SimKey{{ICCID: "test-card"}}, ReqData: json.RawMessage(`{"operationId":"stable-id"}`)})
|
||||
if err == nil {
|
||||
t.Fatal("缺失通道快照应在调用前拒绝")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package upstreamapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
)
|
||||
|
||||
type apnPassthroughTestChannel struct{ reqData json.RawMessage }
|
||||
|
||||
func (c *apnPassthroughTestChannel) SimBase(_ context.Context, input *model.SimBaseReqJson) (model.SimBaseResJson, error) {
|
||||
c.reqData = append(json.RawMessage(nil), input.ReqData...)
|
||||
return model.SimBaseResJson{&model.SimBaseRes[json.RawMessage]{SimKey: input.SimKeys[0], Code: 0, Message: "已受理", ResData: json.RawMessage(`{"orderNum":"job-persisted","pending":true,"uncertain":false}`)}}, nil
|
||||
}
|
||||
func TestAPNProxyUnknownChannelPreservesUncertain(t *testing.T) {
|
||||
api := NewUpstreamAPI()
|
||||
out, err := api.SimBase((&ghttp.Request{Request: httptest.NewRequest("POST", "/sim/change/sim-apn", nil)}).Context(), &model.SimBaseReqJson{
|
||||
SimKeys: []*model.SimKey{{ChanKey: "missing_chan", ICCID: "test"}},
|
||||
ReqData: json.RawMessage(`{"operationId":"stable-id","submitOnly":true}`),
|
||||
})
|
||||
if err != nil || len(out) != 1 || !strings.Contains(string(out[0].ResData), `"uncertain":true`) {
|
||||
t.Fatalf("未知通道不能误报业务失败: result=%+v err=%v", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonAPNProxySameNamedFieldsKeepLegacyFailure(t *testing.T) {
|
||||
api := NewUpstreamAPI()
|
||||
ctx := (&ghttp.Request{Request: httptest.NewRequest("POST", "/sim/change/sim-status", nil)}).Context()
|
||||
out, err := api.SimBase(ctx, &model.SimBaseReqJson{SimKeys: []*model.SimKey{{ChanKey: "missing_chan", ICCID: "test"}}, ReqData: json.RawMessage(`{"operationId":"other-domain","submitOnly":true,"recoverOnly":true,"queryJobId":"unrelated"}`)})
|
||||
if err != nil || len(out) != 1 || string(out[0].ResData) != `{"status":"-2"}` {
|
||||
t.Fatalf("非APN同名字段改变既有失败语义:%+v %v", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPNProxyPreservesOperationAndAsyncFields(t *testing.T) {
|
||||
api := NewUpstreamAPI()
|
||||
instance := &apnPassthroughTestChannel{}
|
||||
api.RegisterChan("test_chan", "test", &model.SimCardChannelCache{Instance: instance})
|
||||
req := json.RawMessage(`{"operationId":"stable-id","submitOnly":true,"recoverOnly":false,"operType":"1"}`)
|
||||
out, err := api.SimBase((&ghttp.Request{Request: httptest.NewRequest("POST", "/sim/change/sim-apn", nil)}).Context(), &model.SimBaseReqJson{SimKeys: []*model.SimKey{{ChanKey: "test_chan", ICCID: "test"}}, ReqData: req})
|
||||
if err != nil || len(out) != 1 {
|
||||
t.Fatalf("proxy请求错误:%v %+v", err, out)
|
||||
}
|
||||
if !strings.Contains(string(instance.reqData), `"operationId":"stable-id"`) {
|
||||
t.Fatalf("operationId丢失:%s", instance.reqData)
|
||||
}
|
||||
if !strings.Contains(string(out[0].ResData), `"pending":true`) || !strings.Contains(string(out[0].ResData), `"orderNum":"job-persisted"`) {
|
||||
t.Fatalf("受理标识丢失:%s", out[0].ResData)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package upstreamapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
)
|
||||
|
||||
type errCodeTestChannel struct {
|
||||
response model.SimBaseResJson
|
||||
}
|
||||
|
||||
func (f *errCodeTestChannel) SimBase(context.Context, *model.SimBaseReqJson) (model.SimBaseResJson, error) {
|
||||
return f.response, nil
|
||||
}
|
||||
|
||||
func TestSimBasePreservesCarrierErrCode(t *testing.T) {
|
||||
const payload = `{"code":0,"message":"success","data":[{"code":9999,"message":"http status 797","errCode":"S00004","simKey":{"chanKey":"channel_003","iccid":"8986000000000000002"},"resData":{}}]}`
|
||||
|
||||
var upstream model.SimResponseData[model.SimBaseResJson]
|
||||
if err := json.Unmarshal([]byte(payload), &upstream); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(upstream.Data) != 1 {
|
||||
t.Fatalf("decoded results = %d, want 1", len(upstream.Data))
|
||||
}
|
||||
if upstream.Data[0].ErrCode != "S00004" {
|
||||
t.Fatalf("decoded errCode = %q, want S00004", upstream.Data[0].ErrCode)
|
||||
}
|
||||
|
||||
api := NewUpstreamAPI()
|
||||
api.RegisterChan("channel_003", "test", &model.SimCardChannelCache{
|
||||
Instance: &errCodeTestChannel{response: upstream.Data},
|
||||
})
|
||||
output, err := api.SimBase(context.Background(), &model.SimBaseReqJson{
|
||||
SimKeys: []*model.SimKey{{
|
||||
ChanKey: "channel_003",
|
||||
ICCID: "8986000000000000002",
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(output) != 1 {
|
||||
t.Fatalf("proxied results = %d, want 1", len(output))
|
||||
}
|
||||
if output[0].ErrCode != "S00004" {
|
||||
t.Fatalf("proxied errCode = %q, want S00004", output[0].ErrCode)
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(output)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(encoded), `"errCode":"S00004"`) {
|
||||
t.Fatalf("serialized response dropped errCode: %s", encoded)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package upstreamapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/logic/channelconfig"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type normalGateTestChannel struct {
|
||||
model.SimCardChannelI
|
||||
}
|
||||
|
||||
type normalGateTestTemplate struct {
|
||||
calls int
|
||||
lastConfig *model.SimChannelConfigItemCache
|
||||
}
|
||||
|
||||
func (f *normalGateTestTemplate) NewTemp(_ context.Context, cfg *model.SimChannelConfigItemCache) model.SimCardChannelI {
|
||||
f.calls++
|
||||
f.lastConfig = cfg
|
||||
return &normalGateTestChannel{}
|
||||
}
|
||||
|
||||
// Exercise real ORM decoding and config versioning as well as both creation paths.
|
||||
// All SQL runs against a temporary SQLite file, never a configured environment DB.
|
||||
func TestUnregisterChanIsIdempotent(t *testing.T) {
|
||||
api := NewUpstreamAPI()
|
||||
api.RegisterChan("missing", "v1", &model.SimCardChannelCache{})
|
||||
api.UnregisterChan("missing")
|
||||
api.UnregisterChan("missing")
|
||||
if _, ok := api.GetChan("missing"); ok {
|
||||
t.Fatal("channel still registered after repeated unregister")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelNormalCreationGate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if err := gdb.SetConfigGroup("default", gdb.ConfigGroup{{
|
||||
Type: "sqlite", Name: filepath.Join(t.TempDir(), "channels.db"),
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db := g.DB()
|
||||
t.Cleanup(func() { _ = db.Close(ctx) })
|
||||
execSQL := func(query string, args ...any) {
|
||||
t.Helper()
|
||||
if _, err := db.Exec(ctx, query, args...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
execSQL("CREATE TABLE t_sim_channels (id INTEGER PRIMARY KEY, chan_key TEXT, temp_key TEXT, chan_name TEXT, is_normal INTEGER NULL)")
|
||||
execSQL("CREATE TABLE t_sim_channel_template (id INTEGER PRIMARY KEY, temp_key TEXT, temp_model TEXT)")
|
||||
execSQL("INSERT INTO t_sim_channel_template (id, temp_key, temp_model) VALUES (1, 'gate_test', 'direct')")
|
||||
|
||||
oldConfig := service.ChannelConfig()
|
||||
t.Cleanup(func() { service.RegisterChannelConfig(oldConfig) })
|
||||
|
||||
for _, path := range []string{"loop", "refresh"} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
value any
|
||||
eligible bool
|
||||
}{
|
||||
{"yes_is_zero", 0, true},
|
||||
{"no_is_one", 1, false},
|
||||
{"null", nil, false},
|
||||
{"unexpected", 2, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
execSQL("DELETE FROM t_sim_channels")
|
||||
execSQL("INSERT INTO t_sim_channels (id, chan_key, temp_key, is_normal) VALUES (1, 'gate_channel', 'gate_test', ?)", tc.value)
|
||||
config := channelconfig.NewChannelConfig()
|
||||
service.RegisterChannelConfig(config)
|
||||
api := NewUpstreamAPI()
|
||||
factory := &normalGateTestTemplate{}
|
||||
api.RegisterTemp("gate_test", factory)
|
||||
reload := func() {
|
||||
t.Helper()
|
||||
var err error
|
||||
if path == "loop" {
|
||||
err = config.LoadAllChannelConfig(ctx)
|
||||
if err == nil {
|
||||
err = api.loop(ctx)
|
||||
}
|
||||
} else {
|
||||
err = api.RefreshChannel(ctx, "gate_channel")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("refresh must succeed: %v", err)
|
||||
}
|
||||
}
|
||||
reload()
|
||||
cfg, ok := config.GetChannel("gate_channel")
|
||||
if !ok || cfg.CanCreateInstance() != tc.eligible {
|
||||
t.Fatalf("wrong eligibility after ORM scan: %+v", cfg)
|
||||
}
|
||||
if tc.value == nil && cfg.IsNormal != nil {
|
||||
t.Fatalf("SQL NULL decoded as non-nil: %v", *cfg.IsNormal)
|
||||
}
|
||||
if cfg.Channel.ChanKey != "gate_channel" {
|
||||
t.Fatal("embedded channel fields were not scanned")
|
||||
}
|
||||
_, exists := api.GetChan("gate_channel")
|
||||
if exists != tc.eligible || (factory.calls == 1) != tc.eligible {
|
||||
t.Fatalf("instance exists=%v factory calls=%d eligible=%v", exists, factory.calls, tc.eligible)
|
||||
}
|
||||
initialVersion := cfg.Version
|
||||
|
||||
// Only is_normal changes: the configuration cache must notice it.
|
||||
execSQL("UPDATE t_sim_channels SET is_normal=0")
|
||||
reload()
|
||||
current, ok := api.GetChan("gate_channel")
|
||||
if !ok || current.Instance == nil {
|
||||
t.Fatal("eligible channel was not instantiated")
|
||||
}
|
||||
cfg, _ = config.GetChannel("gate_channel")
|
||||
if !tc.eligible && cfg.Version == initialVersion {
|
||||
t.Fatal("changing only is_normal did not change config version")
|
||||
}
|
||||
oldCalls := factory.calls
|
||||
|
||||
// Changing only is_normal must evict the old runtime instance.
|
||||
execSQL("UPDATE t_sim_channels SET is_normal=1")
|
||||
reload()
|
||||
if _, exists := api.GetChan("gate_channel"); exists || factory.calls != oldCalls {
|
||||
t.Fatal("ineligible channel retained or recreated its runtime instance")
|
||||
}
|
||||
cfg, _ = config.GetChannel("gate_channel")
|
||||
if cfg.CanCreateInstance() || cfg.Version == current.Version {
|
||||
t.Fatal("ineligible configuration was not cached with a new version")
|
||||
}
|
||||
// Disabled refreshes remain successful even if the template disappears.
|
||||
execSQL("UPDATE t_sim_channels SET chan_name='changed while disabled', temp_key='missing_template'")
|
||||
reload()
|
||||
reload()
|
||||
if _, exists := api.GetChan("gate_channel"); exists || factory.calls != oldCalls {
|
||||
t.Fatal("repeated ineligible refresh was not an idempotent removal")
|
||||
}
|
||||
|
||||
// A process restart cannot recreate the disabled instance.
|
||||
|
||||
restarted := NewUpstreamAPI()
|
||||
restarted.RegisterTemp("gate_test", factory)
|
||||
if err := restarted.loop(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := restarted.GetChan("gate_channel"); ok {
|
||||
t.Fatal("reload recreated disabled instance")
|
||||
}
|
||||
|
||||
execSQL("UPDATE t_sim_channels SET is_normal=0, temp_key='gate_test'")
|
||||
reload()
|
||||
restored, exists := api.GetChan("gate_channel")
|
||||
if !exists || restored == current || factory.calls != oldCalls+1 ||
|
||||
factory.lastConfig.Channel.ChanName != "changed while disabled" {
|
||||
t.Fatal("restored channel did not rebuild using the latest configuration")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package upstreamapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
var REDIS_KEY = "sim:chan:iccid:"
|
||||
|
||||
type sUpstreamAPI struct {
|
||||
tempMap map[string]model.SimCardTempI
|
||||
chanMap map[string]*model.SimCardChannelCache
|
||||
channelMux sync.Mutex
|
||||
channelOps map[string]*sync.Mutex
|
||||
sync.RWMutex
|
||||
running bool
|
||||
}
|
||||
|
||||
func NewUpstreamAPI() *sUpstreamAPI {
|
||||
return &sUpstreamAPI{
|
||||
chanMap: make(map[string]*model.SimCardChannelCache),
|
||||
tempMap: make(map[string]model.SimCardTempI),
|
||||
channelOps: make(map[string]*sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterUpstreamAPI(NewUpstreamAPI())
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) Start(ctx context.Context) error {
|
||||
u.running = true
|
||||
lastReloadTime := int64(0)
|
||||
go func() {
|
||||
for u.running {
|
||||
now := time.Now().Unix()
|
||||
reloadInterval := g.Cfg().MustGet(ctx, "upstreamapi.reload_interval", 60).Int64()
|
||||
if now-lastReloadTime >= reloadInterval {
|
||||
lastReloadTime = now
|
||||
err := u.loop(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sUpstreamAPI loop err %+v", err)
|
||||
}
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}()
|
||||
g.Log().Infof(ctx, "sUpstreamAPI Start success")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) Stop(ctx context.Context) error {
|
||||
u.running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) RefreshChannel(ctx context.Context, chanKey string) error {
|
||||
unlock := u.lockChannel(chanKey)
|
||||
defer unlock()
|
||||
return u.refreshChannelLocked(ctx, chanKey)
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) refreshChannelLocked(ctx context.Context, chanKey string) error {
|
||||
if err := service.ChannelConfig().RefreshChannel(ctx, chanKey); err != nil {
|
||||
return err
|
||||
}
|
||||
channelConf, ok := service.ChannelConfig().GetChannel(chanKey)
|
||||
if !ok || channelConf == nil {
|
||||
return gerror.Newf("channel config not found: %s", chanKey)
|
||||
}
|
||||
if !channelConf.CanCreateInstance() {
|
||||
u.UnregisterChan(chanKey)
|
||||
g.Log().Infof(ctx, "channel instance removed because is_normal is not 0, chanKey: %s", chanKey)
|
||||
return nil
|
||||
}
|
||||
temp, ok := u.GetTemp(channelConf.Template.TempKey)
|
||||
if !ok || temp == nil {
|
||||
return gerror.Newf("channel template not registered: %s", channelConf.Template.TempKey)
|
||||
}
|
||||
instance := temp.NewTemp(ctx, channelConf)
|
||||
if instance == nil {
|
||||
return gerror.Newf("create channel instance failed: %s", chanKey)
|
||||
}
|
||||
u.RegisterChan(chanKey, channelConf.Version, &model.SimCardChannelCache{
|
||||
Instance: instance,
|
||||
Version: channelConf.Version,
|
||||
})
|
||||
g.Log().Infof(ctx, "channel refreshed, chanKey: %s, version: %s", chanKey, channelConf.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveChannel drops the cached configuration and runtime instance for a
|
||||
// channel. Deleting an already absent key is a successful no-op.
|
||||
func (u *sUpstreamAPI) RemoveChannel(ctx context.Context, chanKey string) error {
|
||||
chanKey = strings.TrimSpace(chanKey)
|
||||
if chanKey == "" {
|
||||
return gerror.New("chanKey is required")
|
||||
}
|
||||
unlock := u.lockChannel(chanKey)
|
||||
defer unlock()
|
||||
if err := service.ChannelConfig().RemoveChannel(ctx, chanKey); err != nil {
|
||||
return err
|
||||
}
|
||||
u.UnregisterChan(chanKey)
|
||||
g.Log().Infof(ctx, "channel removed, chanKey: %s", chanKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) loop(ctx context.Context) error {
|
||||
channelConfigs, err := service.ChannelConfig().GetAllChannel(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, snapshot := range channelConfigs {
|
||||
if snapshot == nil || snapshot.Channel == nil {
|
||||
continue
|
||||
}
|
||||
chanKey := snapshot.Channel.ChanKey
|
||||
func() {
|
||||
unlock := u.lockChannel(chanKey)
|
||||
defer unlock()
|
||||
if err := service.ChannelConfig().RefreshChannel(ctx, chanKey); err != nil {
|
||||
g.Log().Errorf(ctx, "reload channel config failed, chanKey: %s, err: %+v", chanKey, err)
|
||||
return
|
||||
}
|
||||
channelConf, ok := service.ChannelConfig().GetChannel(chanKey)
|
||||
if !ok || channelConf == nil {
|
||||
return
|
||||
}
|
||||
if !channelConf.CanCreateInstance() {
|
||||
u.UnregisterChan(chanKey)
|
||||
return
|
||||
}
|
||||
tempKey := channelConf.Template.TempKey
|
||||
channelCache, ok := u.GetChan(chanKey)
|
||||
if ok && channelCache != nil && channelCache.Version == channelConf.Version {
|
||||
return
|
||||
}
|
||||
|
||||
isNew := !ok || channelCache == nil
|
||||
g.Log().Infof(ctx, "Create channel instance begin, chanKey: %s, isNew: %v", chanKey, isNew)
|
||||
temp, ok := u.GetTemp(tempKey)
|
||||
if !ok || temp == nil {
|
||||
g.Log().Errorf(ctx, "Create channel instance failed, Temp not found, tempKey: %s", tempKey)
|
||||
return
|
||||
}
|
||||
instance := temp.NewTemp(ctx, channelConf)
|
||||
if instance == nil {
|
||||
g.Log().Errorf(ctx, "Create channel instance failed, Temp new instance failed, tempKey: %s", tempKey)
|
||||
return
|
||||
}
|
||||
channelCache = &model.SimCardChannelCache{Instance: instance, Version: channelConf.Version}
|
||||
u.RegisterChan(chanKey, channelConf.Version, channelCache)
|
||||
g.Log().Infof(ctx, "Create channel instance success, chanKey: %s, version: %s", chanKey, channelConf.Version)
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) lockChannel(chanKey string) func() {
|
||||
u.channelMux.Lock()
|
||||
lock := u.channelOps[chanKey]
|
||||
if lock == nil {
|
||||
lock = &sync.Mutex{}
|
||||
u.channelOps[chanKey] = lock
|
||||
}
|
||||
u.channelMux.Unlock()
|
||||
lock.Lock()
|
||||
return lock.Unlock
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) UnregisterChan(chanKey string) {
|
||||
u.Lock()
|
||||
delete(u.chanMap, chanKey)
|
||||
u.Unlock()
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) RegisterChan(chanKey string, version string, channel *model.SimCardChannelCache) {
|
||||
u.Lock()
|
||||
defer u.Unlock()
|
||||
u.chanMap[chanKey] = channel
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) removeChan(chanKey string) {
|
||||
u.Lock()
|
||||
defer u.Unlock()
|
||||
delete(u.chanMap, chanKey)
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) RegisterTemp(tempKey string, channel model.SimCardTempI) {
|
||||
u.Lock()
|
||||
defer u.Unlock()
|
||||
u.tempMap[tempKey] = channel
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) GetTemp(tempKey string) (model.SimCardTempI, bool) {
|
||||
u.RLock()
|
||||
defer u.RUnlock()
|
||||
temp, ok := u.tempMap[tempKey]
|
||||
return temp, ok
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) GetChan(chanKey string) (*model.SimCardChannelCache, bool) {
|
||||
u.RLock()
|
||||
defer u.RUnlock()
|
||||
channel, ok := u.chanMap[chanKey]
|
||||
return channel, ok
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) packChanKey(ctx context.Context, simKeys []*model.SimKey) (map[string][]*model.SimKey, error) {
|
||||
chanKeyMap := make(map[string][]*model.SimKey)
|
||||
simKeyMap := make(map[string]*model.SimKey, len(simKeys))
|
||||
unknownChanKeyIccids := make([]string, 0)
|
||||
queryRedisIccidKeys := make([]string, 0)
|
||||
queryDbIccids := make([]string, 0)
|
||||
// 检查通道id是都携带
|
||||
for _, simKey := range simKeys {
|
||||
simKeyMap[simKey.ICCID] = simKey
|
||||
if simKey.ChanKey == "" {
|
||||
unknownChanKeyIccids = append(unknownChanKeyIccids, simKey.ICCID)
|
||||
queryRedisIccidKeys = append(queryRedisIccidKeys, REDIS_KEY+simKey.ICCID)
|
||||
} else {
|
||||
chanKeyMap[simKey.ChanKey] = append(chanKeyMap[simKey.ChanKey], simKey)
|
||||
}
|
||||
}
|
||||
if len(unknownChanKeyIccids) == 0 {
|
||||
return chanKeyMap, nil
|
||||
}
|
||||
|
||||
allIccid := make(map[string]bool)
|
||||
for _, iccid := range unknownChanKeyIccids {
|
||||
allIccid[iccid] = false
|
||||
}
|
||||
// TODO: 临时数据库查询,后面添加内存缓存和Redis缓存
|
||||
// 从Redis中查询通道id
|
||||
redisChanKeys, err := g.Redis().MGet(ctx, queryRedisIccidKeys...)
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, "sUpstreamAPI.packChanKey, query redis error %+v, queryRedisIccidKeys %+v", err, queryRedisIccidKeys)
|
||||
return nil, gerror.New("查询Redis通道id失败")
|
||||
}
|
||||
for iccidKey, chanKey := range redisChanKeys {
|
||||
iccidSplit := strings.Split(iccidKey, ":")
|
||||
if len(iccidSplit) != 4 {
|
||||
g.Log().Debug(ctx, "sUpstreamAPI.packChanKey, iccidKey format error, iccidKey:%s", iccidKey)
|
||||
continue
|
||||
}
|
||||
iccid := iccidSplit[3]
|
||||
chanKeyStr := chanKey.String()
|
||||
if chanKeyStr == "" {
|
||||
// 查询redis为空
|
||||
g.Log().Debug(ctx, "sUpstreamAPI.packChanKey, query redis empty, iccid:%s, simKeys:%v", iccid, simKeys)
|
||||
queryDbIccids = append(queryDbIccids, iccid)
|
||||
continue
|
||||
}
|
||||
allIccid[iccid] = true
|
||||
simKey := simKeyMap[iccid]
|
||||
if simKey == nil {
|
||||
g.Log().Debug(ctx, "sUpstreamAPI.packChanKey, simKey not found, iccid:%s, simKeys:%v", iccid, simKeys)
|
||||
continue
|
||||
}
|
||||
chanKeyMap[chanKeyStr] = append(chanKeyMap[chanKeyStr], &model.SimKey{
|
||||
ChanKey: chanKeyStr,
|
||||
ICCID: iccid,
|
||||
IMSI: simKey.IMSI,
|
||||
MSISDN: simKey.MSISDN,
|
||||
})
|
||||
}
|
||||
if len(queryDbIccids) == 0 {
|
||||
return chanKeyMap, nil
|
||||
}
|
||||
|
||||
var simCardInfos []*entity.TSimCardInfo
|
||||
g.Log().Info(ctx, "sUpstreamAPI.packChanKey, query db, %v", queryDbIccids)
|
||||
err = dao.TSimCardInfo.Ctx(ctx).
|
||||
Fields(dao.TSimCardInfo.Columns().ChanKey, dao.TSimCardInfo.Columns().Iccid).
|
||||
WhereIn(dao.TSimCardInfo.Columns().Iccid, queryDbIccids).Scan(&simCardInfos)
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, "sUpstreamAPI.packChanKey, query db error %+v, queryDbIccids %+v, simKeys %+v", err, queryDbIccids, simKeys)
|
||||
return nil, gerror.New("查询数据库数据失败")
|
||||
}
|
||||
|
||||
for _, simCardInfo := range simCardInfos {
|
||||
allIccid[simCardInfo.Iccid] = true
|
||||
simKey, _ := simKeyMap[simCardInfo.Iccid]
|
||||
chanKeyMap[simCardInfo.ChanKey] = append(chanKeyMap[simCardInfo.ChanKey], &model.SimKey{
|
||||
ChanKey: simCardInfo.ChanKey,
|
||||
ICCID: simCardInfo.Iccid,
|
||||
IMSI: simKey.IMSI,
|
||||
MSISDN: simKey.MSISDN,
|
||||
})
|
||||
}
|
||||
// 异步写入Redis
|
||||
go u.SetRedis(ctx, simCardInfos)
|
||||
for iccid, found := range allIccid {
|
||||
if !found {
|
||||
chanKeyMap[""] = append(chanKeyMap[""], &model.SimKey{
|
||||
ChanKey: "",
|
||||
ICCID: iccid,
|
||||
})
|
||||
}
|
||||
}
|
||||
return chanKeyMap, nil
|
||||
}
|
||||
|
||||
// 物联网卡
|
||||
func (u *sUpstreamAPI) SimBase(ctx context.Context, input *model.SimBaseReqJson) (model.SimBaseResJson, error) {
|
||||
output := make([]*model.SimBaseRes[json.RawMessage], 0)
|
||||
inputMap := make(map[string]*model.SimBaseReqJson)
|
||||
// 提交/恢复/查询阶段的代理异常不是运营商办理失败。
|
||||
failureResData := json.RawMessage(`{"status":"-2"}`)
|
||||
var apnRequest struct {
|
||||
SubmitOnly bool `json:"submitOnly"`
|
||||
RecoverOnly bool `json:"recoverOnly"`
|
||||
QueryJobID string `json:"queryJobId"`
|
||||
}
|
||||
request := g.RequestFromCtx(ctx)
|
||||
if request != nil && request.URL.Path == "/sim/change/sim-apn" && json.Unmarshal(input.ReqData, &apnRequest) == nil && (apnRequest.SubmitOnly || apnRequest.RecoverOnly || apnRequest.QueryJobID != "") {
|
||||
failureResData = json.RawMessage(`{"uncertain":true}`)
|
||||
}
|
||||
chanKeyMap, err := u.packChanKey(ctx, input.SimKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for chanKey, simKeys := range chanKeyMap {
|
||||
inputMap[chanKey] = &model.SimBaseReqJson{
|
||||
SimKeys: simKeys,
|
||||
ReqData: input.ReqData,
|
||||
}
|
||||
}
|
||||
for chanKey, inputNew := range inputMap {
|
||||
if chanKey == "" {
|
||||
for _, simKey := range inputNew.SimKeys {
|
||||
output = append(output, &model.SimBaseRes[json.RawMessage]{
|
||||
SimKey: simKey,
|
||||
Code: consts.ApiStatusError,
|
||||
Message: "通道未配置",
|
||||
ResData: failureResData,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
channel, ok := u.GetChan(chanKey)
|
||||
if !ok || channel.Instance == nil {
|
||||
for _, simKey := range inputNew.SimKeys {
|
||||
output = append(output, &model.SimBaseRes[json.RawMessage]{
|
||||
SimKey: simKey,
|
||||
Code: consts.ApiStatusError,
|
||||
Message: "通道未实例化",
|
||||
ResData: failureResData,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
out, err := channel.Instance.SimBase(ctx, inputNew)
|
||||
if err != nil {
|
||||
for _, simKey := range inputNew.SimKeys {
|
||||
output = append(output, &model.SimBaseRes[json.RawMessage]{
|
||||
SimKey: simKey,
|
||||
Code: consts.ApiStatusError,
|
||||
Message: err.Error(),
|
||||
ResData: failureResData,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
for _, item := range out {
|
||||
output = append(output, &model.SimBaseRes[json.RawMessage]{
|
||||
SimKey: item.SimKey,
|
||||
Code: item.Code,
|
||||
Message: item.Message,
|
||||
ErrCode: item.ErrCode,
|
||||
ResData: item.ResData,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// 异步写入redis
|
||||
func (u *sUpstreamAPI) SetRedis(ctx context.Context, simCardInfos []*entity.TSimCardInfo) {
|
||||
for _, simCardInfo := range simCardInfos {
|
||||
_, err := g.Redis().Set(ctx, REDIS_KEY+simCardInfo.Iccid, simCardInfo.ChanKey)
|
||||
if err != nil {
|
||||
g.Log().Debugf(ctx, "sUpstreamAPI.SetRedis, Set Redis error %+v, key %s, simCardInfo %+v", err, REDIS_KEY+simCardInfo.Iccid, simCardInfo)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package upstreamimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
)
|
||||
|
||||
func TestApikaProxyTemplateRegistration(t *testing.T) {
|
||||
template, ok := service.UpstreamAPI().GetTemp("sim_onelink_kedang_v1")
|
||||
if !ok || template == nil {
|
||||
t.Fatal("apika proxy template not registered")
|
||||
}
|
||||
config := &model.SimChannelConfigItemCache{
|
||||
Template: &entity.TSimChannelTemplate{TempKey: "sim_onelink_kedang_v1"},
|
||||
Channel: &entity.TSimChannels{ChanKey: "apika-test"},
|
||||
}
|
||||
if instance := template.NewTemp(context.Background(), config); instance == nil {
|
||||
t.Fatal("apika proxy template failed to instantiate")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package upstreamimpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gcfg"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/logic/upstreamapi"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
)
|
||||
|
||||
func TestAPNProxyHTTPFailureIsUncertainOnlyOnAPNRoute(t *testing.T) {
|
||||
oldAdapter := g.Cfg().GetAdapter()
|
||||
defer g.Cfg().SetAdapter(oldAdapter)
|
||||
for _, tc := range []struct {
|
||||
name, path, body string
|
||||
disconnect, uncertain bool
|
||||
}{
|
||||
{"HTTP200顶层9999", "/sim/change/sim-apn", `{"code":9999,"message":"模拟网关未确认"}`, false, true},
|
||||
{"通道连接断开", "/sim/change/sim-apn", "", true, true},
|
||||
{"非APN同名字段", "/sim/change/sim-status", `{"code":9999,"message":"模拟非APN网关错误"}`, false, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
chanServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if tc.disconnect {
|
||||
conn, _, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(tc.body))
|
||||
}))
|
||||
defer chanServer.Close()
|
||||
adapter, err := gcfg.NewAdapterContent(`{"gateway":{"address":"` + strings.TrimPrefix(chanServer.URL, "http://") + `","timeout":"1s"}}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g.Cfg().SetAdapter(adapter)
|
||||
api := upstreamapi.NewUpstreamAPI()
|
||||
api.RegisterChan("fake-channel", "test", &model.SimCardChannelCache{Instance: &sSimYD{}})
|
||||
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var input model.SimBaseReqJson
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
ctx := (&ghttp.Request{Request: r}).Context()
|
||||
items, err := api.SimBase(ctx, &input)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": items})
|
||||
}))
|
||||
defer proxy.Close()
|
||||
body := []byte(`{"simKeys":[{"chanKey":"fake-channel","iccid":"fake-card"}],"reqData":{"operationId":"fake-operation","submitOnly":true}}`)
|
||||
response, err := http.Post(proxy.URL+tc.path, "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
var output struct {
|
||||
Code int `json:"code"`
|
||||
Data model.SimBaseResJson `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(response.Body).Decode(&output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.StatusCode != 200 || output.Code != 0 || len(output.Data) != 1 || output.Data[0].Code != 9999 || output.Data[0].Message == "" {
|
||||
t.Fatalf("代理异常响应无效:HTTP=%d result=%+v", response.StatusCode, output)
|
||||
}
|
||||
isUncertain := strings.Contains(string(output.Data[0].ResData), `"uncertain":true`)
|
||||
if isUncertain != tc.uncertain {
|
||||
t.Fatalf("APN路由隔离失效:%s", output.Data[0].ResData)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPNProxyHTTPKeepsDurableJobAndFrozenRequest(t *testing.T) {
|
||||
oldAdapter := g.Cfg().GetAdapter()
|
||||
defer g.Cfg().SetAdapter(oldAdapter)
|
||||
var captured model.SimBaseReqJson
|
||||
chanServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"code":0,"data":[{"code":0,"simKey":{"chanKey":"fake-channel","iccid":"fake-card","msisdn":"frozen-number"},"resData":{"orderNum":"saved-job","pending":true}}]}`))
|
||||
}))
|
||||
defer chanServer.Close()
|
||||
adapter, err := gcfg.NewAdapterContent(`{"gateway":{"address":"` + strings.TrimPrefix(chanServer.URL, "http://") + `","timeout":"1s"}}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g.Cfg().SetAdapter(adapter)
|
||||
api := upstreamapi.NewUpstreamAPI()
|
||||
api.RegisterChan("fake-channel", "test", &model.SimCardChannelCache{Instance: &sSimYD{}})
|
||||
input := &model.SimBaseReqJson{SimKeys: []*model.SimKey{{ChanKey: "fake-channel", ICCID: "fake-card", MSISDN: "frozen-number"}}, ReqData: json.RawMessage(`{"operationId":"stable-operation","recoverOnly":true}`)}
|
||||
ctx := (&ghttp.Request{Request: httptest.NewRequest("POST", "/sim/change/sim-apn", nil)}).Context()
|
||||
items, err := api.SimBase(ctx, input)
|
||||
if err != nil || len(items) != 1 || !strings.Contains(string(items[0].ResData), `"orderNum":"saved-job"`) || !strings.Contains(string(items[0].ResData), `"pending":true`) {
|
||||
t.Fatalf("HTTP转发丢失jobId:%+v %v", items, err)
|
||||
}
|
||||
if string(captured.ReqData) != string(input.ReqData) || captured.SimKeys[0].MSISDN != "frozen-number" {
|
||||
t.Fatalf("恢复请求被改写:%+v", captured)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package dx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config 网关鉴权与请求配置
|
||||
type Config struct {
|
||||
BaseURL string // 默认 https://cmp-api.ctwing.cn:20164
|
||||
AppKey string
|
||||
AppSecret string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// Client API 客户端
|
||||
type Client struct {
|
||||
baseURL string
|
||||
appKey string
|
||||
appSecret string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
// New 创建客户端
|
||||
func NewClient(cfg Config) *Client {
|
||||
base := cfg.BaseURL
|
||||
if base == "" {
|
||||
base = DefaultBaseURL
|
||||
}
|
||||
timeout := cfg.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = 10 * time.Second
|
||||
}
|
||||
return &Client{
|
||||
baseURL: strings.TrimSuffix(base, "/"),
|
||||
appKey: cfg.AppKey,
|
||||
appSecret: cfg.AppSecret,
|
||||
http: &http.Client{
|
||||
Timeout: timeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// DoGetRequest 通用GET请求(自动处理签名)
|
||||
// endpoint: API路径,如 "/api/v1/openbill/queryTraffic"
|
||||
// params: 查询参数,需调用方自行构建
|
||||
func (c *Client) DoGetRequest(ctx context.Context, endpoint string, params url.Values) ([]byte, error) {
|
||||
req, err := c.doGetRequest(ctx, endpoint, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.doRequest(req)
|
||||
}
|
||||
|
||||
// doGetRequest 封装通用的 GET 请求构建逻辑
|
||||
func (c *Client) doGetRequest(ctx context.Context, endpoint string, params url.Values) (*http.Request, error) {
|
||||
queryString := params.Encode()
|
||||
timestamp := GetTimestampGMT()
|
||||
signStr, err := BuildSign(queryString, "", c.appSecret, timestamp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqURL := c.baseURL + endpoint + "?" + queryString
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("AppKey", c.appKey)
|
||||
req.Header.Set("Sign", signStr)
|
||||
req.Header.Set("Timestamp", timestamp)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// doRequest 执行HTTP请求
|
||||
func (c *Client) doRequest(req *http.Request) ([]byte, error) {
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("http status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package dx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultBaseURL 电信API基础地址
|
||||
DefaultBaseURL = "https://cmp-api.ctwing.cn:20164"
|
||||
)
|
||||
|
||||
// GetTimestampGMT 返回 GMT 时区的 yyyyMMddHHmmss 格式时间戳
|
||||
func GetTimestampGMT() string {
|
||||
return time.Now().UTC().Format("20060102150405")
|
||||
}
|
||||
|
||||
// OrderParams 将 URL params 按 key 字典序排序
|
||||
// 输入如 "b=1&c=2&a=1",输出 "a=1&b=1&c=2"
|
||||
func OrderParams(params string) (string, error) {
|
||||
if params == "" {
|
||||
return "", nil
|
||||
}
|
||||
kvs := strings.Split(params, "&")
|
||||
m := make(map[string]string, len(kvs))
|
||||
for _, kv := range kvs {
|
||||
idx := strings.Index(kv, "=")
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
m[kv[:idx]] = kv[idx+1:]
|
||||
}
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var parts []string
|
||||
for _, k := range keys {
|
||||
parts = append(parts, k+"="+m[k])
|
||||
}
|
||||
return strings.Join(parts, "&"), nil
|
||||
}
|
||||
|
||||
// SignMD5 对 reqStr 做 MD5 加密,返回小写十六进制字符串
|
||||
func SignMD5(reqStr string) string {
|
||||
h := md5.Sum([]byte(reqStr))
|
||||
return strings.ToLower(hex.EncodeToString(h[:]))
|
||||
}
|
||||
|
||||
// BuildSign 构造签名:params 排序后拼接 params+body+secretKey+timestamp 再 MD5
|
||||
// 注意:与参考项目一致,对 MD5 结果再做一次 hex 编码
|
||||
func BuildSign(params, body, secretKey, timestamp string) (string, error) {
|
||||
ordered, err := OrderParams(params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
reqStr := ordered + body + secretKey + timestamp
|
||||
md5Str := SignMD5(reqStr)
|
||||
// 对 MD5 结果再做一次 hex 编码(与参考项目一致)
|
||||
hexStr := hex.EncodeToString([]byte(md5Str))
|
||||
return hexStr, nil
|
||||
}
|
||||
|
||||
// BuildSignWithDebug 构造签名并输出调试信息
|
||||
func BuildSignWithDebug(ctx context.Context, params, body, secretKey, timestamp, appKey string) (string, error) {
|
||||
ordered, err := OrderParams(params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
reqStr := ordered + body + secretKey + timestamp
|
||||
|
||||
maskedSecret := secretKey
|
||||
|
||||
g.Log().Debugf(ctx, "[签名调试] AppKey: %s", appKey)
|
||||
g.Log().Debugf(ctx, "[签名调试] SecretKey: %s", maskedSecret)
|
||||
g.Log().Debugf(ctx, "[签名调试] Timestamp: %s", timestamp)
|
||||
g.Log().Debugf(ctx, "[签名调试] 排序后参数: %s", ordered)
|
||||
g.Log().Debugf(ctx, "[签名调试] Body: '%s'", body)
|
||||
g.Log().Debugf(ctx, "[签名调试] 签名原串: %s%s[SecretKey]%s", ordered, body, timestamp)
|
||||
|
||||
md5Str := SignMD5(reqStr)
|
||||
g.Log().Debugf(ctx, "[签名调试] MD5结果: %s", md5Str)
|
||||
|
||||
// 对 MD5 结果再做一次 hex 编码
|
||||
hexStr := hex.EncodeToString([]byte(md5Str))
|
||||
g.Log().Debugf(ctx, "[签名调试] 最终签名(hex编码后): %s", hexStr)
|
||||
|
||||
return hexStr, nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package upstreamimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"strings"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/net/gclient"
|
||||
)
|
||||
|
||||
const (
|
||||
// gatewayBatchTimeoutConfigKey 限制单次批量转发的最长等待,防止上游异常时无限占用连接。
|
||||
gatewayBatchTimeoutConfigKey = "gateway.batchTimeout"
|
||||
// defaultGatewayBatchTimeout 覆盖 100 张卡按单卡 10 秒预算的最坏耗时,并留有余量。
|
||||
defaultGatewayBatchTimeout = 20 * time.Minute
|
||||
// defaultDataServiceTimeout 是数据开停转发的独立预算。
|
||||
defaultDataServiceTimeout = 210 * time.Second
|
||||
)
|
||||
|
||||
// configureGatewayTimeout 让响应头等待与整次请求使用同一预算,避免默认30秒提前中断。
|
||||
// upstream-chan 对批量操作按卡串行调用运营商接口,耗时随卡数线性增长;
|
||||
// 因此批量请求不能沿用 gclient 默认的 30 秒响应头超时,否则请求仍在办理就会被判为失败。
|
||||
func configureGatewayTimeout(ctx context.Context, client *gclient.Client, path string, count int, perCard time.Duration) {
|
||||
if path == "/sim/change/sim-apn" || path == "/sim/query/sim-apn-capability" {
|
||||
client.SetContentType("application/json")
|
||||
}
|
||||
applyGatewayTimeout(client, path, count, perCard,
|
||||
configDuration(ctx, "gateway.dataServiceTimeout", defaultDataServiceTimeout),
|
||||
configDuration(ctx, gatewayBatchTimeoutConfigKey, defaultGatewayBatchTimeout))
|
||||
}
|
||||
|
||||
// applyGatewayTimeout 计算并应用单次转发预算:整次请求与响应头等待必须一致,
|
||||
// 否则响应头超时会在业务仍在办理时提前中断请求。
|
||||
func applyGatewayTimeout(client *gclient.Client, path string, count int, perCard, dataServiceTimeout, batchTimeout time.Duration) {
|
||||
if count < 1 {
|
||||
count = 1
|
||||
}
|
||||
if perCard <= 0 {
|
||||
perCard = 10 * time.Second
|
||||
}
|
||||
timeout := perCard * time.Duration(count)
|
||||
if path == "/sim/change/sim-apn" {
|
||||
timeout = dataServiceTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = defaultDataServiceTimeout
|
||||
}
|
||||
} else if batchTimeout > 0 && timeout > batchTimeout {
|
||||
timeout = batchTimeout
|
||||
}
|
||||
client.SetTimeout(timeout)
|
||||
if transport, ok := client.Transport.(*http.Transport); ok {
|
||||
transport.ResponseHeaderTimeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
// configDuration 读取时长配置,兼容写成 "20m" 字符串或秒数整数两种形式。
|
||||
func configDuration(ctx context.Context, key string, fallback time.Duration) time.Duration {
|
||||
value := g.Cfg().MustGet(ctx, key)
|
||||
if value == nil || value.IsNil() {
|
||||
return fallback
|
||||
}
|
||||
if duration := value.Duration(); duration >= time.Second {
|
||||
return duration
|
||||
}
|
||||
if seconds := value.Int(); seconds > 0 {
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func readGatewayResponse(response *gclient.Response) (model.SimBaseResJson, error) {
|
||||
defer response.Close()
|
||||
body := response.ReadAll()
|
||||
var output struct {
|
||||
Code *int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data model.SimBaseResJson `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &output); err != nil {
|
||||
return nil, gerror.Wrap(err, "解析网关响应失败")
|
||||
}
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 || output.Code == nil || *output.Code != 0 {
|
||||
message := strings.TrimSpace(output.Message)
|
||||
if message == "" {
|
||||
message = fmt.Sprintf("网关响应无效,HTTP状态码:%d", response.StatusCode)
|
||||
}
|
||||
return nil, gerror.New(message)
|
||||
}
|
||||
return output.Data, nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package upstreamimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func TestGatewayTimeoutAllowsDelayedHeaders(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
_, _ = w.Write([]byte(`{"code":0,"data":[]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
client := g.Client()
|
||||
other := g.Client()
|
||||
original := other.Transport.(*http.Transport)
|
||||
original.ResponseHeaderTimeout = time.Millisecond
|
||||
client.Transport.(*http.Transport).ResponseHeaderTimeout = time.Millisecond
|
||||
configureGatewayTimeout(context.Background(), client, "/sim/change/sim-apn", 1, time.Second)
|
||||
response, err := client.Post(context.Background(), server.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Close()
|
||||
if original.ResponseHeaderTimeout != time.Millisecond {
|
||||
t.Fatal("修改了共享Transport")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonAPNGatewayTimeoutPreservesHeaderBudget(t *testing.T) {
|
||||
client := g.Client()
|
||||
transport := client.Transport.(*http.Transport)
|
||||
transport.ResponseHeaderTimeout = 17 * time.Second
|
||||
configureGatewayTimeout(context.Background(), client, "/sim/query/test", 2, 3*time.Second)
|
||||
if client.Client.Timeout != 6*time.Second || transport.ResponseHeaderTimeout != 6*time.Second {
|
||||
t.Fatalf("非APN响应头预算必须与请求预算一致:request=%s header=%s", client.Client.Timeout, transport.ResponseHeaderTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNonAPNBatchExpandsHeaderBudget 覆盖批量停复机等非 APN 路径:
|
||||
// 50 张卡按单卡预算放大后,响应头等待不能继续停留在 gclient 的 30 秒默认值。
|
||||
func TestNonAPNBatchExpandsHeaderBudget(t *testing.T) {
|
||||
client := g.Client()
|
||||
transport := client.Transport.(*http.Transport)
|
||||
transport.ResponseHeaderTimeout = 30 * time.Second
|
||||
applyGatewayTimeout(client, "/sim/change/sim-status", 50, 10*time.Second, defaultDataServiceTimeout, defaultGatewayBatchTimeout)
|
||||
if client.Client.Timeout != 500*time.Second {
|
||||
t.Fatalf("请求预算=%s,期望500s", client.Client.Timeout)
|
||||
}
|
||||
if transport.ResponseHeaderTimeout != client.Client.Timeout {
|
||||
t.Fatalf("响应头预算=%s与请求预算不一致", transport.ResponseHeaderTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNonAPNBatchRespectsBatchTimeoutCap 保证异常上游不会无限占用连接:
|
||||
// 超出批量上限时按上限截断,且响应头预算同步截断。
|
||||
func TestNonAPNBatchRespectsBatchTimeoutCap(t *testing.T) {
|
||||
client := g.Client()
|
||||
transport := client.Transport.(*http.Transport)
|
||||
applyGatewayTimeout(client, "/sim/query/usage", 1000, 10*time.Second, defaultDataServiceTimeout, defaultGatewayBatchTimeout)
|
||||
if client.Client.Timeout != defaultGatewayBatchTimeout {
|
||||
t.Fatalf("请求预算=%s,期望批量上限%s", client.Client.Timeout, defaultGatewayBatchTimeout)
|
||||
}
|
||||
if transport.ResponseHeaderTimeout != defaultGatewayBatchTimeout {
|
||||
t.Fatalf("响应头预算=%s,期望批量上限%s", transport.ResponseHeaderTimeout, defaultGatewayBatchTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNonAPNSingleCardKeepsPerCardBudget 保证单卡请求不被放大成批量预算。
|
||||
func TestNonAPNSingleCardKeepsPerCardBudget(t *testing.T) {
|
||||
for _, count := range []int{0, 1} {
|
||||
client := g.Client()
|
||||
transport := client.Transport.(*http.Transport)
|
||||
applyGatewayTimeout(client, "/sim/query/test", count, 10*time.Second, defaultDataServiceTimeout, defaultGatewayBatchTimeout)
|
||||
if client.Client.Timeout != 10*time.Second || transport.ResponseHeaderTimeout != 10*time.Second {
|
||||
t.Fatalf("count=%d request=%s header=%s", count, client.Client.Timeout, transport.ResponseHeaderTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataServiceTimeoutOverridesCardBudget(t *testing.T) {
|
||||
for _, count := range []int{0, 1, 100} {
|
||||
client := g.Client()
|
||||
configureGatewayTimeout(context.Background(), client, "/sim/change/sim-apn", count, time.Millisecond)
|
||||
if client.Client.Timeout != 210*time.Second {
|
||||
t.Fatalf("count=%d timeout=%s", count, client.Client.Timeout)
|
||||
}
|
||||
if client.Transport.(*http.Transport).ResponseHeaderTimeout != client.Client.Timeout {
|
||||
t.Fatal("响应头预算不一致")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package upstreamimpl_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gcfg"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/logic"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
)
|
||||
|
||||
func TestMingqianProxyTemplate(t *testing.T) {
|
||||
template, ok := service.UpstreamAPI().GetTemp("sim_mingqian_v3")
|
||||
if !ok || template == nil {
|
||||
t.Fatal("mingqian proxy template missing")
|
||||
}
|
||||
instance := template.NewTemp(context.Background(), &model.SimChannelConfigItemCache{Template: &entity.TSimChannelTemplate{TempKey: "sim_mingqian_v3"}, Channel: &entity.TSimChannels{ChanKey: "mingqian-test"}})
|
||||
if instance == nil {
|
||||
t.Fatal("mingqian proxy instance missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMingqianProxyPreservesFlowFields(t *testing.T) {
|
||||
const response = `{"code":0,"data":[{"code":0,"message":"","simKey":{"chanKey":"mingqian-test","iccid":"A"},"resData":{"dataUsage":1,"packageFlow":{"totalFlow":10240,"dataUsage":8192,"remainingFlow":2048}}},{"code":0,"message":"package flow unavailable","simKey":{"chanKey":"mingqian-test","iccid":"B"},"resData":{"dataUsage":2,"packageFlowError":"mingqian: business code 500"}},{"code":1,"message":"monthly usage unavailable","simKey":{"chanKey":"mingqian-test","iccid":"C"},"resData":{"dataUsage":0,"packageFlow":{"totalFlow":10,"dataUsage":8,"remainingFlow":2}}}]}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.Method != http.MethodPost || request.URL.Path != "/sim/query/sim-datausage" {
|
||||
t.Errorf("wrong gateway route: %s %s", request.Method, request.URL.Path)
|
||||
}
|
||||
var input model.SimBaseReqJson
|
||||
if err := request.ParseForm(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(request.Form.Get("simKeys")), &input.SimKeys); err != nil || len(input.SimKeys) != 3 || input.SimKeys[0].ICCID != "A" {
|
||||
t.Errorf("request changed: %+v %v", input, err)
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(writer, response)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
config, err := gcfg.NewAdapterContent(fmt.Sprintf(`{"gateway":{"address":%q,"timeout":"1s"}}`, strings.TrimPrefix(server.URL, "http://")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
original := g.Cfg().GetAdapter()
|
||||
g.Cfg().SetAdapter(config)
|
||||
t.Cleanup(func() { g.Cfg().SetAdapter(original) })
|
||||
template, ok := service.UpstreamAPI().GetTemp("sim_mingqian_v3")
|
||||
if !ok {
|
||||
t.Fatal("missing Mingqian registration")
|
||||
}
|
||||
instance := template.NewTemp(context.Background(), &model.SimChannelConfigItemCache{Template: &entity.TSimChannelTemplate{TempKey: "sim_mingqian_v3"}, Channel: &entity.TSimChannels{ChanKey: "mingqian-test"}})
|
||||
request := &ghttp.Request{Request: httptest.NewRequest(http.MethodPost, "/sim/query/sim-datausage", nil)}
|
||||
output, err := instance.SimBase(request.Context(), &model.SimBaseReqJson{
|
||||
SimKeys: []*model.SimKey{{ChanKey: "mingqian-test", ICCID: "A"}, {ChanKey: "mingqian-test", ICCID: "B"}, {ChanKey: "mingqian-test", ICCID: "C"}},
|
||||
ReqData: json.RawMessage(`{}`),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var expected model.SimResponseData[model.SimBaseResJson]
|
||||
if err := json.Unmarshal([]byte(response), &expected); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(output, expected.Data) {
|
||||
t.Fatalf("proxy dropped package fields or changed failure status: %+v", output)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package upstreamimpl_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/logic"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
)
|
||||
|
||||
func TestMobilePublicProxyTemplateRegistration(t *testing.T) {
|
||||
for _, templateKey := range []string{
|
||||
"sim_onelink_daliuliang_v1",
|
||||
"sim__daliuliang_v1",
|
||||
"sim_onelink_mobile_public_v1",
|
||||
} {
|
||||
t.Run(templateKey, func(t *testing.T) {
|
||||
template, ok := service.UpstreamAPI().GetTemp(templateKey)
|
||||
if !ok || template == nil {
|
||||
t.Fatal("mobile public proxy template not registered")
|
||||
}
|
||||
config := &model.SimChannelConfigItemCache{
|
||||
Template: &entity.TSimChannelTemplate{TempKey: templateKey},
|
||||
Channel: &entity.TSimChannels{ChanKey: "daliuliang-test"},
|
||||
}
|
||||
if instance := template.NewTemp(context.Background(), config); instance == nil {
|
||||
t.Fatal("mobile public proxy failed to instantiate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package upstreamimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/logic/upstreamimpl/dx"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/gsvc"
|
||||
)
|
||||
|
||||
// XML命名空间正则
|
||||
var xmlNamespaceRegex = regexp.MustCompile(`xmlns[^=]*="[^"]*"\s*|:web:`)
|
||||
|
||||
type sSimDX struct {
|
||||
channelConfig *model.SimChannelConfigItemCache
|
||||
client *dx.Client
|
||||
}
|
||||
|
||||
func NewSimDX() *sSimDX {
|
||||
return &sSimDX{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
s := NewSimDX()
|
||||
service.UpstreamAPI().RegisterTemp("sim_dx_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_dx_v3", s)
|
||||
service.RegisterSimDX(s)
|
||||
}
|
||||
|
||||
func (s *sSimDX) NewTemp(ctx context.Context, channelConfig *model.SimChannelConfigItemCache) model.SimCardChannelI {
|
||||
client := dx.NewClient(dx.Config{
|
||||
BaseURL: dx.DefaultBaseURL,
|
||||
AppKey: channelConfig.Channel.AppKey,
|
||||
AppSecret: channelConfig.Channel.AppSecret,
|
||||
})
|
||||
return &sSimDX{
|
||||
channelConfig: channelConfig,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sSimDX) getUpstreamUrl(ctx context.Context, serviceUrl string) (string, error) {
|
||||
address, err := g.Cfg().Get(ctx, "gateway.address")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("http://%s%s", address, serviceUrl), nil
|
||||
// return fmt.Sprintf("http://%s%s", "127.0.0.1:8110", serviceUrl), nil
|
||||
|
||||
}
|
||||
|
||||
func (s *sSimDX) getUpstreamTimeout(ctx context.Context) time.Duration {
|
||||
timeout, _ := g.Cfg().Get(ctx, "gateway.timeout")
|
||||
if timeout == nil {
|
||||
return 10 * time.Second
|
||||
}
|
||||
if d := timeout.Duration(); d > 0 {
|
||||
return d
|
||||
}
|
||||
if seconds := timeout.Int(); seconds > 0 {
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
return 10 * time.Second
|
||||
}
|
||||
|
||||
func (s *sSimDX) SimBase(ctx context.Context, input *model.SimBaseReqJson) (model.SimBaseResJson, error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
if r.Method != "POST" {
|
||||
return nil, gerror.New("method not allowed")
|
||||
}
|
||||
path := r.URL.Path
|
||||
if path == "" {
|
||||
return nil, gerror.New("path is required")
|
||||
}
|
||||
url, err := s.getUpstreamUrl(ctx, path)
|
||||
if err != nil {
|
||||
g.Log().Debugf(ctx, "SimBase getUpstreamUrl error %+v, ", err)
|
||||
return nil, err
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
configureGatewayTimeout(ctx, client, path, len(input.SimKeys), s.getUpstreamTimeout(ctx))
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "SimBase post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, err
|
||||
}
|
||||
return readGatewayResponse(resp)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package upstreamimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/gsvc"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
const (
|
||||
gUrlBase = "https://api.iot.10086.cn/v5/"
|
||||
)
|
||||
|
||||
type sSimYD struct {
|
||||
channelConfig *model.SimChannelConfigItemCache
|
||||
seqId atomic.Int64
|
||||
tokenFlight singleflight.Group
|
||||
}
|
||||
|
||||
func NewSimYD() *sSimYD {
|
||||
return &sSimYD{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
s := NewSimYD()
|
||||
service.UpstreamAPI().RegisterTemp("sim_yd_v5", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_yd_mock_v5", s)
|
||||
// These channel implementations live in the upstream gateway. The proxy
|
||||
// forwards their API calls through the same generic gateway route, so any
|
||||
// template registered in iotlcm-upstream-chan must be registered here too:
|
||||
// channel refresh fails with "channel template not registered" otherwise.
|
||||
// sSimYD has no template-specific logic, so an alias per template suffices.
|
||||
service.UpstreamAPI().RegisterTemp("sim_sdtywl_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_onelink_tengyu_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_onelink_xuzhou_mobile_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_onelink_lelian_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_onelink_qianxun_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_onelink_henan_xinjing_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_henan_xinjing_direct_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_onelink_zibo_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_tibiot_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_qianxun_v3", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_yunda_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_mingqian_v3", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_daihe_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_onelink_daihe_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_onelink_daliuliang_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim__daliuliang_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_onelink_mobile_public_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_onelink_kedang_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("lanshang_8003", s)
|
||||
service.UpstreamAPI().RegisterTemp("lanshang_8000", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_onelink_yunze_v1", s)
|
||||
// 江苏倍信(jsbxkj)共享 Token。协议实现在 chan 的 beixin_token_provider.go;
|
||||
// proxy 只负责把业务 API 转发到 gateway,与腾宇/澜数同一形态。
|
||||
service.UpstreamAPI().RegisterTemp("sim_onelink_beixin_v1", s)
|
||||
service.UpstreamAPI().RegisterTemp("sim_onelink_lanshu_v1", s)
|
||||
service.RegisterSimYD(s)
|
||||
}
|
||||
|
||||
func (s *sSimYD) NewTemp(ctx context.Context, channelConfig *model.SimChannelConfigItemCache) model.SimCardChannelI {
|
||||
return &sSimYD{
|
||||
channelConfig: channelConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sSimYD) getUpstreamUrl(ctx context.Context, serviceUrl string) string {
|
||||
address, _ := g.Cfg().Get(ctx, "gateway.address")
|
||||
return fmt.Sprintf("http://%s%s", address, serviceUrl)
|
||||
// return fmt.Sprintf("http://%s%s", "127.0.0.1:8110", serviceUrl)
|
||||
}
|
||||
|
||||
func (s *sSimYD) getUpstreamTimeout(ctx context.Context) time.Duration {
|
||||
timeout, _ := g.Cfg().Get(ctx, "gateway.timeout")
|
||||
if timeout == nil {
|
||||
return 10 * time.Second
|
||||
}
|
||||
if d := timeout.Duration(); d > 0 {
|
||||
return d
|
||||
}
|
||||
if seconds := timeout.Int(); seconds > 0 {
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
return 10 * time.Second
|
||||
}
|
||||
|
||||
func (s *sSimYD) SimBase(ctx context.Context, input *model.SimBaseReqJson) (model.SimBaseResJson, error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
if r.Method != "POST" {
|
||||
g.Log().Errorf(ctx, "SimYD.SimBase, method not allowed, method %s, input %+v", r.Method, input)
|
||||
return nil, gerror.New("方法调用错误")
|
||||
}
|
||||
path := r.URL.Path
|
||||
if path == "" {
|
||||
g.Log().Errorf(ctx, "SimYD.SimBase, path is required, input %+v", input)
|
||||
return nil, gerror.New("路径不能为空")
|
||||
}
|
||||
url := s.getUpstreamUrl(ctx, path)
|
||||
g.Log().Debugf(ctx, "SimYD.SimBase, url %s", url)
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
configureGatewayTimeout(ctx, client, path, len(input.SimKeys), s.getUpstreamTimeout(ctx))
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "SimYD.SimBase, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.Wrap(err, "请求通道失败")
|
||||
}
|
||||
return readGatewayResponse(resp)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package upstreamimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
)
|
||||
|
||||
// chanTemplateKeys lists every template iotlcm-upstream-chan registers in its
|
||||
// own upstreamimpl init() functions. Source of truth:
|
||||
//
|
||||
// iotlcm-upstream-chan/internal/logic/upstreamimpl/sim_onelink_shared.go:18-29 (shared token family)
|
||||
// iotlcm-upstream-chan/internal/logic/upstreamimpl/sim_daihe.go:17
|
||||
// iotlcm-upstream-chan/internal/logic/upstreamimpl/sim_henan_xinjing_direct.go:18
|
||||
// iotlcm-upstream-chan/internal/logic/upstreamimpl/sim_qianxun.go:25
|
||||
// iotlcm-upstream-chan/internal/logic/upstreamimpl/sim_yunda.go:19
|
||||
// iotlcm-upstream-chan/internal/logic/upstreamimpl/sim_recharge.go:19
|
||||
// iotlcm-upstream-chan/internal/logic/upstreamimpl/sim_yd.go (sim_yd_v5)
|
||||
// iotlcm-upstream-chan/internal/logic/upstreamimpl/sim_yd_mock.go:21
|
||||
// iotlcm-upstream-chan/internal/service/mingqian/adapter.go:16
|
||||
//
|
||||
// Keep this list in sync when a template is added on the chan side: a template
|
||||
// missing here is exactly the failure mode where channel refresh returns
|
||||
// "channel template not registered".
|
||||
var chanTemplateKeys = []string{
|
||||
// 共享 Token 族
|
||||
"sim_onelink_tengyu_v1",
|
||||
"sim_onelink_xuzhou_mobile_v1",
|
||||
"sim_onelink_lelian_v1",
|
||||
"sim_onelink_qianxun_v1",
|
||||
"sim_onelink_henan_xinjing_v1",
|
||||
"sim_onelink_daihe_v1",
|
||||
"sim_onelink_daliuliang_v1",
|
||||
"sim__daliuliang_v1",
|
||||
"sim_onelink_mobile_public_v1",
|
||||
"sim_onelink_kedang_v1",
|
||||
"sim_onelink_yunze_v1",
|
||||
"sim_onelink_lanshu_v1",
|
||||
"sim_onelink_beixin_v1",
|
||||
// 非共享 Token 族
|
||||
"sim_yd_v5",
|
||||
"sim_yd_mock_v5",
|
||||
"sim_sdtywl_v1",
|
||||
"sim_daihe_v1",
|
||||
"sim_henan_xinjing_direct_v1",
|
||||
"sim_qianxun_v3",
|
||||
"sim_yunda_v1",
|
||||
"sim_mingqian_v3",
|
||||
"sim_tibiot_v1",
|
||||
"sim_dx_v1",
|
||||
"sim_dx_v3",
|
||||
}
|
||||
|
||||
// TestUpstreamTemplateParity guards the proxy/gateway template registration
|
||||
// contract: the proxy only forwards to the gateway, so every template the
|
||||
// gateway registers must also be registered here or RefreshChannel fails before
|
||||
// any request is made.
|
||||
func TestUpstreamTemplateParity(t *testing.T) {
|
||||
for _, tempKey := range chanTemplateKeys {
|
||||
tempKey := tempKey
|
||||
t.Run(tempKey, func(t *testing.T) {
|
||||
temp, ok := service.UpstreamAPI().GetTemp(tempKey)
|
||||
if !ok || temp == nil {
|
||||
t.Fatalf("template not registered in proxy: %s", tempKey)
|
||||
}
|
||||
config := &model.SimChannelConfigItemCache{
|
||||
Template: &entity.TSimChannelTemplate{TempKey: tempKey},
|
||||
Channel: &entity.TSimChannels{ChanKey: "parity-test"},
|
||||
}
|
||||
if instance := temp.NewTemp(context.Background(), config); instance == nil {
|
||||
t.Fatalf("template failed to instantiate: %s", tempKey)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user