Files

1795 lines
58 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package upstreamimpl
import (
"context"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"math"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
"time"
"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"
"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
}
}