Files
zt-closet-backend/.codebuddy/plans/微信支付切换到官方wechatpay-go_910cf320.md
T
2026-09-11 01:41:52 +08:00

151 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: 微信支付切换到官方wechatpay-go
overview: 将 pkg/wechat/pay.go 的自实现微信支付(签名/下单/退款/回调解密)替换为官方 SDK github.com/wechatpay-apiv3/wechatpay-go,获得平台证书自动下载轮换与应答/回调自动验签能力;同步调整订单回调链路以传入 *http.Request,code2session 登录保持标准库不变。
todos:
- id: add-dep-init
content: 添加 wechatpay-go 依赖(go get + go mod tidy),基于 core.NewClient + WithWechatPayAutoAuthCipher 重写 InitPayClient,构建 notify.RSANotifyHandler,保留配置校验与单例
status: completed
- id: rewrite-pay-refund
content: 用 SDK 重写 JsapiPrepay(PrepayWithRequestPayment 一步返回 *MiniPayParams,移除 BuildMiniPayParams)与 Refund(RefundsApiService.Create),并删除全部自实现签名/HTTP 辅助函数
status: completed
dependencies:
- add-dep-init
- id: rewrite-notify
content: 用 handler.ParseNotifyRequest 重写两个回调函数,入参改为 *http.Request,分别用 payments.Transaction/refunddomestic.Refund 映射到现有 PayNotifyData/RefundNotifyData
status: completed
dependencies:
- add-dep-init
- id: adapt-callers
content: 适配 internal/service/order.go(Create 一步下单、PayNotify/RefundNotify 入参改 *http.Request、import net/http)与 internal/routers/api/v1/order.go(回调 handler 直接传 c.Request)
status: completed
dependencies:
- rewrite-pay-refund
- rewrite-notify
- id: verify
content: 执行 go build ./... 与 go vet ./...,使用 [subagent:code-explorer] 复核无残留 BuildMiniPayParams 及旧 []byte 回调调用,确认签名一致
status: completed
dependencies:
- adapt-callers
---
## 产品概述
将微信支付能力从「Go 标准库自实现」切换为官方 SDK `github.com/wechatpay-apiv3/wechatpay-go`,在保持对外业务契约基本不变的前提下,获得官方 SDK 内置的**平台证书自动下载与轮换**、**请求签名与应答自动验签**、**回调自动验签 + 解密**能力,弥补自实现方案缺失的验签安全环节。
## 核心功能
- 商户支付客户端初始化:加载商户私钥、自动注册平台证书下载器(定期轮换)、自动应答验签。
- 小程序 JSAPI 下单:一步返回小程序调起支付所需参数(TimeStamp/NonceStr/Package/SignType/PaySign)。
- 申请退款:调用官方退款服务。
- 支付/退款回调:官方处理器完成签名校验 + APIv3Key 解密 + 反序列化。
## 范围与约束
- 仅改造微信支付部分(`pkg/wechat/pay.go` 及其调用方)。
- 微信小程序登录 `code2session`(`pkg/wechat/mini.go`)保持 Go 标准库实现,不改动。
- 配置结构体 `setting.WechatPaySettingS` 字段不变;`main.go` 的初始化时机与「初始化失败仅告警、禁用支付、不 panic」降级逻辑保持不变。
- 对外返回结构体(`MiniPayParams`/`PayNotifyData`/`RefundNotifyData`/`RefundResult`)字段保持不变,最小化上层改动。
## 技术栈
- 语言/框架:Go 1.27 + Gin(沿用现有项目)。
- 新增依赖:`github.com/wechatpay-apiv3/wechatpay-go`(`go get` + `go mod tidy`,GOPROXY 已配置 goproxy.cn 可用)。
- 复用 SDK 子包:`core`、`core/option`、`core/auth/verifiers`、`core/downloader`、`utils`、`services/payments/jsapi`、`services/payments`、`services/refunddomestic`、`core/notify`。
## 实现方案
### 1. 初始化 `InitPayClient(s *setting.WechatPaySettingS) error`(签名不变)
- 保留原有配置完整性校验:字段非空、`APIv3Key` 必须 32 字节。
- `utils.LoadPrivateKeyWithPath(s.PrivateKeyPath)` 加载商户私钥。
- `core.NewClient(ctx, option.WithWechatPayAutoAuthCipher(mchID, serialNo, privateKey, apiV3Key))` 创建客户端:内部完成请求签名 + 平台证书自动下载/轮换 + 应答自动验签。
- 用 `downloader.MgrInstance().GetCertificateVisitor(mchID)` + `verifiers.NewSHA256WithRSAVerifier(...)` 构造 `notify.NewRSANotifyHandler(apiV3Key, verifier)` 作为回调处理器。
- 包级单例持有 `*core.Client`、`*notify.Handler`、`appID/mchID/notifyURL/refundNotifyURL`,沿用现有 `sync.RWMutex` + `getPayClient()` 模式。
- 初始化会联网下载平台证书;失败返回 err(由 `main.go` 已有降级路径处理)。
### 2. 下单(合并两步为一步)
- 用 `jsapi.JsapiApiService{Client}.PrepayWithRequestPayment(ctx, jsapi.PrepayRequest{Appid, Mchid, Description, OutTradeNo, NotifyUrl, Amount:{Total, Currency:"CNY"}, Payer:{Openid}})`,直接返回含 `TimeStamp/NonceStr/Package/SignType/PaySign` 的应答(SDK 内部已用商户私钥完成 paySign 签名)。
- 接口调整:`JsapiPrepay(ctx, openid, orderNo, description string, amount int64) (*MiniPayParams, error)`(原返回 string prepayID)。
- **移除 `BuildMiniPayParams`**(职责被 SDK 合并)。
- 金额单位为分,用 `core.Int64(amount)` 等 SDK 指针辅助函数构造请求。
### 3. 退款 `Refund(...) (*RefundResult, error)`(签名不变)
- 用 `refunddomestic.RefundsApiService{Client}.Create(ctx, refunddomestic.CreateRequest{OutTradeNo, OutRefundNo, NotifyUrl, Reason, Amount:{Refund, Total, Currency:"CNY"}})`。
- 映射应答到 `RefundResult{RefundID, Status}`(注意 SDK 返回为指针,需判空取值)。
### 4. 回调验签 + 解密(关键变更:入参 `[]byte` → `*http.Request`)
- `handler.ParseNotifyRequest(ctx, request, content)`:校验 `Wechatpay-Signature/Timestamp/Nonce/Serial` 签名头 → 平台证书验签 → APIv3Key 解密 → 反序列化。
- `VerifyAndDecryptPayNotify(ctx context.Context, request *http.Request) (*PayNotifyData, error)`:content 用 `payments.Transaction`,映射到现有 `PayNotifyData`(含 `Amount.Total/PayerTotal/Currency`,指针判空)。
- `VerifyAndDecryptRefundNotify(ctx context.Context, request *http.Request) (*RefundNotifyData, error)`:content 用 `refunddomestic.Refund`,`RefundStatus` 取 `refunddomestic.Status` 值映射。
### 5. 调用方适配
- `internal/service/order.go`:
- `Create`:改为一步 `payParams, err := wechat.JsapiPrepay(...)`,删除 `BuildMiniPayParams` 调用;`payParams → dto.WechatPayParams` 映射逻辑不变。
- `PayNotify(ctx, request *http.Request) *errcode.Error` / `RefundNotify(ctx, request *http.Request) *errcode.Error`:入参由 `body []byte` 改为 `*http.Request`,转调新的 Verify 函数;幂等锁、状态条件流转、发放权益、通知等业务逻辑完全不变;新增 `net/http` import。
- `internal/routers/api/v1/order.go`:回调 handler 不再 `c.GetRawData()`,直接 `svc.PayNotify(c.Request.Context(), c.Request)`;微信规范返回(`{"code":"SUCCESS"}`/`{"code":"FAIL"}`)不变。中间件链不消费 body,SDK 可正常读取。
- `main.go`:`InitPayClient` 签名一致,无需改动。
### 6. 移除的自实现代码
删除 `pay.go` 中:`parsePrivateKey`、`doRequest`、`buildAuthorization`、`sign`、`randomHex`、`aesGCMDecrypt`、`decryptNotify`、`notifyEnvelope`、`payAPIBase`/path 常量、`PrepayResult` 等(被 SDK 取代)。`mini.go` 当前未使用 `randomHex`,无需保留。
## 架构与数据流
```mermaid
flowchart TD
A[order.go Create] --> B[wechat.JsapiPrepay 一步]
B --> C[jsapi.PrepayWithRequestPayment]
C --> D[core.Client 签名+应答验签]
E[api/v1 PayNotify/RefundNotify handler] --> F[svc PayNotify/RefundNotify ctx,*http.Request]
F --> G[wechat.VerifyAndDecrypt* ]
G --> H[notify.Handler.ParseNotifyRequest\n验签+解密+反序列化]
D --> I[(微信支付 API\napi.mch.weixin.qq.com)]
H --> J[downloader 平台证书自动轮换]
```
## 安全与健壮性
- 新增应答验签 + 回调验签 + 平台证书自动轮换,杜绝伪造/篡改回调,补齐自实现缺口。
- 商户私钥/APIv3Key 仍仅来自配置文件路径,不硬编码、不写日志。
- 回调验签失败返回微信 FAIL 触发重试;业务幂等(redis 锁 + 状态条件流转)保持不变,重复回调安全。
- 初始化联网失败降级为「禁用支付」,不影响服务启动与其它业务。
## 目录结构
```
zt-closet-backend/
├── go.mod / go.sum # [MODIFY] 新增 wechatpay-go 依赖(go get + go mod tidy)
├── pkg/wechat/
│ ├── pay.go # [REWRITE] 基于官方 SDK 重写:InitPayClient/Refund 签名不变;
│ │ # JsapiPrepay 返回 *MiniPayParams 并移除 BuildMiniPayParams;
│ │ # VerifyAndDecryptPayNotify/RefundNotify 入参改为 *http.Request;
│ │ # 删除全部手写签名/解密/HTTP 辅助函数
│ └── mini.go # [KEEP] code2session 保持标准库实现,不改动
├── internal/service/order.go # [MODIFY] Create 一步下单;PayNotify/RefundNotify 入参改 *http.Request;import net/http
├── internal/routers/api/v1/order.go # [MODIFY] 回调 handler 传 c.Request(不再 GetRawData)
└── main.go # [KEEP] InitPayClient 签名一致,初始化与降级逻辑不变
```
## 关键代码结构(对外契约)
```
// pkg/wechat/pay.go 对外签名(结构体字段保持不变)
func InitPayClient(s *setting.WechatPaySettingS) error
func JsapiPrepay(ctx context.Context, openid, orderNo, description string, amount int64) (*MiniPayParams, error) // 返回类型调整
func Refund(ctx context.Context, orderNo, refundNo string, total, refund int64, reason string) (*RefundResult, error)
func VerifyAndDecryptPayNotify(ctx context.Context, request *http.Request) (*PayNotifyData, error) // 入参改 *http.Request
func VerifyAndDecryptRefundNotify(ctx context.Context, request *http.Request) (*RefundNotifyData, error) // 入参改 *http.Request
// 移除:BuildMiniPayParams、PrepayResult 及所有自实现辅助函数
```
## Agent Extensions
### SubAgent
- **code-explorer**
- Purpose:在改造完成后,全仓复核是否残留对已移除符号(`BuildMiniPayParams`、旧 `VerifyAndDecryptPayNotify([]byte)`/`VerifyAndDecryptRefundNotify([]byte)`)的调用,并确认 `order.go` 与 `api/v1/order.go` 与新签名一致。
- Expected outcome:确认无遗漏引用,调用链签名对齐,避免编译期返工。