96b1edff45
iotlcm-upstream-chan:运营商上游通道适配服务(统一 /sim/* 协议)。 - 内联原私有依赖:yunze-public → internal/pub/(10 包 18 文件), yunze-pb/pb → internal/pb/(9 文件);Consul 注册改用官方 gogf/gf/contrib/registry/consul/v2(API 兼容) - Go module:yz-svr → git.5iot.com/iotlcm-public/iotlcm-upstream-chan - 修复 mingqian 包测试:实现重构后 `adapter.operations` 字段已移除, 测试断言按当前实现语义重写(不改产品行为) - 凭据与内部标识脱敏:MySQL/Redis/Consul 凭据、私网与生产 IP、 内网域名、真实手机号、注释里的真实 token/transid - 移除内部资产:CI 触发器、生产通道种子 SQL、真实凭据测试资源文档 - Dockerfile 改用公共基础镜像,移除写死的 registry 凭据 - 补 Apache-2.0 LICENSE,重写 README,LF 行尾 + .gitattributes
217 lines
7.6 KiB
Go
217 lines
7.6 KiB
Go
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
|
||
}
|