# 设计文档:Gateway API 统一封装 ## 架构设计 ### 文件组织 ``` internal/gateway/ ├── client.go # Gateway 客户端主体(Client 结构体 + doRequest) ├── crypto.go # 加密/签名工具函数(AES + MD5) ├── flow_card.go # 流量卡 7 个 API 方法封装 ├── device.go # 设备 7 个 API 方法封装 ├── models.go # 请求/响应 DTO └── client_test.go # 单元测试和集成测试 ``` **设计理由**: - 按功能职责拆分,清晰易维护 - 单文件长度控制在 100 行以内 - 符合 Go 惯用法的扁平化包结构 ### 客户端设计 ```go // Client Gateway API 客户端 type Client struct { baseURL string appID string appSecret string httpClient *http.Client timeout time.Duration } // NewClient 创建 Gateway 客户端 func NewClient(baseURL, appID, appSecret string) *Client // WithTimeout 设置请求超时时间 func (c *Client) WithTimeout(timeout time.Duration) *Client // doRequest 统一处理请求(加密、签名、发送、解密) func (c *Client) doRequest(ctx context.Context, path string, businessData interface{}) ([]byte, error) ``` **核心方法**: - `doRequest`:统一封装加密、签名、HTTP 请求、响应解析 - 14 个 API 方法复用 `doRequest` ### 加密/签名机制 #### 1. AES-128-ECB 加密 ```go // aesEncrypt 使用 AES-128-ECB 模式加密数据 // 密钥:MD5(appSecret) 的原始字节数组(16字节) // 填充:PKCS5Padding // 编码:Base64 func aesEncrypt(data []byte, appSecret string) (string, error) { // 1. 生成密钥:MD5(appSecret) h := md5.New() h.Write([]byte(appSecret)) key := h.Sum(nil) // 16 字节 // 2. 创建 AES 加密器 block, err := aes.NewCipher(key) if err != nil { return "", errors.Wrap(errors.CodeGatewayEncryptError, err) } // 3. PKCS5 填充 padding := block.BlockSize() - len(data)%block.BlockSize() padText := bytes.Repeat([]byte{byte(padding)}, padding) data = append(data, padText...) // 4. ECB 模式加密 encrypted := make([]byte, len(data)) size := block.BlockSize() for bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size { block.Encrypt(encrypted[bs:be], data[bs:be]) } // 5. Base64 编码 return base64.StdEncoding.EncodeToString(encrypted), nil } ``` #### 2. MD5 签名 ```go // generateSign 生成 MD5 签名 // 参数排序:appId、data、timestamp 按字母序 // 格式:appId=xxx&data=xxx×tamp=xxx&key=appSecret // 输出:大写十六进制字符串 func generateSign(appID, encryptedData string, timestamp int64, appSecret string) string { // 1. 构建签名字符串(参数按字母序) signStr := fmt.Sprintf("appId=%s&data=%s×tamp=%d&key=%s", appID, encryptedData, timestamp, appSecret) // 2. MD5 加密 h := md5.New() h.Write([]byte(signStr)) // 3. 转大写十六进制 return strings.ToUpper(hex.EncodeToString(h.Sum(nil))) } ``` ### 请求流程 ``` 业务数据(Go struct) ↓ JSON 序列化 业务数据(JSON string) ↓ AES 加密 加密数据(Base64 string) ↓ 生成签名 签名(MD5 大写) ↓ 构建请求 { "appId": "...", "data": "...", "sign": "...", "timestamp": ... } ↓ HTTP POST Gateway API ↓ 响应 { "code": 200, "msg": "成功", "data": {...}, "trace_id": "..." } ↓ 解析响应 返回业务数据 ``` ### API 封装示例 #### 流量卡状态查询 ```go // QueryCardStatus 查询流量卡状态 func (c *Client) QueryCardStatus(ctx context.Context, req *CardStatusReq) (*CardStatusResp, error) { // 1. 构建业务数据 businessData := map[string]interface{}{ "params": map[string]interface{}{ "cardNo": req.CardNo, }, } // 2. 调用统一请求方法 resp, err := c.doRequest(ctx, "/flow-card/status", businessData) if err != nil { return nil, err } // 3. 解析响应 var result CardStatusResp if err := sonic.Unmarshal(resp, &result); err != nil { return nil, errors.Wrap(errors.CodeGatewayInvalidResp, err, "解析卡状态响应失败") } return &result, nil } ``` #### 流量卡停机 ```go // StopCard 流量卡停机 func (c *Client) StopCard(ctx context.Context, req *CardOperationReq) error { businessData := map[string]interface{}{ "params": map[string]interface{}{ "cardNo": req.CardNo, }, } _, err := c.doRequest(ctx, "/flow-card/cardStop", businessData) return err } ``` ## 数据模型设计 ### 请求 DTO ```go // CardStatusReq 卡状态查询请求 type CardStatusReq struct { CardNo string `json:"cardNo" validate:"required"` // 物联网卡号(ICCID) } // CardOperationReq 卡操作请求(停机、复机) type CardOperationReq struct { CardNo string `json:"cardNo" validate:"required"` // 物联网卡号(ICCID) Extend string `json:"extend,omitempty"` // 扩展参数(广电国网) } // FlowQueryReq 流量查询请求 type FlowQueryReq struct { CardNo string `json:"cardNo" validate:"required"` // 物联网卡号(ICCID) } // DeviceInfoReq 设备信息查询请求 type DeviceInfoReq struct { CardNo string `json:"cardNo,omitempty"` // 物联网卡号(与 DeviceID 二选一) DeviceID string `json:"deviceId,omitempty"` // 设备编号(IMEI) } ``` ### 响应 DTO ```go // GatewayResponse Gateway 通用响应 type GatewayResponse struct { Code int `json:"code"` // 业务状态码(200 = 成功) Msg string `json:"msg"` // 业务提示信息 Data json.RawMessage `json:"data"` // 业务数据(原始 JSON) TraceID string `json:"trace_id"` // 链路追踪 ID } // CardStatusResp 卡状态查询响应 type CardStatusResp struct { ICCID string `json:"iccid"` // 卡号 CardStatus string `json:"cardStatus"` // 卡状态(准备、正常、停机) Extend string `json:"extend"` // 扩展响应字段(广电国网) } // FlowUsageResp 流量使用查询响应 type FlowUsageResp struct { ICCID string `json:"iccid"` // 卡号 Used float64 `json:"used"` // 已使用流量(MB) Unit string `json:"unit"` // 单位(MB) } // DeviceInfoResp 设备信息响应 type DeviceInfoResp struct { EquipmentID string `json:"equipmentId"` // 设备标识(IMEI) OnlineStatus string `json:"onlineStatus"` // 在线状态 ClientNumber int `json:"clientNumber"` // 连接客户端数 RSSI int `json:"rssi"` // 信号强度 SSIDName string `json:"ssidName"` // WiFi 名称 SSIDPassword string `json:"ssidPassword"` // WiFi 密码 MAC string `json:"mac"` // MAC 地址 UploadSpeed int `json:"uploadSpeed"` // 上行速率 DownloadSpeed int `json:"downloadSpeed"` // 下行速率 Version string `json:"version"` // 软件版本 IP string `json:"ip"` // IP 地址 WanIP string `json:"wanIp"` // 外网 IP } ``` ## 错误处理设计 ### 错误码定义 在 `pkg/errors/codes.go` 中添加: ```go // Gateway 相关错误(1110-1119) const ( CodeGatewayError = 1110 // Gateway 通用错误 CodeGatewayEncryptError = 1111 // 数据加密失败 CodeGatewaySignError = 1112 // 签名生成失败 CodeGatewayTimeout = 1113 // 请求超时 CodeGatewayInvalidResp = 1114 // 响应格式错误 ) ``` 在 `errorMessages` 中添加: ```go errorMessages = map[int]string{ // ... CodeGatewayError: "Gateway 请求失败", CodeGatewayEncryptError: "数据加密失败", CodeGatewaySignError: "签名生成失败", CodeGatewayTimeout: "Gateway 请求超时", CodeGatewayInvalidResp: "Gateway 响应格式错误", } ``` ### 错误处理策略 ```go // doRequest 中的错误处理 func (c *Client) doRequest(ctx context.Context, path string, businessData interface{}) ([]byte, error) { // 1. 序列化业务数据 dataBytes, err := sonic.Marshal(businessData) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "序列化业务数据失败") } // 2. 加密 encryptedData, err := aesEncrypt(dataBytes, c.appSecret) if err != nil { return nil, err // 已在 aesEncrypt 中包装 } // 3. 生成签名 timestamp := time.Now().Unix() sign := generateSign(c.appID, encryptedData, timestamp, c.appSecret) // 4. 构建请求 reqBody := map[string]interface{}{ "appId": c.appID, "data": encryptedData, "sign": sign, "timestamp": timestamp, } reqBytes, err := sonic.Marshal(reqBody) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "序列化请求失败") } // 5. 发送 HTTP 请求 req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(reqBytes)) if err != nil { return nil, errors.Wrap(errors.CodeGatewayError, err, "创建 HTTP 请求失败") } req.Header.Set("Content-Type", "application/json;charset=utf-8") req.Header.Set("Accept", "application/json") resp, err := c.httpClient.Do(req) if err != nil { // 判断是否超时 if ctx.Err() == context.DeadlineExceeded { return nil, errors.Wrap(errors.CodeGatewayTimeout, err, "Gateway 请求超时") } return nil, errors.Wrap(errors.CodeGatewayError, err, "发送 HTTP 请求失败") } defer resp.Body.Close() // 6. 读取响应 respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.Wrap(errors.CodeGatewayError, err, "读取响应失败") } // 7. 检查 HTTP 状态码 if resp.StatusCode != http.StatusOK { return nil, errors.New(errors.CodeGatewayError, fmt.Sprintf("HTTP 状态码异常: %d, 响应: %s", resp.StatusCode, string(respBody))) } // 8. 解析响应 var gatewayResp GatewayResponse if err := sonic.Unmarshal(respBody, &gatewayResp); err != nil { return nil, errors.Wrap(errors.CodeGatewayInvalidResp, err, "解析 Gateway 响应失败") } // 9. 检查业务状态码 if gatewayResp.Code != 200 { return nil, errors.New(errors.CodeGatewayError, fmt.Sprintf("Gateway 业务错误: code=%d, msg=%s", gatewayResp.Code, gatewayResp.Msg)) } // 10. 返回业务数据 return gatewayResp.Data, nil } ``` ## 配置集成设计 ### 配置结构 在 `pkg/config/config.go` 中添加: ```go type Config struct { Server ServerConfig `mapstructure:"server"` Database DatabaseConfig `mapstructure:"database"` Redis RedisConfig `mapstructure:"redis"` Gateway GatewayConfig `mapstructure:"gateway"` // 新增 // ... } // GatewayConfig Gateway API 配置 type GatewayConfig struct { BaseURL string `mapstructure:"base_url"` // Gateway API 基础 URL AppID string `mapstructure:"app_id"` // 应用 ID AppSecret string `mapstructure:"app_secret"` // 应用密钥 Timeout int `mapstructure:"timeout"` // 超时时间(秒,默认 30) } ``` ### 配置文件 在 `pkg/config/defaults/config.yaml` 中添加: ```yaml gateway: base_url: "https://lplan.whjhft.com/openapi" app_id: "60bgt1X8i7AvXqkd" app_secret: "BZeQttaZQt0i73moF" timeout: 30 ``` ### 环境变量覆盖 ```bash export JUNHONG_GATEWAY_BASE_URL=https://lplan.whjhft.com/openapi export JUNHONG_GATEWAY_APP_ID=60bgt1X8i7AvXqkd export JUNHONG_GATEWAY_APP_SECRET=BZeQttaZQt0i73moF export JUNHONG_GATEWAY_TIMEOUT=30 ``` ## 依赖注入设计 ### Bootstrap 初始化 在 `internal/bootstrap/bootstrap.go` 中添加: ```go // Dependencies 系统依赖 type Dependencies struct { DB *gorm.DB Redis *redis.Client QueueClient *asynq.Client Logger *zap.Logger Config *config.Config GatewayClient *gateway.Client // 新增 } // Bootstrap 初始化所有组件 func Bootstrap(deps *Dependencies) (*Handlers, error) { // ... 现有初始化 // 初始化 Gateway 客户端 gatewayClient := gateway.NewClient( deps.Config.Gateway.BaseURL, deps.Config.Gateway.AppID, deps.Config.Gateway.AppSecret, ).WithTimeout(time.Duration(deps.Config.Gateway.Timeout) * time.Second) deps.GatewayClient = gatewayClient // ... 后续初始化 } ``` ### Service 注入 ```go // internal/service/iot_card/service.go type Service struct { store *postgres.IotCardStore gatewayClient *gateway.Client // 新增 logger *zap.Logger } func NewService(store *postgres.IotCardStore, gatewayClient *gateway.Client, logger *zap.Logger) *Service { return &Service{ store: store, gatewayClient: gatewayClient, logger: logger, } } // SyncCardStatus 同步卡状态 func (s *Service) SyncCardStatus(ctx context.Context, cardNo string) error { // 调用 Gateway API resp, err := s.gatewayClient.QueryCardStatus(ctx, &gateway.CardStatusReq{ CardNo: cardNo, }) if err != nil { return errors.Wrap(errors.CodeInternalError, err, "查询卡状态失败") } // 更新数据库 return s.store.UpdateStatus(ctx, cardNo, resp.CardStatus) } ``` ## 测试设计 ### 单元测试 ```go // TestAESEncrypt 测试 AES 加密 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, }, } 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() 返回空字符串") } }) } } // TestGenerateSign 测试签名生成 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("签名应为大写") } } ``` ### 集成测试 ```go // TestQueryCardStatus 测试卡状态查询 func TestQueryCardStatus(t *testing.T) { if testing.Short() { t.Skip("跳过集成测试") } cfg := config.Get() client := gateway.NewClient( cfg.Gateway.BaseURL, cfg.Gateway.AppID, cfg.Gateway.AppSecret, ).WithTimeout(30 * time.Second) ctx := context.Background() resp, err := client.QueryCardStatus(ctx, &gateway.CardStatusReq{ CardNo: "898608070422D0010269", }) require.NoError(t, err) require.NotNil(t, resp) require.NotEmpty(t, resp.ICCID) require.NotEmpty(t, resp.CardStatus) } ``` ## 性能考虑 1. **HTTP 连接复用**:`http.Client` 复用 TCP 连接 2. **超时控制**:通过 `context.WithTimeout` 控制请求超时 3. **并发安全**:`Client` 结构体无状态,可安全并发调用 4. **内存优化**:使用 `sonic` 进行高性能 JSON 序列化 ## 安全性考虑 1. **AES-ECB 模式**:虽不推荐,但由 Gateway 强制要求 2. **密钥管理**:AppSecret 通过环境变量注入,不硬编码 3. **签名验证**:每个请求都进行签名,防止篡改 4. **HTTPS**:生产环境使用 HTTPS 加密传输