package client import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "sort" "strconv" "strings" "time" ) const defaultSimbossAPIBase = "https://api.simboss.com" type SimbossClient struct { appID string secret string apiBase string httpClient *http.Client } type SimbossDeviceDetail struct { Carrier string `json:"carrier"` Status string `json:"status"` DeviceStatus string `json:"deviceStatus"` ExpireDate string `json:"expireDate"` RatePlanID int64 `json:"ratePlanId"` RatePlanName string `json:"iratePlanName"` DataUsage float64 `json:"dataUsage"` TotalDataVolume float64 `json:"totalDataVolume"` RatePlanExpirationDate string `json:"ratePlanExpirationDate"` } type SimbossRatePlan struct { RatePlanID int64 `json:"ratePlanId"` Name string `json:"name"` Description string `json:"description"` DataVolume float64 `json:"dataVolume"` TimeLength int `json:"timeLength"` TimeUnit string `json:"timeUnit"` MaxRechargePeriod int `json:"maxRechargePeriod"` } type simbossResponse struct { Code string `json:"code"` Message string `json:"message"` Success bool `json:"success"` Data json.RawMessage `json:"data"` } func NewSimbossClient(appID, secret, apiBase string) (*SimbossClient, error) { if appID == "" || secret == "" { return nil, errors.New("SIMBOSS credentials are required") } if apiBase == "" { apiBase = defaultSimbossAPIBase } return &SimbossClient{ appID: appID, secret: secret, apiBase: strings.TrimRight(apiBase, "/"), httpClient: &http.Client{Timeout: 30 * time.Second}, }, nil } func (c *SimbossClient) GetDeviceDetail(iccid string) (*SimbossDeviceDetail, error) { var detail SimbossDeviceDetail if err := c.post("/2.0/device/detail", map[string]string{"iccid": iccid}, &detail); err != nil { return nil, err } return &detail, nil } func (c *SimbossClient) GetRatePlans(iccid string) ([]SimbossRatePlan, error) { var plans []SimbossRatePlan if err := c.post("/2.0/device/rateplans", map[string]string{"iccid": iccid}, &plans); err != nil { return nil, err } return plans, nil } func (c *SimbossClient) Recharge(iccid string, ratePlanID int64, months int, externalOrder string) (string, error) { if ratePlanID <= 0 || months <= 0 || externalOrder == "" { return "", errors.New("invalid SIMBOSS recharge request") } var sequence string err := c.post("/2.0/device/recharge", map[string]string{ "iccid": iccid, "ratePlanId": strconv.FormatInt(ratePlanID, 10), "month": strconv.Itoa(months), "externalOrder": externalOrder, }, &sequence) return sequence, err } func (c *SimbossClient) post(path string, params map[string]string, target any) error { params["appid"] = c.appID params["timestamp"] = strconv.FormatInt(time.Now().UnixMilli(), 10) params["sign"] = c.sign(params) form := url.Values{} for key, value := range params { form.Set(key, value) } req, err := http.NewRequest(http.MethodPost, c.apiBase+path, strings.NewReader(form.Encode())) if err != nil { return fmt.Errorf("create SIMBOSS request: %w", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("call SIMBOSS: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return fmt.Errorf("read SIMBOSS response: %w", err) } if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { return fmt.Errorf("SIMBOSS returned HTTP %d", resp.StatusCode) } var result simbossResponse if err := json.Unmarshal(body, &result); err != nil { return fmt.Errorf("decode SIMBOSS response: %w", err) } if result.Code != "0" && !result.Success { return fmt.Errorf("SIMBOSS rejected request: code=%s", result.Code) } if err := json.Unmarshal(result.Data, target); err != nil { return fmt.Errorf("decode SIMBOSS data: %w", err) } return nil } func (c *SimbossClient) sign(params map[string]string) string { keys := make([]string, 0, len(params)) for key := range params { keys = append(keys, key) } sort.Strings(keys) var builder strings.Builder for i, key := range keys { if i > 0 { builder.WriteByte('&') } builder.WriteString(key) builder.WriteByte('=') builder.WriteString(params[key]) } builder.WriteString(c.secret) sum := sha256.Sum256([]byte(builder.String())) return hex.EncodeToString(sum[:]) }