package upstreamimpl import ( "context" "crypto/md5" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" ) // henanXinjingClient is the direct HTTP client for the Henan Xinjing SIM // platform. Authentication is userId+apikey+times+sign where // sign = UPPERCASE(MD5("userId=&apikey=×=")). type henanXinjingClient struct { baseURL string userID string apiKey string httpClient *http.Client } // henanXinjingResponse matches the platform's standard envelope. // Code can be either a JSON number or a JSON string (the platform // returns both shapes for the same logical value), so keep it raw. type henanXinjingResponse struct { Code json.RawMessage `json:"code"` Msg string `json:"msg"` Data json.RawMessage `json:"data"` } func newHenanXinjingClient(baseURL, userID, apiKey string, timeout time.Duration) *henanXinjingClient { if timeout <= 0 { timeout = defaultUpstreamHTTPTimeout } return &henanXinjingClient{ baseURL: strings.TrimRight(strings.TrimSpace(baseURL), "/"), userID: strings.TrimSpace(userID), apiKey: strings.TrimSpace(apiKey), httpClient: &http.Client{Timeout: timeout}, } } // buildSign returns UPPERCASE(MD5(...)) over the document-defined form: // // userId=&apikey=×= func henanXinjingSign(userID, apiKey, times string) string { raw := "userId=" + userID + "&apikey=" + apiKey + "×=" + times sum := md5.Sum([]byte(raw)) return strings.ToUpper(hex.EncodeToString(sum[:])) } // decodeCode normalizes the response code. The platform sometimes returns // `"code":0` and sometimes `"code":"-2"`. Both are valid; we treat any // numeric zero (in either form) as success. func henanXinjingDecodeCode(raw json.RawMessage) (int, error) { if len(raw) == 0 { return -1, fmt.Errorf("missing code field") } var n int if err := json.Unmarshal(raw, &n); err == nil { return n, nil } var s string if err := json.Unmarshal(raw, &s); err != nil { return -1, fmt.Errorf("code is neither number nor string: %s", string(raw)) } s = strings.TrimSpace(s) if s == "" { return -1, fmt.Errorf("empty code string") } v, err := strconv.Atoi(s) if err != nil { return -1, fmt.Errorf("invalid code string %q: %w", s, err) } return v, nil } // get performs a signed GET request and unmarshals the `data` field into out // when out is non-nil. Business-layer errors (code != 0) become errors. func (c *henanXinjingClient) get(ctx context.Context, path string, params map[string]string, out any) error { if c == nil || c.baseURL == "" || c.userID == "" || c.apiKey == "" { return fmt.Errorf("henan xinjing: baseURL, userID and apiKey are required") } values := url.Values{} for k, v := range params { if strings.TrimSpace(v) != "" { values.Set(k, v) } } times := strconv.FormatInt(time.Now().Unix(), 10) values.Set("userId", c.userID) values.Set("times", times) values.Set("sign", henanXinjingSign(c.userID, c.apiKey, times)) endpoint := c.baseURL + "/" + strings.TrimLeft(path, "/") + "?" + values.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return fmt.Errorf("henan xinjing: build request %s: %w", path, err) } resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("henan xinjing: request %s: %w", path, err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("henan xinjing: read body %s: %w", path, err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("henan xinjing: HTTP %d from %s: %s", resp.StatusCode, path, string(body)) } var envelope henanXinjingResponse if err = json.Unmarshal(body, &envelope); err != nil { return fmt.Errorf("henan xinjing: decode envelope %s: %w (body=%s)", path, err, string(body)) } code, err := henanXinjingDecodeCode(envelope.Code) if err != nil { return fmt.Errorf("henan xinjing: %s: %w (body=%s)", path, err, string(body)) } if code != 0 { return fmt.Errorf("henan xinjing: %s failed: code=%d msg=%s", path, code, envelope.Msg) } if out != nil && len(envelope.Data) > 0 && string(envelope.Data) != "null" { if err = json.Unmarshal(envelope.Data, out); err != nil { return fmt.Errorf("henan xinjing: decode data %s: %w (data=%s)", path, err, string(envelope.Data)) } } return nil }