first commit

This commit is contained in:
2026-09-11 01:41:52 +08:00
commit 9f22e6ccae
263 changed files with 28935 additions and 0 deletions
@@ -0,0 +1,151 @@
---
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:确认无遗漏引用,调用链签名对齐,避免编译期返工。