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
+568
View File
@@ -0,0 +1,568 @@
package upstreamimpl
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/logic/upstreamimpl/dx"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/service"
"math"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/gogf/gf/v2/frame/g"
)
type sSimDXV3 struct {
client *dx.Client
custNbr string
enableOperations bool
}
func init() {
service.UpstreamAPI().RegisterTemp("sim_dx_v3", &sSimDXV3{})
}
func (s *sSimDXV3) NewTemp(ctx context.Context, cfg *model.SimChannelConfigItemCache) model.SimCardChannelI {
if cfg == nil || cfg.Channel == nil || cfg.Template == nil {
return nil
}
var template struct {
API struct {
BaseURL string `json:"baseUrl"`
EnableOperations bool `json:"enableOperations"`
} `json:"api"`
}
var channel struct {
CustNbr string `json:"custNbr"`
CustNumber string `json:"custNumber"`
}
channelConfig := strings.TrimSpace(cfg.Channel.ChanConfig)
if channelConfig == "" {
channelConfig = "{}"
}
if json.Unmarshal([]byte(cfg.Template.TempConfig), &template) != nil || json.Unmarshal([]byte(channelConfig), &channel) != nil {
g.Log().Error(ctx, "invalid telecom v3 JSON configuration")
return nil
}
baseURL := strings.TrimRight(strings.TrimSpace(template.API.BaseURL), "/")
if baseURL == "" {
baseURL = dx.DefaultBaseURL
}
u, err := url.Parse(baseURL)
if err != nil || u.Host == "" || (u.Scheme != "https" && u.Scheme != "http") || u.RawQuery != "" || u.Fragment != "" || u.User != nil || (u.Path != "" && u.Path != "/") {
g.Log().Error(ctx, "telecom v3 baseUrl must be a gateway origin")
return nil
}
if strings.TrimSpace(cfg.Channel.AppKey) == "" || strings.TrimSpace(cfg.Channel.AppSecret) == "" {
g.Log().Error(ctx, "telecom v3 credentials are incomplete")
return nil
}
return &sSimDXV3{
client: dx.NewClient(dx.Config{BaseURL: baseURL, AppKey: cfg.Channel.AppKey, AppSecret: cfg.Channel.AppSecret,
Timeout: resolveUpstreamHTTPTimeout(ctx, cfg.Template.TempConfig), UppercaseSign: true}),
custNbr: firstNonEmpty(channel.CustNbr, channel.CustNumber), enableOperations: template.API.EnableOperations,
}
}
type dxV3Error struct{ code, message string }
func (e *dxV3Error) Error() string { return e.message }
func (s *sSimDXV3) request(ctx context.Context, path string, params url.Values, body any) (map[string]any, error) {
var data []byte
var err error
if body == nil {
data, err = s.client.DoGetRequest(ctx, "/api/v3/"+path, params)
} else {
data, err = json.Marshal(body)
if err == nil {
data, err = s.client.DoPostJSONRequest(ctx, "/api/v3/"+path, data)
}
}
if err != nil {
return nil, err
}
var envelope struct {
Code json.RawMessage `json:"code"`
Message string `json:"message"`
Result json.RawMessage `json:"result"`
}
if err = json.Unmarshal(data, &envelope); err != nil {
return nil, fmt.Errorf("invalid telecom v3 JSON response: %w", err)
}
code := dxV3RawCode(envelope.Code)
if code == "" {
return nil, fmt.Errorf("telecom v3 response is missing code")
}
if code != "0" {
return nil, &dxV3Error{code: code, message: firstNonEmpty(envelope.Message, "telecom v3 business error")}
}
var result map[string]any
decoder := json.NewDecoder(bytes.NewReader(envelope.Result))
decoder.UseNumber()
if err = decoder.Decode(&result); err != nil {
return nil, fmt.Errorf("invalid telecom v3 result: %w", err)
}
if result == nil {
return nil, fmt.Errorf("telecom v3 response has no result")
}
return result, nil
}
func dxV3RawCode(raw json.RawMessage) string {
if len(raw) == 0 || strings.TrimSpace(string(raw)) == "null" {
return ""
}
var str string
if json.Unmarshal(raw, &str) == nil {
return strings.TrimSpace(str)
}
var number json.Number
if json.Unmarshal(raw, &number) == nil {
return number.String()
}
return ""
}
func dxV3String(item map[string]any, name string) string {
switch value := item[name].(type) {
case string:
return strings.TrimSpace(value)
case json.Number:
return value.String()
default:
return ""
}
}
func dxV3Params(key *model.SimKey) (url.Values, error) {
if key != nil {
for _, value := range []struct{ name, value string }{{"msisdn", key.MSISDN}, {"iccid", key.ICCID}, {"imsi", key.IMSI}} {
if strings.TrimSpace(value.value) != "" {
return url.Values{value.name: {strings.TrimSpace(value.value)}}, nil
}
}
}
return nil, fmt.Errorf("missing msisdn/iccid/imsi")
}
func dxV3Failure[T any](key *model.SimKey, err error) *model.SimBaseRes[T] {
item := &model.SimBaseRes[T]{SimKey: key, Code: consts.ApiStatusError, Message: err.Error()}
var business *dxV3Error
var httpError *dx.HTTPStatusError
if errors.As(err, &business) {
item.ErrCode = business.code
if code, parseErr := strconv.Atoi(business.code); parseErr == nil && code != 0 {
item.Code = code
}
} else if errors.As(err, &httpError) {
item.ErrCode = httpError.RespCode
}
return item
}
// dxV3LegacyUsageConcurrency 是 v3 逐卡回退查询的并发度,与 v1 保持一致,
// 避免多卡查询退化成串行 N 次请求。
const dxV3LegacyUsageConcurrency = 5
func dxV3QueryOne[T any](ctx context.Context, s *sSimDXV3, key *model.SimKey, path string,
prepare func(url.Values) error, convert func(map[string]any) (T, error), post bool) *model.SimBaseRes[T] {
params, err := dxV3Params(key)
if err == nil && prepare != nil {
err = prepare(params)
}
var result map[string]any
if err == nil {
var body any
if post {
values := make(map[string]string, len(params))
for name := range params {
values[name] = params.Get(name)
}
body = values
}
result, err = s.request(ctx, path, params, body)
}
if err == nil {
for _, identity := range []struct{ name, value string }{{"msisdn", key.MSISDN}, {"iccid", key.ICCID}, {"imsi", key.IMSI}} {
if returned := dxV3String(result, identity.name); returned != "" && identity.value != "" && returned != strings.TrimSpace(identity.value) {
err = fmt.Errorf("telecom v3 returned a mismatched card identifier")
break
}
}
}
var output T
if err == nil {
output, err = convert(result)
}
if err != nil {
return dxV3Failure[T](key, err)
}
return &model.SimBaseRes[T]{SimKey: key, ResData: output}
}
func dxV3Query[T any](ctx context.Context, s *sSimDXV3, keys []*model.SimKey, path string,
prepare func(url.Values) error, convert func(map[string]any) (T, error), post bool) []*model.SimBaseRes[T] {
items := make([]*model.SimBaseRes[T], 0, len(keys))
for _, key := range keys {
items = append(items, dxV3QueryOne(ctx, s, key, path, prepare, convert, post))
}
return items
}
// dxV3QueryConcurrent 与 dxV3Query 结果完全一致(保持输入顺序),只是并行发出单卡请求。
func dxV3QueryConcurrent[T any](ctx context.Context, s *sSimDXV3, keys []*model.SimKey, path string,
prepare func(url.Values) error, convert func(map[string]any) (T, error), post bool) []*model.SimBaseRes[T] {
results := make([]*model.SimBaseRes[T], len(keys))
if len(keys) == 0 {
return results
}
jobs := make(chan int)
var group sync.WaitGroup
workers := min(dxV3LegacyUsageConcurrency, len(keys))
for worker := 0; worker < workers; worker++ {
group.Add(1)
go func() {
defer group.Done()
for index := range jobs {
results[index] = dxV3QueryOne(ctx, s, keys[index], path, prepare, convert, post)
}
}()
}
for index := range keys {
jobs <- index
}
close(jobs)
group.Wait()
return results
}
// dxV3BatchThenFill 让批量接口只承担"加速",正确性以单卡查询为基准:
// 先按批量取一轮结果,再把其中失败或缺失的卡用单卡并发查询补齐,保留批量成功的项。
// 这样批量级故障(鉴权、报文、网关错误)或某张卡被批量接口遗漏时,不会整批/逐卡报废;
// 批量整体不可用(参数/窗口/报文异常)时直接退化为逐卡并发,不丢卡、不错位。
// 本方法只用于查询类接口(幂等),重复一次调用是安全的。
func dxV3BatchThenFill[T any](ctx context.Context, s *sSimDXV3, keys []*model.SimKey, path string,
prepare func(url.Values) error, convert func(map[string]any) (T, error),
batch func() ([]*model.SimBaseRes[T], error)) []*model.SimBaseRes[T] {
results, err := batch()
if err != nil || len(results) != len(keys) {
return dxV3QueryConcurrent(ctx, s, keys, path, prepare, convert, false)
}
retryKeys := make([]*model.SimKey, 0, len(keys))
retryIndex := make([]int, 0, len(keys))
for index, item := range results {
if item == nil || item.Code != consts.ApiStatusSuccess {
retryKeys = append(retryKeys, keys[index])
retryIndex = append(retryIndex, index)
}
}
if len(retryKeys) == 0 {
return results
}
filled := dxV3QueryConcurrent(ctx, s, retryKeys, path, prepare, convert, false)
for offset, index := range retryIndex {
results[index] = filled[offset]
}
return results
}
func dxV3BasicInfo(item map[string]any) (model.SimBasicInfoItem, error) {
output := model.SimBasicInfoItem{ICCID: dxV3String(item, "iccid"), IMSI: dxV3String(item, "imsi"), MSISDN: dxV3String(item, "msisdn"),
OpenDate: dxV3String(item, "createTime"), ActiveDate: dxV3String(item, "activeTime"), SimStatus: dxV3String(item, "simStatus"),
MainOfferName: firstNonEmpty(dxV3String(item, "mainPackageName"), dxV3String(item, "prodName")), CardBindingType: dxV3String(item, "cardBindingType"),
IsDirectVisit: dxV3String(item, "isDirectVisit"), IsHumanLimit: dxV3String(item, "isHumanLimit"), VpdnGreDoamin: dxV3String(item, "vpdnGreDoamin"),
VpdnL2tpDoamin: dxV3String(item, "vpdnL2tpDoamin"), AreaLimitType: dxV3String(item, "areaLimitType"), AreaLimitInfo: dxV3String(item, "areaLimitInfo")}
if output.ICCID == "" && output.MSISDN == "" && output.IMSI == "" {
return output, fmt.Errorf("telecom v3 basic info is missing card identifiers")
}
output.StopTypeList = normalizeDXStringList(item["stopTypeList"])
return output, nil
}
func (s *sSimDXV3) QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error) {
return dxV3Query(ctx, s, input.SimKeys, "getSimInfo", nil, dxV3BasicInfo, false), nil
}
func dxV3Status(item map[string]any) (model.SimCardStatusItem, error) {
status := dxV3String(item, "simStatus")
switch status {
case "1", "2", "3", "4", "5", "6", "7":
default:
return model.SimCardStatusItem{}, fmt.Errorf("invalid telecom v3 simStatus: %s", status)
}
return model.SimCardStatusItem{CardStatus: convertDxCardStatus(context.Background(), status), LastChangeDate: dxV3String(item, "lastChangeTime")}, nil
}
func (s *sSimDXV3) QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error) {
return dxV3Query(ctx, s, input.SimKeys, "getSimStatus", nil, dxV3Status, false), nil
}
func (s *sSimDXV3) QuerySimSession(ctx context.Context, input *model.SimSessionInput) (model.SimSessionOutput, error) {
return dxV3Query(ctx, s, input.SimKeys, "getOnlineStatus", nil, func(item map[string]any) (model.SimSessionItem, error) {
status := consts.CardSessionStatusUnknown
switch dxV3String(item, "onlineStatus") {
case "1":
status = consts.CardSessionStatusOnline
case "2":
status = consts.CardSessionStatusOffline
}
return model.SimSessionItem{Status: status, ApnId: dxV3String(item, "apnName"), Imei: dxV3String(item, "imei"), Rat: dxV3String(item, "ratType"),
Ipv4: dxV3String(item, "ipv4Address"), Ipv6: dxV3String(item, "ipv6Address"), Ipv6Prefix: dxV3String(item, "ipv6PrefixLen"),
CreateTime: firstNonEmpty(dxV3String(item, "startTime"), dxV3String(item, "eventTime"))}, nil
}, false), nil
}
func (s *sSimDXV3) QuerySimImei(ctx context.Context, input *model.SimImeiInput) (model.SimImeiOutput, error) {
return dxV3Query(ctx, s, input.SimKeys, "getImei", nil, func(item map[string]any) (model.SimImeiItem, error) {
return model.SimImeiItem{Imei: dxV3String(item, "imei")}, nil
}, false), nil
}
func dxV3Amount(item map[string]any) (float64, error) {
value := firstNonEmpty(dxV3String(item, "dataAmount"), dxV3String(item, "totalUsageAmount"))
amount, err := strconv.ParseFloat(value, 64)
if err != nil || math.IsNaN(amount) || math.IsInf(amount, 0) || amount < 0 {
return 0, fmt.Errorf("invalid telecom v3 dataAmount")
}
if unit := strings.ToUpper(dxV3String(item, "unit")); unit != "" && unit != "MB" {
return 0, fmt.Errorf("unexpected telecom v3 usage unit: %s", unit)
}
return amount, nil
}
func (s *sSimDXV3) QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) (model.SimDataUsageOutput, error) {
return dxV3Query(ctx, s, input.SimKeys, "getDataUsage", nil, func(item map[string]any) (model.SimDataUsageItem, error) {
amount, err := dxV3Amount(item)
return model.SimDataUsageItem{DataUsage: amount}, err
}, false), nil
}
func (s *sSimDXV3) QuerySimDataUsageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) {
prepare := func(params url.Values) error {
if _, err := time.Parse("200601", input.ReqData.QueryDate); err != nil {
return fmt.Errorf("invalid queryDate: expected yyyyMM")
}
params.Set("month", input.ReqData.QueryDate)
return nil
}
convert := func(item map[string]any) (model.SimDataUsageMonthlyOutputItem, error) {
amount, err := dxV3Amount(item)
return model.SimDataUsageMonthlyOutputItem{DataAmount: amount}, err
}
if s.monthlyBatchEligible(input) {
return dxV3BatchThenFill(ctx, s, input.SimKeys, "getDataUsageByMonth", prepare, convert,
func() (model.SimDataUsageMonthlyOutput, error) { return s.QuerySimDataUsageMonthlyBatch(ctx, input) }), nil
}
if len(input.SimKeys) > 1 {
return dxV3QueryConcurrent(ctx, s, input.SimKeys, "getDataUsageByMonth", prepare, convert, false), nil
}
return dxV3Query(ctx, s, input.SimKeys, "getDataUsageByMonth", prepare, convert, false), nil
}
// monthlyBatchEligible 判断多卡月流量查询能否升级为批量接口:需要配置 custNbr、每张卡都有
// MSISDN,且查询月份为当月。与 v1 一致:v1 的自动批量同样只在当月生效,历史月份继续逐卡查询。
// 任一不满足则回退逐卡(并发)查询。
func (s *sSimDXV3) monthlyBatchEligible(input *model.SimDataUsageMonthlyInput) bool {
if input == nil || len(input.SimKeys) < 2 || strings.TrimSpace(s.custNbr) == "" {
return false
}
if !dxBatchMonthAvailable(input.ReqData.QueryDate, time.Now()) {
return false
}
for _, key := range input.SimKeys {
if key == nil || strings.TrimSpace(key.MSISDN) == "" {
return false
}
}
return true
}
func (s *sSimDXV3) QuerySimDataUsageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) {
prepare := func(params url.Values) error {
day, err := time.ParseInLocation("20060102", input.ReqData.QueryDate, dxBatchLocation)
now := time.Now().In(dxBatchLocation)
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, dxBatchLocation)
if err != nil || !day.Before(today) {
return fmt.Errorf("queryDate must be a completed day in yyyyMMdd format")
}
params.Set("beginDate", input.ReqData.QueryDate)
params.Set("endDate", input.ReqData.QueryDate)
return nil
}
convert := func(item map[string]any) (model.SimDataUsageDailyOutputItem, error) {
amount, err := dxV3Amount(item)
return model.SimDataUsageDailyOutputItem{QueryDate: input.ReqData.QueryDate, DataAmount: amount}, err
}
if s.dailyBatchEligible(input) {
return dxV3BatchThenFill(ctx, s, input.SimKeys, "getDataUsageBetweenDate", prepare, convert,
func() (model.SimDataUsageDailyOutput, error) { return s.QuerySimDataUsageDailyBatch(ctx, input) }), nil
}
if len(input.SimKeys) > 1 {
return dxV3QueryConcurrent(ctx, s, input.SimKeys, "getDataUsageBetweenDate", prepare, convert, false), nil
}
return dxV3Query(ctx, s, input.SimKeys, "getDataUsageBetweenDate", prepare, convert, false), nil
}
// dailyBatchEligible 与 monthlyBatchEligible 同构:批量仅在已结束且落在最近七天窗口内的日期可用。
func (s *sSimDXV3) dailyBatchEligible(input *model.SimDataUsageDailyInput) bool {
if input == nil || len(input.SimKeys) < 2 || strings.TrimSpace(s.custNbr) == "" {
return false
}
if !dxBatchDayAvailable(input.ReqData.QueryDate, time.Now()) {
return false
}
for _, key := range input.SimKeys {
if key == nil || strings.TrimSpace(key.MSISDN) == "" {
return false
}
}
return true
}
func (s *sSimDXV3) QuerySimStopReason(ctx context.Context, input *model.SimStopReasonInput) (model.SimStopReasonOutput, error) {
return dxV3Query(ctx, s, input.SimKeys, "getSimInfo", nil, func(item map[string]any) (model.SimStopReasonItem, error) {
reason := strings.Join(trimNonEmptyStrings([]string{dxV3String(item, "stopStatusName"), dxV3String(item, "stopTypeName")}), "; ")
if reason == "" {
reason = mapDxStopTypeListText(normalizeDXStringList(item["stopTypeList"]))
}
return model.SimStopReasonItem{StopReason: reason}, nil
}, false), nil
}
func (s *sSimDXV3) QuerySimApnInfo(ctx context.Context, input *model.SimApnInfoInput) (model.SimApnInfoOutput, error) {
return dxV3Query(ctx, s, input.SimKeys, "getApnInfo", nil, func(item map[string]any) (model.SimApnInfoItemList, error) {
list, ok := item["apnInfoList"].([]any)
if !ok {
return model.SimApnInfoItemList{}, fmt.Errorf("telecom v3 response is missing apnInfoList")
}
output := model.SimApnInfoItemList{ApnList: make([]model.SimApnInfoItem, 0, len(list))}
for _, entry := range list {
apn, ok := entry.(map[string]any)
if !ok {
return output, fmt.Errorf("invalid telecom v3 APN entry")
}
output.ApnList = append(output.ApnList, model.SimApnInfoItem{ApnName: dxV3String(apn, "apnName"), Status: dxV3String(apn, "status")})
}
return output, nil
}, false), nil
}
func (s *sSimDXV3) QuerySimCardBindStatus(ctx context.Context, input *model.SimCardBindStatusInput) (model.SimCardBindStatusOutput, error) {
return dxV3Query(ctx, s, input.SimKeys, "getSimBindingInfo", nil, func(item map[string]any) (model.SimCardBindStatusItem, error) {
return model.SimCardBindStatusItem{BindStatus: dxV3String(item, "simSeparationStatus"), CardBindingType: dxV3String(item, "bindType"),
OrderStatus: dxV3String(item, "isOrdered"), BindImei: dxV3String(item, "bindImei"), LastImei: dxV3String(item, "lastImei")}, nil
}, false), nil
}
// QuerySimDeviceStatus 查询终端开关机状态(电信口径为“仅 3G”)。
//
// 注意方法必须是 GET:官方文档把 /api/v3/getDevicePowerStatus 写成 POST,但网关实际只接受
// GET(实测 POST 返回 799 S10010“请求方法不匹配”,GET 返回 200)。同一网关下 v1 的
// /api/v1/prod/getDevicePowerStatus 反而只接受 POST,两者不可互推。
//
// 响应有两种形态,需兼容键名:
// - 有记录:{"status":"0","statusName":"开机"}(文档口径,小写)
// - 无记录:{"Result":"-1","resultName":"无终端开关机信息"}(大写 R,仅 3G 接口对 4G/5G 卡常见)
func (s *sSimDXV3) QuerySimDeviceStatus(ctx context.Context, input *model.SimDeviceStatusInput) (model.SimDeviceStatusOutput, error) {
return dxV3Query(ctx, s, input.SimKeys, "getDevicePowerStatus", nil, func(item map[string]any) (model.SimDeviceStatusItem, error) {
status := firstNonEmpty(dxV3String(item, "status"), dxV3String(item, "Status"), dxV3String(item, "Result"))
return model.SimDeviceStatusItem{DeviceStatus: consts.CardSessionStatusUnknown, OperatingStatus: mapDxOperatingStatus(status)}, nil
}, false), nil
}
func (s *sSimDXV3) QuerySimRegionLimitStatus(ctx context.Context, input *model.SimRegionLimitStatusInput) (model.SimRegionLimitStatusOutput, error) {
return dxV3Query(ctx, s, input.SimKeys, "getSimRegionLimitStatus", nil, func(item map[string]any) (model.SimRegionLimitStatusItem, error) {
order, limit := dxV3String(item, "orderStatus"), dxV3String(item, "regionLimitStatus")
return model.SimRegionLimitStatusItem{RegionLimitStatus: mapDxRegionLimitStatusByOrder(order, limit), OrderStatus: order, LimitStatus: limit, LimitType: dxV3String(item, "limitType"), LimitArea: dxV3String(item, "restrictedArea")}, nil
}, false), nil
}
// QuerySimFrozenState 电信没有独立的冻结状态查询接口。与 v1 一致,返回成功项并附说明文案,
// 避免诊断页与通道自检把它当成上游错误(v1 返回 code=0 + “不支持查询”)。
func (s *sSimDXV3) QuerySimFrozenState(_ context.Context, input *model.SimFrozenStateInput) (model.SimFrozenStateOutput, error) {
output := make(model.SimFrozenStateOutput, 0, len(input.SimKeys))
for _, key := range input.SimKeys {
output = append(output, &model.SimBaseRes[model.SimFrozenStateItem]{
SimKey: key,
Code: consts.ApiStatusSuccess,
ResData: model.SimFrozenStateItem{
FrozenStatus: "不支持查询",
FrozenReason: "电信当前无独立冻结查询接口",
},
})
}
return output, nil
}
// QuerySimDataDiagnosis 电信不提供达量限速档位查询。沿用 v1 的约定返回 ApiInvokeException,
// 让上层按“暂不支持”降级为跳过(诊断页 skipped)而不是报错。
func (s *sSimDXV3) QuerySimDataDiagnosis(_ context.Context, input *model.SimDataDiagnosisInput) (model.SimDataDiagnosisOutput, error) {
output := make(model.SimDataDiagnosisOutput, 0, len(input.SimKeys))
for _, key := range input.SimKeys {
output = append(output, &model.SimBaseRes[any]{
SimKey: key,
Code: consts.ApiInvokeException,
Message: "电信官方接口暂不提供当前达量限速查询能力",
})
}
return output, nil
}
func dxV3Order(item map[string]any) (model.SimCardStatusChangeItem, error) {
order := dxV3String(item, "transactionId")
if order == "" {
return model.SimCardStatusChangeItem{}, fmt.Errorf("telecom v3 operation returned no transactionId")
}
return model.SimCardStatusChangeItem{OrderNum: order}, nil
}
func (s *sSimDXV3) ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) (model.SimCardStatusChangeOutput, error) {
return dxV3Query(ctx, s, input.SimKeys, "updateSimStatus", func(params url.Values) error {
if !s.enableOperations {
return fmt.Errorf("telecom v3 operations are disabled")
}
oper, ok := convertDxCardStatusChangeOperType(ctx, input.ReqData.OperType)
if !ok {
return fmt.Errorf("unsupported status operType")
}
params.Set("operType", oper)
return nil
}, dxV3Order, false), nil
}
func (s *sSimDXV3) ChangeSimApnFunction(ctx context.Context, input *model.SimCardOperateChangeApnInput) (model.SimCardOperateChangeApnOutput, error) {
return dxV3Query(ctx, s, input.SimKeys, "singleCutNet", func(params url.Values) error {
if !s.enableOperations {
return fmt.Errorf("telecom v3 operations are disabled")
}
// singleCutNet 是整卡数据开停语义,电信侧没有“按 APN 定向”这一维度。
// 与 v1 保持一致:忽略调用方携带的 ApnName(预警自动断网、批量数据开停都会固定带
// apnName="CMIOT"),不能因此拒绝请求。
if params.Get("msisdn") == "" {
return fmt.Errorf("singleCutNet requires msisdn")
}
switch strings.TrimSpace(input.ReqData.OperType) {
case "0":
params.Set("operType", "1")
case "1":
params.Set("operType", "2")
default:
return fmt.Errorf("unsupported data operType")
}
return nil
}, func(item map[string]any) (model.SimCardOperateChangeApnItem, error) {
order, err := dxV3Order(item)
return model.SimCardOperateChangeApnItem{OrderNum: order.OrderNum}, err
}, true), nil
}
var _ model.SimCardChannelI = (*sSimDXV3)(nil)