248 lines
9.0 KiB
Go
248 lines
9.0 KiB
Go
package upstreamimpl
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"encoding/xml"
|
||
"fmt"
|
||
"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) {
|
||
checkpoint, durable := ctx.Value(model.ApnOperationCheckpointKey{}).(func(model.SimCardOperateChangeApnOutput) error)
|
||
if !durable {
|
||
return s.changeSimApnFunctionCards(ctx, input)
|
||
}
|
||
items := make(model.SimCardOperateChangeApnOutput, 0, len(input.SimKeys))
|
||
for _, key := range input.SimKeys {
|
||
one := *input
|
||
one.SimKeys = []*model.SimKey{key}
|
||
result, err := s.changeSimApnFunctionCards(ctx, &one)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, result...)
|
||
if err := checkpoint(items); err != nil {
|
||
return nil, fmt.Errorf("电信 v1 第%d张卡办理结果持久化失败,停止后续写入:%w", len(items), err)
|
||
}
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
func (s *sSimDX) changeSimApnFunctionCards(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)
|
||
_, durable := ctx.Value(model.ApnOperationCheckpointKey{}).(func(model.SimCardOperateChangeApnOutput) error)
|
||
|
||
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},
|
||
}
|
||
// singleCutNet 虽使用 GET,但为非幂等写请求,禁止运输层重放和重定向。
|
||
respBody, err := s.client.DoGetWriteRequest(ctx, "/5gcmp/openapi/v1/common/singleCutNet", params)
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "电信单独断网请求异常,办理结果未确认,chanKey=%s,错误类型=%T", simKey.ChanKey, err)
|
||
item.Code, item.Message = consts.ApiStatusError, err.Error()
|
||
item.ResData.Uncertain = true
|
||
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()
|
||
item.ResData.Uncertain = true
|
||
continue
|
||
}
|
||
item.Message = strings.TrimSpace(dxResp.Response.RspDesc)
|
||
rawCode := strings.TrimSpace(dxResp.Response.RspCode)
|
||
item.ErrCode = rawCode
|
||
// 缺返回码属于未确认;明确拒绝无需按未知结果重试。
|
||
if rawCode != dxCutNetSuccessCode {
|
||
g.Log().Warningf(ctx, "ChangeSimApnFunction upstream error %s, %s, simKey %s", rawCode, item.Message, simKey.ChanKey)
|
||
item.Code = consts.ApiStatusError
|
||
if rawCode == "" {
|
||
item.ResData.Uncertain = true
|
||
item.Message = "电信单独断网未返回结果码,办理结果未确认,请核对上游"
|
||
} else 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, "电信单独断网未返回订单号,办理结果未确认,请核对上游"
|
||
item.ResData.Uncertain = true
|
||
} else if input.ReqData.SubmitOnly || durable {
|
||
item.ResData.Uncertain = true
|
||
item.Message = "电信单独断网请求已受理,最终办理结果未确认,请核对上游"
|
||
}
|
||
}
|
||
|
||
return items, nil
|
||
}
|