package upstreamimpl import ( "context" "encoding/json" "strings" "sync" "time" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/logic/upstreamimpl/recharge" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/service" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "golang.org/x/sync/singleflight" ) const rechargeTempKey = "sim_sdtywl_v1" const rechargeFindCardCacheTTL = 30 * time.Second // 腾宇卡状态 status_show_id -> 系统 cardStatus // 腾宇: 0未知 1库存 2可测试 3待激活 4已激活 5已停机 6预销户 7已销户 // 系统: 0未知 1待激活 2已激活 4已停机 6可测试 7库存 8预销户 9已销户 var rechargeCardStatusMap = map[int]int{ 0: 0, 1: 7, 2: 6, 3: 1, 4: 2, 5: 4, 6: 8, 7: 9, } type sSimRecharge struct { channelConfig *model.SimChannelConfigItemCache client *recharge.Client cardCache sync.Map cardFlight singleflight.Group } type rechargeCardCacheEntry struct { Info model.RechargeCardInfo UpdatedAt string ExpiresAt time.Time } type rechargeFindCardResult struct { Info *model.RechargeCardInfo Cached bool UpdatedAt string } var _ model.SimCardChannelI = (*sSimRecharge)(nil) type rechargeChannelConfig struct { PwdKey string `json:"pwdkey"` Pwdiv string `json:"pwdiv"` LegacyTimeout json.RawMessage `json:"timeout"` } func NewSimRecharge() *sSimRecharge { return &sSimRecharge{} } func init() { service.UpstreamAPI().RegisterTemp(rechargeTempKey, NewSimRecharge()) } func (s *sSimRecharge) NewTemp(ctx context.Context, channelConfig *model.SimChannelConfigItemCache) model.SimCardChannelI { cfg := rechargeChannelConfig{} if channelConfig != nil && channelConfig.Channel != nil { _ = json.Unmarshal([]byte(channelConfig.Channel.ChanConfig), &cfg) if len(cfg.LegacyTimeout) > 0 && string(cfg.LegacyTimeout) != "null" { g.Log().Warningf(ctx, "recharge channel %s uses deprecated chan_config.timeout; configure template http.timeout instead", channelConfig.Channel.ChanKey) } } baseURL := "" if channelConfig != nil && channelConfig.Template != nil { var templateConfig struct { API struct { BaseURL string `json:"baseUrl"` } `json:"api"` LegacyBaseURL string `json:"baseUrl"` } if err := json.Unmarshal([]byte(channelConfig.Template.TempConfig), &templateConfig); err != nil { g.Log().Warningf(ctx, "parse recharge template config failed, chanKey=%s err=%+v", channelConfig.Channel.ChanKey, err) } else { baseURL = strings.TrimSpace(templateConfig.API.BaseURL) if baseURL == "" && strings.TrimSpace(templateConfig.LegacyBaseURL) != "" { baseURL = strings.TrimSpace(templateConfig.LegacyBaseURL) g.Log().Warningf(ctx, "recharge template %s uses deprecated root baseUrl; move it to api.baseUrl", channelConfig.Template.TempKey) } } } if baseURL == "" { baseURL = recharge.DefaultBaseURL } // 字段复用约定: app_key=pwdkey, app_secret=pwdiv pwdKey := cfg.PwdKey pwdiv := cfg.Pwdiv if channelConfig != nil && channelConfig.Channel != nil { if pwdKey == "" { pwdKey = channelConfig.Channel.AppKey } if pwdiv == "" { pwdiv = channelConfig.Channel.AppSecret } } timeout := resolveUpstreamHTTPTimeout(ctx, templateConfigJSON(channelConfig)) return &sSimRecharge{ channelConfig: channelConfig, client: recharge.NewClient(recharge.Config{ BaseURL: baseURL, PwdKey: pwdKey, Pwdiv: pwdiv, Timeout: timeout, }), } } func convertRechargeCardStatus(ctx context.Context, statusShowID int) int { if status, ok := rechargeCardStatusMap[statusShowID]; ok { return status } g.Log().Warningf(ctx, "Unknown recharge card status %d from upstream", statusShowID) return 0 } // 系统 operType(0停机/1复机) -> 腾宇 changeState type(1停机/0复机),语义相反,禁止透传 func convertRechargeChangeStateType(ctx context.Context, operType string) (int, bool) { switch operType { case consts.CardStatusChangeOperTypeApplyStop: return 1, true case consts.CardStatusChangeOperTypeApplyResume: return 0, true default: g.Log().Warningf(ctx, "Unknown card status change operType %s for recharge upstream", operType) return 0, false } } func (s *sSimRecharge) findCard(ctx context.Context, simKey *model.SimKey) (*rechargeFindCardResult, error) { card := simKey.ICCID if card == "" { card = simKey.MSISDN } card = strings.TrimSpace(card) if cached, ok := s.cachedCard(card); ok { return cached, nil } value, err, _ := s.cardFlight.Do(card, func() (any, error) { if cached, ok := s.cachedCard(card); ok { return cached, nil } output, requestErr := s.client.DoPost(ctx, "/findCard", map[string]any{"card": card}) if requestErr != nil { return nil, requestErr } if output.Code == recharge.CodeRateLimited { g.Log().Warningf(ctx, "recharge findCard rate limited, chanKey %s, card %s", simKey.ChanKey, card) } if output.Code != recharge.CodeSuccess { return nil, &recharge.BusinessError{Code: output.Code, Msg: output.Msg} } var info model.RechargeCardInfo if unmarshalErr := json.Unmarshal(output.Data, &info); unmarshalErr != nil { return nil, unmarshalErr } updatedAt := time.Now().Format("2006-01-02 15:04:05") s.cardCache.Store(card, rechargeCardCacheEntry{ Info: info, UpdatedAt: updatedAt, ExpiresAt: time.Now().Add(rechargeFindCardCacheTTL), }) return &rechargeFindCardResult{Info: &info, UpdatedAt: updatedAt}, nil }) if err != nil { return nil, err } result, ok := value.(*rechargeFindCardResult) if !ok || result == nil || result.Info == nil { return nil, gerror.New("recharge findCard returned invalid result") } return result, nil } func (s *sSimRecharge) cachedCard(card string) (*rechargeFindCardResult, bool) { value, ok := s.cardCache.Load(strings.TrimSpace(card)) if !ok { return nil, false } entry, ok := value.(rechargeCardCacheEntry) if !ok || time.Now().After(entry.ExpiresAt) { s.cardCache.Delete(strings.TrimSpace(card)) return nil, false } info := entry.Info return &rechargeFindCardResult{Info: &info, Cached: true, UpdatedAt: entry.UpdatedAt}, true } func (s *sSimRecharge) invalidateCardCache(card string) { s.cardCache.Delete(strings.TrimSpace(card)) } func rechargeErrorMessage(err error) string { return err.Error() } // QuerySimBasicInfo 单卡基本信息查询 // findCard -> ActiveDate=activate_date, Remark 附带实名状态 func (s *sSimRecharge) QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error) { items := make(model.SimBasicInfoOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { result, err := s.findCard(ctx, simKey) if err != nil { g.Log().Errorf(ctx, "Recharge QuerySimBasicInfo error %+v, simKey %+v", err, simKey) items = append(items, &model.SimBaseRes[model.SimBasicInfoItem]{ SimKey: simKey, Code: consts.ApiStatusError, Message: rechargeErrorMessage(err), }) continue } info := result.Info remark := "未实名" if info.RealName == 1 { remark = "已实名" } items = append(items, &model.SimBaseRes[model.SimBasicInfoItem]{ SimKey: simKey, Code: consts.ApiStatusSuccess, Cached: result.Cached, UpdatedAt: result.UpdatedAt, ResData: model.SimBasicInfoItem{ ActiveDate: info.ActivateDate, OpenDate: "", Remark: remark, }, }) } return items, nil } // QuerySimCardStatus 物联网卡卡状态查询 // findCard.status_show_id -> 系统 cardStatus func (s *sSimRecharge) QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error) { items := make(model.SimCardStatusOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { result, err := s.findCard(ctx, simKey) if err != nil { g.Log().Errorf(ctx, "Recharge QuerySimCardStatus error %+v, simKey %+v", err, simKey) items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{ SimKey: simKey, Code: consts.ApiStatusError, Message: rechargeErrorMessage(err), }) continue } info := result.Info items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{ SimKey: simKey, Code: consts.ApiStatusSuccess, Cached: result.Cached, UpdatedAt: result.UpdatedAt, ResData: model.SimCardStatusItem{ CardStatus: convertRechargeCardStatus(ctx, info.StatusShowID), LastChangeDate: info.ExpireTime, }, }) } return items, nil } // QuerySimDataUsage 单卡本月流量累计使用量查询 // findCard -> c_used/c_total (单位 MB) func (s *sSimRecharge) QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) (model.SimDataUsageOutput, error) { items := make(model.SimDataUsageOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { result, err := s.findCard(ctx, simKey) if err != nil { g.Log().Errorf(ctx, "Recharge QuerySimDataUsage error %+v, simKey %+v", err, simKey) items = append(items, &model.SimBaseRes[model.SimDataUsageItem]{ SimKey: simKey, Code: consts.ApiStatusError, Message: rechargeErrorMessage(err), }) continue } info := result.Info totalFlow := info.TotalFlow items = append(items, &model.SimBaseRes[model.SimDataUsageItem]{ SimKey: simKey, Code: consts.ApiStatusSuccess, Cached: result.Cached, UpdatedAt: result.UpdatedAt, ResData: model.SimDataUsageItem{ DataUsage: info.UsedFlow, TotalFlow: &totalFlow, }, }) } return items, nil } // QuerySimDataDiagnosis 腾宇通道暂不支持达量限速档位查询。 func (s *sSimRecharge) QuerySimDataDiagnosis(ctx context.Context, input *model.SimDataDiagnosisInput) (model.SimDataDiagnosisOutput, error) { items := make(model.SimDataDiagnosisOutput, 0) if input == nil { return items, nil } for _, simKey := range input.SimKeys { items = append(items, &model.SimBaseRes[any]{ SimKey: simKey, Code: consts.ApiInvokeException, Message: rechargeUnsupportedMessage("达量限速档位查询"), }) } return items, nil } // ChangeSimStatus 物联网卡状态变更(停复机) // 系统 operType(0停机/1复机) -> 腾宇 changeState type(1停机/0复机) func (s *sSimRecharge) ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) (model.SimCardStatusChangeOutput, error) { changeType, ok := convertRechargeChangeStateType(ctx, input.ReqData.OperType) if !ok { return newInvalidCardStatusChangeOutput(input.SimKeys), nil } items := make(model.SimCardStatusChangeOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { card := simKey.ICCID if card == "" { card = simKey.MSISDN } output, err := s.client.DoPost(ctx, "/changeState", map[string]any{ "card": card, "type": changeType, }) if err != nil { g.Log().Errorf(ctx, "Recharge ChangeSimStatus error %+v, simKey %+v", err, simKey) items = append(items, &model.SimBaseRes[model.SimCardStatusChangeItem]{ SimKey: simKey, Code: consts.ApiStatusError, Message: err.Error(), ResData: model.SimCardStatusChangeItem{}, }) continue } if output.Code != recharge.CodeSuccess { g.Log().Warningf(ctx, "Recharge ChangeSimStatus upstream error code %d msg %s, simKey %+v", output.Code, output.Msg, simKey) items = append(items, &model.SimBaseRes[model.SimCardStatusChangeItem]{ SimKey: simKey, Code: consts.ApiStatusError, Message: output.Msg, ResData: model.SimCardStatusChangeItem{}, }) continue } s.invalidateCardCache(card) items = append(items, &model.SimBaseRes[model.SimCardStatusChangeItem]{ SimKey: simKey, Code: consts.ApiStatusSuccess, Message: output.Msg, ResData: model.SimCardStatusChangeItem{ OrderNum: "", }, }) } return items, nil } // 腾宇通道暂未支持的查询,参照 sim_dx.go 模式返回 ApiInvokeException func rechargeUnsupportedMessage(name string) string { return "腾宇通道暂不支持" + name } func (s *sSimRecharge) QuerySimSession(ctx context.Context, input *model.SimSessionInput) (model.SimSessionOutput, error) { items := make(model.SimSessionOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { items = append(items, &model.SimBaseRes[model.SimSessionItem]{ SimKey: simKey, Code: consts.ApiInvokeException, Message: rechargeUnsupportedMessage("在线信息查询"), ResData: model.SimSessionItem{Status: consts.CardSessionStatusUnknown}, }) } return items, nil } func (s *sSimRecharge) QuerySimStopReason(ctx context.Context, input *model.SimStopReasonInput) (model.SimStopReasonOutput, error) { items := make(model.SimStopReasonOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { items = append(items, &model.SimBaseRes[model.SimStopReasonItem]{ SimKey: simKey, Code: consts.ApiInvokeException, Message: rechargeUnsupportedMessage("停机原因查询"), }) } return items, nil } func (s *sSimRecharge) QuerySimFrozenState(ctx context.Context, input *model.SimFrozenStateInput) (model.SimFrozenStateOutput, error) { items := make(model.SimFrozenStateOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { items = append(items, &model.SimBaseRes[model.SimFrozenStateItem]{ SimKey: simKey, Code: consts.ApiInvokeException, Message: rechargeUnsupportedMessage("冻结状态查询"), }) } return items, nil } func (s *sSimRecharge) QuerySimCardBindStatus(ctx context.Context, input *model.SimCardBindStatusInput) (model.SimCardBindStatusOutput, error) { items := make(model.SimCardBindStatusOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { items = append(items, &model.SimBaseRes[model.SimCardBindStatusItem]{ SimKey: simKey, Code: consts.ApiInvokeException, Message: rechargeUnsupportedMessage("机卡绑定状态查询"), }) } return items, nil } func (s *sSimRecharge) QuerySimDeviceStatus(ctx context.Context, input *model.SimDeviceStatusInput) (model.SimDeviceStatusOutput, error) { items := make(model.SimDeviceStatusOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { items = append(items, &model.SimBaseRes[model.SimDeviceStatusItem]{ SimKey: simKey, Code: consts.ApiInvokeException, Message: rechargeUnsupportedMessage("设备状态查询"), ResData: model.SimDeviceStatusItem{DeviceStatus: consts.CardSessionStatusUnknown}, }) } return items, nil } func (s *sSimRecharge) QuerySimRegionLimitStatus(ctx context.Context, input *model.SimRegionLimitStatusInput) (model.SimRegionLimitStatusOutput, error) { items := make(model.SimRegionLimitStatusOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { items = append(items, &model.SimBaseRes[model.SimRegionLimitStatusItem]{ SimKey: simKey, Code: consts.ApiInvokeException, Message: rechargeUnsupportedMessage("地域限制状态查询"), }) } return items, nil } func (s *sSimRecharge) QuerySimImei(ctx context.Context, input *model.SimImeiInput) (model.SimImeiOutput, error) { items := make(model.SimImeiOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { items = append(items, &model.SimBaseRes[model.SimImeiItem]{ SimKey: simKey, Code: consts.ApiInvokeException, Message: rechargeUnsupportedMessage("IMEI查询"), }) } return items, nil } func (s *sSimRecharge) QuerySimDataUsageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) { items := make(model.SimDataUsageMonthlyOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{ SimKey: simKey, Code: consts.ApiInvokeException, Message: rechargeUnsupportedMessage("单月流量查询"), }) } return items, nil } func (s *sSimRecharge) QuerySimDataUsageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) { items := make(model.SimDataUsageDailyOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{ SimKey: simKey, Code: consts.ApiInvokeException, Message: rechargeUnsupportedMessage("单日流量查询"), }) } return items, nil } func (s *sSimRecharge) QuerySimApnInfo(ctx context.Context, input *model.SimApnInfoInput) (model.SimApnInfoOutput, error) { items := make(model.SimApnInfoOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { items = append(items, &model.SimBaseRes[model.SimApnInfoItemList]{ SimKey: simKey, Code: consts.ApiInvokeException, Message: rechargeUnsupportedMessage("APN信息查询"), ResData: model.SimApnInfoItemList{ApnList: make([]model.SimApnInfoItem, 0)}, }) } return items, nil } // ChangeSimApnFunction 腾宇无 APN 开关语义(changeNet 是整卡开断网,不等价),不做映射 func (s *sSimRecharge) ChangeSimApnFunction(ctx context.Context, input *model.SimCardOperateChangeApnInput) (model.SimCardOperateChangeApnOutput, error) { items := make(model.SimCardOperateChangeApnOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { items = append(items, &model.SimBaseRes[model.SimCardOperateChangeApnItem]{ SimKey: simKey, Code: consts.ApiInvokeException, Message: rechargeUnsupportedMessage("APN功能开停"), ResData: model.SimCardOperateChangeApnItem{}, }) } return items, nil }