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格式): // // // 0 // 14.01MB // ... // // 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 } }