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
This commit is contained in:
MiFaZhan
2026-10-22 15:42:29 +08:00
commit 96b1edff45
191 changed files with 30805 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
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
}