package upstreamimpl import ( "context" "encoding/json" "encoding/xml" "net/url" "strconv" "strings" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" ) type dxAPNResponse[T any] struct { Code json.RawMessage `json:"code"` Message string `json:"message"` Result *T `json:"result"` } type dxAPNInfo struct { ApnInfoList []struct { ApnName string `json:"apnName"` Status *string `json:"status"` ApnStatus *string `json:"apnStatus"` } `json:"apnInfoList"` } func dxAPNParams(simKey *model.SimKey) url.Values { params := url.Values{} if simKey == nil { return params } if value := strings.TrimSpace(simKey.MSISDN); value != "" { params.Set("msisdn", value) } else if value := strings.TrimSpace(simKey.ICCID); value != "" { params.Set("iccid", value) } else if value := strings.TrimSpace(simKey.IMSI); value != "" { params.Set("imsi", value) } return params } func decodeDXAPNResponse[T any](body []byte) (*dxAPNResponse[T], int, string, error) { var response dxAPNResponse[T] if err := json.Unmarshal(body, &response); err != nil { return nil, consts.ApiStatusError, "", gerror.Wrap(err, "电信 APN 响应格式错误") } rawCode := strings.TrimSpace(string(response.Code)) if strings.HasPrefix(rawCode, "\"") { if err := json.Unmarshal(response.Code, &rawCode); err != nil { return nil, consts.ApiStatusError, "", gerror.Wrap(err, "电信 APN 返回码格式错误") } } code, err := strconv.Atoi(rawCode) if err != nil || (code == 0 && rawCode != "0") { return &response, consts.ApiStatusError, rawCode, gerror.Newf("电信 APN 返回码无效:%s,%s", rawCode, response.Message) } if code != 0 { return &response, code, rawCode, nil } if response.Result == nil { return &response, consts.ApiStatusError, "", gerror.New("电信 APN 成功响应缺少 result") } return &response, code, "", nil } func (s *sSimDX) QuerySimApnInfo(ctx context.Context, input *model.SimApnInfoInput) (model.SimApnInfoOutput, error) { items := make(model.SimApnInfoOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { item := &model.SimBaseRes[model.SimApnInfoItemList]{SimKey: simKey, ResData: model.SimApnInfoItemList{ApnList: []model.SimApnInfoItem{}}} items = append(items, item) params := dxAPNParams(simKey) if len(params) == 0 { item.Code, item.Message = consts.ApiStatusError, "电信 APN 查询缺少卡标识" continue } body, err := s.client.DoGetRequest(ctx, "/api/v3/getApnInfo", params) if err != nil { item.Code, item.Message = consts.ApiStatusError, err.Error() continue } response, code, rawCode, err := decodeDXAPNResponse[dxAPNInfo](body) item.Code, item.ErrCode = code, rawCode if err != nil { item.Message = err.Error() continue } item.Message = response.Message if code != 0 { continue } if response.Result.ApnInfoList == nil { item.Code, item.Message = consts.ApiStatusError, "电信 APN 成功响应缺少 apnInfoList" continue } for _, apn := range response.Result.ApnInfoList { status := "" if apn.Status != nil { status = strings.TrimSpace(*apn.Status) } else if apn.ApnStatus != nil { status = strings.TrimSpace(*apn.ApnStatus) } if apn.Status != nil && apn.ApnStatus != nil && strings.TrimSpace(*apn.Status) != strings.TrimSpace(*apn.ApnStatus) { status = "" } if status != consts.CardApnStatusNormal && status != consts.CardApnStatusPaused { status = "" } item.ResData.ApnList = append(item.ResData.ApnList, model.SimApnInfoItem{ApnName: strings.TrimSpace(apn.ApnName), Status: status}) } } return items, nil } // dxCutNetSuccessCode 是 singleCutNet 响应中表示成功接收的 RspCode。 const dxCutNetSuccessCode = "0000" // ChangeSimApnFunction 添加或取消单独断网 // CTIOT_5GCMP_BA004 - 添加或取消单独断网 // - URL: /5gcmp/openapi/v1/common/singleCutNet // - 参数: access_number + action (ADD/DEL) // - 响应: XML,RspCode=0000 表示成功接收,GROUP_TRANSACTIONID 为流水号 // // 这是整卡数据开停语义(不是按 APN 定向): // - 平台 0(开通数据) -> action=DEL(取消单独断网,恢复联网) // - 平台 1(停用数据) -> action=ADD(单独断网) // // 电信侧没有「按 APN 定向」这一维度,因此忽略调用方携带的 ApnName // (预警自动断网、批量数据开停都会固定带 apnName="CMIOT"),不能因此拒绝请求。 func (s *sSimDX) ChangeSimApnFunction(ctx context.Context, input *model.SimCardOperateChangeApnInput) (model.SimCardOperateChangeApnOutput, error) { // 响应结构 (XML) type dxCutNetResponse struct { XMLName xml.Name `xml:"SvcCont"` GroupTransactionID string `xml:"GROUP_TRANSACTIONID"` Response struct { RspType string `xml:"RspType"` RspCode string `xml:"RspCode"` RspDesc string `xml:"RspDesc"` } `xml:"Response"` } action, ok := convertDxApnOperType(ctx, input.ReqData.OperType) items := make(model.SimCardOperateChangeApnOutput, 0, len(input.SimKeys)) if !ok { for _, simKey := range input.SimKeys { items = append(items, &model.SimBaseRes[model.SimCardOperateChangeApnItem]{ SimKey: simKey, Code: consts.ApiStatusError, Message: "APN 操作类型必须为 0(开通)或 1(停止)", ResData: model.SimCardOperateChangeApnItem{}, }) } return items, nil } for _, simKey := range input.SimKeys { item := &model.SimBaseRes[model.SimCardOperateChangeApnItem]{SimKey: simKey} items = append(items, item) // singleCutNet 只按物联网接入号定位,缺接入号无法定位卡。 accessNumber := strings.TrimSpace(simKey.MSISDN) if accessNumber == "" { item.Code, item.Message = consts.ApiStatusError, "电信单独断网缺少物联网接入号(MSISDN)" continue } params := url.Values{ "access_number": []string{accessNumber}, "action": []string{action}, } respBody, err := s.client.DoGetRequest(ctx, "/5gcmp/openapi/v1/common/singleCutNet", params) if err != nil { g.Log().Errorf(ctx, "ChangeSimApnFunction error %+v, simKey %s, params %+v", err, simKey.ChanKey, params) item.Code, item.Message = consts.ApiStatusError, err.Error() continue } g.Log().Debugf(ctx, "ChangeSimApnFunction response: %s", string(respBody)) // 去除XML命名空间前缀 xmlBody := xmlNamespaceRegex.ReplaceAllString(string(respBody), "") var dxResp dxCutNetResponse if err = xml.Unmarshal([]byte(xmlBody), &dxResp); err != nil { g.Log().Errorf(ctx, "ChangeSimApnFunction unmarshal error %+v, body %s", err, xmlBody) item.Code, item.Message = consts.ApiStatusError, err.Error() continue } item.Message = strings.TrimSpace(dxResp.Response.RspDesc) rawCode := strings.TrimSpace(dxResp.Response.RspCode) item.ErrCode = rawCode // 缺返回码、非 0000 均不视为成功,不自动重试以免重复操作。 if rawCode != dxCutNetSuccessCode { g.Log().Warningf(ctx, "ChangeSimApnFunction upstream error %s, %s, simKey %s", rawCode, item.Message, simKey.ChanKey) item.Code = consts.ApiStatusError if item.Message == "" { item.Message = "电信单独断网未返回结果码,办理结果未确认,请核对上游" } continue } item.Code = consts.ApiStatusSuccess item.ResData.OrderNum = strings.TrimSpace(dxResp.GroupTransactionID) if item.ResData.OrderNum == "" { item.Code, item.Message = consts.ApiStatusError, "电信单独断网未返回订单号,办理结果未确认,请核对上游" } } return items, nil }