Files
iotlcm/upstream-chan/internal/logic/upstreamimpl/sim_dx_batch.go
T

642 lines
20 KiB
Go

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
}