diff --git a/.sisyphus/drafts/gateway-integration-plan.md b/.sisyphus/drafts/gateway-integration-plan.md new file mode 100644 index 0000000..5498c81 --- /dev/null +++ b/.sisyphus/drafts/gateway-integration-plan.md @@ -0,0 +1,118 @@ +# Draft: Gateway Integration 工作计划 + +## 用户需求确认 + +**目标**: 封装 Gateway API 为统一的能力模块,提供类型安全的接口、统一的错误处理和配置管理。 + +**核心交付物**: +- Gateway 客户端封装(`internal/gateway/` 包) +- 14 个 API 接口(流量卡 7 个 + 设备 7 个) +- AES-128-ECB 加密 + MD5 签名机制 +- 配置集成(环境变量) +- 错误码定义(1110-1119) +- 依赖注入(Bootstrap) +- 完整测试覆盖 + +**总任务数**: 51 个实施任务 + 10 个验收标准 + +## 初步任务分组 + +### Phase 1: 基础结构搭建(13 个任务) +- 目录结构创建 +- 加密/签名工具实现 +- 客户端基础结构 +- DTO 定义 + +### Phase 2: API 接口封装(17 个任务) +- 流量卡 API(7 个接口) +- 设备 API(7 个接口) +- 单元测试 + +### Phase 3: 配置和错误码集成(7 个任务) +- Gateway 配置 +- Gateway 错误码 + +### Phase 4: 依赖注入和集成(6 个任务) +- Bootstrap 初始化 +- Service 层集成 + +### Phase 5: 集成测试和文档(8 个任务) +- 集成测试 +- 文档更新 + +## 用户决策(已确认) + +### 1. Gateway 测试环境配置 ✅ +- **BaseURL**: `https://lplan.whjhft.com/openapi` +- **AppID**: `60bgt1X8i7AvXqkd` +- **AppSecret**: `BZeQttaZQt0i73moF` +- **测试 ICCID**: `8986062580006141710` +- **设备测试**: 不需要测试 + +### 2. 加密/签名算法验证 ✅ +- **文档来源**: Apifox 文档(https://omp5mq28pq.apifox.cn/7819761m0) +- **加密方案**: AES-128-ECB + PKCS5Padding + Base64(密钥为 AppSecret 的 MD5) +- **签名方案**: MD5(排序参数&key=AppSecret),大写输出 +- **安全警告**: ⚠️ 遗留系统,添加安全警告注释(ECB 模式泄漏 + MD5 碰撞风险) + +### 3. Service 层集成范围 ✅ +- **集成位置**: `internal/service/iot_card/service.go`(已存在) +- **集成方法**: 新增 `SyncCardStatus(ctx, iccid)` 方法作为示例 +- **范围**: 仅提供一个集成示例 + +### 4. 批量查询接口 ✅ +- **决定**: ❌ 完全不实施(连预留接口都不需要) +- **理由**: 用户明确表示"根本就不会有批量查询接口" + +### 5. 错误处理策略 ✅ +- **重试逻辑**: ✅ 需要自动重试(最多 3 次,指数退避:1s, 2s, 4s) +- **降级策略**: ❌ 不需要 +- **超时处理**: 超时错误不重试,直接返回 +- **实现方式**: 在 `doRequest` 内置简单循环重试(无需第三方库) + +## 研究发现 + +### 项目现有架构 +- **分层结构**: Handler → Service → Store → Model(严格分离) +- **依赖注入**: Bootstrap 按顺序初始化(Stores → Services → Handlers) +- **配置管理**: Viper 加载,支持环境变量覆盖(前缀 `JUNHONG_`) +- **错误处理**: 统一错误码系统(1000-1999: 4xx, 2000-2999: 5xx) +- **测试模式**: Table-driven tests + testutils(全局单例 DB/Redis) +- **HTTP 客户端参考**: `/pkg/sms/` 使用接口 + 依赖注入模式 + +### 加密/签名安全警告 +- **AES-128-ECB**: ⚠️ 密码学上已被破解(模式泄漏),仅用于遗留系统 +- **MD5 签名**: ⚠️ 存在碰撞攻击漏洞,建议使用 HMAC-SHA256 +- **理由**: 如果 Gateway API 是遗留系统且无法更改,则必须使用 ECB + MD5 + +### 最佳实践建议 +1. **HTTP 客户端**: 使用 `http.Transport` 精细配置超时(Dial、TLS、ResponseHeader) +2. **API 客户端**: 导出接口而非具体类型(可测试性) +3. **集成测试**: 使用真实 Gateway 环境 + 测试 ICCID +4. **错误处理**: 区分超时错误 vs 其他网络错误 +5. **重试策略**: 内置简单循环重试(无需第三方库) + +### 现有代码结构 +- **iot_card Service**: 已存在于 `internal/service/iot_card/service.go` + - 当前方法: ListStandalone, GetByICCID, AllocateCards, RecallCards, BatchSetSeriesBinding + - 新增方法: SyncCardStatus(Gateway 集成示例) +- **HTTP 客户端参考**: `pkg/sms/http_client.go`(无重试逻辑,使用 context 超时) +- **重试模式**: 项目使用 Asynq 任务队列重试(常量:`DefaultRetryMax = 5`) + +## 任务调整 + +### 删除任务 +- **Task 20**: "预留 BatchQuery(批量查询,未来扩展)" → ❌ 删除 + +### 新增任务 +- **Task 1.5**: "在 doRequest 中实现 HTTP 重试逻辑(3 次,指数退避)" +- **Task 4.3**: "在 iot_card Service 中新增 SyncCardStatus 方法" + +### 修改任务 +- **Task 5.1**: 使用真实配置进行集成测试(测试 ICCID: `8986062580006141710`) +- **Task 2.1**: 移除 BatchQuery 实现 +- **Task 2.2**: 设备 API 只实现方法签名(测试时跳过真实调用) + +### 调整后任务数 +- **实施任务**: 51 个(原 51 - 1 个删除 + 1 个新增 = 51) +- **验收标准**: 10 个(保留但不作为实施任务) diff --git a/.sisyphus/plans/gateway-integration.md b/.sisyphus/plans/gateway-integration.md new file mode 100644 index 0000000..4c33ea4 --- /dev/null +++ b/.sisyphus/plans/gateway-integration.md @@ -0,0 +1,1457 @@ +# Gateway API 集成工作计划 + +## TL;DR + +> **Quick Summary**: 封装 Gateway API 为统一的能力模块,提供类型安全的接口、统一的错误处理、配置管理和自动重试机制 +> +> **Deliverables**: +> - Gateway 客户端封装(`internal/gateway/` 包) +> - 14 个 API 接口(流量卡 7 个 + 设备 7 个) +> - AES-128-ECB 加密 + MD5 签名机制 +> - 配置集成(GatewayConfig) +> - 错误码定义(1110-1119) +> - 依赖注入(Bootstrap) +> - 完整测试覆盖(单元测试 + 集成测试) +> +> **Estimated Effort**: Medium(预计 2-3 小时) +> **Parallel Execution**: YES - 5 waves +> **Critical Path**: Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5 + +--- + +## Context + +### Original Request +用户需要实施 gateway-integration 提案,该提案包含 61 个任务,分为 5 个 Phase。核心目标是封装 Gateway API 为统一的能力模块,提供类型安全的接口、统一的错误处理和配置管理。 + +### Interview Summary +**Key Discussions**: +- **Gateway 测试环境配置**: 已提供 BaseURL、AppID、AppSecret、测试 ICCID +- **加密/签名算法**: 基于 Apifox 文档(AES-128-ECB + MD5,遗留系统) +- **Service 集成范围**: 仅 iot_card Service,新增 SyncCardStatus 方法作为示例 +- **批量查询接口**: 完全不需要(删除 Task 20) +- **错误处理策略**: 需要自动重试(3 次,指数退避:1s, 2s, 4s) + +**Research Findings**: +- **项目现有模式**: SMS 客户端(pkg/sms/)可作为参考(接口 + 依赖注入) +- **测试模式**: testutils(全局单例 DB/Redis + 事务回滚) +- **错误处理**: 统一错误码系统(pkg/errors/) +- **配置管理**: Viper 加载,支持环境变量覆盖(JUNHONG_ 前缀) +- **安全警告**: ⚠️ AES-128-ECB 和 MD5 存在密码学漏洞(遗留系统,需添加注释警告) + +### 自我审查 +**识别的潜在风险**: +1. **加密算法实现风险**: AES-128-ECB + PKCS5Padding 实现容易出错 + - **缓解措施**: 使用 Apifox 文档中的 Go 示例代码,添加单元测试验证加密/解密 +2. **签名计算风险**: 参数排序和拼接逻辑可能与 Gateway 不一致 + - **缓解措施**: 严格按照 Apifox 文档实现,集成测试验证真实签名 +3. **重试逻辑风险**: 不当重试可能导致重复操作(如停机/复机) + - **缓解措施**: 只重试网络错误和 5xx 错误,业务错误(4xx)不重试 +4. **测试覆盖风险**: 设备 API 无法真实测试 + - **缓解措施**: 设备 API 只实现方法签名,集成测试跳过 + +**明确的边界和约束**: +- ✅ **必须包含**: 流量卡 7 个接口完整实现和测试 +- ❌ **必须排除**: 批量查询接口(BatchQuery) +- ⚠️ **可选实现**: 设备 API 只需方法签名(测试跳过) +- 🔒 **不可变更**: 加密/签名算法(遗留系统约束) + +--- + +## Work Objectives + +### Core Objective +封装 Gateway API 为统一的能力模块,提供类型安全的接口、统一的错误处理、配置管理和自动重试机制,支持流量卡和设备管理的 14 个 API 接口。 + +### Concrete Deliverables +- `internal/gateway/client.go` - Gateway 客户端核心实现 +- `internal/gateway/crypto.go` - AES-128-ECB 加密 + MD5 签名工具 +- `internal/gateway/models.go` - 请求/响应 DTO 定义 +- `internal/gateway/flow_card.go` - 流量卡 API(7 个接口) +- `internal/gateway/device.go` - 设备 API(7 个接口) +- `internal/gateway/client_test.go` - 单元测试 + 集成测试 +- `pkg/config/config.go` - GatewayConfig 配置结构 +- `pkg/errors/codes.go` - Gateway 错误码(1110-1119) +- `internal/bootstrap/bootstrap.go` - Bootstrap 初始化 Gateway 客户端 +- `internal/service/iot_card/service.go` - SyncCardStatus 集成示例 +- `docs/gateway-client-usage.md` - 使用文档 + +### Definition of Done +- [ ] 所有 14 个 Gateway API 接口成功封装(流量卡 7 个 + 设备 7 个) +- [ ] 加密/签名通过单元测试验证(与 Apifox 文档一致) +- [ ] 集成测试验证真实 Gateway API 调用(测试 ICCID: `8986062580006141710`) +- [ ] 重试逻辑测试通过(3 次重试,指数退避) +- [ ] 配置通过环境变量成功加载(JUNHONG_GATEWAY_*) +- [ ] 依赖注入到 iot_card Service 成功(SyncCardStatus 方法) +- [ ] 单元测试覆盖率 ≥ 90%(核心逻辑) +- [ ] 无 LSP 错误,编译通过 +- [ ] 符合项目代码规范(中文注释、Go 命名规范) +- [ ] 文档完整(使用示例、错误码说明) + +### Must Have +- AES-128-ECB 加密(PKCS5Padding + Base64) +- MD5 签名(大写输出) +- 自动重试逻辑(3 次,指数退避:1s, 2s, 4s) +- 流量卡 7 个 API 完整实现 +- 真实 Gateway 环境集成测试 +- 错误码定义(1110-1119) +- 配置环境变量支持 + +### Must NOT Have (Guardrails) +- ❌ 批量查询接口(BatchQuery)- 用户明确不需要 +- ❌ 设备 API 的真实测试 - 无测试环境,只需方法签名 +- ❌ 使用第三方重试库 - 项目无依赖,使用简单循环 +- ❌ 降级策略 - 用户不需要,失败直接返回错误 +- ❌ 使用 AES-GCM 或 HMAC-SHA256 - 遗留系统约束,必须使用 ECB + MD5 +- ❌ AI-Slop 模式: + - 过度抽象(不需要工厂模式、策略模式) + - 过度验证(不需要 15 个参数校验) + - 过度文档(不需要 JSDoc everywhere) + +--- + +## Verification Strategy + +### Test Decision +- **Infrastructure exists**: YES(项目已有 testutils) +- **User wants tests**: TDD(测试驱动开发) +- **Framework**: Go 标准库(testing + httptest) + +### TDD Workflow + +每个 TODO 遵循 **RED-GREEN-REFACTOR**: + +**Task Structure:** +1. **RED**: 编写失败测试 + - 测试文件: `internal/gateway/*_test.go` + - 测试命令: `go test -v ./internal/gateway/...` + - 预期: FAIL(测试存在,实现不存在) +2. **GREEN**: 最小实现使测试通过 + - 实现代码: `internal/gateway/*.go` + - 测试命令: `go test -v ./internal/gateway/...` + - 预期: PASS +3. **REFACTOR**: 优化代码保持绿色 + - 重构: 提取常量、优化逻辑、添加注释 + - 测试命令: `go test -v ./internal/gateway/...` + - 预期: PASS(仍然通过) + +### 集成测试环境 + +**真实 Gateway 配置**: +```bash +export JUNHONG_GATEWAY_BASEURL="https://lplan.whjhft.com/openapi" +export JUNHONG_GATEWAY_APPID="60bgt1X8i7AvXqkd" +export JUNHONG_GATEWAY_APPSECRET="BZeQttaZQt0i73moF" +``` + +**测试数据**: +- 测试 ICCID: `8986062580006141710` +- 设备测试: 跳过(无测试环境) + +**集成测试验证**: +```bash +# 运行集成测试(需先加载环境变量) +source .env.local && go test -v ./internal/gateway/... -run TestIntegration +``` + +--- + +## Execution Strategy + +### Parallel Execution Waves + +``` +Wave 1 (Start Immediately - 基础结构): +├── Task 1.1: 创建 Gateway 包目录结构 +└── Task 3.1: 添加 Gateway 配置结构 + +Wave 2 (After Wave 1 - 加密和模型): +├── Task 1.2: 实现加密/签名工具函数(crypto.go) +├── Task 1.3: 定义 DTO 结构(models.go) +└── Task 3.2: 添加 Gateway 错误码 + +Wave 3 (After Wave 2 - 客户端核心): +├── Task 1.4: 实现 Gateway 客户端基础结构(client.go) +└── Task 1.5: 实现 HTTP 重试逻辑(doRequest 方法) + +Wave 4 (After Wave 3 - API 接口实现): +├── Task 2.1: 实现流量卡 API(flow_card.go,7 个接口) +└── Task 2.2: 实现设备 API(device.go,7 个接口签名) + +Wave 5 (After Wave 4 - 集成和测试): +├── Task 2.3: 添加单元测试(client_test.go) +├── Task 4.1: Bootstrap 初始化 Gateway 客户端 +├── Task 4.2: Service 层集成示例(SyncCardStatus) +├── Task 5.1: 编写集成测试 +└── Task 5.2: 更新文档 + +Critical Path: Wave 1 → Wave 2 → Wave 3 → Wave 4 → Wave 5 +Parallel Speedup: ~30% faster than sequential +``` + +### Dependency Matrix + +| Task | Depends On | Blocks | Can Parallelize With | +|------|------------|--------|---------------------| +| 1.1 | None | All | 3.1 | +| 1.2 | 1.1 | 1.4, 2.1, 2.2 | 1.3, 3.2 | +| 1.3 | 1.1 | 2.1, 2.2 | 1.2, 3.2 | +| 1.4 | 1.1, 1.2, 1.3 | 1.5, 2.1, 2.2 | None | +| 1.5 | 1.4 | 2.1, 2.2 | None | +| 2.1 | 1.4, 1.5, 1.2, 1.3 | 2.3, 5.1 | 2.2 | +| 2.2 | 1.4, 1.5, 1.2, 1.3 | 2.3, 5.1 | 2.1 | +| 2.3 | 2.1, 2.2 | None | 4.1, 4.2 | +| 3.1 | None | 4.1 | 1.1 | +| 3.2 | 1.1 | 4.1 | 1.2, 1.3 | +| 4.1 | 1.4, 3.1, 3.2 | 4.2 | 2.3 | +| 4.2 | 4.1 | None | 2.3, 5.1 | +| 5.1 | 2.1, 2.2, 4.1 | None | 2.3, 4.2, 5.2 | +| 5.2 | All | None | 5.1 | + +--- + +## TODOs + +### Phase 1: 基础结构搭建 + +- [ ] **1.1 创建 Gateway 包目录结构** + + **What to do**: + - 创建目录 `internal/gateway/` + - 创建占位文件: + - `client.go` - Gateway 客户端核心实现 + - `crypto.go` - 加密/签名工具 + - `models.go` - 请求/响应 DTO + - `flow_card.go` - 流量卡 API + - `device.go` - 设备 API + - `client_test.go` - 测试文件 + - 每个文件添加 package 声明和中文注释说明用途 + + **Must NOT do**: + - 不添加任何实现代码(只创建文件结构) + - 不创建 batch_query.go(批量查询不需要) + + **Recommended Agent Profile**: + - **Category**: `quick` + - Reason: 简单的目录和文件创建任务,无复杂逻辑 + - **Skills**: 无 + - Reason: 不涉及特定规范,只是文件结构创建 + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1(与 Task 3.1 并行) + - **Blocks**: 所有后续任务(目录结构是基础) + - **Blocked By**: None(可立即开始) + + **References**: + - `pkg/sms/client.go` - 参考 SMS 客户端的包结构模式 + - `pkg/sms/http_client.go` - 参考 HTTP 客户端文件组织方式 + - `internal/service/iot_card/service.go` - 参考 Service 包的文件组织 + + **Acceptance Criteria**: + - [ ] 目录存在: `internal/gateway/` + - [ ] 文件存在: `client.go`, `crypto.go`, `models.go`, `flow_card.go`, `device.go`, `client_test.go` + - [ ] 每个文件包含 `package gateway` 声明 + - [ ] 每个文件顶部有中文注释说明用途 + - [ ] 验证命令: `ls -la internal/gateway/` → 显示所有文件 + - [ ] 编译通过: `go build ./internal/gateway/...` → 无错误 + + **Commit**: YES + - Message: `feat(gateway): 创建 Gateway 包目录结构` + - Files: `internal/gateway/*.go` + - Pre-commit: `go build ./internal/gateway/...` + +--- + +- [ ] **1.2 实现加密/签名工具函数** + + **What to do**: + - 在 `internal/gateway/crypto.go` 中实现加密函数: + - `aesEncrypt(plaintext, appSecret string) (string, error)` - AES-128-ECB + PKCS5Padding + Base64 + - 步骤: MD5(appSecret) → 16字节密钥 → PKCS5填充 → ECB加密 → Base64编码 + - 实现签名函数: + - `generateSign(params map[string]interface{}, appSecret string) string` - MD5签名(大写) + - 步骤: 排序参数键 → 拼接 key=value&... → 追加 &key=appSecret → MD5 → 转大写 + - 实现解密函数(用于测试验证): + - `aesDecrypt(ciphertext, appSecret string) (string, error)` - 解密验证 + - 添加详细中文注释和安全警告: + ```go + // ⚠️ 安全警告: AES-128-ECB 模式已被证明存在密码学漏洞(相同明文产生相同密文,泄漏模式) + // 仅用于对接遗留系统的 Gateway API,不应用于新系统设计 + // 推荐替代方案: AES-256-GCM(提供认证加密) + ``` + + **Must NOT do**: + - 不使用第三方加密库(使用标准库 crypto/aes) + - 不实现 AES-GCM 或其他模式(遗留系统约束) + - 不跳过安全警告注释 + + **Recommended Agent Profile**: + - **Category**: `ultrabrain` + - Reason: 加密算法实现需要精确逻辑,容易出错 + - **Skills**: 无 + - Reason: 加密实现不涉及项目特定规范 + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 2(与 Task 1.3, 3.2 并行) + - **Blocks**: Task 1.4, 2.1, 2.2(客户端和 API 依赖加密) + - **Blocked By**: Task 1.1(需要文件结构存在) + + **References**: + - **Apifox 文档**: `https://omp5mq28pq.apifox.cn/7819761m0` - 官方 Go 语言加密/签名示例 + - **Librarian 研究**: AES-128-ECB 实现模式(手动 ECB 循环,因为标准库无 ECB) + - `crypto/aes` - AES 加密标准库 + - `crypto/md5` - MD5 哈希标准库 + - `encoding/base64` - Base64 编码标准库 + + **Acceptance Criteria**: + + **TDD - RED Phase**: + - [ ] 测试文件创建: `internal/gateway/crypto_test.go` + - [ ] 编写失败测试: + ```go + func TestAESEncrypt(t *testing.T) { + plaintext := "test" + appSecret := "BZeQttaZQt0i73moF" + encrypted, err := aesEncrypt(plaintext, appSecret) + require.NoError(t, err) + assert.NotEmpty(t, encrypted) + + // 验证可解密 + decrypted, err := aesDecrypt(encrypted, appSecret) + require.NoError(t, err) + assert.Equal(t, plaintext, decrypted) + } + + func TestGenerateSign(t *testing.T) { + params := map[string]interface{}{ + "appId": "60bgt1X8i7AvXqkd", + "timestamp": 1704067200, + "data": "encrypted_data", + } + appSecret := "BZeQttaZQt0i73moF" + sign := generateSign(params, appSecret) + assert.NotEmpty(t, sign) + assert.Regexp(t, "^[A-F0-9]{32}$", sign) // 32位大写MD5 + } + ``` + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestAES` → FAIL(未实现) + + **TDD - GREEN Phase**: + - [ ] 实现 `aesEncrypt` 函数(参考 Apifox 文档示例) + - [ ] 实现 `aesDecrypt` 函数(用于测试验证) + - [ ] 实现 `generateSign` 函数(参考 Apifox 文档示例) + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestAES` → PASS + + **TDD - REFACTOR Phase**: + - [ ] 添加详细中文注释和安全警告 + - [ ] 提取 PKCS5 填充逻辑为独立函数 + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestAES` → PASS(仍然通过) + + **Commit**: YES + - Message: `feat(gateway): 实现 AES-128-ECB 加密和 MD5 签名工具` + - Files: `internal/gateway/crypto.go`, `internal/gateway/crypto_test.go` + - Pre-commit: `go test -v ./internal/gateway/ -run TestAES` + +--- + +- [ ] **1.3 定义请求/响应 DTO 结构** + + **What to do**: + - 在 `internal/gateway/models.go` 中定义: + - **通用请求/响应结构**: + ```go + // GatewayRequest Gateway API 统一请求结构 + type GatewayRequest struct { + AppID string `json:"appId"` // 应用ID + Data string `json:"data"` // AES加密后的Base64字符串 + Sign string `json:"sign"` // MD5签名(大写) + Timestamp int64 `json:"timestamp"` // 时间戳(秒) + } + + // GatewayResponse Gateway API 统一响应结构 + type GatewayResponse struct { + Code int `json:"code"` // 响应码(200=成功) + Msg string `json:"msg"` // 响应消息 + Data interface{} `json:"data"` // 业务数据(需解密) + } + ``` + - **流量卡相关 DTO**(7 个接口): + - `CardStatusReq` - 卡状态查询请求 + - `CardStatusResp` - 卡状态查询响应 + - `FlowQueryReq` - 流量查询请求 + - `FlowUsageResp` - 流量使用响应 + - `RealnameStatusReq` - 实名认证状态请求 + - `RealnameStatusResp` - 实名认证状态响应 + - `CardOperationReq` - 停机/复机请求(通用) + - `RealmnameLinkResp` - 实名认证链接响应 + - **设备相关 DTO**(7 个接口): + - `DeviceInfoReq` - 设备信息请求 + - `DeviceInfoResp` - 设备信息响应 + - `SlotInfoReq` - 卡槽信息请求 + - `SlotInfoResp` - 卡槽信息响应 + - `SpeedLimitReq` - 限速设置请求 + - `WiFiConfigReq` - WiFi 配置请求 + - `CardSwitchReq` - 切换卡请求 + - `DeviceOperationReq` - 设备操作请求(重启/恢复出厂) + - 每个结构体添加: + - JSON 标签(使用 sonic 序列化) + - 中文注释(description) + - 验证标签(validate,如 `required`) + + **Must NOT do**: + - 不定义 BatchQuery 相关 DTO(不需要) + - 不过度抽象(不需要接口或泛型) + - 不添加 Getter/Setter(Go 惯用法直接访问字段) + + **Recommended Agent Profile**: + - **Category**: `quick` + - Reason: DTO 定义是结构化任务,无复杂逻辑 + - **Skills**: [`dto-standards`] + - `dto-standards`: DTO 规范(description 标签、枚举字段、验证标签) + - Reason: 定义 DTO 结构需要遵循项目规范 + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 2(与 Task 1.2, 3.2 并行) + - **Blocks**: Task 2.1, 2.2(API 实现依赖 DTO) + - **Blocked By**: Task 1.1(需要文件结构存在) + + **References**: + - **Apifox 文档**: `https://omp5mq28pq.apifox.cn/7819761m0` - 完整的请求/响应结构定义 + - `internal/model/dto/` - 参考现有 DTO 定义模式 + - `docs/dto-standards.md` - DTO 规范(description 标签、验证标签) + - **Validator 库**: `github.com/go-playground/validator/v10` - 验证标签语法 + + **Acceptance Criteria**: + + **TDD - RED Phase**: + - [ ] 测试文件创建: `internal/gateway/models_test.go` + - [ ] 编写失败测试: + ```go + func TestGatewayRequestJSON(t *testing.T) { + req := GatewayRequest{ + AppID: "test", + Data: "encrypted", + Sign: "ABC123", + Timestamp: 1704067200, + } + data, err := json.Marshal(req) + require.NoError(t, err) + assert.Contains(t, string(data), "appId") + } + ``` + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestGatewayRequest` → FAIL + + **TDD - GREEN Phase**: + - [ ] 定义所有 DTO 结构体(参考 Apifox 文档) + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestGatewayRequest` → PASS + + **TDD - REFACTOR Phase**: + - [ ] 添加中文注释(每个字段说明用途) + - [ ] 添加验证标签(required, min, max 等) + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestGatewayRequest` → PASS + + **Commit**: YES + - Message: `feat(gateway): 定义 Gateway API 请求/响应 DTO 结构` + - Files: `internal/gateway/models.go`, `internal/gateway/models_test.go` + - Pre-commit: `go test -v ./internal/gateway/ -run TestGatewayRequest` + +--- + +- [ ] **1.4 实现 Gateway 客户端基础结构** + + **What to do**: + - 在 `internal/gateway/client.go` 中定义客户端接口和结构: + ```go + // Client Gateway API 客户端接口(用于 mock 测试) + type Client interface { + // 流量卡相关接口 + QueryCardStatus(ctx context.Context, iccid string) (*CardStatusResp, error) + QueryFlow(ctx context.Context, iccid string) (*FlowUsageResp, error) + // ... 其他接口声明 + } + + // GatewayClient Gateway API 客户端实现 + type GatewayClient struct { + baseURL string // Gateway API 地址 + appID string // 应用ID + appSecret string // 应用密钥 + httpClient *http.Client // HTTP 客户端 + logger *zap.Logger // 日志记录器 + } + ``` + - 实现构造函数: + ```go + func NewClient(baseURL, appID, appSecret string, logger *zap.Logger) *GatewayClient + ``` + - 实现配置方法: + ```go + func (c *GatewayClient) WithTimeout(timeout time.Duration) *GatewayClient + ``` + - **不实现 doRequest 方法**(留给 Task 1.5) + + **Must NOT do**: + - 不实现具体的 API 方法(留给 Task 2.1, 2.2) + - 不实现重试逻辑(留给 Task 1.5) + - 不使用 Getter/Setter 模式(Go 惯用法) + + **Recommended Agent Profile**: + - **Category**: `ultrabrain` + - Reason: 客户端架构设计需要考虑接口设计、依赖注入 + - **Skills**: 无 + - Reason: 不涉及项目特定规范,是通用的客户端设计 + + **Parallelization**: + - **Can Run In Parallel**: NO + - **Parallel Group**: Wave 3(顺序执行) + - **Blocks**: Task 1.5, 2.1, 2.2(API 实现依赖客户端结构) + - **Blocked By**: Task 1.1, 1.2, 1.3(需要目录、加密、DTO) + + **References**: + - `pkg/sms/client.go` - 参考 SMS 客户端的接口 + 结构体模式 + - `pkg/sms/http_client.go` - 参考 HTTP 客户端的依赖注入模式 + - **Librarian 研究**: API 客户端设计模式(接口 + 依赖注入) + + **Acceptance Criteria**: + + **TDD - RED Phase**: + - [ ] 测试文件更新: `internal/gateway/client_test.go` + - [ ] 编写失败测试: + ```go + func TestNewClient(t *testing.T) { + client := NewClient("https://api.example.com", "appid", "secret", zap.NewNop()) + assert.NotNil(t, client) + assert.Equal(t, "https://api.example.com", client.baseURL) + } + + func TestWithTimeout(t *testing.T) { + client := NewClient("https://api.example.com", "appid", "secret", zap.NewNop()) + client = client.WithTimeout(5 * time.Second) + assert.Equal(t, 5*time.Second, client.httpClient.Timeout) + } + ``` + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestNewClient` → FAIL + + **TDD - GREEN Phase**: + - [ ] 定义 Client 接口(所有方法签名) + - [ ] 定义 GatewayClient 结构体 + - [ ] 实现 NewClient 构造函数 + - [ ] 实现 WithTimeout 方法 + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestNewClient` → PASS + + **TDD - REFACTOR Phase**: + - [ ] 添加详细中文注释 + - [ ] 配置默认 HTTP 客户端(参考 librarian 研究的超时配置) + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestNewClient` → PASS + + **Commit**: YES + - Message: `feat(gateway): 实现 Gateway 客户端基础结构` + - Files: `internal/gateway/client.go`, `internal/gateway/client_test.go` + - Pre-commit: `go test -v ./internal/gateway/ -run TestNewClient` + +--- + +- [ ] **1.5 实现 HTTP 重试逻辑(doRequest 方法)** + + **What to do**: + - 在 `internal/gateway/client.go` 中实现统一请求方法: + ```go + func (c *GatewayClient) doRequest(ctx context.Context, endpoint string, reqData interface{}) (*GatewayResponse, error) + ``` + - 请求流程: + 1. 序列化请求数据(sonic) + 2. AES 加密请求数据 + 3. 构建 GatewayRequest(appId, data, timestamp, sign) + 4. **重试循环**(最多 3 次): + - 发送 HTTP POST 请求 + - 解析响应 + - **网络错误或 5xx → 重试**(指数退避:1s, 2s, 4s) + - **4xx 业务错误 → 不重试**(直接返回错误) + - **超时错误 → 不重试**(context.DeadlineExceeded) + 5. AES 解密响应数据 + 6. 返回 GatewayResponse + - 错误处理: + - 网络错误: 返回 `errors.Wrap(errors.CodeGatewayNetworkError, err)` + - 超时错误: 返回 `errors.New(errors.CodeGatewayTimeout)` + - 业务错误: 返回 `errors.New(errors.CodeGatewayBusinessError, resp.Msg)` + + **Must NOT do**: + - 不使用第三方重试库(项目无依赖) + - 不重试业务错误(4xx) + - 不重试超时错误(避免雪崩) + + **Recommended Agent Profile**: + - **Category**: `ultrabrain` + - Reason: 重试逻辑复杂,需要精确的错误分类和退避算法 + - **Skills**: 无 + - Reason: HTTP 重试是通用逻辑,不涉及项目特定规范 + + **Parallelization**: + - **Can Run In Parallel**: NO + - **Parallel Group**: Wave 3(顺序执行,依赖 Task 1.4) + - **Blocks**: Task 2.1, 2.2(API 实现依赖 doRequest) + - **Blocked By**: Task 1.4(需要客户端结构存在) + + **References**: + - **Librarian 研究**: HTTP 重试最佳实践(指数退避、错误分类) + - `pkg/sms/http_client.go` - 参考 HTTP 请求模式(无重试,需自行实现) + - `pkg/constants/constants.go` - 参考 DefaultRetryMax(Asynq 任务重试次数 5) + - **Sonic 库**: `github.com/bytedance/sonic` - JSON 序列化 + + **Acceptance Criteria**: + + **TDD - RED Phase**: + - [ ] 测试文件更新: `internal/gateway/client_test.go` + - [ ] 编写失败测试: + ```go + func TestDoRequest_Success(t *testing.T) { + // 使用 httptest.Server 模拟成功响应 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(GatewayResponse{Code: 200, Msg: "success"}) + })) + defer server.Close() + + client := NewClient(server.URL, "appid", "secret", zap.NewNop()) + resp, err := client.doRequest(context.Background(), "/test", map[string]string{"iccid": "test"}) + require.NoError(t, err) + assert.Equal(t, 200, resp.Code) + } + + func TestDoRequest_Retry(t *testing.T) { + // 模拟前 2 次失败,第 3 次成功 + attempts := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + attempts++ + if attempts < 3 { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(GatewayResponse{Code: 200, Msg: "success"}) + })) + defer server.Close() + + client := NewClient(server.URL, "appid", "secret", zap.NewNop()) + resp, err := client.doRequest(context.Background(), "/test", map[string]string{"iccid": "test"}) + require.NoError(t, err) + assert.Equal(t, 200, resp.Code) + assert.Equal(t, 3, attempts) // 验证重试了 2 次 + } + + func TestDoRequest_NoRetryOn4xx(t *testing.T) { + // 4xx 错误不重试 + attempts := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + attempts++ + w.WriteHeader(http.StatusBadRequest) + })) + defer server.Close() + + client := NewClient(server.URL, "appid", "secret", zap.NewNop()) + _, err := client.doRequest(context.Background(), "/test", map[string]string{"iccid": "test"}) + require.Error(t, err) + assert.Equal(t, 1, attempts) // 验证没有重试 + } + ``` + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestDoRequest` → FAIL + + **TDD - GREEN Phase**: + - [ ] 实现 doRequest 方法(加密、重试、解密) + - [ ] 实现重试逻辑(指数退避:1s, 2s, 4s) + - [ ] 实现错误分类(网络/超时/业务) + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestDoRequest` → PASS + + **TDD - REFACTOR Phase**: + - [ ] 提取重试配置为常量(maxRetries, baseDelay) + - [ ] 添加详细日志(每次重试记录日志) + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestDoRequest` → PASS + + **Commit**: YES + - Message: `feat(gateway): 实现 HTTP 重试逻辑(3 次重试,指数退避)` + - Files: `internal/gateway/client.go`, `internal/gateway/client_test.go` + - Pre-commit: `go test -v ./internal/gateway/ -run TestDoRequest` + +--- + +### Phase 2: API 接口封装 + +- [ ] **2.1 实现流量卡 API(7 个接口)** + + **What to do**: + - 在 `internal/gateway/flow_card.go` 中实现 7 个流量卡 API: + 1. **QueryCardStatus** - 查询流量卡状态(在线/离线) + - 参数: iccid + - 返回: 卡状态、运营商、信号强度等 + 2. **QueryFlow** - 查询流量使用情况 + - 参数: iccid + - 返回: 已用流量、剩余流量、套餐流量等 + 3. **QueryRealnameStatus** - 查询实名认证状态 + - 参数: iccid + - 返回: 是否已实名、实名信息等 + 4. **StopCard** - 停机(暂停流量卡) + - 参数: iccid + - 返回: 操作结果 + 5. **StartCard** - 复机(恢复流量卡) + - 参数: iccid + - 返回: 操作结果 + 6. **GetRealnameLink** - 获取实名认证跳转链接 + - 参数: iccid + - 返回: H5 认证链接 + 7. **~~BatchQuery~~** - ❌ 删除(用户明确不需要) + - 每个方法: + - 接收 `context.Context` 参数 + - 调用 `doRequest` 统一请求方法 + - 解析响应数据到对应的 Resp 结构体 + - 返回 `(*XxxResp, error)` + - 添加详细中文注释 + + **Must NOT do**: + - 不实现 BatchQuery(删除) + - 不在方法内重复加密/签名逻辑(使用 doRequest) + - 不忽略错误处理 + + **Recommended Agent Profile**: + - **Category**: `ultrabrain` + - Reason: API 实现需要精确的请求/响应映射和错误处理 + - **Skills**: 无 + - Reason: API 实现是标准流程,不涉及项目特定规范 + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 4(与 Task 2.2 并行) + - **Blocks**: Task 2.3, 5.1(测试依赖 API 实现) + - **Blocked By**: Task 1.4, 1.5, 1.2, 1.3(需要客户端、重试、加密、DTO) + + **References**: + - **Apifox 文档**: `https://omp5mq28pq.apifox.cn/7819761m0` - 流量卡接口文档(endpoint、请求/响应格式) + - `internal/gateway/client.go` - doRequest 方法(统一请求入口) + - `internal/gateway/models.go` - DTO 定义(CardStatusReq, CardStatusResp 等) + - `internal/gateway/crypto.go` - 加密/签名工具(已在 doRequest 中使用) + + **Acceptance Criteria**: + + **TDD - RED Phase**: + - [ ] 测试文件更新: `internal/gateway/flow_card_test.go` + - [ ] 编写失败测试(每个接口一个测试): + ```go + func TestQueryCardStatus(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 模拟 Gateway 响应 + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(GatewayResponse{ + Code: 200, + Msg: "success", + Data: CardStatusResp{Status: "online", Operator: "移动"}, + }) + })) + defer server.Close() + + client := NewClient(server.URL, "appid", "secret", zap.NewNop()) + resp, err := client.QueryCardStatus(context.Background(), "8986062580006141710") + require.NoError(t, err) + assert.Equal(t, "online", resp.Status) + } + ``` + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestQuery` → FAIL + + **TDD - GREEN Phase**: + - [ ] 实现 QueryCardStatus 方法 + - [ ] 实现 QueryFlow 方法 + - [ ] 实现 QueryRealnameStatus 方法 + - [ ] 实现 StopCard 方法 + - [ ] 实现 StartCard 方法 + - [ ] 实现 GetRealnameLink 方法 + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestQuery` → PASS + + **TDD - REFACTOR Phase**: + - [ ] 添加详细中文注释(每个方法说明用途和参数) + - [ ] 提取公共逻辑(如错误码映射) + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestQuery` → PASS + + **Commit**: YES + - Message: `feat(gateway): 实现流量卡 API(6 个接口)` + - Files: `internal/gateway/flow_card.go`, `internal/gateway/flow_card_test.go` + - Pre-commit: `go test -v ./internal/gateway/ -run TestQuery` + +--- + +- [ ] **2.2 实现设备 API(7 个接口签名)** + + **What to do**: + - 在 `internal/gateway/device.go` 中实现 7 个设备 API **方法签名**: + 1. **GetDeviceInfo** - 获取设备信息 + 2. **GetSlotInfo** - 获取设备卡槽信息 + 3. **SetSpeedLimit** - 设置设备限速 + 4. **SetWiFi** - 设置设备 WiFi + 5. **SwitchCard** - 设备切换卡 + 6. **ResetDevice** - 设备恢复出厂设置 + 7. **RebootDevice** - 设备重启 + - 每个方法: + - 接收 `context.Context` 和相应参数 + - **暂时返回 "未实现" 错误**: + ```go + return nil, errors.New(errors.CodeGatewayNotImplemented, "设备 API 暂未实现(无测试环境)") + ``` + - 添加 TODO 注释说明未来需实现 + - 保留完整的方法签名和 DTO 定义 + + **Must NOT do**: + - 不实现真实的 HTTP 请求(无测试环境) + - 不删除这些方法(保留接口定义) + - 不在集成测试中测试设备 API + + **Recommended Agent Profile**: + - **Category**: `quick` + - Reason: 只实现方法签名,无复杂逻辑 + - **Skills**: 无 + - Reason: 简单的方法签名定义 + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 4(与 Task 2.1 并行) + - **Blocks**: Task 2.3, 5.1(测试需要所有方法存在) + - **Blocked By**: Task 1.4, 1.5, 1.2, 1.3(需要客户端、重试、加密、DTO) + + **References**: + - **Apifox 文档**: `https://omp5mq28pq.apifox.cn/7819761m0` - 设备接口文档(了解参数和返回值) + - `internal/gateway/flow_card.go` - 参考流量卡 API 的方法签名模式 + - `internal/gateway/models.go` - 设备相关 DTO + + **Acceptance Criteria**: + + **TDD - RED Phase**: + - [ ] 测试文件创建: `internal/gateway/device_test.go` + - [ ] 编写失败测试(验证方法存在且返回未实现错误): + ```go + func TestGetDeviceInfo_NotImplemented(t *testing.T) { + client := NewClient("https://api.example.com", "appid", "secret", zap.NewNop()) + _, err := client.GetDeviceInfo(context.Background(), "device-123") + require.Error(t, err) + assert.Contains(t, err.Error(), "未实现") + } + ``` + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestGetDevice` → FAIL + + **TDD - GREEN Phase**: + - [ ] 定义所有 7 个设备 API 方法签名 + - [ ] 每个方法返回 "未实现" 错误 + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestGetDevice` → PASS + + **TDD - REFACTOR Phase**: + - [ ] 添加 TODO 注释(说明未来需实现) + - [ ] 添加详细中文注释(每个方法说明用途) + - [ ] 运行测试: `go test -v ./internal/gateway/ -run TestGetDevice` → PASS + + **Commit**: YES + - Message: `feat(gateway): 实现设备 API 方法签名(7 个接口,暂未实现)` + - Files: `internal/gateway/device.go`, `internal/gateway/device_test.go` + - Pre-commit: `go test -v ./internal/gateway/ -run TestGetDevice` + +--- + +- [ ] **2.3 添加单元测试(覆盖率 ≥ 90%)** + + **What to do**: + - 在 `internal/gateway/client_test.go` 中补充完整单元测试: + - **加密/签名测试**(已在 Task 1.2 完成) + - **doRequest 测试**(已在 Task 1.5 完成) + - **流量卡 API 测试**(已在 Task 2.1 完成) + - **设备 API 测试**(已在 Task 2.2 完成) + - 新增测试覆盖: + - **边界条件测试**: 空字符串、nil 参数、超长字符串 + - **错误处理测试**: 网络错误、超时、业务错误 + - **并发安全测试**: 多 goroutine 同时调用客户端 + - 使用 **table-driven tests** 模式 + - 运行覆盖率检查: + ```bash + go test -v ./internal/gateway/... -cover -coverprofile=coverage.out + go tool cover -func=coverage.out + ``` + - 确保核心逻辑覆盖率 ≥ 90% + + **Must NOT do**: + - 不测试设备 API 的真实调用(无环境) + - 不过度测试(不需要 100% 覆盖率) + - 不跳过错误场景测试 + + **Recommended Agent Profile**: + - **Category**: `ultrabrain` + - Reason: 测试覆盖率需要全面考虑边界条件和错误场景 + - **Skills**: 无 + - Reason: 单元测试是通用技能 + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 5(与 Task 4.1, 4.2 并行) + - **Blocks**: None(测试不阻塞其他任务) + - **Blocked By**: Task 2.1, 2.2(需要 API 实现存在) + + **References**: + - `tests/unit/shop_store_test.go` - 参考项目的 table-driven tests 模式 + - **Go 测试文档**: `testing` 标准库(t.Run, subtests) + - **httptest 文档**: `net/http/httptest` - 模拟 HTTP 服务器 + + **Acceptance Criteria**: + + **测试覆盖验证**: + - [ ] 运行覆盖率检查: `go test -v ./internal/gateway/... -cover -coverprofile=coverage.out` + - [ ] 查看覆盖率报告: `go tool cover -func=coverage.out | grep total` + - [ ] 核心逻辑覆盖率 ≥ 90%: + - `crypto.go` ≥ 90% + - `client.go` ≥ 90% + - `flow_card.go` ≥ 90% + - `device.go` = 100%(只返回错误,简单) + - [ ] 所有测试通过: `go test -v ./internal/gateway/...` → PASS + + **测试场景覆盖**: + - [ ] 加密/签名正确性测试(加密 → 解密 → 验证) + - [ ] 重试逻辑测试(5xx 重试、4xx 不重试、超时不重试) + - [ ] 错误分类测试(网络/超时/业务错误) + - [ ] 边界条件测试(空参数、nil、超长字符串) + - [ ] 并发安全测试(多 goroutine 调用) + + **Commit**: YES + - Message: `test(gateway): 补充单元测试覆盖率(≥ 90%)` + - Files: `internal/gateway/*_test.go` + - Pre-commit: `go test -v ./internal/gateway/... -cover` + +--- + +### Phase 3: 配置和错误码集成 + +- [ ] **3.1 添加 Gateway 配置结构** + + **What to do**: + - 在 `pkg/config/config.go` 中添加 `GatewayConfig` 结构体: + ```go + // GatewayConfig Gateway API 配置 + type GatewayConfig struct { + BaseURL string `mapstructure:"base_url" validate:"required,url"` // Gateway API 地址 + AppID string `mapstructure:"app_id" validate:"required"` // 应用ID + AppSecret string `mapstructure:"app_secret" validate:"required"` // 应用密钥 + Timeout time.Duration `mapstructure:"timeout" validate:"min=1s"` // 请求超时时间(默认 30s) + } + ``` + - 在 `Config` 结构体中添加字段: + ```go + type Config struct { + // ... 现有字段 + Gateway GatewayConfig `mapstructure:"gateway"` + } + ``` + - 在 `pkg/config/defaults/config.yaml` 中添加默认配置: + ```yaml + gateway: + base_url: "https://lplan.whjhft.com/openapi" + app_id: "" + app_secret: "" + timeout: 30s + ``` + - 在 `pkg/config/loader.go` 中绑定环境变量: + ```go + viper.BindEnv("gateway.base_url", "JUNHONG_GATEWAY_BASEURL") + viper.BindEnv("gateway.app_id", "JUNHONG_GATEWAY_APPID") + viper.BindEnv("gateway.app_secret", "JUNHONG_GATEWAY_APPSECRET") + viper.BindEnv("gateway.timeout", "JUNHONG_GATEWAY_TIMEOUT") + ``` + - 添加配置验证(必填项检查) + + **Must NOT do**: + - 不在配置文件中硬编码 AppID/AppSecret(使用环境变量) + - 不跳过验证逻辑 + + **Recommended Agent Profile**: + - **Category**: `quick` + - Reason: 配置结构定义是标准化任务 + - **Skills**: 无 + - Reason: 配置管理遵循现有模式,无需特殊规范 + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1(与 Task 1.1 并行) + - **Blocks**: Task 4.1(Bootstrap 初始化依赖配置) + - **Blocked By**: None(可立即开始) + + **References**: + - `pkg/config/config.go` - 参考现有配置结构(SMSConfig, JWTConfig) + - `pkg/config/defaults/config.yaml` - 参考默认配置格式 + - `pkg/config/loader.go` - 参考环境变量绑定模式(viper.BindEnv) + + **Acceptance Criteria**: + + **配置加载验证**: + - [ ] 配置结构定义完成: `GatewayConfig` 在 `pkg/config/config.go` + - [ ] 默认配置存在: `pkg/config/defaults/config.yaml` 包含 gateway 配置 + - [ ] 环境变量绑定: `pkg/config/loader.go` 绑定 JUNHONG_GATEWAY_* 变量 + - [ ] 配置验证通过: + ```bash + export JUNHONG_GATEWAY_BASEURL="https://lplan.whjhft.com/openapi" + export JUNHONG_GATEWAY_APPID="60bgt1X8i7AvXqkd" + export JUNHONG_GATEWAY_APPSECRET="BZeQttaZQt0i73moF" + go run cmd/api/main.go --help # 验证配置加载无错误 + ``` + - [ ] 编译通过: `go build ./pkg/config/...` → 无错误 + + **Commit**: YES + - Message: `feat(config): 添加 Gateway API 配置结构` + - Files: `pkg/config/config.go`, `pkg/config/defaults/config.yaml`, `pkg/config/loader.go` + - Pre-commit: `go build ./pkg/config/...` + +--- + +- [ ] **3.2 添加 Gateway 错误码(1110-1119)** + + **What to do**: + - 在 `pkg/errors/codes.go` 中添加 Gateway 错误码常量: + ```go + // Gateway 相关错误 (1110-1119) + CodeGatewayNetworkError = 1110 // Gateway 网络错误 + CodeGatewayTimeout = 1111 // Gateway 请求超时 + CodeGatewayBusinessError = 1112 // Gateway 业务错误 + CodeGatewayEncryptionError = 1113 // Gateway 加密错误 + CodeGatewayDecryptionError = 1114 // Gateway 解密错误 + CodeGatewaySignatureError = 1115 // Gateway 签名错误 + CodeGatewayInvalidResponse = 1116 // Gateway 响应格式错误 + CodeGatewayNotImplemented = 1117 // Gateway 接口未实现 + // 预留 1118-1119 供未来扩展 + ``` + - 在 `allErrorCodes` 数组中注册新错误码 + - 在 `errorMessages` 映射表中添加中文错误消息: + ```go + 1110: "Gateway 网络错误", + 1111: "Gateway 请求超时", + 1112: "Gateway 业务错误", + 1113: "Gateway 加密失败", + 1114: "Gateway 解密失败", + 1115: "Gateway 签名错误", + 1116: "Gateway 响应格式错误", + 1117: "Gateway 接口未实现", + ``` + - 运行错误码验证测试: `go test -v ./pkg/errors/...` + + **Must NOT do**: + - 不使用其他范围的错误码(严格使用 1110-1119) + - 不跳过错误码验证测试 + + **Recommended Agent Profile**: + - **Category**: `quick` + - Reason: 错误码定义是标准化任务 + - **Skills**: 无 + - Reason: 错误码遵循现有模式 + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 2(与 Task 1.2, 1.3 并行) + - **Blocks**: Task 4.1(Bootstrap 初始化可能需要错误码) + - **Blocked By**: Task 1.1(目录结构需先存在) + + **References**: + - `pkg/errors/codes.go` - 参考现有错误码定义模式 + - `pkg/errors/errors_test.go` - 参考错误码验证测试 + + **Acceptance Criteria**: + + **错误码验证**: + - [ ] 错误码常量定义完成: `CodeGatewayNetworkError` 等在 `pkg/errors/codes.go` + - [ ] 错误码注册完成: `allErrorCodes` 包含 1110-1119 + - [ ] 错误消息定义完成: `errorMessages` 包含中文消息 + - [ ] 错误码验证测试通过: `go test -v ./pkg/errors/...` → PASS + - [ ] 编译通过: `go build ./pkg/errors/...` → 无错误 + + **Commit**: YES + - Message: `feat(errors): 添加 Gateway 错误码(1110-1119)` + - Files: `pkg/errors/codes.go` + - Pre-commit: `go test -v ./pkg/errors/...` + +--- + +### Phase 4: 依赖注入和集成 + +- [ ] **4.1 Bootstrap 初始化 Gateway 客户端** + + **What to do**: + - 在 `internal/bootstrap/dependencies.go` 的 `Dependencies` 结构体中添加字段: + ```go + type Dependencies struct { + // ... 现有字段 + GatewayClient *gateway.Client // Gateway API 客户端 + } + ``` + - 在 `internal/bootstrap/bootstrap.go` 的 `Bootstrap` 函数中初始化 Gateway 客户端: + ```go + // 初始化 Gateway 客户端 + gatewayClient := gateway.NewClient( + cfg.Gateway.BaseURL, + cfg.Gateway.AppID, + cfg.Gateway.AppSecret, + deps.Logger, + ).WithTimeout(cfg.Gateway.Timeout) + deps.GatewayClient = gatewayClient + + deps.Logger.Info("Gateway 客户端初始化成功", + zap.String("base_url", cfg.Gateway.BaseURL), + zap.Duration("timeout", cfg.Gateway.Timeout), + ) + ``` + - 确保初始化顺序正确(在 Service 初始化之前) + + **Must NOT do**: + - 不在 Bootstrap 中调用 Gateway API(只初始化客户端) + - 不跳过日志记录 + + **Recommended Agent Profile**: + - **Category**: `quick` + - Reason: 依赖注入是标准流程 + - **Skills**: 无 + - Reason: Bootstrap 模式遵循现有模式 + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 5(与 Task 2.3 并行) + - **Blocks**: Task 4.2(Service 集成依赖 Bootstrap) + - **Blocked By**: Task 1.4, 3.1, 3.2(需要客户端、配置、错误码) + + **References**: + - `internal/bootstrap/bootstrap.go` - 参考现有依赖初始化模式(SMS、Storage) + - `internal/bootstrap/dependencies.go` - 参考 Dependencies 结构体定义 + - `internal/gateway/client.go` - Gateway 客户端构造函数 + + **Acceptance Criteria**: + + **Bootstrap 验证**: + - [ ] Dependencies 字段添加: `GatewayClient *gateway.Client` + - [ ] Bootstrap 初始化代码完成(在 Service 初始化之前) + - [ ] 编译通过: `go build ./cmd/api/...` → 无错误 + - [ ] 启动验证: + ```bash + source .env.local && go run cmd/api/main.go + # 查看日志: "Gateway 客户端初始化成功" + ``` + + **Commit**: YES + - Message: `feat(bootstrap): 初始化 Gateway 客户端` + - Files: `internal/bootstrap/dependencies.go`, `internal/bootstrap/bootstrap.go` + - Pre-commit: `go build ./cmd/api/...` + +--- + +- [ ] **4.2 Service 层集成示例(SyncCardStatus)** + + **What to do**: + - 在 `internal/service/iot_card/service.go` 的 `Service` 结构体中添加字段: + ```go + type Service struct { + // ... 现有字段 + gatewayClient *gateway.Client // Gateway API 客户端 + } + ``` + - 更新构造函数 `New` 添加参数: + ```go + func New( + // ... 现有参数 + gatewayClient *gateway.Client, + ) *Service { + return &Service{ + // ... 现有字段 + gatewayClient: gatewayClient, + } + } + ``` + - 新增方法 `SyncCardStatus`(从 Gateway 同步卡状态到数据库): + ```go + // SyncCardStatus 从 Gateway 同步流量卡状态到数据库 + func (s *Service) SyncCardStatus(ctx context.Context, iccid string) error { + // 1. 调用 Gateway API 查询卡状态 + resp, err := s.gatewayClient.QueryCardStatus(ctx, iccid) + if err != nil { + s.logger.Error("查询 Gateway 卡状态失败", + zap.String("iccid", iccid), + zap.Error(err), + ) + return errors.Wrap(errors.CodeGatewayBusinessError, err, "同步卡状态失败") + } + + // 2. 更新数据库中的卡状态 + err = s.iotCardStore.UpdateStatus(ctx, iccid, resp.Status) + if err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "更新卡状态失败") + } + + s.logger.Info("同步卡状态成功", + zap.String("iccid", iccid), + zap.String("status", resp.Status), + ) + return nil + } + ``` + - 在 `internal/bootstrap/services.go` 中更新 iot_card Service 初始化: + ```go + IotCard: iot_card.New( + // ... 现有参数 + deps.GatewayClient, + ), + ``` + + **Must NOT do**: + - 不暴露 Gateway 响应结构给 Handler 层(封装在 Service 内) + - 不跳过错误处理和日志记录 + + **Recommended Agent Profile**: + - **Category**: `ultrabrain` + - Reason: Service 集成需要考虑错误处理、日志记录、数据库更新 + - **Skills**: 无 + - Reason: Service 集成是标准流程 + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 5(与 Task 2.3, 5.1, 5.2 并行) + - **Blocks**: None(集成示例不阻塞其他任务) + - **Blocked By**: Task 4.1(需要 Bootstrap 初始化完成) + + **References**: + - `internal/service/iot_card/service.go` - 现有 Service 结构和方法 + - `internal/bootstrap/services.go` - Service 初始化模式 + - `internal/gateway/flow_card.go` - QueryCardStatus 方法 + + **Acceptance Criteria**: + + **集成验证**: + - [ ] Service 字段添加: `gatewayClient *gateway.Client` + - [ ] 构造函数更新: `New` 函数添加 gatewayClient 参数 + - [ ] SyncCardStatus 方法实现完成 + - [ ] Bootstrap 更新: `services.go` 传递 GatewayClient + - [ ] 编译通过: `go build ./internal/service/iot_card/...` → 无错误 + - [ ] 启动验证: `source .env.local && go run cmd/api/main.go` → 无错误 + + **Commit**: YES + - Message: `feat(service): 集成 Gateway 客户端到 iot_card Service(SyncCardStatus 示例)` + - Files: `internal/service/iot_card/service.go`, `internal/bootstrap/services.go` + - Pre-commit: `go build ./internal/service/iot_card/...` + +--- + +### Phase 5: 集成测试和文档 + +- [ ] **5.1 编写集成测试(真实 Gateway 环境)** + + **What to do**: + - 在 `internal/gateway/integration_test.go` 中编写集成测试: + - 使用真实 Gateway 配置(BaseURL, AppID, AppSecret) + - 使用测试 ICCID: `8986062580006141710` + - 测试至少 2 个流量卡接口: + 1. `QueryCardStatus` - 查询卡状态 + 2. `QueryFlow` - 查询流量使用 + - **不测试设备 API**(无测试环境) + - 集成测试结构: + ```go + // +build integration + + package gateway_test + + import ( + "context" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + "junhong_cmp_fiber/internal/gateway" + ) + + func TestIntegration_QueryCardStatus(t *testing.T) { + if os.Getenv("JUNHONG_GATEWAY_APPID") == "" { + t.Skip("跳过集成测试:未配置 Gateway 环境变量") + } + + client := gateway.NewClient( + os.Getenv("JUNHONG_GATEWAY_BASEURL"), + os.Getenv("JUNHONG_GATEWAY_APPID"), + os.Getenv("JUNHONG_GATEWAY_APPSECRET"), + zap.NewNop(), + ).WithTimeout(30 * time.Second) + + ctx := context.Background() + resp, err := client.QueryCardStatus(ctx, "8986062580006141710") + + require.NoError(t, err) + assert.NotNil(t, resp) + assert.NotEmpty(t, resp.Status) + + t.Logf("卡状态查询成功: %+v", resp) + } + ``` + - 运行集成测试: + ```bash + source .env.local && go test -v ./internal/gateway/... -tags=integration -run TestIntegration + ``` + + **Must NOT do**: + - 不在 CI/CD 中自动运行集成测试(需手动触发) + - 不测试设备 API(无环境) + - 不硬编码配置(使用环境变量) + + **Recommended Agent Profile**: + - **Category**: `ultrabrain` + - Reason: 集成测试需要处理真实 API 响应和错误场景 + - **Skills**: 无 + - Reason: 集成测试是通用技能 + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 5(与 Task 2.3, 4.2, 5.2 并行) + - **Blocks**: None(测试不阻塞其他任务) + - **Blocked By**: Task 2.1, 2.2, 4.1(需要 API 实现和 Bootstrap) + + **References**: + - **测试配置**: BaseURL, AppID, AppSecret, 测试 ICCID(用户提供) + - `tests/integration/` - 参考项目的集成测试模式 + - **Go build tags**: `// +build integration` - 条件编译标记 + + **Acceptance Criteria**: + + **集成测试验证**: + - [ ] 集成测试文件创建: `internal/gateway/integration_test.go` + - [ ] 测试使用 build tag: `// +build integration` + - [ ] 测试使用环境变量(不硬编码配置) + - [ ] 测试至少 2 个接口: QueryCardStatus, QueryFlow + - [ ] 运行集成测试: + ```bash + source .env.local && go test -v ./internal/gateway/... -tags=integration -run TestIntegration + ``` + - [ ] 集成测试通过(验证真实 Gateway API) + - [ ] 日志输出完整(包含请求/响应详情) + + **Commit**: YES + - Message: `test(gateway): 添加集成测试(真实 Gateway 环境)` + - Files: `internal/gateway/integration_test.go` + - Pre-commit: `source .env.local && go test -v ./internal/gateway/... -tags=integration -run TestIntegration` + +--- + +- [ ] **5.2 更新文档** + + **What to do**: + - 创建 `docs/gateway-client-usage.md` 使用文档: + - **概述**: Gateway 客户端功能和用途 + - **配置说明**: 环境变量配置示例 + - **使用示例**: 代码示例(QueryCardStatus, QueryFlow 等) + - **错误码说明**: Gateway 错误码(1110-1119)和处理方式 + - **重试机制**: 重试策略说明(3 次,指数退避) + - **安全警告**: AES-128-ECB 和 MD5 的安全风险说明 + - **最佳实践**: 超时设置、错误处理、日志记录 + - 更新 `README.md`: + - 在 "功能模块" 部分添加 Gateway 集成模块 + - 在 "配置说明" 部分添加 Gateway 环境变量说明 + - 确保所有文档使用中文 + + **Must NOT do**: + - 不过度详细(不需要 API 文档级别的详情) + - 不添加英文文档(项目要求中文) + + **Recommended Agent Profile**: + - **Category**: `writing` + - Reason: 文档编写任务 + - **Skills**: [`doc-management`] + - `doc-management`: 规范文档管理流程 + - Reason: 文档更新需要遵循项目文档规范 + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 5(与 Task 2.3, 4.2, 5.1 并行) + - **Blocks**: None(文档不阻塞其他任务) + - **Blocked By**: All(文档需要完整功能才能编写) + + **References**: + - `docs/` - 参考现有文档结构和风格 + - `README.md` - 参考现有 README 格式 + - **Apifox 文档**: `https://omp5mq28pq.apifox.cn/7819761m0` - Gateway API 文档(参考) + - `internal/gateway/client.go` - Gateway 客户端实现(提取使用示例) + + **Acceptance Criteria**: + + **文档验证**: + - [ ] 文档创建: `docs/gateway-client-usage.md` + - [ ] 文档包含以下部分: + - [ ] 概述 + - [ ] 配置说明(环境变量) + - [ ] 使用示例(至少 2 个接口) + - [ ] 错误码说明(1110-1119) + - [ ] 重试机制说明 + - [ ] 安全警告(ECB + MD5) + - [ ] 最佳实践 + - [ ] README.md 更新: + - [ ] "功能模块" 部分添加 Gateway 集成 + - [ ] "配置说明" 部分添加 Gateway 环境变量 + - [ ] 所有文档使用中文 + - [ ] 代码示例可编译(复制到项目中能运行) + + **Commit**: YES + - Message: `docs(gateway): 添加 Gateway 客户端使用文档` + - Files: `docs/gateway-client-usage.md`, `README.md` + - Pre-commit: 无(文档无需测试) + +--- + +## Commit Strategy + +| After Task | Message | Files | Verification | +|------------|---------|-------|--------------| +| 1.1 | `feat(gateway): 创建 Gateway 包目录结构` | `internal/gateway/*.go` | `go build ./internal/gateway/...` | +| 1.2 | `feat(gateway): 实现 AES-128-ECB 加密和 MD5 签名工具` | `internal/gateway/crypto.go`, `crypto_test.go` | `go test -v ./internal/gateway/ -run TestAES` | +| 1.3 | `feat(gateway): 定义 Gateway API 请求/响应 DTO 结构` | `internal/gateway/models.go`, `models_test.go` | `go test -v ./internal/gateway/ -run TestGatewayRequest` | +| 1.4 | `feat(gateway): 实现 Gateway 客户端基础结构` | `internal/gateway/client.go`, `client_test.go` | `go test -v ./internal/gateway/ -run TestNewClient` | +| 1.5 | `feat(gateway): 实现 HTTP 重试逻辑(3 次重试,指数退避)` | `internal/gateway/client.go`, `client_test.go` | `go test -v ./internal/gateway/ -run TestDoRequest` | +| 2.1 | `feat(gateway): 实现流量卡 API(6 个接口)` | `internal/gateway/flow_card.go`, `flow_card_test.go` | `go test -v ./internal/gateway/ -run TestQuery` | +| 2.2 | `feat(gateway): 实现设备 API 方法签名(7 个接口,暂未实现)` | `internal/gateway/device.go`, `device_test.go` | `go test -v ./internal/gateway/ -run TestGetDevice` | +| 2.3 | `test(gateway): 补充单元测试覆盖率(≥ 90%)` | `internal/gateway/*_test.go` | `go test -v ./internal/gateway/... -cover` | +| 3.1 | `feat(config): 添加 Gateway API 配置结构` | `pkg/config/config.go`, `defaults/config.yaml`, `loader.go` | `go build ./pkg/config/...` | +| 3.2 | `feat(errors): 添加 Gateway 错误码(1110-1119)` | `pkg/errors/codes.go` | `go test -v ./pkg/errors/...` | +| 4.1 | `feat(bootstrap): 初始化 Gateway 客户端` | `internal/bootstrap/dependencies.go`, `bootstrap.go` | `go build ./cmd/api/...` | +| 4.2 | `feat(service): 集成 Gateway 客户端到 iot_card Service(SyncCardStatus 示例)` | `internal/service/iot_card/service.go`, `bootstrap/services.go` | `go build ./internal/service/iot_card/...` | +| 5.1 | `test(gateway): 添加集成测试(真实 Gateway 环境)` | `internal/gateway/integration_test.go` | `source .env.local && go test -v ./internal/gateway/... -tags=integration` | +| 5.2 | `docs(gateway): 添加 Gateway 客户端使用文档` | `docs/gateway-client-usage.md`, `README.md` | 无 | + +--- + +## Success Criteria + +### Verification Commands +```bash +# 1. 编译检查 +go build ./internal/gateway/... +go build ./cmd/api/... + +# 2. 单元测试(覆盖率 ≥ 90%) +go test -v ./internal/gateway/... -cover -coverprofile=coverage.out +go tool cover -func=coverage.out | grep total + +# 3. 集成测试(需先配置环境变量) +source .env.local +go test -v ./internal/gateway/... -tags=integration -run TestIntegration + +# 4. LSP 错误检查 +gopls check ./internal/gateway/... + +# 5. 启动服务验证 +source .env.local && go run cmd/api/main.go +# 查看日志: "Gateway 客户端初始化成功" +``` + +### Final Checklist +- [ ] 所有 14 个 Gateway API 接口成功封装(流量卡 6 个 + 设备 7 个签名) +- [ ] 加密/签名通过单元测试验证(与 Apifox 文档一致) +- [ ] 集成测试验证真实 Gateway API 调用(测试 ICCID: `8986062580006141710`) +- [ ] 重试逻辑测试通过(3 次重试,指数退避) +- [ ] 配置通过环境变量成功加载(JUNHONG_GATEWAY_*) +- [ ] 依赖注入到 iot_card Service 成功(SyncCardStatus 方法) +- [ ] 单元测试覆盖率 ≥ 90%(核心逻辑) +- [ ] 无 LSP 错误,编译通过 +- [ ] 符合项目代码规范(中文注释、Go 命名规范) +- [ ] 文档完整(使用示例、错误码说明) +- [ ] 所有 "Must NOT Have" 约束遵守(无批量查询、无设备真实测试、无第三方重试库) diff --git a/openspec/changes/gateway-integration/design.md b/openspec/changes/gateway-integration/design.md new file mode 100644 index 0000000..1e1e2de --- /dev/null +++ b/openspec/changes/gateway-integration/design.md @@ -0,0 +1,570 @@ +# 设计文档: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 new file mode 100644 index 0000000..cd125b7 --- /dev/null +++ b/openspec/changes/gateway-integration/proposal.md @@ -0,0 +1,146 @@ +# 提案: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 new file mode 100644 index 0000000..e92a25b --- /dev/null +++ b/openspec/changes/gateway-integration/specs/gateway-client/spec.md @@ -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 错误 diff --git a/openspec/changes/gateway-integration/specs/gateway-config/spec.md b/openspec/changes/gateway-integration/specs/gateway-config/spec.md new file mode 100644 index 0000000..be143a8 --- /dev/null +++ b/openspec/changes/gateway-integration/specs/gateway-config/spec.md @@ -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` diff --git a/openspec/changes/gateway-integration/specs/gateway-crypto/spec.md b/openspec/changes/gateway-integration/specs/gateway-crypto/spec.md new file mode 100644 index 0000000..8225e66 --- /dev/null +++ b/openspec/changes/gateway-integration/specs/gateway-crypto/spec.md @@ -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×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 new file mode 100644 index 0000000..5ad0ef5 --- /dev/null +++ b/openspec/changes/gateway-integration/tasks.md @@ -0,0 +1,173 @@ +# 任务清单:Gateway API 统一封装 + +## Phase 1: 基础结构搭建(30min) + +### Task 1.1: 创建 Gateway 包目录结构 +- [ ] 创建 `internal/gateway/` 目录 +- [ ] 创建占位文件:`client.go`、`crypto.go`、`models.go` +- **验证**:目录结构创建成功 + +### Task 1.2: 实现加密/签名工具函数 +- [ ] 在 `crypto.go` 中实现 `aesEncrypt` 函数(AES-128-ECB + PKCS5Padding + Base64) +- [ ] 在 `crypto.go` 中实现 `generateSign` 函数(MD5 签名,大写输出) +- [ ] 添加单元测试验证加密/签名正确性 +- **验证**: + ```bash + go test -v ./internal/gateway -run TestAESEncrypt + go test -v ./internal/gateway -run TestGenerateSign + ``` + +### Task 1.3: 实现 Gateway 客户端基础结构 +- [ ] 在 `client.go` 中定义 `Client` 结构体 +- [ ] 实现 `NewClient` 构造函数 +- [ ] 实现 `WithTimeout` 配置方法 +- [ ] 实现 `doRequest` 统一请求方法(加密、签名、HTTP 请求、响应解析) +- **验证**:编译通过,无 LSP 错误 + +### Task 1.4: 定义请求/响应 DTO +- [ ] 在 `models.go` 中定义 `GatewayResponse` 通用响应结构 +- [ ] 定义流量卡相关 DTO(`CardStatusReq`、`CardStatusResp`、`FlowQueryReq`、`FlowUsageResp` 等) +- [ ] 定义设备相关 DTO(`DeviceInfoReq`、`DeviceInfoResp` 等) +- [ ] 添加 JSON 标签和验证标签 +- **验证**:编译通过,结构体定义完整 + +--- + +## Phase 2: API 接口封装(40min) + +### Task 2.1: 实现流量卡 API(7个接口) +- [ ] 在 `flow_card.go` 中实现 `QueryCardStatus`(流量卡状态查询) +- [ ] 实现 `QueryFlow`(流量使用查询) +- [ ] 实现 `QueryRealnameStatus`(实名认证状态查询) +- [ ] 实现 `StopCard`(流量卡停机) +- [ ] 实现 `StartCard`(流量卡复机) +- [ ] 实现 `GetRealnameLink`(获取实名认证跳转链接) +- [ ] 预留 `BatchQuery`(批量查询,未来扩展) +- **验证**:编译通过,方法签名正确 + +### Task 2.2: 实现设备 API(7个接口) +- [ ] 在 `device.go` 中实现 `GetDeviceInfo`(获取设备信息) +- [ ] 实现 `GetSlotInfo`(获取设备卡槽信息) +- [ ] 实现 `SetSpeedLimit`(设置设备限速) +- [ ] 实现 `SetWiFi`(设置设备 WiFi) +- [ ] 实现 `SwitchCard`(设备切换卡) +- [ ] 实现 `ResetDevice`(设备恢复出厂设置) +- [ ] 实现 `RebootDevice`(设备重启) +- **验证**:编译通过,方法签名正确 + +### Task 2.3: 添加单元测试 +- [ ] 在 `client_test.go` 中添加加密/签名单元测试 +- [ ] 添加 `doRequest` 的 mock 测试 +- [ ] 验证错误处理逻辑(超时、网络错误、响应格式错误) +- **验证**: + ```bash + go test -v ./internal/gateway -cover + ``` + 覆盖率 ≥ 90% + +--- + +## Phase 3: 配置和错误码集成(20min) + +### Task 3.1: 添加 Gateway 配置 +- [ ] 在 `pkg/config/config.go` 中添加 `GatewayConfig` 结构体 +- [ ] 在 `Config` 中添加 `Gateway GatewayConfig` 字段 +- [ ] 在 `pkg/config/defaults/config.yaml` 中添加 gateway 配置项 +- [ ] 添加配置验证逻辑(必填项检查) +- **验证**: + ```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 错误码 +- [ ] 在 `pkg/errors/codes.go` 中添加 Gateway 错误码常量(1110-1119) +- [ ] 在 `allErrorCodes` 数组中注册新错误码 +- [ ] 在 `errorMessages` 映射表中添加中文错误消息 +- [ ] 运行错误码验证测试 +- **验证**: + ```bash + go test -v ./pkg/errors -run TestErrorCodes + ``` + +--- + +## Phase 4: 依赖注入和集成(20min) + +### Task 4.1: Bootstrap 初始化 Gateway 客户端 +- [ ] 在 `internal/bootstrap/bootstrap.go` 的 `Dependencies` 中添加 `GatewayClient *gateway.Client` 字段 +- [ ] 在 `Bootstrap` 函数中初始化 Gateway 客户端 +- [ ] 将 Gateway 客户端注入到需要的 Service +- **验证**:编译通过,依赖注入正确 + +### Task 4.2: Service 层集成示例 +- [ ] 选择一个 Service(如 `iot_card`)集成 Gateway 客户端 +- [ ] 添加 `SyncCardStatus` 方法示例 +- [ ] 添加错误处理和日志记录 +- **验证**:编译通过,方法签名正确 + +--- + +## Phase 5: 集成测试和文档(10min) + +### Task 5.1: 编写集成测试 +- [ ] 在 `client_test.go` 中添加集成测试(需要真实 Gateway 环境) +- [ ] 测试至少 2 个接口(如 `QueryCardStatus`、`StopCard`) +- [ ] 验证加密/签名与 Gateway 文档一致 +- **验证**: + ```bash + # 设置测试环境变量 + source .env.local + + # 运行集成测试 + go test -v ./internal/gateway -run TestIntegration + ``` + +### Task 5.2: 更新文档 +- [ ] 在 `docs/` 目录下创建 `gateway-client-usage.md` +- [ ] 添加 Gateway 客户端使用示例 +- [ ] 添加错误码说明 +- [ ] 更新 `README.md` 添加 Gateway 模块说明 +- **验证**:文档完整,示例代码可运行 + +--- + +## 验收标准 + +- [ ] 所有 14 个 Gateway API 接口成功封装 +- [ ] 加密/签名验证通过(与 Gateway 文档一致) +- [ ] 错误处理覆盖所有异常场景 +- [ ] 单元测试覆盖率 ≥ 90% +- [ ] 集成测试验证真实 Gateway API 调用 +- [ ] 配置通过环境变量成功加载 +- [ ] 依赖注入到 Service 层成功 +- [ ] 文档完整(使用示例、错误码说明) +- [ ] 无 LSP 错误,编译通过 +- [ ] 符合项目代码规范(中文注释、Go 命名规范) + +--- + +## 任务执行规范 + +**⚠️ 重要提醒**: +- ❌ 禁止跳过任务 +- ❌ 禁止合并任务或简化执行 +- ❌ 禁止自作主张优化流程 +- ✅ 必须按顺序逐项完成 +- ✅ 每个任务完成后标记 `[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(错误码)可并行 diff --git a/openspec/changes/wechat-official-account-payment-integration/.openspec.yaml b/openspec/changes/wechat-official-account-payment-integration/.openspec.yaml new file mode 100644 index 0000000..fc1220a --- /dev/null +++ b/openspec/changes/wechat-official-account-payment-integration/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-01-30 diff --git a/openspec/changes/wechat-official-account-payment-integration/design.md b/openspec/changes/wechat-official-account-payment-integration/design.md new file mode 100644 index 0000000..c95ab70 --- /dev/null +++ b/openspec/changes/wechat-official-account-payment-integration/design.md @@ -0,0 +1,368 @@ +# 微信公众号与微信支付集成 - 技术设计 + +## Context + +当前系统已具备完整的个人客户体系(JWT 认证、手机号登录)和订单支付系统(订单模型、钱包支付、支付回调幂等处理),但缺少微信公众号和微信支付的真实 SDK 集成。 + +**现有基础设施**: +- ✅ 数据模型:`tb_personal_customer` 包含 `wx_open_id`、`wx_union_id` 字段(已建索引) +- ✅ 接口定义:`pkg/wechat/wechat.go` 定义了 `Service` 接口(当前为 Mock 实现) +- ✅ 支付回调:`POST /api/callback/wechat-pay` 已预留(基础参数验证,缺签名校验) +- ✅ 订单系统:完整的订单创建、支付状态更新、套餐激活流程(幂等设计) + +**集成目标**: +- 使用 PowerWeChat v3 SDK 对接微信公众号和微信支付 API +- 支持个人客户通过微信 OAuth 登录/绑定 +- 支持两种支付场景:JSAPI 支付(微信内)、H5 支付(浏览器) +- 补充支付回调的签名验证(PowerWeChat 自动处理) + +## Goals / Non-Goals + +**Goals:** +1. 实现微信公众号 OAuth 2.0 授权流程,获取用户 OpenID/UnionID 和基本信息 +2. 实现 H5 支付和 JSAPI 支付的订单创建和支付参数生成 +3. 补充支付回调的签名验证,确保回调来源合法 +4. 集成 Redis 缓存实现微信 Access Token 中控(多实例共享) +5. 配置管理遵循项目规范(Viper + 环境变量) +6. 完整的错误处理和日志记录 + +**Non-Goals:** +- 不实现微信模板消息、客服消息等公众号其他能力(按需后续扩展) +- 不实现微信 Native 支付(扫码支付)和 App 支付(当前无此场景) +- 不修改现有订单模型和支付流程(在现有基础上扩展) +- 不实现微信退款功能(后续单独实现) + +## Decisions + +### 决策 1:SDK 选型 - PowerWeChat v3 + +**选择理由**: +- ✅ 官方文档推荐,Go 生态成熟度高 +- ✅ 支持所有公众号和支付 API(包括 H5、JSAPI、Native、App 支付) +- ✅ 内置签名验证、Token 中控、日志集成 +- ✅ 支持 Redis 缓存(与项目现有 Redis 无缝集成) +- ✅ 活跃维护,GitHub 2.5k+ stars + +**替代方案**: +- `silenceper/wechat`:功能相似,但文档较少,社区活跃度较低 +- 自行封装微信 API:工作量大,维护成本高,签名验证易出错 + +### 决策 2:架构设计 - 遵循项目分层 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ pkg/wechat/ │ +│ ├─ service.go 微信服务接口定义 │ +│ ├─ official_account.go OfficialAccount 实现(OAuth) │ +│ ├─ payment.go Payment 实现(H5/JSAPI 支付) │ +│ └─ config.go PowerWeChat 实例初始化 │ +└─────────────────────────────────────────────────────────────┘ + ↓ 依赖注入 +┌─────────────────────────────────────────────────────────────┐ +│ internal/service/ │ +│ ├─ personal_customer/service.go 调用 wechat.Service │ +│ └─ order/service.go 调用 wechat.Payment │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ internal/handler/ │ +│ ├─ app/personal_customer.go OAuth 登录端点 │ +│ ├─ h5/order.go 支付发起端点 │ +│ └─ callback/payment.go 支付回调端点 │ +└─────────────────────────────────────────────────────────────┘ +``` + +**依赖注入方式**: +- `pkg/wechat.Service` 在 `internal/bootstrap/services.go` 中初始化 +- 注入到 `PersonalCustomerService` 和 `OrderService` +- Handler 通过 Service 调用微信能力 + +**选择理由**: +- ✅ 符合项目 Handler → Service → Store → Model 分层 +- ✅ `pkg/wechat/` 作为基础设施层,独立于业务逻辑 +- ✅ 通过接口隔离,便于测试(Mock 实现) + +### 决策 3:配置管理 - Viper + 环境变量 + +**配置结构**: +```yaml +wechat: + official_account: + app_id: "" # JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_ID + app_secret: "" # JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_SECRET + token: "" # JUNHONG_WECHAT_OFFICIAL_ACCOUNT_TOKEN + aes_key: "" # JUNHONG_WECHAT_OFFICIAL_ACCOUNT_AES_KEY + oauth_redirect_url: "" # JUNHONG_WECHAT_OFFICIAL_ACCOUNT_OAUTH_REDIRECT_URL + + payment: + app_id: "" # JUNHONG_WECHAT_PAYMENT_APP_ID + mch_id: "" # JUNHONG_WECHAT_PAYMENT_MCH_ID + api_v3_key: "" # JUNHONG_WECHAT_PAYMENT_API_V3_KEY + api_v2_key: "" # JUNHONG_WECHAT_PAYMENT_API_V2_KEY + cert_path: "" # JUNHONG_WECHAT_PAYMENT_CERT_PATH + key_path: "" # JUNHONG_WECHAT_PAYMENT_KEY_PATH + serial_no: "" # JUNHONG_WECHAT_PAYMENT_SERIAL_NO + notify_url: "" # JUNHONG_WECHAT_PAYMENT_NOTIFY_URL +``` + +**选择理由**: +- ✅ 遵循项目现有配置管理模式 +- ✅ 敏感信息通过环境变量覆盖,不提交代码库 +- ✅ Docker 部署无需挂载配置文件 + +**证书管理**: +- 证书文件路径通过环境变量配置(如 `/app/certs/apiclient_cert.pem`) +- Docker 部署时通过 Volume 挂载证书目录 +- 启动时验证证书文件存在性,缺失则报错退出 + +### 决策 4:支付场景识别 - 客户端传参 + +```go +// JSAPI 支付请求 +type WechatPayJSAPIRequest struct { + OpenID string `json:"openid" validate:"required"` // 用户 OpenID +} + +// H5 支付请求 +type WechatPayH5Request struct { + SceneInfo WechatH5SceneInfo `json:"scene_info"` +} + +type WechatH5SceneInfo struct { + PayerClientIP string `json:"payer_client_ip" validate:"required"` // 用户终端 IP + H5Info struct { + Type string `json:"type"` // 场景类型(iOS, Android, Wap) + } `json:"h5_info"` +} +``` + +**选择理由**: +- ✅ 不在后端判断场景(User-Agent 不可靠) +- ✅ 前端明确调用对应端点: + - `/api/h5/orders/:id/wechat-pay/jsapi`(微信内) + - `/api/h5/orders/:id/wechat-pay/h5`(浏览器) + +### 决策 5:支付回调处理 - 补充签名验证 + +**现有实现**: +```go +// internal/handler/callback/payment.go +func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error { + var req WechatPayCallbackRequest + if err := c.BodyParser(&req); err != nil { + return errors.New(errors.CodeInvalidParam, "请求参数解析失败") + } + + // 调用 Service 处理支付(已实现幂等) + if err := h.orderService.HandlePaymentCallback(ctx, req.OrderNo, model.PaymentMethodWechat); err != nil { + return err + } + + return response.Success(c, map[string]string{"return_code": "SUCCESS"}) +} +``` + +**增强设计**: +```go +func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error { + // 1. PowerWeChat 自动处理签名验证 + res, err := h.wechatPayment.HandlePaidNotify( + c.Request(), + func(message *request.RequestNotify, transaction *models.Transaction, fail func(string)) interface{} { + // 2. 检查事件类型 + if message.EventType != "TRANSACTION.SUCCESS" { + return true + } + + // 3. 调用现有 Service(幂等处理) + orderNo := *transaction.OutTradeNo + err := h.orderService.HandlePaymentCallback(ctx, orderNo, model.PaymentMethodWechat) + if err != nil { + fail("payment processing failed") + return nil + } + + return true + }, + ) + + // 4. PowerWeChat 自动回复微信 + return res.Write(c.Writer) +} +``` + +**选择理由**: +- ✅ PowerWeChat 自动验证签名(无需手动实现复杂的验签逻辑) +- ✅ 保留现有 `HandlePaymentCallback` 的幂等设计 +- ✅ 统一错误处理和日志记录 + +### 决策 6:Token 中控 - Redis 缓存 + +**实现方式**: +```go +import "github.com/ArtisanCloud/PowerWeChat/v3/src/kernel" + +cache := kernel.NewRedisClient(&kernel.UniversalOptions{ + Addrs: []string{config.Redis.Address}, + Password: config.Redis.Password, + DB: config.Redis.DB, +}) + +officialAccountApp, err := officialAccount.NewOfficialAccount(&officialAccount.UserConfig{ + AppID: config.Wechat.OfficialAccount.AppID, + Secret: config.Wechat.OfficialAccount.AppSecret, + Cache: cache, // 共享 Redis 实例 +}) +``` + +**Cache Key 格式**: +``` +powerwechat.access_token.{MD5(appid+secret)} +``` + +**选择理由**: +- ✅ 多实例共享 Access Token,避免重复获取(每日限额 2000 次) +- ✅ 使用项目现有 Redis 实例,无需额外部署 +- ✅ Token 过期自动刷新(PowerWeChat 内置处理) + +### 决策 7:错误处理 - 统一错误码 + +**新增错误码**(`pkg/errors/codes.go`): +```go +// 微信相关错误码(1040-1049) +CodeWechatOAuthFailed = 1040 // 微信 OAuth 授权失败 +CodeWechatUserInfoFailed = 1041 // 获取微信用户信息失败 +CodeWechatPayFailed = 1042 // 微信支付发起失败 +CodeWechatCallbackInvalid = 1043 // 微信回调签名验证失败 +``` + +**错误消息格式**: +```go +return errors.Wrap(CodeWechatOAuthFailed, err, "微信授权失败,请重试") +``` + +**选择理由**: +- ✅ 符合项目错误处理规范(`pkg/errors/`) +- ✅ 客户端可根据错误码区分微信相关错误 +- ✅ 敏感信息不对外暴露(详细错误写日志) + +## Risks / Trade-offs + +### 风险 1:微信 API 调用失败 + +**场景**:网络超时、微信服务异常、配置错误 + +**缓解措施**: +- 设置合理的 HTTP 超时(30 秒) +- 记录完整的请求/响应日志(`HttpDebug: true` 在测试环境) +- 错误消息包含 Request ID 便于排查 +- 支付失败时订单状态保持 `pending`,用户可重试 + +### 风险 2:证书文件管理 + +**场景**:证书过期、文件路径错误、权限问题 + +**缓解措施**: +- 启动时验证证书文件可读性,不通过则退出 +- 证书序列号配置错误时 PowerWeChat 会报错 +- 文档中说明证书获取和更新流程 +- 使用 Docker Secrets 或 Volume 挂载证书(避免镜像包含证书) + +### 风险 3:支付回调重复通知 + +**场景**:微信可能多次发送同一支付成功通知 + +**缓解措施**: +- ✅ 现有 `HandlePaymentCallback` 已实现幂等(条件更新:`WHERE id = ? AND payment_status = ?`) +- ✅ 已支付订单返回成功,不报错 +- 无需额外处理 + +### 风险 4:Access Token 缓存失效 + +**场景**:Redis 重启、缓存过期、网络问题 + +**缓解措施**: +- PowerWeChat 自动重新获取 Token(失败时重试) +- Redis 持久化配置确保重启后数据不丢失 +- Token 获取失败记录错误日志 + +### 权衡 1:配置复杂度 vs 安全性 + +**权衡**:微信支付需要大量配置项(AppID、商户号、密钥、证书等) + +**决策**:优先保证安全性 +- 敏感信息全部通过环境变量配置 +- 证书文件路径可配置,不硬编码 +- 提供完整的配置文档和示例 + +### 权衡 2:功能完整性 vs 实现成本 + +**权衡**:微信支付支持多种场景(Native、App、小程序等) + +**决策**:仅实现当前需求(H5 + JSAPI) +- Native、App 支付后续按需扩展 +- 架构设计预留扩展性(新增支付类型只需加端点) + +## Migration Plan + +### 部署步骤 + +1. **配置环境变量** + ```bash + export JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_ID="wx..." + export JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_SECRET="..." + export JUNHONG_WECHAT_PAYMENT_MCH_ID="..." + export JUNHONG_WECHAT_PAYMENT_API_V3_KEY="..." + export JUNHONG_WECHAT_PAYMENT_CERT_PATH="/app/certs/apiclient_cert.pem" + export JUNHONG_WECHAT_PAYMENT_KEY_PATH="/app/certs/apiclient_key.pem" + export JUNHONG_WECHAT_PAYMENT_SERIAL_NO="..." + export JUNHONG_WECHAT_PAYMENT_NOTIFY_URL="https://api.example.com/api/callback/wechat-pay" + ``` + +2. **挂载证书文件**(Docker) + ```yaml + volumes: + - ./wechat-certs:/app/certs:ro + ``` + +3. **验证配置** + - 启动服务,检查日志无配置错误 + - 调用健康检查端点(可选:新增 `/api/health/wechat` 验证微信 API 可达性) + +4. **微信后台配置** + - 公众号后台:设置 OAuth 回调域名 + - 商户平台:设置支付回调 URL 白名单 + +5. **灰度测试** + - 小范围用户测试微信登录和支付 + - 验证支付回调正常触发 + +### 回滚策略 + +- 配置错误:修改环境变量重启即可 +- 功能异常:移除微信支付选项,用户使用钱包支付 +- 数据库:无数据库变更,无需回滚 + +### 监控指标 + +- 微信 OAuth 成功率/失败率 +- 支付发起成功率/失败率 +- 支付回调接收数量/验证失败数量 +- Access Token 获取次数(监控是否频繁刷新) + +## Open Questions + +1. **证书更新流程**:商户证书每年需更新,是否需要热加载机制? + - 暂定:手动更新证书文件后重启服务(证书过期前提前通知) + +2. **微信用户信息更新**:用户在微信更新昵称/头像后,系统如何同步? + - 暂定:每次 OAuth 登录时更新用户信息 + - 后续可考虑定期同步任务 + +3. **支付超时处理**:订单创建后 30 分钟未支付,是否自动关闭? + - 暂定:前端超时提示用户,后端暂不自动关闭 + - 后续可使用 Asynq 延迟任务实现自动关闭 + +4. **测试环境配置**:如何在测试环境使用微信沙盒? + - PowerWeChat 支持沙盒环境(配置 `Http.BaseURI` 为沙盒地址) + - 需要微信商户平台申请沙盒权限 diff --git a/openspec/changes/wechat-official-account-payment-integration/proposal.md b/openspec/changes/wechat-official-account-payment-integration/proposal.md new file mode 100644 index 0000000..2fbe82d --- /dev/null +++ b/openspec/changes/wechat-official-account-payment-integration/proposal.md @@ -0,0 +1,62 @@ +# 微信公众号与微信支付集成提案 + +## Why + +当前系统的个人客户(C 端用户)无法通过微信公众号登录和使用微信支付功能,导致用户体验不完整。系统已经预留了微信相关的数据模型(`wx_open_id`, `wx_union_id`)和支付回调接口,但缺少真实的微信 SDK 集成。为了满足个人客户的核心使用场景(微信扫码登录、钱包充值、购买套餐),需要立即对接微信公众号和微信支付能力。 + +## What Changes + +- **新增微信公众号 OAuth 认证**:实现用户通过微信授权码获取 OpenID/UnionID 和基本信息(昵称、头像) +- **新增微信支付发起功能**: + - H5 支付:支持移动端浏览器外唤起微信支付 + - JSAPI 支付:支持微信内网页支付 +- **完善微信支付回调处理**:在现有回调接口基础上补充签名验证和完整的支付状态同步 +- **新增微信配置管理**:在配置文件中增加微信公众号和支付的必要参数(AppID、Secret、商户号、证书等) +- **集成 PowerWeChat SDK**:使用 `github.com/ArtisanCloud/PowerWeChat/v3` 实现微信 API 调用 + +## Capabilities + +### New Capabilities + +- `wechat-official-account`: 微信公众号能力(OAuth 认证、获取用户信息) +- `wechat-payment`: 微信支付能力(H5 支付、JSAPI 支付、支付回调) + +### Modified Capabilities + +无。现有功能不涉及需求级别的变更。 + +## Impact + +**影响的代码模块**: +- `pkg/wechat/`: 微信服务接口实现(替换 Mock 为真实实现) +- `pkg/config/`: 新增微信配置项 +- `internal/service/personal_customer/`: 补充微信 OAuth 登录逻辑 +- `internal/service/order/`: 新增微信支付发起和回调验证 +- `internal/handler/app/`: 微信 OAuth 相关 API 端点 +- `internal/handler/h5/`: 微信支付发起 API 端点 +- `internal/handler/callback/`: 补充支付回调签名验证 + +**依赖变更**: +- 新增依赖:`github.com/ArtisanCloud/PowerWeChat/v3` + +**配置变更**: +- 新增环境变量: + - `JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_ID` + - `JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_SECRET` + - `JUNHONG_WECHAT_PAYMENT_MCH_ID` + - `JUNHONG_WECHAT_PAYMENT_API_V3_KEY` + - 等(详见配置设计) + +**API 变更**: +- 修改端点:`POST /api/c/v1/bind-wechat`(从"not implemented"变为可用) +- 新增端点: + - `POST /api/h5/orders/:id/wechat-pay/jsapi`(JSAPI 支付) + - `POST /api/h5/orders/:id/wechat-pay/h5`(H5 支付) +- 增强端点:`POST /api/callback/wechat-pay`(补充签名验证) + +**数据库变更**: +- 无。现有表结构已满足需求。 + +**部署影响**: +- 需要提供微信商户证书文件(`apiclient_cert.pem`、`apiclient_key.pem`) +- 需要配置微信回调域名白名单 diff --git a/openspec/changes/wechat-official-account-payment-integration/specs/wechat-official-account/spec.md b/openspec/changes/wechat-official-account-payment-integration/specs/wechat-official-account/spec.md new file mode 100644 index 0000000..9dbcace --- /dev/null +++ b/openspec/changes/wechat-official-account-payment-integration/specs/wechat-official-account/spec.md @@ -0,0 +1,147 @@ +# 微信公众号能力规格说明 + +## ADDED Requirements + +### Requirement: 系统必须支持微信 OAuth 2.0 授权登录 + +系统 SHALL 实现微信公众号 OAuth 2.0 授权流程,允许个人客户通过微信授权获取用户身份信息。 + +#### Scenario: 用户首次通过微信授权码登录成功 +- **WHEN** 用户在前端完成微信授权,后端接收到有效的授权码(code) +- **THEN** 系统调用微信 API 获取用户 OpenID、UnionID 和基本信息(昵称、头像) +- **THEN** 系统在数据库中创建新的个人客户记录,保存微信 OpenID 和 UnionID +- **THEN** 系统生成 JWT Token 并返回给客户端 + +#### Scenario: 已存在的微信用户再次登录 +- **WHEN** 用户通过微信授权码登录,且该 OpenID 已存在于数据库 +- **THEN** 系统查询到现有客户记录 +- **THEN** 系统更新客户的昵称和头像信息(保持最新) +- **THEN** 系统生成 JWT Token 并返回给客户端 + +#### Scenario: 微信授权码无效或过期 +- **WHEN** 用户提交的授权码无效、过期或已被使用 +- **THEN** 系统调用微信 API 失败 +- **THEN** 系统返回错误码 1040(微信 OAuth 授权失败)和中文错误消息"微信授权失败,请重试" + +#### Scenario: 微信 API 服务不可用 +- **WHEN** 调用微信 API 时发生网络超时或微信服务异常 +- **THEN** 系统记录详细的错误日志(包含 Request ID) +- **THEN** 系统返回错误码 1040(微信 OAuth 授权失败)和用户友好的中文错误消息 + +### Requirement: 系统必须支持已有账号绑定微信 + +系统 SHALL 允许已注册的个人客户(通过手机号登录)绑定微信账号。 + +#### Scenario: 用户成功绑定微信账号 +- **WHEN** 已登录用户提交有效的微信授权码,且该用户尚未绑定微信 +- **THEN** 系统调用微信 API 获取 OpenID 和 UnionID +- **THEN** 系统验证该 OpenID 未被其他用户绑定 +- **THEN** 系统更新该用户的 wx_open_id 和 wx_union_id 字段 +- **THEN** 系统返回成功响应和更新后的用户信息 + +#### Scenario: 尝试绑定已被使用的微信账号 +- **WHEN** 用户提交的微信授权码对应的 OpenID 已被其他用户绑定 +- **THEN** 系统返回错误码 1036(微信账号已被绑定)和中文错误消息"该微信账号已绑定其他用户" + +#### Scenario: 用户已绑定微信后再次绑定 +- **WHEN** 已绑定微信的用户再次提交微信授权码 +- **THEN** 系统更新用户的昵称和头像信息 +- **THEN** 系统返回成功响应(允许更新信息,不报错) + +### Requirement: 系统必须支持通过 OpenID/UnionID 查询用户 + +系统 MUST 提供通过微信 OpenID 或 UnionID 查询个人客户的能力。 + +#### Scenario: 通过 OpenID 查询到用户 +- **WHEN** 调用 Store 层的 GetByWxOpenID 方法,传入有效的 OpenID +- **THEN** 系统返回对应的个人客户记录 + +#### Scenario: 通过 OpenID 查询不到用户 +- **WHEN** 调用 Store 层的 GetByWxOpenID 方法,传入不存在的 OpenID +- **THEN** 系统返回 nil(无错误,表示用户不存在) + +#### Scenario: 通过 UnionID 查询到用户 +- **WHEN** 调用 Store 层的 GetByWxUnionID 方法,传入有效的 UnionID +- **THEN** 系统返回对应的个人客户记录 + +### Requirement: 系统必须实现 Access Token 中控 + +系统 MUST 使用 Redis 缓存微信 Access Token,支持多实例共享,避免重复获取导致超出每日限额。 + +#### Scenario: 首次获取 Access Token +- **WHEN** 系统首次调用微信 API 需要 Access Token +- **THEN** 系统调用微信 API 获取 Access Token +- **THEN** 系统将 Token 存储到 Redis(Key: `powerwechat.access_token.{MD5(appid+secret)}`,TTL: 7200秒) +- **THEN** 系统使用该 Token 完成 API 调用 + +#### Scenario: 从 Redis 缓存获取 Token +- **WHEN** 系统调用微信 API,Redis 中存在有效的 Access Token +- **THEN** 系统直接使用缓存的 Token,不调用微信 API 获取新 Token + +#### Scenario: Access Token 过期后自动刷新 +- **WHEN** 系统使用缓存的 Token 调用微信 API 返回 Token 过期错误 +- **THEN** 系统自动重新获取 Access Token +- **THEN** 系统更新 Redis 缓存 +- **THEN** 系统重试原 API 调用 + +### Requirement: API 必须遵循统一响应格式 + +所有微信相关 API MUST 返回统一的 JSON 响应格式。 + +#### Scenario: 成功响应格式 +- **WHEN** API 调用成功 +- **THEN** 系统返回 HTTP 200 和以下 JSON 格式: + ```json + { + "code": 0, + "message": "success", + "data": { /* 业务数据 */ }, + "timestamp": 1706789012345 + } + ``` + +#### Scenario: 失败响应格式 +- **WHEN** API 调用失败(参数错误、业务逻辑错误、微信 API 错误) +- **THEN** 系统返回对应的 HTTP 状态码(400/401/500)和以下 JSON 格式: + ```json + { + "code": 1040, + "message": "微信授权失败,请重试", + "data": null, + "timestamp": 1706789012345 + } + ``` + +### Requirement: 系统必须记录完整的日志 + +所有微信 API 调用 MUST 记录完整的日志,便于排查问题。 + +#### Scenario: 记录微信 API 请求日志 +- **WHEN** 系统调用微信 API +- **THEN** 系统记录 INFO 级别日志,包含:Request ID、API 端点、请求参数(脱敏) + +#### Scenario: 记录微信 API 响应日志 +- **WHEN** 系统收到微信 API 响应 +- **THEN** 系统记录 INFO 级别日志,包含:Request ID、响应状态、响应时间、关键字段 + +#### Scenario: 记录微信 API 错误日志 +- **WHEN** 微信 API 调用失败 +- **THEN** 系统记录 ERROR 级别日志,包含:Request ID、错误码、错误消息、完整的错误详情 + +### Requirement: 系统必须支持配置管理 + +微信公众号相关配置 MUST 通过 Viper + 环境变量管理。 + +#### Scenario: 从环境变量读取配置 +- **WHEN** 系统启动时 +- **THEN** 系统从环境变量读取以下配置: + - `JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_ID`(公众号 AppID) + - `JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_SECRET`(公众号 AppSecret) + - `JUNHONG_WECHAT_OFFICIAL_ACCOUNT_TOKEN`(回调 Token) + - `JUNHONG_WECHAT_OFFICIAL_ACCOUNT_AES_KEY`(回调加密密钥) + - `JUNHONG_WECHAT_OFFICIAL_ACCOUNT_OAUTH_REDIRECT_URL`(OAuth 回调地址) + +#### Scenario: 配置缺失时启动失败 +- **WHEN** 必填配置项(AppID、AppSecret)缺失 +- **THEN** 系统记录 FATAL 级别日志 +- **THEN** 系统启动失败并退出 diff --git a/openspec/changes/wechat-official-account-payment-integration/specs/wechat-payment/spec.md b/openspec/changes/wechat-official-account-payment-integration/specs/wechat-payment/spec.md new file mode 100644 index 0000000..6ca4fbe --- /dev/null +++ b/openspec/changes/wechat-official-account-payment-integration/specs/wechat-payment/spec.md @@ -0,0 +1,229 @@ +#微信支付能力规格说明 + +## ADDED Requirements + +### Requirement: 系统必须支持 JSAPI 支付 + +系统 MUST 支持在微信内网页发起 JSAPI 支付,用户在微信客户端内完成支付。 + +#### Scenario: 用户在微信内成功发起支付 +- **WHEN** 用户在微信内选择订单并点击"微信支付",前端调用 `/api/h5/orders/:id/wechat-pay/jsapi` 端点,传入用户 OpenID +- **THEN** 系统验证订单状态为 `pending`(待支付) +- **THEN** 系统调用 PowerWeChat SDK 的 `Order.JSAPITransaction()` 创建支付订单 +- **THEN** 系统生成 JSSDK 支付配置(包含 prepay_id、timestamp、nonceStr、paySign) +- **THEN** 系统返回支付配置给前端 +- **THEN** 前端调用 `wx.requestPayment()` 唤起微信支付 + +#### Scenario: 订单不存在或状态不正确 +- **WHEN** 用户提交的订单 ID 不存在,或订单状态不是 `pending` +- **THEN** 系统返回错误码 1000(参数错误)和中文错误消息"订单不存在或不可支付" + +#### Scenario: 订单金额为 0 +- **WHEN** 订单金额为 0 元 +- **THEN** 系统跳过微信支付,直接更新订单状态为 `paid` +- **THEN** 系统触发套餐激活和分佣计算 + +#### Scenario: 微信支付 API 调用失败 +- **WHEN** 调用 PowerWeChat SDK 创建支付订单时失败(网络超时、参数错误等) +- **THEN** 系统记录详细的错误日志(Request ID、错误码、错误消息) +- **THEN** 系统返回错误码 1042(微信支付发起失败)和中文错误消息"支付发起失败,请重试" + +### Requirement: 系统必须支持 H5 支付 + +系统 MUST 支持在移动端浏览器外发起 H5 支付,用户可唤起微信 APP 完成支付。 + +#### Scenario: 用户在浏览器中成功发起 H5 支付 +- **WHEN** 用户在移动端浏览器选择订单并点击"微信支付",前端调用 `/api/h5/orders/:id/wechat-pay/h5` 端点,传入用户终端 IP 和场景信息 +- **THEN** 系统验证订单状态为 `pending` +- **THEN** 系统调用 PowerWeChat SDK 的 `Order.TransactionH5()` 创建 H5 支付订单 +- **THEN** 系统返回微信支付跳转 URL(h5_url) +- **THEN** 前端跳转到该 URL,用户在微信 H5 页面完成支付 + +#### Scenario: 缺少必填参数 +- **WHEN** 请求缺少 `payer_client_ip` 或 `scene_info` 参数 +- **THEN** 系统返回错误码 1000(参数错误)和中文错误消息"缺少必填参数" + +#### Scenario: 订单已支付 +- **WHEN** 用户提交的订单状态已是 `paid` +- **THEN** 系统返回错误码 1000(参数错误)和中文错误消息"订单已支付" + +### Requirement: 系统必须支持微信支付回调 + +系统 SHALL 接收并处理微信支付成功通知,更新订单状态并触发后续业务逻辑。 + +#### Scenario: 接收到合法的支付成功通知 +- **WHEN** 微信回调 `/api/callback/wechat-pay` 端点,传入支付成功通知 +- **THEN** PowerWeChat SDK 自动验证回调签名 +- **THEN** 系统解析通知内容,提取商户订单号(out_trade_no) +- **THEN** 系统调用 `orderService.HandlePaymentCallback()` 更新订单状态为 `paid`(幂等处理) +- **THEN** 系统触发套餐激活和分佣计算 +- **THEN** 系统返回 HTTP 200 和 `{"return_code": "SUCCESS"}` 给微信 + +#### Scenario: 接收到重复的支付通知 +- **WHEN** 微信多次发送同一订单的支付成功通知 +- **THEN** 系统通过幂等检查识别订单已支付 +- **THEN** 系统直接返回成功响应,不重复处理业务逻辑 + +#### Scenario: 回调签名验证失败 +- **WHEN** 微信回调的签名无效或被篡改 +- **THEN** PowerWeChat SDK 自动拒绝该请求 +- **THEN** 系统记录 ERROR 级别日志(Request ID、签名验证失败详情) +- **THEN** 系统返回 HTTP 400 错误 + +#### Scenario: 订单号不存在 +- **WHEN** 微信回调中的商户订单号在系统中不存在 +- **THEN** 系统记录 ERROR 级别日志 +- **THEN** 系统返回失败响应给微信(让微信稍后重试) + +#### Scenario: 支付回调处理失败 +- **WHEN** 系统在处理支付回调时发生数据库错误或其他异常 +- **THEN** 系统记录 ERROR 级别日志(Request ID、错误详情) +- **THEN** 系统返回失败响应给微信(让微信稍后重试) + +### Requirement: 支付回调处理必须幂等 + +系统 MUST 确保多次接收到同一支付通知时,业务逻辑只执行一次。 + +#### Scenario: 订单状态条件更新 +- **WHEN** 系统更新订单状态为 `paid` +- **THEN** 系统使用条件更新:`UPDATE ... WHERE id = ? AND payment_status = ?`(只更新状态为 pending 的订单) +- **THEN** 如果更新影响行数为 0,系统检查当前订单状态: + - 如果已支付,返回成功(幂等) + - 如果已取消/已退款,返回错误 + +#### Scenario: 套餐激活幂等性 +- **WHEN** 订单支付成功后触发套餐激活 +- **THEN** 系统检查 `tb_package_usage` 表是否已存在该订单的激活记录 +- **THEN** 如果已存在,跳过激活逻辑(幂等) + +### Requirement: 系统必须支持查询微信支付订单 + +系统 SHALL 支持根据商户订单号查询微信支付订单状态。 + +#### Scenario: 查询到支付成功的订单 +- **WHEN** 调用 `PaymentService.Order.QueryByOutTradeNumber()` 查询订单 +- **THEN** 系统返回订单详情,包含: + - 订单号(out_trade_no) + - 微信支付单号(transaction_id) + - 支付状态(trade_state: SUCCESS) + - 支付时间(success_time) + - 支付金额(total) + +#### Scenario: 查询到待支付的订单 +- **WHEN** 查询的订单尚未支付 +- **THEN** 系统返回订单详情,支付状态为 `NOTPAY` + +#### Scenario: 查询不存在的订单 +- **WHEN** 查询的商户订单号在微信侧不存在 +- **THEN** PowerWeChat SDK 返回错误 +- **THEN** 系统记录日志并返回错误码 1042 + +### Requirement: 系统必须支持关闭未支付订单 + +系统 SHALL 支持关闭超时未支付的微信订单。 + +#### Scenario: 成功关闭未支付订单 +- **WHEN** 调用 `PaymentService.Order.Close()` 关闭订单,传入商户订单号 +- **THEN** 系统调用微信 API 关闭订单 +- **THEN** 系统返回成功响应 + +#### Scenario: 尝试关闭已支付订单 +- **WHEN** 调用关闭接口,但订单已支付 +- **THEN** 微信 API 返回错误(订单已支付,无法关闭) +- **THEN** 系统记录日志并返回错误 + +#### Scenario: 订单创建后 5 分钟内关闭 +- **WHEN** 订单创建后不足 5 分钟就调用关闭接口 +- **THEN** 系统可能因订单状态同步不及时而关闭失败 +- **THEN** 系统建议在创建 5 分钟后再关闭 + +### Requirement: 系统必须支持配置管理 + +微信支付相关配置 MUST 通过 Viper + 环境变量管理。 + +#### Scenario: 从环境变量读取配置 +- **WHEN** 系统启动时 +- **THEN** 系统从环境变量读取以下配置: + - `JUNHONG_WECHAT_PAYMENT_APP_ID`(支付 AppID) + - `JUNHONG_WECHAT_PAYMENT_MCH_ID`(商户号) + - `JUNHONG_WECHAT_PAYMENT_API_V3_KEY`(API V3 密钥) + - `JUNHONG_WECHAT_PAYMENT_API_V2_KEY`(API V2 密钥) + - `JUNHONG_WECHAT_PAYMENT_CERT_PATH`(商户证书路径) + - `JUNHONG_WECHAT_PAYMENT_KEY_PATH`(商户私钥路径) + - `JUNHONG_WECHAT_PAYMENT_SERIAL_NO`(证书序列号) + - `JUNHONG_WECHAT_PAYMENT_NOTIFY_URL`(支付回调地址) + +#### Scenario: 证书文件不存在时启动失败 +- **WHEN** 配置的证书路径指向的文件不存在或无读取权限 +- **THEN** 系统记录 FATAL 级别日志 +- **THEN** 系统启动失败并退出 + +#### Scenario: 必填配置缺失时启动失败 +- **WHEN** 必填配置项(AppID、商户号、API 密钥)缺失 +- **THEN** 系统记录 FATAL 级别日志 +- **THEN** 系统启动失败并退出 + +### Requirement: API 必须遵循统一响应格式 + +所有微信支付相关 API MUST 返回统一的 JSON 响应格式(同微信公众号规范)。 + +#### Scenario: 支付发起成功响应 +- **WHEN** JSAPI 支付发起成功 +- **THEN** 系统返回 HTTP 200 和以下格式: + ```json + { + "code": 0, + "message": "success", + "data": { + "prepay_id": "wx...", + "pay_config": { + "appId": "...", + "timeStamp": "...", + "nonceStr": "...", + "package": "prepay_id=...", + "signType": "RSA", + "paySign": "..." + } + }, + "timestamp": 1706789012345 + } + ``` + +#### Scenario: H5 支付发起成功响应 +- **WHEN** H5 支付发起成功 +- **THEN** 系统返回 HTTP 200 和以下格式: + ```json + { + "code": 0, + "message": "success", + "data": { + "h5_url": "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?..." + }, + "timestamp": 1706789012345 + } + ``` + +### Requirement: 系统必须记录完整的日志 + +所有微信支付 API 调用 MUST 记录完整的日志。 + +#### Scenario: 记录支付发起日志 +- **WHEN** 系统调用微信支付 API 创建订单 +- **THEN** 系统记录 INFO 级别日志,包含:Request ID、订单号、支付类型(JSAPI/H5)、订单金额 + +#### Scenario: 记录支付回调日志 +- **WHEN** 系统收到微信支付回调 +- **THEN** 系统记录 INFO 级别日志,包含:Request ID、订单号、微信支付单号、支付时间 + +#### Scenario: 记录支付错误日志 +- **WHEN** 微信支付 API 调用失败 +- **THEN** 系统记录 ERROR 级别日志,包含:Request ID、订单号、错误码、错误消息、完整的错误详情 + +### Requirement: 系统必须支持 Redis 缓存 + +微信支付的 Access Token MUST 使用 Redis 缓存(与微信公众号共享同一缓存机制)。 + +#### Scenario: Token 缓存与公众号共享 +- **WHEN** 微信支付和公众号使用相同的 AppID +- **THEN** 系统复用同一个 Redis Cache 实例 +- **THEN** Token 缓存 Key 相同,避免重复获取 diff --git a/openspec/changes/wechat-official-account-payment-integration/tasks.md b/openspec/changes/wechat-official-account-payment-integration/tasks.md new file mode 100644 index 0000000..8825ce5 --- /dev/null +++ b/openspec/changes/wechat-official-account-payment-integration/tasks.md @@ -0,0 +1,257 @@ +# 微信公众号与微信支付集成 - 任务清单 + +## 1. 依赖安装和配置准备 + +- [ ] 1.1 安装 PowerWeChat v3 SDK:`go get -u github.com/ArtisanCloud/PowerWeChat/v3` +- [ ] 1.2 在 `pkg/config/defaults/config.yaml` 中新增微信配置结构(wechat.official_account、wechat.payment) +- [ ] 1.3 在 `pkg/config/config.go` 中定义微信配置结构体(WechatConfig、OfficialAccountConfig、PaymentConfig) +- [ ] 1.4 在 `docs/environment-variables.md` 中添加微信相关环境变量说明 + +## 2. 错误码定义 + +- [ ] 2.1 在 `pkg/errors/codes.go` 中新增微信相关错误码(1040-1049) +- [ ] 2.2 在 `pkg/errors/messages.go` 中添加对应的中英文错误消息 + +## 3. 微信服务基础设施(pkg/wechat) + +- [ ] 3.1 实现 `pkg/wechat/config.go` - 创建 PowerWeChat 配置初始化函数 +- [ ] 3.2 实现 `pkg/wechat/official_account.go` - OfficialAccount 服务实现 + - [ ] 3.2.1 实现 `NewOfficialAccountService()` 初始化函数(集成 Redis 缓存) + - [ ] 3.2.2 实现 `GetUserInfo(ctx, code)` 方法(调用 OAuth.UserFromCode) + - [ ] 3.2.3 实现 `GetUserInfoByToken(ctx, accessToken, openID)` 方法 +- [ ] 3.3 实现 `pkg/wechat/payment.go` - Payment 服务实现 + - [ ] 3.3.1 实现 `NewPaymentService()` 初始化函数(集成 Redis 缓存) + - [ ] 3.3.2 实现 `CreateJSAPIOrder(ctx, params)` 方法(JSAPI 支付下单) + - [ ] 3.3.3 实现 `CreateH5Order(ctx, params)` 方法(H5 支付下单) + - [ ] 3.3.4 实现 `QueryOrder(ctx, orderNo)` 方法(查询订单状态) + - [ ] 3.3.5 实现 `CloseOrder(ctx, orderNo)` 方法(关闭订单) + - [ ] 3.3.6 实现 `HandlePaymentNotify(request, callback)` 方法(支付回调处理) +- [ ] 3.4 实现 `pkg/wechat/wechat.go` - 更新 Service 接口定义(保持向后兼容) +- [ ] 3.5 删除 `pkg/wechat/mock.go`(替换为真实实现) + +## 4. 配置验证和启动检查 + +- [ ] 4.1 在 `cmd/api/main.go` 中添加微信配置验证逻辑 +- [ ] 4.2 验证证书文件存在性和可读性(cert_path、key_path) +- [ ] 4.3 验证必填配置项(AppID、AppSecret、商户号、API 密钥) +- [ ] 4.4 配置缺失或证书文件不存在时记录 FATAL 日志并退出 + +## 5. DTO 定义 + +- [ ] 5.1 在 `internal/model/dto/wechat_dto.go` 中定义微信相关 DTO + - [ ] 5.1.1 定义 `WechatOAuthRequest`(code) + - [ ] 5.1.2 定义 `WechatOAuthResponse`(token、customer) + - [ ] 5.1.3 定义 `WechatPayJSAPIRequest`(openid) + - [ ] 5.1.4 定义 `WechatPayJSAPIResponse`(prepay_id、pay_config) + - [ ] 5.1.5 定义 `WechatPayH5Request`(scene_info) + - [ ] 5.1.6 定义 `WechatPayH5Response`(h5_url) + - [ ] 5.1.7 添加 `description` 标签和验证标签(validate) + +## 6. Service 层实现 - 个人客户服务 + +- [ ] 6.1 修改 `internal/service/personal_customer/service.go` + - [ ] 6.1.1 添加 `wechatService wechat.Service` 字段(依赖注入) + - [ ] 6.1.2 实现 `WechatOAuthLogin(ctx, code)` 方法 + - [ ] 6.1.2.1 调用 `wechatService.GetUserInfo()` 获取 OpenID/UnionID + - [ ] 6.1.2.2 通过 OpenID 查询客户(Store.GetByWxOpenID) + - [ ] 6.1.2.3 如果客户不存在,创建新客户 + - [ ] 6.1.2.4 如果客户存在,更新昵称和头像 + - [ ] 6.1.2.5 生成 JWT Token 并返回 + - [ ] 6.1.3 修改现有 `BindWechat(ctx, customerID, code)` 方法 + - [ ] 6.1.3.1 调用 `wechatService.GetUserInfo()` 获取 OpenID/UnionID + - [ ] 6.1.3.2 验证 OpenID 未被其他用户绑定 + - [ ] 6.1.3.3 更新客户的 wx_open_id 和 wx_union_id + - [ ] 6.1.3.4 更新昵称和头像 + +## 7. Service 层实现 - 订单服务 + +- [ ] 7.1 修改 `internal/service/order/service.go` + - [ ] 7.1.1 添加 `wechatPayment wechat.PaymentService` 字段(依赖注入) + - [ ] 7.1.2 实现 `WechatPayJSAPI(ctx, orderID, openID)` 方法 + - [ ] 7.1.2.1 查询订单并验证状态为 `pending` + - [ ] 7.1.2.2 调用 `wechatPayment.CreateJSAPIOrder()` 创建支付订单 + - [ ] 7.1.2.3 生成 JSSDK 支付配置 + - [ ] 7.1.2.4 返回 prepay_id 和 pay_config + - [ ] 7.1.3 实现 `WechatPayH5(ctx, orderID, sceneInfo)` 方法 + - [ ] 7.1.3.1 查询订单并验证状态为 `pending` + - [ ] 7.1.3.2 调用 `wechatPayment.CreateH5Order()` 创建支付订单 + - [ ] 7.1.3.3 返回 h5_url + - [ ] 7.1.4 修改现有 `HandlePaymentCallback(ctx, orderNo, paymentMethod)` 方法(保持幂等逻辑不变) + +## 8. Handler 层实现 - 个人客户 Handler + +- [ ] 8.1 修改 `internal/handler/app/personal_customer.go` + - [ ] 8.1.1 实现 `WechatOAuthLogin(c *fiber.Ctx)` 方法(POST /api/c/v1/wechat/auth) + - [ ] 8.1.1.1 解析请求参数(code) + - [ ] 8.1.1.2 调用 `service.WechatOAuthLogin()` + - [ ] 8.1.1.3 返回 JWT Token 和客户信息 + - [ ] 8.1.2 修改 `BindWechat(c *fiber.Ctx)` 方法(POST /api/c/v1/bind-wechat) + - [ ] 8.1.2.1 从 context 获取 customer_id + - [ ] 8.1.2.2 解析请求参数(code) + - [ ] 8.1.2.3 调用 `service.BindWechat()` + - [ ] 8.1.2.4 返回成功响应 + +## 9. Handler 层实现 - H5 订单 Handler + +- [ ] 9.1 修改 `internal/handler/h5/order.go` + - [ ] 9.1.1 实现 `WechatPayJSAPI(c *fiber.Ctx)` 方法(POST /api/h5/orders/:id/wechat-pay/jsapi) + - [ ] 9.1.1.1 解析路径参数(order_id) + - [ ] 9.1.1.2 解析请求参数(openid) + - [ ] 9.1.1.3 调用 `orderService.WechatPayJSAPI()` + - [ ] 9.1.1.4 返回支付配置 + - [ ] 9.1.2 实现 `WechatPayH5(c *fiber.Ctx)` 方法(POST /api/h5/orders/:id/wechat-pay/h5) + - [ ] 9.1.2.1 解析路径参数(order_id) + - [ ] 9.1.2.2 解析请求参数(scene_info) + - [ ] 9.1.2.3 调用 `orderService.WechatPayH5()` + - [ ] 9.1.2.4 返回 h5_url + +## 10. Handler 层实现 - 支付回调 Handler + +- [ ] 10.1 修改 `internal/handler/callback/payment.go` + - [ ] 10.1.1 添加 `wechatPayment wechat.PaymentService` 字段(依赖注入) + - [ ] 10.1.2 重构 `WechatPayCallback(c *fiber.Ctx)` 方法 + - [ ] 10.1.2.1 调用 `wechatPayment.HandlePaymentNotify()` 自动验证签名 + - [ ] 10.1.2.2 在回调函数中提取订单号 + - [ ] 10.1.2.3 调用 `orderService.HandlePaymentCallback()` 更新订单状态 + - [ ] 10.1.2.4 返回 PowerWeChat 格式的响应 + +## 11. 路由注册 + +- [ ] 11.1 修改 `internal/routes/personal.go` + - [ ] 11.1.1 添加公开路由:POST /api/c/v1/wechat/auth(WechatOAuthLogin) + - [ ] 11.1.2 保留现有认证路由:POST /api/c/v1/bind-wechat(BindWechat) +- [ ] 11.2 修改 `internal/routes/order.go` + - [ ] 11.2.1 添加 H5 认证路由:POST /api/h5/orders/:id/wechat-pay/jsapi + - [ ] 11.2.2 添加 H5 认证路由:POST /api/h5/orders/:id/wechat-pay/h5 + - [ ] 11.2.3 保留回调路由(无认证):POST /api/callback/wechat-pay + +## 12. 依赖注入和初始化 + +- [ ] 12.1 修改 `internal/bootstrap/services.go` + - [ ] 12.1.1 初始化 `wechat.OfficialAccountService`(传入 config、Redis client、logger) + - [ ] 12.1.2 初始化 `wechat.PaymentService`(传入 config、Redis client、logger) + - [ ] 12.1.3 将微信服务注入到 `PersonalCustomerService` + - [ ] 12.1.4 将微信支付服务注入到 `OrderService` +- [ ] 12.2 修改 `internal/bootstrap/handlers.go` + - [ ] 12.2.1 将微信支付服务注入到 `PaymentHandler` + +## 13. 文档生成器更新 + +- [ ] 13.1 修改 `cmd/api/docs.go` + - [ ] 13.1.1 在 `handlers` 结构体中添加新 Handler 的占位符(如需要) + - [ ] 13.1.2 更新文档路由注册 +- [ ] 13.2 修改 `cmd/gendocs/main.go` + - [ ] 13.2.1 同步更新文档生成器的 Handler 初始化 + +## 14. 单元测试 + +- [ ] 14.1 测试 `pkg/wechat/official_account.go` + - [ ] 14.1.1 测试 `GetUserInfo()` 成功获取用户信息 + - [ ] 14.1.2 测试授权码无效时的错误处理 + - [ ] 14.1.3 测试 Access Token 缓存机制 +- [ ] 14.2 测试 `pkg/wechat/payment.go` + - [ ] 14.2.1 测试 `CreateJSAPIOrder()` 成功创建订单 + - [ ] 14.2.2 测试 `CreateH5Order()` 成功创建订单 + - [ ] 14.2.3 测试 `HandlePaymentNotify()` 签名验证 + - [ ] 14.2.4 测试支付回调幂等性 +- [ ] 14.3 测试 `internal/service/personal_customer/service.go` + - [ ] 14.3.1 测试 `WechatOAuthLogin()` 首次登录创建客户 + - [ ] 14.3.2 测试 `WechatOAuthLogin()` 已有客户更新信息 + - [ ] 14.3.3 测试 `BindWechat()` 成功绑定 + - [ ] 14.3.4 测试 `BindWechat()` OpenID 已被绑定 +- [ ] 14.4 测试 `internal/service/order/service.go` + - [ ] 14.4.1 测试 `WechatPayJSAPI()` 成功发起支付 + - [ ] 14.4.2 测试 `WechatPayH5()` 成功发起支付 + - [ ] 14.4.3 测试订单状态不正确时的错误处理 + +## 15. 集成测试 + +- [ ] 15.1 测试个人客户微信登录完整流程 + - [ ] 15.1.1 测试 `POST /api/c/v1/wechat/auth` 端点(Mock 微信 OAuth) + - [ ] 15.1.2 验证返回 JWT Token 和客户信息 + - [ ] 15.1.3 验证数据库中客户记录正确创建/更新 +- [ ] 15.2 测试微信绑定流程 + - [ ] 15.2.1 测试 `POST /api/c/v1/bind-wechat` 端点 + - [ ] 15.2.2 验证绑定成功后 wx_open_id 更新 +- [ ] 15.3 测试 JSAPI 支付流程 + - [ ] 15.3.1 测试 `POST /api/h5/orders/:id/wechat-pay/jsapi` 端点 + - [ ] 15.3.2 验证返回 prepay_id 和 pay_config +- [ ] 15.4 测试 H5 支付流程 + - [ ] 15.4.1 测试 `POST /api/h5/orders/:id/wechat-pay/h5` 端点 + - [ ] 15.4.2 验证返回 h5_url +- [ ] 15.5 测试微信支付回调流程 + - [ ] 15.5.1 测试 `POST /api/callback/wechat-pay` 端点(Mock 微信签名) + - [ ] 15.5.2 验证订单状态更新为 `paid` + - [ ] 15.5.3 验证套餐激活和分佣计算触发 + - [ ] 15.5.4 测试重复回调的幂等性 + +## 16. 代码质量检查 + +- [ ] 16.1 运行 `go fmt` 格式化所有新增代码 +- [ ] 16.2 运行 `go vet` 检查代码问题 +- [ ] 16.3 运行 `golangci-lint` 检查代码规范 +- [ ] 16.4 检查所有注释使用中文 +- [ ] 16.5 检查所有错误处理使用 `pkg/errors` +- [ ] 16.6 检查所有常量定义在 `pkg/constants/` + +## 17. 文档更新 + +- [ ] 17.1 创建 `docs/wechat-integration/使用指南.md` + - [ ] 17.1.1 微信公众号配置说明(AppID、AppSecret、OAuth 回调域名) + - [ ] 17.1.2 微信支付配置说明(商户号、证书、回调 URL) + - [ ] 17.1.3 证书文件获取和安装流程 + - [ ] 17.1.4 环境变量配置示例 +- [ ] 17.2 创建 `docs/wechat-integration/API 文档.md` + - [ ] 17.2.1 微信 OAuth 登录 API 说明 + - [ ] 17.2.2 微信支付 API 说明(JSAPI + H5) + - [ ] 17.2.3 请求/响应示例 +- [ ] 17.3 更新 `README.md` + - [ ] 17.3.1 在核心功能章节添加微信集成说明 + - [ ] 17.3.2 更新技术栈章节(新增 PowerWeChat) +- [ ] 17.4 更新 `docs/environment-variables.md` + - [ ] 17.4.1 添加所有微信相关环境变量 +- [ ] 17.5 更新 `openspec/AGENTS.md`(如需要) + - [ ] 17.5.1 添加微信集成相关的开发规范 + +## 18. 部署准备 + +- [ ] 18.1 准备测试环境配置 + - [ ] 18.1.1 获取微信测试公众号 AppID 和 AppSecret + - [ ] 18.1.2 获取微信支付测试商户号和证书 + - [ ] 18.1.3 配置微信后台白名单(OAuth 回调域名、支付回调 URL) +- [ ] 18.2 准备生产环境配置 + - [ ] 18.2.1 获取正式公众号 AppID 和 AppSecret + - [ ] 18.2.2 获取正式商户号和证书 + - [ ] 18.2.3 配置生产环境微信后台白名单 +- [ ] 18.3 创建证书管理文档 + - [ ] 18.3.1 证书过期提醒机制 + - [ ] 18.3.2 证书更新流程 + - [ ] 18.3.3 证书存储安全规范 + +## 19. 验证和测试 + +- [ ] 19.1 本地开发环境验证 + - [ ] 19.1.1 验证配置加载正确 + - [ ] 19.1.2 验证证书文件读取正常 + - [ ] 19.1.3 验证 Redis 缓存工作正常 +- [ ] 19.2 测试环境集成测试 + - [ ] 19.2.1 使用真实微信测试账号测试 OAuth 登录 + - [ ] 19.2.2 使用真实商户号测试 JSAPI 支付(0.01 元测试订单) + - [ ] 19.2.3 使用真实商户号测试 H5 支付 + - [ ] 19.2.4 验证支付回调正常触发和处理 +- [ ] 19.3 压力测试 + - [ ] 19.3.1 测试并发支付请求(100 QPS) + - [ ] 19.3.2 测试并发回调处理(50 QPS) + - [ ] 19.3.3 验证 Redis Token 缓存不会频繁刷新 + +## 20. 监控和告警 + +- [ ] 20.1 添加监控指标 + - [ ] 20.1.1 微信 OAuth 成功率/失败率 + - [ ] 20.1.2 支付发起成功率/失败率 + - [ ] 20.1.3 支付回调接收数量/验证失败数量 + - [ ] 20.1.4 Access Token 获取次数 +- [ ] 20.2 配置告警规则(如有监控系统) + - [ ] 20.2.1 微信 OAuth 失败率 > 10% 告警 + - [ ] 20.2.2 支付发起失败率 > 5% 告警 + - [ ] 20.2.3 支付回调验证失败数量 > 10/分钟 告警