# 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` - 请求超时时间 - `logger *zap.Logger` - 日志记录器 - `maxRetries int` - 最大重试次数 #### Scenario: 创建 Gateway 客户端 - **WHEN** 调用 `gateway.NewClient(baseURL, appID, appSecret, logger)` - **THEN** 返回已初始化的 `Client` 实例 - **AND** HTTP 客户端配置正确(支持 Keep-Alive) - **AND** 默认最大重试次数为 2 #### Scenario: 配置超时时间 - **WHEN** 调用 `client.WithTimeout(30 * time.Second)` - **THEN** 客户端的 `timeout` 字段更新为 30 秒 - **AND** 返回客户端自身(支持链式调用) ### Requirement: 统一请求方法 系统 SHALL 提供 `doRequest` 方法,统一处理加密、签名、HTTP 请求和响应解析。请求参数 SHALL 直接接收结构体,内部自动序列化并包装为 `{"params": }` 格式。 #### Scenario: 请求参数自动序列化 - **WHEN** 调用 `doRequest(ctx, "/device/speed-limit", &SpeedLimitReq{CardNo: "xxx", SpeedLimit: 1024})` - **THEN** 请求结构体自动通过 `sonic.Marshal` 序列化 - **AND** 序列化结果嵌入 `{"params": <序列化JSON>}` 中进行加密和签名 #### Scenario: 成功的 API 调用 - **WHEN** 调用 `doRequest(ctx, "/flow-card/status", req)` - **THEN** 业务数据使用 AES-128-ECB 加密 - **AND** 请求使用 MD5 签名 - **AND** HTTP POST 发送到 `{baseURL}/flow-card/status` - **AND** 响应中的 `data` 字段返回为 `json.RawMessage` #### Scenario: 网络错误 - **WHEN** HTTP 请求失败(网络中断、DNS 解析失败) - **THEN** 返回 `CodeGatewayError` 错误 - **AND** 错误信息包含原始网络错误 #### Scenario: 请求超时 - **WHEN** HTTP 请求超过配置的超时时间 - **THEN** 返回 `CodeGatewayTimeout` 错误 - **AND** Context 超时错误被正确识别 #### Scenario: 响应格式错误 - **WHEN** Gateway 响应无法解析为 JSON - **THEN** 返回 `CodeGatewayInvalidResp` 错误 - **AND** 错误信息包含原始响应内容 #### Scenario: Gateway 业务错误 - **WHEN** Gateway 响应中 `code != 200` - **THEN** 返回 `CodeGatewayError` 错误 - **AND** 错误信息包含 Gateway 的 code 和 msg ### Requirement: 泛型响应解析方法 系统 SHALL 提供 `doRequestWithResponse[T any]` 泛型方法,自动完成请求发送和响应反序列化。 #### Scenario: 自动反序列化响应 - **WHEN** 调用 `doRequestWithResponse[CardStatusResp](ctx, "/flow-card/status", req)` - **THEN** 返回 `*CardStatusResp` 类型的结构体 - **AND** 内部调用 `doRequest` 获取 `json.RawMessage` 后自动 unmarshal #### Scenario: 反序列化失败 - **WHEN** Gateway 返回的 JSON 无法匹配目标结构体 - **THEN** 返回 `CodeGatewayInvalidResp` 错误 - **AND** 错误信息为 "解析 Gateway 响应失败" ### Requirement: 请求结构体直接序列化 系统 SHALL 消除手动 `map[string]interface{}` 构建,所有业务方法直接将请求结构体传递给 `doRequest` 或 `doRequestWithResponse`。 #### Scenario: 设备限速请求 - **WHEN** 调用 `SetSpeedLimit(ctx, &SpeedLimitReq{CardNo: "xxx", SpeedLimit: 1024})` - **THEN** `SpeedLimitReq` 结构体直接序列化为 JSON - **AND** 不再手动构建 `map[string]interface{}` #### Scenario: 流量卡停机请求 - **WHEN** 调用 `StopCard(ctx, &CardOperationReq{CardNo: "xxx", Extend: "ext"})` - **THEN** `CardOperationReq` 结构体直接序列化 - **AND** `Extend` 字段通过 `json:"extend,omitempty"` 标签在为空时自动省略 ### 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 错误