96b1edff45
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
1795 lines
58 KiB
Go
1795 lines
58 KiB
Go
package upstreamimpl
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"encoding/xml"
|
||
"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"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/util/gconv"
|
||
)
|
||
|
||
// XML命名空间正则
|
||
var xmlNamespaceRegex = regexp.MustCompile(`xmlns[^=]*="[^"]*"\s*|:web:`)
|
||
|
||
const dxLegacyUsageConcurrency = 5
|
||
|
||
type dxRegionLimitItem struct {
|
||
OrderStatus any `json:"orderStatus"`
|
||
LimitStatus string `json:"limitStatus"`
|
||
LimitType any `json:"limitType"`
|
||
LimitArea string `json:"limitArea"`
|
||
}
|
||
|
||
func decodeDxRegionLimitData(raw json.RawMessage) (*dxRegionLimitItem, error) {
|
||
rawText := strings.TrimSpace(string(raw))
|
||
if rawText == "" || rawText == "null" {
|
||
return nil, nil
|
||
}
|
||
var item dxRegionLimitItem
|
||
if err := json.Unmarshal([]byte(rawText), &item); err != nil {
|
||
return nil, err
|
||
}
|
||
return &item, nil
|
||
}
|
||
|
||
type sSimDX struct {
|
||
channelConfig *model.SimChannelConfigItemCache
|
||
client *dx.Client
|
||
custNumber string
|
||
}
|
||
|
||
func NewSimDX() *sSimDX {
|
||
return &sSimDX{}
|
||
}
|
||
|
||
func init() {
|
||
s := NewSimDX()
|
||
service.UpstreamAPI().RegisterTemp("sim_dx_v1", s)
|
||
service.RegisterSimDX(s)
|
||
}
|
||
|
||
func (s *sSimDX) NewTemp(ctx context.Context, channelConfig *model.SimChannelConfigItemCache) model.SimCardChannelI {
|
||
client := dx.NewClient(dx.Config{
|
||
BaseURL: parseDXBaseURL(ctx, channelConfig),
|
||
AppKey: channelConfig.Channel.AppKey,
|
||
AppSecret: channelConfig.Channel.AppSecret,
|
||
Timeout: resolveUpstreamHTTPTimeout(ctx, templateConfigJSON(channelConfig)),
|
||
})
|
||
return &sSimDX{
|
||
channelConfig: channelConfig,
|
||
client: client,
|
||
custNumber: parseDXCustNumber(ctx, channelConfig),
|
||
}
|
||
}
|
||
|
||
// QuerySimSession 物联网卡在线信息查询
|
||
// CTIOT_5GCMP_NQ09 - 在线会话状态查询
|
||
// - URL: /api/v1/onlineSessionOpen/qryOnlineSataus
|
||
// - 支持参数: accNum / access_number / iccid / imsi
|
||
func (s *sSimDX) QuerySimSession(ctx context.Context, input *model.SimSessionInput) (model.SimSessionOutput, error) {
|
||
type dxSessionResp struct {
|
||
Code any `json:"code"`
|
||
ResultCode any `json:"resultCode"`
|
||
Msg string `json:"msg"`
|
||
ResultMsg string `json:"resultMsg"`
|
||
Message string `json:"message"`
|
||
Data json.RawMessage `json:"data"`
|
||
}
|
||
type dxSessionItem struct {
|
||
OnlineStatus string `json:"onlineStatus"`
|
||
AccNum string `json:"accNum"`
|
||
ICCID string `json:"iccid"`
|
||
IMSI string `json:"imsi"`
|
||
NetModel string `json:"netModel"`
|
||
AccessType string `json:"accessType"`
|
||
SessionStatus string `json:"sessionStatus"`
|
||
StartTime string `json:"startTime"`
|
||
CreateTime string `json:"createTime"`
|
||
EndTime string `json:"endTime"`
|
||
IPv4Address string `json:"ipv4Address"`
|
||
IPv4 string `json:"ipv4"`
|
||
IPv6Address string `json:"ipv6Address"`
|
||
IPv6 string `json:"ipv6"`
|
||
IPv6Prefix string `json:"ipv6Prefix"`
|
||
Apn string `json:"apn"`
|
||
Imei string `json:"imei"`
|
||
Province string `json:"province"`
|
||
}
|
||
|
||
decodeSessionData := func(raw json.RawMessage) (*dxSessionItem, error) {
|
||
rawText := strings.TrimSpace(string(raw))
|
||
if rawText == "" || rawText == "null" {
|
||
return nil, nil
|
||
}
|
||
if strings.HasPrefix(rawText, "[") {
|
||
var list []dxSessionItem
|
||
if err := json.Unmarshal([]byte(rawText), &list); err != nil {
|
||
return nil, err
|
||
}
|
||
if len(list) == 0 {
|
||
return nil, nil
|
||
}
|
||
return &list[0], nil
|
||
}
|
||
var item dxSessionItem
|
||
if err := json.Unmarshal([]byte(rawText), &item); err != nil {
|
||
return nil, err
|
||
}
|
||
return &item, nil
|
||
}
|
||
|
||
items := make(model.SimSessionOutput, 0, len(input.SimKeys))
|
||
for _, simKey := range input.SimKeys {
|
||
accessNumber := firstNonEmpty(simKey.MSISDN, simKey.ICCID, simKey.IMSI)
|
||
if accessNumber == "" {
|
||
items = append(items, &model.SimBaseRes[model.SimSessionItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: "missing access number",
|
||
ResData: model.SimSessionItem{Status: "2"},
|
||
})
|
||
continue
|
||
}
|
||
|
||
var (
|
||
sessionItem *dxSessionItem
|
||
respCode = consts.ApiStatusError
|
||
respMsg string
|
||
lastErr error
|
||
)
|
||
|
||
attempts := []struct {
|
||
endpoint string
|
||
paramName string
|
||
}{
|
||
{endpoint: "/api/v1/onlineSessionOpen/qryOnlineSataus", paramName: "accNum"},
|
||
{endpoint: "/api/v1/onlineSessionOpen/qryOnlineStatus", paramName: "accNum"},
|
||
{endpoint: "/api/v1/onlineSessionOpen/qryOnlineSataus", paramName: "access_number"},
|
||
{endpoint: "/api/v1/onlineSessionOpen/qryOnlineStatus", paramName: "access_number"},
|
||
{endpoint: "/api/v1/onlineSessionOpen/qryOnlineSataus", paramName: "iccid"},
|
||
{endpoint: "/api/v1/onlineSessionOpen/qryOnlineSataus", paramName: "imsi"},
|
||
}
|
||
|
||
for _, attempt := range attempts {
|
||
paramValue := accessNumber
|
||
switch attempt.paramName {
|
||
case "iccid":
|
||
paramValue = firstNonEmpty(simKey.ICCID, accessNumber)
|
||
case "imsi":
|
||
paramValue = firstNonEmpty(simKey.IMSI, accessNumber)
|
||
}
|
||
respBody, err := s.client.DoGetRequest(ctx, attempt.endpoint, url.Values{
|
||
attempt.paramName: []string{paramValue},
|
||
})
|
||
if err != nil {
|
||
lastErr = err
|
||
continue
|
||
}
|
||
g.Log().Debugf(ctx, "QuerySimSession response endpoint=%s body=%s", attempt.endpoint, string(respBody))
|
||
|
||
var dxResp dxSessionResp
|
||
if err := json.Unmarshal(respBody, &dxResp); err != nil {
|
||
lastErr = err
|
||
continue
|
||
}
|
||
|
||
codeStr := firstNonEmpty(gconv.String(dxResp.Code), gconv.String(dxResp.ResultCode))
|
||
if codeStr == "" {
|
||
codeStr = "0"
|
||
}
|
||
respCode = gconv.Int(codeStr)
|
||
respMsg = firstNonEmpty(dxResp.Msg, dxResp.ResultMsg, dxResp.Message)
|
||
if respCode != 0 {
|
||
continue
|
||
}
|
||
|
||
item, err := decodeSessionData(dxResp.Data)
|
||
if err != nil {
|
||
lastErr = err
|
||
continue
|
||
}
|
||
if item == nil {
|
||
sessionItem = &dxSessionItem{OnlineStatus: "2"}
|
||
lastErr = nil
|
||
break
|
||
}
|
||
sessionItem = item
|
||
lastErr = nil
|
||
break
|
||
}
|
||
|
||
if sessionItem == nil {
|
||
message := respMsg
|
||
if lastErr != nil {
|
||
message = lastErr.Error()
|
||
}
|
||
if message == "" {
|
||
message = "telecom session query failed"
|
||
}
|
||
items = append(items, &model.SimBaseRes[model.SimSessionItem]{
|
||
SimKey: simKey,
|
||
Code: respCode,
|
||
Message: message,
|
||
ResData: model.SimSessionItem{Status: "2"},
|
||
})
|
||
continue
|
||
}
|
||
|
||
sessionStatus := firstNonEmpty(sessionItem.OnlineStatus, sessionItem.SessionStatus)
|
||
if sessionStatus == "" {
|
||
sessionStatus = "2"
|
||
}
|
||
items = append(items, &model.SimBaseRes[model.SimSessionItem]{
|
||
SimKey: simKey,
|
||
Code: 0,
|
||
Message: "",
|
||
ResData: model.SimSessionItem{
|
||
Status: convertDxSessionStatus(ctx, sessionStatus),
|
||
Rat: dxSessionAccessMode(sessionItem.NetModel, sessionItem.AccessType),
|
||
ApnId: sessionItem.Apn,
|
||
Imei: sessionItem.Imei,
|
||
CreateTime: firstNonEmpty(sessionItem.StartTime, sessionItem.CreateTime),
|
||
Ipv4: firstNonEmpty(sessionItem.IPv4Address, sessionItem.IPv4),
|
||
Ipv6: firstNonEmpty(sessionItem.IPv6Address, sessionItem.IPv6),
|
||
Ipv6Prefix: strings.TrimSpace(sessionItem.IPv6Prefix),
|
||
},
|
||
})
|
||
}
|
||
|
||
return items, nil
|
||
}
|
||
|
||
// QuerySimCardStatus 物联网卡卡状态查询
|
||
// CTIOT_5GCMP_LC003 - 卡主状态查询
|
||
// - URL: /openapi/v1/prodinst/queryCardMainStatus
|
||
// - 支持参数: MSISDN(接入号码)
|
||
func (s *sSimDX) QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error) {
|
||
type ProductInfo struct {
|
||
ProductMainStatusCd string `json:"productMainStatusCd"` // 产品主状态码
|
||
ProductMainStatusName string `json:"productMainStatusName"` // 产品主状态名称
|
||
OperatorDefinitionStatusName string `json:"operatorDefinitionStatusName"` // 运营商定义状态名称
|
||
}
|
||
|
||
type DxResp struct {
|
||
Result string `json:"result"` // 状态标识
|
||
ResultCode string `json:"resultCode"` // 状态码
|
||
ResultMsg string `json:"resultMsg"` // 标识说明
|
||
NetBlockStatusName string `json:"netBlockStatusName"`
|
||
NetBlockTypeList []model.SimNetBlockType `json:"netBlockTypeList"`
|
||
Number string `json:"number"` // 查询号码
|
||
ServCreateDate string `json:"servCreateDate"` // 卡生效时间
|
||
ProductInfo []ProductInfo `json:"productInfo"` // 产品信息数组
|
||
}
|
||
|
||
items := make(model.SimCardStatusOutput, 0)
|
||
for _, simKey := range input.SimKeys {
|
||
accessNumber := simKey.MSISDN
|
||
|
||
respBody, err := s.client.DoGetRequest(ctx, "/openapi/v1/prodinst/queryCardMainStatus", url.Values{"access_number": []string{accessNumber}})
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "QuerySimCardStatus error %+v, accessNumber %s", err, accessNumber)
|
||
var statusErr *dx.HTTPStatusError
|
||
errCode := ""
|
||
if errors.As(err, &statusErr) {
|
||
errCode = statusErr.RespCode
|
||
}
|
||
items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
ErrCode: errCode,
|
||
})
|
||
continue
|
||
}
|
||
|
||
g.Log().Debugf(ctx, "QuerySimCardStatus response: %s", string(respBody))
|
||
|
||
var dxResp DxResp
|
||
err = json.Unmarshal(respBody, &dxResp)
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "QuerySimCardStatus unmarshal error %+v, body %s", err, string(respBody))
|
||
items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
|
||
// result = "0" 表示API调用成功
|
||
if dxResp.Result != "0" {
|
||
g.Log().Warningf(ctx, "QuerySimCardStatus upstream error %s, %s", dxResp.Result, dxResp.ResultMsg)
|
||
carrierCode := strings.TrimSpace(dxResp.Result)
|
||
code := gconv.Int(carrierCode)
|
||
if code == 0 {
|
||
code = consts.ApiStatusError
|
||
}
|
||
items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{
|
||
SimKey: simKey,
|
||
Code: code,
|
||
Message: dxResp.ResultMsg,
|
||
ErrCode: carrierCode,
|
||
})
|
||
continue
|
||
}
|
||
|
||
// 从 productInfo 数组获取状态码
|
||
if len(dxResp.ProductInfo) == 0 {
|
||
g.Log().Warningf(ctx, "QuerySimCardStatus productInfo is empty, accessNumber %s", accessNumber)
|
||
items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: "productInfo is empty",
|
||
})
|
||
continue
|
||
}
|
||
|
||
// 转换卡状态码
|
||
cardStatus := convertDxCardStatus(ctx, dxResp.ProductInfo[0].ProductMainStatusCd)
|
||
|
||
items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{
|
||
SimKey: simKey,
|
||
Code: gconv.Int(dxResp.Result),
|
||
Message: dxResp.ResultMsg,
|
||
ResData: model.SimCardStatusItem{
|
||
CardStatus: cardStatus,
|
||
NetBlockStatus: dxNetworkBlockStatus(dxResp.NetBlockStatusName, dxResp.NetBlockTypeList),
|
||
NetBlockTypes: dxResp.NetBlockTypeList,
|
||
LastChangeDate: dxResp.ServCreateDate, // 卡生效时间
|
||
},
|
||
})
|
||
}
|
||
|
||
return items, nil
|
||
}
|
||
|
||
// QuerySimImei 物联网卡IMEI查询
|
||
// CTIOT_5GCMP_NQ003-设备识别码查询
|
||
// - URL: /openapi/v1/prodinst/gete2einfo
|
||
// - 支持参数: iccid / imsi / MSISDN(接入号码)
|
||
func (s *sSimDX) QuerySimImei(ctx context.Context, input *model.SimImeiInput) (model.SimImeiOutput, error) {
|
||
type DxResp struct {
|
||
Result string `json:"result"` // 结果
|
||
ResultMsg string `json:"resultMsg"` // 消息
|
||
Message string `json:"MESSAGE"` // 电信NQ003响应消息
|
||
Imei string `json:"imei"` // IMEI
|
||
}
|
||
|
||
items := make(model.SimImeiOutput, 0)
|
||
for _, simKey := range input.SimKeys {
|
||
|
||
iccid := simKey.ICCID
|
||
|
||
respBody, err := s.client.DoGetRequest(ctx, "/openapi/v1/prodinst/gete2einfo", url.Values{"iccid": []string{iccid}})
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "QuerySimImei error %+v, iccid %s", err, iccid)
|
||
items = append(items, &model.SimBaseRes[model.SimImeiItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
|
||
g.Log().Debugf(ctx, "QuerySimImei response: %s", string(respBody))
|
||
|
||
var dxResp DxResp
|
||
err = json.Unmarshal(respBody, &dxResp)
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "QuerySimImei unmarshal error %+v, body %s", err, string(respBody))
|
||
items = append(items, &model.SimBaseRes[model.SimImeiItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
|
||
if dxResp.Result != "0" {
|
||
message := firstNonEmpty(dxResp.Message, dxResp.ResultMsg)
|
||
code := gconv.Int(dxResp.Result)
|
||
if code == 0 {
|
||
code = consts.ApiStatusError
|
||
}
|
||
g.Log().Warningf(ctx, "QuerySimImei upstream error %s, %s", dxResp.Result, message)
|
||
items = append(items, &model.SimBaseRes[model.SimImeiItem]{
|
||
SimKey: simKey,
|
||
Code: code,
|
||
Message: message,
|
||
})
|
||
continue
|
||
}
|
||
|
||
items = append(items, &model.SimBaseRes[model.SimImeiItem]{
|
||
SimKey: simKey,
|
||
Code: gconv.Int(dxResp.Result),
|
||
Message: firstNonEmpty(dxResp.Message, dxResp.ResultMsg),
|
||
ResData: model.SimImeiItem{
|
||
Imei: dxResp.Imei,
|
||
},
|
||
})
|
||
}
|
||
|
||
return items, nil
|
||
}
|
||
|
||
// QuerySimDataUsage 单卡本月流量累计使用量查询
|
||
// CTIOT_5GCMP_UQ001-流量查询(当月)
|
||
// - URL: /api/v1/openbill/queryTraffic
|
||
// - 支持参数: iccid / MSISDN(接入号码)
|
||
func (s *sSimDX) QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) (model.SimDataUsageOutput, error) {
|
||
// 响应结构 (实际返回XML格式):
|
||
// <root>
|
||
// <web:NEW_DATA_TICKET_QRsp xmlns:web="http://www.example.org/webservice">
|
||
// <IRESULT>0</IRESULT>
|
||
// <TOTAL_BYTES_CNT>14.01MB</TOTAL_BYTES_CNT>
|
||
// ...
|
||
// </web:NEW_DATA_TICKET_QRsp>
|
||
// </root>
|
||
type DxResp struct {
|
||
XMLName xml.Name `xml:"root"`
|
||
Data *struct {
|
||
Result string `xml:"IRESULT"` // 结果 0=成功
|
||
TotalBytesCnt string `xml:"TOTAL_BYTES_CNT"` // 流量
|
||
} `xml:"NEW_DATA_TICKET_QRsp"`
|
||
}
|
||
|
||
items := make(model.SimDataUsageOutput, 0)
|
||
for _, simKey := range input.SimKeys {
|
||
if simKey.MSISDN == "" {
|
||
g.Log().Warningf(ctx, "Msisdn not found for simKey %s", simKey.ChanKey)
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: "MSISDN not found",
|
||
})
|
||
continue
|
||
}
|
||
|
||
accessNumber := simKey.MSISDN
|
||
|
||
respBody, err := s.client.DoGetRequest(ctx, "/api/v1/openbill/queryTraffic", url.Values{"access_number": []string{accessNumber}})
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "QuerySimDataUsage request failed, chanKey:%s, err:%+v", simKey.ChanKey, err)
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
|
||
// 去除XML命名空间前缀
|
||
xmlBody := xmlNamespaceRegex.ReplaceAllString(string(respBody), "")
|
||
|
||
var dxResp DxResp
|
||
err = xml.Unmarshal([]byte(xmlBody), &dxResp)
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "QuerySimDataUsage response parse failed, chanKey:%s, err:%+v", simKey.ChanKey, err)
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
|
||
if dxResp.Data == nil {
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageItem]{SimKey: simKey, Code: consts.ApiStatusError, Message: "电信响应缺少 NEW_DATA_TICKET_QRsp"})
|
||
continue
|
||
}
|
||
resultCode, resultErr := parseDXResultCode(dxResp.Data.Result)
|
||
if resultErr != nil {
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageItem]{SimKey: simKey, Code: consts.ApiStatusError, Message: resultErr.Error(), ErrCode: dxResp.Data.Result})
|
||
continue
|
||
}
|
||
if resultCode != 0 {
|
||
g.Log().Warningf(ctx, "QuerySimDataUsage upstream error, chanKey:%s, code:%s", simKey.ChanKey, dxResp.Data.Result)
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageItem]{
|
||
SimKey: simKey,
|
||
Code: resultCode,
|
||
Message: fmt.Sprintf("流量查询失败: %s", dxResp.Data.Result),
|
||
ErrCode: dxResp.Data.Result,
|
||
})
|
||
continue
|
||
}
|
||
dataUsage, amountErr := parseDXDataUsage(dxResp.Data.TotalBytesCnt)
|
||
if amountErr != nil {
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageItem]{SimKey: simKey, Code: consts.ApiStatusError, Message: amountErr.Error()})
|
||
continue
|
||
}
|
||
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageItem]{
|
||
SimKey: simKey,
|
||
Code: resultCode,
|
||
//Message: "流量查询成功",
|
||
ResData: model.SimDataUsageItem{
|
||
DataUsage: dataUsage,
|
||
},
|
||
})
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
// parseDXDataUsage 解析流量值,格式转换为MB
|
||
func parseDXDataUsage(value string) (float64, error) {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return 0, errors.New("电信流量响应缺少 TOTAL_BYTES_CNT")
|
||
}
|
||
// 检查是否包含单位
|
||
value = strings.ToUpper(strings.TrimSpace(value))
|
||
if strings.HasSuffix(value, "MB") {
|
||
value = strings.TrimSpace(strings.TrimSuffix(value, "MB"))
|
||
mbVal, err := strconv.ParseFloat(value, 64)
|
||
if err != nil || math.IsNaN(mbVal) || math.IsInf(mbVal, 0) || mbVal < 0 {
|
||
return 0, fmt.Errorf("电信流量值无效: %s", value)
|
||
}
|
||
return mbVal, nil
|
||
}
|
||
if strings.HasSuffix(value, "GB") {
|
||
value = strings.TrimSpace(strings.TrimSuffix(value, "GB"))
|
||
gbVal, err := strconv.ParseFloat(value, 64)
|
||
if err != nil || math.IsNaN(gbVal) || math.IsInf(gbVal, 0) || gbVal < 0 {
|
||
return 0, fmt.Errorf("电信流量值无效: %s", value)
|
||
}
|
||
amount := gbVal * 1024
|
||
if math.IsInf(amount, 0) {
|
||
return 0, fmt.Errorf("invalid telecom usage after GB conversion: %s", value)
|
||
}
|
||
return amount, nil // 转换为MB
|
||
}
|
||
if strings.HasSuffix(value, "KB") {
|
||
value = strings.TrimSpace(strings.TrimSuffix(value, "KB"))
|
||
kbVal, err := strconv.ParseFloat(value, 64)
|
||
if err != nil || math.IsNaN(kbVal) || math.IsInf(kbVal, 0) || kbVal < 0 {
|
||
return 0, fmt.Errorf("电信流量值无效: %s", value)
|
||
}
|
||
return kbVal / 1024, nil // 转换为MB
|
||
}
|
||
// 如果是纯数字,直接返回
|
||
amount, err := strconv.ParseFloat(value, 64)
|
||
if err != nil || math.IsNaN(amount) || math.IsInf(amount, 0) || amount < 0 {
|
||
return 0, fmt.Errorf("电信流量值无效: %s", value)
|
||
}
|
||
return amount, nil
|
||
}
|
||
|
||
func parseDataUsage(value string) float64 {
|
||
if value == "" {
|
||
return 0
|
||
}
|
||
// 检查是否包含单位
|
||
value = strings.ToUpper(strings.TrimSpace(value))
|
||
if strings.HasSuffix(value, "MB") {
|
||
mb := strings.TrimSuffix(value, "MB")
|
||
mbVal := gconv.Float64(mb)
|
||
return mbVal
|
||
}
|
||
if strings.HasSuffix(value, "GB") {
|
||
gb := strings.TrimSuffix(value, "GB")
|
||
gbVal := gconv.Float64(gb)
|
||
return gbVal * 1024 // 转换为MB
|
||
}
|
||
if strings.HasSuffix(value, "KB") {
|
||
kb := strings.TrimSuffix(value, "KB")
|
||
kbVal := gconv.Float64(kb)
|
||
return kbVal / 1024 // 转换为MB
|
||
}
|
||
// 如果是纯数字,直接返回
|
||
return gconv.Float64(value)
|
||
}
|
||
|
||
func parseDXResultCode(value string) (int, error) {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return 0, errors.New("电信响应缺少 IRESULT")
|
||
}
|
||
code, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("电信 IRESULT 无效: %s", value)
|
||
}
|
||
return code, nil
|
||
}
|
||
|
||
// QuerySimDataUsageMonthly 单卡单月GPRS流量使用量批量查询
|
||
func (s *sSimDX) QuerySimDataUsageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) {
|
||
if input == nil || len(input.SimKeys) == 0 {
|
||
return make(model.SimDataUsageMonthlyOutput, 0), nil
|
||
}
|
||
if strings.TrimSpace(s.custNumber) != "" && dxBatchMonthAvailable(input.ReqData.QueryDate, time.Now()) {
|
||
return s.queryDataUsageMonthlyBatch(ctx, input)
|
||
}
|
||
if len(input.SimKeys) > 1 {
|
||
return s.querySimDataUsageMonthlyConcurrent(ctx, input), nil
|
||
}
|
||
// XML响应结构 (正常情况)
|
||
type DxRespXML struct {
|
||
XMLName xml.Name `xml:"root"`
|
||
Data *struct {
|
||
Result string `xml:"IRESULT"` // 结果
|
||
SMsg string `xml:"SMSG"` // 消息
|
||
Total string `xml:"TOTAL_BYTES_CNT"` // 流量
|
||
} `xml:"NEW_DATA_TICKET_QRsp"`
|
||
}
|
||
|
||
// JSON错误响应结构 (异常情况)
|
||
type DxRespJSON struct {
|
||
Result string `json:"result"`
|
||
Msg string `json:"msg"`
|
||
}
|
||
|
||
items := make(model.SimDataUsageMonthlyOutput, 0)
|
||
for _, simKey := range input.SimKeys {
|
||
|
||
accessNumber := simKey.MSISDN
|
||
|
||
// 计算月末日期 (API要求YYYYMMDD格式)
|
||
startDate := input.ReqData.QueryDate + "01" // 例如: 20260301
|
||
t, err := time.Parse("20060102", startDate)
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "QuerySimDataUsageMonthly parse date error %+v, queryDate %s", err, input.ReqData.QueryDate)
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: "日期格式错误",
|
||
})
|
||
continue
|
||
}
|
||
endDate := t.AddDate(0, 1, -1).Format("20060102") // 例如: 20260331
|
||
|
||
respBody, err := s.client.DoGetRequest(ctx, "/api/v1/openbill/queryTrafficByDate", url.Values{
|
||
"access_number": []string{accessNumber},
|
||
"startDate": []string{startDate},
|
||
"endDate": []string{endDate},
|
||
"needDtl": []string{"0"},
|
||
})
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "QuerySimDataUsageMonthly error %+v, accessNumber %s", err, accessNumber)
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
|
||
g.Log().Debugf(ctx, "QuerySimDataUsageMonthly response: %s", string(respBody))
|
||
|
||
// 去除XML命名空间前缀
|
||
xmlBody := xmlNamespaceRegex.ReplaceAllString(string(respBody), "")
|
||
|
||
var dxResp DxRespXML
|
||
if err = xml.Unmarshal([]byte(xmlBody), &dxResp); err != nil {
|
||
g.Log().Errorf(ctx, "QuerySimDataUsageMonthly unmarshal error %+v, body %s", err, xmlBody)
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
if dxResp.Data == nil {
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{SimKey: simKey, Code: consts.ApiStatusError, Message: "电信响应缺少 NEW_DATA_TICKET_QRsp"})
|
||
continue
|
||
}
|
||
|
||
resultCode, resultErr := parseDXResultCode(dxResp.Data.Result)
|
||
if resultErr != nil {
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{SimKey: simKey, Code: consts.ApiStatusError, Message: resultErr.Error(), ErrCode: dxResp.Data.Result})
|
||
continue
|
||
}
|
||
if resultCode != 0 {
|
||
g.Log().Warningf(ctx, "QuerySimDataUsageMonthly upstream error %s, %s", dxResp.Data.Result, dxResp.Data.Total)
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{
|
||
SimKey: simKey,
|
||
Code: resultCode,
|
||
Message: dxResp.Data.SMsg,
|
||
})
|
||
continue
|
||
}
|
||
dataUsage, amountErr := parseDXDataUsage(dxResp.Data.Total)
|
||
if amountErr != nil {
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{SimKey: simKey, Code: consts.ApiStatusError, Message: amountErr.Error()})
|
||
continue
|
||
}
|
||
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{
|
||
SimKey: simKey,
|
||
Code: gconv.Int(dxResp.Data.Result),
|
||
//Message: "流量查询成功",
|
||
ResData: model.SimDataUsageMonthlyOutputItem{
|
||
DeadLine: input.ReqData.QueryDate,
|
||
DataAmount: dataUsage,
|
||
},
|
||
})
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
// QuerySimDataUsageDaily 单卡单日GPRS流量使用量查询
|
||
// CTIOT_5GCMP_UQ001-流量查询(按日期)
|
||
// - URL: /api/v1/openbill/queryTrafficByDate
|
||
// - startDate/endDate 均使用 queryDate
|
||
// - TOTAL_BYTES_CNT 格式为 *.**MB,parseDataUsage 统一转换为 MB
|
||
func (s *sSimDX) QuerySimDataUsageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) {
|
||
if input == nil || len(input.SimKeys) == 0 {
|
||
return make(model.SimDataUsageDailyOutput, 0), nil
|
||
}
|
||
if strings.TrimSpace(s.custNumber) != "" && dxBatchDayAvailable(input.ReqData.QueryDate, time.Now()) {
|
||
batchOutput, batchErr := s.queryDataUsageDailyBatch(ctx, input)
|
||
if batchErr == nil && dailyBatchOutputSuccessful(input, batchOutput) {
|
||
return batchOutput, nil
|
||
}
|
||
g.Log().Warningf(ctx, "QuerySimDataUsageDaily batch query failed, fallback legacy, err:%v", batchErr)
|
||
}
|
||
if len(input.SimKeys) > 1 {
|
||
return s.querySimDataUsageDailyConcurrent(ctx, input), nil
|
||
}
|
||
return s.querySimDataUsageDailyLegacy(ctx, input), nil
|
||
}
|
||
|
||
func (s *sSimDX) querySimDataUsageDailyLegacy(ctx context.Context, input *model.SimDataUsageDailyInput) model.SimDataUsageDailyOutput {
|
||
type DxRespXML struct {
|
||
XMLName xml.Name `xml:"root"`
|
||
Data *struct {
|
||
Result string `xml:"IRESULT"`
|
||
SMsg string `xml:"SMSG"`
|
||
Total string `xml:"TOTAL_BYTES_CNT"`
|
||
} `xml:"NEW_DATA_TICKET_QRsp"`
|
||
}
|
||
|
||
items := make(model.SimDataUsageDailyOutput, 0)
|
||
for _, simKey := range input.SimKeys {
|
||
accessNumber := simKey.MSISDN
|
||
queryDate := input.ReqData.QueryDate
|
||
if _, err := time.Parse("20060102", queryDate); err != nil {
|
||
g.Log().Errorf(ctx, "QuerySimDataUsageDaily parse date error %+v, queryDate %s", err, queryDate)
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: "日期格式错误",
|
||
})
|
||
continue
|
||
}
|
||
|
||
respBody, err := s.client.DoGetRequest(ctx, "/api/v1/openbill/queryTrafficByDate", url.Values{
|
||
"access_number": []string{accessNumber},
|
||
"startDate": []string{queryDate},
|
||
"endDate": []string{queryDate},
|
||
"needDtl": []string{"0"},
|
||
})
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "QuerySimDataUsageDaily error %+v, accessNumber %s", err, accessNumber)
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
|
||
g.Log().Debugf(ctx, "QuerySimDataUsageDaily response: %s", string(respBody))
|
||
|
||
xmlBody := xmlNamespaceRegex.ReplaceAllString(string(respBody), "")
|
||
|
||
var dxResp DxRespXML
|
||
if err = xml.Unmarshal([]byte(xmlBody), &dxResp); err != nil {
|
||
g.Log().Errorf(ctx, "QuerySimDataUsageDaily unmarshal error %+v, body %s", err, xmlBody)
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
|
||
if dxResp.Data == nil {
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{SimKey: simKey, Code: consts.ApiStatusError, Message: "电信响应缺少 NEW_DATA_TICKET_QRsp"})
|
||
continue
|
||
}
|
||
resultCode, resultErr := parseDXResultCode(dxResp.Data.Result)
|
||
if resultErr != nil {
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{SimKey: simKey, Code: consts.ApiStatusError, Message: resultErr.Error(), ErrCode: dxResp.Data.Result})
|
||
continue
|
||
}
|
||
if resultCode != 0 {
|
||
g.Log().Warningf(ctx, "QuerySimDataUsageDaily upstream error %s, %s", dxResp.Data.Result, dxResp.Data.Total)
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{
|
||
SimKey: simKey,
|
||
Code: resultCode,
|
||
Message: dxResp.Data.SMsg,
|
||
ResData: model.SimDataUsageDailyOutputItem{
|
||
QueryDate: queryDate,
|
||
DataAmount: 0,
|
||
},
|
||
})
|
||
continue
|
||
}
|
||
dataUsageMB, amountErr := parseDXDataUsage(dxResp.Data.Total)
|
||
if amountErr != nil {
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{SimKey: simKey, Code: consts.ApiStatusError, Message: amountErr.Error(), ResData: model.SimDataUsageDailyOutputItem{QueryDate: queryDate}})
|
||
continue
|
||
}
|
||
|
||
items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{
|
||
SimKey: simKey,
|
||
Code: resultCode,
|
||
ResData: model.SimDataUsageDailyOutputItem{
|
||
QueryDate: queryDate,
|
||
DataAmount: dataUsageMB,
|
||
},
|
||
})
|
||
}
|
||
return items
|
||
}
|
||
|
||
func (s *sSimDX) querySimDataUsageMonthlyConcurrent(ctx context.Context, input *model.SimDataUsageMonthlyInput) model.SimDataUsageMonthlyOutput {
|
||
results := make(model.SimDataUsageMonthlyOutput, len(input.SimKeys))
|
||
jobs := make(chan int)
|
||
var group sync.WaitGroup
|
||
workers := min(dxLegacyUsageConcurrency, len(input.SimKeys))
|
||
for worker := 0; worker < workers; worker++ {
|
||
group.Add(1)
|
||
go func() {
|
||
defer group.Done()
|
||
for index := range jobs {
|
||
simKey := input.SimKeys[index]
|
||
if err := ctx.Err(); err != nil {
|
||
results[index] = dxMonthlyContextFailure(simKey, err)
|
||
continue
|
||
}
|
||
output, err := s.QuerySimDataUsageMonthly(ctx, &model.SimDataUsageMonthlyInput{
|
||
SimKeys: []*model.SimKey{simKey},
|
||
ReqData: input.ReqData,
|
||
})
|
||
if err != nil || len(output) == 0 {
|
||
results[index] = dxMonthlyContextFailure(simKey, err)
|
||
continue
|
||
}
|
||
results[index] = output[0]
|
||
}
|
||
}()
|
||
}
|
||
for index := range input.SimKeys {
|
||
jobs <- index
|
||
}
|
||
close(jobs)
|
||
group.Wait()
|
||
return results
|
||
}
|
||
|
||
func (s *sSimDX) querySimDataUsageDailyConcurrent(ctx context.Context, input *model.SimDataUsageDailyInput) model.SimDataUsageDailyOutput {
|
||
results := make(model.SimDataUsageDailyOutput, len(input.SimKeys))
|
||
jobs := make(chan int)
|
||
var group sync.WaitGroup
|
||
workers := min(dxLegacyUsageConcurrency, len(input.SimKeys))
|
||
for worker := 0; worker < workers; worker++ {
|
||
group.Add(1)
|
||
go func() {
|
||
defer group.Done()
|
||
for index := range jobs {
|
||
simKey := input.SimKeys[index]
|
||
if err := ctx.Err(); err != nil {
|
||
results[index] = dxDailyContextFailure(simKey, err)
|
||
continue
|
||
}
|
||
output := s.querySimDataUsageDailyLegacy(ctx, &model.SimDataUsageDailyInput{
|
||
SimKeys: []*model.SimKey{simKey},
|
||
ReqData: input.ReqData,
|
||
})
|
||
if len(output) == 0 {
|
||
results[index] = dxDailyContextFailure(simKey, nil)
|
||
continue
|
||
}
|
||
results[index] = output[0]
|
||
}
|
||
}()
|
||
}
|
||
for index := range input.SimKeys {
|
||
jobs <- index
|
||
}
|
||
close(jobs)
|
||
group.Wait()
|
||
return results
|
||
}
|
||
|
||
func dxMonthlyContextFailure(simKey *model.SimKey, err error) *model.SimBaseRes[model.SimDataUsageMonthlyOutputItem] {
|
||
message := "telecom monthly usage query returned no result"
|
||
if err != nil {
|
||
message = err.Error()
|
||
}
|
||
return &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{SimKey: simKey, Code: consts.ApiStatusError, Message: message}
|
||
}
|
||
|
||
func dxDailyContextFailure(simKey *model.SimKey, err error) *model.SimBaseRes[model.SimDataUsageDailyOutputItem] {
|
||
message := "telecom daily usage query returned no result"
|
||
if err != nil {
|
||
message = err.Error()
|
||
}
|
||
return &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{SimKey: simKey, Code: consts.ApiStatusError, Message: message}
|
||
}
|
||
|
||
func simDXBasicInfoQueryParamName(simKey *model.SimKey) string {
|
||
if strings.TrimSpace(simKey.ICCID) != "" {
|
||
return "iccid"
|
||
}
|
||
return "access_number"
|
||
}
|
||
|
||
func simDXBasicInfoQueryParamValue(simKey *model.SimKey) string {
|
||
if iccid := strings.TrimSpace(simKey.ICCID); iccid != "" {
|
||
return iccid
|
||
}
|
||
return strings.TrimSpace(simKey.MSISDN)
|
||
}
|
||
|
||
func simDXBasicInfoQueryValues(simKey *model.SimKey) url.Values {
|
||
return url.Values{
|
||
"pageIndex": []string{"1"},
|
||
simDXBasicInfoQueryParamName(simKey): []string{simDXBasicInfoQueryParamValue(simKey)},
|
||
}
|
||
}
|
||
|
||
// QuerySimBasicInfo 单卡基本信息查询
|
||
// CTIOT_5GCMP_BQ004 - SIM卡列表查询
|
||
// - URL: /openapi/v1/prodinst/getSIMList
|
||
// - 支持参数: access_number / iccid / pageIndex(必填)
|
||
func (s *sSimDX) QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error) {
|
||
if output, ok, err := s.querySimBasicInfoBatch(ctx, input); ok {
|
||
if err == nil {
|
||
return output, nil
|
||
}
|
||
g.Log().Warningf(ctx, "QuerySimBasicInfo batch query failed, fallback legacy, err:%+v", err)
|
||
}
|
||
return s.querySimBasicInfoLegacy(ctx, input)
|
||
}
|
||
|
||
func (s *sSimDX) querySimBasicInfoLegacy(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error) {
|
||
type DxResp struct {
|
||
ResultCode string `json:"resultCode"` // 返回状态标识 0成功, 1失败, -3 sign错误, -2参数错误, -1无权限
|
||
ResultMsg string `json:"resultMsg"` // 返回标识说明
|
||
GroupTransactionId string `json:"groupTransactionId"` // 流水号
|
||
Description struct {
|
||
PageIndex string `json:"pageIndex"` // 页码
|
||
SimList []struct {
|
||
AccNumber string `json:"accNumber"` // 接入号码
|
||
LastChangeDate string `json:"lastChangeDate"` // 最后变更时间
|
||
StatusCd string `json:"statusCd"` // 运营商定义状态
|
||
StopTypeList []string `json:"stopTypeList"` // 停机类型
|
||
CustId string `json:"custId"` // 客户ID
|
||
SimStatus []string `json:"simStatus"` // SIM卡状态
|
||
Imsi string `json:"imsi"` // IMSI
|
||
Iccid string `json:"iccid"` // ICCID
|
||
ActivationTime string `json:"activationTime"` // 激活时间
|
||
CreateTime string `json:"createTime"` // 开卡时间
|
||
MainOfferName string `json:"mainOfferName"` // 主套餐名称
|
||
CardBindingType string `json:"cardBindingType"` // 机卡绑定类型
|
||
IsDirectVisit string `json:"isDirectVisit"` // 是否乐通定向
|
||
IsHumanLimit string `json:"isHumanLimit"` // 是否有人联网限制
|
||
VpdnGreDoamin string `json:"vpdnGreDoamin"` // VPDN(GRE)域名
|
||
VpdnL2tpDoamin string `json:"vpdnL2tpDoamin"` // VPDN(L2TP)域名
|
||
AreaLimitType string `json:"areaLimitType"` // 区域限制类型
|
||
AreaLimitInfo string `json:"areaLimitInfo"` // 区域限制信息
|
||
} `json:"simList"` // SIM卡列表
|
||
} `json:"description"`
|
||
}
|
||
|
||
items := make(model.SimBasicInfoOutput, 0)
|
||
for _, simKey := range input.SimKeys {
|
||
|
||
respBody, err := s.client.DoGetRequest(ctx, "/openapi/v1/prodinst/getSIMList", simDXBasicInfoQueryValues(simKey))
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "QuerySimBasicInfo error %+v, simKey %s", err, simKey.ChanKey)
|
||
items = append(items, &model.SimBaseRes[model.SimBasicInfoItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
|
||
g.Log().Debugf(ctx, "QuerySimBasicInfo response: %s", string(respBody))
|
||
|
||
var dxResp DxResp
|
||
err = json.Unmarshal(respBody, &dxResp)
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "QuerySimBasicInfo unmarshal error %+v, body %s", err, string(respBody))
|
||
items = append(items, &model.SimBaseRes[model.SimBasicInfoItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
|
||
// resultCode = "0" 表示成功
|
||
if dxResp.ResultCode != "0" {
|
||
g.Log().Warningf(ctx, "QuerySimBasicInfo upstream error %s, %s", dxResp.ResultCode, dxResp.ResultMsg)
|
||
items = append(items, &model.SimBaseRes[model.SimBasicInfoItem]{
|
||
SimKey: simKey,
|
||
Code: gconv.Int(dxResp.ResultCode),
|
||
Message: dxResp.ResultMsg,
|
||
})
|
||
continue
|
||
}
|
||
|
||
// 从返回列表中查找匹配的SIM卡
|
||
var matchedIdx = -1
|
||
for i := range dxResp.Description.SimList {
|
||
sim := &dxResp.Description.SimList[i]
|
||
// 根据请求参数匹配对应的SIM卡
|
||
if (simKey.MSISDN != "" && sim.AccNumber == simKey.MSISDN) ||
|
||
(simKey.ICCID != "" && sim.Iccid == simKey.ICCID) {
|
||
matchedIdx = i
|
||
break
|
||
}
|
||
}
|
||
|
||
if matchedIdx == -1 {
|
||
items = append(items, &model.SimBaseRes[model.SimBasicInfoItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: "未查询到对应SIM卡信息",
|
||
})
|
||
continue
|
||
}
|
||
|
||
matchedSim := &dxResp.Description.SimList[matchedIdx]
|
||
|
||
items = append(items, &model.SimBaseRes[model.SimBasicInfoItem]{
|
||
SimKey: simKey,
|
||
Code: gconv.Int(dxResp.ResultCode),
|
||
Message: dxResp.ResultMsg,
|
||
ResData: model.SimBasicInfoItem{
|
||
ICCID: matchedSim.Iccid,
|
||
IMSI: matchedSim.Imsi,
|
||
MSISDN: matchedSim.AccNumber,
|
||
OpenDate: matchedSim.CreateTime,
|
||
ActiveDate: matchedSim.ActivationTime,
|
||
SimStatus: mapDXLegacyBasicStatus(ctx, firstNonEmpty(matchedSim.StatusCd, strings.Join(matchedSim.SimStatus, ","))),
|
||
StopTypeList: matchedSim.StopTypeList,
|
||
MainOfferName: matchedSim.MainOfferName,
|
||
CardBindingType: matchedSim.CardBindingType,
|
||
IsDirectVisit: matchedSim.IsDirectVisit,
|
||
IsHumanLimit: matchedSim.IsHumanLimit,
|
||
VpdnGreDoamin: matchedSim.VpdnGreDoamin,
|
||
VpdnL2tpDoamin: matchedSim.VpdnL2tpDoamin,
|
||
AreaLimitType: matchedSim.AreaLimitType,
|
||
AreaLimitInfo: matchedSim.AreaLimitInfo,
|
||
},
|
||
})
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
type dxBatchInfoResponse struct {
|
||
Code any `json:"code"`
|
||
Message string `json:"message"`
|
||
Msg string `json:"msg"`
|
||
Data struct {
|
||
QrySimInfoList []dxBatchInfoItem `json:"qrySimInfoList"`
|
||
SimInfoList []dxBatchInfoItem `json:"simInfoList"`
|
||
} `json:"data"`
|
||
}
|
||
|
||
type dxBatchInfoItem struct {
|
||
AccessNumber string `json:"accessNumber"`
|
||
AccNum string `json:"accNum"`
|
||
Result any `json:"result"`
|
||
ICCID string `json:"iccid"`
|
||
IMSI string `json:"imsi"`
|
||
ActiveTime string `json:"activeTime"`
|
||
ActivationTime string `json:"activationTime"`
|
||
CreateTime string `json:"createTime"`
|
||
SimStatus any `json:"simStatus"`
|
||
StopTypeList any `json:"stopTypeList"`
|
||
MainOfferName string `json:"mainOfferName"`
|
||
CardBindingType string `json:"cardBindingType"`
|
||
IsDirectVisit string `json:"isDirectVisit"`
|
||
IsHumanLimit string `json:"isHumanLimit"`
|
||
VpdnGreDoamin string `json:"vpdnGreDoamin"`
|
||
VpdnL2tpDoamin string `json:"vpdnL2tpDoamin"`
|
||
AreaLimitType string `json:"areaLimitType"`
|
||
AreaLimitInfo string `json:"areaLimitInfo"`
|
||
}
|
||
|
||
func (s *sSimDX) querySimBasicInfoBatch(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, bool, error) {
|
||
if input == nil || len(input.SimKeys) == 0 {
|
||
return make(model.SimBasicInfoOutput, 0), true, nil
|
||
}
|
||
if strings.TrimSpace(s.custNumber) == "" {
|
||
return nil, false, nil
|
||
}
|
||
for _, simKey := range input.SimKeys {
|
||
if simKey == nil || strings.TrimSpace(simKey.MSISDN) == "" {
|
||
return nil, false, nil
|
||
}
|
||
}
|
||
|
||
results := make(model.SimBasicInfoOutput, len(input.SimKeys))
|
||
pending := make([]telecomBatchKey, 0, len(input.SimKeys))
|
||
for i, simKey := range input.SimKeys {
|
||
pending = append(pending, telecomBatchKey{
|
||
index: i,
|
||
simKey: simKey,
|
||
accessNumber: strings.TrimSpace(simKey.MSISDN),
|
||
})
|
||
}
|
||
|
||
for _, chunk := range chunkTelecomBatchKeys(pending, dxBatchStatusLimit) {
|
||
accessNumbers := make([]string, 0, len(chunk))
|
||
for _, item := range chunk {
|
||
accessNumbers = append(accessNumbers, item.accessNumber)
|
||
}
|
||
|
||
resp, err := s.doDXBatchInfoPost(ctx, "/api/v1/prod/batchQrySimInfo", dxBatchRequest{
|
||
AccessNumbers: accessNumbers,
|
||
CustNumber: s.custNumber,
|
||
GroupID: "",
|
||
SimStatus: "",
|
||
})
|
||
if err != nil {
|
||
for _, item := range chunk {
|
||
results[item.index] = &model.SimBaseRes[model.SimBasicInfoItem]{
|
||
SimKey: item.simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
|
||
batchItems := resp.Data.QrySimInfoList
|
||
if len(batchItems) == 0 {
|
||
batchItems = resp.Data.SimInfoList
|
||
}
|
||
itemMap := make(map[string]dxBatchInfoItem, len(batchItems)*3)
|
||
for _, item := range batchItems {
|
||
putDXBatchInfoItem(itemMap, "msisdn", telecomBatchAccessNumber(item.AccessNumber, item.AccNum), item)
|
||
putDXBatchInfoItem(itemMap, "iccid", item.ICCID, item)
|
||
putDXBatchInfoItem(itemMap, "imsi", item.IMSI, item)
|
||
}
|
||
|
||
for _, item := range chunk {
|
||
batchItem, ok := getDXBatchInfoItem(itemMap, item.simKey)
|
||
if !ok {
|
||
results[item.index] = &model.SimBaseRes[model.SimBasicInfoItem]{
|
||
SimKey: item.simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: "batch response missing sim info",
|
||
}
|
||
continue
|
||
}
|
||
|
||
itemResult := strings.TrimSpace(gconv.String(batchItem.Result))
|
||
if itemResult != "" && itemResult != "1" {
|
||
results[item.index] = &model.SimBaseRes[model.SimBasicInfoItem]{
|
||
SimKey: item.simKey,
|
||
Code: telecomBatchResultCode(itemResult),
|
||
Message: firstNonEmpty(resp.Message, resp.Msg, "telecom batch basic info query failed"),
|
||
}
|
||
continue
|
||
}
|
||
|
||
results[item.index] = &model.SimBaseRes[model.SimBasicInfoItem]{
|
||
SimKey: item.simKey,
|
||
Code: 0,
|
||
Message: "",
|
||
ResData: mapDXBatchInfoToBasicInfo(batchItem),
|
||
}
|
||
}
|
||
}
|
||
|
||
return compactBasicInfoResults(results), true, nil
|
||
}
|
||
|
||
func (s *sSimDX) doDXBatchInfoPost(ctx context.Context, endpoint string, req dxBatchRequest) (*dxBatchInfoResponse, error) {
|
||
respBody, err := s.doDXBatchPost(ctx, endpoint, req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var resp dxBatchInfoResponse
|
||
if err := json.Unmarshal(respBody, &resp); err != nil {
|
||
return nil, err
|
||
}
|
||
if gconv.String(resp.Code) != "0" {
|
||
return nil, fmt.Errorf("telecom batch basic info request failed: code=%s message=%s", gconv.String(resp.Code), firstNonEmpty(resp.Message, resp.Msg))
|
||
}
|
||
return &resp, nil
|
||
}
|
||
|
||
func mapDXBatchInfoToBasicInfo(item dxBatchInfoItem) model.SimBasicInfoItem {
|
||
status := strings.TrimSpace(gconv.String(item.SimStatus))
|
||
statusValue := ""
|
||
if status != "" {
|
||
statusValue = strconv.Itoa(convertDXBatchCardStatus(context.Background(), status))
|
||
}
|
||
return model.SimBasicInfoItem{
|
||
ICCID: strings.TrimSpace(item.ICCID),
|
||
IMSI: strings.TrimSpace(item.IMSI),
|
||
MSISDN: telecomBatchAccessNumber(item.AccessNumber, item.AccNum),
|
||
ActiveDate: firstNonEmpty(item.ActiveTime, item.ActivationTime),
|
||
OpenDate: strings.TrimSpace(item.CreateTime),
|
||
SimStatus: statusValue,
|
||
StopTypeList: normalizeDXStringList(item.StopTypeList),
|
||
MainOfferName: strings.TrimSpace(item.MainOfferName),
|
||
CardBindingType: strings.TrimSpace(item.CardBindingType),
|
||
IsDirectVisit: strings.TrimSpace(item.IsDirectVisit),
|
||
IsHumanLimit: strings.TrimSpace(item.IsHumanLimit),
|
||
VpdnGreDoamin: strings.TrimSpace(item.VpdnGreDoamin),
|
||
VpdnL2tpDoamin: strings.TrimSpace(item.VpdnL2tpDoamin),
|
||
AreaLimitType: strings.TrimSpace(item.AreaLimitType),
|
||
AreaLimitInfo: strings.TrimSpace(item.AreaLimitInfo),
|
||
}
|
||
}
|
||
|
||
func mapDXLegacyBasicStatus(ctx context.Context, value string) string {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return ""
|
||
}
|
||
return strconv.Itoa(convertDxCardStatus(ctx, value))
|
||
}
|
||
|
||
func normalizeDXStringList(value any) []string {
|
||
switch v := value.(type) {
|
||
case nil:
|
||
return nil
|
||
case []string:
|
||
return trimNonEmptyStrings(v)
|
||
case []any:
|
||
values := make([]string, 0, len(v))
|
||
for _, item := range v {
|
||
values = append(values, gconv.String(item))
|
||
}
|
||
return trimNonEmptyStrings(values)
|
||
case string:
|
||
return trimNonEmptyStrings(strings.FieldsFunc(v, func(r rune) bool {
|
||
return r == ',' || r == ';' || r == '|' || r == '_' || r == '/'
|
||
}))
|
||
default:
|
||
return trimNonEmptyStrings([]string{gconv.String(value)})
|
||
}
|
||
}
|
||
|
||
func trimNonEmptyStrings(values []string) []string {
|
||
output := make([]string, 0, len(values))
|
||
for _, value := range values {
|
||
if value = strings.TrimSpace(value); value != "" {
|
||
output = append(output, value)
|
||
}
|
||
}
|
||
return output
|
||
}
|
||
|
||
func putDXBatchInfoItem(items map[string]dxBatchInfoItem, prefix string, value string, item dxBatchInfoItem) {
|
||
if value = strings.TrimSpace(value); value != "" {
|
||
items[prefix+":"+strings.ToUpper(value)] = item
|
||
}
|
||
}
|
||
|
||
func getDXBatchInfoItem(items map[string]dxBatchInfoItem, simKey *model.SimKey) (dxBatchInfoItem, bool) {
|
||
if simKey == nil {
|
||
return dxBatchInfoItem{}, false
|
||
}
|
||
for _, candidate := range []struct {
|
||
prefix string
|
||
value string
|
||
}{
|
||
{prefix: "msisdn", value: simKey.MSISDN},
|
||
{prefix: "iccid", value: simKey.ICCID},
|
||
{prefix: "imsi", value: simKey.IMSI},
|
||
} {
|
||
if value := strings.TrimSpace(candidate.value); value != "" {
|
||
if item, ok := items[candidate.prefix+":"+strings.ToUpper(value)]; ok {
|
||
return item, true
|
||
}
|
||
}
|
||
}
|
||
return dxBatchInfoItem{}, false
|
||
}
|
||
|
||
func compactBasicInfoResults(items model.SimBasicInfoOutput) model.SimBasicInfoOutput {
|
||
output := make(model.SimBasicInfoOutput, 0, len(items))
|
||
for _, item := range items {
|
||
if item != nil {
|
||
output = append(output, item)
|
||
}
|
||
}
|
||
return output
|
||
}
|
||
|
||
// QuerySimApnInfo 物联网卡已开通APN信息查询
|
||
// 电信当前复用在线会话查询结果,从 ResData.ApnId 中取 APN/DNN 名称
|
||
func (s *sSimDX) QuerySimStopReason(ctx context.Context, input *model.SimStopReasonInput) (model.SimStopReasonOutput, error) {
|
||
basicInfo, err := s.QuerySimBasicInfo(ctx, &model.SimBasicInfoInput{
|
||
SimKeys: input.SimKeys,
|
||
ReqData: model.SimInputReqNullDate{},
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
items := make(model.SimStopReasonOutput, 0, len(basicInfo))
|
||
for _, item := range basicInfo {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
stopReason := ""
|
||
if item.Code == 0 {
|
||
stopReason = mapDxStopTypeListText(item.ResData.StopTypeList)
|
||
}
|
||
items = append(items, &model.SimBaseRes[model.SimStopReasonItem]{
|
||
SimKey: item.SimKey,
|
||
Code: item.Code,
|
||
Message: item.Message,
|
||
ResData: model.SimStopReasonItem{
|
||
StopReason: stopReason,
|
||
},
|
||
})
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
func (s *sSimDX) QuerySimFrozenState(ctx context.Context, input *model.SimFrozenStateInput) (model.SimFrozenStateOutput, error) {
|
||
items := make(model.SimFrozenStateOutput, 0, len(input.SimKeys))
|
||
for _, simKey := range input.SimKeys {
|
||
items = append(items, &model.SimBaseRes[model.SimFrozenStateItem]{
|
||
SimKey: simKey,
|
||
Code: 0,
|
||
Message: "",
|
||
ResData: model.SimFrozenStateItem{
|
||
FrozenStatus: "不支持查询",
|
||
FrozenReason: "电信当前无独立冻结查询接口",
|
||
OperTime: "",
|
||
},
|
||
})
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
// ChangeSimStatus 物联网卡状态变更
|
||
// CTIOT_5GCMP_LC002 - 停机保号相关
|
||
// - URL: /5gcmp/openapi/v1/common/disabledNumberService
|
||
// - orderTypeId: 19=停机保号, 20=停机保号复机
|
||
//
|
||
// 平台 operType 与电信 orderTypeId 映射:
|
||
// - 0(申请停机,已激活转已停机) -> 19
|
||
// - 1(申请复机,已停机转已激活) -> 20
|
||
func (s *sSimDX) ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) (model.SimCardStatusChangeOutput, error) {
|
||
// 响应结构 (XML)
|
||
type DxResp struct {
|
||
XMLName xml.Name `xml:"businessServiceResponse"`
|
||
RspType string `xml:"RspType"` // 请求状态
|
||
Result string `xml:"result"` // 状态响应码: 0=成功, -2=业务校验异常, 100008=系统内部错误
|
||
ResultMsg string `xml:"resultMsg"` // 返回消息
|
||
GroupTransactionID string `xml:"GROUP_TRANSACTIONID"` // 流水号
|
||
}
|
||
|
||
orderTypeID, ok := convertDxCardStatusChangeOperType(ctx, input.ReqData.OperType)
|
||
if !ok {
|
||
return newInvalidCardStatusChangeOutput(input.SimKeys), nil
|
||
}
|
||
|
||
items := make(model.SimCardStatusChangeOutput, 0, len(input.SimKeys))
|
||
|
||
for _, simKey := range input.SimKeys {
|
||
params := url.Values{
|
||
"access_number": []string{simKey.MSISDN},
|
||
"orderTypeId": []string{orderTypeID},
|
||
}
|
||
|
||
respBody, err := s.client.DoGetRequest(ctx, "/5gcmp/openapi/v1/common/disabledNumberService", params)
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "ChangeSimStatus error %+v, simKey %s, params %+v", err, simKey.ChanKey, params)
|
||
items = append(items, &model.SimBaseRes[model.SimCardStatusChangeItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
ResData: model.SimCardStatusChangeItem{
|
||
OrderNum: "",
|
||
},
|
||
})
|
||
continue
|
||
}
|
||
|
||
g.Log().Debugf(ctx, "ChangeSimStatus response: %s", string(respBody))
|
||
|
||
// 去除XML命名空间前缀
|
||
xmlBody := xmlNamespaceRegex.ReplaceAllString(string(respBody), "")
|
||
|
||
var dxResp DxResp
|
||
err = xml.Unmarshal([]byte(xmlBody), &dxResp)
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "ChangeSimStatus unmarshal error %+v, body %s", err, xmlBody)
|
||
items = append(items, &model.SimBaseRes[model.SimCardStatusChangeItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
ResData: model.SimCardStatusChangeItem{
|
||
OrderNum: "",
|
||
},
|
||
})
|
||
continue
|
||
}
|
||
|
||
// result = "0" 表示成功
|
||
code := gconv.Int(dxResp.Result)
|
||
if dxResp.Result != "0" {
|
||
g.Log().Warningf(ctx, "ChangeSimStatus upstream error %s, %s, simKey %s", dxResp.Result, dxResp.ResultMsg, simKey.ChanKey)
|
||
}
|
||
|
||
items = append(items, &model.SimBaseRes[model.SimCardStatusChangeItem]{
|
||
SimKey: simKey,
|
||
Code: code,
|
||
Message: dxResp.ResultMsg,
|
||
ResData: model.SimCardStatusChangeItem{
|
||
OrderNum: dxResp.GroupTransactionID,
|
||
},
|
||
})
|
||
}
|
||
|
||
return items, nil
|
||
}
|
||
|
||
// QuerySimCardBindStatus 物联网卡机卡绑定状态查询
|
||
func (s *sSimDX) QuerySimCardBindStatus(ctx context.Context, input *model.SimCardBindStatusInput) (model.SimCardBindStatusOutput, error) {
|
||
type bindData struct {
|
||
OrderStatus string `json:"orderStatus"`
|
||
OrderStaus string `json:"orderStaus"`
|
||
BindType string `json:"bindType"`
|
||
BindImei string `json:"bindImei"`
|
||
LastImei string `json:"lastImei"`
|
||
BindStatus string `json:"bindStatus"`
|
||
BindStaus string `json:"bindStaus"`
|
||
}
|
||
type bindResp struct {
|
||
Code any `json:"code"`
|
||
Msg string `json:"msg"`
|
||
Data bindData `json:"data"`
|
||
}
|
||
items := make(model.SimCardBindStatusOutput, 0, len(input.SimKeys))
|
||
for _, simKey := range input.SimKeys {
|
||
name, value := "iccid", strings.TrimSpace(simKey.ICCID)
|
||
if msisdn := strings.TrimSpace(simKey.MSISDN); msisdn != "" {
|
||
name, value = "access_number", msisdn
|
||
}
|
||
item := &model.SimBaseRes[model.SimCardBindStatusItem]{SimKey: simKey}
|
||
if value == "" {
|
||
item.Code, item.Message = consts.ApiStatusError, "missing access number or iccid"
|
||
items = append(items, item)
|
||
continue
|
||
}
|
||
body, err := s.client.DoGetRequest(ctx, "/api/v1/corp/connect/chkDeviceBindingInfo", url.Values{name: {value}})
|
||
if err != nil {
|
||
item.Code, item.Message = consts.ApiStatusError, err.Error()
|
||
items = append(items, item)
|
||
continue
|
||
}
|
||
var result bindResp
|
||
if err := json.Unmarshal(body, &result); err != nil {
|
||
item.Code, item.Message = consts.ApiStatusError, err.Error()
|
||
items = append(items, item)
|
||
continue
|
||
}
|
||
code := strings.TrimSpace(gconv.String(result.Code))
|
||
item.Code, item.Message = gconv.Int(code), result.Msg
|
||
if code == "" || (code != "0" && item.Code == 0) {
|
||
item.Code = consts.ApiStatusError
|
||
if item.Message == "" {
|
||
item.Message = "invalid card binding response code"
|
||
}
|
||
}
|
||
if item.Code == 0 {
|
||
item.ResData = model.SimCardBindStatusItem{
|
||
OrderStatus: firstNonEmpty(result.Data.OrderStatus, result.Data.OrderStaus),
|
||
CardBindingType: result.Data.BindType,
|
||
BindStatus: firstNonEmpty(result.Data.BindStaus, result.Data.BindStatus),
|
||
BindImei: result.Data.BindImei,
|
||
LastImei: result.Data.LastImei,
|
||
}
|
||
}
|
||
items = append(items, item)
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
// QuerySimDeviceStatus 查询运营商终端开关机状态。
|
||
// 电信官方 NQ007 接口只提供电源状态,不提供统一设备健康状态。
|
||
func (s *sSimDX) QuerySimDeviceStatus(ctx context.Context, input *model.SimDeviceStatusInput) (model.SimDeviceStatusOutput, error) {
|
||
type powerStatusResp struct {
|
||
Code any `json:"code"`
|
||
Message string `json:"message"`
|
||
Status struct {
|
||
Status string `json:"Status"`
|
||
LastedPowerOnTime string `json:"Lastedpowerontime"`
|
||
LastedPowerOffTime string `json:"Lastedpowerofftime"`
|
||
} `json:"status"`
|
||
}
|
||
|
||
items := make(model.SimDeviceStatusOutput, 0, len(input.SimKeys))
|
||
for _, simKey := range input.SimKeys {
|
||
accessNumber := firstNonEmpty(simKey.MSISDN, simKey.ICCID)
|
||
if accessNumber == "" {
|
||
items = append(items, &model.SimBaseRes[model.SimDeviceStatusItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: "missing access number or iccid",
|
||
})
|
||
continue
|
||
}
|
||
|
||
paramName := "access_number"
|
||
if strings.TrimSpace(simKey.MSISDN) == "" {
|
||
paramName = "iccid"
|
||
}
|
||
requestBody, err := json.Marshal(map[string]string{paramName: accessNumber})
|
||
if err != nil {
|
||
items = append(items, &model.SimBaseRes[model.SimDeviceStatusItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
|
||
respBody, err := s.client.DoPostJSONRequest(ctx, "/api/v1/prod/getDevicePowerStatus", requestBody)
|
||
if err != nil {
|
||
g.Log().Warningf(ctx, "QuerySimDeviceStatus error %+v, simKey %+v", err, simKey)
|
||
items = append(items, &model.SimBaseRes[model.SimDeviceStatusItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
|
||
var resp powerStatusResp
|
||
if err = json.Unmarshal(respBody, &resp); err != nil {
|
||
g.Log().Warningf(ctx, "QuerySimDeviceStatus unmarshal error %+v, body %s", err, string(respBody))
|
||
items = append(items, &model.SimBaseRes[model.SimDeviceStatusItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
|
||
code := gconv.Int(resp.Code)
|
||
if code != 0 {
|
||
items = append(items, &model.SimBaseRes[model.SimDeviceStatusItem]{
|
||
SimKey: simKey,
|
||
Code: code,
|
||
Message: resp.Message,
|
||
})
|
||
continue
|
||
}
|
||
|
||
operatingStatus := mapDxOperatingStatus(resp.Status.Status)
|
||
items = append(items, &model.SimBaseRes[model.SimDeviceStatusItem]{
|
||
SimKey: simKey,
|
||
Code: 0,
|
||
Message: resp.Message,
|
||
ResData: model.SimDeviceStatusItem{
|
||
DeviceStatus: consts.CardSessionStatusUnknown,
|
||
OperatingStatus: operatingStatus,
|
||
},
|
||
})
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
// QuerySimRegionLimitStatus 物联网卡地域限制状态查询
|
||
func (s *sSimDX) QuerySimRegionLimitStatus(ctx context.Context, input *model.SimRegionLimitStatusInput) (model.SimRegionLimitStatusOutput, error) {
|
||
type dxRegionLimitResp struct {
|
||
Code any `json:"code"`
|
||
ResultCode any `json:"resultCode"`
|
||
Msg string `json:"msg"`
|
||
ResultMsg string `json:"resultMsg"`
|
||
Message string `json:"message"`
|
||
Data json.RawMessage `json:"data"`
|
||
}
|
||
items := make(model.SimRegionLimitStatusOutput, 0, len(input.SimKeys))
|
||
for _, simKey := range input.SimKeys {
|
||
accessNumber := firstNonEmpty(simKey.MSISDN, simKey.ICCID, simKey.IMSI)
|
||
if accessNumber == "" {
|
||
items = append(items, &model.SimBaseRes[model.SimRegionLimitStatusItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: "missing access number",
|
||
})
|
||
continue
|
||
}
|
||
|
||
var (
|
||
regionItem *dxRegionLimitItem
|
||
respCode = consts.ApiStatusError
|
||
respMsg string
|
||
lastErr error
|
||
)
|
||
|
||
for _, endpoint := range []string{"/openapi/v1/areaLimit/qryAreaLimitInfo"} {
|
||
respBody, err := s.client.DoGetRequest(ctx, endpoint, url.Values{
|
||
"accNum": []string{accessNumber},
|
||
})
|
||
if err != nil {
|
||
lastErr = err
|
||
continue
|
||
}
|
||
g.Log().Debugf(ctx, "QuerySimRegionLimitStatus response endpoint=%s body=%s", endpoint, string(respBody))
|
||
|
||
var dxResp dxRegionLimitResp
|
||
if err := json.Unmarshal(respBody, &dxResp); err != nil {
|
||
lastErr = err
|
||
continue
|
||
}
|
||
|
||
codeStr := firstNonEmpty(gconv.String(dxResp.Code), gconv.String(dxResp.ResultCode))
|
||
if codeStr == "" {
|
||
codeStr = "0"
|
||
}
|
||
respCode = gconv.Int(codeStr)
|
||
respMsg = firstNonEmpty(dxResp.Msg, dxResp.ResultMsg, dxResp.Message)
|
||
if respCode != 0 {
|
||
continue
|
||
}
|
||
|
||
item, err := decodeDxRegionLimitData(dxResp.Data)
|
||
if err != nil {
|
||
lastErr = err
|
||
continue
|
||
}
|
||
if item == nil {
|
||
regionItem = &dxRegionLimitItem{}
|
||
lastErr = nil
|
||
break
|
||
}
|
||
regionItem = item
|
||
lastErr = nil
|
||
break
|
||
}
|
||
|
||
if regionItem == nil {
|
||
basicInfo, basicErr := s.QuerySimBasicInfo(ctx, &model.SimBasicInfoInput{
|
||
SimKeys: []*model.SimKey{simKey},
|
||
ReqData: model.SimInputReqNullDate{},
|
||
})
|
||
if basicErr != nil {
|
||
message := respMsg
|
||
if lastErr != nil {
|
||
message = lastErr.Error()
|
||
}
|
||
if message == "" {
|
||
message = "telecom region limit query failed"
|
||
}
|
||
items = append(items, &model.SimBaseRes[model.SimRegionLimitStatusItem]{
|
||
SimKey: simKey,
|
||
Code: respCode,
|
||
Message: message,
|
||
})
|
||
continue
|
||
}
|
||
if len(basicInfo) == 0 || basicInfo[0] == nil {
|
||
items = append(items, &model.SimBaseRes[model.SimRegionLimitStatusItem]{
|
||
SimKey: simKey,
|
||
Code: consts.ApiStatusError,
|
||
Message: "telecom region limit query returned empty data",
|
||
})
|
||
continue
|
||
}
|
||
items = append(items, &model.SimBaseRes[model.SimRegionLimitStatusItem]{
|
||
SimKey: simKey,
|
||
Code: 0,
|
||
Message: "",
|
||
ResData: model.SimRegionLimitStatusItem{
|
||
RegionLimitStatus: "2",
|
||
OrderStatus: "",
|
||
LimitStatus: "",
|
||
LimitType: basicInfo[0].ResData.AreaLimitType,
|
||
LimitArea: basicInfo[0].ResData.AreaLimitInfo,
|
||
},
|
||
})
|
||
continue
|
||
}
|
||
|
||
orderStatus := gconv.String(regionItem.OrderStatus)
|
||
limitStatus, limitType, limitArea := normalizeDxRegionLimitDetails(
|
||
orderStatus,
|
||
regionItem.LimitStatus,
|
||
gconv.String(regionItem.LimitType),
|
||
regionItem.LimitArea,
|
||
)
|
||
items = append(items, &model.SimBaseRes[model.SimRegionLimitStatusItem]{
|
||
SimKey: simKey,
|
||
Code: 0,
|
||
Message: "",
|
||
ResData: model.SimRegionLimitStatusItem{
|
||
RegionLimitStatus: mapDxRegionLimitStatusByOrder(orderStatus, limitStatus),
|
||
OrderStatus: orderStatus,
|
||
LimitStatus: limitStatus,
|
||
LimitType: limitType,
|
||
LimitArea: limitArea,
|
||
},
|
||
})
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
// convertDxSessionStatus 转换电信会话状态码
|
||
// 电信状态: 0=在线, 1=不在线, 2=未查询到会话信息
|
||
// 系统状态: 0=在线, -1=不在线, -2=未查询到会话信息
|
||
// func convertDxSessionStatus(ctx context.Context, upstreamStatus string) string {
|
||
// switch upstreamStatus {
|
||
// case "0":
|
||
// return "0" // 在线
|
||
// case "1":
|
||
// return "1" // 不在线
|
||
// case "2":
|
||
// return "2" // 未查询到会话信息
|
||
// default:
|
||
// g.Log().Warningf(ctx, "Unknown session status %s from upstream", upstreamStatus)
|
||
// return "2"
|
||
// }
|
||
// }
|
||
|
||
// BQ018 uses 3 for test deactivation, unlike the single-card product status.
|
||
func convertDXBatchCardStatus(ctx context.Context, upstreamStatus string) int {
|
||
switch strings.TrimSpace(upstreamStatus) {
|
||
case "1":
|
||
return 1
|
||
case "2", "3":
|
||
return 6
|
||
case "4":
|
||
return 2
|
||
case "5":
|
||
return 4
|
||
case "6":
|
||
return 0
|
||
default:
|
||
g.Log().Warningf(ctx, "Unknown DX batch card status %q", upstreamStatus)
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// convertDxCardStatus 转换电信卡状态码
|
||
func convertDxCardStatus(ctx context.Context, upstreamStatus string) int {
|
||
if upstreamStatus == "" {
|
||
return -2
|
||
}
|
||
status := gconv.Int(upstreamStatus)
|
||
switch status {
|
||
case 1:
|
||
return 1 // 待激活
|
||
case 4:
|
||
return 2 // 已激活
|
||
case 5:
|
||
return 4 // 停机
|
||
case 2:
|
||
return 6 // 可测试
|
||
case 3:
|
||
return 7 // 库存
|
||
case 7:
|
||
return 9 // 已销户
|
||
case 6:
|
||
return 0 // 未知
|
||
default:
|
||
g.Log().Warningf(ctx, "Unknown card status %d from upstream", status)
|
||
return 0
|
||
}
|
||
}
|