chore: 并入 iotlcm-upstream-chan 历史到 upstream-chan/(原提交 1e74497)
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
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=<id>&apikey=<key>×=<unix_seconds>")).
|
||||
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=<id>&apikey=<key>×=<unix_seconds>
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user