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

10 KiB
Raw Blame History

name, overview, todos
name overview todos
微信支付切换到官方wechatpay-go 将 pkg/wechat/pay.go 的自实现微信支付(签名/下单/退款/回调解密)替换为官方 SDK github.com/wechatpay-apiv3/wechatpay-go,获得平台证书自动下载轮换与应答/回调自动验签能力;同步调整订单回调链路以传入 *http.Request,code2session 登录保持标准库不变。
id content status
add-dep-init 添加 wechatpay-go 依赖(go get + go mod tidy),基于 core.NewClient + WithWechatPayAutoAuthCipher 重写 InitPayClient,构建 notify.RSANotifyHandler,保留配置校验与单例 completed
id content status dependencies
rewrite-pay-refund 用 SDK 重写 JsapiPrepay(PrepayWithRequestPayment 一步返回 *MiniPayParams,移除 BuildMiniPayParams)与 Refund(RefundsApiService.Create),并删除全部自实现签名/HTTP 辅助函数 completed
add-dep-init
id content status dependencies
rewrite-notify 用 handler.ParseNotifyRequest 重写两个回调函数,入参改为 *http.Request,分别用 payments.Transaction/refunddomestic.Refund 映射到现有 PayNotifyData/RefundNotifyData completed
add-dep-init
id content status dependencies
adapt-callers 适配 internal/service/order.go(Create 一步下单、PayNotify/RefundNotify 入参改 *http.Request、import net/http)与 internal/routers/api/v1/order.go(回调 handler 直接传 c.Request) completed
rewrite-pay-refund
rewrite-notify
id content status dependencies
verify 执行 go build ./... 与 go vet ./...,使用 [subagent:code-explorer] 复核无残留 BuildMiniPayParams 及旧 []byte 回调调用,确认签名一致 completed
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,无需保留。

架构与数据流

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