Files
iotlcm/internal/logic/upstreamimpl/sim_dx_v3.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

569 lines
23 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 (
"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)