# 设计文档：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&timestamp=xxx&key=appSecret
// 输出：大写十六进制字符串
func generateSign(appID, encryptedData string, timestamp int64, appSecret string) string {
	// 1. 构建签名字符串（参数按字母序）
	signStr := fmt.Sprintf("appId=%s&data=%s&timestamp=%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 加密传输
