基于WXPayUtility调通第一个品牌API
更新时间:2025.11.11一、概述
本文档提供基于官方提供的工具类 WXPayUtility,调用“设置商品券事件通知地址”接口的示例。
二、准备开发参数
商户调用接口前需要需要先准备开发必要参数:brand_id(品牌ID)、品牌API证书以及证书序列号、微信支付公钥以及公钥ID等信息,具体可参考:开发必要参数说明
三、接口调用示例
1、引入依赖
工具类 WXPayUtility 依赖以下第三方库:
Google Gson:用于 JSON 数据的序列化和反序列化。
OkHttp:用于 HTTP 请求处理。
你可以通过 Maven 或 Gradle 来引入这些依赖。其中依赖的版本号可自行选择版本进行填写,本文仅为示例。
如果你使用的 Gradle,请在 build.gradle 中加入:
1implementation 'com.google.code.gson:gson:2.13.2' 2implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14'
如果你使用的 Maven,请在 pom.xml 中加入:
1<!-- Google Gson --> 2<dependency> 3 <groupId>com.google.code.gson</groupId> 4 <artifactId>gson</artifactId> 5 <version>2.13.2</version> 6</dependency> 7<!-- OkHttp --> 8<dependency> 9 <groupId>com.squareup.okhttp3</groupId> 10 <artifactId>okhttp</artifactId> 11 <version>5.0.0-alpha.14</version> 12</dependency>
2、编写工具类代码
创建WXPayUtility类,复制以下代码内容进去并补充当前类的包路径:
1import com.google.gson.ExclusionStrategy; 2import com.google.gson.FieldAttributes; 3import com.google.gson.Gson; 4import com.google.gson.GsonBuilder; 5import com.google.gson.JsonElement; 6import com.google.gson.JsonObject; 7import com.google.gson.JsonSyntaxException; 8import com.google.gson.annotations.Expose; 9import com.google.gson.annotations.SerializedName; 10import okhttp3.Headers; 11import okhttp3.Response; 12import okio.BufferedSource; 13 14import javax.crypto.BadPaddingException; 15import javax.crypto.Cipher; 16import javax.crypto.IllegalBlockSizeException; 17import javax.crypto.NoSuchPaddingException; 18import javax.crypto.spec.GCMParameterSpec; 19import javax.crypto.spec.SecretKeySpec; 20import java.io.IOException; 21import java.io.UncheckedIOException; 22import java.io.UnsupportedEncodingException; 23import java.net.URLEncoder; 24import java.nio.charset.StandardCharsets; 25import java.nio.file.Files; 26import java.nio.file.Paths; 27import java.security.InvalidAlgorithmParameterException; 28import java.security.InvalidKeyException; 29import java.security.KeyFactory; 30import java.security.NoSuchAlgorithmException; 31import java.security.PrivateKey; 32import java.security.PublicKey; 33import java.security.SecureRandom; 34import java.security.Signature; 35import java.security.SignatureException; 36import java.security.spec.InvalidKeySpecException; 37import java.security.spec.PKCS8EncodedKeySpec; 38import java.security.spec.X509EncodedKeySpec; 39import java.time.DateTimeException; 40import java.time.Duration; 41import java.time.Instant; 42import java.util.Base64; 43import java.util.Map; 44import java.util.Objects; 45 46public class WXPayBrandUtility { 47 private static final Gson gson = new GsonBuilder() 48 .disableHtmlEscaping() 49 .addSerializationExclusionStrategy(new ExclusionStrategy() { 50 @Override 51 public boolean shouldSkipField(FieldAttributes fieldAttributes) { 52 final Expose expose = fieldAttributes.getAnnotation(Expose.class); 53 return expose != null && !expose.serialize(); 54 } 55 56 @Override 57 public boolean shouldSkipClass(Class<?> aClass) { 58 return false; 59 } 60 }) 61 .addDeserializationExclusionStrategy(new ExclusionStrategy() { 62 @Override 63 public boolean shouldSkipField(FieldAttributes fieldAttributes) { 64 final Expose expose = fieldAttributes.getAnnotation(Expose.class); 65 return expose != null && !expose.deserialize(); 66 } 67 68 @Override 69 public boolean shouldSkipClass(Class<?> aClass) { 70 return false; 71 } 72 }) 73 .create(); 74 private static final char[] SYMBOLS = 75 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); 76 private static final SecureRandom random = new SecureRandom(); 77 78 /** 79 * 将 Object 转换为 JSON 字符串 80 */ 81 public static String toJson(Object object) { 82 return gson.toJson(object); 83 } 84 85 /** 86 * 将 JSON 字符串解析为特定类型的实例 87 */ 88 public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException { 89 return gson.fromJson(json, classOfT); 90 } 91 92 /** 93 * 从公私钥文件路径中读取文件内容 94 * 95 * @param keyPath 文件路径 96 * @return 文件内容 97 */ 98 private static String readKeyStringFromPath(String keyPath) { 99 try { 100 return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8); 101 } catch (IOException e) { 102 throw new UncheckedIOException(e); 103 } 104 } 105 106 /** 107 * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象 108 * 109 * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头 110 * @return PrivateKey 对象 111 */ 112 public static PrivateKey loadPrivateKeyFromString(String keyString) { 113 try { 114 keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "") 115 .replace("-----END PRIVATE KEY-----", "") 116 .replaceAll("\\s+", ""); 117 return KeyFactory.getInstance("RSA").generatePrivate( 118 new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString))); 119 } catch (NoSuchAlgorithmException e) { 120 throw new UnsupportedOperationException(e); 121 } catch (InvalidKeySpecException e) { 122 throw new IllegalArgumentException(e); 123 } 124 } 125 126 /** 127 * 从 PKCS#8 格式的私钥文件中加载私钥 128 * 129 * @param keyPath 私钥文件路径 130 * @return PrivateKey 对象 131 */ 132 public static PrivateKey loadPrivateKeyFromPath(String keyPath) { 133 return loadPrivateKeyFromString(readKeyStringFromPath(keyPath)); 134 } 135 136 /** 137 * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象 138 * 139 * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头 140 * @return PublicKey 对象 141 */ 142 public static PublicKey loadPublicKeyFromString(String keyString) { 143 try { 144 keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "") 145 .replace("-----END PUBLIC KEY-----", "") 146 .replaceAll("\\s+", ""); 147 return KeyFactory.getInstance("RSA").generatePublic( 148 new X509EncodedKeySpec(Base64.getDecoder().decode(keyString))); 149 } catch (NoSuchAlgorithmException e) { 150 throw new UnsupportedOperationException(e); 151 } catch (InvalidKeySpecException e) { 152 throw new IllegalArgumentException(e); 153 } 154 } 155 156 /** 157 * 从 PKCS#8 格式的公钥文件中加载公钥 158 * 159 * @param keyPath 公钥文件路径 160 * @return PublicKey 对象 161 */ 162 public static PublicKey loadPublicKeyFromPath(String keyPath) { 163 return loadPublicKeyFromString(readKeyStringFromPath(keyPath)); 164 } 165 166 /** 167 * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途 168 */ 169 public static String createNonce(int length) { 170 char[] buf = new char[length]; 171 for (int i = 0; i < length; ++i) { 172 buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)]; 173 } 174 return new String(buf); 175 } 176 177 /** 178 * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密 179 * 180 * @param publicKey 加密用公钥对象 181 * @param plaintext 待加密明文 182 * @return 加密后密文 183 */ 184 public static String encrypt(PublicKey publicKey, String plaintext) { 185 final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; 186 187 try { 188 Cipher cipher = Cipher.getInstance(transformation); 189 cipher.init(Cipher.ENCRYPT_MODE, publicKey); 190 return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8))); 191 } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { 192 throw new IllegalArgumentException("The current Java environment does not support " + transformation, e); 193 } catch (InvalidKeyException e) { 194 throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e); 195 } catch (BadPaddingException | IllegalBlockSizeException e) { 196 throw new IllegalArgumentException("Plaintext is too long", e); 197 } 198 } 199 200 public static String aesAeadDecrypt(byte[] key, byte[] associatedData, byte[] nonce, 201 byte[] ciphertext) { 202 final String transformation = "AES/GCM/NoPadding"; 203 final String algorithm = "AES"; 204 final int tagLengthBit = 128; 205 206 try { 207 Cipher cipher = Cipher.getInstance(transformation); 208 cipher.init( 209 Cipher.DECRYPT_MODE, 210 new SecretKeySpec(key, algorithm), 211 new GCMParameterSpec(tagLengthBit, nonce)); 212 if (associatedData != null) { 213 cipher.updateAAD(associatedData); 214 } 215 return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8); 216 } catch (InvalidKeyException 217 | InvalidAlgorithmParameterException 218 | BadPaddingException 219 | IllegalBlockSizeException 220 | NoSuchAlgorithmException 221 | NoSuchPaddingException e) { 222 throw new IllegalArgumentException(String.format("AesAeadDecrypt with %s Failed", 223 transformation), e); 224 } 225 } 226 227 /** 228 * 使用私钥按照指定算法进行签名 229 * 230 * @param message 待签名串 231 * @param algorithm 签名算法,如 SHA256withRSA 232 * @param privateKey 签名用私钥对象 233 * @return 签名结果 234 */ 235 public static String sign(String message, String algorithm, PrivateKey privateKey) { 236 byte[] sign; 237 try { 238 Signature signature = Signature.getInstance(algorithm); 239 signature.initSign(privateKey); 240 signature.update(message.getBytes(StandardCharsets.UTF_8)); 241 sign = signature.sign(); 242 } catch (NoSuchAlgorithmException e) { 243 throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e); 244 } catch (InvalidKeyException e) { 245 throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e); 246 } catch (SignatureException e) { 247 throw new RuntimeException("An error occurred during the sign process.", e); 248 } 249 return Base64.getEncoder().encodeToString(sign); 250 } 251 252 /** 253 * 使用公钥按照特定算法验证签名 254 * 255 * @param message 待签名串 256 * @param signature 待验证的签名内容 257 * @param algorithm 签名算法,如:SHA256withRSA 258 * @param publicKey 验签用公钥对象 259 * @return 签名验证是否通过 260 */ 261 public static boolean verify(String message, String signature, String algorithm, 262 PublicKey publicKey) { 263 try { 264 Signature sign = Signature.getInstance(algorithm); 265 sign.initVerify(publicKey); 266 sign.update(message.getBytes(StandardCharsets.UTF_8)); 267 return sign.verify(Base64.getDecoder().decode(signature)); 268 } catch (SignatureException e) { 269 return false; 270 } catch (InvalidKeyException e) { 271 throw new IllegalArgumentException("verify uses an illegal publickey.", e); 272 } catch (NoSuchAlgorithmException e) { 273 throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e); 274 } 275 } 276 277 /** 278 * 根据品牌API请求签名规则构造 Authorization 签名 279 * 280 * @param brand_id 品牌ID 281 * @param certificateSerialNo 品牌API证书序列号 282 * @param privateKey 品牌API证书私钥 283 * @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE 284 * @param uri 请求接口的URL 285 * @param body 请求接口的Body 286 * @return 构造好的品牌API Authorization 头 287 */ 288 public static String buildAuthorization(String brand_id, String certificateSerialNo, 289 PrivateKey privateKey, 290 String method, String uri, String body) { 291 String nonce = createNonce(32); 292 long timestamp = Instant.now().getEpochSecond(); 293 294 String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce, 295 body == null ? "" : body); 296 297 String signature = sign(message, "SHA256withRSA", privateKey); 298 299 return String.format( 300 "WECHATPAY-BRAND-SHA256-RSA2048 brand_id=\"%s\",nonce_str=\"%s\",signature=\"%s\"," + 301 "timestamp=\"%d\",serial_no=\"%s\"", 302 brand_id, nonce, signature, timestamp, certificateSerialNo); 303 } 304 305 /** 306 * 对参数进行 URL 编码 307 * 308 * @param content 参数内容 309 * @return 编码后的内容 310 */ 311 public static String urlEncode(String content) { 312 try { 313 return URLEncoder.encode(content, StandardCharsets.UTF_8.name()); 314 } catch (UnsupportedEncodingException e) { 315 throw new RuntimeException(e); 316 } 317 } 318 319 /** 320 * 对参数Map进行 URL 编码,生成 QueryString 321 * 322 * @param params Query参数Map 323 * @return QueryString 324 */ 325 public static String urlEncode(Map<String, Object> params) { 326 if (params == null || params.isEmpty()) { 327 return ""; 328 } 329 330 int index = 0; 331 StringBuilder result = new StringBuilder(); 332 for (Map.Entry<String, Object> entry : params.entrySet()) { 333 result.append(entry.getKey()) 334 .append("=") 335 .append(urlEncode(entry.getValue().toString())); 336 index++; 337 if (index < params.size()) { 338 result.append("&"); 339 } 340 } 341 return result.toString(); 342 } 343 344 /** 345 * 从应答中提取 Body 346 * 347 * @param response HTTP 请求应答对象 348 * @return 应答中的Body内容,Body为空时返回空字符串 349 */ 350 public static String extractBody(Response response) { 351 if (response.body() == null) { 352 return ""; 353 } 354 355 try { 356 BufferedSource source = response.body().source(); 357 return source.readUtf8(); 358 } catch (IOException e) { 359 throw new RuntimeException(String.format("An error occurred during reading response body. " + 360 "Status: %d", response.code()), e); 361 } 362 } 363 364 /** 365 * 根据品牌API应答验签规则对应答签名进行验证,验证不通过时抛出异常 366 * 367 * @param wechatpayPublicKeyId 微信支付公钥ID 368 * @param wechatpayPublicKey 微信支付公钥对象 369 * @param headers 微信支付应答 Header 列表 370 * @param body 微信支付应答 Body 371 */ 372 public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey, 373 Headers headers, 374 String body) { 375 String timestamp = headers.get("Wechatpay-Timestamp"); 376 String requestId = headers.get("Request-ID"); 377 try { 378 Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); 379 // 拒绝过期请求 380 if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { 381 throw new IllegalArgumentException( 382 String.format("Validate response failed, timestamp[%s] is expired, request-id[%s]", 383 timestamp, requestId)); 384 } 385 } catch (DateTimeException | NumberFormatException e) { 386 throw new IllegalArgumentException( 387 String.format("Validate response failed, timestamp[%s] is invalid, request-id[%s]", 388 timestamp, requestId)); 389 } 390 String serialNumber = headers.get("Wechatpay-Serial"); 391 if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { 392 throw new IllegalArgumentException( 393 String.format("Validate response failed, Invalid Wechatpay-Serial, Local: %s, Remote: " + 394 "%s", wechatpayPublicKeyId, serialNumber)); 395 } 396 397 String signature = headers.get("Wechatpay-Signature"); 398 String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), 399 body == null ? "" : body); 400 401 boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); 402 if (!success) { 403 throw new IllegalArgumentException( 404 String.format("Validate response failed,the WechatPay signature is incorrect.%n" 405 + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]", 406 headers.get("Request-ID"), headers, body)); 407 } 408 } 409 410 /** 411 * 根据品牌API通知验签规则对通知签名进行验证,验证不通过时抛出异常 412 * @param wechatpayPublicKeyId 微信支付公钥ID 413 * @param wechatpayPublicKey 微信支付公钥对象 414 * @param headers 微信支付通知 Header 列表 415 * @param body 微信支付通知 Body 416 */ 417 public static void validateNotification(String wechatpayPublicKeyId, 418 PublicKey wechatpayPublicKey, Headers headers, 419 String body) { 420 String timestamp = headers.get("Wechatpay-Timestamp"); 421 try { 422 Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); 423 // 拒绝过期请求 424 if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { 425 throw new IllegalArgumentException( 426 String.format("Validate notification failed, timestamp[%s] is expired", timestamp)); 427 } 428 } catch (DateTimeException | NumberFormatException e) { 429 throw new IllegalArgumentException( 430 String.format("Validate notification failed, timestamp[%s] is invalid", timestamp)); 431 } 432 String serialNumber = headers.get("Wechatpay-Serial"); 433 if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { 434 throw new IllegalArgumentException( 435 String.format("Validate notification failed, Invalid Wechatpay-Serial, Local: %s, " + 436 "Remote: %s", 437 wechatpayPublicKeyId, 438 serialNumber)); 439 } 440 441 String signature = headers.get("Wechatpay-Signature"); 442 String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), 443 body == null ? "" : body); 444 445 boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); 446 if (!success) { 447 throw new IllegalArgumentException( 448 String.format("Validate notification failed, WechatPay signature is incorrect.\n" 449 + "responseHeader[%s]\tresponseBody[%.1024s]", 450 headers, body)); 451 } 452 } 453 454 /** 455 * 对微信支付通知进行签名验证、解析,同时将业务数据解密。验签名失败、解析失败、解密失败时抛出异常 456 * @param apiv3Key 品牌API密钥 457 * @param wechatpayPublicKeyId 微信支付公钥ID 458 * @param wechatpayPublicKey 微信支付公钥对象 459 * @param headers 微信支付应答 Header 列表 460 * @param body 微信支付应答 Body 461 * @return 解析后的通知内容,解密后的业务数据可以使用 Notification.getPlaintext() 访问 462 */ 463 public static Notification parseNotification(String apiv3Key, String wechatpayPublicKeyId, 464 PublicKey wechatpayPublicKey, Headers headers, 465 String body) { 466 validateNotification(wechatpayPublicKeyId, wechatpayPublicKey, headers, body); 467 Notification notification = gson.fromJson(body, Notification.class); 468 notification.decrypt(apiv3Key); 469 return notification; 470 } 471 472 /** 473 * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常 474 */ 475 public static class ApiException extends RuntimeException { 476 private static final long serialVersionUID = 2261086748874802175L; 477 478 private final int statusCode; 479 private final String body; 480 private final Headers headers; 481 private final String errorCode; 482 private final String errorMessage; 483 484 public ApiException(int statusCode, String body, Headers headers) { 485 super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode, 486 body, headers)); 487 this.statusCode = statusCode; 488 this.body = body; 489 this.headers = headers; 490 491 if (body != null && !body.isEmpty()) { 492 JsonElement code; 493 JsonElement message; 494 495 try { 496 JsonObject jsonObject = gson.fromJson(body, JsonObject.class); 497 code = jsonObject.get("code"); 498 message = jsonObject.get("message"); 499 } catch (JsonSyntaxException ignored) { 500 code = null; 501 message = null; 502 } 503 this.errorCode = code == null ? null : code.getAsString(); 504 this.errorMessage = message == null ? null : message.getAsString(); 505 } else { 506 this.errorCode = null; 507 this.errorMessage = null; 508 } 509 } 510 511 /** 512 * 获取 HTTP 应答状态码 513 */ 514 public int getStatusCode() { 515 return statusCode; 516 } 517 518 /** 519 * 获取 HTTP 应答包体内容 520 */ 521 public String getBody() { 522 return body; 523 } 524 525 /** 526 * 获取 HTTP 应答 Header 527 */ 528 public Headers getHeaders() { 529 return headers; 530 } 531 532 /** 533 * 获取 错误码 (错误应答中的 code 字段) 534 */ 535 public String getErrorCode() { 536 return errorCode; 537 } 538 539 /** 540 * 获取 错误消息 (错误应答中的 message 字段) 541 */ 542 public String getErrorMessage() { 543 return errorMessage; 544 } 545 } 546 547 public static class Notification { 548 @SerializedName("id") 549 private String id; 550 @SerializedName("create_time") 551 private String createTime; 552 @SerializedName("event_type") 553 private String eventType; 554 @SerializedName("resource_type") 555 private String resourceType; 556 @SerializedName("summary") 557 private String summary; 558 @SerializedName("resource") 559 private Resource resource; 560 private String plaintext; 561 562 public String getId() { 563 return id; 564 } 565 566 public String getCreateTime() { 567 return createTime; 568 } 569 570 public String getEventType() { 571 return eventType; 572 } 573 574 public String getResourceType() { 575 return resourceType; 576 } 577 578 public String getSummary() { 579 return summary; 580 } 581 582 public Resource getResource() { 583 return resource; 584 } 585 586 /** 587 * 获取解密后的业务数据(JSON字符串,需要自行解析) 588 */ 589 public String getPlaintext() { 590 return plaintext; 591 } 592 593 private void validate() { 594 if (resource == null) { 595 throw new IllegalArgumentException("Missing required field `resource` in notification"); 596 } 597 resource.validate(); 598 } 599 600 /** 601 * 使用 APIv3Key 对通知中的业务数据解密,解密结果可以通过 getPlainText 访问。 602 * 外部拿到的 Notification 一定是解密过的,因此本方法没有设置为 public 603 * @param apiv3Key 品牌API密钥 604 */ 605 private void decrypt(String apiv3Key) { 606 validate(); 607 608 plaintext = aesAeadDecrypt( 609 apiv3Key.getBytes(StandardCharsets.UTF_8), 610 resource.associatedData.getBytes(StandardCharsets.UTF_8), 611 resource.nonce.getBytes(StandardCharsets.UTF_8), 612 Base64.getDecoder().decode(resource.ciphertext) 613 ); 614 } 615 616 public static class Resource { 617 @SerializedName("algorithm") 618 private String algorithm; 619 620 @SerializedName("ciphertext") 621 private String ciphertext; 622 623 @SerializedName("associated_data") 624 private String associatedData; 625 626 @SerializedName("nonce") 627 private String nonce; 628 629 @SerializedName("original_type") 630 private String originalType; 631 632 public String getAlgorithm() { 633 return algorithm; 634 } 635 636 public String getCiphertext() { 637 return ciphertext; 638 } 639 640 public String getAssociatedData() { 641 return associatedData; 642 } 643 644 public String getNonce() { 645 return nonce; 646 } 647 648 public String getOriginalType() { 649 return originalType; 650 } 651 652 private void validate() { 653 if (algorithm == null || algorithm.isEmpty()) { 654 throw new IllegalArgumentException("Missing required field `algorithm` in Notification" + 655 ".Resource"); 656 } 657 if (!Objects.equals(algorithm, "AEAD_AES_256_GCM")) { 658 throw new IllegalArgumentException(String.format("Unsupported `algorithm`[%s] in " + 659 "Notification.Resource", algorithm)); 660 } 661 662 if (ciphertext == null || ciphertext.isEmpty()) { 663 throw new IllegalArgumentException("Missing required field `ciphertext` in Notification" + 664 ".Resource"); 665 } 666 667 if (associatedData == null || associatedData.isEmpty()) { 668 throw new IllegalArgumentException("Missing required field `associatedData` in " + 669 "Notification.Resource"); 670 } 671 672 if (nonce == null || nonce.isEmpty()) { 673 throw new IllegalArgumentException("Missing required field `nonce` in Notification" + 674 ".Resource"); 675 } 676 677 if (originalType == null || originalType.isEmpty()) { 678 throw new IllegalArgumentException("Missing required field `originalType` in " + 679 "Notification.Resource"); 680 } 681 } 682 } 683 } 684}
|
3、调用接口示例
创建SetNotifyConfig类,复制以下代码内容进去并补充当前类的包路径,最后根据注释替换对应参数:
1import com.google.gson.annotations.SerializedName; 2import com.google.gson.annotations.Expose; 3import okhttp3.MediaType; 4import okhttp3.OkHttpClient; 5import okhttp3.Request; 6import okhttp3.RequestBody; 7import okhttp3.Response; 8 9import java.io.IOException; 10import java.io.UncheckedIOException; 11import java.security.PrivateKey; 12import java.security.PublicKey; 13import java.util.ArrayList; 14import java.util.HashMap; 15import java.util.List; 16import java.util.Map; 17 18/** 19 * 设置商品券事件通知地址 20 */ 21public class SetNotifyConfig { 22 private static String HOST = "https://api.mch.weixin.qq.com"; 23 private static String METHOD = "POST"; 24 private static String PATH = "/brand/marketing/product-coupon/notify-configs"; 25 26 public static void main(String[] args) { 27 // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/brand/4015415289 28 SetNotifyConfig client = new SetNotifyConfig( 29 "xxxxxxxx", // 品牌ID,是由微信支付系统生成并分配给每个品牌方的唯一标识符,品牌ID获取方式参考 https://pay.weixin.qq.com/doc/brand/4015415289 30 "1DDE55AD98Exxxxxxxxxx", // 品牌API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/brand/4015407570 31 "/path/to/apiclient_key.pem", // 品牌API证书私钥文件路径,本地文件路径 32 "PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/brand/4015453439 33 "/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径 34 ); 35 36 SetNotifyConfigRequest request = new SetNotifyConfigRequest(); 37 request.notifyUrl = "https://www.xxxxxxx.com/notify"; //需要设置的回调地址,回调地址设置规范请参考 https://pay.weixin.qq.com/doc/brand/4015407551 38 try { 39 SetNotifyConfigResponse response = client.run(request); 40 // TODO: 请求成功,继续业务逻辑 41 System.out.println(response); 42 } catch (WXPayBrandUtility.ApiException e) { 43 // TODO: 请求失败,根据状态码执行不同的逻辑 44 e.printStackTrace(); 45 } 46 } 47 48 public SetNotifyConfigResponse run(SetNotifyConfigRequest request) { 49 String uri = PATH; 50 String reqBody = WXPayBrandUtility.toJson(request); 51 52 Request.Builder reqBuilder = new Request.Builder().url(HOST + uri); 53 reqBuilder.addHeader("Accept", "application/json"); 54 reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId); 55 reqBuilder.addHeader("Authorization", WXPayBrandUtility.buildAuthorization(brand_id, certificateSerialNo,privateKey, METHOD, uri, reqBody)); 56 reqBuilder.addHeader("Content-Type", "application/json"); 57 RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody); 58 reqBuilder.method(METHOD, requestBody); 59 Request httpRequest = reqBuilder.build(); 60 61 // 发送HTTP请求 62 OkHttpClient client = new OkHttpClient.Builder().build(); 63 try (Response httpResponse = client.newCall(httpRequest).execute()) { 64 String respBody = WXPayBrandUtility.extractBody(httpResponse); 65 if (httpResponse.code() >= 200 && httpResponse.code() < 300) { 66 // 2XX 成功,验证应答签名 67 WXPayBrandUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey, 68 httpResponse.headers(), respBody); 69 70 // 从HTTP应答报文构建返回数据 71 return WXPayBrandUtility.fromJson(respBody, SetNotifyConfigResponse.class); 72 } else { 73 throw new WXPayBrandUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers()); 74 } 75 } catch (IOException e) { 76 throw new UncheckedIOException("Sending request to " + uri + " failed.", e); 77 } 78 } 79 80 private final String brand_id; 81 private final String certificateSerialNo; 82 private final PrivateKey privateKey; 83 private final String wechatPayPublicKeyId; 84 private final PublicKey wechatPayPublicKey; 85 86 public SetNotifyConfig(String brand_id, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) { 87 this.brand_id = brand_id; 88 this.certificateSerialNo = certificateSerialNo; 89 this.privateKey = WXPayBrandUtility.loadPrivateKeyFromPath(privateKeyFilePath); 90 this.wechatPayPublicKeyId = wechatPayPublicKeyId; 91 this.wechatPayPublicKey = WXPayBrandUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath); 92 } 93 94 public static class SetNotifyConfigRequest { 95 @SerializedName("notify_url") 96 public String notifyUrl; 97 } 98 99 public static class SetNotifyConfigResponse { 100 @SerializedName("notify_url") 101 public String notifyUrl; 102 103 @SerializedName("update_time") 104 public String updateTime; 105 } 106 107}
|
若请求成功,请求主体将返回以下信息:
1{"notify_url" : "你的回调地址","update_time" : "更新时间"}若请求失败,请求主体将返回报错,将返回以下格式的信息,请根据具体的错误描述进行排查。
1{"code":"错误码","message":"错误描述"} 为了信息安全,请求成功后还需使用公钥对返回信息进行验签,确认返回信息来自微信支付。
应答验签validateResponse()若无报错即为验签通过。
文档是否有帮助

