chore: 初始开源发布
iotlcm-upstream-chan:运营商上游通道适配服务(统一 /sim/* 协议)。 - 内联原私有依赖:yunze-public → internal/pub/(10 包 18 文件), yunze-pb/pb → internal/pb/(9 文件);Consul 注册改用官方 gogf/gf/contrib/registry/consul/v2(API 兼容) - Go module:yz-svr → git.5iot.com/iotlcm-public/iotlcm-upstream-chan - 修复 mingqian 包测试:实现重构后 `adapter.operations` 字段已移除, 测试断言按当前实现语义重写(不改产品行为) - 凭据与内部标识脱敏:MySQL/Redis/Consul 凭据、私网与生产 IP、 内网域名、真实手机号、注释里的真实 token/transid - 移除内部资产:CI 触发器、生产通道种子 SQL、真实凭据测试资源文档 - Dockerfile 改用公共基础镜像,移除写死的 registry 凭据 - 补 Apache-2.0 LICENSE,重写 README,LF 行尾 + .gitattributes
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
package mingqian
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
const TemplateKey = "sim_mingqian_v3"
|
||||
const statePath = "/simlist/get-state-by-number"
|
||||
const usagePath = "/simlist/get-flow-use-by-number"
|
||||
const packageUsagePath = "/use/get-card-use-by-number"
|
||||
|
||||
type Config struct {
|
||||
API struct {
|
||||
BaseURL string `json:"baseUrl"`
|
||||
Timeout string `json:"timeout"`
|
||||
ResumePath string `json:"resumePath"`
|
||||
} `json:"api"`
|
||||
}
|
||||
|
||||
type Adapter struct {
|
||||
client *Client
|
||||
resumePath string
|
||||
}
|
||||
|
||||
var _ model.SimCardChannelI = (*Adapter)(nil)
|
||||
|
||||
func New(cfg *model.SimChannelConfigItemCache) (*Adapter, error) {
|
||||
if cfg == nil || cfg.Template == nil || cfg.Channel == nil {
|
||||
return nil, gerror.New("mingqian: channel and template required")
|
||||
}
|
||||
config := Config{}
|
||||
config.API.BaseURL, config.API.Timeout = DefaultBaseURL, "10s"
|
||||
if strings.TrimSpace(cfg.Template.TempConfig) != "" {
|
||||
if err := json.Unmarshal([]byte(cfg.Template.TempConfig), &config); err != nil {
|
||||
return nil, gerror.New("mingqian: invalid template JSON")
|
||||
}
|
||||
}
|
||||
timeout, err := time.ParseDuration(config.API.Timeout)
|
||||
if err != nil || timeout <= 0 {
|
||||
return nil, gerror.New("mingqian: invalid api.timeout")
|
||||
}
|
||||
resume := strings.TrimSpace(config.API.ResumePath)
|
||||
if resume != "" && (!strings.HasPrefix(resume, "/simlist/") || strings.ContainsAny(resume, "?#") || strings.Contains(resume, "..") || resume == "/simlist/get-sim-switchoff-by-number") {
|
||||
return nil, gerror.New("mingqian: resumePath must be a confirmed distinct /simlist/ path")
|
||||
}
|
||||
client, err := NewClient(config.API.BaseURL, cfg.Channel.AppKey, cfg.Channel.AppSecret, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Adapter{client: client, resumePath: resume}, nil
|
||||
}
|
||||
|
||||
type number float64
|
||||
|
||||
func (value *number) UnmarshalJSON(raw []byte) error {
|
||||
text := strings.TrimSpace(string(raw))
|
||||
if text == "null" {
|
||||
return gerror.New("mingqian: null numeric field")
|
||||
}
|
||||
if strings.HasPrefix(text, `"`) {
|
||||
if err := json.Unmarshal(raw, &text); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
parsed, err := strconv.ParseFloat(text, 64)
|
||||
if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) {
|
||||
return gerror.New("mingqian: invalid numeric field")
|
||||
}
|
||||
*value = number(parsed)
|
||||
return nil
|
||||
}
|
||||
|
||||
type card struct {
|
||||
ICCID string `json:"iccid"`
|
||||
MSISDN string `json:"msisdn"`
|
||||
State *int `json:"state"`
|
||||
StateName string `json:"statename"`
|
||||
Usage *number `json:"use"`
|
||||
PackageTotal *number `json:"totalpackage"`
|
||||
PackageUsage *number `json:"targetpackageusage"`
|
||||
Success *int `json:"issuccess"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func identifier(key *model.SimKey) string {
|
||||
if key == nil {
|
||||
return ""
|
||||
}
|
||||
if value := strings.TrimSpace(key.ICCID); value != "" {
|
||||
return value
|
||||
}
|
||||
return strings.TrimSpace(key.MSISDN)
|
||||
}
|
||||
|
||||
func query[T any](ctx context.Context, adapter *Adapter, keys []*model.SimKey, path string, limit int, convert func(card) (T, error)) []*model.SimBaseRes[T] {
|
||||
output := make([]*model.SimBaseRes[T], len(keys))
|
||||
for index, key := range keys {
|
||||
output[index] = &model.SimBaseRes[T]{SimKey: key, Code: consts.ApiStatusError, Message: "mingqian: ICCID or MSISDN required (IMSI-only unsupported)"}
|
||||
}
|
||||
for start := 0; start < len(keys); start += limit {
|
||||
end := start + limit
|
||||
if end > len(keys) {
|
||||
end = len(keys)
|
||||
}
|
||||
numbers := make([]string, 0, end-start)
|
||||
seen := map[string]bool{}
|
||||
for _, key := range keys[start:end] {
|
||||
value := identifier(key)
|
||||
if value != "" && !seen[value] {
|
||||
numbers = append(numbers, value)
|
||||
seen[value] = true
|
||||
}
|
||||
}
|
||||
if len(numbers) == 0 {
|
||||
continue
|
||||
}
|
||||
var cards []card
|
||||
err := adapter.client.post(ctx, path, map[string]any{"numberArray": numbers}, &cards)
|
||||
for index := start; index < end; index++ {
|
||||
requested := identifier(keys[index])
|
||||
if requested == "" {
|
||||
continue
|
||||
}
|
||||
item := output[index]
|
||||
if err != nil {
|
||||
item.Message = err.Error()
|
||||
continue
|
||||
}
|
||||
var matched *card
|
||||
duplicate := false
|
||||
for cardIndex := range cards {
|
||||
candidate := &cards[cardIndex]
|
||||
matches := false
|
||||
if strings.TrimSpace(keys[index].ICCID) != "" {
|
||||
matches = strings.TrimSpace(candidate.ICCID) == requested
|
||||
} else {
|
||||
matches = strings.TrimSpace(candidate.MSISDN) == requested
|
||||
}
|
||||
if matches {
|
||||
if matched != nil {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
matched = candidate
|
||||
}
|
||||
}
|
||||
if duplicate {
|
||||
item.Message = "mingqian: duplicate card in response"
|
||||
continue
|
||||
}
|
||||
if matched == nil {
|
||||
item.Message = "mingqian: requested card missing from response"
|
||||
continue
|
||||
}
|
||||
result, mapErr := convert(*matched)
|
||||
if mapErr != nil {
|
||||
item.Message = mapErr.Error()
|
||||
continue
|
||||
}
|
||||
item.Code = consts.ApiStatusSuccess
|
||||
item.Message = ""
|
||||
item.ResData = result
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func cardStatus(state *int) (int, error) {
|
||||
if state == nil {
|
||||
return 0, gerror.New("mingqian: state missing")
|
||||
}
|
||||
switch *state {
|
||||
case 1:
|
||||
return 1, nil
|
||||
case 2:
|
||||
return 2, nil
|
||||
case 3:
|
||||
return 4, nil
|
||||
case 4:
|
||||
return 6, nil
|
||||
case 5:
|
||||
return 7, nil
|
||||
case 6:
|
||||
return 9, nil
|
||||
default:
|
||||
return 0, gerror.Newf("mingqian: unmapped card state %d", *state)
|
||||
}
|
||||
}
|
||||
|
||||
func (adapter *Adapter) QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error) {
|
||||
return query(ctx, adapter, input.SimKeys, statePath, 50, func(item card) (model.SimBasicInfoItem, error) {
|
||||
state, err := cardStatus(item.State)
|
||||
if err != nil {
|
||||
return model.SimBasicInfoItem{}, err
|
||||
}
|
||||
return model.SimBasicInfoItem{ICCID: item.ICCID, MSISDN: item.MSISDN, SimStatus: strconv.Itoa(state), Remark: item.StateName}, nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (adapter *Adapter) QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error) {
|
||||
return query(ctx, adapter, input.SimKeys, statePath, 50, func(item card) (model.SimCardStatusItem, error) {
|
||||
state, err := cardStatus(item.State)
|
||||
return model.SimCardStatusItem{CardStatus: state}, err
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (adapter *Adapter) QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) (model.SimDataUsageOutput, error) {
|
||||
if input == nil {
|
||||
return nil, gerror.New("mingqian: input required")
|
||||
}
|
||||
output := query(ctx, adapter, input.SimKeys, usagePath, 50, func(item card) (model.SimDataUsageItem, error) {
|
||||
if item.Usage == nil || *item.Usage < 0 {
|
||||
return model.SimDataUsageItem{}, gerror.New("mingqian: current-month usage missing or unknown")
|
||||
}
|
||||
return model.SimDataUsageItem{DataUsage: float64(*item.Usage) / 1024}, nil
|
||||
})
|
||||
packages, err := adapter.QueryPackageFlow(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for index, item := range output {
|
||||
if packages[index].Code != consts.ApiStatusSuccess {
|
||||
item.ResData.PackageFlowError = packages[index].Message
|
||||
if item.Code == consts.ApiStatusSuccess {
|
||||
item.Message = "mingqian: current-month usage available; package flow unavailable (see packageFlowError)"
|
||||
}
|
||||
continue
|
||||
}
|
||||
item.ResData.PackageFlow = &packages[index].ResData
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (adapter *Adapter) ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) (model.SimCardStatusChangeOutput, error) {
|
||||
if input == nil {
|
||||
return nil, gerror.New("mingqian: input required")
|
||||
}
|
||||
path := ""
|
||||
switch strings.TrimSpace(input.ReqData.OperType) {
|
||||
case consts.CardStatusChangeOperTypeApplyStop:
|
||||
path = "/simlist/get-sim-switchoff-by-number"
|
||||
case consts.CardStatusChangeOperTypeApplyResume:
|
||||
path = adapter.resumePath
|
||||
default:
|
||||
return unsupported[model.SimCardStatusChangeItem](input.SimKeys, "invalid operType"), nil
|
||||
}
|
||||
if path == "" {
|
||||
return unsupported[model.SimCardStatusChangeItem](input.SimKeys, "resume endpoint unconfirmed; configure api.resumePath"), nil
|
||||
}
|
||||
return query(ctx, adapter, input.SimKeys, path, 20, func(item card) (model.SimCardStatusChangeItem, error) {
|
||||
if item.Success == nil || *item.Success != 1 {
|
||||
return model.SimCardStatusChangeItem{}, gerror.New("mingqian: card operation failed or success flag missing")
|
||||
}
|
||||
return model.SimCardStatusChangeItem{}, nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
func unsupported[T any](keys []*model.SimKey, capability string) []*model.SimBaseRes[T] {
|
||||
output := make([]*model.SimBaseRes[T], 0, len(keys))
|
||||
for _, key := range keys {
|
||||
output = append(output, &model.SimBaseRes[T]{SimKey: key, Code: consts.ApiStatusError, Message: "mingqian: " + capability})
|
||||
}
|
||||
return output
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package mingqian
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
const DefaultBaseURL = "http://iotapi.bjmqtx.com/openapi/wlw/v3/cool"
|
||||
|
||||
type gate struct {
|
||||
mu sync.Mutex
|
||||
next time.Time
|
||||
}
|
||||
|
||||
var accountGates sync.Map
|
||||
|
||||
func (limiter *gate) wait(ctx context.Context) error {
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
limiter.mu.Lock()
|
||||
delay := time.Until(limiter.next)
|
||||
if delay <= 0 {
|
||||
limiter.next = time.Now().Add(60 * time.Millisecond)
|
||||
limiter.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
limiter.mu.Unlock()
|
||||
timer := time.NewTimer(delay)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
appID string
|
||||
secret string
|
||||
http *http.Client
|
||||
gate *gate
|
||||
}
|
||||
|
||||
func NewClient(baseURL, appID, secret string, timeout time.Duration) (*Client, error) {
|
||||
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
||||
parsed, err := url.Parse(baseURL)
|
||||
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
|
||||
return nil, gerror.New("mingqian: invalid api.baseUrl")
|
||||
}
|
||||
appID, secret = strings.TrimSpace(appID), strings.TrimSpace(secret)
|
||||
if appID == "" || secret == "" {
|
||||
return nil, gerror.New("mingqian: channel AppKey (appId) and AppSecret (APPKEY) are required")
|
||||
}
|
||||
if timeout <= 0 {
|
||||
return nil, gerror.New("mingqian: timeout must be positive")
|
||||
}
|
||||
limiter, _ := accountGates.LoadOrStore(baseURL+"|"+appID, &gate{})
|
||||
return &Client{baseURL: baseURL, appID: appID, secret: secret, gate: limiter.(*gate), http: &http.Client{Timeout: timeout, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}}, nil
|
||||
}
|
||||
|
||||
func Sign(params map[string]string, secret string) string {
|
||||
keys := make([]string, 0, len(params))
|
||||
for key := range params {
|
||||
if key != "sign" {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var source strings.Builder
|
||||
source.WriteString(secret)
|
||||
for _, key := range keys {
|
||||
source.WriteString(key)
|
||||
source.WriteString("_")
|
||||
source.WriteString(params[key])
|
||||
}
|
||||
source.WriteString(secret)
|
||||
sum := md5.Sum([]byte(source.String()))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func (client *Client) post(ctx context.Context, path string, data any, output any) error {
|
||||
if err := client.gate.wait(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now().In(time.FixedZone("Asia/Shanghai", 8*60*60))
|
||||
random := make([]byte, 3)
|
||||
if _, err := rand.Read(random); err != nil {
|
||||
return gerror.New("mingqian: cannot generate request ID")
|
||||
}
|
||||
common := map[string]string{
|
||||
"appId": client.appID, "version": "3.0", "reqtime": now.Format("2006-01-02"),
|
||||
"timestamp": strconv.FormatInt(now.Add(time.Minute).UnixMilli(), 10),
|
||||
"transid": now.Format("20060102150405") + strconv.Itoa(1000 + now.Nanosecond()/1000000)[1:] + hex.EncodeToString(random),
|
||||
}
|
||||
common["sign"] = Sign(common, client.secret)
|
||||
payload := struct {
|
||||
Body map[string]string `json:"body"`
|
||||
Data string `json:"data,omitempty"`
|
||||
}{Body: common}
|
||||
if data != nil {
|
||||
encoded, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return gerror.New("mingqian: invalid request data")
|
||||
}
|
||||
payload.Data = string(encoded)
|
||||
}
|
||||
encoded, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return gerror.New("mingqian: invalid request envelope")
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, client.baseURL+path, bytes.NewReader(encoded))
|
||||
if err != nil {
|
||||
return gerror.New("mingqian: invalid request URL")
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := client.http.Do(request)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return gerror.New("mingqian: upstream request failed or timed out; no automatic retry")
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return gerror.Newf("mingqian: HTTP %d", response.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(response.Body, 4*1024*1024+1))
|
||||
if err != nil || len(body) > 4*1024*1024 {
|
||||
return gerror.New("mingqian: invalid or oversized response")
|
||||
}
|
||||
var envelope struct {
|
||||
Code *int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &envelope); err != nil {
|
||||
return gerror.New("mingqian: malformed response JSON")
|
||||
}
|
||||
if envelope.Code == nil {
|
||||
return gerror.New("mingqian: response code missing")
|
||||
}
|
||||
if *envelope.Code != 200 {
|
||||
return gerror.Newf("mingqian: business code %d", *envelope.Code)
|
||||
}
|
||||
if output != nil {
|
||||
if len(envelope.Data) == 0 || string(envelope.Data) == "null" {
|
||||
return gerror.New("mingqian: response data missing")
|
||||
}
|
||||
if err := json.Unmarshal(envelope.Data, output); err != nil {
|
||||
return gerror.New("mingqian: invalid response data")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package mingqian
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
||||
)
|
||||
|
||||
func TestPackageFlowFailuresPreserveMonthlyUsage(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
body string
|
||||
}{
|
||||
{"business failure", `{"code":500,"msg":"private upstream detail","data":[]}`},
|
||||
{"missing card", `{"code":200,"data":[]}`},
|
||||
{"duplicate card", `{"code":200,"data":[{"iccid":"A","totalpackage":10,"targetpackageusage":2},{"iccid":"A","totalpackage":10,"targetpackageusage":2}]}`},
|
||||
{"missing total", `{"code":200,"data":[{"iccid":"A","targetpackageusage":2}]}`},
|
||||
{"missing usage", `{"code":200,"data":[{"iccid":"A","totalpackage":10}]}`},
|
||||
{"null total", `{"code":200,"data":[{"iccid":"A","totalpackage":null,"targetpackageusage":2}]}`},
|
||||
{"negative total", `{"code":200,"data":[{"iccid":"A","totalpackage":-1,"targetpackageusage":2}]}`},
|
||||
{"negative usage", `{"code":200,"data":[{"iccid":"A","totalpackage":10,"targetpackageusage":-1}]}`},
|
||||
{"nonfinite total", `{"code":200,"data":[{"iccid":"A","totalpackage":"NaN","targetpackageusage":2}]}`},
|
||||
{"invalid usage", `{"code":200,"data":[{"iccid":"A","totalpackage":10,"targetpackageusage":""}]}`},
|
||||
}
|
||||
for _, test := range cases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.URL.Path == usagePath {
|
||||
fmt.Fprint(writer, `{"code":200,"data":[{"iccid":"A","use":1024}]}`)
|
||||
return
|
||||
}
|
||||
fmt.Fprint(writer, test.body)
|
||||
})
|
||||
output, err := adapter.QuerySimDataUsage(context.Background(), &model.SimDataUsageInput{SimKeys: []*model.SimKey{{ICCID: "A"}}})
|
||||
if err != nil || len(output) != 1 {
|
||||
t.Fatalf("output=%+v err=%v", output, err)
|
||||
}
|
||||
item := output[0]
|
||||
if item.Code != 0 || item.ResData.DataUsage != 1 || item.ResData.PackageFlow != nil || item.ResData.PackageFlowError == "" || item.Message == "" {
|
||||
t.Fatalf("must retain monthly usage with an explicit package failure: %+v", item)
|
||||
}
|
||||
if strings.Contains(item.ResData.PackageFlowError, "private upstream detail") {
|
||||
t.Fatal("raw upstream errors must not leak")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageFlowNeverSubstitutesForMonthlyUsage(t *testing.T) {
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.URL.Path == usagePath {
|
||||
writer.WriteHeader(http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
fmt.Fprint(writer, `{"code":200,"data":[{"iccid":"A","totalpackage":10,"targetpackageusage":8}]}`)
|
||||
})
|
||||
output, err := adapter.QuerySimDataUsage(context.Background(), &model.SimDataUsageInput{SimKeys: []*model.SimKey{{ICCID: "A"}}})
|
||||
if err != nil || len(output) != 1 || output[0].Code == 0 || !strings.Contains(output[0].Message, "502") {
|
||||
t.Fatalf("monthly failure must remain a failure: %+v, %v", output, err)
|
||||
}
|
||||
if output[0].ResData.DataUsage != 0 || output[0].ResData.PackageFlow == nil || output[0].ResData.PackageFlow.DataUsage != 8 {
|
||||
t.Fatal("package usage must not become monthly usage")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageFlowPerCardMatchingAndZero(t *testing.T) {
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.URL.Path == usagePath {
|
||||
fmt.Fprint(writer, `{"code":200,"data":[{"iccid":"A","use":0},{"msisdn":"144000","use":512},{"iccid":"missing-package","use":2048}]}`)
|
||||
return
|
||||
}
|
||||
fmt.Fprint(writer, `{"code":200,"data":[{"msisdn":"144000","totalpackage":"1.5","targetpackageusage":"2.25"},{"iccid":"A","totalpackage":0,"targetpackageusage":0}]}`)
|
||||
})
|
||||
keys := []*model.SimKey{{ICCID: " A "}, {MSISDN: "144000"}, {ICCID: "missing-package"}, {ICCID: "A"}}
|
||||
output, err := adapter.QuerySimDataUsage(context.Background(), &model.SimDataUsageInput{SimKeys: keys})
|
||||
if err != nil || len(output) != len(keys) {
|
||||
t.Fatalf("output=%+v err=%v", output, err)
|
||||
}
|
||||
for index, item := range output {
|
||||
if item.Code != 0 || item.SimKey != keys[index] {
|
||||
t.Fatalf("identity or monthly status changed: %+v", item)
|
||||
}
|
||||
}
|
||||
if output[0].ResData.PackageFlow == nil || *output[0].ResData.PackageFlow != (model.SimPackageFlowItem{}) || output[3].ResData.PackageFlow == nil {
|
||||
t.Fatal("zero flow is valid and duplicate request identifiers must retain their results")
|
||||
}
|
||||
flow := output[1].ResData.PackageFlow
|
||||
if flow == nil || flow.TotalFlow != 1.5 || flow.DataUsage != 2.25 || flow.RemainingFlow != 0 || output[1].ResData.DataUsage != 0.5 {
|
||||
t.Fatalf("incorrect overage or units: %+v", flow)
|
||||
}
|
||||
if output[2].ResData.PackageFlow != nil || output[2].ResData.PackageFlowError == "" {
|
||||
t.Fatal("missing package card must not be reported as zero")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowBothEndpointsBatchAtFifty(t *testing.T) {
|
||||
calls := map[string]int{}
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
calls[request.URL.Path]++
|
||||
var payload struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
var data struct {
|
||||
Numbers []string `json:"numberArray"`
|
||||
}
|
||||
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(payload.Data), &data); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(data.Numbers) == 0 || len(data.Numbers) > 50 {
|
||||
t.Errorf("wrong batch size: %d", len(data.Numbers))
|
||||
}
|
||||
rows := []map[string]any{}
|
||||
for _, identifier := range data.Numbers {
|
||||
rows = append(rows, map[string]any{"iccid": identifier, "use": 1024, "totalpackage": 10, "targetpackageusage": 8})
|
||||
}
|
||||
json.NewEncoder(writer).Encode(map[string]any{"code": 200, "data": rows})
|
||||
})
|
||||
keys := make([]*model.SimKey, 51)
|
||||
for index := range keys {
|
||||
keys[index] = &model.SimKey{ICCID: strconv.Itoa(index)}
|
||||
}
|
||||
output, err := adapter.QuerySimDataUsage(context.Background(), &model.SimDataUsageInput{SimKeys: keys})
|
||||
if err != nil || len(output) != 51 || calls[usagePath] != 2 || calls[packageUsagePath] != 2 {
|
||||
t.Fatalf("calls=%v count=%d err=%v", calls, len(output), err)
|
||||
}
|
||||
for _, item := range output {
|
||||
if item.Code != 0 || item.ResData.DataUsage != 1 || item.ResData.PackageFlow == nil || item.ResData.PackageFlow.RemainingFlow != 2 {
|
||||
t.Fatalf("incomplete mapping: %+v", item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowEmptyInputAndUnsupportedQueries(t *testing.T) {
|
||||
calls := 0
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) { calls++ })
|
||||
if _, err := adapter.QuerySimDataUsage(context.Background(), nil); err == nil {
|
||||
t.Fatal("nil input must fail")
|
||||
}
|
||||
output, err := adapter.QuerySimDataUsage(context.Background(), &model.SimDataUsageInput{})
|
||||
if err != nil || len(output) != 0 {
|
||||
t.Fatalf("empty input: %+v %v", output, err)
|
||||
}
|
||||
keys := []*model.SimKey{{ICCID: "A"}}
|
||||
session, _ := adapter.QuerySimSession(context.Background(), &model.SimSessionInput{SimKeys: keys})
|
||||
monthly, _ := adapter.QuerySimDataUsageMonthly(context.Background(), &model.SimDataUsageMonthlyInput{SimKeys: keys})
|
||||
daily, _ := adapter.QuerySimDataUsageDaily(context.Background(), &model.SimDataUsageDailyInput{SimKeys: keys})
|
||||
if session[0].Code == 0 || monthly[0].Code == 0 || daily[0].Code == 0 || calls != 0 {
|
||||
t.Fatal("unsupported queries must not invent data or contact upstream")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package mingqian
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model/entity"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func testAdapter(t *testing.T, handler http.HandlerFunc) *Adapter {
|
||||
t.Helper()
|
||||
server := httptest.NewServer(handler)
|
||||
t.Cleanup(server.Close)
|
||||
client, err := NewClient(server.URL, "test-app", "test-secret", time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &Adapter{client: client}
|
||||
}
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
params := map[string]string{"appId": "test-app", "reqtime": "2026-09-17", "timestamp": "1789603260000", "transid": "20260917080000000abcdef", "version": "3.0", "sign": "ignored"}
|
||||
if got := Sign(params, "test-secret"); got != "78e5d4f24b86159ad2f664ddbcc66282" {
|
||||
t.Fatalf("wrong sign: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWireAndUsageMapping(t *testing.T) {
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.Method != http.MethodPost || (request.URL.Path != usagePath && request.URL.Path != packageUsagePath) || request.Header.Get("Content-Type") != "application/json" {
|
||||
t.Error("incorrect HTTP contract")
|
||||
}
|
||||
var payload struct {
|
||||
Body map[string]string `json:"body"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
|
||||
t.Error(err)
|
||||
writer.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
if payload.Body["sign"] != Sign(payload.Body, "test-secret") {
|
||||
t.Error("invalid signature")
|
||||
}
|
||||
stamp, err := strconv.ParseInt(payload.Body["timestamp"], 10, 64)
|
||||
if err != nil || len(payload.Body["timestamp"]) != 13 || time.Until(time.UnixMilli(stamp)) < 55*time.Second || time.Until(time.UnixMilli(stamp)) > 65*time.Second {
|
||||
t.Error("wrong millisecond timestamp offset")
|
||||
}
|
||||
if !regexp.MustCompile(`^\d{17}[0-9a-f]{6}$`).MatchString(payload.Body["transid"]) {
|
||||
t.Error("invalid transaction ID")
|
||||
}
|
||||
var data struct {
|
||||
Numbers []string `json:"numberArray"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(payload.Data), &data); err != nil || len(data.Numbers) != 3 {
|
||||
t.Error("data must be escaped JSON containing requested cards")
|
||||
}
|
||||
if request.URL.Path == packageUsagePath {
|
||||
fmt.Fprint(writer, `{"code":200,"data":[{"iccid":"B","totalpackage":10240,"targetpackageusage":2048},{"iccid":"A","totalpackage":"10240","targetpackageusage":"8192"}]}`)
|
||||
return
|
||||
}
|
||||
fmt.Fprint(writer, `{"code":200,"data":[{"iccid":"B","use":2048},{"iccid":"A","use":"1024"}]}`)
|
||||
})
|
||||
result, err := adapter.QuerySimDataUsage(context.Background(), &model.SimDataUsageInput{SimKeys: []*model.SimKey{{ICCID: "A"}, {ICCID: "B"}, {ICCID: "missing"}}})
|
||||
if err != nil || len(result) != 3 {
|
||||
t.Fatalf("result: %v %v", result, err)
|
||||
}
|
||||
if result[0].Code != 0 || result[0].ResData.DataUsage != 1 || result[1].ResData.DataUsage != 2 || result[2].Code == 0 {
|
||||
t.Fatalf("incorrect per-card mapping: %+v", result)
|
||||
}
|
||||
if result[0].ResData.TotalFlow != nil {
|
||||
t.Fatal("monthly usage must not invent package total")
|
||||
}
|
||||
packageFlow := result[0].ResData.PackageFlow
|
||||
if packageFlow == nil || packageFlow.TotalFlow != 10240 || packageFlow.DataUsage != 8192 || packageFlow.RemainingFlow != 2048 {
|
||||
t.Fatalf("package cumulative flow must remain in MB and separate from monthly usage: %+v", packageFlow)
|
||||
}
|
||||
if result[1].ResData.PackageFlow == nil || result[1].ResData.PackageFlow.RemainingFlow != 8192 {
|
||||
t.Fatal("package response must match the requested card, not array position")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidUsage(t *testing.T) {
|
||||
for _, data := range []string{`{"iccid":"A"}`, `{"iccid":"A","use":null}`, `{"iccid":"A","use":-1}`, `{"iccid":"A","use":"NaN"}`, `{"iccid":"A","use":""}`} {
|
||||
t.Run(data, func(t *testing.T) {
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
fmt.Fprintf(writer, `{"code":200,"data":[%s]}`, data)
|
||||
})
|
||||
result, _ := adapter.QuerySimDataUsage(context.Background(), &model.SimDataUsageInput{SimKeys: []*model.SimKey{{ICCID: "A"}}})
|
||||
if result[0].Code == 0 {
|
||||
t.Fatal("unknown usage reported as zero success")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCardStates(t *testing.T) {
|
||||
for state, want := range map[int]int{1: 1, 2: 2, 3: 4, 4: 6, 5: 7, 6: 9} {
|
||||
got, err := cardStatus(&state)
|
||||
if err != nil || got != want {
|
||||
t.Fatalf("state %d mapped to %d: %v", state, got, err)
|
||||
}
|
||||
}
|
||||
unknown := 7
|
||||
if _, err := cardStatus(&unknown); err == nil {
|
||||
t.Fatal("expired state cannot be assumed to mean cancelled")
|
||||
}
|
||||
if _, err := cardStatus(nil); err == nil {
|
||||
t.Fatal("missing state must fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchAndBasicInfo(t *testing.T) {
|
||||
calls := 0
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
calls++
|
||||
var payload struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
json.NewDecoder(request.Body).Decode(&payload)
|
||||
var data struct {
|
||||
Numbers []string `json:"numberArray"`
|
||||
}
|
||||
json.Unmarshal([]byte(payload.Data), &data)
|
||||
if len(data.Numbers) > 50 {
|
||||
t.Error("query batch exceeds 50")
|
||||
}
|
||||
rows := []map[string]any{}
|
||||
for _, value := range data.Numbers {
|
||||
rows = append(rows, map[string]any{"iccid": value, "msisdn": "144000", "state": 2, "statename": "active", "activedate": "2026-01-01"})
|
||||
}
|
||||
json.NewEncoder(writer).Encode(map[string]any{"code": 200, "data": rows})
|
||||
})
|
||||
keys := []*model.SimKey{}
|
||||
for index := 0; index < 51; index++ {
|
||||
keys = append(keys, &model.SimKey{ICCID: strconv.Itoa(index)})
|
||||
}
|
||||
result, _ := adapter.QuerySimBasicInfo(context.Background(), &model.SimBasicInfoInput{SimKeys: keys})
|
||||
if calls != 2 || len(result) != 51 {
|
||||
t.Fatalf("calls=%d rows=%d", calls, len(result))
|
||||
}
|
||||
for _, item := range result {
|
||||
if item.Code != 0 || item.ResData.ActiveDate != "" || item.ResData.IMSI != "" {
|
||||
t.Fatal("incorrect or fabricated basic fields")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperationsAreSafe(t *testing.T) {
|
||||
calls := 0
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
calls++
|
||||
if request.URL.Path != "/simlist/test-resume" && request.URL.Path != "/simlist/get-sim-switchoff-by-number" {
|
||||
t.Error("wrong operation route")
|
||||
}
|
||||
fmt.Fprint(writer, `{"code":200,"data":[{"iccid":"A","issuccess":1},{"iccid":"B","issuccess":0}]}`)
|
||||
})
|
||||
input := &model.SimCardStatusChangeInput{SimKeys: []*model.SimKey{{ICCID: "A"}, {ICCID: "B"}}, ReqData: model.SimCardStatusChangeInputItem{OperType: "0"}}
|
||||
result, _ := adapter.ChangeSimStatus(context.Background(), input)
|
||||
if calls != 1 || result[0].Code != 0 || result[1].Code == 0 {
|
||||
t.Fatal("stop operation must reach the confirmed endpoint and inspect per-card success flags")
|
||||
}
|
||||
before := calls
|
||||
input.ReqData.OperType = "1"
|
||||
result, _ = adapter.ChangeSimStatus(context.Background(), input)
|
||||
if calls != before || result[0].Code == 0 {
|
||||
t.Fatal("unconfirmed resume path must not send any request")
|
||||
}
|
||||
adapter.resumePath = "/simlist/test-resume"
|
||||
for _, operation := range []string{"0", "1"} {
|
||||
input.ReqData.OperType = operation
|
||||
result, _ = adapter.ChangeSimStatus(context.Background(), input)
|
||||
if result[0].Code != 0 || result[1].Code == 0 {
|
||||
t.Fatal("must inspect per-card success flags")
|
||||
}
|
||||
}
|
||||
before = calls
|
||||
input.ReqData.OperType = "invalid"
|
||||
adapter.ChangeSimStatus(context.Background(), input)
|
||||
if calls != before {
|
||||
t.Fatal("invalid operation sent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseFailuresAndNoRetry(t *testing.T) {
|
||||
for _, body := range []string{`{}`, `{"code":401,"msg":"test-secret"}`, `{"code":200}`, `{"code":200,"data":null}`, `{"code":200,"data":[{"iccid":"A","state":2},{"iccid":"A","state":3}]}`} {
|
||||
t.Run(body, func(t *testing.T) {
|
||||
calls := 0
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) { calls++; fmt.Fprint(writer, body) })
|
||||
result, _ := adapter.QuerySimCardStatus(context.Background(), &model.SimCardStatusInput{SimKeys: []*model.SimKey{{ICCID: "A"}}})
|
||||
if calls != 1 || result[0].Code == 0 || strings.Contains(result[0].Message, "test-secret") {
|
||||
t.Fatal("invalid response accepted, retried or leaked secret")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguration(t *testing.T) {
|
||||
cfg := &model.SimChannelConfigItemCache{Template: &entity.TSimChannelTemplate{TempConfig: `{"api":{"timeout":"1s"}}`}, Channel: &entity.TSimChannels{AppKey: "app", AppSecret: "secret"}}
|
||||
adapter, err := New(cfg)
|
||||
if err != nil || adapter.resumePath != "" {
|
||||
t.Fatalf("invalid default configuration: %v", err)
|
||||
}
|
||||
cfg.Channel.AppSecret = ""
|
||||
if _, err := New(cfg); err == nil {
|
||||
t.Fatal("missing secret accepted")
|
||||
}
|
||||
cfg.Channel.AppSecret = "secret"
|
||||
for _, raw := range []string{`{"api":`, `{"api":{"timeout":"0s"}}`, `{"api":{"resumePath":"/simlist/get-sim-switchoff-by-number"}}`} {
|
||||
cfg.Template.TempConfig = raw
|
||||
if _, err := New(cfg); err == nil {
|
||||
t.Fatal("invalid config accepted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperationBatchLimitAndHTTPFailure(t *testing.T) {
|
||||
calls := 0
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
calls++
|
||||
var payload struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
var data struct {
|
||||
Numbers []string `json:"numberArray"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(payload.Data), &data); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(data.Numbers) > 20 {
|
||||
t.Error("operation batch exceeds 20")
|
||||
}
|
||||
writer.WriteHeader(http.StatusTooManyRequests)
|
||||
})
|
||||
keys := make([]*model.SimKey, 21)
|
||||
for index := range keys {
|
||||
keys[index] = &model.SimKey{ICCID: strconv.Itoa(index)}
|
||||
}
|
||||
output, err := adapter.ChangeSimStatus(context.Background(), &model.SimCardStatusChangeInput{SimKeys: keys, ReqData: model.SimCardStatusChangeInputItem{OperType: "0"}})
|
||||
if err != nil || calls != 2 || len(output) != 21 {
|
||||
t.Fatalf("calls=%d len=%d err=%v", calls, len(output), err)
|
||||
}
|
||||
for _, item := range output {
|
||||
if item.Code == 0 || !strings.Contains(item.Message, "429") {
|
||||
t.Fatal("HTTP failure must not appear as successful operation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountGateSharing(t *testing.T) {
|
||||
first, err := NewClient("http://127.0.0.1", "same-account", "secret", time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := NewClient("http://127.0.0.1", "same-account", "secret", time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first.gate != second.gate {
|
||||
t.Fatal("same account must share rate limiter")
|
||||
}
|
||||
first.gate.mu.Lock()
|
||||
first.gate.next = time.Now().Add(time.Second)
|
||||
first.gate.mu.Unlock()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)
|
||||
defer cancel()
|
||||
if err := second.gate.wait(ctx); err == nil {
|
||||
t.Fatal("rate limit wait must honor context timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelledAndUnsupportedDoNotCall(t *testing.T) {
|
||||
calls := 0
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) { calls++ })
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
adapter.QuerySimCardStatus(ctx, &model.SimCardStatusInput{SimKeys: []*model.SimKey{{ICCID: "A"}}})
|
||||
adapter.QuerySimDataUsage(context.Background(), &model.SimDataUsageInput{SimKeys: []*model.SimKey{nil, {IMSI: "only-imsi"}}})
|
||||
adapter.QuerySimImei(context.Background(), &model.SimImeiInput{SimKeys: []*model.SimKey{{ICCID: "A"}}})
|
||||
if calls != 0 {
|
||||
t.Fatal("unexpected upstream request")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package mingqian
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type PackageFlowOutput = []*model.SimBaseRes[model.SimPackageFlowItem]
|
||||
|
||||
func (adapter *Adapter) QueryPackageFlow(ctx context.Context, input *model.SimDataUsageInput) (PackageFlowOutput, error) {
|
||||
if input == nil {
|
||||
return nil, gerror.New("mingqian: input required")
|
||||
}
|
||||
return query(ctx, adapter, input.SimKeys, packageUsagePath, 50, func(item card) (model.SimPackageFlowItem, error) {
|
||||
if item.PackageTotal == nil || item.PackageUsage == nil || *item.PackageTotal < 0 || *item.PackageUsage < 0 {
|
||||
return model.SimPackageFlowItem{}, gerror.New("mingqian: package total or cumulative usage missing or unknown")
|
||||
}
|
||||
var (
|
||||
total = float64(*item.PackageTotal)
|
||||
used = float64(*item.PackageUsage)
|
||||
remaining = math.Max(total-used, 0)
|
||||
)
|
||||
return model.SimPackageFlowItem{TotalFlow: total, DataUsage: used, RemainingFlow: remaining}, nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
func QueryChannelsPackageFlow(ctx context.Context, input *model.SimDataUsageInput, lookup func(string) (*model.SimCardChannelCache, bool)) (PackageFlowOutput, error) {
|
||||
if input == nil || lookup == nil {
|
||||
return nil, gerror.New("mingqian: input and channel lookup required")
|
||||
}
|
||||
var (
|
||||
output = make(PackageFlowOutput, len(input.SimKeys))
|
||||
groups = make(map[string][]int)
|
||||
)
|
||||
for index, key := range input.SimKeys {
|
||||
output[index] = &model.SimBaseRes[model.SimPackageFlowItem]{SimKey: key, Code: consts.ApiStatusError, Message: "mingqian: explicit chanKey and ICCID or MSISDN required"}
|
||||
if key == nil || strings.TrimSpace(key.ChanKey) == "" || identifier(key) == "" {
|
||||
continue
|
||||
}
|
||||
channelKey := strings.TrimSpace(key.ChanKey)
|
||||
groups[channelKey] = append(groups[channelKey], index)
|
||||
}
|
||||
for channelKey, indices := range groups {
|
||||
var (
|
||||
adapter *Adapter
|
||||
message string
|
||||
)
|
||||
cached, ok := lookup(channelKey)
|
||||
if !ok || cached == nil || cached.Instance == nil {
|
||||
message = "mingqian: channel not instantiated"
|
||||
} else {
|
||||
adapter, ok = cached.Instance.(*Adapter)
|
||||
if !ok || adapter == nil {
|
||||
message = "mingqian: package flow endpoint only supports sim_mingqian_v3 channels"
|
||||
}
|
||||
}
|
||||
if message != "" {
|
||||
for _, index := range indices {
|
||||
output[index].Message = message
|
||||
}
|
||||
continue
|
||||
}
|
||||
keys := make([]*model.SimKey, len(indices))
|
||||
for position, index := range indices {
|
||||
keys[position] = input.SimKeys[index]
|
||||
}
|
||||
results, err := adapter.QueryPackageFlow(ctx, &model.SimDataUsageInput{SimKeys: keys})
|
||||
for position, index := range indices {
|
||||
if err != nil {
|
||||
output[index].Message = err.Error()
|
||||
} else {
|
||||
output[index] = results[position]
|
||||
}
|
||||
}
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package mingqian
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
||||
)
|
||||
|
||||
func (adapter *Adapter) QuerySimSession(ctx context.Context, input *model.SimSessionInput) (model.SimSessionOutput, error) {
|
||||
return unsupported[model.SimSessionItem](input.SimKeys, "current API documentation provides no real-time session, online status, APN or IP query; card lifecycle state is not online status"), nil
|
||||
}
|
||||
func (adapter *Adapter) QuerySimStopReason(ctx context.Context, input *model.SimStopReasonInput) (model.SimStopReasonOutput, error) {
|
||||
return unsupported[model.SimStopReasonItem](input.SimKeys, "stop reason query unsupported"), nil
|
||||
}
|
||||
func (adapter *Adapter) QuerySimFrozenState(ctx context.Context, input *model.SimFrozenStateInput) (model.SimFrozenStateOutput, error) {
|
||||
return unsupported[model.SimFrozenStateItem](input.SimKeys, "frozen state query unsupported"), nil
|
||||
}
|
||||
func (adapter *Adapter) QuerySimCardBindStatus(ctx context.Context, input *model.SimCardBindStatusInput) (model.SimCardBindStatusOutput, error) {
|
||||
return unsupported[model.SimCardBindStatusItem](input.SimKeys, "binding status not implied by IMEI; query unsupported"), nil
|
||||
}
|
||||
func (adapter *Adapter) QuerySimDeviceStatus(ctx context.Context, input *model.SimDeviceStatusInput) (model.SimDeviceStatusOutput, error) {
|
||||
return unsupported[model.SimDeviceStatusItem](input.SimKeys, "device status query unsupported"), nil
|
||||
}
|
||||
func (adapter *Adapter) QuerySimRegionLimitStatus(ctx context.Context, input *model.SimRegionLimitStatusInput) (model.SimRegionLimitStatusOutput, error) {
|
||||
return unsupported[model.SimRegionLimitStatusItem](input.SimKeys, "region limit query unsupported"), nil
|
||||
}
|
||||
func (adapter *Adapter) QuerySimImei(ctx context.Context, input *model.SimImeiInput) (model.SimImeiOutput, error) {
|
||||
return unsupported[model.SimImeiItem](input.SimKeys, "per-card IMEI endpoint unavailable; account inventory sync not implemented"), nil
|
||||
}
|
||||
func (adapter *Adapter) QuerySimDataDiagnosis(ctx context.Context, input *model.SimDataDiagnosisInput) (model.SimDataDiagnosisOutput, error) {
|
||||
return unsupported[any](input.SimKeys, "data diagnosis unsupported"), nil
|
||||
}
|
||||
func (adapter *Adapter) QuerySimDataUsageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) {
|
||||
return unsupported[model.SimDataUsageMonthlyOutputItem](input.SimKeys, "historical monthly usage unsupported"), nil
|
||||
}
|
||||
func (adapter *Adapter) QuerySimDataUsageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) {
|
||||
return unsupported[model.SimDataUsageDailyOutputItem](input.SimKeys, "historical daily usage unsupported"), nil
|
||||
}
|
||||
func (adapter *Adapter) QuerySimApnInfo(ctx context.Context, input *model.SimApnInfoInput) (model.SimApnInfoOutput, error) {
|
||||
return unsupported[model.SimApnInfoItemList](input.SimKeys, "APN query unsupported"), nil
|
||||
}
|
||||
func (adapter *Adapter) ChangeSimApnFunction(ctx context.Context, input *model.SimCardOperateChangeApnInput) (model.SimCardOperateChangeApnOutput, error) {
|
||||
return unsupported[model.SimCardOperateChangeApnItem](input.SimKeys, "APN changes unsupported"), nil
|
||||
}
|
||||
Reference in New Issue
Block a user