chore: 初始开源发布
iotlcm-upstream-chan:运营商上游通道适配服务(统一 /sim/* 协议)。 - 内联原私有依赖:yunze-public → internal/pub/(10 包 18 文件), yunze-pb/pb → internal/pb/(9 文件);Consul 注册改用官方 gogf/gf/contrib/registry/consul/v2(API 兼容) - Go module:yz-svr → git.5iot.com/iotlcm-public/iotlcm-upstream-chan - 修复 mingqian 包测试:实现重构后 `adapter.operations` 字段已移除, 测试断言按当前实现语义重写(不改产品行为) - 凭据与内部标识脱敏:MySQL/Redis/Consul 凭据、私网与生产 IP、 内网域名、真实手机号、注释里的真实 token/transid - 移除内部资产:CI 触发器、生产通道种子 SQL、真实凭据测试资源文档 - Dockerfile 改用公共基础镜像,移除写死的 registry 凭据 - 补 Apache-2.0 LICENSE,重写 README,LF 行尾 + .gitattributes
This commit is contained in:
@@ -0,0 +1,568 @@
|
||||
package upstreamimpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/logic/upstreamimpl/dx"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/service"
|
||||
"math"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sSimDXV3 struct {
|
||||
client *dx.Client
|
||||
custNbr string
|
||||
enableOperations bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.UpstreamAPI().RegisterTemp("sim_dx_v3", &sSimDXV3{})
|
||||
}
|
||||
|
||||
func (s *sSimDXV3) NewTemp(ctx context.Context, cfg *model.SimChannelConfigItemCache) model.SimCardChannelI {
|
||||
if cfg == nil || cfg.Channel == nil || cfg.Template == nil {
|
||||
return nil
|
||||
}
|
||||
var template struct {
|
||||
API struct {
|
||||
BaseURL string `json:"baseUrl"`
|
||||
EnableOperations bool `json:"enableOperations"`
|
||||
} `json:"api"`
|
||||
}
|
||||
var channel struct {
|
||||
CustNbr string `json:"custNbr"`
|
||||
CustNumber string `json:"custNumber"`
|
||||
}
|
||||
channelConfig := strings.TrimSpace(cfg.Channel.ChanConfig)
|
||||
if channelConfig == "" {
|
||||
channelConfig = "{}"
|
||||
}
|
||||
if json.Unmarshal([]byte(cfg.Template.TempConfig), &template) != nil || json.Unmarshal([]byte(channelConfig), &channel) != nil {
|
||||
g.Log().Error(ctx, "invalid telecom v3 JSON configuration")
|
||||
return nil
|
||||
}
|
||||
baseURL := strings.TrimRight(strings.TrimSpace(template.API.BaseURL), "/")
|
||||
if baseURL == "" {
|
||||
baseURL = dx.DefaultBaseURL
|
||||
}
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil || u.Host == "" || (u.Scheme != "https" && u.Scheme != "http") || u.RawQuery != "" || u.Fragment != "" || u.User != nil || (u.Path != "" && u.Path != "/") {
|
||||
g.Log().Error(ctx, "telecom v3 baseUrl must be a gateway origin")
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(cfg.Channel.AppKey) == "" || strings.TrimSpace(cfg.Channel.AppSecret) == "" {
|
||||
g.Log().Error(ctx, "telecom v3 credentials are incomplete")
|
||||
return nil
|
||||
}
|
||||
return &sSimDXV3{
|
||||
client: dx.NewClient(dx.Config{BaseURL: baseURL, AppKey: cfg.Channel.AppKey, AppSecret: cfg.Channel.AppSecret,
|
||||
Timeout: resolveUpstreamHTTPTimeout(ctx, cfg.Template.TempConfig), UppercaseSign: true}),
|
||||
custNbr: firstNonEmpty(channel.CustNbr, channel.CustNumber), enableOperations: template.API.EnableOperations,
|
||||
}
|
||||
}
|
||||
|
||||
type dxV3Error struct{ code, message string }
|
||||
|
||||
func (e *dxV3Error) Error() string { return e.message }
|
||||
|
||||
func (s *sSimDXV3) request(ctx context.Context, path string, params url.Values, body any) (map[string]any, error) {
|
||||
var data []byte
|
||||
var err error
|
||||
if body == nil {
|
||||
data, err = s.client.DoGetRequest(ctx, "/api/v3/"+path, params)
|
||||
} else {
|
||||
data, err = json.Marshal(body)
|
||||
if err == nil {
|
||||
data, err = s.client.DoPostJSONRequest(ctx, "/api/v3/"+path, data)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var envelope struct {
|
||||
Code json.RawMessage `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
}
|
||||
if err = json.Unmarshal(data, &envelope); err != nil {
|
||||
return nil, fmt.Errorf("invalid telecom v3 JSON response: %w", err)
|
||||
}
|
||||
code := dxV3RawCode(envelope.Code)
|
||||
if code == "" {
|
||||
return nil, fmt.Errorf("telecom v3 response is missing code")
|
||||
}
|
||||
if code != "0" {
|
||||
return nil, &dxV3Error{code: code, message: firstNonEmpty(envelope.Message, "telecom v3 business error")}
|
||||
}
|
||||
var result map[string]any
|
||||
decoder := json.NewDecoder(bytes.NewReader(envelope.Result))
|
||||
decoder.UseNumber()
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("invalid telecom v3 result: %w", err)
|
||||
}
|
||||
if result == nil {
|
||||
return nil, fmt.Errorf("telecom v3 response has no result")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func dxV3RawCode(raw json.RawMessage) string {
|
||||
if len(raw) == 0 || strings.TrimSpace(string(raw)) == "null" {
|
||||
return ""
|
||||
}
|
||||
var str string
|
||||
if json.Unmarshal(raw, &str) == nil {
|
||||
return strings.TrimSpace(str)
|
||||
}
|
||||
var number json.Number
|
||||
if json.Unmarshal(raw, &number) == nil {
|
||||
return number.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func dxV3String(item map[string]any, name string) string {
|
||||
switch value := item[name].(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(value)
|
||||
case json.Number:
|
||||
return value.String()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func dxV3Params(key *model.SimKey) (url.Values, error) {
|
||||
if key != nil {
|
||||
for _, value := range []struct{ name, value string }{{"msisdn", key.MSISDN}, {"iccid", key.ICCID}, {"imsi", key.IMSI}} {
|
||||
if strings.TrimSpace(value.value) != "" {
|
||||
return url.Values{value.name: {strings.TrimSpace(value.value)}}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("missing msisdn/iccid/imsi")
|
||||
}
|
||||
|
||||
func dxV3Failure[T any](key *model.SimKey, err error) *model.SimBaseRes[T] {
|
||||
item := &model.SimBaseRes[T]{SimKey: key, Code: consts.ApiStatusError, Message: err.Error()}
|
||||
var business *dxV3Error
|
||||
var httpError *dx.HTTPStatusError
|
||||
if errors.As(err, &business) {
|
||||
item.ErrCode = business.code
|
||||
if code, parseErr := strconv.Atoi(business.code); parseErr == nil && code != 0 {
|
||||
item.Code = code
|
||||
}
|
||||
} else if errors.As(err, &httpError) {
|
||||
item.ErrCode = httpError.RespCode
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
// dxV3LegacyUsageConcurrency 是 v3 逐卡回退查询的并发度,与 v1 保持一致,
|
||||
// 避免多卡查询退化成串行 N 次请求。
|
||||
const dxV3LegacyUsageConcurrency = 5
|
||||
|
||||
func dxV3QueryOne[T any](ctx context.Context, s *sSimDXV3, key *model.SimKey, path string,
|
||||
prepare func(url.Values) error, convert func(map[string]any) (T, error), post bool) *model.SimBaseRes[T] {
|
||||
params, err := dxV3Params(key)
|
||||
if err == nil && prepare != nil {
|
||||
err = prepare(params)
|
||||
}
|
||||
var result map[string]any
|
||||
if err == nil {
|
||||
var body any
|
||||
if post {
|
||||
values := make(map[string]string, len(params))
|
||||
for name := range params {
|
||||
values[name] = params.Get(name)
|
||||
}
|
||||
body = values
|
||||
}
|
||||
result, err = s.request(ctx, path, params, body)
|
||||
}
|
||||
if err == nil {
|
||||
for _, identity := range []struct{ name, value string }{{"msisdn", key.MSISDN}, {"iccid", key.ICCID}, {"imsi", key.IMSI}} {
|
||||
if returned := dxV3String(result, identity.name); returned != "" && identity.value != "" && returned != strings.TrimSpace(identity.value) {
|
||||
err = fmt.Errorf("telecom v3 returned a mismatched card identifier")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
var output T
|
||||
if err == nil {
|
||||
output, err = convert(result)
|
||||
}
|
||||
if err != nil {
|
||||
return dxV3Failure[T](key, err)
|
||||
}
|
||||
return &model.SimBaseRes[T]{SimKey: key, ResData: output}
|
||||
}
|
||||
|
||||
func dxV3Query[T any](ctx context.Context, s *sSimDXV3, keys []*model.SimKey, path string,
|
||||
prepare func(url.Values) error, convert func(map[string]any) (T, error), post bool) []*model.SimBaseRes[T] {
|
||||
items := make([]*model.SimBaseRes[T], 0, len(keys))
|
||||
for _, key := range keys {
|
||||
items = append(items, dxV3QueryOne(ctx, s, key, path, prepare, convert, post))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// dxV3QueryConcurrent 与 dxV3Query 结果完全一致(保持输入顺序),只是并行发出单卡请求。
|
||||
func dxV3QueryConcurrent[T any](ctx context.Context, s *sSimDXV3, keys []*model.SimKey, path string,
|
||||
prepare func(url.Values) error, convert func(map[string]any) (T, error), post bool) []*model.SimBaseRes[T] {
|
||||
results := make([]*model.SimBaseRes[T], len(keys))
|
||||
if len(keys) == 0 {
|
||||
return results
|
||||
}
|
||||
jobs := make(chan int)
|
||||
var group sync.WaitGroup
|
||||
workers := min(dxV3LegacyUsageConcurrency, len(keys))
|
||||
for worker := 0; worker < workers; worker++ {
|
||||
group.Add(1)
|
||||
go func() {
|
||||
defer group.Done()
|
||||
for index := range jobs {
|
||||
results[index] = dxV3QueryOne(ctx, s, keys[index], path, prepare, convert, post)
|
||||
}
|
||||
}()
|
||||
}
|
||||
for index := range keys {
|
||||
jobs <- index
|
||||
}
|
||||
close(jobs)
|
||||
group.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
// dxV3BatchThenFill 让批量接口只承担"加速",正确性以单卡查询为基准:
|
||||
// 先按批量取一轮结果,再把其中失败或缺失的卡用单卡并发查询补齐,保留批量成功的项。
|
||||
// 这样批量级故障(鉴权、报文、网关错误)或某张卡被批量接口遗漏时,不会整批/逐卡报废;
|
||||
// 批量整体不可用(参数/窗口/报文异常)时直接退化为逐卡并发,不丢卡、不错位。
|
||||
// 本方法只用于查询类接口(幂等),重复一次调用是安全的。
|
||||
func dxV3BatchThenFill[T any](ctx context.Context, s *sSimDXV3, keys []*model.SimKey, path string,
|
||||
prepare func(url.Values) error, convert func(map[string]any) (T, error),
|
||||
batch func() ([]*model.SimBaseRes[T], error)) []*model.SimBaseRes[T] {
|
||||
results, err := batch()
|
||||
if err != nil || len(results) != len(keys) {
|
||||
return dxV3QueryConcurrent(ctx, s, keys, path, prepare, convert, false)
|
||||
}
|
||||
retryKeys := make([]*model.SimKey, 0, len(keys))
|
||||
retryIndex := make([]int, 0, len(keys))
|
||||
for index, item := range results {
|
||||
if item == nil || item.Code != consts.ApiStatusSuccess {
|
||||
retryKeys = append(retryKeys, keys[index])
|
||||
retryIndex = append(retryIndex, index)
|
||||
}
|
||||
}
|
||||
if len(retryKeys) == 0 {
|
||||
return results
|
||||
}
|
||||
filled := dxV3QueryConcurrent(ctx, s, retryKeys, path, prepare, convert, false)
|
||||
for offset, index := range retryIndex {
|
||||
results[index] = filled[offset]
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func dxV3BasicInfo(item map[string]any) (model.SimBasicInfoItem, error) {
|
||||
output := model.SimBasicInfoItem{ICCID: dxV3String(item, "iccid"), IMSI: dxV3String(item, "imsi"), MSISDN: dxV3String(item, "msisdn"),
|
||||
OpenDate: dxV3String(item, "createTime"), ActiveDate: dxV3String(item, "activeTime"), SimStatus: dxV3String(item, "simStatus"),
|
||||
MainOfferName: firstNonEmpty(dxV3String(item, "mainPackageName"), dxV3String(item, "prodName")), CardBindingType: dxV3String(item, "cardBindingType"),
|
||||
IsDirectVisit: dxV3String(item, "isDirectVisit"), IsHumanLimit: dxV3String(item, "isHumanLimit"), VpdnGreDoamin: dxV3String(item, "vpdnGreDoamin"),
|
||||
VpdnL2tpDoamin: dxV3String(item, "vpdnL2tpDoamin"), AreaLimitType: dxV3String(item, "areaLimitType"), AreaLimitInfo: dxV3String(item, "areaLimitInfo")}
|
||||
if output.ICCID == "" && output.MSISDN == "" && output.IMSI == "" {
|
||||
return output, fmt.Errorf("telecom v3 basic info is missing card identifiers")
|
||||
}
|
||||
output.StopTypeList = normalizeDXStringList(item["stopTypeList"])
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (s *sSimDXV3) QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error) {
|
||||
return dxV3Query(ctx, s, input.SimKeys, "getSimInfo", nil, dxV3BasicInfo, false), nil
|
||||
}
|
||||
|
||||
func dxV3Status(item map[string]any) (model.SimCardStatusItem, error) {
|
||||
status := dxV3String(item, "simStatus")
|
||||
switch status {
|
||||
case "1", "2", "3", "4", "5", "6", "7":
|
||||
default:
|
||||
return model.SimCardStatusItem{}, fmt.Errorf("invalid telecom v3 simStatus: %s", status)
|
||||
}
|
||||
return model.SimCardStatusItem{CardStatus: convertDxCardStatus(context.Background(), status), LastChangeDate: dxV3String(item, "lastChangeTime")}, nil
|
||||
}
|
||||
|
||||
func (s *sSimDXV3) QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error) {
|
||||
return dxV3Query(ctx, s, input.SimKeys, "getSimStatus", nil, dxV3Status, false), nil
|
||||
}
|
||||
|
||||
func (s *sSimDXV3) QuerySimSession(ctx context.Context, input *model.SimSessionInput) (model.SimSessionOutput, error) {
|
||||
return dxV3Query(ctx, s, input.SimKeys, "getOnlineStatus", nil, func(item map[string]any) (model.SimSessionItem, error) {
|
||||
status := consts.CardSessionStatusUnknown
|
||||
switch dxV3String(item, "onlineStatus") {
|
||||
case "1":
|
||||
status = consts.CardSessionStatusOnline
|
||||
case "2":
|
||||
status = consts.CardSessionStatusOffline
|
||||
}
|
||||
return model.SimSessionItem{Status: status, ApnId: dxV3String(item, "apnName"), Imei: dxV3String(item, "imei"), Rat: dxV3String(item, "ratType"),
|
||||
Ipv4: dxV3String(item, "ipv4Address"), Ipv6: dxV3String(item, "ipv6Address"), Ipv6Prefix: dxV3String(item, "ipv6PrefixLen"),
|
||||
CreateTime: firstNonEmpty(dxV3String(item, "startTime"), dxV3String(item, "eventTime"))}, nil
|
||||
}, false), nil
|
||||
}
|
||||
|
||||
func (s *sSimDXV3) QuerySimImei(ctx context.Context, input *model.SimImeiInput) (model.SimImeiOutput, error) {
|
||||
return dxV3Query(ctx, s, input.SimKeys, "getImei", nil, func(item map[string]any) (model.SimImeiItem, error) {
|
||||
return model.SimImeiItem{Imei: dxV3String(item, "imei")}, nil
|
||||
}, false), nil
|
||||
}
|
||||
|
||||
func dxV3Amount(item map[string]any) (float64, error) {
|
||||
value := firstNonEmpty(dxV3String(item, "dataAmount"), dxV3String(item, "totalUsageAmount"))
|
||||
amount, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil || math.IsNaN(amount) || math.IsInf(amount, 0) || amount < 0 {
|
||||
return 0, fmt.Errorf("invalid telecom v3 dataAmount")
|
||||
}
|
||||
if unit := strings.ToUpper(dxV3String(item, "unit")); unit != "" && unit != "MB" {
|
||||
return 0, fmt.Errorf("unexpected telecom v3 usage unit: %s", unit)
|
||||
}
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
func (s *sSimDXV3) QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) (model.SimDataUsageOutput, error) {
|
||||
return dxV3Query(ctx, s, input.SimKeys, "getDataUsage", nil, func(item map[string]any) (model.SimDataUsageItem, error) {
|
||||
amount, err := dxV3Amount(item)
|
||||
return model.SimDataUsageItem{DataUsage: amount}, err
|
||||
}, false), nil
|
||||
}
|
||||
|
||||
func (s *sSimDXV3) QuerySimDataUsageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) {
|
||||
prepare := func(params url.Values) error {
|
||||
if _, err := time.Parse("200601", input.ReqData.QueryDate); err != nil {
|
||||
return fmt.Errorf("invalid queryDate: expected yyyyMM")
|
||||
}
|
||||
params.Set("month", input.ReqData.QueryDate)
|
||||
return nil
|
||||
}
|
||||
convert := func(item map[string]any) (model.SimDataUsageMonthlyOutputItem, error) {
|
||||
amount, err := dxV3Amount(item)
|
||||
return model.SimDataUsageMonthlyOutputItem{DataAmount: amount}, err
|
||||
}
|
||||
if s.monthlyBatchEligible(input) {
|
||||
return dxV3BatchThenFill(ctx, s, input.SimKeys, "getDataUsageByMonth", prepare, convert,
|
||||
func() (model.SimDataUsageMonthlyOutput, error) { return s.QuerySimDataUsageMonthlyBatch(ctx, input) }), nil
|
||||
}
|
||||
if len(input.SimKeys) > 1 {
|
||||
return dxV3QueryConcurrent(ctx, s, input.SimKeys, "getDataUsageByMonth", prepare, convert, false), nil
|
||||
}
|
||||
return dxV3Query(ctx, s, input.SimKeys, "getDataUsageByMonth", prepare, convert, false), nil
|
||||
}
|
||||
|
||||
// monthlyBatchEligible 判断多卡月流量查询能否升级为批量接口:需要配置 custNbr、每张卡都有
|
||||
// MSISDN,且查询月份为当月。与 v1 一致:v1 的自动批量同样只在当月生效,历史月份继续逐卡查询。
|
||||
// 任一不满足则回退逐卡(并发)查询。
|
||||
func (s *sSimDXV3) monthlyBatchEligible(input *model.SimDataUsageMonthlyInput) bool {
|
||||
if input == nil || len(input.SimKeys) < 2 || strings.TrimSpace(s.custNbr) == "" {
|
||||
return false
|
||||
}
|
||||
if !dxBatchMonthAvailable(input.ReqData.QueryDate, time.Now()) {
|
||||
return false
|
||||
}
|
||||
for _, key := range input.SimKeys {
|
||||
if key == nil || strings.TrimSpace(key.MSISDN) == "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *sSimDXV3) QuerySimDataUsageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) {
|
||||
prepare := func(params url.Values) error {
|
||||
day, err := time.ParseInLocation("20060102", input.ReqData.QueryDate, dxBatchLocation)
|
||||
now := time.Now().In(dxBatchLocation)
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, dxBatchLocation)
|
||||
if err != nil || !day.Before(today) {
|
||||
return fmt.Errorf("queryDate must be a completed day in yyyyMMdd format")
|
||||
}
|
||||
params.Set("beginDate", input.ReqData.QueryDate)
|
||||
params.Set("endDate", input.ReqData.QueryDate)
|
||||
return nil
|
||||
}
|
||||
convert := func(item map[string]any) (model.SimDataUsageDailyOutputItem, error) {
|
||||
amount, err := dxV3Amount(item)
|
||||
return model.SimDataUsageDailyOutputItem{QueryDate: input.ReqData.QueryDate, DataAmount: amount}, err
|
||||
}
|
||||
if s.dailyBatchEligible(input) {
|
||||
return dxV3BatchThenFill(ctx, s, input.SimKeys, "getDataUsageBetweenDate", prepare, convert,
|
||||
func() (model.SimDataUsageDailyOutput, error) { return s.QuerySimDataUsageDailyBatch(ctx, input) }), nil
|
||||
}
|
||||
if len(input.SimKeys) > 1 {
|
||||
return dxV3QueryConcurrent(ctx, s, input.SimKeys, "getDataUsageBetweenDate", prepare, convert, false), nil
|
||||
}
|
||||
return dxV3Query(ctx, s, input.SimKeys, "getDataUsageBetweenDate", prepare, convert, false), nil
|
||||
}
|
||||
|
||||
// dailyBatchEligible 与 monthlyBatchEligible 同构:批量仅在已结束且落在最近七天窗口内的日期可用。
|
||||
func (s *sSimDXV3) dailyBatchEligible(input *model.SimDataUsageDailyInput) bool {
|
||||
if input == nil || len(input.SimKeys) < 2 || strings.TrimSpace(s.custNbr) == "" {
|
||||
return false
|
||||
}
|
||||
if !dxBatchDayAvailable(input.ReqData.QueryDate, time.Now()) {
|
||||
return false
|
||||
}
|
||||
for _, key := range input.SimKeys {
|
||||
if key == nil || strings.TrimSpace(key.MSISDN) == "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *sSimDXV3) QuerySimStopReason(ctx context.Context, input *model.SimStopReasonInput) (model.SimStopReasonOutput, error) {
|
||||
return dxV3Query(ctx, s, input.SimKeys, "getSimInfo", nil, func(item map[string]any) (model.SimStopReasonItem, error) {
|
||||
reason := strings.Join(trimNonEmptyStrings([]string{dxV3String(item, "stopStatusName"), dxV3String(item, "stopTypeName")}), "; ")
|
||||
if reason == "" {
|
||||
reason = mapDxStopTypeListText(normalizeDXStringList(item["stopTypeList"]))
|
||||
}
|
||||
return model.SimStopReasonItem{StopReason: reason}, nil
|
||||
}, false), nil
|
||||
}
|
||||
|
||||
func (s *sSimDXV3) QuerySimApnInfo(ctx context.Context, input *model.SimApnInfoInput) (model.SimApnInfoOutput, error) {
|
||||
return dxV3Query(ctx, s, input.SimKeys, "getApnInfo", nil, func(item map[string]any) (model.SimApnInfoItemList, error) {
|
||||
list, ok := item["apnInfoList"].([]any)
|
||||
if !ok {
|
||||
return model.SimApnInfoItemList{}, fmt.Errorf("telecom v3 response is missing apnInfoList")
|
||||
}
|
||||
output := model.SimApnInfoItemList{ApnList: make([]model.SimApnInfoItem, 0, len(list))}
|
||||
for _, entry := range list {
|
||||
apn, ok := entry.(map[string]any)
|
||||
if !ok {
|
||||
return output, fmt.Errorf("invalid telecom v3 APN entry")
|
||||
}
|
||||
output.ApnList = append(output.ApnList, model.SimApnInfoItem{ApnName: dxV3String(apn, "apnName"), Status: dxV3String(apn, "status")})
|
||||
}
|
||||
return output, nil
|
||||
}, false), nil
|
||||
}
|
||||
|
||||
func (s *sSimDXV3) QuerySimCardBindStatus(ctx context.Context, input *model.SimCardBindStatusInput) (model.SimCardBindStatusOutput, error) {
|
||||
return dxV3Query(ctx, s, input.SimKeys, "getSimBindingInfo", nil, func(item map[string]any) (model.SimCardBindStatusItem, error) {
|
||||
return model.SimCardBindStatusItem{BindStatus: dxV3String(item, "simSeparationStatus"), CardBindingType: dxV3String(item, "bindType"),
|
||||
OrderStatus: dxV3String(item, "isOrdered"), BindImei: dxV3String(item, "bindImei"), LastImei: dxV3String(item, "lastImei")}, nil
|
||||
}, false), nil
|
||||
}
|
||||
|
||||
// QuerySimDeviceStatus 查询终端开关机状态(电信口径为“仅 3G”)。
|
||||
//
|
||||
// 注意方法必须是 GET:官方文档把 /api/v3/getDevicePowerStatus 写成 POST,但网关实际只接受
|
||||
// GET(实测 POST 返回 799 S10010“请求方法不匹配”,GET 返回 200)。同一网关下 v1 的
|
||||
// /api/v1/prod/getDevicePowerStatus 反而只接受 POST,两者不可互推。
|
||||
//
|
||||
// 响应有两种形态,需兼容键名:
|
||||
// - 有记录:{"status":"0","statusName":"开机"}(文档口径,小写)
|
||||
// - 无记录:{"Result":"-1","resultName":"无终端开关机信息"}(大写 R,仅 3G 接口对 4G/5G 卡常见)
|
||||
func (s *sSimDXV3) QuerySimDeviceStatus(ctx context.Context, input *model.SimDeviceStatusInput) (model.SimDeviceStatusOutput, error) {
|
||||
return dxV3Query(ctx, s, input.SimKeys, "getDevicePowerStatus", nil, func(item map[string]any) (model.SimDeviceStatusItem, error) {
|
||||
status := firstNonEmpty(dxV3String(item, "status"), dxV3String(item, "Status"), dxV3String(item, "Result"))
|
||||
return model.SimDeviceStatusItem{DeviceStatus: consts.CardSessionStatusUnknown, OperatingStatus: mapDxOperatingStatus(status)}, nil
|
||||
}, false), nil
|
||||
}
|
||||
|
||||
func (s *sSimDXV3) QuerySimRegionLimitStatus(ctx context.Context, input *model.SimRegionLimitStatusInput) (model.SimRegionLimitStatusOutput, error) {
|
||||
return dxV3Query(ctx, s, input.SimKeys, "getSimRegionLimitStatus", nil, func(item map[string]any) (model.SimRegionLimitStatusItem, error) {
|
||||
order, limit := dxV3String(item, "orderStatus"), dxV3String(item, "regionLimitStatus")
|
||||
return model.SimRegionLimitStatusItem{RegionLimitStatus: mapDxRegionLimitStatusByOrder(order, limit), OrderStatus: order, LimitStatus: limit, LimitType: dxV3String(item, "limitType"), LimitArea: dxV3String(item, "restrictedArea")}, nil
|
||||
}, false), nil
|
||||
}
|
||||
|
||||
// QuerySimFrozenState 电信没有独立的冻结状态查询接口。与 v1 一致,返回成功项并附说明文案,
|
||||
// 避免诊断页与通道自检把它当成上游错误(v1 返回 code=0 + “不支持查询”)。
|
||||
func (s *sSimDXV3) QuerySimFrozenState(_ context.Context, input *model.SimFrozenStateInput) (model.SimFrozenStateOutput, error) {
|
||||
output := make(model.SimFrozenStateOutput, 0, len(input.SimKeys))
|
||||
for _, key := range input.SimKeys {
|
||||
output = append(output, &model.SimBaseRes[model.SimFrozenStateItem]{
|
||||
SimKey: key,
|
||||
Code: consts.ApiStatusSuccess,
|
||||
ResData: model.SimFrozenStateItem{
|
||||
FrozenStatus: "不支持查询",
|
||||
FrozenReason: "电信当前无独立冻结查询接口",
|
||||
},
|
||||
})
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// QuerySimDataDiagnosis 电信不提供达量限速档位查询。沿用 v1 的约定返回 ApiInvokeException,
|
||||
// 让上层按“暂不支持”降级为跳过(诊断页 skipped)而不是报错。
|
||||
func (s *sSimDXV3) QuerySimDataDiagnosis(_ context.Context, input *model.SimDataDiagnosisInput) (model.SimDataDiagnosisOutput, error) {
|
||||
output := make(model.SimDataDiagnosisOutput, 0, len(input.SimKeys))
|
||||
for _, key := range input.SimKeys {
|
||||
output = append(output, &model.SimBaseRes[any]{
|
||||
SimKey: key,
|
||||
Code: consts.ApiInvokeException,
|
||||
Message: "电信官方接口暂不提供当前达量限速查询能力",
|
||||
})
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func dxV3Order(item map[string]any) (model.SimCardStatusChangeItem, error) {
|
||||
order := dxV3String(item, "transactionId")
|
||||
if order == "" {
|
||||
return model.SimCardStatusChangeItem{}, fmt.Errorf("telecom v3 operation returned no transactionId")
|
||||
}
|
||||
return model.SimCardStatusChangeItem{OrderNum: order}, nil
|
||||
}
|
||||
|
||||
func (s *sSimDXV3) ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) (model.SimCardStatusChangeOutput, error) {
|
||||
return dxV3Query(ctx, s, input.SimKeys, "updateSimStatus", func(params url.Values) error {
|
||||
if !s.enableOperations {
|
||||
return fmt.Errorf("telecom v3 operations are disabled")
|
||||
}
|
||||
oper, ok := convertDxCardStatusChangeOperType(ctx, input.ReqData.OperType)
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported status operType")
|
||||
}
|
||||
params.Set("operType", oper)
|
||||
return nil
|
||||
}, dxV3Order, false), nil
|
||||
}
|
||||
|
||||
func (s *sSimDXV3) ChangeSimApnFunction(ctx context.Context, input *model.SimCardOperateChangeApnInput) (model.SimCardOperateChangeApnOutput, error) {
|
||||
return dxV3Query(ctx, s, input.SimKeys, "singleCutNet", func(params url.Values) error {
|
||||
if !s.enableOperations {
|
||||
return fmt.Errorf("telecom v3 operations are disabled")
|
||||
}
|
||||
// singleCutNet 是整卡数据开停语义,电信侧没有“按 APN 定向”这一维度。
|
||||
// 与 v1 保持一致:忽略调用方携带的 ApnName(预警自动断网、批量数据开停都会固定带
|
||||
// apnName="CMIOT"),不能因此拒绝请求。
|
||||
if params.Get("msisdn") == "" {
|
||||
return fmt.Errorf("singleCutNet requires msisdn")
|
||||
}
|
||||
switch strings.TrimSpace(input.ReqData.OperType) {
|
||||
case "0":
|
||||
params.Set("operType", "1")
|
||||
case "1":
|
||||
params.Set("operType", "2")
|
||||
default:
|
||||
return fmt.Errorf("unsupported data operType")
|
||||
}
|
||||
return nil
|
||||
}, func(item map[string]any) (model.SimCardOperateChangeApnItem, error) {
|
||||
order, err := dxV3Order(item)
|
||||
return model.SimCardOperateChangeApnItem{OrderNum: order.OrderNum}, err
|
||||
}, true), nil
|
||||
}
|
||||
|
||||
var _ model.SimCardChannelI = (*sSimDXV3)(nil)
|
||||
Reference in New Issue
Block a user