Files
iotlcm/upstream-chan/internal/logic/upstreamimpl/sim_tibiot.go
T

1136 lines
38 KiB
Go

package upstreamimpl
import (
"bytes"
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
"golang.org/x/sync/singleflight"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/service"
)
const (
tibiotDefaultBaseURL = "https://api.tibiot.com"
tibiotTimeLayout = "20060102150405"
tibiotUsageCacheTTL = 30 * time.Second
)
var tibiotEast8 = time.FixedZone("UTC+8", 8*60*60)
type sSimTibiot struct {
channelConfig *model.SimChannelConfigItemCache
baseURL string
userName string
password string
http *http.Client
usageCache sync.Map
usageFlight singleflight.Group
}
type tibiotUsageCacheEntry struct {
Usage float64
UpdatedAt string
ExpiresAt time.Time
}
type tibiotUsageResult struct {
Usage float64
Cached bool
UpdatedAt string
BusinessError *tibiotResponse
}
type tibiotResponse struct {
Status any `json:"status"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
}
type tibiotCardInfo struct {
ICCID string `json:"iccid"`
IMSI string `json:"imsi"`
MSISDN string `json:"msisdn"`
IMEI string `json:"imei"`
CardStatus any `json:"cardStatus"`
PackageName string `json:"packageName"`
TotalFlow string `json:"totalFlow"`
CardFlow string `json:"cardFlow"`
LeftFlow string `json:"leftFlow"`
OpenCardTime string `json:"openCardTime"`
LastActivateTime string `json:"lastActivateTime"`
ActivateTime string `json:"activateTime"`
PackageTime string `json:"packageTime"`
Remark string `json:"remark"`
ChannelID string `json:"channelId"`
PackageID string `json:"packageId"`
CardType any `json:"cardType"`
PoolID string `json:"poolId"`
}
type tibiotOnlineInfo struct {
ICCID string `json:"iccid"`
MSISDN string `json:"msisdn"`
CardStatus string `json:"cardStatus"`
NetworkStatus string `json:"netWorkStatus"`
EquipmentStatus string `json:"equipmentStatus"`
OnlineStatus string `json:"onlineStatus"`
SeparationStatus string `json:"separationStatus"`
NetworkType string `json:"networkType"`
RegionLimitStatus string `json:"regionLimitStatus"`
APN string `json:"apn"`
IP string `json:"ip"`
IMEI string `json:"imei"`
}
type tibiotMonthUsage struct {
Years string `json:"years"`
CardFlow string `json:"cardFlow"`
ICCID string `json:"iccid"`
MSISDN string `json:"msisdn"`
SyncTime string `json:"syncTime"`
}
type tibiotDailyUsage struct {
SyncStatus string `json:"syncStatus"`
CardFlow string `json:"cardFlow"`
CreateTime string `json:"createTime"`
SyncTime string `json:"syncTime"`
ICCID string `json:"iccid"`
IMSI string `json:"imsi"`
MSISDN string `json:"msisdn"`
QueryDate string `json:"queryTime"`
}
type tibiotBatchCardData struct {
PageNum int `json:"pageNum"`
PageSize int `json:"pageSize"`
TotalCount int `json:"totalCount"`
PageCount int `json:"pageCount"`
Records []tibiotCardInfo `json:"records"`
}
func NewSimTibiot() *sSimTibiot {
return &sSimTibiot{}
}
func init() {
service.UpstreamAPI().RegisterTemp("sim_tibiot_v1", NewSimTibiot())
}
func (s *sSimTibiot) NewTemp(ctx context.Context, channelConfig *model.SimChannelConfigItemCache) model.SimCardChannelI {
baseURL := tibiotDefaultBaseURL
if channelConfig != nil && channelConfig.Template != nil {
var cfg struct {
API struct {
BaseURL string `json:"baseUrl"`
} `json:"api"`
}
if err := json.Unmarshal([]byte(channelConfig.Template.TempConfig), &cfg); err != nil {
g.Log().Warningf(ctx, "parse Tibiot template config failed: %v", err)
} else if strings.TrimSpace(cfg.API.BaseURL) != "" {
baseURL = strings.TrimRight(strings.TrimSpace(cfg.API.BaseURL), "/")
}
}
var credentials struct {
UserName string `json:"userName"`
Password string `json:"password"`
}
if channelConfig != nil && channelConfig.Channel != nil {
if err := json.Unmarshal([]byte(channelConfig.Channel.ChanConfig), &credentials); err != nil {
g.Log().Warningf(ctx, "parse Tibiot channel config failed, chanKey=%s err=%v", channelConfig.Channel.ChanKey, err)
}
}
return &sSimTibiot{
channelConfig: channelConfig,
baseURL: baseURL,
userName: strings.TrimSpace(credentials.UserName),
password: credentials.Password,
http: &http.Client{Timeout: resolveUpstreamHTTPTimeout(ctx, templateConfigJSON(channelConfig))},
}
}
func tibiotSignature(password, tKey string) string {
first := md5.Sum([]byte(password))
firstText := hex.EncodeToString(first[:])
second := md5.Sum([]byte(firstText + tKey))
return hex.EncodeToString(second[:])
}
func tibiotAuthFields(userName, password string, now time.Time) map[string]any {
tKey := now.In(tibiotEast8).Format(tibiotTimeLayout)
return map[string]any{
"userName": userName,
"passWord": tibiotSignature(password, tKey),
"tKey": tKey,
}
}
func (s *sSimTibiot) post(ctx context.Context, endpoint string, params map[string]any) (tibiotResponse, error) {
if s == nil {
return tibiotResponse{}, fmt.Errorf("Tibiot client is nil")
}
if strings.TrimSpace(s.userName) == "" || s.password == "" {
return tibiotResponse{}, fmt.Errorf("Tibiot credentials are not configured")
}
payload := tibiotAuthFields(s.userName, s.password, time.Now())
for key, value := range params {
payload[key] = value
}
body, err := json.Marshal(payload)
if err != nil {
return tibiotResponse{}, fmt.Errorf("marshal Tibiot request: %w", err)
}
url := strings.TrimRight(s.baseURL, "/") + "/" + strings.TrimLeft(endpoint, "/")
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return tibiotResponse{}, fmt.Errorf("create Tibiot request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := s.http
if client == nil {
client = &http.Client{Timeout: defaultUpstreamHTTPTimeout}
}
if endpoint == "/api/v1/card/cardNetWork" {
transport := http.DefaultTransport.(*http.Transport).Clone()
if original, ok := client.Transport.(*http.Transport); ok {
transport = original.Clone()
}
transport.DisableKeepAlives = true
defer transport.CloseIdleConnections()
writeClient := *client
writeClient.Transport = transport
writeClient.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
client = &writeClient
}
resp, err := client.Do(req)
if err != nil {
return tibiotResponse{}, fmt.Errorf("Tibiot request %s: %w", endpoint, err)
}
defer resp.Body.Close()
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return tibiotResponse{}, fmt.Errorf("read Tibiot response %s: %w", endpoint, err)
}
rawResponse := strings.TrimSpace(string(responseBody))
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
g.Log().Warningf(ctx, "Tibiot HTTP error endpoint=%s status=%d response=%s", endpoint, resp.StatusCode, rawResponse)
return tibiotResponse{}, fmt.Errorf("Tibiot HTTP status %d", resp.StatusCode)
}
var output tibiotResponse
if err := json.Unmarshal(responseBody, &output); err != nil {
g.Log().Warningf(ctx, "decode Tibiot response failed endpoint=%s response=%s err=%v", endpoint, rawResponse, err)
return tibiotResponse{}, fmt.Errorf("decode Tibiot response %s: %w", endpoint, err)
}
if !tibiotSuccess(output) {
g.Log().Warningf(ctx, "Tibiot business error endpoint=%s status=%d message=%s response=%s", endpoint, tibiotCode(output), output.Message, rawResponse)
} else {
g.Log().Infof(ctx, "Tibiot response endpoint=%s status=%d response=%s", endpoint, tibiotCode(output), rawResponse)
}
return output, nil
}
func tibiotCode(resp tibiotResponse) int {
return gconv.Int(resp.Status)
}
func tibiotSuccess(resp tibiotResponse) bool {
return tibiotCode(resp) == 0
}
func tibiotCardIdentifier(simKey *model.SimKey) (string, string) {
if simKey == nil {
return "", ""
}
if value := strings.TrimSpace(simKey.ICCID); value != "" {
return "iccid", value
}
if value := strings.TrimSpace(simKey.MSISDN); value != "" {
return "msisdn", value
}
if value := strings.TrimSpace(simKey.IMSI); value != "" {
return "imsi", value
}
return "", ""
}
func tibiotParams(simKey *model.SimKey) (map[string]any, error) {
name, value := tibiotCardIdentifier(simKey)
if name == "" {
return nil, fmt.Errorf("missing iccid/msisdn/imsi")
}
return map[string]any{name: value}, nil
}
func tibiotUnsupported[T any](simKeys []*model.SimKey, message string) []*model.SimBaseRes[T] {
items := make([]*model.SimBaseRes[T], 0, len(simKeys))
for _, simKey := range simKeys {
items = append(items, &model.SimBaseRes[T]{
SimKey: simKey,
Code: consts.ApiInvokeException,
Message: message,
})
}
return items
}
func tibiotError[T any](simKey *model.SimKey, err error) *model.SimBaseRes[T] {
return &model.SimBaseRes[T]{SimKey: simKey, Code: consts.ApiStatusError, Message: err.Error()}
}
func tibiotBusinessError[T any](simKey *model.SimKey, resp tibiotResponse) *model.SimBaseRes[T] {
return &model.SimBaseRes[T]{
SimKey: simKey,
Code: tibiotCode(resp),
ErrCode: strconv.Itoa(tibiotCode(resp)),
Message: resp.Message,
}
}
func decodeTibiotData[T any](resp tibiotResponse, target *T) error {
if len(resp.Data) == 0 || string(resp.Data) == "null" {
return fmt.Errorf("Tibiot response data is empty")
}
if err := json.Unmarshal(resp.Data, target); err != nil {
return fmt.Errorf("decode Tibiot response data: %w", err)
}
return nil
}
func decodeTibiotBatchCards(resp tibiotResponse) (records []tibiotCardInfo, pageCount int, err error) {
var pageData tibiotBatchCardData
if err = decodeTibiotData(resp, &pageData); err == nil && pageData.Records != nil {
return pageData.Records, pageData.PageCount, nil
}
var list []tibiotCardInfo
if listErr := decodeTibiotData(resp, &list); listErr != nil {
if err != nil {
return nil, 0, err
}
return nil, 0, listErr
}
return list, 1, nil
}
func parseTibiotFlow(value string) float64 {
value = strings.ToUpper(strings.TrimSpace(value))
if value == "" {
return 0
}
for _, unit := range []struct {
suffix string
factor float64
}{
{suffix: "EB", factor: 1024 * 1024 * 1024 * 1024},
{suffix: "PB", factor: 1024 * 1024 * 1024},
{suffix: "TB", factor: 1024 * 1024},
{suffix: "GB", factor: 1024},
{suffix: "MB", factor: 1},
{suffix: "KB", factor: 1.0 / 1024},
} {
if strings.HasSuffix(value, unit.suffix) {
return gconv.Float64(strings.TrimSpace(strings.TrimSuffix(value, unit.suffix))) * unit.factor
}
}
return gconv.Float64(value)
}
func tibiotSessionStatus(value string) string {
switch strings.ToUpper(strings.TrimSpace(value)) {
case "1", "ONLINE":
return consts.CardSessionStatusOnline
case "2", "OFFLINE":
return consts.CardSessionStatusOffline
default:
return consts.CardSessionStatusUnknown
}
}
func tibiotDeviceStatus(value string) string {
switch strings.ToUpper(strings.TrimSpace(value)) {
case "1", "ON", "ONLINE":
return "1"
case "2", "0", "OFF", "OFFLINE":
return "0"
default:
return consts.CardSessionStatusUnknown
}
}
func (s *sSimTibiot) QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error) {
if input == nil {
return make(model.SimBasicInfoOutput, 0), nil
}
items := make(model.SimBasicInfoOutput, 0, len(input.SimKeys))
for _, simKey := range input.SimKeys {
params, err := tibiotParams(simKey)
if err != nil {
items = append(items, tibiotError[model.SimBasicInfoItem](simKey, err))
continue
}
resp, err := s.post(ctx, "/api/v1/card/queryCardInfo", params)
if err != nil {
items = append(items, tibiotError[model.SimBasicInfoItem](simKey, err))
continue
}
if !tibiotSuccess(resp) {
items = append(items, tibiotBusinessError[model.SimBasicInfoItem](simKey, resp))
continue
}
var data tibiotCardInfo
if err := decodeTibiotData(resp, &data); err != nil {
items = append(items, tibiotError[model.SimBasicInfoItem](simKey, err))
continue
}
items = append(items, &model.SimBaseRes[model.SimBasicInfoItem]{
SimKey: simKey,
Code: consts.ApiStatusSuccess,
Message: resp.Message,
ResData: model.SimBasicInfoItem{
ICCID: data.ICCID,
IMSI: data.IMSI,
MSISDN: data.MSISDN,
ActiveDate: firstNonEmpty(data.ActivateTime, data.LastActivateTime),
OpenDate: firstNonEmpty(data.OpenCardTime, data.ActivateTime),
Remark: data.Remark,
SimStatus: strconv.Itoa(convertTibiotCardStatus(context.Background(), gconv.Int(data.CardStatus))),
MainOfferName: data.PackageName,
},
})
}
return items, nil
}
func (s *sSimTibiot) QuerySimSession(ctx context.Context, input *model.SimSessionInput) (model.SimSessionOutput, error) {
if input == nil {
return make(model.SimSessionOutput, 0), nil
}
items := make(model.SimSessionOutput, 0, len(input.SimKeys))
for _, simKey := range input.SimKeys {
params, err := tibiotParams(simKey)
if err != nil {
items = append(items, tibiotError[model.SimSessionItem](simKey, err))
continue
}
resp, err := s.post(ctx, "/api/v1/card/cardOnlineInformation", params)
if err != nil {
items = append(items, tibiotError[model.SimSessionItem](simKey, err))
continue
}
if !tibiotSuccess(resp) {
items = append(items, tibiotBusinessError[model.SimSessionItem](simKey, resp))
continue
}
var data tibiotOnlineInfo
if err := decodeTibiotData(resp, &data); err != nil {
items = append(items, tibiotError[model.SimSessionItem](simKey, err))
continue
}
items = append(items, &model.SimBaseRes[model.SimSessionItem]{
SimKey: simKey,
Code: consts.ApiStatusSuccess,
Message: resp.Message,
ResData: model.SimSessionItem{
Imei: data.IMEI,
Rat: data.NetworkType,
Ipv4: data.IP,
ApnId: data.APN,
Status: tibiotSessionStatus(data.OnlineStatus),
},
})
}
return items, nil
}
func (s *sSimTibiot) QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error) {
if input == nil {
return make(model.SimCardStatusOutput, 0), nil
}
items := make(model.SimCardStatusOutput, 0, len(input.SimKeys))
for _, simKey := range input.SimKeys {
params, err := tibiotParams(simKey)
if err != nil {
items = append(items, tibiotError[model.SimCardStatusItem](simKey, err))
continue
}
resp, err := s.post(ctx, "/api/v1/card/queryCardInfo", params)
if err != nil {
items = append(items, tibiotError[model.SimCardStatusItem](simKey, err))
continue
}
if !tibiotSuccess(resp) {
items = append(items, tibiotBusinessError[model.SimCardStatusItem](simKey, resp))
continue
}
var data tibiotCardInfo
if err := decodeTibiotData(resp, &data); err != nil {
items = append(items, tibiotError[model.SimCardStatusItem](simKey, err))
continue
}
items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{
SimKey: simKey,
Code: consts.ApiStatusSuccess,
Message: resp.Message,
ResData: model.SimCardStatusItem{CardStatus: convertTibiotCardStatus(ctx, gconv.Int(data.CardStatus)), LastChangeDate: data.LastActivateTime},
})
}
return items, nil
}
func (s *sSimTibiot) QuerySimStopReason(ctx context.Context, input *model.SimStopReasonInput) (model.SimStopReasonOutput, error) {
if input == nil {
return make(model.SimStopReasonOutput, 0), nil
}
items := make(model.SimStopReasonOutput, 0, len(input.SimKeys))
for _, simKey := range input.SimKeys {
params, err := tibiotParams(simKey)
if err != nil {
items = append(items, tibiotError[model.SimStopReasonItem](simKey, err))
continue
}
resp, err := s.post(ctx, "/api/v1/card/getCardStopReason", params)
if err != nil {
items = append(items, tibiotError[model.SimStopReasonItem](simKey, err))
continue
}
if !tibiotSuccess(resp) {
items = append(items, tibiotBusinessError[model.SimStopReasonItem](simKey, resp))
continue
}
var reasons []string
if err := decodeTibiotData(resp, &reasons); err != nil {
items = append(items, tibiotError[model.SimStopReasonItem](simKey, err))
continue
}
items = append(items, &model.SimBaseRes[model.SimStopReasonItem]{SimKey: simKey, Code: 0, Message: resp.Message, ResData: model.SimStopReasonItem{StopReason: strings.Join(reasons, ";")}})
}
return items, nil
}
func (s *sSimTibiot) QuerySimFrozenState(ctx context.Context, input *model.SimFrozenStateInput) (model.SimFrozenStateOutput, error) {
if input == nil {
return make(model.SimFrozenStateOutput, 0), nil
}
return tibiotUnsupported[model.SimFrozenStateItem](input.SimKeys, "Tibiot does not support frozen state query"), nil
}
func (s *sSimTibiot) QuerySimCardBindStatus(ctx context.Context, input *model.SimCardBindStatusInput) (model.SimCardBindStatusOutput, error) {
if input == nil {
return make(model.SimCardBindStatusOutput, 0), nil
}
items := make(model.SimCardBindStatusOutput, 0, len(input.SimKeys))
for _, simKey := range input.SimKeys {
params, err := tibiotParams(simKey)
if err != nil {
items = append(items, tibiotError[model.SimCardBindStatusItem](simKey, err))
continue
}
resp, err := s.post(ctx, "/api/v1/card/queryCardBindStatus", params)
if err != nil {
items = append(items, tibiotError[model.SimCardBindStatusItem](simKey, err))
continue
}
if !tibiotSuccess(resp) {
items = append(items, tibiotBusinessError[model.SimCardBindStatusItem](simKey, resp))
continue
}
var value string
if err := decodeTibiotData(resp, &value); err != nil {
items = append(items, tibiotError[model.SimCardBindStatusItem](simKey, err))
continue
}
items = append(items, &model.SimBaseRes[model.SimCardBindStatusItem]{SimKey: simKey, Code: 0, Message: resp.Message, ResData: model.SimCardBindStatusItem{BindStatus: value}})
}
return items, nil
}
func (s *sSimTibiot) QuerySimDeviceStatus(ctx context.Context, input *model.SimDeviceStatusInput) (model.SimDeviceStatusOutput, error) {
if input == nil {
return make(model.SimDeviceStatusOutput, 0), nil
}
items := make(model.SimDeviceStatusOutput, 0, len(input.SimKeys))
for _, simKey := range input.SimKeys {
params, err := tibiotParams(simKey)
if err != nil {
items = append(items, tibiotError[model.SimDeviceStatusItem](simKey, err))
continue
}
resp, err := s.post(ctx, "/api/v1/card/cardOnlineInformation", params)
if err != nil {
items = append(items, tibiotError[model.SimDeviceStatusItem](simKey, err))
continue
}
if !tibiotSuccess(resp) {
items = append(items, tibiotBusinessError[model.SimDeviceStatusItem](simKey, resp))
continue
}
var data tibiotOnlineInfo
if err := decodeTibiotData(resp, &data); err != nil {
items = append(items, tibiotError[model.SimDeviceStatusItem](simKey, err))
continue
}
items = append(items, &model.SimBaseRes[model.SimDeviceStatusItem]{SimKey: simKey, Code: 0, Message: resp.Message, ResData: model.SimDeviceStatusItem{DeviceStatus: tibiotDeviceStatus(data.EquipmentStatus)}})
}
return items, nil
}
func (s *sSimTibiot) QuerySimRegionLimitStatus(ctx context.Context, input *model.SimRegionLimitStatusInput) (model.SimRegionLimitStatusOutput, error) {
if input == nil {
return make(model.SimRegionLimitStatusOutput, 0), nil
}
items := make(model.SimRegionLimitStatusOutput, 0, len(input.SimKeys))
for _, simKey := range input.SimKeys {
params, err := tibiotParams(simKey)
if err != nil {
items = append(items, tibiotError[model.SimRegionLimitStatusItem](simKey, err))
continue
}
resp, err := s.post(ctx, "/api/v1/card/cardOnlineInformation", params)
if err != nil {
items = append(items, tibiotError[model.SimRegionLimitStatusItem](simKey, err))
continue
}
if !tibiotSuccess(resp) {
items = append(items, tibiotBusinessError[model.SimRegionLimitStatusItem](simKey, resp))
continue
}
var data tibiotOnlineInfo
if err := decodeTibiotData(resp, &data); err != nil {
items = append(items, tibiotError[model.SimRegionLimitStatusItem](simKey, err))
continue
}
items = append(items, &model.SimBaseRes[model.SimRegionLimitStatusItem]{SimKey: simKey, Code: 0, Message: resp.Message, ResData: model.SimRegionLimitStatusItem{RegionLimitStatus: data.RegionLimitStatus}})
}
return items, nil
}
func (s *sSimTibiot) QuerySimImei(ctx context.Context, input *model.SimImeiInput) (model.SimImeiOutput, error) {
if input == nil {
return make(model.SimImeiOutput, 0), nil
}
items := make(model.SimImeiOutput, 0, len(input.SimKeys))
for _, simKey := range input.SimKeys {
params, err := tibiotParams(simKey)
if err != nil {
items = append(items, tibiotError[model.SimImeiItem](simKey, err))
continue
}
resp, err := s.post(ctx, "/api/v1/card/querySimIMEI", params)
if err != nil {
items = append(items, tibiotError[model.SimImeiItem](simKey, err))
continue
}
if !tibiotSuccess(resp) {
items = append(items, tibiotBusinessError[model.SimImeiItem](simKey, resp))
continue
}
var data struct {
IMEI string `json:"imei"`
}
if err := decodeTibiotData(resp, &data); err != nil {
items = append(items, tibiotError[model.SimImeiItem](simKey, err))
continue
}
items = append(items, &model.SimBaseRes[model.SimImeiItem]{SimKey: simKey, Code: 0, Message: resp.Message, ResData: model.SimImeiItem{Imei: data.IMEI}})
}
return items, nil
}
func (s *sSimTibiot) QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) (model.SimDataUsageOutput, error) {
if input == nil {
return make(model.SimDataUsageOutput, 0), nil
}
items := make(model.SimDataUsageOutput, 0, len(input.SimKeys))
for _, simKey := range input.SimKeys {
result, err := s.querySimDataUsage(ctx, simKey)
if err != nil {
items = append(items, tibiotError[model.SimDataUsageItem](simKey, err))
continue
}
if result.BusinessError != nil {
items = append(items, tibiotBusinessError[model.SimDataUsageItem](simKey, *result.BusinessError))
continue
}
items = append(items, &model.SimBaseRes[model.SimDataUsageItem]{
SimKey: simKey,
Code: 0,
Cached: result.Cached,
UpdatedAt: result.UpdatedAt,
ResData: model.SimDataUsageItem{DataUsage: result.Usage},
})
}
return items, nil
}
func (s *sSimTibiot) querySimDataUsage(ctx context.Context, simKey *model.SimKey) (*tibiotUsageResult, error) {
cacheKey := tibiotUsageCacheKey(simKey)
if cached, ok := s.cachedUsage(cacheKey); ok {
return cached, nil
}
value, err, _ := s.usageFlight.Do(cacheKey, func() (any, error) {
if cached, ok := s.cachedUsage(cacheKey); ok {
return cached, nil
}
params, paramsErr := tibiotParams(simKey)
if paramsErr != nil {
return nil, paramsErr
}
params["flowUnit"] = "MB"
var (
lastResp tibiotResponse
lastErr error
usage float64
)
for attempt := 0; attempt < 2; attempt++ {
if waitErr := tibiotSimDataMarginLimiter.wait(ctx); waitErr != nil {
lastErr = waitErr
break
}
lastResp, lastErr = s.post(ctx, "/api/v1/card/simDataMargin", params)
if lastErr != nil {
if errors.Is(lastErr, context.Canceled) || errors.Is(lastErr, context.DeadlineExceeded) {
break
}
continue
}
if !tibiotSuccess(lastResp) {
continue
}
var value string
value, lastErr = decodeTibiotUsage(lastResp)
if lastErr == nil {
usage = parseTibiotFlow(value)
break
}
}
if lastErr != nil {
return nil, lastErr
}
if !tibiotSuccess(lastResp) {
return &tibiotUsageResult{BusinessError: &lastResp}, nil
}
updatedAt := time.Now().Format("2006-01-02 15:04:05")
s.usageCache.Store(cacheKey, tibiotUsageCacheEntry{Usage: usage, UpdatedAt: updatedAt, ExpiresAt: time.Now().Add(tibiotUsageCacheTTL)})
return &tibiotUsageResult{Usage: usage, UpdatedAt: updatedAt}, nil
})
if err != nil {
return nil, err
}
result, ok := value.(*tibiotUsageResult)
if !ok || result == nil {
return nil, fmt.Errorf("Tibiot usage query returned invalid result")
}
return result, nil
}
func (s *sSimTibiot) cachedUsage(cacheKey string) (*tibiotUsageResult, bool) {
value, ok := s.usageCache.Load(cacheKey)
if !ok {
return nil, false
}
entry, ok := value.(tibiotUsageCacheEntry)
if !ok || time.Now().After(entry.ExpiresAt) {
s.usageCache.Delete(cacheKey)
return nil, false
}
return &tibiotUsageResult{Usage: entry.Usage, Cached: true, UpdatedAt: entry.UpdatedAt}, true
}
func tibiotUsageCacheKey(simKey *model.SimKey) string {
if simKey == nil {
return "unknown"
}
for _, value := range []string{simKey.ICCID, simKey.IMSI, simKey.MSISDN} {
if value = strings.TrimSpace(value); value != "" {
return value
}
}
return "unknown"
}
func decodeTibiotUsage(resp tibiotResponse) (string, error) {
var value string
if err := decodeTibiotData(resp, &value); err == nil {
if strings.TrimSpace(value) == "" {
return "", fmt.Errorf("Tibiot usage data is empty")
}
return value, nil
} else {
var number float64
if numberErr := decodeTibiotData(resp, &number); numberErr != nil {
return "", err
}
return strconv.FormatFloat(number, 'f', -1, 64), nil
}
}
func (s *sSimTibiot) QuerySimDataDiagnosis(ctx context.Context, input *model.SimDataDiagnosisInput) (model.SimDataDiagnosisOutput, error) {
if input == nil {
return make(model.SimDataDiagnosisOutput, 0), nil
}
return tibiotUnsupported[any](input.SimKeys, "Tibiot does not support data diagnosis"), nil
}
func (s *sSimTibiot) QuerySimDataUsageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) {
if input == nil {
return make(model.SimDataUsageMonthlyOutput, 0), nil
}
queryMonth, err := tibiotMonth(input.ReqData.QueryDate)
if err != nil {
return nil, err
}
items := make(model.SimDataUsageMonthlyOutput, 0, len(input.SimKeys))
for _, simKey := range input.SimKeys {
params, err := tibiotParams(simKey)
if err != nil {
items = append(items, tibiotError[model.SimDataUsageMonthlyOutputItem](simKey, err))
continue
}
params["flowUnit"] = "MB"
resp, err := s.post(ctx, "/api/v1/card/queryCardMonthData", params)
if err != nil {
items = append(items, tibiotError[model.SimDataUsageMonthlyOutputItem](simKey, err))
continue
}
if !tibiotSuccess(resp) {
items = append(items, tibiotBusinessError[model.SimDataUsageMonthlyOutputItem](simKey, resp))
continue
}
var data []tibiotMonthUsage
if err := decodeTibiotData(resp, &data); err != nil {
items = append(items, tibiotError[model.SimDataUsageMonthlyOutputItem](simKey, err))
continue
}
var selected *tibiotMonthUsage
for i := range data {
dataMonth, parseErr := tibiotMonth(data[i].Years)
if queryMonth == "" || (parseErr == nil && dataMonth == queryMonth) {
selected = &data[i]
break
}
}
if selected == nil {
items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{SimKey: simKey, Code: consts.ApiStatusError, Message: "monthly usage data not found"})
continue
}
if strings.TrimSpace(selected.CardFlow) == "" {
items = append(items, tibiotError[model.SimDataUsageMonthlyOutputItem](simKey, fmt.Errorf("Tibiot monthly usage data is empty")))
continue
}
items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{SimKey: simKey, Code: 0, Message: resp.Message, ResData: model.SimDataUsageMonthlyOutputItem{DeadLine: selected.Years, DataAmount: parseTibiotFlow(selected.CardFlow)}})
}
return items, nil
}
func tibiotDate(queryDate string) (string, error) {
queryDate = strings.TrimSpace(queryDate)
if queryDate == "" {
return "", fmt.Errorf("queryDate is required")
}
if parsed, err := time.Parse("20060102", queryDate); err == nil {
return parsed.Format("2006-01-02"), nil
}
if parsed, err := time.Parse("2006-01-02", queryDate); err == nil {
return parsed.Format("2006-01-02"), nil
}
return "", fmt.Errorf("invalid queryDate: %s", queryDate)
}
func tibiotMonth(queryDate string) (string, error) {
queryDate = strings.TrimSpace(queryDate)
if queryDate == "" {
return "", nil
}
for _, layout := range []string{"200601", "2006-01", "20060102", "2006-01-02"} {
if parsed, err := time.Parse(layout, queryDate); err == nil {
return parsed.Format("200601"), nil
}
}
return "", fmt.Errorf("invalid monthly queryDate: %s", queryDate)
}
func (s *sSimTibiot) QuerySimDataUsageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) {
if input == nil {
return make(model.SimDataUsageDailyOutput, 0), nil
}
items := make(model.SimDataUsageDailyOutput, 0, len(input.SimKeys))
queryDate, err := tibiotDate(input.ReqData.QueryDate)
if err != nil {
return nil, err
}
for _, simKey := range input.SimKeys {
params, err := tibiotParams(simKey)
if err != nil {
items = append(items, tibiotError[model.SimDataUsageDailyOutputItem](simKey, err))
continue
}
params["queryTime"] = queryDate
params["flowUnit"] = "MB"
resp, err := s.post(ctx, "/api/v1/card/newIotData", params)
if err != nil {
items = append(items, tibiotError[model.SimDataUsageDailyOutputItem](simKey, err))
continue
}
if !tibiotSuccess(resp) {
items = append(items, tibiotBusinessError[model.SimDataUsageDailyOutputItem](simKey, resp))
continue
}
var data tibiotDailyUsage
if err := decodeTibiotData(resp, &data); err != nil {
items = append(items, tibiotError[model.SimDataUsageDailyOutputItem](simKey, err))
continue
}
if strings.TrimSpace(data.CardFlow) == "" {
items = append(items, tibiotError[model.SimDataUsageDailyOutputItem](simKey, fmt.Errorf("Tibiot daily usage data is empty")))
continue
}
items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{SimKey: simKey, Code: 0, Message: resp.Message, ResData: model.SimDataUsageDailyOutputItem{QueryDate: input.ReqData.QueryDate, DataAmount: parseTibiotFlow(data.CardFlow)}})
}
return items, nil
}
func (s *sSimTibiot) QuerySimApnInfo(ctx context.Context, input *model.SimApnInfoInput) (model.SimApnInfoOutput, error) {
if input == nil {
return make(model.SimApnInfoOutput, 0), nil
}
return tibiotUnsupported[model.SimApnInfoItemList](input.SimKeys, "Tibiot does not support APN information query"), nil
}
func (s *sSimTibiot) ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) (model.SimCardStatusChangeOutput, error) {
if input == nil {
return make(model.SimCardStatusChangeOutput, 0), nil
}
return tibiotUnsupported[model.SimCardStatusChangeItem](input.SimKeys, "Tibiot does not support SIM card status changes"), nil
}
func (s *sSimTibiot) ChangeSimApnFunction(ctx context.Context, input *model.SimCardOperateChangeApnInput) (model.SimCardOperateChangeApnOutput, error) {
if input == nil {
return make(model.SimCardOperateChangeApnOutput, 0), nil
}
operateType := 0
switch strings.TrimSpace(input.ReqData.OperType) {
case consts.CardApnOperTypeOpen:
operateType = 1
case consts.CardApnOperTypeStop:
operateType = 2
default:
return tibiotUnsupported[model.SimCardOperateChangeApnItem](input.SimKeys, "unsupported operType, expected 0(open) or 1(stop)"), nil
}
items := make(model.SimCardOperateChangeApnOutput, 0, len(input.SimKeys))
for _, simKey := range input.SimKeys {
params, err := tibiotParams(simKey)
if err != nil {
items = append(items, tibiotError[model.SimCardOperateChangeApnItem](simKey, err))
continue
}
params["operateType"] = operateType
resp, err := s.post(ctx, "/api/v1/card/cardNetWork", params)
if err != nil {
item := tibiotError[model.SimCardOperateChangeApnItem](simKey, err)
item.ResData.Uncertain = true
items = append(items, item)
continue
}
statusText := strings.TrimSpace(fmt.Sprint(resp.Status))
status, statusErr := strconv.Atoi(statusText)
if statusErr != nil || (status == 0 && statusText != "0") {
items = append(items, &model.SimBaseRes[model.SimCardOperateChangeApnItem]{
SimKey: simKey, Code: consts.ApiStatusError,
Message: "天比高 APN 开停响应状态无效,办理结果未确认:" + statusText,
ResData: model.SimCardOperateChangeApnItem{Uncertain: true},
})
continue
}
if status != 0 {
items = append(items, tibiotBusinessError[model.SimCardOperateChangeApnItem](simKey, resp))
continue
}
items = append(items, &model.SimBaseRes[model.SimCardOperateChangeApnItem]{SimKey: simKey, Code: 0, Message: resp.Message, ResData: model.SimCardOperateChangeApnItem{OrderNum: ""}})
}
return items, nil
}
func (s *sSimTibiot) QuerySimCardStatusBatch(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error) {
// Tibiot's batch endpoint returns paged card records. Query pages until all
// requested identifiers are found, preserving the caller's input order.
if input == nil || len(input.SimKeys) == 0 {
return make(model.SimCardStatusOutput, 0), nil
}
requested := make(map[string]*model.SimKey, len(input.SimKeys)*2)
for _, key := range input.SimKeys {
if key == nil {
continue
}
if key.ICCID != "" {
requested["iccid:"+strings.ToUpper(strings.TrimSpace(key.ICCID))] = key
}
if key.MSISDN != "" {
requested["msisdn:"+strings.TrimSpace(key.MSISDN)] = key
}
if key.IMSI != "" {
requested["imsi:"+strings.ToUpper(strings.TrimSpace(key.IMSI))] = key
}
}
found := make(map[*model.SimKey]tibiotCardInfo, len(input.SimKeys))
for page := 1; page <= 100 && len(found) < len(input.SimKeys); page++ {
params := map[string]any{"cardType": 1, "pageNum": page, "pageSize": 100, "flowUnit": "MB"}
if len(input.SimKeys) == 1 {
if name, value := tibiotCardIdentifier(input.SimKeys[0]); name != "" {
params[name] = value
}
}
resp, err := s.post(ctx, "/api/v1/card/batchQueryCardInfo", params)
if err != nil {
return nil, err
}
if !tibiotSuccess(resp) {
return nil, fmt.Errorf("Tibiot batch status failed: code=%d message=%s", tibiotCode(resp), resp.Message)
}
records, pageCount, err := decodeTibiotBatchCards(resp)
if err != nil {
return nil, err
}
for _, item := range records {
if key := requested["iccid:"+strings.ToUpper(strings.TrimSpace(item.ICCID))]; key != nil {
found[key] = item
}
if key := requested["msisdn:"+strings.TrimSpace(item.MSISDN)]; key != nil {
found[key] = item
}
if key := requested["imsi:"+strings.ToUpper(strings.TrimSpace(item.IMSI))]; key != nil {
found[key] = item
}
}
if pageCount > 0 && page >= pageCount {
break
}
if len(records) < 100 {
break
}
}
items := make(model.SimCardStatusOutput, 0, len(input.SimKeys))
for _, simKey := range input.SimKeys {
data, ok := found[simKey]
if !ok {
items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{SimKey: simKey, Code: consts.ApiStatusError, Message: "batch response missing card"})
continue
}
items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{SimKey: simKey, Code: 0, ResData: model.SimCardStatusItem{CardStatus: convertTibiotCardStatus(ctx, gconv.Int(data.CardStatus)), LastChangeDate: data.LastActivateTime}})
}
return items, nil
}
func (s *sSimTibiot) QuerySimDataUsageMonthlyBatch(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) {
return s.QuerySimDataUsageMonthly(ctx, input)
}
func (s *sSimTibiot) QuerySimDataUsageDailyBatch(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) {
if input == nil || len(input.SimKeys) == 0 {
return make(model.SimDataUsageDailyOutput, 0), nil
}
queryDate, err := tibiotDate(input.ReqData.QueryDate)
if err != nil {
return nil, err
}
params := map[string]any{"queryTime": queryDate, "flowUnit": "MB"}
useICCID := true
for _, key := range input.SimKeys {
if key == nil || strings.TrimSpace(key.ICCID) == "" {
useICCID = false
break
}
}
if useICCID {
values := make([]string, 0, len(input.SimKeys))
for _, key := range input.SimKeys {
values = append(values, strings.TrimSpace(key.ICCID))
}
params["iccids"] = strings.Join(values, ",")
} else {
values := make([]string, 0, len(input.SimKeys))
for _, key := range input.SimKeys {
if key == nil || strings.TrimSpace(key.MSISDN) == "" {
return nil, fmt.Errorf("batch daily query requires all ICCIDs or MSISDNs")
}
values = append(values, strings.TrimSpace(key.MSISDN))
}
params["msisdns"] = strings.Join(values, ",")
}
resp, err := s.post(ctx, "/api/v1/card/batchNewIotData", params)
if err != nil {
return nil, err
}
if !tibiotSuccess(resp) {
return nil, fmt.Errorf("Tibiot batch daily failed: code=%d message=%s", tibiotCode(resp), resp.Message)
}
var data []tibiotDailyUsage
if err := decodeTibiotData(resp, &data); err != nil {
return nil, err
}
byID := make(map[string]tibiotDailyUsage, len(data)*2)
for _, item := range data {
if item.ICCID != "" {
byID["iccid:"+strings.ToUpper(strings.TrimSpace(item.ICCID))] = item
}
if item.IMSI != "" {
byID["imsi:"+strings.ToUpper(strings.TrimSpace(item.IMSI))] = item
}
if item.MSISDN != "" {
byID["msisdn:"+strings.TrimSpace(item.MSISDN)] = item
}
}
items := make(model.SimDataUsageDailyOutput, 0, len(input.SimKeys))
for _, key := range input.SimKeys {
var item tibiotDailyUsage
var ok bool
if key != nil {
for _, identifier := range []struct {
name string
value string
}{
{name: "iccid", value: key.ICCID},
{name: "msisdn", value: key.MSISDN},
{name: "imsi", value: key.IMSI},
} {
value := strings.TrimSpace(identifier.value)
if value == "" {
continue
}
lookup := identifier.name + ":" + value
if identifier.name != "msisdn" {
lookup = identifier.name + ":" + strings.ToUpper(value)
}
if item, ok = byID[lookup]; ok {
break
}
}
}
if !ok {
items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{SimKey: key, Code: consts.ApiStatusError, Message: "batch response missing card", ResData: model.SimDataUsageDailyOutputItem{QueryDate: input.ReqData.QueryDate}})
continue
}
if strings.TrimSpace(item.CardFlow) == "" {
items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{SimKey: key, Code: consts.ApiStatusError, Message: "batch response card flow is empty", ResData: model.SimDataUsageDailyOutputItem{QueryDate: input.ReqData.QueryDate}})
continue
}
items = append(items, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{SimKey: key, Code: 0, Message: resp.Message, ResData: model.SimDataUsageDailyOutputItem{QueryDate: input.ReqData.QueryDate, DataAmount: parseTibiotFlow(item.CardFlow)}})
}
return items, nil
}