552 lines
13 KiB
Markdown
552 lines
13 KiB
Markdown
# Gateway 客户端使用指南
|
||
|
||
## 概述
|
||
|
||
Gateway 客户端是对第三方 Gateway API 的 Go 封装,提供流量卡和设备管理的统一接口。客户端内置了 AES-128-ECB 加密、MD5 签名验证、HTTP 连接池管理等功能。
|
||
|
||
**核心特性**:
|
||
- ✅ 自动加密/签名处理
|
||
- ✅ 统一错误处理
|
||
- ✅ HTTP Keep-Alive 连接池
|
||
- ✅ 可配置超时时间
|
||
- ✅ 完整的测试覆盖(88.8%)
|
||
|
||
## 配置说明
|
||
|
||
### 环境变量配置
|
||
|
||
Gateway 客户端通过环境变量配置,支持以下参数:
|
||
|
||
| 环境变量 | 说明 | 默认值 | 必填 |
|
||
|---------|------|--------|------|
|
||
| `JUNHONG_GATEWAY_BASE_URL` | Gateway API 基础 URL | - | ✅ |
|
||
| `JUNHONG_GATEWAY_APP_ID` | 应用 ID | - | ✅ |
|
||
| `JUNHONG_GATEWAY_APP_SECRET` | 应用密钥 | - | ✅ |
|
||
| `JUNHONG_GATEWAY_TIMEOUT` | 请求超时时间(秒) | 30 | ❌ |
|
||
|
||
### 配置示例
|
||
|
||
**开发环境** (`.env.local`):
|
||
```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
|
||
```
|
||
|
||
**生产环境** (`docker-compose.yml`):
|
||
```yaml
|
||
services:
|
||
api:
|
||
environment:
|
||
- JUNHONG_GATEWAY_BASE_URL=https://gateway.prod.example.com
|
||
- JUNHONG_GATEWAY_APP_ID=${GATEWAY_APP_ID}
|
||
- JUNHONG_GATEWAY_APP_SECRET=${GATEWAY_APP_SECRET}
|
||
- JUNHONG_GATEWAY_TIMEOUT=60
|
||
```
|
||
|
||
## 使用示例
|
||
|
||
### 1. 基础用法
|
||
|
||
#### 获取客户端实例
|
||
|
||
Gateway 客户端在 Bootstrap 阶段自动初始化,通过依赖注入获取:
|
||
|
||
```go
|
||
package iot_card
|
||
|
||
import (
|
||
"context"
|
||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
type Service struct {
|
||
gatewayClient *gateway.Client
|
||
logger *zap.Logger
|
||
}
|
||
|
||
func New(gatewayClient *gateway.Client, logger *zap.Logger) *Service {
|
||
return &Service{
|
||
gatewayClient: gatewayClient,
|
||
logger: logger,
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 查询流量卡状态
|
||
|
||
```go
|
||
func (s *Service) SyncCardStatus(ctx context.Context, iccid string) error {
|
||
if s.gatewayClient == nil {
|
||
return errors.New(errors.CodeGatewayError, "Gateway 客户端未配置")
|
||
}
|
||
|
||
resp, err := s.gatewayClient.QueryCardStatus(ctx, &gateway.CardStatusReq{
|
||
CardNo: iccid,
|
||
})
|
||
if err != nil {
|
||
s.logger.Error("查询卡状态失败", zap.String("iccid", iccid), zap.Error(err))
|
||
return errors.Wrap(errors.CodeGatewayError, err, "查询卡状态失败")
|
||
}
|
||
|
||
s.logger.Info("查询卡状态成功",
|
||
zap.String("iccid", resp.ICCID),
|
||
zap.String("status", resp.CardStatus),
|
||
)
|
||
|
||
return nil
|
||
}
|
||
```
|
||
|
||
### 2. 流量卡管理
|
||
|
||
#### 查询流量使用情况
|
||
|
||
```go
|
||
func (s *Service) GetFlowUsage(ctx context.Context, iccid string) (*gateway.FlowUsageResp, error) {
|
||
resp, err := s.gatewayClient.QueryFlow(ctx, &gateway.FlowQueryReq{
|
||
CardNo: iccid,
|
||
})
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeGatewayError, err, "查询流量失败")
|
||
}
|
||
|
||
return resp, nil
|
||
}
|
||
```
|
||
|
||
#### 流量卡停机
|
||
|
||
```go
|
||
func (s *Service) StopCard(ctx context.Context, iccid string) error {
|
||
err := s.gatewayClient.StopCard(ctx, &gateway.CardOperationReq{
|
||
CardNo: iccid,
|
||
})
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeGatewayError, err, "停机失败")
|
||
}
|
||
|
||
s.logger.Info("停机成功", zap.String("iccid", iccid))
|
||
return nil
|
||
}
|
||
```
|
||
|
||
#### 流量卡复机
|
||
|
||
```go
|
||
func (s *Service) StartCard(ctx context.Context, iccid string) error {
|
||
err := s.gatewayClient.StartCard(ctx, &gateway.CardOperationReq{
|
||
CardNo: iccid,
|
||
})
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeGatewayError, err, "复机失败")
|
||
}
|
||
|
||
s.logger.Info("复机成功", zap.String("iccid", iccid))
|
||
return nil
|
||
}
|
||
```
|
||
|
||
#### 查询实名认证状态
|
||
|
||
```go
|
||
func (s *Service) CheckRealnameStatus(ctx context.Context, iccid string) (string, error) {
|
||
resp, err := s.gatewayClient.QueryRealnameStatus(ctx, &gateway.CardStatusReq{
|
||
CardNo: iccid,
|
||
})
|
||
if err != nil {
|
||
return "", errors.Wrap(errors.CodeGatewayError, err, "查询实名状态失败")
|
||
}
|
||
|
||
return resp.Status, nil
|
||
}
|
||
```
|
||
|
||
#### 获取实名认证链接
|
||
|
||
```go
|
||
func (s *Service) GetRealnameLink(ctx context.Context, iccid string) (string, error) {
|
||
resp, err := s.gatewayClient.GetRealnameLink(ctx, &gateway.CardStatusReq{
|
||
CardNo: iccid,
|
||
})
|
||
if err != nil {
|
||
return "", errors.Wrap(errors.CodeGatewayError, err, "获取实名链接失败")
|
||
}
|
||
|
||
return resp.Link, nil
|
||
}
|
||
```
|
||
|
||
### 3. 设备管理
|
||
|
||
#### 查询设备信息
|
||
|
||
```go
|
||
func (s *Service) GetDeviceInfo(ctx context.Context, imei string) (*gateway.DeviceInfoResp, error) {
|
||
resp, err := s.gatewayClient.GetDeviceInfo(ctx, &gateway.DeviceInfoReq{
|
||
DeviceID: imei,
|
||
})
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeGatewayError, err, "查询设备信息失败")
|
||
}
|
||
|
||
return resp, nil
|
||
}
|
||
```
|
||
|
||
#### 查询设备卡槽信息
|
||
|
||
```go
|
||
func (s *Service) GetDeviceSlots(ctx context.Context, imei string) ([]gateway.SlotInfo, error) {
|
||
resp, err := s.gatewayClient.GetSlotInfo(ctx, &gateway.DeviceInfoReq{
|
||
DeviceID: imei,
|
||
})
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeGatewayError, err, "查询卡槽信息失败")
|
||
}
|
||
|
||
return resp.Slots, nil
|
||
}
|
||
```
|
||
|
||
#### 设置设备限速
|
||
|
||
```go
|
||
func (s *Service) SetDeviceSpeed(ctx context.Context, imei string, uploadKBps, downloadKBps int) error {
|
||
err := s.gatewayClient.SetSpeedLimit(ctx, &gateway.SpeedLimitReq{
|
||
DeviceID: imei,
|
||
UploadSpeed: uploadKBps,
|
||
DownloadSpeed: downloadKBps,
|
||
})
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeGatewayError, err, "设置限速失败")
|
||
}
|
||
|
||
s.logger.Info("设置限速成功",
|
||
zap.String("imei", imei),
|
||
zap.Int("upload", uploadKBps),
|
||
zap.Int("download", downloadKBps),
|
||
)
|
||
return nil
|
||
}
|
||
```
|
||
|
||
#### 设置设备 WiFi
|
||
|
||
```go
|
||
func (s *Service) ConfigureWiFi(ctx context.Context, imei, ssid, password string, enabled bool) error {
|
||
enabledInt := 0
|
||
if enabled {
|
||
enabledInt = 1
|
||
}
|
||
|
||
err := s.gatewayClient.SetWiFi(ctx, &gateway.WiFiReq{
|
||
DeviceID: imei,
|
||
SSID: ssid,
|
||
Password: password,
|
||
Enabled: enabledInt,
|
||
})
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeGatewayError, err, "设置WiFi失败")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
```
|
||
|
||
#### 设备切换卡
|
||
|
||
```go
|
||
func (s *Service) SwitchDeviceCard(ctx context.Context, imei, targetICCID string) error {
|
||
err := s.gatewayClient.SwitchCard(ctx, &gateway.SwitchCardReq{
|
||
DeviceID: imei,
|
||
TargetICCID: targetICCID,
|
||
})
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeGatewayError, err, "切换卡失败")
|
||
}
|
||
|
||
s.logger.Info("切换卡成功",
|
||
zap.String("imei", imei),
|
||
zap.String("targetICCID", targetICCID),
|
||
)
|
||
return nil
|
||
}
|
||
```
|
||
|
||
#### 设备重启
|
||
|
||
```go
|
||
func (s *Service) RebootDevice(ctx context.Context, imei string) error {
|
||
err := s.gatewayClient.RebootDevice(ctx, &gateway.DeviceOperationReq{
|
||
DeviceID: imei,
|
||
})
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeGatewayError, err, "重启设备失败")
|
||
}
|
||
|
||
s.logger.Info("重启设备成功", zap.String("imei", imei))
|
||
return nil
|
||
}
|
||
```
|
||
|
||
#### 设备恢复出厂设置
|
||
|
||
```go
|
||
func (s *Service) ResetDevice(ctx context.Context, imei string) error {
|
||
err := s.gatewayClient.ResetDevice(ctx, &gateway.DeviceOperationReq{
|
||
DeviceID: imei,
|
||
})
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeGatewayError, err, "恢复出厂设置失败")
|
||
}
|
||
|
||
s.logger.Info("恢复出厂设置成功", zap.String("imei", imei))
|
||
return nil
|
||
}
|
||
```
|
||
|
||
## 错误处理
|
||
|
||
### 统一错误码
|
||
|
||
Gateway 客户端使用统一的错误码系统(`pkg/errors/codes.go`):
|
||
|
||
| 错误码 | 说明 |
|
||
|-------|------|
|
||
| `1110` | Gateway 连接失败 |
|
||
| `1111` | Gateway 请求超时 |
|
||
| `1112` | Gateway 业务错误 |
|
||
| `1113` | Gateway 响应解析失败 |
|
||
| `1114` | Gateway 签名验证失败 |
|
||
|
||
### 错误处理最佳实践
|
||
|
||
```go
|
||
func (s *Service) ProcessCard(ctx context.Context, iccid string) error {
|
||
resp, err := s.gatewayClient.QueryCardStatus(ctx, &gateway.CardStatusReq{
|
||
CardNo: iccid,
|
||
})
|
||
if err != nil {
|
||
s.logger.Error("查询卡状态失败",
|
||
zap.String("iccid", iccid),
|
||
zap.Error(err),
|
||
)
|
||
|
||
return errors.Wrap(errors.CodeGatewayError, err, "查询卡状态失败")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
```
|
||
|
||
### 错误分类处理
|
||
|
||
```go
|
||
func (s *Service) HandleGatewayError(err error) {
|
||
switch {
|
||
case strings.Contains(err.Error(), "超时"):
|
||
s.logger.Warn("Gateway 请求超时,请稍后重试")
|
||
case strings.Contains(err.Error(), "业务错误"):
|
||
s.logger.Error("Gateway 业务处理失败", zap.Error(err))
|
||
case strings.Contains(err.Error(), "连接失败"):
|
||
s.logger.Error("Gateway 连接失败,请检查网络", zap.Error(err))
|
||
default:
|
||
s.logger.Error("Gateway 未知错误", zap.Error(err))
|
||
}
|
||
}
|
||
```
|
||
|
||
## 高级用法
|
||
|
||
### 自定义超时时间
|
||
|
||
```go
|
||
client := gateway.NewClient(baseURL, appID, appSecret).
|
||
WithTimeout(60 * time.Second)
|
||
|
||
resp, err := client.QueryCardStatus(ctx, &gateway.CardStatusReq{
|
||
CardNo: iccid,
|
||
})
|
||
```
|
||
|
||
### 使用扩展字段(广电国网)
|
||
|
||
部分 API 支持 `extend` 扩展字段用于特殊参数:
|
||
|
||
```go
|
||
err := s.gatewayClient.StopCard(ctx, &gateway.CardOperationReq{
|
||
CardNo: iccid,
|
||
Extend: "special_param=value",
|
||
})
|
||
```
|
||
|
||
### Context 传递
|
||
|
||
所有 API 方法都支持 `context.Context`,可以传递超时、取消信号等:
|
||
|
||
```go
|
||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
|
||
resp, err := s.gatewayClient.QueryCardStatus(ctx, &gateway.CardStatusReq{
|
||
CardNo: iccid,
|
||
})
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
### 1. 空值检查
|
||
|
||
```go
|
||
func (s *Service) SafeCall(ctx context.Context) error {
|
||
if s.gatewayClient == nil {
|
||
return errors.New(errors.CodeGatewayError, "Gateway 客户端未配置")
|
||
}
|
||
|
||
}
|
||
```
|
||
|
||
### 2. 日志记录
|
||
|
||
```go
|
||
func (s *Service) CallWithLogging(ctx context.Context, iccid string) error {
|
||
s.logger.Info("开始查询卡状态", zap.String("iccid", iccid))
|
||
|
||
resp, err := s.gatewayClient.QueryCardStatus(ctx, &gateway.CardStatusReq{
|
||
CardNo: iccid,
|
||
})
|
||
if err != nil {
|
||
s.logger.Error("查询失败", zap.String("iccid", iccid), zap.Error(err))
|
||
return err
|
||
}
|
||
|
||
s.logger.Info("查询成功",
|
||
zap.String("iccid", resp.ICCID),
|
||
zap.String("status", resp.CardStatus),
|
||
)
|
||
return nil
|
||
}
|
||
```
|
||
|
||
### 3. 重试机制
|
||
|
||
```go
|
||
func (s *Service) QueryWithRetry(ctx context.Context, iccid string) (*gateway.CardStatusResp, error) {
|
||
maxRetries := 3
|
||
var lastErr error
|
||
|
||
for i := 0; i < maxRetries; i++ {
|
||
resp, err := s.gatewayClient.QueryCardStatus(ctx, &gateway.CardStatusReq{
|
||
CardNo: iccid,
|
||
})
|
||
if err == nil {
|
||
return resp, nil
|
||
}
|
||
|
||
lastErr = err
|
||
s.logger.Warn("重试查询",
|
||
zap.Int("attempt", i+1),
|
||
zap.String("iccid", iccid),
|
||
zap.Error(err),
|
||
)
|
||
|
||
time.Sleep(time.Second * time.Duration(i+1))
|
||
}
|
||
|
||
return nil, errors.Wrap(errors.CodeGatewayError, lastErr, "重试失败")
|
||
}
|
||
```
|
||
|
||
### 4. 批量处理
|
||
|
||
```go
|
||
func (s *Service) BatchSyncStatus(ctx context.Context, iccids []string) error {
|
||
for _, iccid := range iccids {
|
||
if err := s.SyncCardStatus(ctx, iccid); err != nil {
|
||
s.logger.Error("同步失败", zap.String("iccid", iccid), zap.Error(err))
|
||
continue
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
```
|
||
|
||
## 测试
|
||
|
||
### 单元测试
|
||
|
||
Gateway 客户端提供完整的单元测试覆盖(88.8%):
|
||
|
||
```bash
|
||
go test -v ./internal/gateway
|
||
go test -cover ./internal/gateway
|
||
```
|
||
|
||
### 集成测试
|
||
|
||
使用 `-short` 标志跳过集成测试:
|
||
|
||
```bash
|
||
go test -v ./internal/gateway -short
|
||
```
|
||
|
||
运行集成测试(需要真实 Gateway 环境):
|
||
|
||
```bash
|
||
source .env.local && go test -v ./internal/gateway
|
||
```
|
||
|
||
## 故障排查
|
||
|
||
### 常见问题
|
||
|
||
#### 1. Gateway 客户端未配置
|
||
|
||
**错误**: `Gateway 客户端未配置`
|
||
|
||
**原因**: 环境变量未设置或 Bootstrap 初始化失败
|
||
|
||
**解决方案**:
|
||
```bash
|
||
export JUNHONG_GATEWAY_BASE_URL=https://lplan.whjhft.com/openapi
|
||
export JUNHONG_GATEWAY_APP_ID=your_app_id
|
||
export JUNHONG_GATEWAY_APP_SECRET=your_app_secret
|
||
```
|
||
|
||
#### 2. 请求超时
|
||
|
||
**错误**: `Gateway 请求超时`
|
||
|
||
**原因**: 网络延迟或 Gateway 服务响应慢
|
||
|
||
**解决方案**:
|
||
```go
|
||
client := client.WithTimeout(60 * time.Second)
|
||
```
|
||
|
||
#### 3. 业务错误
|
||
|
||
**错误**: `业务错误: code=500, msg=xxx`
|
||
|
||
**原因**: Gateway 服务端业务逻辑错误
|
||
|
||
**解决方案**: 检查请求参数是否正确,查看日志中的 `TraceID` 联系 Gateway 服务提供方
|
||
|
||
#### 4. 签名验证失败
|
||
|
||
**错误**: `签名验证失败`
|
||
|
||
**原因**: `AppSecret` 配置错误
|
||
|
||
**解决方案**: 检查 `JUNHONG_GATEWAY_APP_SECRET` 环境变量是否正确
|
||
|
||
## 相关文档
|
||
|
||
- [Gateway API 参考](./gateway-api-reference.md) - 完整的 API 接口文档
|
||
- [错误处理指南](./003-error-handling/使用指南.md) - 统一错误处理规范
|
||
- [测试连接管理规范](./testing/test-connection-guide.md) - 测试规范和最佳实践
|