522 lines
18 KiB
Go
522 lines
18 KiB
Go
package upstreamimpl
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
|
|
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts"
|
|
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
|
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/service"
|
|
)
|
|
|
|
const henanXinjingDirectTemplateKey = "sim_henan_xinjing_direct_v1"
|
|
|
|
// henanXinjingConfig is the channel-level config for the direct Henan Xinjing
|
|
// template. The top-level userId/apiKey form is the canonical shape; the
|
|
// nested `credentials` block is kept for backward compatibility with earlier
|
|
// drafts of this template.
|
|
type henanXinjingConfig struct {
|
|
UserID string `json:"userId"`
|
|
APIKey string `json:"apiKey"`
|
|
APIKeyLegacy string `json:"apikey"`
|
|
API struct {
|
|
BaseURL string `json:"baseUrl"`
|
|
Timeout string `json:"timeout"`
|
|
} `json:"api"`
|
|
Credentials struct {
|
|
UserID string `json:"userId"`
|
|
APIKey string `json:"apiKey"`
|
|
APIKeyLegacy string `json:"apikey"`
|
|
} `json:"credentials"`
|
|
}
|
|
|
|
// henanXinjingParseConfig unmarshals one JSON config blob (channel or
|
|
// template) into henanXinjingConfig. Empty input yields a zero config.
|
|
func henanXinjingParseConfig(raw string) henanXinjingConfig {
|
|
var c henanXinjingConfig
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return c
|
|
}
|
|
_ = json.Unmarshal([]byte(raw), &c)
|
|
return c
|
|
}
|
|
|
|
// henanXinjingPickCreds resolves (userId, apiKey) with precedence:
|
|
// channel.chan_config > channel.app_key/app_secret > template.temp_config.
|
|
// This matches how the LCM admin stores credentials: per-channel in
|
|
// t_sim_channels, not in the shared template row.
|
|
func henanXinjingPickCreds(chanCfg, tplCfg henanXinjingConfig, appKey, appSecret string) (user, key string) {
|
|
firstNonEmptyStr := func(vals ...string) string {
|
|
for _, v := range vals {
|
|
if v = strings.TrimSpace(v); v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
user = firstNonEmptyStr(
|
|
chanCfg.UserID, chanCfg.Credentials.UserID,
|
|
appKey,
|
|
tplCfg.UserID, tplCfg.Credentials.UserID,
|
|
)
|
|
key = firstNonEmptyStr(
|
|
chanCfg.APIKey, chanCfg.APIKeyLegacy,
|
|
chanCfg.Credentials.APIKey, chanCfg.Credentials.APIKeyLegacy,
|
|
appSecret,
|
|
tplCfg.APIKey, tplCfg.APIKeyLegacy,
|
|
tplCfg.Credentials.APIKey, tplCfg.Credentials.APIKeyLegacy,
|
|
)
|
|
return
|
|
}
|
|
|
|
// henanXinjingFlexFloat accepts either a JSON number or a JSON string and
|
|
// normalizes to float64. The platform returns `used` as a number and
|
|
// `surplus`/`canused` as strings in some responses.
|
|
type henanXinjingFlexFloat float64
|
|
|
|
func (f *henanXinjingFlexFloat) UnmarshalJSON(data []byte) error {
|
|
if len(data) == 0 || string(data) == "null" {
|
|
*f = 0
|
|
return nil
|
|
}
|
|
var n float64
|
|
if err := json.Unmarshal(data, &n); err == nil {
|
|
*f = henanXinjingFlexFloat(n)
|
|
return nil
|
|
}
|
|
var s string
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
return err
|
|
}
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
*f = 0
|
|
return nil
|
|
}
|
|
v, err := strconv.ParseFloat(s, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("henan xinjing: parse float %q: %w", s, err)
|
|
}
|
|
*f = henanXinjingFlexFloat(v)
|
|
return nil
|
|
}
|
|
|
|
// henanXinjingCard mirrors `data` in `/api/v1/getChaxun`. Only fields we
|
|
// actually use are decoded; the platform returns many more.
|
|
//
|
|
// Documented field meanings (per upstream doc):
|
|
// - state : string card status code ("00" ok, "02" stopped, etc.)
|
|
// - status : int online/auth status (NOT the card lifecycle status)
|
|
// - used : KB (current month)
|
|
// - surplus : KB
|
|
// - expired_at / shouchong_time : yyyy-MM-dd HH:mm:ss strings
|
|
// - gprs_status : 0 unknown 1 on 2 off
|
|
// - power_status: 0 unknown 1 online 2 offline
|
|
// - display_status: 1 pending, 2 normal, 3 abnormal, 4 pre-close, 5 closed
|
|
type henanXinjingCard struct {
|
|
ICCID string `json:"iccid"`
|
|
CardNo string `json:"cardno"`
|
|
IMEI string `json:"imei"`
|
|
Used henanXinjingFlexFloat `json:"used"`
|
|
Surplus henanXinjingFlexFloat `json:"surplus"`
|
|
CanUsed henanXinjingFlexFloat `json:"canused"`
|
|
State string `json:"state"`
|
|
Status int `json:"status"`
|
|
OnlineState int `json:"onlinestate"`
|
|
GPRSStatus int `json:"gprs_status"`
|
|
PowerStatus int `json:"power_status"`
|
|
DisplayStatus int `json:"display_status"`
|
|
ExpiredAt string `json:"expired_at"`
|
|
FirstChargeAt string `json:"shouchong_time"`
|
|
Message string `json:"message"`
|
|
AutoName string `json:"autoname"`
|
|
MonthUseds []struct {
|
|
MonthDate string `json:"month_date"` // yyyyMM
|
|
UsedFlow string `json:"used_flow"` // MB per upstream doc
|
|
} `json:"month_useds"`
|
|
Package []struct {
|
|
OrderNo string `json:"orderno"`
|
|
PackageName string `json:"package_name"`
|
|
State int `json:"state"`
|
|
ExpiredAt string `json:"expired_at"`
|
|
CreatedAt string `json:"created_at"`
|
|
Flow string `json:"flow"`
|
|
MonthlyFlow henanXinjingFlexFloat `json:"monthly_flow"`
|
|
Surplus henanXinjingFlexFloat `json:"surplus"`
|
|
Used henanXinjingFlexFloat `json:"used"`
|
|
} `json:"package"`
|
|
}
|
|
|
|
type sSimHenanXinjingDirect struct {
|
|
channelConfig *model.SimChannelConfigItemCache
|
|
client *henanXinjingClient
|
|
}
|
|
|
|
func init() {
|
|
service.UpstreamAPI().RegisterTemp(henanXinjingDirectTemplateKey, NewSimHenanXinjingDirect())
|
|
}
|
|
|
|
func NewSimHenanXinjingDirect() *sSimHenanXinjingDirect { return &sSimHenanXinjingDirect{} }
|
|
|
|
func (s *sSimHenanXinjingDirect) NewTemp(ctx context.Context, cfg *model.SimChannelConfigItemCache) model.SimCardChannelI {
|
|
chanKey := ""
|
|
if cfg != nil && cfg.Channel != nil {
|
|
chanKey = cfg.Channel.ChanKey
|
|
}
|
|
if cfg == nil || cfg.Channel == nil || cfg.Template == nil {
|
|
g.Log().Warningf(ctx, "henan_xinjing_direct.NewTemp: nil cfg/channel/template, chanKey=%s", chanKey)
|
|
return nil
|
|
}
|
|
chanCfg := henanXinjingParseConfig(cfg.Channel.ChanConfig)
|
|
tplCfg := henanXinjingParseConfig(cfg.Template.TempConfig)
|
|
user, key := henanXinjingPickCreds(chanCfg, tplCfg, cfg.Channel.AppKey, cfg.Channel.AppSecret)
|
|
baseURL := strings.TrimSpace(chanCfg.API.BaseURL)
|
|
if baseURL == "" {
|
|
baseURL = strings.TrimSpace(tplCfg.API.BaseURL)
|
|
}
|
|
if user == "" || key == "" || baseURL == "" {
|
|
g.Log().Warningf(ctx,
|
|
"henan_xinjing_direct.NewTemp: missing required config, chanKey=%s userIdEmpty=%t apiKeyEmpty=%t baseURLEmpty=%t",
|
|
chanKey, user == "", key == "", baseURL == "")
|
|
return nil
|
|
}
|
|
timeout := defaultUpstreamHTTPTimeout
|
|
for _, raw := range []string{chanCfg.API.Timeout, tplCfg.API.Timeout} {
|
|
if raw == "" {
|
|
continue
|
|
}
|
|
if d, err := time.ParseDuration(raw); err == nil && d > 0 {
|
|
timeout = d
|
|
break
|
|
}
|
|
}
|
|
return &sSimHenanXinjingDirect{
|
|
channelConfig: cfg,
|
|
client: newHenanXinjingClient(baseURL, user, key, timeout),
|
|
}
|
|
}
|
|
|
|
// simKeyCardNo picks the identifier Henan Xinjing accepts for `cardno`.
|
|
// The platform keys on its own card-number column, which corresponds to
|
|
// MSISDN for these cards; fall back to ICCID/IMSI when MSISDN is absent.
|
|
func henanXinjingCardNo(k *model.SimKey) string {
|
|
if k == nil {
|
|
return ""
|
|
}
|
|
if v := strings.TrimSpace(k.MSISDN); v != "" {
|
|
return v
|
|
}
|
|
if v := strings.TrimSpace(k.ICCID); v != "" {
|
|
return v
|
|
}
|
|
return strings.TrimSpace(k.IMSI)
|
|
}
|
|
|
|
// fetchCard calls /api/v1/getChaxun for a single SimKey.
|
|
func (s *sSimHenanXinjingDirect) fetchCard(ctx context.Context, k *model.SimKey) (henanXinjingCard, error) {
|
|
if k == nil {
|
|
return henanXinjingCard{}, fmt.Errorf("sim key is required")
|
|
}
|
|
no := henanXinjingCardNo(k)
|
|
if no == "" {
|
|
return henanXinjingCard{}, fmt.Errorf("msisdn/iccid/imsi is required")
|
|
}
|
|
var c henanXinjingCard
|
|
err := s.client.get(ctx, "/api/v1/getChaxun", map[string]string{"cardno": no}, &c)
|
|
return c, err
|
|
}
|
|
|
|
// henanXinjingKBToMB converts upstream KB to the MB unit SimDataUsageItem
|
|
// requires. Rounds to 4 decimal places to keep values readable.
|
|
func henanXinjingKBToMB(kb float64) float64 {
|
|
if kb <= 0 {
|
|
return 0
|
|
}
|
|
mb := kb / 1024
|
|
return float64(int(mb*10000+0.5)) / 10000
|
|
}
|
|
|
|
// henanXinjingMapCardStatus translates the platform's `state` string into the
|
|
// internal CardStatus code used across the gateway.
|
|
//
|
|
// Upstream doc:
|
|
//
|
|
// 0 unknown, 00 normal, 01 one-way stop, 02 stopped, 03 pre-close,
|
|
// 04 closed, 05 transfer, 06 dormant, 07 pending, 08 disabled,
|
|
// 09 inventory, 10 expired, 11 violation stop, 12 lost, 13 user-stop,
|
|
// 14 testing, 99 not-exists
|
|
//
|
|
// Internal consts (from existing adapters like tibiot):
|
|
//
|
|
// 1待激活 2已激活 4停机 6可测试 7库存 8预销户 9已销户
|
|
func henanXinjingMapCardStatus(state string) int {
|
|
switch strings.TrimSpace(state) {
|
|
case "00":
|
|
return 2 // 已激活/正常
|
|
case "01", "02", "08", "11", "12", "13":
|
|
return 4 // 停机
|
|
case "03":
|
|
return 8 // 预销户
|
|
case "04", "10":
|
|
return 9 // 已销户
|
|
case "06":
|
|
return 1 // 休眠 → 待激活
|
|
case "07":
|
|
return 1 // 待激活
|
|
case "09":
|
|
return 7 // 库存
|
|
case "14":
|
|
return 6 // 可测试
|
|
default:
|
|
return 0 // 未知
|
|
}
|
|
}
|
|
|
|
func henanXinjingStatusChangePath(operType string) (string, bool) {
|
|
switch operType {
|
|
case consts.CardStatusChangeOperTypeApplyStop:
|
|
return "/api/v1/stop", true
|
|
case consts.CardStatusChangeOperTypeApplyResume:
|
|
return "/api/v1/start", true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func henanXinjingMainOfferName(card henanXinjingCard) string {
|
|
if name := strings.TrimSpace(card.AutoName); name != "" {
|
|
return name
|
|
}
|
|
for _, item := range card.Package {
|
|
if name := strings.TrimSpace(item.PackageName); name != "" {
|
|
return name
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *sSimHenanXinjingDirect) QuerySimBasicInfo(ctx context.Context, in *model.SimBasicInfoInput) (out model.SimBasicInfoOutput, err error) {
|
|
for _, k := range in.SimKeys {
|
|
c, e := s.fetchCard(ctx, k)
|
|
r := &model.SimBaseRes[model.SimBasicInfoItem]{SimKey: k}
|
|
if e != nil {
|
|
r.Code = consts.ApiStatusError
|
|
r.Message = e.Error()
|
|
} else {
|
|
r.Code = 0
|
|
r.ResData = model.SimBasicInfoItem{
|
|
ICCID: firstNonEmpty(c.ICCID, k.ICCID),
|
|
IMSI: k.IMSI,
|
|
MSISDN: firstNonEmpty(c.CardNo, k.MSISDN),
|
|
ActiveDate: c.FirstChargeAt,
|
|
OpenDate: c.FirstChargeAt, // use 首充时间 as the open/activate timestamp; expired_at goes to remark
|
|
Remark: c.Message,
|
|
SimStatus: strconv.Itoa(henanXinjingMapCardStatus(c.State)),
|
|
MainOfferName: henanXinjingMainOfferName(c),
|
|
}
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *sSimHenanXinjingDirect) QuerySimImei(ctx context.Context, in *model.SimImeiInput) (out model.SimImeiOutput, err error) {
|
|
for _, k := range in.SimKeys {
|
|
c, e := s.fetchCard(ctx, k)
|
|
r := &model.SimBaseRes[model.SimImeiItem]{SimKey: k}
|
|
if e != nil {
|
|
r.Code = consts.ApiStatusError
|
|
r.Message = e.Error()
|
|
} else {
|
|
r.Code = 0
|
|
r.ResData.Imei = c.IMEI
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *sSimHenanXinjingDirect) QuerySimCardStatus(ctx context.Context, in *model.SimCardStatusInput) (out model.SimCardStatusOutput, err error) {
|
|
for _, k := range in.SimKeys {
|
|
c, e := s.fetchCard(ctx, k)
|
|
r := &model.SimBaseRes[model.SimCardStatusItem]{SimKey: k}
|
|
if e != nil {
|
|
r.Code = consts.ApiStatusError
|
|
r.Message = e.Error()
|
|
} else {
|
|
r.Code = 0
|
|
r.ResData.CardStatus = henanXinjingMapCardStatus(c.State)
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *sSimHenanXinjingDirect) QuerySimDataUsage(ctx context.Context, in *model.SimDataUsageInput) (out model.SimDataUsageOutput, err error) {
|
|
for _, k := range in.SimKeys {
|
|
c, e := s.fetchCard(ctx, k)
|
|
r := &model.SimBaseRes[model.SimDataUsageItem]{SimKey: k}
|
|
if e != nil {
|
|
r.Code = consts.ApiStatusError
|
|
r.Message = e.Error()
|
|
} else {
|
|
r.Code = 0
|
|
r.ResData.DataUsage = henanXinjingKBToMB(float64(c.Used))
|
|
total := henanXinjingKBToMB(float64(c.Used) + float64(c.Surplus))
|
|
if total > 0 {
|
|
r.ResData.TotalFlow = &total
|
|
}
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *sSimHenanXinjingDirect) ChangeSimStatus(ctx context.Context, in *model.SimCardStatusChangeInput) (out model.SimCardStatusChangeOutput, err error) {
|
|
operType, ok := normalizeCardStatusChangeOperType(ctx, in.ReqData.OperType)
|
|
if !ok {
|
|
return newInvalidCardStatusChangeOutput(in.SimKeys), nil
|
|
}
|
|
for _, k := range in.SimKeys {
|
|
r := &model.SimBaseRes[model.SimCardStatusChangeItem]{SimKey: k}
|
|
path, _ := henanXinjingStatusChangePath(operType)
|
|
no := henanXinjingCardNo(k)
|
|
var e error
|
|
if no == "" {
|
|
e = fmt.Errorf("msisdn/iccid/imsi is required")
|
|
} else {
|
|
e = s.client.get(ctx, path, map[string]string{"cardno": no}, nil)
|
|
}
|
|
if e != nil {
|
|
r.Code = consts.ApiStatusError
|
|
r.Message = e.Error()
|
|
} else {
|
|
r.Code = 0
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return
|
|
}
|
|
|
|
func henanXinjingUnsupported[T any](keys []*model.SimKey, name string) (out []*model.SimBaseRes[T], err error) {
|
|
for _, k := range keys {
|
|
out = append(out, &model.SimBaseRes[T]{SimKey: k, Code: consts.ApiStatusError, Message: "henan xinjing direct does not support " + name})
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *sSimHenanXinjingDirect) QuerySimSession(ctx context.Context, in *model.SimSessionInput) (model.SimSessionOutput, error) {
|
|
return henanXinjingUnsupported[model.SimSessionItem](in.SimKeys, "session query")
|
|
}
|
|
func (s *sSimHenanXinjingDirect) QuerySimStopReason(ctx context.Context, in *model.SimStopReasonInput) (model.SimStopReasonOutput, error) {
|
|
return henanXinjingUnsupported[model.SimStopReasonItem](in.SimKeys, "stop reason query")
|
|
}
|
|
func (s *sSimHenanXinjingDirect) QuerySimFrozenState(ctx context.Context, in *model.SimFrozenStateInput) (model.SimFrozenStateOutput, error) {
|
|
return henanXinjingUnsupported[model.SimFrozenStateItem](in.SimKeys, "frozen state query")
|
|
}
|
|
func (s *sSimHenanXinjingDirect) QuerySimCardBindStatus(ctx context.Context, in *model.SimCardBindStatusInput) (model.SimCardBindStatusOutput, error) {
|
|
return henanXinjingUnsupported[model.SimCardBindStatusItem](in.SimKeys, "card binding query")
|
|
}
|
|
func (s *sSimHenanXinjingDirect) QuerySimDeviceStatus(ctx context.Context, in *model.SimDeviceStatusInput) (model.SimDeviceStatusOutput, error) {
|
|
out := make(model.SimDeviceStatusOutput, 0, len(in.SimKeys))
|
|
for _, k := range in.SimKeys {
|
|
c, e := s.fetchCard(ctx, k)
|
|
r := &model.SimBaseRes[model.SimDeviceStatusItem]{SimKey: k}
|
|
if e != nil {
|
|
r.Code = consts.ApiStatusError
|
|
r.Message = e.Error()
|
|
} else {
|
|
r.Code = 0
|
|
// gprs_status: 0 unknown 1 power-on 2 power-off
|
|
// power_status: 0 unknown 1 online 2 offline
|
|
// DeviceStatus (compatible semantics): 0 abnormal 1 normal.
|
|
r.ResData.DeviceStatus = henanXinjingMapDeviceStatus(c.GPRSStatus, c.PowerStatus)
|
|
switch c.GPRSStatus {
|
|
case 1:
|
|
r.ResData.OperatingStatus = "1"
|
|
case 2:
|
|
r.ResData.OperatingStatus = "0"
|
|
}
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// henanXinjingMapDeviceStatus merges the platform's gprs_status (power on/off)
|
|
// and power_status (online/offline) into the gateway's binary device status.
|
|
// Only a card that is powered on AND online is "normal" (1); anything else,
|
|
// including unknown, maps to abnormal (0).
|
|
func henanXinjingMapDeviceStatus(gprsStatus, powerStatus int) string {
|
|
if gprsStatus == 1 && powerStatus == 1 {
|
|
return "1"
|
|
}
|
|
return "0"
|
|
}
|
|
func (s *sSimHenanXinjingDirect) QuerySimRegionLimitStatus(ctx context.Context, in *model.SimRegionLimitStatusInput) (model.SimRegionLimitStatusOutput, error) {
|
|
return henanXinjingUnsupported[model.SimRegionLimitStatusItem](in.SimKeys, "region limit query")
|
|
}
|
|
func (s *sSimHenanXinjingDirect) QuerySimDataDiagnosis(ctx context.Context, in *model.SimDataDiagnosisInput) (model.SimDataDiagnosisOutput, error) {
|
|
return henanXinjingUnsupported[any](in.SimKeys, "data diagnosis")
|
|
}
|
|
func (s *sSimHenanXinjingDirect) QuerySimDataUsageMonthly(ctx context.Context, in *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) {
|
|
queryMonth := ""
|
|
if in != nil {
|
|
queryMonth = strings.TrimSpace(in.ReqData.QueryDate)
|
|
}
|
|
out := make(model.SimDataUsageMonthlyOutput, 0, len(in.SimKeys))
|
|
for _, k := range in.SimKeys {
|
|
c, e := s.fetchCard(ctx, k)
|
|
r := &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{SimKey: k}
|
|
if e != nil {
|
|
r.Code = consts.ApiStatusError
|
|
r.Message = e.Error()
|
|
} else {
|
|
var selected *struct {
|
|
MonthDate string
|
|
UsedFlow string
|
|
}
|
|
for i := range c.MonthUseds {
|
|
m := strings.TrimSpace(c.MonthUseds[i].MonthDate)
|
|
if queryMonth == "" || strings.Contains(m, queryMonth) {
|
|
selected = &struct {
|
|
MonthDate string
|
|
UsedFlow string
|
|
}{c.MonthUseds[i].MonthDate, c.MonthUseds[i].UsedFlow}
|
|
break
|
|
}
|
|
}
|
|
if selected == nil {
|
|
r.Code = consts.ApiStatusError
|
|
r.Message = "monthly usage data not found"
|
|
} else {
|
|
r.Code = 0
|
|
r.ResData.DeadLine = strings.TrimSpace(selected.MonthDate)
|
|
// month_useds.used_flow is MB per the platform doc, unlike the
|
|
// KB used/surplus fields on the same response.
|
|
if v, perr := strconv.ParseFloat(strings.TrimSpace(selected.UsedFlow), 64); perr == nil {
|
|
r.ResData.DataAmount = v
|
|
}
|
|
}
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out, nil
|
|
}
|
|
func (s *sSimHenanXinjingDirect) QuerySimDataUsageDaily(ctx context.Context, in *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) {
|
|
return henanXinjingUnsupported[model.SimDataUsageDailyOutputItem](in.SimKeys, "historical daily usage query")
|
|
}
|
|
func (s *sSimHenanXinjingDirect) QuerySimApnInfo(ctx context.Context, in *model.SimApnInfoInput) (model.SimApnInfoOutput, error) {
|
|
return henanXinjingUnsupported[model.SimApnInfoItemList](in.SimKeys, "APN query")
|
|
}
|
|
func (s *sSimHenanXinjingDirect) ChangeSimApnFunction(ctx context.Context, in *model.SimCardOperateChangeApnInput) (model.SimCardOperateChangeApnOutput, error) {
|
|
return henanXinjingUnsupported[model.SimCardOperateChangeApnItem](in.SimKeys, "APN change")
|
|
}
|