docs: 新增 Gateway 集成和微信公众号支付集成的 OpenSpec 规划文档
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 43s

This commit is contained in:
2026-01-30 16:09:32 +08:00
parent 1cf17e8f14
commit 4856a88d41
14 changed files with 4079 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
# 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 错误

View File

@@ -0,0 +1,175 @@
# 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`

View File

@@ -0,0 +1,155 @@
# 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&timestamp=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 期望的编码一致