package daihe import ( "bytes" "context" "crypto/md5" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/url" "sort" "strings" "time" ) type Config struct { BaseURL string AppID string AppSecret string Timeout time.Duration } type Client struct { baseURL, appID, appSecret string httpClient *http.Client } type Response struct { Code int `json:"code"` Msg string `json:"msg"` Data json.RawMessage `json:"data"` } func NewClient(cfg Config) *Client { timeout := cfg.Timeout if timeout <= 0 { timeout = 15 * time.Second } return &Client{baseURL: strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/"), appID: strings.TrimSpace(cfg.AppID), appSecret: cfg.AppSecret, httpClient: &http.Client{Timeout: timeout}} } func BuildSign(params map[string]string, appSecret string) string { keys := make([]string, 0, len(params)+1) for key := range params { if key != "sign" { keys = append(keys, key) } } keys = append(keys, "appSecret") sort.Strings(keys) var b strings.Builder for _, key := range keys { if key == "appSecret" { b.WriteString(key) b.WriteString(appSecret) continue } b.WriteString(key) b.WriteString(params[key]) } sum := md5.Sum([]byte(b.String())) return strings.ToUpper(hex.EncodeToString(sum[:])) } func (c *Client) request(ctx context.Context, method, path string, values map[string]string, body any, out any) error { if c.baseURL == "" || c.appID == "" || c.appSecret == "" { return fmt.Errorf("daihe base URL, appId and appSecret are required") } params := make(map[string]string, len(values)+1) for k, v := range values { if strings.TrimSpace(v) != "" { params[k] = v } } params["appId"] = c.appID params["sign"] = BuildSign(params, c.appSecret) var reader io.Reader var endpoint string if method == http.MethodGet { query := url.Values{} for k, v := range params { query.Set(k, v) } endpoint = c.baseURL + "/" + strings.TrimLeft(path, "/") + "?" + query.Encode() } else { payload := map[string]any{} if body != nil { raw, err := json.Marshal(body) if err != nil { return fmt.Errorf("marshal daihe request: %w", err) } if err := json.Unmarshal(raw, &payload); err != nil { return fmt.Errorf("marshal daihe request: %w", err) } } for k, v := range params { payload[k] = v } raw, err := json.Marshal(payload) if err != nil { return fmt.Errorf("marshal daihe request: %w", err) } reader = bytes.NewReader(raw) endpoint = c.baseURL + "/" + strings.TrimLeft(path, "/") } req, err := http.NewRequestWithContext(ctx, method, endpoint, reader) if err != nil { return err } req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("daihe request failed: %w", err) } defer resp.Body.Close() raw, err := io.ReadAll(io.LimitReader(resp.Body, 4*1024*1024+1)) if err != nil { return err } if len(raw) > 4*1024*1024 { return fmt.Errorf("daihe response is too large") } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("daihe HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(raw))) } var envelope Response if err := json.Unmarshal(raw, &envelope); err != nil { return fmt.Errorf("decode daihe response: %w", err) } if envelope.Code != 0 { return fmt.Errorf("daihe business code %d: %s", envelope.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("decode daihe data: %w", err) } } return nil } type CMCCToken struct { AppID string `json:"appid"` Token string `json:"token"` TokenDevice string `json:"token_device"` DeviceTTL string `json:"device_ttl"` } func (c *Client) QueryCMCCToken(ctx context.Context, cmccAppID string, out *CMCCToken) error { return c.request(ctx, http.MethodGet, "/sim/query/cmccToken", map[string]string{"cmccAppId": cmccAppID}, nil, out) } func (c *Client) QueryUsage(ctx context.Context, iccid, month string, out any) error { values := map[string]string{"iccid": iccid} if month != "" { values["month"] = month } return c.request(ctx, http.MethodGet, "/sim/query/monthUsed", values, nil, out) } func (c *Client) QueryStatus(ctx context.Context, iccid string, out any) error { return c.request(ctx, http.MethodGet, "/sim/query/status", map[string]string{"iccid": iccid}, nil, out) } func (c *Client) Activate(ctx context.Context, iccid string) error { return c.request(ctx, http.MethodPost, "/sim/setup/activate", map[string]string{"iccid": iccid}, nil, nil) } func (c *Client) Deactivate(ctx context.Context, iccid string) error { return c.request(ctx, http.MethodPost, "/sim/setup/deactivate", map[string]string{"iccid": iccid}, nil, nil) }