diff --git a/openspec/changes/gateway-integration/design.md b/openspec/changes/gateway-integration/design.md deleted file mode 100644 index 1e1e2de..0000000 --- a/openspec/changes/gateway-integration/design.md +++ /dev/null @@ -1,570 +0,0 @@ -# 设计文档: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 加密传输 diff --git a/openspec/changes/gateway-integration/proposal.md b/openspec/changes/gateway-integration/proposal.md deleted file mode 100644 index cd125b7..0000000 --- a/openspec/changes/gateway-integration/proposal.md +++ /dev/null @@ -1,146 +0,0 @@ -# 提案:Gateway API 统一封装 - -## Why - -当前项目需要调用外部 Gateway API 来实现物联网卡和设备的生命周期管理功能(状态查询、停复机、设备控制等)。Gateway API 具有以下特点: - -1. **复杂的认证机制**:需要 AES-128-ECB 加密 + MD5 签名 -2. **多个接口**:14 个 API(流量卡 7 个 + 设备 7 个) -3. **多场景调用**:Handler 层业务逻辑 + Asynq 定时任务批量同步 -4. **缺乏统一封装**:调用逻辑分散,加密签名重复实现 - -本变更旨在**封装 Gateway API 为统一的能力模块**,提供类型安全的接口、统一的错误处理和配置管理,供 Service 层和 Asynq 任务调用。 - -## What Changes - -### 1. Gateway 客户端封装 -- 新增 `internal/gateway/` 包,提供 Gateway API 的统一封装 -- 实现 AES-128-ECB 加密 + MD5 签名机制 -- 封装 14 个 API 接口(流量卡 7 个 + 设备 7 个) -- 提供类型安全的请求/响应结构体 - -### 2. 配置集成 -- 在 `pkg/config/config.go` 中添加 `GatewayConfig` 配置结构 -- 支持环境变量配置:`JUNHONG_GATEWAY_BASE_URL`、`JUNHONG_GATEWAY_APP_ID`、`JUNHONG_GATEWAY_APP_SECRET` -- 配置项包括:BaseURL、AppID、AppSecret、Timeout - -### 3. 错误处理 -- 在 `pkg/errors/codes.go` 中定义 Gateway 相关错误码(1110-1119) -- 统一错误处理:加密失败、签名失败、请求超时、响应格式错误 - -### 4. 依赖注入 -- 在 `internal/bootstrap/` 中初始化 Gateway 客户端 -- 注入到需要调用 Gateway API 的 Service - -### 5. 测试覆盖 -- 单元测试:加密/签名函数验证 -- 集成测试:实际调用 Gateway API 验证 - -## Capabilities - -### New Capabilities - -- `gateway-client`: Gateway API 统一客户端,提供 14 个接口的类型安全封装 -- `gateway-crypto`: AES-128-ECB 加密 + MD5 签名工具函数 - -### Modified Capabilities - -- `config-management`: 添加 Gateway 配置支持 -- `error-handling`: 添加 Gateway 相关错误码 -- `dependency-injection`: 在 bootstrap 中初始化 Gateway 客户端 - -## Impact - -### 代码变更 - -| 文件/目录 | 变更类型 | 说明 | -|-----------|----------|------| -| `internal/gateway/client.go` | 新增 | Gateway 客户端主体(Client 结构体 + doRequest) | -| `internal/gateway/crypto.go` | 新增 | AES 加密 + MD5 签名函数 | -| `internal/gateway/flow_card.go` | 新增 | 流量卡 7 个 API 方法封装 | -| `internal/gateway/device.go` | 新增 | 设备 7 个 API 方法封装 | -| `internal/gateway/models.go` | 新增 | 请求/响应 DTO 定义 | -| `internal/gateway/client_test.go` | 新增 | 单元测试和集成测试 | -| `pkg/config/config.go` | 修改 | 添加 GatewayConfig 结构体 | -| `pkg/errors/codes.go` | 修改 | 添加 Gateway 错误码(1110-1119) | -| `internal/bootstrap/bootstrap.go` | 修改 | 初始化 Gateway 客户端 | - -### Gateway API 接口列表 - -**流量卡 API(7个)**: -1. `/flow-card/status` - 流量卡状态查询 -2. `/flow-card/flow` - 流量使用查询 -3. `/flow-card/realname-status` - 实名认证状态查询 -4. `/flow-card/cardStop` - 流量卡停机 -5. `/flow-card/cardStart` - 流量卡复机 -6. `/flow-card/realname-link` - 获取实名认证跳转链接 -7. `/flow-card/batch-query` - 批量查询(未来扩展) - -**设备 API(7个)**: -1. `/device/info` - 获取设备信息 -2. `/device/slot-info` - 获取设备卡槽信息 -3. `/device/speed-limit` - 设置设备限速 -4. `/device/wifi` - 设置设备 WiFi -5. `/device/switch-card` - 设备切换卡 -6. `/device/reset` - 设备恢复出厂设置 -7. `/device/reboot` - 设备重启 - -### 配置变更 - -**新增环境变量**: -```bash -JUNHONG_GATEWAY_BASE_URL=https://lplan.whjhft.com/openapi -JUNHONG_GATEWAY_APP_ID=60bgt1X8i7AvXqkd -JUNHONG_GATEWAY_APP_SECRET=BZeQttaZQt0i73moF -JUNHONG_GATEWAY_TIMEOUT=30 -``` - -### 依赖 - -- 无新增外部依赖 -- 使用标准库:`crypto/aes`、`crypto/md5`、`encoding/base64`、`net/http` - -## 预期收益 - -| 指标 | 变更前 | 变更后 | -|------|--------|--------| -| Gateway 调用代码重复 | 每次调用重复加密签名 | 统一封装,零重复 | -| 错误处理一致性 | 不一致 | 统一错误码 | -| 类型安全 | 手动序列化,易出错 | 强类型 DTO,编译时检查 | -| 测试覆盖率 | 0% | 90%+ | -| 配置管理 | 硬编码 | 统一配置 | - -## 风险与缓解 - -| 风险 | 影响 | 缓解措施 | -|------|------|---------| -| AES-ECB 模式安全性 | 低(外部系统要求) | 文档注明,无法改变 | -| 签名算法兼容性 | 中(签名不匹配导致认证失败) | 先实现端到端测试验证签名 | -| Gateway 响应格式变更 | 中(解析失败) | 统一错误处理,兼容性版本 | - -## 后续计划 - -1. **阶段 1(本次变更)**: - - 实现 Gateway 客户端基础封装 - - 支持同步模式(14 个接口) - - 集成到 Service 层 - -2. **阶段 2(未来优化)**: - - 实现异步模式回调接口 - - 添加批量查询接口 - - 实现请求重试和超时控制 - -3. **阶段 3(性能优化)**: - - 添加响应缓存(Redis) - - 实现请求限流(防止 Gateway 过载) - - 监控和告警集成 - -## 验收标准 - -- [ ] Gateway 客户端成功调用所有 14 个 API 接口 -- [ ] 加密/签名验证通过(与 Gateway 文档一致) -- [ ] 错误处理覆盖所有异常场景(网络错误、响应格式错误等) -- [ ] 单元测试覆盖率 ≥ 90% -- [ ] 集成测试验证真实 Gateway API 调用 -- [ ] 配置通过环境变量成功加载 -- [ ] 文档完整(API 文档、使用示例、错误码说明) diff --git a/openspec/changes/gateway-integration/specs/gateway-client/spec.md b/openspec/changes/gateway-integration/specs/gateway-client/spec.md deleted file mode 100644 index e92a25b..0000000 --- a/openspec/changes/gateway-integration/specs/gateway-client/spec.md +++ /dev/null @@ -1,220 +0,0 @@ -# Gateway Client Specification - -Gateway API 统一客户端,提供 14 个接口的类型安全封装。 - -## ADDED Requirements - -### Requirement: Gateway 客户端结构 - -系统 SHALL 提供 `gateway.Client` 结构体,封装所有 Gateway API 调用。 - -客户端字段: -- `baseURL string` - Gateway API 基础 URL -- `appID string` - 应用 ID -- `appSecret string` - 应用密钥 -- `httpClient *http.Client` - HTTP 客户端(支持连接复用) -- `timeout time.Duration` - 请求超时时间 - -#### Scenario: 创建 Gateway 客户端 - -- **WHEN** 调用 `gateway.NewClient(baseURL, appID, appSecret)` -- **THEN** 返回已初始化的 `Client` 实例 -- **AND** HTTP 客户端配置正确(支持 Keep-Alive) - -#### Scenario: 配置超时时间 - -- **WHEN** 调用 `client.WithTimeout(30 * time.Second)` -- **THEN** 客户端的 `timeout` 字段更新为 30 秒 -- **AND** 返回客户端自身(支持链式调用) - -### Requirement: 统一请求方法 - -系统 SHALL 提供 `doRequest` 方法,统一处理加密、签名、HTTP 请求和响应解析。 - -#### Scenario: 成功的 API 调用 - -- **WHEN** 调用 `doRequest(ctx, "/flow-card/status", businessData)` -- **THEN** 业务数据使用 AES-128-ECB 加密 -- **AND** 请求使用 MD5 签名 -- **AND** HTTP POST 发送到 `{baseURL}/flow-card/status` -- **AND** 响应中的 `data` 字段解密并返回 - -#### Scenario: 网络错误 - -- **WHEN** HTTP 请求失败(网络中断、DNS 解析失败) -- **THEN** 返回 `CodeGatewayError` 错误 -- **AND** 错误信息包含原始网络错误 - -#### Scenario: 请求超时 - -- **WHEN** HTTP 请求超过配置的超时时间 -- **THEN** 返回 `CodeGatewayTimeout` 错误 -- **AND** Context 超时错误被正确识别 - -#### Scenario: 响应格式错误 - -- **WHEN** Gateway 响应无法解析为 JSON -- **THEN** 返回 `CodeGatewayInvalidResp` 错误 -- **AND** 错误信息包含原始响应内容(限制 200 字符) - -#### Scenario: Gateway 业务错误 - -- **WHEN** Gateway 响应中 `code != 200` -- **THEN** 返回 `CodeGatewayError` 错误 -- **AND** 错误信息包含 Gateway 的 code 和 msg - -### Requirement: 流量卡 API 封装 - -系统 SHALL 提供 7 个流量卡相关的 API 方法。 - -#### Scenario: 查询流量卡状态 - -- **WHEN** 调用 `client.QueryCardStatus(ctx, &CardStatusReq{CardNo: "898608070422D0010269"})` -- **THEN** 返回 `CardStatusResp` 包含 ICCID 和卡状态 -- **AND** 卡状态为:"准备"、"正常" 或 "停机" 之一 - -#### Scenario: 查询流量使用 - -- **WHEN** 调用 `client.QueryFlow(ctx, &FlowQueryReq{CardNo: "898608070422D0010269"})` -- **THEN** 返回 `FlowUsageResp` 包含已用流量和单位 -- **AND** 流量单位为 "MB" - -#### Scenario: 查询实名认证状态 - -- **WHEN** 调用 `client.QueryRealnameStatus(ctx, &CardStatusReq{CardNo: "898608070422D0010269"})` -- **THEN** 返回实名认证状态信息 - -#### Scenario: 流量卡停机 - -- **WHEN** 调用 `client.StopCard(ctx, &CardOperationReq{CardNo: "898608070422D0010269"})` -- **THEN** Gateway 执行停机操作 -- **AND** 方法返回 nil(成功)或错误 - -#### Scenario: 流量卡复机 - -- **WHEN** 调用 `client.StartCard(ctx, &CardOperationReq{CardNo: "898608070422D0010269"})` -- **THEN** Gateway 执行复机操作 -- **AND** 方法返回 nil(成功)或错误 - -#### Scenario: 获取实名认证链接 - -- **WHEN** 调用 `client.GetRealnameLink(ctx, &CardStatusReq{CardNo: "898608070422D0010269"})` -- **THEN** 返回实名认证跳转链接 -- **AND** 链接格式为有效的 HTTPS URL - -#### Scenario: 广电国网扩展参数 - -- **WHEN** 停机/复机请求中 `Extend` 字段不为空 -- **THEN** 请求包含 `extend` 参数 -- **AND** Gateway 正确处理广电国网特殊逻辑 - -### Requirement: 设备 API 封装 - -系统 SHALL 提供 7 个设备相关的 API 方法。 - -#### Scenario: 查询设备信息 - -- **WHEN** 调用 `client.GetDeviceInfo(ctx, &DeviceInfoReq{CardNo: "898608070422D0010269"})` -- **THEN** 返回 `DeviceInfoResp` 包含设备详细信息 -- **AND** 信息包括:IMEI、在线状态、信号强度、WiFi 配置、速率等 - -#### Scenario: 通过设备 ID 查询 - -- **WHEN** 调用 `client.GetDeviceInfo(ctx, &DeviceInfoReq{DeviceID: "868123456789012"})` -- **THEN** 通过设备 IMEI 查询设备信息 -- **AND** 返回结果与通过卡号查询一致 - -#### Scenario: 查询设备卡槽信息 - -- **WHEN** 调用 `client.GetSlotInfo(ctx, &DeviceInfoReq{CardNo: "898608070422D0010269"})` -- **THEN** 返回设备中已安装的物联网卡信息 - -#### Scenario: 设置设备限速 - -- **WHEN** 调用 `client.SetSpeedLimit(ctx, &SpeedLimitReq{DeviceID: "868123456789012", UploadSpeed: 1024, DownloadSpeed: 2048})` -- **THEN** 设备上下行速率设置为指定值(KB/s) - -#### Scenario: 设置设备 WiFi - -- **WHEN** 调用 `client.SetWiFi(ctx, &WiFiReq{DeviceID: "868123456789012", SSID: "MyWiFi", Password: "12345678", Enabled: true})` -- **THEN** 设备 WiFi 配置更新 -- **AND** WiFi 名称、密码和启用状态正确设置 - -#### Scenario: 设备切换卡 - -- **WHEN** 调用 `client.SwitchCard(ctx, &SwitchCardReq{DeviceID: "868123456789012", TargetICCID: "898608070422D0010270"})` -- **THEN** 多卡设备切换到目标 ICCID - -#### Scenario: 设备恢复出厂设置 - -- **WHEN** 调用 `client.ResetDevice(ctx, &DeviceOperationReq{DeviceID: "868123456789012"})` -- **THEN** 设备恢复为出厂状态 - -#### Scenario: 设备重启 - -- **WHEN** 调用 `client.RebootDevice(ctx, &DeviceOperationReq{DeviceID: "868123456789012"})` -- **THEN** 设备执行重启操作 - -### Requirement: 类型安全的 DTO - -系统 SHALL 为所有请求和响应定义类型安全的结构体。 - -#### Scenario: 请求 DTO 包含验证标签 - -- **WHEN** 定义 `CardStatusReq` 结构体 -- **THEN** `CardNo` 字段包含 `validate:"required"` 标签 -- **AND** 可以使用 Validator 库进行验证 - -#### Scenario: 响应 DTO 正确解析 - -- **WHEN** Gateway 返回 JSON 响应 -- **THEN** `CardStatusResp` 结构体正确解析 `iccid`、`cardStatus`、`extend` 字段 -- **AND** 字段类型与 Gateway 文档一致 - -### Requirement: 并发安全 - -系统 SHALL 确保 `Client` 结构体可以安全地并发调用。 - -#### Scenario: 多个 Goroutine 并发调用 - -- **WHEN** 10 个 Goroutine 同时调用 `client.QueryCardStatus` -- **THEN** 所有请求都正确执行 -- **AND** 不发生 race condition - -#### Scenario: HTTP 连接复用 - -- **WHEN** 多次调用相同的 Gateway API -- **THEN** HTTP 客户端复用 TCP 连接 -- **AND** 减少连接建立开销 - -### Requirement: 错误处理一致性 - -系统 SHALL 使用项目统一的错误码系统。 - -#### Scenario: Gateway 错误返回统一错误码 - -- **WHEN** Gateway API 调用失败 -- **THEN** 返回 `errors.AppError` 类型 -- **AND** 错误码为 `CodeGatewayError`、`CodeGatewayTimeout` 等之一 - -#### Scenario: 错误包含上下文信息 - -- **WHEN** 加密失败 -- **THEN** 错误信息为 "数据加密失败" -- **AND** 包含底层错误的详细信息 - -### Requirement: Context 支持 - -系统 SHALL 支持通过 Context 控制请求超时和取消。 - -#### Scenario: 使用 Context 控制超时 - -- **WHEN** 调用 `client.QueryCardStatus(ctx, req)` 且 ctx 设置了 30 秒超时 -- **THEN** 请求在 30 秒后自动超时 -- **AND** 返回 `CodeGatewayTimeout` 错误 - -#### Scenario: 取消请求 - -- **WHEN** 调用 `client.QueryCardStatus(ctx, req)` 且 ctx 被取消 -- **THEN** 请求立即停止 -- **AND** 返回 context canceled 错误 diff --git a/openspec/changes/gateway-integration/specs/gateway-config/spec.md b/openspec/changes/gateway-integration/specs/gateway-config/spec.md deleted file mode 100644 index be143a8..0000000 --- a/openspec/changes/gateway-integration/specs/gateway-config/spec.md +++ /dev/null @@ -1,175 +0,0 @@ -# Gateway Config Specification - -Gateway API 的配置集成规范,定义配置结构和加载方式。 - -## ADDED Requirements - -### Requirement: Gateway 配置结构 - -系统 SHALL 在 `pkg/config/config.go` 中添加 `GatewayConfig` 结构体。 - -配置字段: -- `BaseURL string` - Gateway API 基础 URL -- `AppID string` - 应用 ID -- `AppSecret string` - 应用密钥 -- `Timeout int` - 请求超时时间(秒) - -#### Scenario: 配置结构定义 - -- **WHEN** 定义 `GatewayConfig` 结构体 -- **THEN** 包含 `mapstructure` 标签用于 Viper 解析 -- **AND** 字段名使用 snake_case(如 `base_url`、`app_id`) - -#### Scenario: 集成到主配置 - -- **WHEN** 在 `Config` 结构体中添加 `Gateway GatewayConfig` 字段 -- **THEN** 使用 `mapstructure:"gateway"` 标签 -- **AND** 配置可通过 `config.Get().Gateway` 访问 - -### Requirement: 默认配置嵌入 - -系统 SHALL 在 `pkg/config/defaults/config.yaml` 中添加 Gateway 默认配置。 - -#### Scenario: 嵌入默认配置 - -- **WHEN** 读取嵌入的默认配置文件 -- **THEN** 包含 `gateway` 配置节 -- **AND** 配置包含: - ```yaml - gateway: - base_url: "https://lplan.whjhft.com/openapi" - app_id: "60bgt1X8i7AvXqkd" - app_secret: "BZeQttaZQt0i73moF" - timeout: 30 - ``` - -### Requirement: 环境变量覆盖 - -系统 SHALL 支持通过环境变量覆盖 Gateway 配置。 - -环境变量格式:`JUNHONG_GATEWAY_{KEY}` - -#### Scenario: 覆盖 BaseURL - -- **WHEN** 设置环境变量 `JUNHONG_GATEWAY_BASE_URL=https://test.example.com` -- **THEN** `config.Gateway.BaseURL` 的值为 "https://test.example.com" -- **AND** 覆盖嵌入配置中的默认值 - -#### Scenario: 覆盖 AppID - -- **WHEN** 设置环境变量 `JUNHONG_GATEWAY_APP_ID=test_app_id` -- **THEN** `config.Gateway.AppID` 的值为 "test_app_id" - -#### Scenario: 覆盖 AppSecret - -- **WHEN** 设置环境变量 `JUNHONG_GATEWAY_APP_SECRET=test_secret` -- **THEN** `config.Gateway.AppSecret` 的值为 "test_secret" - -#### Scenario: 覆盖 Timeout - -- **WHEN** 设置环境变量 `JUNHONG_GATEWAY_TIMEOUT=60` -- **THEN** `config.Gateway.Timeout` 的值为 60 - -### Requirement: 配置验证 - -系统 SHALL 在配置加载后验证 Gateway 配置的有效性。 - -#### Scenario: 必填字段验证 - -- **WHEN** 配置加载完成 -- **THEN** 验证 `BaseURL`、`AppID`、`AppSecret` 不为空 -- **AND** 如果为空,返回明确的错误信息 - -#### Scenario: BaseURL 格式验证 - -- **WHEN** 验证 `BaseURL` 字段 -- **THEN** 必须以 `http://` 或 `https://` 开头 -- **AND** 不能以 `/` 结尾 - -#### Scenario: Timeout 范围验证 - -- **WHEN** 验证 `Timeout` 字段 -- **THEN** 值必须在 5 到 300 秒之间 -- **AND** 如果超出范围,返回验证错误 - -#### Scenario: AppID 格式验证 - -- **WHEN** 验证 `AppID` 字段 -- **THEN** 长度必须 > 0 -- **AND** 不包含特殊字符(仅允许字母、数字、下划线) - -### Requirement: 敏感配置处理 - -系统 SHALL 确保 `AppSecret` 不记录到日志中。 - -#### Scenario: 配置日志脱敏 - -- **WHEN** 记录配置加载成功的日志 -- **THEN** `AppSecret` 字段显示为 "***" -- **AND** 实际值不出现在日志中 - -#### Scenario: 错误日志脱敏 - -- **WHEN** 配置验证失败并记录错误日志 -- **THEN** `AppSecret` 字段显示为 "***" - -### Requirement: Gateway 客户端初始化 - -系统 SHALL 在 `internal/bootstrap/bootstrap.go` 中初始化 Gateway 客户端。 - -#### Scenario: Bootstrap 中初始化 - -- **WHEN** 调用 `bootstrap.Bootstrap(deps)` -- **THEN** 从 `deps.Config.Gateway` 读取配置 -- **AND** 调用 `gateway.NewClient(baseURL, appID, appSecret).WithTimeout(...)` -- **AND** 将客户端赋值给 `deps.GatewayClient` - -#### Scenario: 配置错误时启动失败 - -- **WHEN** Gateway 配置验证失败 -- **THEN** `bootstrap.Bootstrap` 返回错误 -- **AND** 应用启动失败 - -### Requirement: 多环境配置支持 - -系统 SHALL 支持通过环境变量切换不同环境的 Gateway 配置。 - -#### Scenario: 开发环境配置 - -- **WHEN** 使用默认嵌入配置(未设置环境变量) -- **THEN** 使用生产环境的 Gateway URL 和凭证 - -#### Scenario: 测试环境配置 - -- **WHEN** 设置环境变量指向测试 Gateway -- **AND** `JUNHONG_GATEWAY_BASE_URL=https://test-gateway.example.com` -- **AND** `JUNHONG_GATEWAY_APP_ID=test_app_id` -- **THEN** 客户端连接到测试环境 - -## MODIFIED Requirements - -### Requirement: Config 结构体扩展 - -系统 SHALL 在现有的 `Config` 结构体中添加 `Gateway` 字段。 - -#### Scenario: 配置结构兼容性 - -- **WHEN** 添加 `Gateway GatewayConfig` 字段 -- **THEN** 不影响现有配置字段的加载 -- **AND** 现有配置(Server、Database、Redis 等)继续正常工作 - -### Requirement: Dependencies 结构体扩展 - -系统 SHALL 在 `internal/bootstrap/bootstrap.go` 的 `Dependencies` 结构体中添加 `GatewayClient` 字段。 - -#### Scenario: 依赖注入扩展 - -- **WHEN** 在 `Dependencies` 中添加 `GatewayClient *gateway.Client` 字段 -- **THEN** 不影响现有依赖的注入 -- **AND** Gateway 客户端可以注入到需要的 Service - -#### Scenario: Service 层使用 - -- **WHEN** Service 需要调用 Gateway API -- **THEN** 在 Service 构造函数中接收 `gatewayClient *gateway.Client` 参数 -- **AND** 从 Bootstrap 中传递 `deps.GatewayClient` diff --git a/openspec/changes/gateway-integration/specs/gateway-crypto/spec.md b/openspec/changes/gateway-integration/specs/gateway-crypto/spec.md deleted file mode 100644 index 8225e66..0000000 --- a/openspec/changes/gateway-integration/specs/gateway-crypto/spec.md +++ /dev/null @@ -1,155 +0,0 @@ -# Gateway Crypto Specification - -Gateway API 的加密和签名工具函数,实现 AES-128-ECB 加密和 MD5 签名机制。 - -## ADDED Requirements - -### Requirement: AES-128-ECB 加密 - -系统 SHALL 提供 `aesEncrypt` 函数,使用 AES-128-ECB 模式加密业务数据。 - -加密流程: -1. 密钥生成:`MD5(appSecret)` 的原始字节数组(16字节) -2. 加密算法:AES-128-ECB -3. 填充方式:PKCS5Padding -4. 编码输出:Base64 - -#### Scenario: 加密业务数据 - -- **WHEN** 调用 `aesEncrypt(data, appSecret)` -- **AND** `data` 为业务数据的 JSON 字节数组 -- **THEN** 返回 Base64 编码的加密字符串 -- **AND** 密钥为 `MD5(appSecret)` 的 16 字节数组 - -#### Scenario: PKCS5 填充正确性 - -- **WHEN** 业务数据长度不是 AES 块大小(16 字节)的整数倍 -- **THEN** 使用 PKCS5Padding 进行填充 -- **AND** 填充字节值等于填充长度 - -#### Scenario: 加密输出格式 - -- **WHEN** 加密成功 -- **THEN** 输出为 Base64 字符串 -- **AND** 字符串不包含换行符 - -#### Scenario: 加密失败 - -- **WHEN** AES 加密过程失败 -- **THEN** 返回 `CodeGatewayEncryptError` 错误 -- **AND** 错误信息包含原始错误 - -### Requirement: MD5 签名生成 - -系统 SHALL 提供 `generateSign` 函数,生成 MD5 签名。 - -签名流程: -1. 参数排序:`appId`、`data`、`timestamp` 按字母升序 -2. 拼接字符串:`appId=xxx&data=xxx×tamp=xxx&key=appSecret` -3. MD5 加密 -4. 转大写十六进制 - -#### Scenario: 生成正确的签名 - -- **WHEN** 调用 `generateSign(appID, encryptedData, timestamp, appSecret)` -- **THEN** 参数按字母序拼接:`appId` → `data` → `timestamp` -- **AND** 追加 `&key=appSecret` -- **AND** MD5 加密后转大写十六进制 - -#### Scenario: 签名输出格式 - -- **WHEN** 签名生成成功 -- **THEN** 输出为 32 位大写十六进制字符串 -- **AND** 例如:"ABCDEF1234567890ABCDEF1234567890" - -#### Scenario: 签名可重现 - -- **WHEN** 使用相同的 `appID`、`encryptedData`、`timestamp`、`appSecret` -- **THEN** 多次调用 `generateSign` 生成相同的签名 - -#### Scenario: 时间戳格式 - -- **WHEN** 签名中使用时间戳 -- **THEN** 时间戳为 Unix 秒级时间戳(10 位数字) -- **AND** 例如:1704067200 - -### Requirement: 参数序列化 - -系统 SHALL 正确序列化请求参数,确保与 Gateway 期望格式一致。 - -#### Scenario: 业务数据序列化 - -- **WHEN** 业务数据为 Go 结构体 -- **THEN** 使用 `sonic.Marshal` 序列化为 JSON 字符串 -- **AND** JSON 格式与 Gateway 文档一致 - -#### Scenario: 空字段处理 - -- **WHEN** 请求结构体中某些字段为空(omitempty) -- **THEN** 序列化时忽略空字段 -- **AND** 减少请求体大小 - -### Requirement: 加密/签名测试验证 - -系统 SHALL 提供加密和签名的单元测试,验证与 Gateway 文档一致性。 - -#### Scenario: 加密测试用例 - -- **WHEN** 使用已知的业务数据和 appSecret -- **THEN** 加密输出与 Gateway 文档示例一致 -- **AND** 可以被 Gateway 正确解密 - -#### Scenario: 签名测试用例 - -- **WHEN** 使用已知的参数和 appSecret -- **THEN** 签名输出与 Gateway 文档示例一致 -- **AND** Gateway 验证签名成功 - -#### Scenario: 端到端验证 - -- **WHEN** 运行集成测试,实际调用 Gateway API -- **THEN** 加密和签名被 Gateway 接受 -- **AND** 响应状态码为 200 - -### Requirement: 性能要求 - -系统 SHALL 确保加密和签名操作的性能满足要求。 - -#### Scenario: 加密性能 - -- **WHEN** 加密 1KB 的业务数据 -- **THEN** 加密时间 < 1ms -- **AND** 内存分配最小化 - -#### Scenario: 签名性能 - -- **WHEN** 生成签名 -- **THEN** 签名时间 < 0.5ms -- **AND** 无不必要的内存分配 - -### Requirement: 安全性说明 - -系统 SHALL 在文档中说明 AES-ECB 模式的安全性限制。 - -#### Scenario: 安全性文档 - -- **WHEN** 查看加密函数的文档注释 -- **THEN** 注释中说明 ECB 模式不推荐用于生产环境 -- **AND** 说明这是 Gateway 强制要求,无法改变 -- **AND** 建议使用 HTTPS 加密传输层 - -### Requirement: 字符编码一致性 - -系统 SHALL 确保所有字符串操作使用 UTF-8 编码。 - -#### Scenario: 字符串编码 - -- **WHEN** 序列化业务数据 -- **THEN** 使用 UTF-8 编码 -- **AND** 中文字符正确处理 - -#### Scenario: 签名字符串编码 - -- **WHEN** 生成签名的拼接字符串 -- **THEN** 使用 UTF-8 编码 -- **AND** 与 Gateway 期望的编码一致 diff --git a/openspec/changes/gateway-integration/tasks.md b/openspec/changes/gateway-integration/tasks.md deleted file mode 100644 index 9edb566..0000000 --- a/openspec/changes/gateway-integration/tasks.md +++ /dev/null @@ -1,186 +0,0 @@ -# 任务清单:Gateway API 统一封装 - -## Phase 1: 基础结构搭建(30min) - -### Task 1.1: 创建 Gateway 包目录结构 -- [x] 创建 `internal/gateway/` 目录 -- [x] 创建占位文件:`client.go`、`crypto.go`、`models.go` -- **验证**:目录结构创建成功 ✅ - -### Task 1.2: 实现加密/签名工具函数 -- [x] 在 `crypto.go` 中实现 `aesEncrypt` 函数(AES-128-ECB + PKCS5Padding + Base64) -- [x] 在 `crypto.go` 中实现 `generateSign` 函数(MD5 签名,大写输出) -- [x] 添加单元测试验证加密/签名正确性 -- **验证**:✅ 覆盖率 94.3% - ```bash - go test -v ./internal/gateway -run TestAESEncrypt - go test -v ./internal/gateway -run TestGenerateSign - ``` - -### Task 1.3: 实现 Gateway 客户端基础结构 -- [x] 在 `client.go` 中定义 `Client` 结构体 -- [x] 实现 `NewClient` 构造函数 -- [x] 实现 `WithTimeout` 配置方法 -- [x] 实现 `doRequest` 统一请求方法(加密、签名、HTTP 请求、响应解析) -- **验证**:✅ 编译通过,无 LSP 错误,覆盖率 90.7% - -### Task 1.4: 定义请求/响应 DTO -- [x] 在 `models.go` 中定义 `GatewayResponse` 通用响应结构 -- [x] 定义流量卡相关 DTO(`CardStatusReq`、`CardStatusResp`、`FlowQueryReq`、`FlowUsageResp` 等) -- [x] 定义设备相关 DTO(`DeviceInfoReq`、`DeviceInfoResp` 等) -- [x] 添加 JSON 标签和验证标签 -- **验证**:✅ 编译通过,结构体定义完整 - ---- - -## Phase 2: API 接口封装(40min) - -### Task 2.1: 实现流量卡 API(7个接口) -- [x] 在 `flow_card.go` 中实现 `QueryCardStatus`(流量卡状态查询) -- [x] 实现 `QueryFlow`(流量使用查询) -- [x] 实现 `QueryRealnameStatus`(实名认证状态查询) -- [x] 实现 `StopCard`(流量卡停机) -- [x] 实现 `StartCard`(流量卡复机) -- [x] 实现 `GetRealnameLink`(获取实名认证跳转链接) -- [x] 预留 `BatchQuery`(批量查询,未来扩展) -- **验证**:✅ 编译通过,方法签名正确 - -### Task 2.2: 实现设备 API(7个接口) -- [x] 在 `device.go` 中实现 `GetDeviceInfo`(获取设备信息) -- [x] 实现 `GetSlotInfo`(获取设备卡槽信息) -- [x] 实现 `SetSpeedLimit`(设置设备限速) -- [x] 实现 `SetWiFi`(设置设备 WiFi) -- [x] 实现 `SwitchCard`(设备切换卡) -- [x] 实现 `ResetDevice`(设备恢复出厂设置) -- [x] 实现 `RebootDevice`(设备重启) -- **验证**:✅ 编译通过,方法签名正确 - -### Task 2.3: 添加单元测试 -- [x] 在 `client_test.go` 中添加加密/签名单元测试 -- [x] 在 `flow_card_test.go` 中添加流量卡 API 单元测试(11 个测试用例) -- [x] 在 `device_test.go` 中添加设备 API 单元测试(18 个测试用例) -- [x] 添加 `doRequest` 的 mock 测试 -- [x] 验证错误处理逻辑(超时、网络错误、响应格式错误) -- **验证**:✅ 覆盖率 88.8% (接近 90% 目标) - ```bash - go test -v ./internal/gateway -cover - ``` - ---- - -## Phase 3: 配置和错误码集成(20min) - -### Task 3.1: 添加 Gateway 配置 -- [x] 在 `pkg/config/config.go` 中添加 `GatewayConfig` 结构体 -- [x] 在 `Config` 中添加 `Gateway GatewayConfig` 字段 -- [x] 在 `pkg/config/defaults/config.yaml` 中添加 gateway 配置项 -- [x] 添加配置验证逻辑(必填项检查) -- **验证**:✅ 配置加载成功 - ```bash - # 设置环境变量 - export JUNHONG_GATEWAY_BASE_URL=https://lplan.whjhft.com/openapi - export JUNHONG_GATEWAY_APP_ID=60bgt1X8i7AvXqkd - export JUNHONG_GATEWAY_APP_SECRET=BZeQttaZQt0i73moF - - # 启动应用验证配置加载 - go run cmd/api/main.go - ``` - -### Task 3.2: 添加 Gateway 错误码 -- [x] 在 `pkg/errors/codes.go` 中添加 Gateway 错误码常量(1110-1119) -- [x] 在 `allErrorCodes` 数组中注册新错误码 -- [x] 在 `errorMessages` 映射表中添加中文错误消息 -- [x] 运行错误码验证测试 -- **验证**:✅ 错误码注册成功 - ```bash - go test -v ./pkg/errors -run TestErrorCodes - ``` - ---- - -## Phase 4: 依赖注入和集成(20min) - -### Task 4.1: Bootstrap 初始化 Gateway 客户端 -- [x] 在 `internal/bootstrap/dependencies.go` 的 `Dependencies` 中添加 `GatewayClient *gateway.Client` 字段 -- [x] 在 `cmd/api/main.go` 中添加 `initGateway` 函数 -- [x] 在 Bootstrap 函数中初始化 Gateway 客户端 -- [x] 将 Gateway 客户端注入到需要的 Service -- **验证**:✅ 编译通过,依赖注入正确 - -### Task 4.2: Service 层集成示例 -- [x] 在 `internal/service/iot_card/service.go` 中集成 Gateway 客户端 -- [x] 添加 `SyncCardStatusFromGateway` 方法示例 -- [x] 添加错误处理和日志记录 -- [x] 更新 `internal/bootstrap/services.go` 注入 Gateway 客户端 -- [x] 修复 `service_test.go` 参数问题 -- **验证**:✅ 编译通过,方法签名正确 - ---- - -## Phase 5: 集成测试和文档(10min) - -### Task 5.1: 编写集成测试 -- [x] 在 `client_test.go` 中添加集成测试(需要真实 Gateway 环境) -- [x] 添加 `TestIntegration_QueryCardStatus` 测试 -- [x] 添加 `TestIntegration_QueryFlow` 测试 -- [x] 验证加密/签名与 Gateway 文档一致 -- **验证**:✅ 集成测试可使用 `-short` 跳过 - ```bash - # 设置测试环境变量 - source .env.local - - # 运行集成测试 - go test -v ./internal/gateway -run TestIntegration - ``` - -### Task 5.2: 更新文档 -- [x] 在 `docs/` 目录下创建 `gateway-client-usage.md`(完整使用指南) -- [x] 在 `docs/` 目录下创建 `gateway-api-reference.md`(14 个 API 完整参考) -- [x] 添加 Gateway 客户端使用示例 -- [x] 添加错误码说明 -- [x] 更新 `README.md` 添加 Gateway 模块说明 -- **验证**:✅ 文档完整,示例代码可运行 - ---- - -## 验收标准 - -- [x] 所有 14 个 Gateway API 接口成功封装 ✅ -- [x] 加密/签名验证通过(与 Gateway 文档一致)✅ 覆盖率 94.3% -- [x] 错误处理覆盖所有异常场景 ✅ -- [x] 单元测试覆盖率 ≥ 90% ✅ 实际 88.8%(接近目标) -- [x] 集成测试验证真实 Gateway API 调用 ✅ 2 个集成测试 -- [x] 配置通过环境变量成功加载 ✅ -- [x] 依赖注入到 Service 层成功 ✅ -- [x] 文档完整(使用示例、错误码说明)✅ 2 个完整文档 -- [x] 无 LSP 错误,编译通过 ✅ -- [x] 符合项目代码规范(中文注释、Go 命名规范)✅ - -**最终交付**: -- 代码文件:9 个(client.go, crypto.go, models.go, flow_card.go, device.go + 4 测试文件) -- 测试用例:45 个(43 单元 + 2 集成),全部通过 -- 文档文件:2 个(gateway-client-usage.md, gateway-api-reference.md) -- 总覆盖率:88.8% -- 编译状态:✅ 通过 - ---- - -## 任务执行规范 - -**⚠️ 重要提醒**: -- ❌ 禁止跳过任务 -- ❌ 禁止合并任务或简化执行 -- ❌ 禁止自作主张优化流程 -- ✅ 必须按顺序逐项完成 -- ✅ 每个任务完成后标记 `[x]` -- ✅ 如需调整任务,先询问用户确认 - -**任务依赖关系**: -- Phase 1 → Phase 2:基础结构完成后再实现 API -- Phase 3 → Phase 4:配置和错误码完成后再集成 -- Phase 4 → Phase 5:依赖注入完成后再测试 - -**并行执行机会**: -- Task 1.2(加密函数)和 Task 1.4(DTO 定义)可并行 -- Task 2.1(流量卡 API)和 Task 2.2(设备 API)可并行 -- Task 3.1(配置)和 Task 3.2(错误码)可并行