package upstreamimpl import ( "context" "encoding/json" "fmt" "math" "strconv" "strings" "time" "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" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) const ( dxBatchStatusLimit = 10000 dxBatchUsageLimit = 100 ) type telecomBatchKey struct { index int simKey *model.SimKey accessNumber string } type dxBatchRequest struct { AccessNumbers []string `json:"accessNumbers"` CustNumber string `json:"custNumber"` GroupID string `json:"groupId,omitempty"` SimStatus string `json:"simStatus,omitempty"` FlowDate string `json:"flowDate,omitempty"` } type dxBatchStatusResponse struct { Code any `json:"code"` Message string `json:"message"` Msg string `json:"msg"` Data struct { QrySimStatusList []dxBatchStatusItem `json:"qrySimStatusList"` StatusList []dxBatchStatusItem `json:"statusList"` } `json:"data"` } type dxBatchStatusItem struct { AccessNumber string `json:"accessNumber"` AccNum string `json:"accNum"` Result any `json:"result"` Iccid string `json:"iccid"` Imsi string `json:"imsi"` SimStatus any `json:"simStatus"` } type dxBatchFlowResponse struct { Code any `json:"code"` Message string `json:"message"` Msg string `json:"msg"` Data []dxBatchFlowItem `json:"data"` } type dxBatchFlowItem struct { AccessNumber string `json:"accessNumber"` AccNum string `json:"accNum"` Result any `json:"result"` Unit string `json:"unit"` FlowAmount any `json:"flowAmount"` FlowDate string `json:"flowDate"` FlowDay string `json:"flowDay"` } func parseDXCustNumber(ctx context.Context, channelConfig *model.SimChannelConfigItemCache) string { if channelConfig == nil || channelConfig.Channel == nil { return "" } var cfg struct { CustNumber string `json:"custNumber"` } if err := json.Unmarshal([]byte(channelConfig.Channel.ChanConfig), &cfg); err != nil { g.Log().Warningf(ctx, "parse telecom channel config failed, chanKey=%s err=%+v", channelConfig.Channel.ChanKey, err) return "" } return strings.TrimSpace(cfg.CustNumber) } func parseDXBaseURL(ctx context.Context, channelConfig *model.SimChannelConfigItemCache) string { if channelConfig == nil || channelConfig.Channel == nil { return "" } if channelConfig.Template != nil { var template struct { API struct { BaseURL string `json:"baseUrl"` } `json:"api"` } if err := json.Unmarshal([]byte(channelConfig.Template.TempConfig), &template); err == nil && strings.TrimSpace(template.API.BaseURL) != "" { return strings.TrimRight(strings.TrimSpace(template.API.BaseURL), "/") } } var cfg struct { BaseURL string `json:"baseURL"` URL string `json:"url"` } if err := json.Unmarshal([]byte(channelConfig.Channel.ChanConfig), &cfg); err != nil { g.Log().Warningf(ctx, "parse telecom channel config failed, chanKey=%s err=%+v", channelConfig.Channel.ChanKey, err) return "" } baseURL := strings.TrimSpace(firstNonEmpty(cfg.BaseURL, cfg.URL)) if baseURL == "" { baseURL = strings.TrimSpace(g.Cfg().MustGet(ctx, "dependencies.sim_dx_v1.url", dx.DefaultBaseURL).String()) } return strings.TrimRight(baseURL, "/") } func (s *sSimDX) QuerySimCardStatusBatch(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error) { return s.queryCardStatusBatch(ctx, input) } func (s *sSimDX) QuerySimDataUsageMonthlyBatch(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) { return s.queryDataUsageMonthlyBatch(ctx, input) } func (s *sSimDX) QuerySimDataUsageDailyBatch(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) { return s.queryDataUsageDailyBatch(ctx, input) } func (s *sSimDX) queryCardStatusBatch(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error) { if err := s.ensureDXBatchConfig(); err != nil { return nil, err } if input == nil || len(input.SimKeys) == 0 { return make(model.SimCardStatusOutput, 0), nil } results := make(model.SimCardStatusOutput, len(input.SimKeys)) pending := make([]telecomBatchKey, 0, len(input.SimKeys)) for i, simKey := range input.SimKeys { if simKey == nil || strings.TrimSpace(simKey.MSISDN) == "" { results[i] = &model.SimBaseRes[model.SimCardStatusItem]{ SimKey: simKey, Code: consts.ApiStatusError, Message: "MSISDN not found", } continue } pending = append(pending, telecomBatchKey{ index: i, simKey: simKey, accessNumber: strings.TrimSpace(simKey.MSISDN), }) } for _, chunk := range chunkTelecomBatchKeys(pending, dxBatchStatusLimit) { accessNumbers := make([]string, 0, len(chunk)) for _, item := range chunk { accessNumbers = append(accessNumbers, item.accessNumber) } resp, err := s.doDXBatchStatusPost(ctx, "/api/v1/prod/batchQrySimStatus", dxBatchRequest{ AccessNumbers: accessNumbers, CustNumber: s.custNumber, GroupID: "", SimStatus: "", }) if err != nil { for _, item := range chunk { results[item.index] = &model.SimBaseRes[model.SimCardStatusItem]{ SimKey: item.simKey, Code: consts.ApiStatusError, Message: err.Error(), } } continue } batchItems := resp.Data.QrySimStatusList if len(batchItems) == 0 { batchItems = resp.Data.StatusList } itemMap := make(map[string]dxBatchStatusItem, len(batchItems)) for _, item := range batchItems { accessNumber := telecomBatchAccessNumber(item.AccessNumber, item.AccNum) if accessNumber == "" { continue } itemMap[accessNumber] = item } for _, item := range chunk { batchItem, ok := itemMap[item.accessNumber] if !ok { results[item.index] = &model.SimBaseRes[model.SimCardStatusItem]{ SimKey: item.simKey, Code: consts.ApiStatusError, Message: "batch response missing accessNumber", } continue } itemResult := gconv.String(batchItem.Result) itemStatus := gconv.String(batchItem.SimStatus) if itemResult != "1" { results[item.index] = &model.SimBaseRes[model.SimCardStatusItem]{ SimKey: item.simKey, Code: telecomBatchResultCode(itemResult), Message: firstNonEmpty(resp.Message, resp.Msg, "telecom batch status query failed"), ResData: model.SimCardStatusItem{ CardStatus: 0, LastChangeDate: "", }, } continue } if itemStatus == "" { results[item.index] = &model.SimBaseRes[model.SimCardStatusItem]{ SimKey: item.simKey, Code: consts.ApiStatusError, Message: "simStatus missing in batch response", } continue } results[item.index] = &model.SimBaseRes[model.SimCardStatusItem]{ SimKey: item.simKey, Code: 0, Message: "", ResData: model.SimCardStatusItem{ CardStatus: convertDXBatchCardStatus(ctx, itemStatus), LastChangeDate: "", }, } } } return compactStatusBatchResults(results), nil } func (s *sSimDX) queryDataUsageMonthlyBatch(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) { if err := s.ensureDXBatchConfig(); err != nil { return nil, err } if input == nil || len(input.SimKeys) == 0 { return make(model.SimDataUsageMonthlyOutput, 0), nil } queryMonth := strings.TrimSpace(input.ReqData.QueryDate) if queryMonth == "" { return nil, fmt.Errorf("queryDate is required") } if _, err := time.Parse("200601", queryMonth); err != nil { return nil, fmt.Errorf("invalid queryDate: %s", queryMonth) } if !dxBatchMonthAvailable(queryMonth, time.Now()) { return nil, fmt.Errorf("电信月批量接口仅支持当月;历史月份请使用 /sim/query/sim-datausage-monthly") } results := make(model.SimDataUsageMonthlyOutput, len(input.SimKeys)) pending := make([]telecomBatchKey, 0, len(input.SimKeys)) for i, simKey := range input.SimKeys { if simKey == nil || strings.TrimSpace(simKey.MSISDN) == "" { results[i] = &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{ SimKey: simKey, Code: consts.ApiStatusError, Message: "MSISDN not found", ResData: model.SimDataUsageMonthlyOutputItem{}, } continue } pending = append(pending, telecomBatchKey{ index: i, simKey: simKey, accessNumber: strings.TrimSpace(simKey.MSISDN), }) } for _, chunk := range chunkTelecomBatchKeys(pending, dxBatchUsageLimit) { accessNumbers := make([]string, 0, len(chunk)) for _, item := range chunk { accessNumbers = append(accessNumbers, item.accessNumber) } resp, err := s.doDXBatchFlowPost(ctx, "/api/v1/batchQry/batchQryFlowByMonth", dxBatchRequest{ AccessNumbers: accessNumbers, CustNumber: s.custNumber, }) if err != nil { for _, item := range chunk { results[item.index] = &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{ SimKey: item.simKey, Code: consts.ApiStatusError, Message: err.Error(), ResData: model.SimDataUsageMonthlyOutputItem{DeadLine: queryMonth}, } } continue } itemMap := make(map[string]dxBatchFlowItem, len(resp.Data)) for _, item := range resp.Data { accessNumber := telecomBatchAccessNumber(item.AccessNumber, item.AccNum) if accessNumber == "" { continue } itemMap[accessNumber] = item } for _, item := range chunk { batchItem, ok := itemMap[item.accessNumber] if !ok { results[item.index] = &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{ SimKey: item.simKey, Code: consts.ApiStatusError, Message: "batch response missing accessNumber", ResData: model.SimDataUsageMonthlyOutputItem{}, } continue } itemResult := gconv.String(batchItem.Result) dataAmount, amountErr := parseDXBatchAmount(batchItem) if itemResult == "1" && amountErr != nil { results[item.index] = &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{SimKey: item.simKey, Code: consts.ApiStatusError, Message: amountErr.Error()} continue } if itemResult != "1" { results[item.index] = &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{ SimKey: item.simKey, Code: telecomBatchResultCode(itemResult), Message: firstNonEmpty(resp.Message, resp.Msg, "telecom batch monthly usage query failed"), ResData: model.SimDataUsageMonthlyOutputItem{ DeadLine: queryMonth, DataAmount: dataAmount, }, } continue } results[item.index] = &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{ SimKey: item.simKey, Code: 0, Message: "", ResData: model.SimDataUsageMonthlyOutputItem{ DeadLine: queryMonth, DataAmount: dataAmount, }, } } } return compactMonthlyBatchResults(results), nil } func (s *sSimDX) queryDataUsageDailyBatch(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) { if err := s.ensureDXBatchConfig(); err != nil { return nil, err } if input == nil || len(input.SimKeys) == 0 { return make(model.SimDataUsageDailyOutput, 0), nil } queryDate := strings.TrimSpace(input.ReqData.QueryDate) if queryDate == "" { return nil, fmt.Errorf("queryDate is required") } if _, err := time.Parse("20060102", queryDate); err != nil { return nil, fmt.Errorf("invalid queryDate: %s", queryDate) } if !dxBatchDayAvailable(queryDate, time.Now()) { return nil, fmt.Errorf("电信日批量接口仅支持最近7个已结束日期") } results := make(model.SimDataUsageDailyOutput, len(input.SimKeys)) pending := make([]telecomBatchKey, 0, len(input.SimKeys)) for i, simKey := range input.SimKeys { if simKey == nil || strings.TrimSpace(simKey.MSISDN) == "" { results[i] = &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{ SimKey: simKey, Code: consts.ApiStatusError, Message: "MSISDN not found", ResData: model.SimDataUsageDailyOutputItem{QueryDate: queryDate}, } continue } pending = append(pending, telecomBatchKey{ index: i, simKey: simKey, accessNumber: strings.TrimSpace(simKey.MSISDN), }) } for _, chunk := range chunkTelecomBatchKeys(pending, dxBatchUsageLimit) { accessNumbers := make([]string, 0, len(chunk)) for _, item := range chunk { accessNumbers = append(accessNumbers, item.accessNumber) } resp, err := s.doDXBatchFlowPost(ctx, "/api/v1/batchQry/batchQryFlowByDate", dxBatchRequest{ AccessNumbers: accessNumbers, CustNumber: s.custNumber, FlowDate: queryDate, }) if err != nil { for _, item := range chunk { results[item.index] = &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{ SimKey: item.simKey, Code: consts.ApiStatusError, Message: err.Error(), ResData: model.SimDataUsageDailyOutputItem{QueryDate: queryDate}, } } continue } itemMap := make(map[string]dxBatchFlowItem, len(resp.Data)) for _, item := range resp.Data { accessNumber := telecomBatchAccessNumber(item.AccessNumber, item.AccNum) if accessNumber == "" { continue } itemMap[accessNumber] = item } for _, item := range chunk { batchItem, ok := itemMap[item.accessNumber] if !ok { results[item.index] = &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{ SimKey: item.simKey, Code: consts.ApiStatusError, Message: "batch response missing accessNumber", ResData: model.SimDataUsageDailyOutputItem{QueryDate: queryDate}, } continue } itemResult := gconv.String(batchItem.Result) dataAmount, amountErr := parseDXBatchAmount(batchItem) if itemResult == "1" && amountErr != nil { results[item.index] = &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{SimKey: item.simKey, Code: consts.ApiStatusError, Message: amountErr.Error()} continue } batchQueryDate := firstNonEmpty(strings.TrimSpace(batchItem.FlowDate), strings.TrimSpace(batchItem.FlowDay)) if batchQueryDate == "" { batchQueryDate = queryDate } if batchQueryDate != queryDate { results[item.index] = &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{SimKey: item.simKey, Code: consts.ApiStatusError, Message: "batch response date mismatched"} continue } if itemResult != "1" { results[item.index] = &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{ SimKey: item.simKey, Code: telecomBatchResultCode(itemResult), Message: firstNonEmpty(resp.Message, resp.Msg, "telecom batch daily usage query failed"), ResData: model.SimDataUsageDailyOutputItem{ QueryDate: batchQueryDate, DataAmount: dataAmount, }, } continue } results[item.index] = &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{ SimKey: item.simKey, Code: 0, Message: "", ResData: model.SimDataUsageDailyOutputItem{ QueryDate: batchQueryDate, DataAmount: dataAmount, }, } } } return compactDailyBatchResults(results), nil } func (s *sSimDX) ensureDXBatchConfig() error { if strings.TrimSpace(s.custNumber) == "" { return fmt.Errorf("telecom custNumber not configured") } return nil } var dxBatchLocation = time.FixedZone("UTC+8", 8*60*60) func parseDXBatchAmount(item dxBatchFlowItem) (float64, error) { amount, err := strconv.ParseFloat(strings.TrimSpace(gconv.String(item.FlowAmount)), 64) if err != nil || math.IsNaN(amount) || math.IsInf(amount, 0) || amount < 0 { return 0, fmt.Errorf("invalid telecom batch flowAmount") } if unit := strings.ToUpper(strings.TrimSpace(item.Unit)); unit != "" && unit != "MB" { return 0, fmt.Errorf("unexpected telecom batch usage unit: %s", unit) } return amount, nil } func dailyBatchOutputSuccessful(input *model.SimDataUsageDailyInput, output model.SimDataUsageDailyOutput) bool { if input == nil || len(output) != len(input.SimKeys) { return false } queryDate := strings.TrimSpace(input.ReqData.QueryDate) for _, item := range output { if item == nil || item.Code != 0 || item.ResData.QueryDate != queryDate { return false } } return true } func dxBatchDayAvailable(queryDate string, now time.Time) bool { date, err := time.ParseInLocation("20060102", strings.TrimSpace(queryDate), dxBatchLocation) if err != nil { return false } now = now.In(dxBatchLocation) today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, dxBatchLocation) // CTIOT_5GCMP_UQ013 only accepts one completed day within the latest 7 days. return !date.Before(today.AddDate(0, 0, -7)) && date.Before(today) } func dxBatchMonthAvailable(queryMonth string, now time.Time) bool { month, err := time.ParseInLocation("200601", strings.TrimSpace(queryMonth), dxBatchLocation) if err != nil { return false } now = now.In(dxBatchLocation) // CTIOT_5GCMP_UQ014 is explicitly a current-month batch endpoint. return month.Year() == now.Year() && month.Month() == now.Month() } func (s *sSimDX) doDXBatchPost(ctx context.Context, endpoint string, req dxBatchRequest) ([]byte, error) { body, err := json.Marshal(req) if err != nil { return nil, err } return s.client.DoPostJSONRequest(ctx, endpoint, body) } func (s *sSimDX) doDXBatchStatusPost(ctx context.Context, endpoint string, req dxBatchRequest) (*dxBatchStatusResponse, error) { respBody, err := s.doDXBatchPost(ctx, endpoint, req) if err != nil { return nil, err } var resp dxBatchStatusResponse if err := json.Unmarshal(respBody, &resp); err != nil { return nil, err } if gconv.String(resp.Code) != "0" { return nil, fmt.Errorf("telecom batch status request failed: code=%s message=%s", gconv.String(resp.Code), firstNonEmpty(resp.Message, resp.Msg)) } return &resp, nil } func (s *sSimDX) doDXBatchFlowPost(ctx context.Context, endpoint string, req dxBatchRequest) (*dxBatchFlowResponse, error) { respBody, err := s.doDXBatchPost(ctx, endpoint, req) if err != nil { return nil, err } var resp dxBatchFlowResponse if err := json.Unmarshal(respBody, &resp); err != nil { return nil, err } if gconv.String(resp.Code) != "0" { return nil, fmt.Errorf("telecom batch flow request failed: code=%s message=%s", gconv.String(resp.Code), firstNonEmpty(resp.Message, resp.Msg)) } return &resp, nil } func chunkTelecomBatchKeys(items []telecomBatchKey, size int) [][]telecomBatchKey { if size <= 0 { size = 1 } if len(items) == 0 { return nil } chunks := make([][]telecomBatchKey, 0, (len(items)+size-1)/size) for start := 0; start < len(items); start += size { end := start + size if end > len(items) { end = len(items) } chunks = append(chunks, items[start:end]) } return chunks } func telecomBatchAccessNumber(values ...string) string { for _, value := range values { if value = strings.TrimSpace(value); value != "" { return value } } return "" } func telecomBatchResultCode(result string) int { result = strings.TrimSpace(result) if result == "" { return consts.ApiStatusError } return gconv.Int(result) } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return value } } return "" } func compactStatusBatchResults(items model.SimCardStatusOutput) model.SimCardStatusOutput { output := make(model.SimCardStatusOutput, 0, len(items)) for _, item := range items { if item != nil { output = append(output, item) } } return output } func compactMonthlyBatchResults(items model.SimDataUsageMonthlyOutput) model.SimDataUsageMonthlyOutput { output := make(model.SimDataUsageMonthlyOutput, 0, len(items)) for _, item := range items { if item != nil { output = append(output, item) } } return output } func compactDailyBatchResults(items model.SimDataUsageDailyOutput) model.SimDataUsageDailyOutput { output := make(model.SimDataUsageDailyOutput, 0, len(items)) for _, item := range items { if item != nil { output = append(output, item) } } return output }