chore: 并入 iotlcm-upstream-chan 历史到 upstream-chan/(原提交 1e74497)
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user