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:
MiFaZhan
2026-10-22 15:42:29 +08:00
commit 96b1edff45
191 changed files with 30805 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
// ================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// You can delete these comments if you wish manually maintain this interface file.
// ================================================================================
package service
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
)
type (
IChannelConfig interface {
Start(ctx context.Context) error
Stop(ctx context.Context) error
Loop(ctx context.Context) error
LoadAllChannelConfig(ctx context.Context) error
RefreshChannel(ctx context.Context, chanKey string) error
RemoveChannel(ctx context.Context, chanKey string) error
LoadChannelForTest(ctx context.Context, channelID int64, tenantID string) (*model.SimChannelConfigItemCache, error)
// get alarm cache by id
GetChannel(chanKey string) (*model.SimChannelConfigItemCache, bool)
GetAllChannel(ctx context.Context) ([]*model.SimChannelConfigItemCache, error)
}
IChannelUpdate interface {
UpdateAccessToken(ctx context.Context, input *model.UpdateAccessTokenInput) error
}
)
var (
localChannelConfig IChannelConfig
localChannelUpdate IChannelUpdate
)
func ChannelConfig() IChannelConfig {
if localChannelConfig == nil {
panic("implement not found for interface IChannelConfig, forgot register?")
}
return localChannelConfig
}
func RegisterChannelConfig(i IChannelConfig) {
localChannelConfig = i
}
func ChannelUpdate() IChannelUpdate {
if localChannelUpdate == nil {
panic("implement not found for interface IChannelUpdate, forgot register?")
}
return localChannelUpdate
}
func RegisterChannelUpdate(i IChannelUpdate) {
localChannelUpdate = i
}
+169
View File
@@ -0,0 +1,169 @@
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)
}
+273
View File
@@ -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
}
+172
View File
@@ -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
}
+159
View File
@@ -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")
}
}
+294
View File
@@ -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")
}
}
+82
View File
@@ -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
}
+43
View File
@@ -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
}
+74
View File
@@ -0,0 +1,74 @@
// ================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// You can delete these comments if you wish manually maintain this interface file.
// ================================================================================
package service
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
)
type (
IUpstreamAPI interface {
Start(ctx context.Context) error
Stop(ctx context.Context) error
RefreshChannel(ctx context.Context, chanKey string) error
RemoveChannel(ctx context.Context, chanKey string) error
TestChannel(ctx context.Context, input *model.ChannelTestInput) (*model.ChannelTestOutput, error)
RegisterChan(chanKey string, version string, channel *model.SimCardChannelCache)
RegisterTemp(tempKey string, channel model.SimCardTempI)
GetTemp(tempKey string) (model.SimCardTempI, bool)
RegisterModel(modelKey string, factory model.SimCardModelFactory)
GetModel(modelKey string) (model.SimCardModelFactory, bool)
GetChan(chanKey string) (*model.SimCardChannelCache, bool)
// 物联网卡基本信息查询
QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error)
// 物联网卡在线信息实时查询
QuerySimSession(ctx context.Context, input *model.SimSessionInput) (model.SimSessionOutput, error)
// 物联网卡卡状态查询
QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) (output model.SimCardStatusOutput, err error)
QuerySimStopReason(ctx context.Context, input *model.SimStopReasonInput) (output model.SimStopReasonOutput, err error)
QuerySimFrozenState(ctx context.Context, input *model.SimFrozenStateInput) (output model.SimFrozenStateOutput, err error)
QuerySimCardStatusBatch(ctx context.Context, input *model.SimCardStatusInput) (output model.SimCardStatusOutput, err error)
// 物联网卡机卡绑定状态查询
QuerySimCardBindStatus(ctx context.Context, input *model.SimCardBindStatusInput) (output model.SimCardBindStatusOutput, err error)
// 物联网卡设备状态查询
QuerySimDeviceStatus(ctx context.Context, input *model.SimDeviceStatusInput) (output model.SimDeviceStatusOutput, err error)
// 物联网卡地域限制状态查询
QuerySimRegionLimitStatus(ctx context.Context, input *model.SimRegionLimitStatusInput) (output model.SimRegionLimitStatusOutput, err error)
// 物联网卡IMEI查询
QuerySimImei(ctx context.Context, input *model.SimImeiInput) (output model.SimImeiOutput, err error)
// 单卡本月流量累计使用量查询
QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) (output model.SimDataUsageOutput, err error)
// 物联卡达量限速档位查询
QuerySimDataDiagnosis(ctx context.Context, input *model.SimDataDiagnosisInput) (output model.SimDataDiagnosisOutput, err error)
// 物联卡单月 GPRS流量使用量批量查询
QuerySimDataUsageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) (output model.SimDataUsageMonthlyOutput, err error)
QuerySimDataUsageMonthlyBatch(ctx context.Context, input *model.SimDataUsageMonthlyInput) (output model.SimDataUsageMonthlyOutput, err error)
// 物联网卡数据流量查询 日查询
QuerySimDataUsageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) (output model.SimDataUsageDailyOutput, err error)
QuerySimDataUsageDailyBatch(ctx context.Context, input *model.SimDataUsageDailyInput) (output model.SimDataUsageDailyOutput, err error)
// 物联网卡已开通APN信息查询
QuerySimApnInfo(ctx context.Context, input *model.SimApnInfoInput) (output model.SimApnInfoOutput, err error)
// 物联网卡状态变更
ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) (output model.SimCardStatusChangeOutput, err error)
ChangeSimCardOperateApn(ctx context.Context, input *model.SimCardOperateChangeApnInput) (output model.SimCardOperateChangeApnOutput, err error)
}
)
var (
localUpstreamAPI IUpstreamAPI
)
func UpstreamAPI() IUpstreamAPI {
if localUpstreamAPI == nil {
panic("implement not found for interface IUpstreamAPI, forgot register?")
}
return localUpstreamAPI
}
func RegisterUpstreamAPI(i IUpstreamAPI) {
localUpstreamAPI = i
}
+112
View File
@@ -0,0 +1,112 @@
// ================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// You can delete these comments if you wish manually maintain this interface file.
// ================================================================================
package service
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
)
type (
ISimDX interface {
NewTemp(ctx context.Context, channelConfig *model.SimChannelConfigItemCache) model.SimCardChannelI
QuerySimSession(ctx context.Context, input *model.SimSessionInput) (model.SimSessionOutput, error)
QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error)
QuerySimStopReason(ctx context.Context, input *model.SimStopReasonInput) (model.SimStopReasonOutput, error)
QuerySimFrozenState(ctx context.Context, input *model.SimFrozenStateInput) (model.SimFrozenStateOutput, error)
QuerySimImei(ctx context.Context, input *model.SimImeiInput) (model.SimImeiOutput, error)
QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) (model.SimDataUsageOutput, error)
QuerySimDataDiagnosis(ctx context.Context, input *model.SimDataDiagnosisInput) (model.SimDataDiagnosisOutput, error)
QuerySimDataUsageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error)
QuerySimDataUsageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error)
QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error)
QuerySimApnInfo(ctx context.Context, input *model.SimApnInfoInput) (model.SimApnInfoOutput, error)
ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) (model.SimCardStatusChangeOutput, error)
ChangeSimApnFunction(ctx context.Context, input *model.SimCardOperateChangeApnInput) (model.SimCardOperateChangeApnOutput, error)
QuerySimCardBindStatus(ctx context.Context, input *model.SimCardBindStatusInput) (model.SimCardBindStatusOutput, error)
QuerySimDeviceStatus(ctx context.Context, input *model.SimDeviceStatusInput) (model.SimDeviceStatusOutput, error)
QuerySimRegionLimitStatus(ctx context.Context, input *model.SimRegionLimitStatusInput) (model.SimRegionLimitStatusOutput, error)
}
ISimYD interface {
NewTemp(ctx context.Context, channelConfig *model.SimChannelConfigItemCache) model.SimCardChannelI
QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error)
QuerySimSession(ctx context.Context, input *model.SimSessionInput) (model.SimSessionOutput, error)
QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error)
QuerySimStopReason(ctx context.Context, input *model.SimStopReasonInput) (model.SimStopReasonOutput, error)
QuerySimFrozenState(ctx context.Context, input *model.SimFrozenStateInput) (model.SimFrozenStateOutput, error)
QuerySimImei(ctx context.Context, input *model.SimImeiInput) (model.SimImeiOutput, error)
QuerySimDataUsageInPool(ctx context.Context, input *model.SimDataUsageInPoolInput) (model.SimDataUsageInPoolOutput, error)
QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) (model.SimDataUsageOutput, error)
QuerySimDataDiagnosis(ctx context.Context, input *model.SimDataDiagnosisInput) (model.SimDataDiagnosisOutput, error)
QuerySimDataUsageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error)
QuerySimDataUsageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error)
QuerySimApnInfo(ctx context.Context, input *model.SimApnInfoInput) (model.SimApnInfoOutput, error)
ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) (model.SimCardStatusChangeOutput, error)
ChangeSimApnFunction(ctx context.Context, input *model.SimCardOperateChangeApnInput) (model.SimCardOperateChangeApnOutput, error)
QuerySimCardBindStatus(ctx context.Context, input *model.SimCardBindStatusInput) (model.SimCardBindStatusOutput, error)
QuerySimDeviceStatus(ctx context.Context, input *model.SimDeviceStatusInput) (model.SimDeviceStatusOutput, error)
QuerySimRegionLimitStatus(ctx context.Context, input *model.SimRegionLimitStatusInput) (model.SimRegionLimitStatusOutput, error)
}
ISimYDMock interface {
NewTemp(ctx context.Context, channelConfig *model.SimChannelConfigItemCache) model.SimCardChannelI
QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error)
QuerySimSession(ctx context.Context, input *model.SimSessionInput) (model.SimSessionOutput, error)
QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error)
QuerySimStopReason(ctx context.Context, input *model.SimStopReasonInput) (model.SimStopReasonOutput, error)
QuerySimFrozenState(ctx context.Context, input *model.SimFrozenStateInput) (model.SimFrozenStateOutput, error)
QuerySimImei(ctx context.Context, input *model.SimImeiInput) (model.SimImeiOutput, error)
QuerySimDataUsageInPool(ctx context.Context, input *model.SimDataUsageInPoolInput) (model.SimDataUsageInPoolOutput, error)
QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) (model.SimDataUsageOutput, error)
QuerySimDataDiagnosis(ctx context.Context, input *model.SimDataDiagnosisInput) (model.SimDataDiagnosisOutput, error)
QuerySimDataUsageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error)
QuerySimDataUsageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error)
QuerySimApnInfo(ctx context.Context, input *model.SimApnInfoInput) (model.SimApnInfoOutput, error)
ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) (model.SimCardStatusChangeOutput, error)
ChangeSimApnFunction(ctx context.Context, input *model.SimCardOperateChangeApnInput) (model.SimCardOperateChangeApnOutput, error)
QuerySimCardBindStatus(ctx context.Context, input *model.SimCardBindStatusInput) (model.SimCardBindStatusOutput, error)
QuerySimDeviceStatus(ctx context.Context, input *model.SimDeviceStatusInput) (model.SimDeviceStatusOutput, error)
QuerySimRegionLimitStatus(ctx context.Context, input *model.SimRegionLimitStatusInput) (model.SimRegionLimitStatusOutput, error)
}
)
var (
localSimDX ISimDX
localSimYD ISimYD
localSimYDMock ISimYDMock
)
func SimDX() ISimDX {
if localSimDX == nil {
panic("implement not found for interface ISimDX, forgot register?")
}
return localSimDX
}
func RegisterSimDX(i ISimDX) {
localSimDX = i
}
func SimYD() ISimYD {
if localSimYD == nil {
panic("implement not found for interface ISimYD, forgot register?")
}
return localSimYD
}
func RegisterSimYD(i ISimYD) {
localSimYD = i
}
func SimYDMock() ISimYDMock {
if localSimYDMock == nil {
panic("implement not found for interface ISimYDMock, forgot register?")
}
return localSimYDMock
}
func RegisterSimYDMock(i ISimYDMock) {
localSimYDMock = i
}