package upstreamimpl import ( "context" "encoding/json" "net/http" "time" "fmt" "github.com/gogf/gf/v2/frame/g" "strings" "git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/net/gclient" ) const ( // gatewayBatchTimeoutConfigKey 限制单次批量转发的最长等待,防止上游异常时无限占用连接。 gatewayBatchTimeoutConfigKey = "gateway.batchTimeout" // defaultGatewayBatchTimeout 覆盖 100 张卡按单卡 10 秒预算的最坏耗时,并留有余量。 defaultGatewayBatchTimeout = 20 * time.Minute // defaultDataServiceTimeout 是数据开停转发的独立预算。 defaultDataServiceTimeout = 210 * time.Second ) // configureGatewayTimeout 让响应头等待与整次请求使用同一预算,避免默认30秒提前中断。 // upstream-chan 对批量操作按卡串行调用运营商接口,耗时随卡数线性增长; // 因此批量请求不能沿用 gclient 默认的 30 秒响应头超时,否则请求仍在办理就会被判为失败。 func configureGatewayTimeout(ctx context.Context, client *gclient.Client, path string, count int, perCard time.Duration) { if path == "/sim/change/sim-apn" || path == "/sim/query/sim-apn-capability" { client.SetContentType("application/json") } applyGatewayTimeout(client, path, count, perCard, configDuration(ctx, "gateway.dataServiceTimeout", defaultDataServiceTimeout), configDuration(ctx, gatewayBatchTimeoutConfigKey, defaultGatewayBatchTimeout)) } // applyGatewayTimeout 计算并应用单次转发预算:整次请求与响应头等待必须一致, // 否则响应头超时会在业务仍在办理时提前中断请求。 func applyGatewayTimeout(client *gclient.Client, path string, count int, perCard, dataServiceTimeout, batchTimeout time.Duration) { if count < 1 { count = 1 } if perCard <= 0 { perCard = 10 * time.Second } timeout := perCard * time.Duration(count) if path == "/sim/change/sim-apn" { timeout = dataServiceTimeout if timeout <= 0 { timeout = defaultDataServiceTimeout } } else if batchTimeout > 0 && timeout > batchTimeout { timeout = batchTimeout } client.SetTimeout(timeout) if transport, ok := client.Transport.(*http.Transport); ok { transport.ResponseHeaderTimeout = timeout } } // configDuration 读取时长配置,兼容写成 "20m" 字符串或秒数整数两种形式。 func configDuration(ctx context.Context, key string, fallback time.Duration) time.Duration { value := g.Cfg().MustGet(ctx, key) if value == nil || value.IsNil() { return fallback } if duration := value.Duration(); duration >= time.Second { return duration } if seconds := value.Int(); seconds > 0 { return time.Duration(seconds) * time.Second } return fallback } func readGatewayResponse(response *gclient.Response) (model.SimBaseResJson, error) { defer response.Close() body := response.ReadAll() var output struct { Code *int `json:"code"` Message string `json:"message"` Data model.SimBaseResJson `json:"data"` } if err := json.Unmarshal(body, &output); err != nil { return nil, gerror.Wrap(err, "解析网关响应失败") } if response.StatusCode < 200 || response.StatusCode >= 300 || output.Code == nil || *output.Code != 0 { message := strings.TrimSpace(output.Message) if message == "" { message = fmt.Sprintf("网关响应无效,HTTP状态码:%d", response.StatusCode) } return nil, gerror.New(message) } return output.Data, nil }