package upstream import ( "context" "encoding/json" "fmt" "net/http" "os" "strings" "time" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gclient" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model" ) const inboundBasicInfoClientNameEnv = "UPSTREAM_INBOUND_BASIC_INFO_CLIENT_NAME" const upstreamProxyMaxTimeoutConfigKey = "upstream.proxy.maxTimeout" type Client struct { currentUsageClientName string currentUsageTimeout time.Duration inboundBasicInfoClientName string imeiClientName string imeiTimeout time.Duration } type response[T any] struct { Code int `json:"code"` Message string `json:"message"` Data []model.SimBaseRes[T] `json:"data"` } func New() *Client { return &Client{} } func (c *Client) QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) ([]model.SimBaseRes[model.SimBasicInfoItem], error) { address, err := c.inboundBasicInfoAddress(ctx) if err != nil { return nil, err } return requestAllWithAddress[model.SimBasicInfoItem](ctx, "/sim/query/sim-basic-info", input, len(input.SimKeys), address) } // NewWithCurrentUsageClient keeps the full-sync current-usage call independent // from the configured upstream.proxy endpoint. func NewWithCurrentUsageClient(address string, timeout ...time.Duration) *Client { var currentUsageTimeout time.Duration if len(timeout) > 0 { currentUsageTimeout = timeout[0] } return &Client{currentUsageClientName: address, currentUsageTimeout: currentUsageTimeout} } func NewWithInboundBasicInfoClient(address string) *Client { return &Client{inboundBasicInfoClientName: address} } // NewWithImeiClient pins the IMEI full-sync path to an explicit chan endpoint // and timeout. An empty address falls back to the shared upstream.proxy // configuration, which is only intended for local runs. func NewWithImeiClient(address string, timeout ...time.Duration) *Client { var imeiTimeout time.Duration if len(timeout) > 0 { imTimeout := timeout[0] if imTimeout > 0 { imeiTimeout = imTimeout } } return &Client{imeiClientName: address, imeiTimeout: imeiTimeout} } func (c *Client) inboundBasicInfoAddress(ctx context.Context) (string, error) { if strings.TrimSpace(c.inboundBasicInfoClientName) != "" { return strings.TrimSpace(c.inboundBasicInfoClientName), nil } if address := strings.TrimSpace(os.Getenv(inboundBasicInfoClientNameEnv)); address != "" { return address, nil } config, ok, err := optionalUpstreamConfig(ctx, "upstream.gateway") if err != nil { return "", err } if !ok || strings.TrimSpace(config.Address) == "" { return "", gerror.Newf("upstream gateway address is empty, configure upstream.gateway.address or %s", inboundBasicInfoClientNameEnv) } return strings.TrimSpace(config.Address), nil } // QuerySimDataUsage calls Chan's current-month real-time usage endpoint. The // response keeps one item per requested card so the caller can classify // per-card errors without reissuing successful cards. func (c *Client) QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) ([]model.SimBaseRes[model.SimDataUsageItem], error) { return requestAllWithAddressTimeout[model.SimDataUsageItem](ctx, "/sim/query/sim-datausage", input, len(input.SimKeys), c.currentUsageClientName, c.currentUsageTimeout) } // QuerySimCardStatus queries the carrier status for the supplied cards. The // status queue currently sends one card per request, while retaining the // response shape needed for future batch support. func (c *Client) QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) ([]model.SimBaseRes[model.SimCardStatusItem], error) { return requestAll[model.SimCardStatusItem](ctx, "/sim/query/sim-status", input, len(input.SimKeys)) } // QuerySimImei queries the terminal IMEI for the supplied cards. The IMEI queue // sends one card per request. // // It deliberately uses requestAll rather than request: a per-item carrier // failure (unsupported template, missing value) must reach the handler as an // item with a non-zero Code, not be silently dropped from the slice. func (c *Client) QuerySimImei(ctx context.Context, input *model.SimImeiInput) ([]model.SimBaseRes[model.SimImeiItem], error) { return requestAllWithAddressTimeout[model.SimImeiItem](ctx, "/sim/query/sim-imei", input, len(input.SimKeys), c.imeiClientName, c.imeiTimeout) } func (c *Client) QuerySimDatausageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) ([]model.SimBaseRes[model.SimDataUsageDailyOutputItem], error) { return requestAll[model.SimDataUsageDailyOutputItem](ctx, "/sim/query/sim-datausage-daily", input, len(input.SimKeys)) } func (c *Client) QuerySimDatausageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) ([]model.SimBaseRes[model.SimDataUsageMonthlyOutputItem], error) { return requestAll[model.SimDataUsageMonthlyOutputItem](ctx, "/sim/query/sim-datausage-monthly", input, len(input.SimKeys)) } // ChangeSimStatus returns one result for every upstream item, including failed items. // Lifecycle batch execution needs those failures to be persisted to the matching detail. func (c *Client) ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) ([]model.SimBaseRes[model.SimCardStatusChangeOutputItem], error) { return requestAll[model.SimCardStatusChangeOutputItem](ctx, "/sim/change/sim-status", input, len(input.SimKeys)) } // ChangeSimApnFunction returns one result for every upstream item, including failed items. // Data service batch execution needs those failures to be persisted to the matching detail. func (c *Client) ChangeSimApnFunction(ctx context.Context, input *model.SimCardApnChangeInput) ([]model.SimBaseRes[model.SimCardApnChangeOutputItem], error) { return requestAll[model.SimCardApnChangeOutputItem](ctx, "/sim/change/sim-apn", input, len(input.SimKeys)) } func request[T any](ctx context.Context, path string, input any, itemCount int) ([]model.SimBaseRes[T], error) { items, err := requestAll[T](ctx, path, input, itemCount) if err != nil { return nil, err } successItems := make([]model.SimBaseRes[T], 0, len(items)) for _, item := range items { if item.Code == 0 { successItems = append(successItems, item) } } return successItems, nil } func requestAll[T any](ctx context.Context, path string, input any, itemCount int) ([]model.SimBaseRes[T], error) { return requestAllWithAddress[T](ctx, path, input, itemCount, "") } func requestAllWithAddress[T any](ctx context.Context, path string, input any, itemCount int, address string) ([]model.SimBaseRes[T], error) { return requestAllWithAddressTimeout[T](ctx, path, input, itemCount, address, 0) } func requestAllWithAddressTimeout[T any](ctx context.Context, path string, input any, itemCount int, address string, timeoutOverride time.Duration) ([]model.SimBaseRes[T], error) { config, err := upstreamConfig(ctx, "upstream.proxy") if err != nil { return nil, err } if address == "" { address = config.Address } if address == "" { return nil, gerror.New("upstream proxy address is empty") } if itemCount < 1 { itemCount = 1 } client := g.Client() timeout := timeoutOverride if timeout <= 0 { timeout = config.Timeout } if timeout <= 0 { timeout = 10 * time.Second } if itemCount > 1 { timeout *= time.Duration(itemCount) } if maxTimeout := g.Cfg().MustGet(ctx, upstreamProxyMaxTimeoutConfigKey, 2*time.Minute).Duration(); maxTimeout > 0 && timeout > maxTimeout { timeout = maxTimeout } setClientTimeout(client, timeout) clientResponse, err := client.Post(ctx, fmt.Sprintf("http://%s%s", address, path), input) if err != nil { return nil, gerror.Wrap(err, "request upstream failed") } defer clientResponse.Close() return decodeResponse[T](clientResponse.ReadAll()) } func optionalUpstreamConfig(ctx context.Context, key string) (model.UpstreamChanConfig, bool, error) { configValue, err := g.Cfg().Get(ctx, key) if err != nil { return model.UpstreamChanConfig{}, false, gerror.Wrapf(err, "read upstream configuration failed: %s", key) } if configValue == nil || configValue.IsNil() { return model.UpstreamChanConfig{}, false, nil } config := model.UpstreamChanConfig{} if err = configValue.Scan(&config); err != nil { return model.UpstreamChanConfig{}, false, gerror.Wrapf(err, "parse upstream configuration failed: %s", key) } return config, true, nil } func upstreamConfig(ctx context.Context, key string) (model.UpstreamChanConfig, error) { config, ok, err := optionalUpstreamConfig(ctx, key) if err != nil { return model.UpstreamChanConfig{}, err } if !ok { return model.UpstreamChanConfig{}, gerror.Newf("upstream configuration is missing: %s", key) } return config, nil } func decodeResponse[T any](body []byte) ([]model.SimBaseRes[T], error) { output := response[T]{} if err := json.Unmarshal(body, &output); err != nil { return nil, gerror.Wrap(err, "parse upstream response failed") } if output.Code != 0 { return nil, gerror.New(output.Message) } return output.Data, nil } func setClientTimeout(client *gclient.Client, timeout time.Duration) { client.SetTimeout(timeout) if transport, ok := client.Transport.(*http.Transport); ok { transport.ResponseHeaderTimeout = timeout } }