--- 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:确认无遗漏引用,调用链签名对齐,避免编译期返工。