package mingqian import ( "context" "encoding/json" "math" "strconv" "strings" "time" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model" "github.com/gogf/gf/v2/errors/gerror" ) const TemplateKey = "sim_mingqian_v3" const statePath = "/simlist/get-state-by-number" const usagePath = "/simlist/get-flow-use-by-number" const packageUsagePath = "/use/get-card-use-by-number" type Config struct { API struct { BaseURL string `json:"baseUrl"` Timeout string `json:"timeout"` ResumePath string `json:"resumePath"` } `json:"api"` } type Adapter struct { client *Client resumePath string } var _ model.SimCardChannelI = (*Adapter)(nil) func New(cfg *model.SimChannelConfigItemCache) (*Adapter, error) { if cfg == nil || cfg.Template == nil || cfg.Channel == nil { return nil, gerror.New("mingqian: channel and template required") } config := Config{} config.API.BaseURL, config.API.Timeout = DefaultBaseURL, "10s" if strings.TrimSpace(cfg.Template.TempConfig) != "" { if err := json.Unmarshal([]byte(cfg.Template.TempConfig), &config); err != nil { return nil, gerror.New("mingqian: invalid template JSON") } } timeout, err := time.ParseDuration(config.API.Timeout) if err != nil || timeout <= 0 { return nil, gerror.New("mingqian: invalid api.timeout") } resume := strings.TrimSpace(config.API.ResumePath) if resume != "" && (!strings.HasPrefix(resume, "/simlist/") || strings.ContainsAny(resume, "?#") || strings.Contains(resume, "..") || resume == "/simlist/get-sim-switchoff-by-number") { return nil, gerror.New("mingqian: resumePath must be a confirmed distinct /simlist/ path") } client, err := NewClient(config.API.BaseURL, cfg.Channel.AppKey, cfg.Channel.AppSecret, timeout) if err != nil { return nil, err } return &Adapter{client: client, resumePath: resume}, nil } type number float64 func (value *number) UnmarshalJSON(raw []byte) error { text := strings.TrimSpace(string(raw)) if text == "null" { return gerror.New("mingqian: null numeric field") } if strings.HasPrefix(text, `"`) { if err := json.Unmarshal(raw, &text); err != nil { return err } } parsed, err := strconv.ParseFloat(text, 64) if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) { return gerror.New("mingqian: invalid numeric field") } *value = number(parsed) return nil } type card struct { ICCID string `json:"iccid"` MSISDN string `json:"msisdn"` State *int `json:"state"` StateName string `json:"statename"` Usage *number `json:"use"` PackageTotal *number `json:"totalpackage"` PackageUsage *number `json:"targetpackageusage"` Success *int `json:"issuccess"` Message string `json:"message"` } func identifier(key *model.SimKey) string { if key == nil { return "" } if value := strings.TrimSpace(key.ICCID); value != "" { return value } return strings.TrimSpace(key.MSISDN) } func query[T any](ctx context.Context, adapter *Adapter, keys []*model.SimKey, path string, limit int, convert func(card) (T, error)) []*model.SimBaseRes[T] { output := make([]*model.SimBaseRes[T], len(keys)) for index, key := range keys { output[index] = &model.SimBaseRes[T]{SimKey: key, Code: consts.ApiStatusError, Message: "mingqian: ICCID or MSISDN required (IMSI-only unsupported)"} } for start := 0; start < len(keys); start += limit { end := start + limit if end > len(keys) { end = len(keys) } numbers := make([]string, 0, end-start) seen := map[string]bool{} for _, key := range keys[start:end] { value := identifier(key) if value != "" && !seen[value] { numbers = append(numbers, value) seen[value] = true } } if len(numbers) == 0 { continue } var cards []card err := adapter.client.post(ctx, path, map[string]any{"numberArray": numbers}, &cards) for index := start; index < end; index++ { requested := identifier(keys[index]) if requested == "" { continue } item := output[index] if err != nil { item.Message = err.Error() continue } var matched *card duplicate := false for cardIndex := range cards { candidate := &cards[cardIndex] matches := false if strings.TrimSpace(keys[index].ICCID) != "" { matches = strings.TrimSpace(candidate.ICCID) == requested } else { matches = strings.TrimSpace(candidate.MSISDN) == requested } if matches { if matched != nil { duplicate = true break } matched = candidate } } if duplicate { item.Message = "mingqian: duplicate card in response" continue } if matched == nil { item.Message = "mingqian: requested card missing from response" continue } result, mapErr := convert(*matched) if mapErr != nil { item.Message = mapErr.Error() continue } item.Code = consts.ApiStatusSuccess item.Message = "" item.ResData = result } } return output } func cardStatus(state *int) (int, error) { if state == nil { return 0, gerror.New("mingqian: state missing") } switch *state { case 1: return 1, nil case 2: return 2, nil case 3: return 4, nil case 4: return 6, nil case 5: return 7, nil case 6: return 9, nil default: return 0, gerror.Newf("mingqian: unmapped card state %d", *state) } } func (adapter *Adapter) QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error) { return query(ctx, adapter, input.SimKeys, statePath, 50, func(item card) (model.SimBasicInfoItem, error) { state, err := cardStatus(item.State) if err != nil { return model.SimBasicInfoItem{}, err } return model.SimBasicInfoItem{ICCID: item.ICCID, MSISDN: item.MSISDN, SimStatus: strconv.Itoa(state), Remark: item.StateName}, nil }), nil } func (adapter *Adapter) QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error) { return query(ctx, adapter, input.SimKeys, statePath, 50, func(item card) (model.SimCardStatusItem, error) { state, err := cardStatus(item.State) return model.SimCardStatusItem{CardStatus: state}, err }), nil } func (adapter *Adapter) QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) (model.SimDataUsageOutput, error) { if input == nil { return nil, gerror.New("mingqian: input required") } output := query(ctx, adapter, input.SimKeys, usagePath, 50, func(item card) (model.SimDataUsageItem, error) { if item.Usage == nil || *item.Usage < 0 { return model.SimDataUsageItem{}, gerror.New("mingqian: current-month usage missing or unknown") } return model.SimDataUsageItem{DataUsage: float64(*item.Usage) / 1024}, nil }) packages, err := adapter.QueryPackageFlow(ctx, input) if err != nil { return nil, err } for index, item := range output { if packages[index].Code != consts.ApiStatusSuccess { item.ResData.PackageFlowError = packages[index].Message if item.Code == consts.ApiStatusSuccess { item.Message = "mingqian: current-month usage available; package flow unavailable (see packageFlowError)" } continue } item.ResData.PackageFlow = &packages[index].ResData } return output, nil } func (adapter *Adapter) ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) (model.SimCardStatusChangeOutput, error) { if input == nil { return nil, gerror.New("mingqian: input required") } path := "" switch strings.TrimSpace(input.ReqData.OperType) { case consts.CardStatusChangeOperTypeApplyStop: path = "/simlist/get-sim-switchoff-by-number" case consts.CardStatusChangeOperTypeApplyResume: path = adapter.resumePath default: return unsupported[model.SimCardStatusChangeItem](input.SimKeys, "invalid operType"), nil } if path == "" { return unsupported[model.SimCardStatusChangeItem](input.SimKeys, "resume endpoint unconfirmed; configure api.resumePath"), nil } return query(ctx, adapter, input.SimKeys, path, 20, func(item card) (model.SimCardStatusChangeItem, error) { if item.Success == nil || *item.Success != 1 { return model.SimCardStatusChangeItem{}, gerror.New("mingqian: card operation failed or success flag missing") } return model.SimCardStatusChangeItem{}, nil }), nil } func unsupported[T any](keys []*model.SimKey, capability string) []*model.SimBaseRes[T] { output := make([]*model.SimBaseRes[T], 0, len(keys)) for _, key := range keys { output = append(output, &model.SimBaseRes[T]{SimKey: key, Code: consts.ApiStatusError, Message: "mingqian: " + capability}) } return output }