diff --git a/internal/biz/integration/integration_config_definition.go b/internal/biz/integration/integration_config_definition.go index 26f9ab9..3414d1f 100644 --- a/internal/biz/integration/integration_config_definition.go +++ b/internal/biz/integration/integration_config_definition.go @@ -17,6 +17,31 @@ func integrationSelect(key, label string, required bool, values ...string) Integ return IntegrationConfigField{Key: key, Label: label, Required: required, Type: "select", Options: options} } +var paymentOptionLabels = map[string]string{ + "production": "生产环境", "sandbox": "沙箱环境", + "alipay.trade.create": "统一收单交易创建", "alipay.trade.pay": "付款码支付", + "alipay.trade.precreate": "当面付扫码", "alipay.trade.app.pay": "App 支付", + "alipay.trade.page.pay": "电脑网站支付", "alipay.trade.wap.pay": "手机网站支付", + "JSAPI": "公众号 / 小程序支付", "APP": "App 支付", "NATIVE": "扫码支付", + "MWEB": "H5 支付", "MICROPAY": "付款码支付", + "jsapi": "公众号 / 小程序支付", "app": "App 支付", "native": "扫码支付", + "h5": "H5 支付", "codepay": "付款码支付", "mini": "小程序支付", + "qrcode": "二维码支付", "native_jsapi": "扫码后小程序支付", "sdk": "SDK 支付", + "web": "网页支付", "retail": "门店收银", "retail_qrcode": "门店扫码", + "Wechat": "微信支付", "Alipay": "支付宝", "UnionPay": "银联", + "reqsn": "商户订单号", "trxid": "平台交易号", +} + +func paymentSelect(key, label string, required bool, values ...string) IntegrationConfigField { + field := integrationSelect(key, label, required, values...) + for i := range field.Options { + if name := paymentOptionLabels[values[i]]; name != "" { + field.Options[i].Label = name + " (" + values[i] + ")" + } + } + return field +} + func paymentDefinition(provider, name, description string, defaults map[string]any, fields ...IntegrationConfigField) IntegrationConfigDefinition { common := []IntegrationConfigField{ integrationField("notify_url", "支付回调地址", false, false, "url"), @@ -172,29 +197,29 @@ var integrationDefinitions = map[string][]IntegrationConfigDefinition{ IntegrationKindPayment: { paymentDefinition(bizpayment.PaymentAlipay, "支付宝", "支付宝 OpenAPI RSA2 支付", map[string]any{"app_id": "", "private_key": "", "public_key": "", "environment": "production", "sign_type": "RSA2", "gateway_url": "https://openapi.alipay.com/gateway.do", "method": "alipay.trade.create"}, integrationField("app_id", "应用 ID", true, false, "text"), integrationField("private_key", "应用私钥", true, true, "textarea"), integrationField("public_key", "支付宝公钥", true, true, "textarea"), - integrationSelect("environment", "环境", false, "production", "sandbox"), integrationSelect("sign_type", "签名算法", false, "RSA2", "RSA"), integrationField("gateway_url", "网关地址", false, false, "url"), - integrationSelect("method", "默认支付方式", false, "alipay.trade.create", "alipay.trade.pay", "alipay.trade.precreate", "alipay.trade.app.pay", "alipay.trade.page.pay", "alipay.trade.wap.pay")), + paymentSelect("environment", "环境", false, "production", "sandbox"), integrationSelect("sign_type", "签名算法", false, "RSA2", "RSA"), integrationField("gateway_url", "网关地址", false, false, "url"), + paymentSelect("method", "默认支付方式", false, "alipay.trade.create", "alipay.trade.pay", "alipay.trade.precreate", "alipay.trade.app.pay", "alipay.trade.page.pay", "alipay.trade.wap.pay")), paymentDefinition(bizpayment.PaymentAlipayV3, "支付宝 V3", "支付宝证书模式 V3 接口", map[string]any{"app_id": "", "private_key": "", "app_cert": "", "root_cert": "", "public_cert": "", "environment": "production", "api_base_url": "https://openapi.alipay.com", "gateway_url": "https://openapi.alipay.com/gateway.do", "method": "alipay.trade.create"}, integrationField("app_id", "应用 ID", true, false, "text"), integrationField("private_key", "应用私钥", true, true, "textarea"), integrationField("app_cert", "应用公钥证书", true, true, "textarea"), integrationField("root_cert", "支付宝根证书", true, true, "textarea"), integrationField("public_cert", "支付宝公钥证书", true, true, "textarea"), - integrationSelect("environment", "环境", false, "production", "sandbox"), integrationField("api_base_url", "API 地址", false, false, "url"), integrationField("gateway_url", "网关地址", false, false, "url"), integrationSelect("method", "默认支付方式", false, "alipay.trade.create", "alipay.trade.pay", "alipay.trade.precreate", "alipay.trade.app.pay", "alipay.trade.page.pay", "alipay.trade.wap.pay")), + paymentSelect("environment", "环境", false, "production", "sandbox"), integrationField("api_base_url", "API 地址", false, false, "url"), integrationField("gateway_url", "网关地址", false, false, "url"), paymentSelect("method", "默认支付方式", false, "alipay.trade.create", "alipay.trade.pay", "alipay.trade.precreate", "alipay.trade.app.pay", "alipay.trade.page.pay", "alipay.trade.wap.pay")), paymentDefinition(bizpayment.PaymentWechatV2, "微信支付 V2", "微信支付 V2,含退款双向证书", map[string]any{"app_id": "", "merchant_id": "", "mch_key": "", "sign_type": "MD5", "trade_type": "NATIVE", "client_cert": "", "client_key": ""}, - integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("mch_key", "API 密钥", true, true, "password"), integrationSelect("sign_type", "签名算法", false, "MD5", "HMAC-SHA256"), integrationSelect("trade_type", "默认交易类型", false, "JSAPI", "APP", "NATIVE", "MWEB", "MICROPAY"), integrationField("client_cert", "商户证书", true, true, "textarea"), integrationField("client_key", "证书私钥", true, true, "textarea")), + integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("mch_key", "API 密钥", true, true, "password"), integrationSelect("sign_type", "签名算法", false, "MD5", "HMAC-SHA256"), paymentSelect("trade_type", "默认交易类型", false, "JSAPI", "APP", "NATIVE", "MWEB", "MICROPAY"), integrationField("client_cert", "商户证书", true, true, "textarea"), integrationField("client_key", "证书私钥", true, true, "textarea")), paymentDefinition(bizpayment.PaymentWechatV3, "微信支付 V3", "微信支付 API v3", map[string]any{"app_id": "", "merchant_id": "", "serial_no": "", "private_key": "", "api_v3_key": "", "platform_cert": "", "platform_serial_no": "", "trade_type": "jsapi"}, - integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("serial_no", "商户证书序列号", true, false, "text"), integrationField("private_key", "商户私钥", true, true, "textarea"), integrationField("api_v3_key", "API v3 密钥", true, true, "password"), integrationField("platform_cert", "平台证书", true, true, "textarea"), integrationField("platform_serial_no", "平台证书序列号", false, false, "text"), integrationSelect("trade_type", "默认交易类型", false, "jsapi", "app", "native", "h5", "codepay")), + integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("serial_no", "商户证书序列号", true, false, "text"), integrationField("private_key", "商户私钥", true, true, "textarea"), integrationField("api_v3_key", "API v3 密钥", true, true, "password"), IntegrationConfigField{Key: "platform_cert", Label: "平台证书 / 微信支付公钥", Type: "textarea", Secret: true, Description: "可留空,使用商户凭证自动获取平台证书;使用微信支付公钥模式时填写公钥和对应 ID。"}, integrationField("platform_serial_no", "证书序列号 / 公钥 ID", false, false, "text"), paymentSelect("trade_type", "默认交易类型", false, "jsapi", "app", "native", "h5", "codepay")), paymentDefinition(bizpayment.PaymentApple, "Apple IAP", "Apple App Store Server API", map[string]any{"issuer_id": "", "key_id": "", "bundle_id": "", "private_key": "", "price_divisor": 10, "environment": "production", "test_product_id": "", "test_transaction_id": ""}, - integrationField("issuer_id", "Issuer ID", true, false, "text"), integrationField("key_id", "Key ID", true, false, "text"), integrationField("bundle_id", "Bundle ID", true, false, "text"), integrationField("private_key", "P8 私钥", true, true, "textarea"), integrationField("price_divisor", "价格除数", false, false, "number"), integrationSelect("environment", "环境", false, "production", "sandbox"), integrationField("test_product_id", "测试商品 ID", false, false, "text"), integrationField("test_transaction_id", "沙箱交易 ID", false, false, "text")), + integrationField("issuer_id", "Issuer ID", true, false, "text"), integrationField("key_id", "Key ID", true, false, "text"), integrationField("bundle_id", "Bundle ID", true, false, "text"), integrationField("private_key", "P8 私钥", true, true, "textarea"), integrationField("price_divisor", "价格除数", false, false, "number"), paymentSelect("environment", "环境", false, "production", "sandbox"), integrationField("test_product_id", "测试商品 ID", false, false, "text"), integrationField("test_transaction_id", "沙箱交易 ID", false, false, "text")), paymentDefinition(bizpayment.PaymentDouyin, "抖音支付", "抖音开放平台支付", map[string]any{"app_id": "", "merchant_id": "", "serial_no": "", "api_key": "", "private_key": "", "platform_cert": "", "platform_serial_no": "", "trade_type": "jsapi", "environment": "production"}, - integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("serial_no", "商户证书序列号", true, false, "text"), integrationField("api_key", "API 密钥", true, true, "password"), integrationField("private_key", "商户私钥", true, true, "textarea"), integrationField("platform_cert", "平台证书", true, true, "textarea"), integrationField("platform_serial_no", "平台证书序列号", true, false, "text"), integrationSelect("trade_type", "默认交易类型", false, "app", "jsapi", "h5", "native"), integrationSelect("environment", "环境", false, "production")), + integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("serial_no", "商户证书序列号", true, false, "text"), integrationField("api_key", "API 密钥", true, true, "password"), integrationField("private_key", "商户私钥", true, true, "textarea"), integrationField("platform_cert", "平台证书", true, true, "textarea"), integrationField("platform_serial_no", "平台证书序列号", true, false, "text"), paymentSelect("trade_type", "默认交易类型", false, "app", "jsapi", "h5", "native"), paymentSelect("environment", "环境", false, "production")), paymentDefinition(bizpayment.PaymentQQ, "QQ 钱包", "QQ 钱包支付", map[string]any{"mch_id": "", "api_key": "", "sign_type": "MD5", "trade_type": "NATIVE", "cert_file": "", "key_file": "", "environment": "production"}, - integrationField("mch_id", "商户号", true, false, "text"), integrationField("api_key", "API 密钥", true, true, "password"), integrationSelect("sign_type", "签名算法", false, "MD5", "HMAC-SHA256"), integrationSelect("trade_type", "默认交易类型", false, "JSAPI", "NATIVE", "APP", "MICROPAY"), integrationField("cert_file", "退款证书路径", false, false, "text"), integrationField("key_file", "退款私钥路径", false, true, "text"), integrationSelect("environment", "环境", false, "production")), + integrationField("mch_id", "商户号", true, false, "text"), integrationField("api_key", "API 密钥", true, true, "password"), integrationSelect("sign_type", "签名算法", false, "MD5", "HMAC-SHA256"), paymentSelect("trade_type", "默认交易类型", false, "JSAPI", "NATIVE", "APP", "MICROPAY"), integrationField("cert_file", "退款证书路径", false, false, "text"), integrationField("key_file", "退款私钥路径", false, true, "text"), paymentSelect("environment", "环境", false, "production")), paymentDefinition(bizpayment.PaymentAllinPay, "通联支付", "通联收银宝支付", map[string]any{"cus_id": "", "app_id": "", "private_key": "", "public_key": "", "org_id": "", "pay_type": "W02", "query_order_type": "reqsn", "currency": "CNY", "environment": "production"}, - integrationField("cus_id", "商户号", true, false, "text"), integrationField("app_id", "应用 ID", true, false, "text"), integrationField("private_key", "商户私钥", true, true, "textarea"), integrationField("public_key", "平台公钥", true, true, "textarea"), integrationField("org_id", "机构号", false, false, "text"), integrationField("pay_type", "支付类型", false, false, "text"), integrationSelect("query_order_type", "查单标识", false, "reqsn", "trxid"), integrationField("currency", "币种", false, false, "text"), integrationSelect("environment", "环境", false, "production", "sandbox")), + integrationField("cus_id", "商户号", true, false, "text"), integrationField("app_id", "应用 ID", true, false, "text"), integrationField("private_key", "商户私钥", true, true, "textarea"), integrationField("public_key", "平台公钥", true, true, "textarea"), integrationField("org_id", "机构号", false, false, "text"), integrationField("pay_type", "支付类型", false, false, "text"), paymentSelect("query_order_type", "查单标识", false, "reqsn", "trxid"), integrationField("currency", "币种", false, false, "text"), paymentSelect("environment", "环境", false, "production", "sandbox")), paymentDefinition(bizpayment.PaymentLakala, "拉卡拉", "拉卡拉聚合支付", map[string]any{"partner_code": "", "credential_code": "", "channel": "Wechat", "method": "jsapi", "currency": "CNY", "environment": "production"}, - integrationField("partner_code", "合作方编号", true, false, "text"), integrationField("credential_code", "凭证码", true, true, "password"), integrationSelect("channel", "支付渠道", false, "Wechat", "Alipay", "UnionPay"), integrationSelect("method", "默认支付方式", false, "jsapi", "h5", "mini", "native", "qrcode", "native_jsapi", "sdk", "web", "retail", "retail_qrcode"), integrationField("currency", "币种", false, false, "text"), integrationSelect("environment", "环境", false, "production")), + integrationField("partner_code", "合作方编号", true, false, "text"), integrationField("credential_code", "凭证码", true, true, "password"), paymentSelect("channel", "支付渠道", false, "Wechat", "Alipay", "UnionPay"), paymentSelect("method", "默认支付方式", false, "jsapi", "h5", "mini", "native", "qrcode", "native_jsapi", "sdk", "web", "retail", "retail_qrcode"), integrationField("currency", "币种", false, false, "text"), paymentSelect("environment", "环境", false, "production")), paymentDefinition(bizpayment.PaymentPayPal, "PayPal", "PayPal Checkout", map[string]any{"client_id": "", "client_secret": "", "webhook_id": "", "environment": "sandbox", "return_url": "", "cancel_url": "", "auto_capture": true, "test_currency": "USD"}, - integrationField("client_id", "Client ID", true, false, "text"), integrationField("client_secret", "Client Secret", true, true, "password"), integrationField("webhook_id", "Webhook ID", true, false, "text"), integrationSelect("environment", "环境", false, "sandbox", "production"), integrationField("cancel_url", "取消跳转地址", false, false, "url"), integrationField("auto_capture", "自动捕获", false, false, "switch")), + integrationField("client_id", "Client ID", true, false, "text"), integrationField("client_secret", "Client Secret", true, true, "password"), integrationField("webhook_id", "Webhook ID", true, false, "text"), paymentSelect("environment", "环境", false, "sandbox", "production"), integrationField("cancel_url", "取消跳转地址", false, false, "url"), integrationField("auto_capture", "自动捕获", false, false, "switch")), paymentDefinition(bizpayment.PaymentSaobei, "扫呗", "扫呗聚合支付", map[string]any{"inst_no": "", "key": "", "merchant_no": "", "terminal_id": "", "access_token": "", "pay_type": "010", "currency": "CNY", "environment": "production"}, - integrationField("inst_no", "机构号", true, false, "text"), integrationField("key", "机构密钥", true, true, "password"), integrationField("merchant_no", "商户号", true, false, "text"), integrationField("terminal_id", "终端号", true, false, "text"), integrationField("access_token", "访问令牌", true, true, "password"), integrationField("pay_type", "支付类型", false, false, "text"), integrationField("currency", "币种", false, false, "text"), integrationSelect("environment", "环境", false, "production", "sandbox")), + integrationField("inst_no", "机构号", true, false, "text"), integrationField("key", "机构密钥", true, true, "password"), integrationField("merchant_no", "商户号", true, false, "text"), integrationField("terminal_id", "终端号", true, false, "text"), integrationField("access_token", "访问令牌", true, true, "password"), integrationField("pay_type", "支付类型", false, false, "text"), paymentSelect("environment", "环境", false, "production", "sandbox")), genericPaymentDefinition(bizpayment.PaymentChinaums, "银联商务", "按商户协议配置的银联商务适配器"), genericPaymentDefinition(bizpayment.PaymentSFT, "商福通", "按商户协议配置的商福通适配器"), genericPaymentDefinition(bizpayment.PaymentSuperPay, "Supper Pay", "按商户协议配置的 Supper Pay 适配器"), diff --git a/internal/biz/integration/integration_config_payment_test.go b/internal/biz/integration/integration_config_payment_test.go new file mode 100644 index 0000000..b5434ce --- /dev/null +++ b/internal/biz/integration/integration_config_payment_test.go @@ -0,0 +1,56 @@ +package integration + +import ( + bizpayment "kra/internal/biz/payment" + "testing" +) + +func TestPaymentSelectLabelsKeepProtocolValues(t *testing.T) { + for _, test := range []struct { + provider, key, value, label string + }{ + {bizpayment.PaymentWechatV3, "trade_type", "native", "扫码支付 (native)"}, + {bizpayment.PaymentWechatV2, "trade_type", "MICROPAY", "付款码支付 (MICROPAY)"}, + {bizpayment.PaymentAlipay, "method", "alipay.trade.wap.pay", "手机网站支付 (alipay.trade.wap.pay)"}, + } { + definition, ok := IntegrationDefinition(IntegrationKindPayment, test.provider) + if !ok { + t.Fatalf("missing definition %s", test.provider) + } + found := false + for _, field := range definition.Fields { + if field.Key != test.key { + continue + } + for _, option := range field.Options { + if option.Value == test.value { + found = true + if option.Label != test.label { + t.Errorf("%s/%s label = %q, want %q", test.provider, test.value, option.Label, test.label) + } + } + } + } + if !found { + t.Errorf("missing option %s/%s", test.provider, test.value) + } + } +} + +func TestWechatV3PlatformCertificateIsOptional(t *testing.T) { + definition, _ := IntegrationDefinition(IntegrationKindPayment, bizpayment.PaymentWechatV3) + for _, field := range definition.Fields { + if field.Key == "platform_cert" && field.Required { + t.Fatal("platform certificate is marked required") + } + } + values := DefaultIntegrationConfig(IntegrationKindPayment, bizpayment.PaymentWechatV3) + values["app_id"] = "wx-app" + values["merchant_id"] = "merchant" + values["serial_no"] = "merchant-serial" + values["private_key"] = "merchant-private-key" + values["api_v3_key"] = "01234567890123456789012345678901" + if err := ValidateIntegrationConfig(IntegrationKindPayment, bizpayment.PaymentWechatV3, values); err != nil { + t.Fatalf("valid merchant credentials rejected without platform certificate: %v", err) + } +} diff --git a/internal/integration/payment/wechat_v3.go b/internal/integration/payment/wechat_v3.go index 0279f04..1f824bc 100644 --- a/internal/integration/payment/wechat_v3.go +++ b/internal/integration/payment/wechat_v3.go @@ -418,23 +418,10 @@ func newWechatV3Client(c map[string]any, endpointKey string) (*gopayWechatV3.Cli return nil, err } - platformCert := firstAny(c, "platform_cert", "platform_certificate", "wechatpay_platform_cert") - if platformCert == "" { - return nil, errors.New("微信支付 v3 缺少 platform_cert") - } - platformSerial := firstAny(c, "platform_serial_no", "platform_cert_serial", "wechatpay_serial_no") - if platformSerial == "" { - platformSerial, err = wechatV3PlatformSerial(platformCert) - if err != nil { - return nil, err - } - } - if err = client.AutoVerifySignByCert([]byte(platformCert), platformSerial); err != nil { - return nil, err - } client.SetBodySize(8) client.GetHttpClient().SetTimeout(20 * time.Second) + endpointConfigured := false if endpointKey != "" { if endpoint := strings.TrimSpace(text(c, endpointKey)); endpoint != "" { parsed, parseErr := url.Parse(endpoint) @@ -446,15 +433,37 @@ func newWechatV3Client(c map[string]any, endpointKey string) (*gopayWechatV3.Cli } httpClient := client.GetHttpClient() httpClient.SetTransport(&wechatV3EndpointTransport{base: httpClient.HttpClient.Transport, endpoint: parsed}) - return client, nil + endpointConfigured = true } } - if baseURL := strings.TrimRight(firstAny(c, "base_url", "api_base_url", "proxy_url"), "/"); baseURL != "" { - parsed, parseErr := url.Parse(baseURL) - if parseErr != nil || parsed.Scheme == "" || parsed.Host == "" { - return nil, errors.New("微信支付 v3 base_url 配置无效") + if !endpointConfigured { + if baseURL := strings.TrimRight(firstAny(c, "base_url", "api_base_url", "proxy_url"), "/"); baseURL != "" { + parsed, parseErr := url.Parse(baseURL) + if parseErr != nil || parsed.Scheme == "" || parsed.Host == "" { + return nil, errors.New("微信支付 v3 base_url 配置无效") + } + client.SetProxyHost(baseURL) + } + } + + platformCert := firstAny(c, "platform_cert", "platform_certificate", "wechatpay_platform_cert") + if platformCert == "" { + // Fetch the current platform certificates using the merchant credentials. + // This client is request-scoped, so avoid starting a background refresher. + if err = client.AutoVerifySign(false); err != nil { + return nil, fmt.Errorf("微信支付 v3 自动获取平台证书失败: %w", err) + } + } else { + platformSerial := firstAny(c, "platform_serial_no", "platform_cert_serial", "wechatpay_serial_no") + if platformSerial == "" { + platformSerial, err = wechatV3PlatformSerial(platformCert) + if err != nil { + return nil, err + } + } + if err = client.AutoVerifySignByCert([]byte(platformCert), platformSerial); err != nil { + return nil, err } - client.SetProxyHost(baseURL) } return client, nil } diff --git a/internal/integration/payment/wechat_v3_certificate_test.go b/internal/integration/payment/wechat_v3_certificate_test.go new file mode 100644 index 0000000..fc82d87 --- /dev/null +++ b/internal/integration/payment/wechat_v3_certificate_test.go @@ -0,0 +1,79 @@ +package payment + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestWechatV3WithoutPlatformCertificateLoadsVerificationKey(t *testing.T) { + _, merchantPrivatePEM, _ := testRSAKeyPair(t) + _, _, platformPublicPEM := testRSAKeyPair(t) + apiV3Key := "01234567890123456789012345678901" + block, err := aes.NewCipher([]byte(apiV3Key)) + if err != nil { + t.Fatal(err) + } + gcm, err := cipher.NewGCM(block) + if err != nil { + t.Fatal(err) + } + nonce, associatedData := "0123456789ab", "certificate" + ciphertext := base64.StdEncoding.EncodeToString(gcm.Seal(nil, []byte(nonce), []byte(platformPublicPEM), []byte(associatedData))) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v3/certificates" { + t.Errorf("unexpected path %q", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{ + "serial_no": "platform-serial", + "effective_time": time.Now().Add(-time.Hour).Format(time.RFC3339), + "expire_time": time.Now().Add(24 * time.Hour).Format(time.RFC3339), + "encrypt_certificate": map[string]any{ + "algorithm": "AEAD_AES_256_GCM", "nonce": nonce, + "associated_data": associatedData, "ciphertext": ciphertext, + }, + }}}) + })) + defer server.Close() + + client, err := newWechatV3Client(map[string]any{ + "merchant_id": "merchant", "serial_no": "merchant-serial", + "api_v3_key": apiV3Key, "private_key": merchantPrivatePEM, + "create_url": server.URL, + }, "create_url") + if err != nil { + t.Fatal(err) + } + if client.WxPublicKeyMap()["platform-serial"] == nil { + t.Fatal("fetched platform key was not installed for signature verification") + } +} + +func TestWechatV3WithoutPlatformCertificateFetchesFromConfiguredEndpoint(t *testing.T) { + _, merchantPrivatePEM, _ := testRSAKeyPair(t) + requestPath := "" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestPath = r.URL.Path + w.WriteHeader(http.StatusServiceUnavailable) + })) + defer server.Close() + + _, err := newWechatV3Client(map[string]any{ + "merchant_id": "merchant", "serial_no": "merchant-serial", + "api_v3_key": "01234567890123456789012345678901", + "private_key": merchantPrivatePEM, "create_url": server.URL, + }, "create_url") + if requestPath != "/v3/certificates" { + t.Fatalf("certificate request path = %q", requestPath) + } + if err == nil || !strings.Contains(err.Error(), "自动获取平台证书失败") { + t.Fatalf("certificate fetch error = %v", err) + } +} diff --git a/web/src/view/systemTools/payment/config.vue b/web/src/view/systemTools/payment/config.vue index 374e0dc..0ef2fcc 100644 --- a/web/src/view/systemTools/payment/config.vue +++ b/web/src/view/systemTools/payment/config.vue @@ -1,496 +1,28 @@ - - - - - 支付渠道配置 - 管理渠道凭证、回调地址和测试交易参数。 - - 刷新 - - - - - - - - - - {{ selected.name || providerText(selected.provider) }} - 未保存 - 运行中 - - {{ selected.description || selected.provider }} - - - {{ selected.enabled ? '已启用' : '已停用' }} - - - - - - - - - - - - {{ field.label }}{{ field.key }} - - - - - - - - - - - - {{ isSecretVisible(selected, field.key) ? '隐藏凭证' : '编辑凭证' }} - - - - - {{ fieldHint(field, selected) }} - - - - - - - - - - - - - - - - - {{ stageName(stage.name) }}{{ stage.message || stageStatusText(stage.status) }} - {{ stage.durationMs }} ms - - - - 关闭 - - - - - - - +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDH6e6GdHqs6LOr +6dNCHsL/QkdSU5u3e3o4nRD9qx6IEL72OH+/FZtgAbXZbpQOUSTkT96Yh/BhqCBD +l+xGoXOTj/aUe8+Xd5YGFz8OBv/eFjV2sLm2kmb7pgOpIVuBh94HdrPBS8iR5HUz +ad5DWh2BUJFIa9WroY/EGG7wyzZzWWb0jcZgDTut5+nP0cet3U7m0EHrJmsLiN/3 +/u9CXCdhpwQcutxCCGAUuO4/QGficVFv4X5U1AN66S+xWjQAOvPyIQSz2k/yDlXS +AXbfipTKX3odjDwPvI2WFAoi7hAbCWntE+dRNV3mrfGmCy9SvUcbdL1cu9SF5F8R +6/mLscVnAgMBAAECggEBALj0ZDq/HxREAp7FqS9eOHhjAItZURLTSYOBRUv7ejbW +j2ZLZV0Lh4Guc2kj1hTc2+3jOwURl8MyAg+nSfEaxEw8gBxl4+y1tfYSocK/lRAK +qhISdO9FycN+l5ptgzR22GM9XhcynvDjVc9oXjsf73ZJbPB82+V7hwAcvt9Gkyzp +Ydc5zkTIhw10Gf3oVQrFpPhWafM2BcBTyApck5twuQXMMJk3vygi+8xRsDxORda+ +fzkvcROeRYaLkkZQb2rAKxav3xymyAvWPNvLhk1u5Pc3pIjqh3N27OGk211wtQUA +OZt7CRlXuOUj34KFFbGHBqfInr65xwTGzefTjbhXCpECgYEA64me+ljbrpN+6h7y +FGPrKaTg2+sdD2AG5cf3poWh7mUOGuNuwdyXV285N7yd7ood0VdqhNRffez7Rc/y +F0Xxj2v6USLQJFAXC5ypQpXQ973qABy459Wn8IlqMnL0LcBYzLL2GFwReV5qp0QY +1frizUe2JGVq/4cYpUgvneuOVF8CgYEA2UgIws3zbF+Tif2hcxdNhqqOyEndXjF8 +cdve2EKBBxvLLqNVnkIEb9NQt+Y+vQehrFHF5c0UvO2nJbicCo8hQO2MAxrc6i7U +6iWAY9eJciBHeS6IsFL20QV6ft+wpTKyaQR50qvkLyAHLoGpJ0MRsBUMBDCNiNjS +oVfvWJ/Ca/kCgYEAoXzQr+oebFoaCODArvQ2tm2h8YYIfZMkB4ULqXMzq+RDQHIw +bOAWL09Flo5OZc7yVKYvlPSaqBbDzICfBg/hZ6jrjZdlx4/VzRNTBSD8je76sar/ +p4vYQdWlJwOjoH6hI2+LxPXS0TjHvPi2YndJ6t7A5cFXE+0HG8MbSpXHFtcCgYEA +h5LGxKY0yN4p9OPopSNZIDmztY1BLtC50WFbfpygXYseEA+iEIkos42qw9RKOTBV +1GuBi42W7vtqW8MAuiXsndKaliVgyk5GU4LqxWFrRWaWsioy57IchvdgI2H3mlwz +rgs46BaQ3kahtIb6wKC8nJ3FTaft6AFCC6IOmcG1UiECgYA1zxm5ttIcKR39cQG2 +t4WbHcAlFnMER4SXB34Bfj0WKclgLWG+RoNcke0smmGqdV3r3CmNiK0sIsfL+w2z +1rXls+laro+cg+jZaG0J7O7b36+vJQOwElQ16yb3qZ9SJ0w8iguUBbcklcPhsaXK +Snuim3XDr9wHW63afVWjE+RN3w== +-----END PRIVATE KEY----- \ No newline at end of file
管理渠道凭证、回调地址和测试交易参数。
{{ selected.description || selected.provider }}
{{ fieldHint(field, selected) }}
{{ stage.message || stageStatusText(stage.status) }}