package upstreamimpl import ( "bytes" "context" "encoding/json" "errors" "fmt" "strconv" "strings" "time" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/logic/upstreamimpl/yunda" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/service" ) const yundaTemplateKey = "sim_yunda_v1" type sSimYunda struct { channelConfig *model.SimChannelConfigItemCache client *yunda.Client } type yundaFloat float64 func (f *yundaFloat) UnmarshalJSON(data []byte) error { var number float64 if err := json.Unmarshal(data, &number); err == nil { *f = yundaFloat(number) return nil } var text string if err := json.Unmarshal(data, &text); err != nil { return err } if text == "" { *f = 0 return nil } value, err := strconv.ParseFloat(text, 64) if err != nil { return err } *f = yundaFloat(value) return nil } type yundaCards []yundaCard func (cards *yundaCards) UnmarshalJSON(data []byte) error { data = bytes.TrimSpace(data) if len(data) == 0 || bytes.Equal(data, []byte("null")) { *cards = nil return nil } if data[0] == '[' { return json.Unmarshal(data, (*[]yundaCard)(cards)) } var card yundaCard if err := json.Unmarshal(data, &card); err != nil { return err } *cards = []yundaCard{card} return nil } type yundaCard struct { MSISDN string `json:"msisdn"` ICCID string `json:"iccid"` IMEI string `json:"imei"` IMSI string `json:"imsi"` Code string `json:"code"` Error string `json:"error"` Status string `json:"status"` GPRSStatus string `json:"gprsStatus"` GPRSStatusLower string `json:"gprsstatus"` IP string `json:"ip"` RAT string `json:"rat"` StatTimestamp string `json:"statustimestamp"` OpenTime string `json:"opentime"` PackageID string `json:"packageid"` PackageName string `json:"packagename"` LifecycleStatus string `json:"lifecycleStatus"` LifecycleStatusLower string `json:"lifecyclestatus"` APNList []yundaAPN `json:"apnlist"` APN string `json:"apn"` } type yundaAPN struct { APNName string `json:"apnname"` UsedFlow yundaFloat `json:"usedflow"` DailyList []yundaDailyUse `json:"dailylist"` } type yundaDailyUse struct { UsedFlow yundaFloat `json:"usedflow"` Date string `json:"date"` } type yundaConfig struct { BaseURL string `json:"baseUrl"` Timeout string `json:"timeout"` } type yundaConfigEnvelope struct { API yundaConfig `json:"api"` BaseURL string `json:"baseUrl"` Timeout string `json:"timeout"` } func NewSimYunda() *sSimYunda { return &sSimYunda{} } func init() { service.UpstreamAPI().RegisterTemp(yundaTemplateKey, NewSimYunda()) } func (s *sSimYunda) NewTemp(ctx context.Context, channelConfig *model.SimChannelConfigItemCache) model.SimCardChannelI { cfg := yundaConfigEnvelope{} if channelConfig != nil && channelConfig.Template != nil { _ = json.Unmarshal([]byte(channelConfig.Template.TempConfig), &cfg) } if channelConfig != nil && channelConfig.Channel != nil && strings.TrimSpace(channelConfig.Channel.ChanConfig) != "" { var channelCfg yundaConfigEnvelope if json.Unmarshal([]byte(channelConfig.Channel.ChanConfig), &channelCfg) == nil { if channelCfg.API.BaseURL != "" { cfg.API.BaseURL = channelCfg.API.BaseURL } if channelCfg.BaseURL != "" { cfg.BaseURL = channelCfg.BaseURL } if channelCfg.API.Timeout != "" { cfg.API.Timeout = channelCfg.API.Timeout } if channelCfg.Timeout != "" { cfg.Timeout = channelCfg.Timeout } } } baseURL := cfg.API.BaseURL if baseURL == "" { baseURL = cfg.BaseURL } if baseURL == "" { baseURL = "http://api.yundaiot.cn/v1" } timeout := 10 * time.Second if cfg.API.Timeout != "" { if parsed, err := time.ParseDuration(cfg.API.Timeout); err == nil { timeout = parsed } } if cfg.Timeout != "" { if parsed, err := time.ParseDuration(cfg.Timeout); err == nil { timeout = parsed } } appKey, appSecret := "", "" if channelConfig != nil && channelConfig.Channel != nil { appKey = channelConfig.Channel.AppKey appSecret = channelConfig.Channel.AppSecret } return &sSimYunda{channelConfig: channelConfig, client: yunda.NewClient(yunda.Config{BaseURL: baseURL, AppKey: appKey, AppSecret: appSecret, Timeout: timeout})} } func yundaCardValue(key *model.SimKey, field string) string { if key == nil { return "" } switch field { case "iccid": return key.ICCID case "msisdn": return key.MSISDN case "imsi": return key.IMSI } return "" } func (s *sSimYunda) clientPost(ctx context.Context, path string, params map[string]string, out *yundaCards) error { _, err := s.client.Post(ctx, path, params, out) return err } func (s *sSimYunda) requestCard(ctx context.Context, path string, key *model.SimKey, fields map[string]string, out *yundaCards) error { if key == nil { return errors.New("sim key is required") } params := map[string]string{} for name, field := range fields { if value := yundaCardValue(key, field); value != "" { params[name] = value } } if len(params) == 0 { return errors.New("iccid, msisdn or imsi is required") } return s.clientPost(ctx, path, params, out) } func (s *sSimYunda) QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error) { items := make(model.SimBasicInfoOutput, 0, len(input.SimKeys)) for _, key := range input.SimKeys { var result yundaCards err := s.requestCard(ctx, "BatchGetSimInfo", key, map[string]string{"iccids": "iccid", "msisdns": "msisdn", "imsis": "imsi"}, &result) card := firstYundaCard(result) if err != nil || card == nil { items = append(items, yundaBasicError(key, errOr(err, "yunda returned empty card info"))) continue } items = append(items, &model.SimBaseRes[model.SimBasicInfoItem]{Code: consts.ApiStatusSuccess, SimKey: key, ResData: model.SimBasicInfoItem{ICCID: card.ICCID, IMSI: card.IMSI, MSISDN: card.MSISDN, OpenDate: card.OpenTime}}) } return items, nil } func (s *sSimYunda) QuerySimImei(ctx context.Context, input *model.SimImeiInput) (model.SimImeiOutput, error) { items := make(model.SimImeiOutput, 0, len(input.SimKeys)) for _, key := range input.SimKeys { var result yundaCards err := s.requestCard(ctx, "BatchGetSimInfo", key, map[string]string{"iccids": "iccid", "msisdns": "msisdn", "imsis": "imsi"}, &result) card := firstYundaCard(result) if err != nil || card == nil { items = append(items, &model.SimBaseRes[model.SimImeiItem]{Code: consts.ApiStatusError, SimKey: key, Message: errMessage(err, "yunda returned empty card info")}) continue } items = append(items, &model.SimBaseRes[model.SimImeiItem]{Code: consts.ApiStatusSuccess, SimKey: key, ResData: model.SimImeiItem{Imei: card.IMEI}}) } return items, nil } func (s *sSimYunda) QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error) { items := make(model.SimCardStatusOutput, 0, len(input.SimKeys)) for _, key := range input.SimKeys { var result yundaCards err := s.requestCard(ctx, "BatchGetSimStatus", key, map[string]string{"iccids": "iccid", "msisdns": "msisdn"}, &result) card := firstYundaCard(result) if err != nil || card == nil { items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{Code: consts.ApiStatusError, SimKey: key, Message: errMessage(err, "yunda returned empty card status")}) continue } status, err := mapYundaCardStatus(card.Status) if err != nil { items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{Code: consts.ApiStatusError, SimKey: key, Message: err.Error()}) continue } items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{Code: consts.ApiStatusSuccess, SimKey: key, ResData: model.SimCardStatusItem{CardStatus: status}}) } return items, nil } // Yunda lifecycle codes: 0 testing, 1 dormant, 2 inventory, 3 in use, 4 stopped, 5 cancelled. func mapYundaCardStatus(value string) (int, error) { switch strings.TrimSpace(value) { case "0", "测试期": return 6, nil case "1", "沉默期": return 1, nil case "2", "库存期": return 7, nil case "3", "正使用": return 2, nil case "4", "已停机": return 4, nil case "5", "已销户": return 9, nil default: return 0, fmt.Errorf("yunda: unsupported card status %q", value) } } func (s *sSimYunda) QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) (model.SimDataUsageOutput, error) { items := make(model.SimDataUsageOutput, 0, len(input.SimKeys)) for _, key := range input.SimKeys { var result yundaCards err := s.requestCard(ctx, "BatchGetCurrentMonthGprs", key, map[string]string{"iccids": "iccid", "msisdns": "msisdn"}, &result) card := firstYundaCard(result) if err != nil || card == nil { items = append(items, &model.SimBaseRes[model.SimDataUsageItem]{Code: consts.ApiStatusError, SimKey: key, Message: errMessage(err, "yunda returned empty usage")}) continue } items = append(items, &model.SimBaseRes[model.SimDataUsageItem]{Code: consts.ApiStatusSuccess, SimKey: key, ResData: model.SimDataUsageItem{DataUsage: firstYundaUsage(card)}}) } return items, nil } func (s *sSimYunda) QuerySimDataUsageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) { items := make(model.SimDataUsageMonthlyOutput, 0, len(input.SimKeys)) for _, key := range input.SimKeys { var result yundaCards params := map[string]string{"month": input.ReqData.QueryDate} if key != nil { params["iccids"] = key.ICCID params["msisdns"] = key.MSISDN } err := s.clientPost(ctx, "BatchGetSimMonthGPRS", params, &result) card := firstYundaCard(result) if err != nil || card == nil { items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{Code: consts.ApiStatusError, SimKey: key, Message: errMessage(err, "yunda returned empty monthly usage"), ResData: model.SimDataUsageMonthlyOutputItem{DeadLine: input.ReqData.QueryDate}}) continue } items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{Code: consts.ApiStatusSuccess, SimKey: key, ResData: model.SimDataUsageMonthlyOutputItem{DeadLine: input.ReqData.QueryDate, DataAmount: firstYundaUsage(card)}}) } return items, nil } func (s *sSimYunda) QuerySimDataUsageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) { items := make(model.SimDataUsageDailyOutput, 0, len(input.SimKeys)) for _, key := range input.SimKeys { var result yundaCards month, date, dateErr := yundaMonthAndDate(input.ReqData.QueryDate) if dateErr != nil { items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{Code: consts.ApiStatusError, SimKey: key, Message: dateErr.Error()}) continue } params := map[string]string{"month": month, "date": date} if key != nil { params["iccids"] = key.ICCID params["msisdns"] = key.MSISDN } err := s.clientPost(ctx, "BatchGetSimDayGPRS", params, &result) card := firstYundaCard(result) amount, date := firstYundaDailyUsage(card, date) if err != nil || card == nil { items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{Code: consts.ApiStatusError, SimKey: key, Message: errMessage(err, "yunda returned empty daily usage"), ResData: model.SimDataUsageDailyOutputItem{QueryDate: date}}) continue } items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{Code: consts.ApiStatusSuccess, SimKey: key, ResData: model.SimDataUsageDailyOutputItem{QueryDate: date, DataAmount: amount}}) } return items, nil } func (s *sSimYunda) QuerySimSession(ctx context.Context, input *model.SimSessionInput) (model.SimSessionOutput, error) { items := make(model.SimSessionOutput, 0, len(input.SimKeys)) for _, key := range input.SimKeys { keyword := yundaCardValue(key, "iccid") if keyword == "" { keyword = yundaCardValue(key, "msisdn") } if keyword == "" { keyword = yundaCardValue(key, "imsi") } var result yundaCards err := s.clientPost(ctx, "SimOnlineQuery", map[string]string{"keyword": keyword}, &result) card := firstYundaCard(result) if err != nil || card == nil { items = append(items, &model.SimBaseRes[model.SimSessionItem]{Code: consts.ApiStatusError, SimKey: key, Message: errMessage(err, "yunda returned empty online status")}) continue } status := "-1" if card.GPRSStatus == "01" || card.GPRSStatusLower == "01" { status = "0" } items = append(items, &model.SimBaseRes[model.SimSessionItem]{Code: consts.ApiStatusSuccess, SimKey: key, ResData: model.SimSessionItem{ApnId: card.APN, Imei: card.IMEI, Rat: card.RAT, Ipv4: card.IP, Status: status, CreateTime: card.StatTimestamp}}) } return items, nil } func (s *sSimYunda) QuerySimDeviceStatus(ctx context.Context, input *model.SimDeviceStatusInput) (model.SimDeviceStatusOutput, error) { items := make(model.SimDeviceStatusOutput, 0, len(input.SimKeys)) for _, key := range input.SimKeys { var result yundaCards err := s.requestCard(ctx, "BatchGetSimGPRSStatus", key, map[string]string{"iccids": "iccid", "msisdns": "msisdn"}, &result) card := firstYundaCard(result) if err != nil || card == nil { items = append(items, &model.SimBaseRes[model.SimDeviceStatusItem]{Code: consts.ApiStatusError, SimKey: key, Message: errMessage(err, "yunda returned empty GPRS status")}) continue } status := card.GPRSStatus if status == "" { status = card.GPRSStatusLower } items = append(items, &model.SimBaseRes[model.SimDeviceStatusItem]{Code: consts.ApiStatusSuccess, SimKey: key, ResData: model.SimDeviceStatusItem{DeviceStatus: status}}) } return items, nil } func yundaUnsupported[T any](items []*model.SimBaseRes[T], keys []*model.SimKey, operation string) ([]*model.SimBaseRes[T], error) { for _, key := range keys { items = append(items, &model.SimBaseRes[T]{Code: consts.ApiStatusError, SimKey: key, Message: fmt.Sprintf("yunda does not support %s yet", operation)}) } return items, nil } func (s *sSimYunda) QuerySimStopReason(ctx context.Context, input *model.SimStopReasonInput) (model.SimStopReasonOutput, error) { return yundaUnsupported[model.SimStopReasonItem](nil, input.SimKeys, "stop reason query") } func (s *sSimYunda) QuerySimFrozenState(ctx context.Context, input *model.SimFrozenStateInput) (model.SimFrozenStateOutput, error) { return yundaUnsupported[model.SimFrozenStateItem](nil, input.SimKeys, "frozen state query") } func (s *sSimYunda) QuerySimDataDiagnosis(ctx context.Context, input *model.SimDataDiagnosisInput) (model.SimDataDiagnosisOutput, error) { return yundaUnsupported[any](nil, input.SimKeys, "data diagnosis") } func (s *sSimYunda) QuerySimApnInfo(ctx context.Context, input *model.SimApnInfoInput) (model.SimApnInfoOutput, error) { items := make(model.SimApnInfoOutput, 0, len(input.SimKeys)) for _, key := range input.SimKeys { keyword := yundaCardValue(key, "iccid") if keyword == "" { keyword = yundaCardValue(key, "msisdn") } if keyword == "" { keyword = yundaCardValue(key, "imsi") } var result yundaCards err := s.clientPost(ctx, "SimOnlineQuery", map[string]string{"keyword": keyword}, &result) card := firstYundaCard(result) if err != nil || card == nil { items = append(items, &model.SimBaseRes[model.SimApnInfoItemList]{Code: consts.ApiStatusError, SimKey: key, Message: errMessage(err, "yunda returned empty APN info")}) continue } status := "0" if strings.TrimSpace(card.APN) != "" { status = "1" } items = append(items, &model.SimBaseRes[model.SimApnInfoItemList]{Code: consts.ApiStatusSuccess, SimKey: key, ResData: model.SimApnInfoItemList{ApnList: []model.SimApnInfoItem{{ApnName: card.APN, Status: status}}}}) } return items, nil } func (s *sSimYunda) ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) (model.SimCardStatusChangeOutput, error) { return yundaUnsupported[model.SimCardStatusChangeItem](nil, input.SimKeys, "status change") } func (s *sSimYunda) ChangeSimApnFunction(ctx context.Context, input *model.SimCardOperateChangeApnInput) (model.SimCardOperateChangeApnOutput, error) { return yundaUnsupported[model.SimCardOperateChangeApnItem](nil, input.SimKeys, "APN change") } func (s *sSimYunda) QuerySimCardBindStatus(ctx context.Context, input *model.SimCardBindStatusInput) (model.SimCardBindStatusOutput, error) { items := make(model.SimCardBindStatusOutput, 0, len(input.SimKeys)) for _, key := range input.SimKeys { var result yundaCards err := s.requestCard(ctx, "BatchGetSimInfo", key, map[string]string{"iccids": "iccid", "msisdns": "msisdn", "imsis": "imsi"}, &result) card := firstYundaCard(result) if err != nil || card == nil { items = append(items, &model.SimBaseRes[model.SimCardBindStatusItem]{Code: consts.ApiStatusError, SimKey: key, Message: errMessage(err, "yunda returned empty card info")}) continue } bindStatus, bindingType := "0", "sim" if strings.TrimSpace(card.IMEI) != "" { bindStatus, bindingType = "1", "imei" } items = append(items, &model.SimBaseRes[model.SimCardBindStatusItem]{Code: consts.ApiStatusSuccess, SimKey: key, ResData: model.SimCardBindStatusItem{BindStatus: bindStatus, CardBindingType: bindingType}}) } return items, nil } func (s *sSimYunda) QuerySimRegionLimitStatus(ctx context.Context, input *model.SimRegionLimitStatusInput) (model.SimRegionLimitStatusOutput, error) { return yundaUnsupported[model.SimRegionLimitStatusItem](nil, input.SimKeys, "region limit query") } func firstYundaCard(cards yundaCards) *yundaCard { if len(cards) == 0 { return nil } return &cards[0] } func firstYundaUsage(card *yundaCard) float64 { if card == nil || len(card.APNList) == 0 { return 0 } return float64(card.APNList[0].UsedFlow) } func firstYundaDailyUsage(card *yundaCard, fallback string) (float64, string) { if card == nil || len(card.APNList) == 0 || len(card.APNList[0].DailyList) == 0 { return 0, fallback } item := card.APNList[0].DailyList[0] if item.Date == "" { item.Date = fallback } return float64(item.UsedFlow), item.Date } func yundaBasicError(key *model.SimKey, err error) *model.SimBaseRes[model.SimBasicInfoItem] { return &model.SimBaseRes[model.SimBasicInfoItem]{Code: consts.ApiStatusError, SimKey: key, Message: err.Error()} } func errOr(err error, fallback string) error { if err != nil { return err } return errors.New(fallback) } func errMessage(err error, fallback string) string { if err != nil { return err.Error() } return fallback } func yundaMonthAndDate(value string) (string, string, error) { value = strings.TrimSpace(value) for _, layout := range []string{"20060102", "2006-01-02"} { parsed, err := time.Parse(layout, value) if err == nil { return parsed.Format("2006-01"), parsed.Format("2006-01-02"), nil } } return "", "", fmt.Errorf("invalid daily queryDate: %s", value) }