10 KiB
10 KiB
name, overview, todos
| name | overview | todos | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 微信支付切换到官方wechatpay-go | 将 pkg/wechat/pay.go 的自实现微信支付(签名/下单/退款/回调解密)替换为官方 SDK github.com/wechatpay-apiv3/wechatpay-go,获得平台证书自动下载轮换与应答/回调自动验签能力;同步调整订单回调链路以传入 *http.Request,code2session 登录保持标准库不变。 |
|
产品概述
将微信支付能力从「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/httpimport。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:确认无遗漏引用,调用链签名对齐,避免编译期返工。