chore: apply task changes
This commit is contained in:
130
internal/gateway/client.go
Normal file
130
internal/gateway/client.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Package gateway 提供 Gateway API 的统一客户端封装
|
||||
// 实现 AES-128-ECB 加密 + MD5 签名认证机制
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 30 * time.Second
|
||||
maxIdleConns = 100
|
||||
maxIdleConnsPerHost = 10
|
||||
idleConnTimeout = 90 * time.Second
|
||||
contentTypeJSON = "application/json;charset=utf-8"
|
||||
gatewaySuccessCode = 200
|
||||
)
|
||||
|
||||
// Client 是 Gateway API 的 HTTP 客户端
|
||||
type Client struct {
|
||||
baseURL string
|
||||
appID string
|
||||
appSecret string
|
||||
httpClient *http.Client
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewClient 创建 Gateway 客户端实例
|
||||
// baseURL: Gateway 服务基础地址
|
||||
// appID: 应用 ID
|
||||
// appSecret: 应用密钥(用于加密和签名)
|
||||
func NewClient(baseURL, appID, appSecret string) *Client {
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
appID: appID,
|
||||
appSecret: appSecret,
|
||||
httpClient: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: maxIdleConns,
|
||||
MaxIdleConnsPerHost: maxIdleConnsPerHost,
|
||||
IdleConnTimeout: idleConnTimeout,
|
||||
},
|
||||
},
|
||||
timeout: defaultTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// WithTimeout 设置请求超时时间(支持链式调用)
|
||||
func (c *Client) WithTimeout(timeout time.Duration) *Client {
|
||||
c.timeout = timeout
|
||||
return c
|
||||
}
|
||||
|
||||
// doRequest 执行 Gateway API 请求的统一方法
|
||||
// 流程:序列化 → 加密 → 签名 → HTTP POST → 解析响应 → 检查业务状态码
|
||||
func (c *Client) doRequest(ctx context.Context, path string, businessData interface{}) (json.RawMessage, error) {
|
||||
dataBytes, err := sonic.Marshal(businessData)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "序列化业务数据失败")
|
||||
}
|
||||
|
||||
encryptedData, err := aesEncrypt(dataBytes, c.appSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timestamp := time.Now().UnixMilli()
|
||||
sign := generateSign(c.appID, encryptedData, timestamp, c.appSecret)
|
||||
|
||||
reqBody := map[string]interface{}{
|
||||
"appId": c.appID,
|
||||
"data": encryptedData,
|
||||
"sign": sign,
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
|
||||
reqBodyBytes, err := sonic.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "序列化请求体失败")
|
||||
}
|
||||
|
||||
reqCtx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, c.baseURL+path, bytes.NewReader(reqBodyBytes))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeGatewayError, err, "创建 HTTP 请求失败")
|
||||
}
|
||||
req.Header.Set("Content-Type", contentTypeJSON)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
if reqCtx.Err() == context.DeadlineExceeded {
|
||||
return nil, errors.Wrap(errors.CodeGatewayTimeout, err, "Gateway 请求超时")
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return nil, errors.Wrap(errors.CodeGatewayError, ctx.Err(), "请求被取消")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeGatewayError, err, "发送 HTTP 请求失败")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(errors.CodeGatewayError, fmt.Sprintf("HTTP 状态码异常: %d", resp.StatusCode))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeGatewayInvalidResp, err, "读取响应体失败")
|
||||
}
|
||||
|
||||
var gatewayResp GatewayResponse
|
||||
if err := sonic.Unmarshal(body, &gatewayResp); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeGatewayInvalidResp, err, "解析 Gateway 响应失败")
|
||||
}
|
||||
|
||||
if gatewayResp.Code != gatewaySuccessCode {
|
||||
return nil, errors.New(errors.CodeGatewayError, fmt.Sprintf("Gateway 业务错误: code=%d, msg=%s", gatewayResp.Code, gatewayResp.Msg))
|
||||
}
|
||||
|
||||
return gatewayResp.Data, nil
|
||||
}
|
||||
323
internal/gateway/client_test.go
Normal file
323
internal/gateway/client_test.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
client := NewClient("https://test.example.com", "testAppID", "testSecret")
|
||||
|
||||
if client.baseURL != "https://test.example.com" {
|
||||
t.Errorf("baseURL = %s, want https://test.example.com", client.baseURL)
|
||||
}
|
||||
if client.appID != "testAppID" {
|
||||
t.Errorf("appID = %s, want testAppID", client.appID)
|
||||
}
|
||||
if client.appSecret != "testSecret" {
|
||||
t.Errorf("appSecret = %s, want testSecret", client.appSecret)
|
||||
}
|
||||
if client.timeout != 30*time.Second {
|
||||
t.Errorf("timeout = %v, want 30s", client.timeout)
|
||||
}
|
||||
if client.httpClient == nil {
|
||||
t.Error("httpClient should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithTimeout(t *testing.T) {
|
||||
client := NewClient("https://test.example.com", "testAppID", "testSecret").
|
||||
WithTimeout(60 * time.Second)
|
||||
|
||||
if client.timeout != 60*time.Second {
|
||||
t.Errorf("timeout = %v, want 60s", client.timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithTimeout_Chain(t *testing.T) {
|
||||
// 验证链式调用返回同一个 Client 实例
|
||||
client := NewClient("https://test.example.com", "testAppID", "testSecret")
|
||||
returned := client.WithTimeout(45 * time.Second)
|
||||
|
||||
if returned != client {
|
||||
t.Error("WithTimeout should return the same Client instance for chaining")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoRequest_Success(t *testing.T) {
|
||||
// 创建 mock HTTP 服务器
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 验证请求方法
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("Method = %s, want POST", r.Method)
|
||||
}
|
||||
|
||||
// 验证 Content-Type
|
||||
if r.Header.Get("Content-Type") != "application/json;charset=utf-8" {
|
||||
t.Errorf("Content-Type = %s, want application/json;charset=utf-8", r.Header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
// 验证请求体格式
|
||||
var reqBody map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
||||
t.Fatalf("解析请求体失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证必需字段
|
||||
if _, ok := reqBody["appId"]; !ok {
|
||||
t.Error("请求体缺少 appId 字段")
|
||||
}
|
||||
if _, ok := reqBody["data"]; !ok {
|
||||
t.Error("请求体缺少 data 字段")
|
||||
}
|
||||
if _, ok := reqBody["sign"]; !ok {
|
||||
t.Error("请求体缺少 sign 字段")
|
||||
}
|
||||
if _, ok := reqBody["timestamp"]; !ok {
|
||||
t.Error("请求体缺少 timestamp 字段")
|
||||
}
|
||||
|
||||
// 返回 mock 响应
|
||||
resp := GatewayResponse{
|
||||
Code: 200,
|
||||
Msg: "成功",
|
||||
Data: json.RawMessage(`{"test":"data"}`),
|
||||
TraceID: "test-trace-id",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
|
||||
ctx := context.Background()
|
||||
businessData := map[string]interface{}{
|
||||
"params": map[string]string{
|
||||
"cardNo": "898608070422D0010269",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := client.doRequest(ctx, "/test", businessData)
|
||||
if err != nil {
|
||||
t.Fatalf("doRequest() error = %v", err)
|
||||
}
|
||||
|
||||
if string(data) != `{"test":"data"}` {
|
||||
t.Errorf("data = %s, want {\"test\":\"data\"}", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoRequest_BusinessError(t *testing.T) {
|
||||
// 创建返回业务错误的 mock 服务器
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{
|
||||
Code: 500,
|
||||
Msg: "业务处理失败",
|
||||
Data: nil,
|
||||
TraceID: "error-trace-id",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := client.doRequest(ctx, "/test", map[string]interface{}{})
|
||||
if err == nil {
|
||||
t.Fatal("doRequest() expected business error")
|
||||
}
|
||||
|
||||
// 验证错误信息包含业务错误内容
|
||||
if !strings.Contains(err.Error(), "业务错误") {
|
||||
t.Errorf("error should contain '业务错误', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoRequest_Timeout(t *testing.T) {
|
||||
// 创建延迟响应的服务器
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(500 * time.Millisecond) // 延迟 500ms
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret").
|
||||
WithTimeout(100 * time.Millisecond) // 设置 100ms 超时
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := client.doRequest(ctx, "/test", map[string]interface{}{})
|
||||
if err == nil {
|
||||
t.Fatal("doRequest() expected timeout error")
|
||||
}
|
||||
|
||||
// 验证是超时错误
|
||||
if !strings.Contains(err.Error(), "超时") {
|
||||
t.Errorf("error should contain '超时', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoRequest_HTTPStatusError(t *testing.T) {
|
||||
// 创建返回 500 状态码的服务器
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Internal Server Error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := client.doRequest(ctx, "/test", map[string]interface{}{})
|
||||
if err == nil {
|
||||
t.Fatal("doRequest() expected HTTP status error")
|
||||
}
|
||||
|
||||
// 验证错误信息包含 HTTP 状态码
|
||||
if !strings.Contains(err.Error(), "500") {
|
||||
t.Errorf("error should contain '500', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoRequest_InvalidResponse(t *testing.T) {
|
||||
// 创建返回无效 JSON 的服务器
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("invalid json"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := client.doRequest(ctx, "/test", map[string]interface{}{})
|
||||
if err == nil {
|
||||
t.Fatal("doRequest() expected JSON parse error")
|
||||
}
|
||||
|
||||
// 验证错误信息包含解析失败提示
|
||||
if !strings.Contains(err.Error(), "解析") {
|
||||
t.Errorf("error should contain '解析', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoRequest_ContextCanceled(t *testing.T) {
|
||||
// 创建正常响应的服务器(但会延迟)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
resp := GatewayResponse{Code: 200, Msg: "成功"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
|
||||
// 创建已取消的 context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // 立即取消
|
||||
|
||||
_, err := client.doRequest(ctx, "/test", map[string]interface{}{})
|
||||
if err == nil {
|
||||
t.Fatal("doRequest() expected context canceled error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoRequest_NetworkError(t *testing.T) {
|
||||
// 使用无效的服务器地址
|
||||
client := NewClient("http://127.0.0.1:1", "testAppID", "testSecret").
|
||||
WithTimeout(1 * time.Second)
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := client.doRequest(ctx, "/test", map[string]interface{}{})
|
||||
if err == nil {
|
||||
t.Fatal("doRequest() expected network error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoRequest_EmptyBusinessData(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{
|
||||
Code: 200,
|
||||
Msg: "成功",
|
||||
Data: json.RawMessage(`{}`),
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
|
||||
ctx := context.Background()
|
||||
data, err := client.doRequest(ctx, "/test", map[string]interface{}{})
|
||||
if err != nil {
|
||||
t.Fatalf("doRequest() error = %v", err)
|
||||
}
|
||||
|
||||
if string(data) != `{}` {
|
||||
t.Errorf("data = %s, want {}", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegration_QueryCardStatus(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("跳过集成测试")
|
||||
}
|
||||
|
||||
baseURL := "https://lplan.whjhft.com/openapi"
|
||||
appID := "60bgt1X8i7AvXqkd"
|
||||
appSecret := "BZeQttaZQt0i73moF"
|
||||
|
||||
client := NewClient(baseURL, appID, appSecret).WithTimeout(30 * time.Second)
|
||||
ctx := context.Background()
|
||||
|
||||
resp, err := client.QueryCardStatus(ctx, &CardStatusReq{
|
||||
CardNo: "898608070422D0010269",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("QueryCardStatus() error = %v", err)
|
||||
}
|
||||
|
||||
if resp.ICCID == "" {
|
||||
t.Error("ICCID should not be empty")
|
||||
}
|
||||
if resp.CardStatus == "" {
|
||||
t.Error("CardStatus should not be empty")
|
||||
}
|
||||
|
||||
t.Logf("Integration test passed: ICCID=%s, Status=%s", resp.ICCID, resp.CardStatus)
|
||||
}
|
||||
|
||||
func TestIntegration_QueryFlow(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("跳过集成测试")
|
||||
}
|
||||
|
||||
baseURL := "https://lplan.whjhft.com/openapi"
|
||||
appID := "60bgt1X8i7AvXqkd"
|
||||
appSecret := "BZeQttaZQt0i73moF"
|
||||
|
||||
client := NewClient(baseURL, appID, appSecret).WithTimeout(30 * time.Second)
|
||||
ctx := context.Background()
|
||||
|
||||
resp, err := client.QueryFlow(ctx, &FlowQueryReq{
|
||||
CardNo: "898608070422D0010269",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("QueryFlow() error = %v", err)
|
||||
}
|
||||
|
||||
if resp.UsedFlow < 0 {
|
||||
t.Error("UsedFlow should not be negative")
|
||||
}
|
||||
|
||||
t.Logf("Integration test passed: UsedFlow=%d %s", resp.UsedFlow, resp.Unit)
|
||||
}
|
||||
89
internal/gateway/crypto.go
Normal file
89
internal/gateway/crypto.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Package gateway 提供 Gateway API 加密和签名工具函数
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
const aesBlockSize = 16
|
||||
|
||||
// 注意:AES-ECB 存在严重安全缺陷(相同明文块会产生相同密文块),
|
||||
// 这是 Gateway 强制要求无法改变,生产环境必须使用 HTTPS 保障传输层安全。
|
||||
func aesEncrypt(data []byte, appSecret string) (string, error) {
|
||||
key := md5.Sum([]byte(appSecret))
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errors.CodeGatewayEncryptError, err, "数据加密失败")
|
||||
}
|
||||
|
||||
// 使用 PKCS5 进行填充,确保明文长度为 16 的整数倍
|
||||
padded := pkcs5Padding(data, aesBlockSize)
|
||||
encrypted := make([]byte, len(padded))
|
||||
newECBEncrypter(block).CryptBlocks(encrypted, padded)
|
||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||
}
|
||||
|
||||
// generateSign 生成 Gateway 签名(appId、data、timestamp、key 字母序)
|
||||
func generateSign(appID, encryptedData string, timestamp int64, appSecret string) string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("appId=")
|
||||
builder.WriteString(appID)
|
||||
builder.WriteString("&data=")
|
||||
builder.WriteString(encryptedData)
|
||||
builder.WriteString("×tamp=")
|
||||
builder.WriteString(strconv.FormatInt(timestamp, 10))
|
||||
builder.WriteString("&key=")
|
||||
builder.WriteString(appSecret)
|
||||
|
||||
sum := md5.Sum([]byte(builder.String()))
|
||||
return strings.ToUpper(hex.EncodeToString(sum[:]))
|
||||
}
|
||||
|
||||
// ecb 表示 AES-ECB 加密模式的基础结构
|
||||
type ecb struct {
|
||||
b cipher.Block
|
||||
blockSize int
|
||||
}
|
||||
|
||||
type ecbEncrypter ecb
|
||||
|
||||
func newECBEncrypter(b cipher.Block) cipher.BlockMode {
|
||||
if b == nil {
|
||||
panic("crypto/cipher: 传入的加密块为空")
|
||||
}
|
||||
return &ecbEncrypter{b: b, blockSize: b.BlockSize()}
|
||||
}
|
||||
|
||||
func (x *ecbEncrypter) BlockSize() int {
|
||||
return x.blockSize
|
||||
}
|
||||
|
||||
func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
|
||||
if len(src)%x.blockSize != 0 {
|
||||
panic("crypto/cipher: 输入数据不是完整块")
|
||||
}
|
||||
for len(src) > 0 {
|
||||
x.b.Encrypt(dst, src[:x.blockSize])
|
||||
src = src[x.blockSize:]
|
||||
dst = dst[x.blockSize:]
|
||||
}
|
||||
}
|
||||
|
||||
// pkcs5Padding 对明文进行 PKCS5 填充
|
||||
func pkcs5Padding(data []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(data)%blockSize
|
||||
padded := make([]byte, len(data)+padding)
|
||||
copy(padded, data)
|
||||
for i := len(data); i < len(padded); i++ {
|
||||
padded[i] = byte(padding)
|
||||
}
|
||||
return padded
|
||||
}
|
||||
103
internal/gateway/crypto_test.go
Normal file
103
internal/gateway/crypto_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAESEncrypt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
appSecret string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "正常加密",
|
||||
data: []byte(`{"params":{"cardNo":"898608070422D0010269"}}`),
|
||||
appSecret: "BZeQttaZQt0i73moF",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "空数据加密",
|
||||
data: []byte(""),
|
||||
appSecret: "BZeQttaZQt0i73moF",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
encrypted, err := aesEncrypt(tt.data, tt.appSecret)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("aesEncrypt() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && encrypted == "" {
|
||||
t.Error("aesEncrypt() 返回空字符串")
|
||||
}
|
||||
// 验证 Base64 格式
|
||||
if !tt.wantErr {
|
||||
_, err := base64.StdEncoding.DecodeString(encrypted)
|
||||
if err != nil {
|
||||
t.Errorf("aesEncrypt() 返回的不是有效的 Base64: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateSign(t *testing.T) {
|
||||
appID := "60bgt1X8i7AvXqkd"
|
||||
encryptedData := "test_encrypted_data"
|
||||
timestamp := int64(1704067200)
|
||||
appSecret := "BZeQttaZQt0i73moF"
|
||||
|
||||
sign := generateSign(appID, encryptedData, timestamp, appSecret)
|
||||
|
||||
// 验证签名格式(32 位大写十六进制)
|
||||
if len(sign) != 32 {
|
||||
t.Errorf("签名长度错误: got %d, want 32", len(sign))
|
||||
}
|
||||
|
||||
if sign != strings.ToUpper(sign) {
|
||||
t.Error("签名应为大写")
|
||||
}
|
||||
|
||||
// 验证签名可重现
|
||||
sign2 := generateSign(appID, encryptedData, timestamp, appSecret)
|
||||
if sign != sign2 {
|
||||
t.Error("相同参数应生成相同签名")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewECBEncrypterPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("newECBEncrypter 期望触发 panic,但未触发")
|
||||
}
|
||||
}()
|
||||
|
||||
newECBEncrypter(nil)
|
||||
}
|
||||
|
||||
func TestECBEncrypterCryptBlocksPanic(t *testing.T) {
|
||||
block, err := aes.NewCipher(make([]byte, aesBlockSize))
|
||||
if err != nil {
|
||||
t.Fatalf("创建 AES cipher 失败: %v", err)
|
||||
}
|
||||
|
||||
encrypter := newECBEncrypter(block)
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("CryptBlocks 期望触发 panic,但未触发")
|
||||
}
|
||||
}()
|
||||
|
||||
// 传入非完整块长度,触发 panic
|
||||
src := []byte("short")
|
||||
dst := make([]byte, len(src))
|
||||
encrypter.CryptBlocks(dst, src)
|
||||
}
|
||||
169
internal/gateway/device.go
Normal file
169
internal/gateway/device.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// Package gateway 提供设备相关的 7 个 API 方法封装
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
// GetDeviceInfo 获取设备信息
|
||||
// 通过卡号或设备 ID 查询设备的在线状态、信号强度、WiFi 信息等
|
||||
func (c *Client) GetDeviceInfo(ctx context.Context, req *DeviceInfoReq) (*DeviceInfoResp, error) {
|
||||
if req.CardNo == "" && req.DeviceID == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "cardNo 和 deviceId 至少需要一个")
|
||||
}
|
||||
|
||||
params := make(map[string]interface{})
|
||||
if req.CardNo != "" {
|
||||
params["cardNo"] = req.CardNo
|
||||
}
|
||||
if req.DeviceID != "" {
|
||||
params["deviceId"] = req.DeviceID
|
||||
}
|
||||
|
||||
businessData := map[string]interface{}{
|
||||
"params": params,
|
||||
}
|
||||
|
||||
resp, err := c.doRequest(ctx, "/device/info", businessData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result DeviceInfoResp
|
||||
if err := sonic.Unmarshal(resp, &result); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeGatewayInvalidResp, err, "解析设备信息响应失败")
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetSlotInfo 获取设备卡槽信息
|
||||
// 查询设备的所有卡槽及其中的卡信息
|
||||
func (c *Client) GetSlotInfo(ctx context.Context, req *DeviceInfoReq) (*SlotInfoResp, error) {
|
||||
if req.CardNo == "" && req.DeviceID == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "cardNo 和 deviceId 至少需要一个")
|
||||
}
|
||||
|
||||
params := make(map[string]interface{})
|
||||
if req.CardNo != "" {
|
||||
params["cardNo"] = req.CardNo
|
||||
}
|
||||
if req.DeviceID != "" {
|
||||
params["deviceId"] = req.DeviceID
|
||||
}
|
||||
|
||||
businessData := map[string]interface{}{
|
||||
"params": params,
|
||||
}
|
||||
|
||||
resp, err := c.doRequest(ctx, "/device/slot-info", businessData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result SlotInfoResp
|
||||
if err := sonic.Unmarshal(resp, &result); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeGatewayInvalidResp, err, "解析卡槽信息响应失败")
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SetSpeedLimit 设置设备限速
|
||||
// 设置设备的上行和下行速率限制
|
||||
func (c *Client) SetSpeedLimit(ctx context.Context, req *SpeedLimitReq) error {
|
||||
params := map[string]interface{}{
|
||||
"deviceId": req.DeviceID,
|
||||
"uploadSpeed": req.UploadSpeed,
|
||||
"downloadSpeed": req.DownloadSpeed,
|
||||
}
|
||||
if req.Extend != "" {
|
||||
params["extend"] = req.Extend
|
||||
}
|
||||
|
||||
businessData := map[string]interface{}{
|
||||
"params": params,
|
||||
}
|
||||
|
||||
_, err := c.doRequest(ctx, "/device/speed-limit", businessData)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWiFi 设置设备 WiFi
|
||||
// 设置设备的 WiFi 名称、密码和启用状态
|
||||
func (c *Client) SetWiFi(ctx context.Context, req *WiFiReq) error {
|
||||
params := map[string]interface{}{
|
||||
"deviceId": req.DeviceID,
|
||||
"ssid": req.SSID,
|
||||
"password": req.Password,
|
||||
"enabled": req.Enabled,
|
||||
}
|
||||
if req.Extend != "" {
|
||||
params["extend"] = req.Extend
|
||||
}
|
||||
|
||||
businessData := map[string]interface{}{
|
||||
"params": params,
|
||||
}
|
||||
|
||||
_, err := c.doRequest(ctx, "/device/wifi", businessData)
|
||||
return err
|
||||
}
|
||||
|
||||
// SwitchCard 设备切换卡
|
||||
// 切换设备当前使用的卡到指定的目标卡
|
||||
func (c *Client) SwitchCard(ctx context.Context, req *SwitchCardReq) error {
|
||||
params := map[string]interface{}{
|
||||
"deviceId": req.DeviceID,
|
||||
"targetIccid": req.TargetICCID,
|
||||
}
|
||||
if req.Extend != "" {
|
||||
params["extend"] = req.Extend
|
||||
}
|
||||
|
||||
businessData := map[string]interface{}{
|
||||
"params": params,
|
||||
}
|
||||
|
||||
_, err := c.doRequest(ctx, "/device/switch-card", businessData)
|
||||
return err
|
||||
}
|
||||
|
||||
// ResetDevice 设备恢复出厂设置
|
||||
// 将设备恢复到出厂设置状态
|
||||
func (c *Client) ResetDevice(ctx context.Context, req *DeviceOperationReq) error {
|
||||
params := map[string]interface{}{
|
||||
"deviceId": req.DeviceID,
|
||||
}
|
||||
if req.Extend != "" {
|
||||
params["extend"] = req.Extend
|
||||
}
|
||||
|
||||
businessData := map[string]interface{}{
|
||||
"params": params,
|
||||
}
|
||||
|
||||
_, err := c.doRequest(ctx, "/device/reset", businessData)
|
||||
return err
|
||||
}
|
||||
|
||||
// RebootDevice 设备重启
|
||||
// 远程重启设备
|
||||
func (c *Client) RebootDevice(ctx context.Context, req *DeviceOperationReq) error {
|
||||
params := map[string]interface{}{
|
||||
"deviceId": req.DeviceID,
|
||||
}
|
||||
if req.Extend != "" {
|
||||
params["extend"] = req.Extend
|
||||
}
|
||||
|
||||
businessData := map[string]interface{}{
|
||||
"params": params,
|
||||
}
|
||||
|
||||
_, err := c.doRequest(ctx, "/device/reboot", businessData)
|
||||
return err
|
||||
}
|
||||
404
internal/gateway/device_test.go
Normal file
404
internal/gateway/device_test.go
Normal file
@@ -0,0 +1,404 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetDeviceInfo_ByCardNo_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{
|
||||
Code: 200,
|
||||
Msg: "成功",
|
||||
Data: json.RawMessage(`{"imei":"123456789012345","onlineStatus":1,"signalLevel":25,"wifiSsid":"TestWiFi","wifiEnabled":1,"uploadSpeed":100,"downloadSpeed":500}`),
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := client.GetDeviceInfo(ctx, &DeviceInfoReq{
|
||||
CardNo: "898608070422D0010269",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("GetDeviceInfo() error = %v", err)
|
||||
}
|
||||
|
||||
if result.IMEI != "123456789012345" {
|
||||
t.Errorf("IMEI = %s, want 123456789012345", result.IMEI)
|
||||
}
|
||||
if result.OnlineStatus != 1 {
|
||||
t.Errorf("OnlineStatus = %d, want 1", result.OnlineStatus)
|
||||
}
|
||||
if result.SignalLevel != 25 {
|
||||
t.Errorf("SignalLevel = %d, want 25", result.SignalLevel)
|
||||
}
|
||||
if result.WiFiSSID != "TestWiFi" {
|
||||
t.Errorf("WiFiSSID = %s, want TestWiFi", result.WiFiSSID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDeviceInfo_ByDeviceID_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{
|
||||
Code: 200,
|
||||
Msg: "成功",
|
||||
Data: json.RawMessage(`{"imei":"123456789012345","onlineStatus":0,"signalLevel":0}`),
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := client.GetDeviceInfo(ctx, &DeviceInfoReq{
|
||||
DeviceID: "123456789012345",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("GetDeviceInfo() error = %v", err)
|
||||
}
|
||||
|
||||
if result.IMEI != "123456789012345" {
|
||||
t.Errorf("IMEI = %s, want 123456789012345", result.IMEI)
|
||||
}
|
||||
if result.OnlineStatus != 0 {
|
||||
t.Errorf("OnlineStatus = %d, want 0", result.OnlineStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDeviceInfo_MissingParams(t *testing.T) {
|
||||
client := NewClient("https://test.example.com", "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := client.GetDeviceInfo(ctx, &DeviceInfoReq{})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("GetDeviceInfo() expected validation error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "至少需要一个") {
|
||||
t.Errorf("error should contain '至少需要一个', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDeviceInfo_InvalidResponse(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{
|
||||
Code: 200,
|
||||
Msg: "成功",
|
||||
Data: json.RawMessage(`invalid json`),
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := client.GetDeviceInfo(ctx, &DeviceInfoReq{CardNo: "test"})
|
||||
if err == nil {
|
||||
t.Fatal("GetDeviceInfo() expected JSON parse error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "解析") {
|
||||
t.Errorf("error should contain '解析', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSlotInfo_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{
|
||||
Code: 200,
|
||||
Msg: "成功",
|
||||
Data: json.RawMessage(`{"imei":"123456789012345","slots":[{"slotNo":1,"iccid":"898608070422D0010269","cardStatus":"正常","isActive":1},{"slotNo":2,"iccid":"898608070422D0010270","cardStatus":"停机","isActive":0}]}`),
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := client.GetSlotInfo(ctx, &DeviceInfoReq{
|
||||
DeviceID: "123456789012345",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("GetSlotInfo() error = %v", err)
|
||||
}
|
||||
|
||||
if result.IMEI != "123456789012345" {
|
||||
t.Errorf("IMEI = %s, want 123456789012345", result.IMEI)
|
||||
}
|
||||
if len(result.Slots) != 2 {
|
||||
t.Errorf("len(Slots) = %d, want 2", len(result.Slots))
|
||||
}
|
||||
if result.Slots[0].SlotNo != 1 {
|
||||
t.Errorf("Slots[0].SlotNo = %d, want 1", result.Slots[0].SlotNo)
|
||||
}
|
||||
if result.Slots[0].ICCID != "898608070422D0010269" {
|
||||
t.Errorf("Slots[0].ICCID = %s, want 898608070422D0010269", result.Slots[0].ICCID)
|
||||
}
|
||||
if result.Slots[0].IsActive != 1 {
|
||||
t.Errorf("Slots[0].IsActive = %d, want 1", result.Slots[0].IsActive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSlotInfo_MissingParams(t *testing.T) {
|
||||
client := NewClient("https://test.example.com", "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := client.GetSlotInfo(ctx, &DeviceInfoReq{})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("GetSlotInfo() expected validation error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "至少需要一个") {
|
||||
t.Errorf("error should contain '至少需要一个', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSpeedLimit_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{Code: 200, Msg: "成功"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.SetSpeedLimit(ctx, &SpeedLimitReq{
|
||||
DeviceID: "123456789012345",
|
||||
UploadSpeed: 100,
|
||||
DownloadSpeed: 500,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("SetSpeedLimit() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSpeedLimit_WithExtend(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{Code: 200, Msg: "成功"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.SetSpeedLimit(ctx, &SpeedLimitReq{
|
||||
DeviceID: "123456789012345",
|
||||
UploadSpeed: 100,
|
||||
DownloadSpeed: 500,
|
||||
Extend: "test-extend",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("SetSpeedLimit() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSpeedLimit_BusinessError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{Code: 500, Msg: "设置失败"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.SetSpeedLimit(ctx, &SpeedLimitReq{
|
||||
DeviceID: "123456789012345",
|
||||
UploadSpeed: 100,
|
||||
DownloadSpeed: 500,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("SetSpeedLimit() expected business error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "业务错误") {
|
||||
t.Errorf("error should contain '业务错误', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetWiFi_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{Code: 200, Msg: "成功"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.SetWiFi(ctx, &WiFiReq{
|
||||
DeviceID: "123456789012345",
|
||||
SSID: "TestWiFi",
|
||||
Password: "password123",
|
||||
Enabled: 1,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("SetWiFi() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetWiFi_WithExtend(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{Code: 200, Msg: "成功"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.SetWiFi(ctx, &WiFiReq{
|
||||
DeviceID: "123456789012345",
|
||||
SSID: "TestWiFi",
|
||||
Password: "password123",
|
||||
Enabled: 0,
|
||||
Extend: "test-extend",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("SetWiFi() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchCard_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{Code: 200, Msg: "成功"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.SwitchCard(ctx, &SwitchCardReq{
|
||||
DeviceID: "123456789012345",
|
||||
TargetICCID: "898608070422D0010270",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("SwitchCard() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchCard_BusinessError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{Code: 404, Msg: "目标卡不存在"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.SwitchCard(ctx, &SwitchCardReq{
|
||||
DeviceID: "123456789012345",
|
||||
TargetICCID: "invalid",
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("SwitchCard() expected business error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetDevice_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{Code: 200, Msg: "成功"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.ResetDevice(ctx, &DeviceOperationReq{
|
||||
DeviceID: "123456789012345",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ResetDevice() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetDevice_WithExtend(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{Code: 200, Msg: "成功"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.ResetDevice(ctx, &DeviceOperationReq{
|
||||
DeviceID: "123456789012345",
|
||||
Extend: "test-extend",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ResetDevice() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRebootDevice_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{Code: 200, Msg: "成功"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.RebootDevice(ctx, &DeviceOperationReq{
|
||||
DeviceID: "123456789012345",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("RebootDevice() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRebootDevice_BusinessError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{Code: 500, Msg: "设备离线"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.RebootDevice(ctx, &DeviceOperationReq{
|
||||
DeviceID: "123456789012345",
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("RebootDevice() expected business error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "业务错误") {
|
||||
t.Errorf("error should contain '业务错误', got: %v", err)
|
||||
}
|
||||
}
|
||||
128
internal/gateway/flow_card.go
Normal file
128
internal/gateway/flow_card.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Package gateway 提供流量卡相关的 7 个 API 方法封装
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
// QueryCardStatus 查询流量卡状态
|
||||
func (c *Client) QueryCardStatus(ctx context.Context, req *CardStatusReq) (*CardStatusResp, error) {
|
||||
businessData := map[string]interface{}{
|
||||
"params": map[string]interface{}{
|
||||
"cardNo": req.CardNo,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := c.doRequest(ctx, "/flow-card/status", businessData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result CardStatusResp
|
||||
if err := sonic.Unmarshal(resp, &result); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeGatewayInvalidResp, err, "解析卡状态响应失败")
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// QueryFlow 查询流量使用情况
|
||||
func (c *Client) QueryFlow(ctx context.Context, req *FlowQueryReq) (*FlowUsageResp, error) {
|
||||
businessData := map[string]interface{}{
|
||||
"params": map[string]interface{}{
|
||||
"cardNo": req.CardNo,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := c.doRequest(ctx, "/flow-card/flow", businessData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result FlowUsageResp
|
||||
if err := sonic.Unmarshal(resp, &result); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeGatewayInvalidResp, err, "解析流量使用响应失败")
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// QueryRealnameStatus 查询实名认证状态
|
||||
func (c *Client) QueryRealnameStatus(ctx context.Context, req *CardStatusReq) (*RealnameStatusResp, error) {
|
||||
businessData := map[string]interface{}{
|
||||
"params": map[string]interface{}{
|
||||
"cardNo": req.CardNo,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := c.doRequest(ctx, "/flow-card/realname-status", businessData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result RealnameStatusResp
|
||||
if err := sonic.Unmarshal(resp, &result); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeGatewayInvalidResp, err, "解析实名认证状态响应失败")
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// StopCard 流量卡停机
|
||||
func (c *Client) StopCard(ctx context.Context, req *CardOperationReq) error {
|
||||
businessData := map[string]interface{}{
|
||||
"params": map[string]interface{}{
|
||||
"cardNo": req.CardNo,
|
||||
},
|
||||
}
|
||||
if req.Extend != "" {
|
||||
businessData["params"].(map[string]interface{})["extend"] = req.Extend
|
||||
}
|
||||
|
||||
_, err := c.doRequest(ctx, "/flow-card/cardStop", businessData)
|
||||
return err
|
||||
}
|
||||
|
||||
// StartCard 流量卡复机
|
||||
func (c *Client) StartCard(ctx context.Context, req *CardOperationReq) error {
|
||||
businessData := map[string]interface{}{
|
||||
"params": map[string]interface{}{
|
||||
"cardNo": req.CardNo,
|
||||
},
|
||||
}
|
||||
if req.Extend != "" {
|
||||
businessData["params"].(map[string]interface{})["extend"] = req.Extend
|
||||
}
|
||||
|
||||
_, err := c.doRequest(ctx, "/flow-card/cardStart", businessData)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetRealnameLink 获取实名认证跳转链接
|
||||
func (c *Client) GetRealnameLink(ctx context.Context, req *CardStatusReq) (*RealnameLinkResp, error) {
|
||||
businessData := map[string]interface{}{
|
||||
"params": map[string]interface{}{
|
||||
"cardNo": req.CardNo,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := c.doRequest(ctx, "/flow-card/realname-link", businessData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result RealnameLinkResp
|
||||
if err := sonic.Unmarshal(resp, &result); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeGatewayInvalidResp, err, "解析实名认证链接响应失败")
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// BatchQuery 批量查询(预留接口,暂未实现)
|
||||
func (c *Client) BatchQuery(ctx context.Context, req *BatchQueryReq) (*BatchQueryResp, error) {
|
||||
return nil, errors.New(errors.CodeGatewayError, "批量查询接口暂未实现")
|
||||
}
|
||||
292
internal/gateway/flow_card_test.go
Normal file
292
internal/gateway/flow_card_test.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestQueryCardStatus_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{
|
||||
Code: 200,
|
||||
Msg: "成功",
|
||||
Data: json.RawMessage(`{"iccid":"898608070422D0010269","cardStatus":"正常"}`),
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := client.QueryCardStatus(ctx, &CardStatusReq{
|
||||
CardNo: "898608070422D0010269",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("QueryCardStatus() error = %v", err)
|
||||
}
|
||||
|
||||
if result.ICCID != "898608070422D0010269" {
|
||||
t.Errorf("ICCID = %s, want 898608070422D0010269", result.ICCID)
|
||||
}
|
||||
if result.CardStatus != "正常" {
|
||||
t.Errorf("CardStatus = %s, want 正常", result.CardStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryCardStatus_InvalidResponse(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{
|
||||
Code: 200,
|
||||
Msg: "成功",
|
||||
Data: json.RawMessage(`invalid json`),
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := client.QueryCardStatus(ctx, &CardStatusReq{CardNo: "test"})
|
||||
if err == nil {
|
||||
t.Fatal("QueryCardStatus() expected JSON parse error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "解析") {
|
||||
t.Errorf("error should contain '解析', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryFlow_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{
|
||||
Code: 200,
|
||||
Msg: "成功",
|
||||
Data: json.RawMessage(`{"usedFlow":1024,"unit":"MB"}`),
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := client.QueryFlow(ctx, &FlowQueryReq{
|
||||
CardNo: "898608070422D0010269",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("QueryFlow() error = %v", err)
|
||||
}
|
||||
|
||||
if result.UsedFlow != 1024 {
|
||||
t.Errorf("UsedFlow = %d, want 1024", result.UsedFlow)
|
||||
}
|
||||
if result.Unit != "MB" {
|
||||
t.Errorf("Unit = %s, want MB", result.Unit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryFlow_BusinessError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{
|
||||
Code: 404,
|
||||
Msg: "卡号不存在",
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := client.QueryFlow(ctx, &FlowQueryReq{CardNo: "invalid"})
|
||||
if err == nil {
|
||||
t.Fatal("QueryFlow() expected business error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "业务错误") {
|
||||
t.Errorf("error should contain '业务错误', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryRealnameStatus_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{
|
||||
Code: 200,
|
||||
Msg: "成功",
|
||||
Data: json.RawMessage(`{"status":"已实名"}`),
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := client.QueryRealnameStatus(ctx, &CardStatusReq{
|
||||
CardNo: "898608070422D0010269",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("QueryRealnameStatus() error = %v", err)
|
||||
}
|
||||
|
||||
if result.Status != "已实名" {
|
||||
t.Errorf("Status = %s, want 已实名", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopCard_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{
|
||||
Code: 200,
|
||||
Msg: "成功",
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.StopCard(ctx, &CardOperationReq{
|
||||
CardNo: "898608070422D0010269",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("StopCard() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopCard_WithExtend(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{Code: 200, Msg: "成功"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.StopCard(ctx, &CardOperationReq{
|
||||
CardNo: "898608070422D0010269",
|
||||
Extend: "test-extend",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("StopCard() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartCard_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{Code: 200, Msg: "成功"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.StartCard(ctx, &CardOperationReq{
|
||||
CardNo: "898608070422D0010269",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("StartCard() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartCard_BusinessError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{Code: 500, Msg: "操作失败"}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.StartCard(ctx, &CardOperationReq{CardNo: "test"})
|
||||
if err == nil {
|
||||
t.Fatal("StartCard() expected business error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRealnameLink_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{
|
||||
Code: 200,
|
||||
Msg: "成功",
|
||||
Data: json.RawMessage(`{"link":"https://realname.example.com/verify?token=abc123"}`),
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := client.GetRealnameLink(ctx, &CardStatusReq{
|
||||
CardNo: "898608070422D0010269",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("GetRealnameLink() error = %v", err)
|
||||
}
|
||||
|
||||
if result.Link != "https://realname.example.com/verify?token=abc123" {
|
||||
t.Errorf("Link = %s, want https://realname.example.com/verify?token=abc123", result.Link)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRealnameLink_InvalidResponse(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := GatewayResponse{
|
||||
Code: 200,
|
||||
Msg: "成功",
|
||||
Data: json.RawMessage(`{"invalid": "structure"}`),
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := client.GetRealnameLink(ctx, &CardStatusReq{CardNo: "test"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("GetRealnameLink() unexpected error = %v", err)
|
||||
}
|
||||
if result.Link != "" {
|
||||
t.Errorf("Link = %s, want empty string", result.Link)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchQuery_NotImplemented(t *testing.T) {
|
||||
client := NewClient("https://test.example.com", "testAppID", "testSecret")
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := client.BatchQuery(ctx, &BatchQueryReq{
|
||||
CardNos: []string{"test1", "test2"},
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("BatchQuery() expected not implemented error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "暂未实现") {
|
||||
t.Errorf("error should contain '暂未实现', got: %v", err)
|
||||
}
|
||||
}
|
||||
132
internal/gateway/models.go
Normal file
132
internal/gateway/models.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Package gateway 定义 Gateway API 的请求和响应数据传输对象(DTO)
|
||||
package gateway
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// GatewayResponse 是 Gateway API 的通用响应结构
|
||||
type GatewayResponse struct {
|
||||
Code int `json:"code" description:"业务状态码(200 = 成功)"`
|
||||
Msg string `json:"msg" description:"业务提示信息"`
|
||||
Data json.RawMessage `json:"data" description:"业务数据(原始 JSON)"`
|
||||
TraceID string `json:"trace_id" description:"链路追踪 ID"`
|
||||
}
|
||||
|
||||
// ============ 流量卡相关 DTO ============
|
||||
|
||||
// CardStatusReq 是查询流量卡状态的请求
|
||||
type CardStatusReq struct {
|
||||
CardNo string `json:"cardNo" validate:"required" required:"true" description:"流量卡号"`
|
||||
}
|
||||
|
||||
// CardStatusResp 是查询流量卡状态的响应
|
||||
type CardStatusResp struct {
|
||||
ICCID string `json:"iccid" description:"ICCID"`
|
||||
CardStatus string `json:"cardStatus" description:"卡状态(准备、正常、停机)"`
|
||||
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
||||
}
|
||||
|
||||
// FlowQueryReq 是查询流量使用的请求
|
||||
type FlowQueryReq struct {
|
||||
CardNo string `json:"cardNo" validate:"required" required:"true" description:"流量卡号"`
|
||||
}
|
||||
|
||||
// FlowUsageResp 是查询流量使用的响应
|
||||
type FlowUsageResp struct {
|
||||
UsedFlow int64 `json:"usedFlow" description:"已用流量"`
|
||||
Unit string `json:"unit" description:"流量单位(MB)"`
|
||||
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
||||
}
|
||||
|
||||
// CardOperationReq 是停机/复机请求
|
||||
type CardOperationReq struct {
|
||||
CardNo string `json:"cardNo" validate:"required" required:"true" description:"流量卡号"`
|
||||
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
||||
}
|
||||
|
||||
// RealnameStatusResp 是实名认证状态的响应
|
||||
type RealnameStatusResp struct {
|
||||
Status string `json:"status" description:"实名认证状态"`
|
||||
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
||||
}
|
||||
|
||||
// RealnameLinkResp 是实名认证链接的响应
|
||||
type RealnameLinkResp struct {
|
||||
Link string `json:"link" description:"实名认证跳转链接(HTTPS URL)"`
|
||||
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
||||
}
|
||||
|
||||
// BatchQueryReq 是批量查询的请求
|
||||
type BatchQueryReq struct {
|
||||
CardNos []string `json:"cardNos" validate:"required,min=1,max=100" required:"true" description:"流量卡号列表(最多100个)"`
|
||||
}
|
||||
|
||||
// BatchQueryResp 是批量查询的响应
|
||||
type BatchQueryResp struct {
|
||||
Results []CardStatusResp `json:"results" description:"查询结果列表"`
|
||||
}
|
||||
|
||||
// ============ 设备相关 DTO ============
|
||||
|
||||
// DeviceInfoReq 是查询设备信息的请求
|
||||
type DeviceInfoReq struct {
|
||||
CardNo string `json:"cardNo,omitempty" description:"流量卡号(与 DeviceID 二选一)"`
|
||||
DeviceID string `json:"deviceId,omitempty" description:"设备 ID/IMEI(与 CardNo 二选一)"`
|
||||
}
|
||||
|
||||
// DeviceInfoResp 是查询设备信息的响应
|
||||
type DeviceInfoResp struct {
|
||||
IMEI string `json:"imei" description:"设备 IMEI"`
|
||||
OnlineStatus int `json:"onlineStatus" description:"在线状态(0:离线, 1:在线)"`
|
||||
SignalLevel int `json:"signalLevel" description:"信号强度(0-31)"`
|
||||
WiFiSSID string `json:"wifiSsid,omitempty" description:"WiFi 名称"`
|
||||
WiFiEnabled int `json:"wifiEnabled" description:"WiFi 启用状态(0:禁用, 1:启用)"`
|
||||
UploadSpeed int `json:"uploadSpeed" description:"上行速率(KB/s)"`
|
||||
DownloadSpeed int `json:"downloadSpeed" description:"下行速率(KB/s)"`
|
||||
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
||||
}
|
||||
|
||||
// SpeedLimitReq 是设置设备限速的请求
|
||||
type SpeedLimitReq struct {
|
||||
DeviceID string `json:"deviceId" validate:"required" required:"true" description:"设备 ID/IMEI"`
|
||||
UploadSpeed int `json:"uploadSpeed" validate:"required,min=1" required:"true" minimum:"1" description:"上行速率(KB/s)"`
|
||||
DownloadSpeed int `json:"downloadSpeed" validate:"required,min=1" required:"true" minimum:"1" description:"下行速率(KB/s)"`
|
||||
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
||||
}
|
||||
|
||||
// WiFiReq 是设置设备 WiFi 的请求
|
||||
type WiFiReq struct {
|
||||
DeviceID string `json:"deviceId" validate:"required" required:"true" description:"设备 ID/IMEI"`
|
||||
SSID string `json:"ssid" validate:"required,min=1,max=32" required:"true" minLength:"1" maxLength:"32" description:"WiFi 名称"`
|
||||
Password string `json:"password" validate:"required,min=8,max=63" required:"true" minLength:"8" maxLength:"63" description:"WiFi 密码"`
|
||||
Enabled int `json:"enabled" validate:"required,oneof=0 1" required:"true" description:"启用状态(0:禁用, 1:启用)"`
|
||||
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
||||
}
|
||||
|
||||
// SwitchCardReq 是设备切换卡的请求
|
||||
type SwitchCardReq struct {
|
||||
DeviceID string `json:"deviceId" validate:"required" required:"true" description:"设备 ID/IMEI"`
|
||||
TargetICCID string `json:"targetIccid" validate:"required" required:"true" description:"目标卡 ICCID"`
|
||||
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
||||
}
|
||||
|
||||
// DeviceOperationReq 是设备操作(重启、恢复出厂)的请求
|
||||
type DeviceOperationReq struct {
|
||||
DeviceID string `json:"deviceId" validate:"required" required:"true" description:"设备 ID/IMEI"`
|
||||
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
||||
}
|
||||
|
||||
// SlotInfo 是单个卡槽信息
|
||||
type SlotInfo struct {
|
||||
SlotNo int `json:"slotNo" description:"卡槽编号"`
|
||||
ICCID string `json:"iccid" description:"卡槽中的 ICCID"`
|
||||
CardStatus string `json:"cardStatus" description:"卡状态(准备、正常、停机)"`
|
||||
IsActive int `json:"isActive" description:"是否为当前使用的卡槽(0:否, 1:是)"`
|
||||
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
||||
}
|
||||
|
||||
// SlotInfoResp 是查询设备卡槽信息的响应
|
||||
type SlotInfoResp struct {
|
||||
IMEI string `json:"imei" description:"设备 IMEI"`
|
||||
Slots []SlotInfo `json:"slots" description:"卡槽信息列表"`
|
||||
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
||||
}
|
||||
Reference in New Issue
Block a user