feat(payment): support wxpay legacy platform-certificate verifier mode

之前的改动只保留了微信支付"新公钥验签"路径,对仍在用平台证书的老商户不可用。

- wxpay.go: publicKeyId 从必填移除;buildVerifier 根据是否配置 publicKeyId 自动切换:
  - 有 → 新公钥方式(NewSHA256WithRSAPubkeyVerifier + 配置的 publicKey)
  - 无 → 平台证书方式(downloader 自动下载平台证书 + NewSHA256WithRSAVerifier)
  publicKey/publicKeyId 必须成对提供,单独一个视为配置错误
- providerConfig.ts: publicKey/publicKeyId 标记为 optional,certSerial 保留必填
- wxpay_test.go: 补 legacy 模式成功用例和成对校验错误用例
This commit is contained in:
erio
2026-04-20 18:19:56 +08:00
parent 7207c5553b
commit d635b66c29
3 changed files with 48 additions and 19 deletions
+30 -12
View File
@@ -14,7 +14,9 @@ import (
"github.com/Wei-Shaw/sub2api/internal/payment"
"github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/auth"
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
@@ -57,7 +59,7 @@ type Wxpay struct {
}
func NewWxpay(instanceID string, config map[string]string) (*Wxpay, error) {
required := []string{"appId", "mchId", "privateKey", "apiV3Key", "publicKey", "publicKeyId", "certSerial"}
required := []string{"appId", "mchId", "privateKey", "apiV3Key", "certSerial"}
for _, k := range required {
if config[k] == "" {
return nil, fmt.Errorf("wxpay config missing required key: %s", k)
@@ -66,6 +68,13 @@ func NewWxpay(instanceID string, config map[string]string) (*Wxpay, error) {
if len(config["apiV3Key"]) != 32 {
return nil, fmt.Errorf("wxpay apiV3Key must be exactly 32 bytes, got %d", len(config["apiV3Key"]))
}
// publicKey + publicKeyId are a pair used by the new pubkey verifier.
// If either is set, both must be set; otherwise fall back to legacy platform certificate mode.
hasPubKey := config["publicKey"] != ""
hasPubKeyID := config["publicKeyId"] != ""
if hasPubKey != hasPubKeyID {
return nil, fmt.Errorf("wxpay publicKey and publicKeyId must be provided together")
}
return &Wxpay{instanceID: instanceID, config: config}, nil
}
@@ -89,14 +98,16 @@ func (w *Wxpay) ensureClient() (*core.Client, error) {
if w.coreClient != nil {
return w.coreClient, nil
}
privateKey, publicKey, err := w.loadKeyPair()
privateKey, err := utils.LoadPrivateKey(formatPEM(w.config["privateKey"], "PRIVATE KEY"))
if err != nil {
return nil, fmt.Errorf("wxpay load private key: %w", err)
}
verifier, err := w.buildVerifier(privateKey)
if err != nil {
return nil, err
}
certSerial := w.config["certSerial"]
verifier := verifiers.NewSHA256WithRSAPubkeyVerifier(w.config["publicKeyId"], *publicKey)
client, err := core.NewClient(context.Background(),
option.WithMerchantCredential(w.config["mchId"], certSerial, privateKey),
option.WithMerchantCredential(w.config["mchId"], w.config["certSerial"], privateKey),
option.WithVerifier(verifier))
if err != nil {
return nil, fmt.Errorf("wxpay init client: %w", err)
@@ -110,16 +121,23 @@ func (w *Wxpay) ensureClient() (*core.Client, error) {
return w.coreClient, nil
}
func (w *Wxpay) loadKeyPair() (*rsa.PrivateKey, *rsa.PublicKey, error) {
privateKey, err := utils.LoadPrivateKey(formatPEM(w.config["privateKey"], "PRIVATE KEY"))
if err != nil {
return nil, nil, fmt.Errorf("wxpay load private key: %w", err)
// buildVerifier picks the verifier mode based on whether publicKeyId is configured:
// - publicKeyId set → new pubkey verifier using the configured publicKey
// - publicKeyId empty → legacy mode: auto-download platform certs with apiV3Key
func (w *Wxpay) buildVerifier(privateKey *rsa.PrivateKey) (auth.Verifier, error) {
if w.config["publicKeyId"] != "" {
publicKey, err := utils.LoadPublicKey(formatPEM(w.config["publicKey"], "PUBLIC KEY"))
if err != nil {
return nil, fmt.Errorf("wxpay load public key: %w", err)
}
return verifiers.NewSHA256WithRSAPubkeyVerifier(w.config["publicKeyId"], *publicKey), nil
}
publicKey, err := utils.LoadPublicKey(formatPEM(w.config["publicKey"], "PUBLIC KEY"))
mgr := downloader.MgrInstance()
err := mgr.RegisterDownloaderWithPrivateKey(context.Background(), privateKey, w.config["certSerial"], w.config["mchId"], w.config["apiV3Key"])
if err != nil {
return nil, nil, fmt.Errorf("wxpay load public key: %w", err)
return nil, fmt.Errorf("wxpay register platform cert downloader: %w", err)
}
return privateKey, publicKey, nil
return verifiers.NewSHA256WithRSAVerifier(mgr.GetCertificateVisitor(w.config["mchId"])), nil
}
func (w *Wxpay) CreatePayment(ctx context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
@@ -207,16 +207,27 @@ func TestNewWxpay(t *testing.T) {
errSubstr: "apiV3Key",
},
{
name: "missing publicKey",
config: withOverride(map[string]string{"publicKey": ""}),
name: "missing certSerial",
config: withOverride(map[string]string{"certSerial": ""}),
wantErr: true,
errSubstr: "publicKey",
errSubstr: "certSerial",
},
{
name: "missing publicKeyId",
name: "legacy mode: publicKey+publicKeyId both empty",
config: withOverride(map[string]string{"publicKey": "", "publicKeyId": ""}),
wantErr: false,
},
{
name: "publicKey without publicKeyId",
config: withOverride(map[string]string{"publicKeyId": ""}),
wantErr: true,
errSubstr: "publicKeyId",
errSubstr: "must be provided together",
},
{
name: "publicKeyId without publicKey",
config: withOverride(map[string]string{"publicKey": ""}),
wantErr: true,
errSubstr: "must be provided together",
},
{
name: "apiV3Key too short",
@@ -99,9 +99,9 @@ export const PROVIDER_CONFIG_FIELDS: Record<string, ConfigFieldDef[]> = {
{ key: 'mchId', label: '', sensitive: false },
{ key: 'privateKey', label: '', sensitive: true },
{ key: 'apiV3Key', label: '', sensitive: true },
{ key: 'publicKey', label: '', sensitive: true },
{ key: 'publicKeyId', label: '', sensitive: false },
{ key: 'certSerial', label: '', sensitive: false },
{ key: 'publicKey', label: '', sensitive: true, optional: true },
{ key: 'publicKeyId', label: '', sensitive: false, optional: true },
],
stripe: [
{ key: 'secretKey', label: '', sensitive: true },