You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
156 lines
4.6 KiB
156 lines
4.6 KiB
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[:])
|
|
}
|
|
|