Files
iotlcm/internal/logic/upstreamimpl/sim_dx_apn.go
T
MiFaZhan 96b1edff45 chore: 初始开源发布
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
2026-10-22 16:08:06 +08:00

217 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}