服务商须获得品牌授权,门店信息创建和更新可能进入审核流程。
+ * @see 开发指引 + */ +public interface BrandStoreService { + /** + * 创建门店并获取微信支付门店ID。 + * @param request 品牌ID和门店信息,地址必填 + * @return 门店信息和审核状态 + * @throws WxPayException 微信支付接口调用失败 + */ + BrandStoreResult create(BrandStoreRequest request) throws WxPayException; + + /** + * 查询品牌门店及审核状态。 + * @param brandId 品牌ID + * @param storeId 微信支付门店ID + * @return 门店信息、审核状态和收款商户 + * @throws WxPayException 微信支付接口调用失败 + */ + BrandStoreResult query(String brandId, String storeId) throws WxPayException; + + /** + * 更新门店信息,生效门店的营业状态不受审核影响。 + * @param storeId 微信支付门店ID + * @param request 品牌ID和待更新的门店信息 + * @return 更新后的门店信息和审核状态 + * @throws WxPayException 微信支付接口调用失败 + */ + BrandStoreResult update(String storeId, BrandStoreRequest request) throws WxPayException; + + /** + * 删除已关闭的品牌门店。 + * @param brandId 品牌ID + * @param storeId 微信支付门店ID + * @throws WxPayException 微信支付接口调用失败 + */ + void delete(String brandId, String storeId) throws WxPayException; + + /** + * 绑定品牌下已有的收款商户号。 + * @param storeId 微信支付门店ID + * @param request 品牌ID、商户号和主体名称 + * @return 收款商户绑定状态 + * @throws WxPayException 微信支付接口调用失败 + */ + BrandStoreBindRecipientResult bindRecipient(String storeId, BrandStoreBindRecipientRequest request) throws WxPayException; + + /** + * 解绑收款商户号。 + * @param storeId 微信支付门店ID + * @param request 品牌ID和商户号 + * @return 解绑结果,失败时包含失败原因 + * @throws WxPayException 微信支付接口调用失败 + */ + BrandStoreUnbindRecipientResult unbindRecipient(String storeId, BrandStoreUnbindRecipientRequest request) throws WxPayException; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java index d0d43285d..89f66efed 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java @@ -387,6 +387,30 @@ default WxPayService switchoverTo(String mchIdOrConfigKey) { */ BrandMerchantTransferService getBrandMerchantTransferService(); + /** + * 获取连锁品牌入驻服务。 + * @return 品牌入驻服务 + */ + default BrandApplymentService getBrandApplymentService() { + return new com.github.binarywang.wxpay.service.impl.BrandApplymentServiceImpl(this); + } + + /** + * 获取连锁品牌门店服务。 + * @return 品牌门店服务 + */ + default BrandStoreService getBrandStoreService() { + return new com.github.binarywang.wxpay.service.impl.BrandStoreServiceImpl(this); + } + + /** + * 获取连锁品牌分账服务。 + * @return 品牌分账服务 + */ + default BrandProfitSharingService getBrandProfitSharingService() { + return new com.github.binarywang.wxpay.service.impl.BrandProfitSharingServiceImpl(this); + } + /** * 获取微信支付预约扣费服务类 (连续包月功能) * diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java index 18e9b05b1..2dcb418cd 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java @@ -133,6 +133,15 @@ public abstract class BaseWxPayServiceImpl implements WxPayService { @Getter private final BrandMerchantTransferService brandMerchantTransferService = new BrandMerchantTransferServiceImpl(this); + @Getter + private final BrandApplymentService brandApplymentService = new BrandApplymentServiceImpl(this); + + @Getter + private final BrandStoreService brandStoreService = new BrandStoreServiceImpl(this); + + @Getter + private final BrandProfitSharingService brandProfitSharingService = new BrandProfitSharingServiceImpl(this); + @Getter private final SubscriptionBillingService subscriptionBillingService = new SubscriptionBillingServiceImpl(this); diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BrandApplymentServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BrandApplymentServiceImpl.java new file mode 100644 index 000000000..f6864c706 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BrandApplymentServiceImpl.java @@ -0,0 +1,49 @@ +package com.github.binarywang.wxpay.service.impl; + +import com.github.binarywang.wxpay.bean.brandapplyment.BrandApplymentCancelRequest; +import com.github.binarywang.wxpay.bean.brandapplyment.BrandApplymentQueryResult; +import com.github.binarywang.wxpay.bean.brandapplyment.BrandApplymentRequest; +import com.github.binarywang.wxpay.bean.brandapplyment.BrandApplymentResult; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.BrandApplymentService; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.v3.util.RsaCryptoUtil; +import com.google.gson.Gson; +import lombok.RequiredArgsConstructor; + +/** 连锁品牌入驻申请 API 实现。 */ +@RequiredArgsConstructor +public class BrandApplymentServiceImpl implements BrandApplymentService { + private static final Gson GSON = new Gson(); + private final WxPayService payService; + + @Override + public BrandApplymentResult createApplyment(BrandApplymentRequest request) throws WxPayException { + BrandApplymentRequest encrypted = GSON.fromJson(GSON.toJson(request), BrandApplymentRequest.class); + RsaCryptoUtil.encryptFields(encrypted, this.payService.getConfig().getVerifier().getValidCertificate()); + String response = this.payService.postV3WithWechatpaySerial( + this.payService.getPayBaseUrl() + "/v3/brand/applyments", GSON.toJson(encrypted)); + return GSON.fromJson(response, BrandApplymentResult.class); + } + + @Override + public BrandApplymentResult cancelApplyment(BrandApplymentCancelRequest request) throws WxPayException { + String response = this.payService.postV3(this.payService.getPayBaseUrl() + + "/v3/brand/applyments/cancel-applyment", GSON.toJson(request)); + return GSON.fromJson(response, BrandApplymentResult.class); + } + + @Override + public BrandApplymentQueryResult queryApplymentByBusinessCode(String businessCode) throws WxPayException { + String response = this.payService.getV3(this.payService.getPayBaseUrl() + + "/v3/brand/applyments/business-code/" + businessCode); + return GSON.fromJson(response, BrandApplymentQueryResult.class); + } + + @Override + public BrandApplymentQueryResult queryApplymentByApplymentId(String applymentId) throws WxPayException { + String response = this.payService.getV3(this.payService.getPayBaseUrl() + + "/v3/brand/applyments/applyment-id/" + applymentId); + return GSON.fromJson(response, BrandApplymentQueryResult.class); + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BrandProfitSharingServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BrandProfitSharingServiceImpl.java new file mode 100644 index 000000000..4d28b596a --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BrandProfitSharingServiceImpl.java @@ -0,0 +1,126 @@ +package com.github.binarywang.wxpay.service.impl; + +import com.github.binarywang.wxpay.bean.brandprofitsharing.*; +import com.github.binarywang.wxpay.bean.notify.SignatureHeader; +import com.github.binarywang.wxpay.bean.profitsharing.notify.ProfitSharingNotifyV3Result; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.BrandProfitSharingService; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.v3.util.RsaCryptoUtil; +import com.google.gson.Gson; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** 连锁品牌分账接口实现。 */ +public class BrandProfitSharingServiceImpl implements BrandProfitSharingService { + private static final Gson GSON = new Gson(); + private static final String PATH = "/v3/brand/profitsharing"; + private final WxPayService payService; + + /** + * 创建连锁品牌分账服务。 + * @param payService 微信支付服务,提供 API v3 配置和 HTTP 执行能力 + */ + public BrandProfitSharingServiceImpl(WxPayService payService) { + this.payService = payService; + } + + @Override + public BrandProfitSharingResult profitSharing(BrandProfitSharingRequest request) throws WxPayException { + // 保留原始姓名,允许调用方在暂时失败后使用同一请求对象重试。 + BrandProfitSharingRequest encrypted = GSON.fromJson(GSON.toJson(request), BrandProfitSharingRequest.class); + RsaCryptoUtil.encryptFields(encrypted, this.payService.getConfig().getVerifier().getValidCertificate()); + return GSON.fromJson(this.payService.postV3WithWechatpaySerial(url("/orders"), GSON.toJson(encrypted)), + BrandProfitSharingResult.class); + } + + @Override + public BrandProfitSharingResult query(BrandProfitSharingQueryRequest request) throws WxPayException { + String path = "/orders?sub_mchid=" + encode(request.getSubMchId()) + + "&transaction_id=" + encode(request.getTransactionId()) + + "&out_order_no=" + encode(request.getOutOrderNo()); + return GSON.fromJson(this.payService.getV3(url(path)), BrandProfitSharingResult.class); + } + + @Override + public BrandProfitSharingReturnResult returnOrder(BrandProfitSharingReturnRequest request) throws WxPayException { + return GSON.fromJson(this.payService.postV3(url("/returnorders"), GSON.toJson(request)), + BrandProfitSharingReturnResult.class); + } + + @Override + public BrandProfitSharingReturnResult queryReturn(BrandProfitSharingReturnQueryRequest request) throws WxPayException { + String path = "/returnorders?sub_mchid=" + encode(request.getSubMchId()) + + "&out_return_no=" + encode(request.getOutReturnNo()); + if (request.getOrderId() != null) { + path += "&order_id=" + encode(request.getOrderId()); + } + if (request.getOutOrderNo() != null) { + path += "&out_order_no=" + encode(request.getOutOrderNo()); + } + return GSON.fromJson(this.payService.getV3(url(path)), BrandProfitSharingReturnResult.class); + } + + @Override + public BrandProfitSharingFinishResult finish(BrandProfitSharingFinishRequest request) throws WxPayException { + return GSON.fromJson(this.payService.postV3(url("/finish-order"), GSON.toJson(request)), + BrandProfitSharingFinishResult.class); + } + + @Override + public BrandProfitSharingAmountResult queryRemainingAmount(String transactionId) throws WxPayException { + return GSON.fromJson(this.payService.getV3(url("/orders/" + encode(transactionId) + "/amounts")), + BrandProfitSharingAmountResult.class); + } + + @Override + public BrandProfitSharingRatioResult queryMaxRatio(String brandMchId) throws WxPayException { + return GSON.fromJson(this.payService.getV3(url("/brand-configs/" + encode(brandMchId))), + BrandProfitSharingRatioResult.class); + } + + @Override + public BrandProfitSharingReceiverResult addReceiver(BrandProfitSharingReceiverRequest request) throws WxPayException { + return GSON.fromJson(postReceiver("/receivers/add", request), + BrandProfitSharingReceiverResult.class); + } + + @Override + public BrandProfitSharingReceiverResult deleteReceiver(BrandProfitSharingReceiverDeleteRequest request) throws WxPayException { + return GSON.fromJson(postReceiver("/receivers/delete", request), + BrandProfitSharingReceiverResult.class); + } + + @Override + public ProfitSharingNotifyV3Result parseNotifyResult(String notifyData, SignatureHeader header) throws WxPayException { + if (header == null) { + throw new WxPayException("通知签名头不能为空"); + } + return new ProfitSharingServiceImpl(this.payService).parseProfitSharingNotifyResult(notifyData, header); + } + + // 使用通用执行器,保留接口要求的明文实体,同时避免记录收款人信息。 + private String postReceiver(String path, Object request) throws WxPayException { + String address = url(path); + HttpPost post = new HttpPost(address); + post.setEntity(new StringEntity(GSON.toJson(request), ContentType.create("application/json", StandardCharsets.UTF_8))); + return this.payService.requestV3(address, post); + } + + private String url(String path) { + return this.payService.getPayBaseUrl() + PATH + path; + } + + private static String encode(String value) { + try { + return URLEncoder.encode(value, "UTF-8").replace("+", "%20").replace("*", "%2A"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BrandStoreServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BrandStoreServiceImpl.java new file mode 100644 index 000000000..1aeb76d91 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BrandStoreServiceImpl.java @@ -0,0 +1,83 @@ +package com.github.binarywang.wxpay.service.impl; + +import com.github.binarywang.wxpay.bean.brandstore.*; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.BrandStoreService; +import com.github.binarywang.wxpay.service.WxPayService; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import lombok.RequiredArgsConstructor; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** 品牌门店接口实现。 */ +@RequiredArgsConstructor +public class BrandStoreServiceImpl implements BrandStoreService { + private static final Gson GSON = new Gson(); + private static final String PATH = "/v3/brand/partner/store/brandstores"; + private final WxPayService wxPayService; + + @Override + public BrandStoreResult create(BrandStoreRequest request) throws WxPayException { + return GSON.fromJson(wxPayService.postV3(wxPayService.getPayBaseUrl() + PATH, GSON.toJson(request)), BrandStoreResult.class); + } + + @Override + public BrandStoreResult query(String brandId, String storeId) throws WxPayException { + return GSON.fromJson(wxPayService.getV3(storeUrl(storeId) + "?brand_id=" + encode(brandId)), BrandStoreResult.class); + } + + @Override + public BrandStoreResult update(String storeId, BrandStoreRequest request) throws WxPayException { + return GSON.fromJson(wxPayService.patchV3(storeUrl(storeId), GSON.toJson(request)), BrandStoreResult.class); + } + + @Override + public void delete(String brandId, String storeId) throws WxPayException { + String url = storeUrl(storeId); + // 删除接口的 brand_id 位于 JSON 请求体,普通 HttpDelete 不支持实体。 + HttpEntityEnclosingRequestBase request = new HttpEntityEnclosingRequestBase() { + @Override + public String getMethod() { + return "DELETE"; + } + }; + request.setURI(URI.create(url)); + request.setHeader("Accept", "application/json"); + JsonObject body = new JsonObject(); + body.addProperty("brand_id", brandId); + request.setEntity(new StringEntity(GSON.toJson(body), ContentType.APPLICATION_JSON)); + wxPayService.requestV3(url, request); + } + + @Override + public BrandStoreBindRecipientResult bindRecipient(String storeId, BrandStoreBindRecipientRequest request) throws WxPayException { + return GSON.fromJson(wxPayService.postV3(storeUrl(storeId) + "/bindrecipient", GSON.toJson(request)), BrandStoreBindRecipientResult.class); + } + + @Override + public BrandStoreUnbindRecipientResult unbindRecipient(String storeId, BrandStoreUnbindRecipientRequest request) throws WxPayException { + return GSON.fromJson(wxPayService.postV3(storeUrl(storeId) + "/unbindrecipient", GSON.toJson(request)), BrandStoreUnbindRecipientResult.class); + } + + private String storeUrl(String storeId) { + return wxPayService.getPayBaseUrl() + PATH + "/" + encode(storeId); + } + + private static String encode(String value) { + if (value == null) { + throw new IllegalArgumentException("微信支付接口必填参数不能为空"); + } + try { + return URLEncoder.encode(value, StandardCharsets.UTF_8.name()).replace("+", "%20"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java index 69a67ca0b..9772b9707 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java @@ -140,7 +140,11 @@ private String requestV3(String url, String requestStr, HttpRequestBase httpRequ } if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode || HttpStatus.SC_ACCEPTED == statusCode) { - this.logRequestAndResponse(url, requestStr, responseString); + if (java.net.URI.create(url).getPath().contains("/v3/brand/")) { + log.info("\n【请求地址】:{}\n【响应状态码】:{}", url, statusCode); + } else { + this.logRequestAndResponse(url, requestStr, responseString); + } return responseString; } @@ -148,7 +152,11 @@ private String requestV3(String url, String requestStr, HttpRequestBase httpRequ JsonObject jsonObject = GsonParser.parse(responseString); throw convertException(jsonObject); } catch (Exception e) { - this.logError(url, requestStr, e); + if (java.net.URI.create(url).getPath().contains("/v3/brand/")) { + log.error("\n【请求地址】:{}\n【异常类型】:{}", url, e.getClass().getSimpleName()); + } else { + this.logError(url, requestStr, e); + } throw (e instanceof WxPayException) ? (WxPayException) e : new WxPayException(e.getMessage(), e); } finally { httpRequestBase.releaseConnection(); @@ -180,7 +188,11 @@ public String postV3WithWechatpaySerial(String url, String requestStr) throws Wx if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode || (HttpStatus.SC_ACCEPTED == statusCode && url.endsWith("/codepay"))) { - this.logRequestAndResponse(url, requestStr, responseString); + if (java.net.URI.create(url).getPath().contains("/v3/brand/")) { + log.info("\n【请求地址】:{}\n【响应状态码】:{}", url, statusCode); + } else { + this.logRequestAndResponse(url, requestStr, responseString); + } return responseString; } @@ -188,7 +200,11 @@ public String postV3WithWechatpaySerial(String url, String requestStr) throws Wx JsonObject jsonObject = GsonParser.parse(responseString); throw convertException(jsonObject); } catch (Exception e) { - this.logError(url, requestStr, e); + if (java.net.URI.create(url).getPath().contains("/v3/brand/")) { + log.error("\n【请求地址】:{}\n【异常类型】:{}", url, e.getClass().getSimpleName()); + } else { + this.logError(url, requestStr, e); + } throw (e instanceof WxPayException) ? (WxPayException) e : new WxPayException(e.getMessage(), e); } finally { httpPost.releaseConnection(); @@ -215,7 +231,7 @@ public String requestV3(String url, HttpRequestBase httpRequest) throws WxPayExc } if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode || HttpStatus.SC_ACCEPTED == statusCode) { - log.info("\n【请求地址】:{}\n【响应数据】:{}", url, responseString); + log.info("\n【请求地址】:{}\n【响应状态码】:{}", url, statusCode); return responseString; } @@ -223,7 +239,7 @@ public String requestV3(String url, HttpRequestBase httpRequest) throws WxPayExc JsonObject jsonObject = GsonParser.parse(responseString); throw convertException(jsonObject); } catch (Exception e) { - log.error("\n【请求地址】:{}\n【异常信息】:{}", url, e.getMessage()); + log.error("\n【请求地址】:{}\n【异常类型】:{}", url, e.getClass().getSimpleName()); throw (e instanceof WxPayException) ? (WxPayException) e : new WxPayException(e.getMessage(), e); } finally { httpRequest.releaseConnection(); diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java index d28cbc890..d357beeee 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java @@ -137,7 +137,11 @@ private String requestV3(String url, String requestStr, HttpRequestBase httpRequ } if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode || HttpStatus.SC_ACCEPTED == statusCode) { - this.logRequestAndResponse(url, requestStr, responseString); + if (java.net.URI.create(url).getPath().contains("/v3/brand/")) { + log.info("\n【请求地址】:{}\n【响应状态码】:{}", url, statusCode); + } else { + this.logRequestAndResponse(url, requestStr, responseString); + } return responseString; } @@ -145,7 +149,11 @@ private String requestV3(String url, String requestStr, HttpRequestBase httpRequ JsonObject jsonObject = GsonParser.parse(responseString); throw convertException(jsonObject); } catch (Exception e) { - this.logError(url, requestStr, e); + if (java.net.URI.create(url).getPath().contains("/v3/brand/")) { + log.error("\n【请求地址】:{}\n【异常类型】:{}", url, e.getClass().getSimpleName()); + } else { + this.logError(url, requestStr, e); + } throw (e instanceof WxPayException) ? (WxPayException) e : new WxPayException(e.getMessage(), e); } finally { httpRequestBase.releaseConnection(); @@ -177,7 +185,11 @@ public String postV3WithWechatpaySerial(String url, String requestStr) throws Wx if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode || (HttpStatus.SC_ACCEPTED == statusCode && url.endsWith("/codepay"))) { - this.logRequestAndResponse(url, requestStr, responseString); + if (java.net.URI.create(url).getPath().contains("/v3/brand/")) { + log.info("\n【请求地址】:{}\n【响应状态码】:{}", url, statusCode); + } else { + this.logRequestAndResponse(url, requestStr, responseString); + } return responseString; } @@ -185,7 +197,11 @@ public String postV3WithWechatpaySerial(String url, String requestStr) throws Wx JsonObject jsonObject = GsonParser.parse(responseString); throw convertException(jsonObject); } catch (Exception e) { - this.logError(url, requestStr, e); + if (java.net.URI.create(url).getPath().contains("/v3/brand/")) { + log.error("\n【请求地址】:{}\n【异常类型】:{}", url, e.getClass().getSimpleName()); + } else { + this.logError(url, requestStr, e); + } throw (e instanceof WxPayException) ? (WxPayException) e : new WxPayException(e.getMessage(), e); } finally { httpPost.releaseConnection(); @@ -212,7 +228,7 @@ public String requestV3(String url, HttpRequestBase httpRequest) throws WxPayExc } if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode || HttpStatus.SC_ACCEPTED == statusCode) { - log.info("\n【请求地址】:{}\n【响应数据】:{}", url, responseString); + log.info("\n【请求地址】:{}\n【响应状态码】:{}", url, statusCode); return responseString; } @@ -220,7 +236,7 @@ public String requestV3(String url, HttpRequestBase httpRequest) throws WxPayExc JsonObject jsonObject = GsonParser.parse(responseString); throw convertException(jsonObject); } catch (Exception e) { - log.error("\n【请求地址】:{}\n【异常信息】:{}", url, e.getMessage()); + log.error("\n【请求地址】:{}\n【异常类型】:{}", url, e.getClass().getSimpleName()); throw (e instanceof WxPayException) ? (WxPayException) e : new WxPayException(e.getMessage(), e); } finally { httpRequest.releaseConnection(); diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java index 6c8bcec89..044278699 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java @@ -2,6 +2,9 @@ import com.github.binarywang.wxpay.bean.WxPayApiData; import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.BrandApplymentService; +import com.github.binarywang.wxpay.service.BrandStoreService; +import com.github.binarywang.wxpay.service.BrandProfitSharingService; import jodd.http.HttpConnectionProvider; import jodd.http.HttpRequest; import jodd.http.HttpResponse; @@ -27,6 +30,22 @@ */ @Slf4j public class WxPayServiceJoddHttpImpl extends BaseWxPayServiceImpl { + /** Jodd 尚不支持 APIv3,品牌服务不能通过该客户端执行。 */ + @Override + public BrandApplymentService getBrandApplymentService() { + throw new UnsupportedOperationException("Jodd 客户端不支持 APIv3 品牌入驻,请使用 ApacheHttp 或 HttpComponents 客户端"); + } + + @Override + public BrandStoreService getBrandStoreService() { + throw new UnsupportedOperationException("Jodd 客户端不支持 APIv3 品牌门店,请使用 ApacheHttp 或 HttpComponents 客户端"); + } + + @Override + public BrandProfitSharingService getBrandProfitSharingService() { + throw new UnsupportedOperationException("Jodd 客户端不支持 APIv3 品牌分账,请使用 ApacheHttp 或 HttpComponents 客户端"); + } + @Override public byte[] postForBytes(String url, String requestStr, boolean useKey) throws WxPayException { try { diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayCredentials.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayCredentials.java index 4b78a26f7..85506f82e 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayCredentials.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayCredentials.java @@ -79,7 +79,6 @@ public final String getToken(HttpRequestWrapper request) throws IOException { long timestamp = generateTimestamp(); String message = buildMessage(nonceStr, timestamp, request); - log.debug("authorization message=[{}]", message); Signer.SignatureResult signature = signer.sign(message.getBytes(StandardCharsets.UTF_8)); @@ -88,7 +87,6 @@ public final String getToken(HttpRequestWrapper request) throws IOException { + "timestamp=\"" + timestamp + "\"," + "serial_no=\"" + signature.certificateSerialNumber + "\"," + "signature=\"" + signature.sign + "\""; - log.debug("authorization token=[{}]", token); return token; } diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/BrandApplymentServiceTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/BrandApplymentServiceTest.java new file mode 100644 index 000000000..f475ad1ca --- /dev/null +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/BrandApplymentServiceTest.java @@ -0,0 +1,135 @@ +package com.github.binarywang.wxpay.service; + +import com.github.binarywang.wxpay.bean.brandapplyment.BrandApplymentCancelRequest; +import com.github.binarywang.wxpay.bean.brandapplyment.BrandApplymentRequest; +import com.github.binarywang.wxpay.bean.brandapplyment.BrandApplymentQueryResult; +import com.github.binarywang.wxpay.bean.brandapplyment.BrandApplymentResult; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.impl.BrandApplymentServiceImpl; +import com.github.binarywang.wxpay.v3.auth.PublicCertificateVerifier; +import com.github.binarywang.wxpay.v3.util.RsaCryptoUtil; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.lang.reflect.Proxy; +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +/** 品牌入驻申请离线契约测试。 */ +public class BrandApplymentServiceTest { + private static final String BASE = "https://api.mch.weixin.qq.com"; + private static final Gson GSON = new Gson(); + + @Test + public void shouldEncryptAdminAndPreserveReusableRequest() throws Exception { + Capture capture = new Capture(); + BrandApplymentService service = new BrandApplymentServiceImpl(capture.service()); + BrandApplymentRequest request = GSON.fromJson("{\"business_code\":\"brand_1\"," + + "\"admin_info\":{\"admin_name\":\"测试管理员\",\"id_doc_type\":\"IDENTIFICATION_TYPE_MAINLAND_ID_CARD\",\"id_card_number\":\"test-id\"}," + + "\"subject_info\":{\"subject_type\":\"SUBJECT_TYPE_ENTERPRISE\",\"subject_name\":\"测试企业\",\"unified_social_credit_code\":\"test-code\"}," + + "\"brand_basic_info\":{\"brand_name\":\"测试品牌\",\"brand_logo\":\"logo-media\"}," + + "\"trademark\":{\"trademark_exists\":\"TRADEMARK_EXISTS\",\"trademark_registration_certificate\":{\"name\":\"商标\",\"certificate_list\":[\"cert-1\",\"cert-2\"],\"license_list\":[\"license-1\"]}," + + "\"logo_trademark_registration_certificate\":{\"number\":\"logo-1\"},\"no_trademark_addition_prove_list\":[\"proof-1\"]}}", BrandApplymentRequest.class); + BrandApplymentResult result = service.createApplyment(request); + Assert.assertEquals(capture.method, "postV3WithWechatpaySerial"); + Assert.assertEquals(capture.url, BASE + "/v3/brand/applyments"); + JsonObject body = GSON.fromJson(capture.body, JsonObject.class); + JsonObject admin = body.getAsJsonObject("admin_info"); + Assert.assertEquals(RsaCryptoUtil.decryptOAEP(admin.get("admin_name").getAsString(), capture.keys.getPrivate()), "测试管理员"); + Assert.assertEquals(RsaCryptoUtil.decryptOAEP(admin.get("id_card_number").getAsString(), capture.keys.getPrivate()), "test-id"); + Assert.assertEquals(admin.get("id_doc_type").getAsString(), "IDENTIFICATION_TYPE_MAINLAND_ID_CARD"); + Assert.assertEquals(body.getAsJsonObject("trademark").getAsJsonObject("trademark_registration_certificate").getAsJsonArray("certificate_list").size(), 2); + Assert.assertEquals(result.getApplymentId(), "1234567890123456"); + Assert.assertEquals(request.getAdminInfo().getAdminName(), "测试管理员"); + service.createApplyment(request); + admin = GSON.fromJson(capture.body, JsonObject.class).getAsJsonObject("admin_info"); + Assert.assertEquals(RsaCryptoUtil.decryptOAEP(admin.get("admin_name").getAsString(), capture.keys.getPrivate()), "测试管理员"); + } + + @Test + public void shouldCancelByEitherIdentifier() throws Exception { + Capture capture = new Capture(); + BrandApplymentService service = new BrandApplymentServiceImpl(capture.service()); + BrandApplymentCancelRequest request = new BrandApplymentCancelRequest(); + request.setBusinessCode("brand_1"); + Assert.assertEquals(service.cancelApplyment(request).getBusinessCode(), "brand_1"); + Assert.assertEquals(capture.method, "postV3"); + Assert.assertEquals(capture.url, BASE + "/v3/brand/applyments/cancel-applyment"); + Assert.assertEquals(GSON.fromJson(capture.body, JsonObject.class).get("business_code").getAsString(), "brand_1"); + request.setBusinessCode(null); + request.setApplymentId("1234567890123456"); + service.cancelApplyment(request); + Assert.assertFalse(GSON.fromJson(capture.body, JsonObject.class).has("business_code")); + } + + @Test + public void shouldQueryBothIdentifiersAndReadState() throws Exception { + Capture capture = new Capture(); + capture.response = "{\"applyment_id\":\"1234567890123456\",\"business_code\":\"brand_1\",\"applyment_state\":\"APPLYMENT_STATE_FINISH\",\"applyment_state_desc\":\"入驻完成\",\"authorization_confirmation_qr_code\":\"https://example.com/auth\",\"reject_reason\":\"测试原因\",\"brand_id\":\"12345678901234567890\"}"; + BrandApplymentService service = new BrandApplymentServiceImpl(capture.service()); + BrandApplymentQueryResult result = service.queryApplymentByBusinessCode("brand_1"); + Assert.assertEquals(result.getApplymentState(), "APPLYMENT_STATE_FINISH"); + Assert.assertEquals(result.getApplymentStateDesc(), "入驻完成"); + Assert.assertEquals(result.getAuthorizationConfirmationQrCode(), "https://example.com/auth"); + Assert.assertEquals(result.getRejectReason(), "测试原因"); + Assert.assertEquals(result.getBrandId(), "12345678901234567890"); + Assert.assertEquals(capture.method, "getV3"); + Assert.assertEquals(capture.url, BASE + "/v3/brand/applyments/business-code/brand_1"); + Assert.assertEquals(service.queryApplymentByApplymentId("1234567890123456").getApplymentId(), "1234567890123456"); + Assert.assertEquals(capture.url, BASE + "/v3/brand/applyments/applyment-id/1234567890123456"); + } + + @Test + public void shouldPropagateWechatErrors() throws Exception { + Capture capture = new Capture(); + capture.error = new WxPayException("RESOURCE_NOT_EXISTS"); + BrandApplymentService service = new BrandApplymentServiceImpl(capture.service()); + try { + service.queryApplymentByBusinessCode("brand_1"); + Assert.fail("Expected WeChat error"); + } catch (WxPayException e) { + Assert.assertSame(e, capture.error); + } + } + + private static class Capture { + final KeyPair keys; + final WxPayConfig config = new WxPayConfig(); + String method; + String url; + String body; + String response = "{\"applyment_id\":\"1234567890123456\",\"business_code\":\"brand_1\"}"; + WxPayException error; + + Capture() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + keys = generator.generateKeyPair(); + config.setVerifier(new PublicCertificateVerifier(keys.getPublic(), "PUB_KEY_ID_TEST")); + } + + WxPayService service() { + return (WxPayService) Proxy.newProxyInstance(WxPayService.class.getClassLoader(), new Class>[]{WxPayService.class}, (proxy, called, args) -> { + if (called.getName().equals("getPayBaseUrl")) { + return BASE; + } + if (called.getName().equals("getConfig")) { + return config; + } + if (called.getName().equals("getV3") || called.getName().equals("postV3") || called.getName().equals("postV3WithWechatpaySerial")) { + method = called.getName(); + url = (String) args[0]; + body = args.length > 1 ? (String) args[1] : null; + if (error != null) { + throw error; + } + return response; + } + throw new UnsupportedOperationException(called.getName()); + }); + } + } +} diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BrandProfitSharingServiceTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BrandProfitSharingServiceTest.java new file mode 100644 index 000000000..6cafcccff --- /dev/null +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BrandProfitSharingServiceTest.java @@ -0,0 +1,206 @@ +package com.github.binarywang.wxpay.service.impl; + +import com.github.binarywang.wxpay.bean.brandprofitsharing.*; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.BrandProfitSharingService; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.v3.auth.PublicCertificateVerifier; +import com.github.binarywang.wxpay.v3.util.RsaCryptoUtil; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.lang.reflect.Proxy; +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +/** 连锁品牌分账接口契约测试,无需商户凭据。 */ +public class BrandProfitSharingServiceTest { + private static final Gson GSON = new Gson(); + private static final String BASE = "https://example.com/v3/brand/profitsharing"; + + @Test + public void shouldUseBrandQueryRouteAndEncodeOrderNumber() throws Exception { + Capture capture = new Capture(); + capture.response = "{\"status\":\"FINISHED\",\"finish_amount\":123,\"finish_description\":\"完成\"," + + "\"receivers\":[{\"detail_id\":\"detail\",\"result\":\"SUCCESS\",\"finish_time\":\"2026-10-13T10:00:00+08:00\"}]}"; + BrandProfitSharingResult result = capture.service().query(BrandProfitSharingQueryRequest.newBuilder() + .subMchId("store").transactionId("transaction").outOrderNo("order|*@").build()); + Assert.assertEquals(capture.url, BASE + "/orders?sub_mchid=store&transaction_id=transaction&out_order_no=order%7C%2A%40"); + Assert.assertEquals(capture.method, "getV3"); + Assert.assertEquals(result.getFinishAmount(), Integer.valueOf(123)); + Assert.assertEquals(result.getFinishDescription(), "完成"); + Assert.assertEquals(result.getReceivers().get(0).getDetailId(), "detail"); + } + + @Test + public void shouldRouteReturnAndFinishAndReadResults() throws Exception { + Capture capture = new Capture(); + BrandProfitSharingService service = capture.service(); + capture.response = "{\"return_no\":\"wechat-return\",\"result\":\"SUCCESS\",\"amount\":10}"; + BrandProfitSharingReturnResult result = service.returnOrder(BrandProfitSharingReturnRequest.newBuilder() + .subMchId("store").outOrderNo("order").outReturnNo("return").returnMchId("receiver") + .amount(10).description("退款").build()); + Assert.assertEquals(capture.url, BASE + "/returnorders"); + Assert.assertEquals(capture.method, "postV3"); + Assert.assertEquals(capture.json().get("return_mchid").getAsString(), "receiver"); + Assert.assertEquals(capture.json().get("amount").getAsInt(), 10); + Assert.assertEquals(result.getReturnNo(), "wechat-return"); + Assert.assertEquals(result.getResult(), "SUCCESS"); + service.queryReturn(BrandProfitSharingReturnQueryRequest.newBuilder() + .subMchId("store").outReturnNo("return").outOrderNo("order|*@").build()); + Assert.assertEquals(capture.url, BASE + "/returnorders?sub_mchid=store&out_return_no=return&out_order_no=order%7C%2A%40"); + service.queryReturn(BrandProfitSharingReturnQueryRequest.newBuilder() + .subMchId("store").outReturnNo("return").orderId("wechat-order").build()); + Assert.assertEquals(capture.url, BASE + "/returnorders?sub_mchid=store&out_return_no=return&order_id=wechat-order"); + capture.response = "{\"order_id\":\"finish-order\",\"sub_mchid\":\"store\"}"; + Assert.assertEquals(service.finish(BrandProfitSharingFinishRequest.newBuilder().subMchId("store") + .transactionId("transaction").outOrderNo("finish").description("完成").build()).getOrderId(), "finish-order"); + Assert.assertEquals(capture.url, BASE + "/finish-order"); + Assert.assertEquals(capture.json().get("sub_mchid").getAsString(), "store"); + } + + @Test + public void shouldRouteRemainingAmountAndBrandRatio() throws Exception { + Capture capture = new Capture(); + capture.response = "{\"transaction_id\":\"transaction\",\"unsplit_amount\":99}"; + Assert.assertEquals(capture.service().queryRemainingAmount("transaction").getUnsplitAmount(), Integer.valueOf(99)); + Assert.assertEquals(capture.url, BASE + "/orders/transaction/amounts"); + capture.response = "{\"brand_mchid\":\"brand\",\"max_ratio\":3000}"; + Assert.assertEquals(capture.service().queryMaxRatio("brand").getMaxRatio(), Integer.valueOf(3000)); + Assert.assertEquals(capture.url, BASE + "/brand-configs/brand"); + } + + @Test + public void shouldTransmitReceiverRegistrationNameAsPlaintextPerContract() throws Exception { + Capture capture = new Capture(); + BrandProfitSharingService service = capture.service(); + service.addReceiver(BrandProfitSharingReceiverRequest.newBuilder().brandMchId("brand") + .appid("app").subAppid("brand-app").type("MERCHANT_ID").account("receiver") + .name("测试名称").relationType("SUPPLIER").build()); + Assert.assertEquals(capture.url, BASE + "/receivers/add"); + Assert.assertEquals(capture.method, "requestV3"); + Assert.assertEquals(capture.json().get("brand_mchid").getAsString(), "brand"); + Assert.assertEquals(capture.json().get("name").getAsString(), "测试名称"); + Assert.assertFalse(capture.json().has("sub_mchid")); + service.deleteReceiver(BrandProfitSharingReceiverDeleteRequest.newBuilder().brandMchId("brand") + .appid("app").subAppid("brand-app").type("MERCHANT_ID").account("receiver").build()); + Assert.assertEquals(capture.url, BASE + "/receivers/delete"); + Assert.assertEquals(capture.method, "requestV3"); + Assert.assertFalse(capture.json().has("name")); + } + + @Test + public void shouldEncryptReceiverNameWithoutMutatingReusableRequest() throws Exception { + Capture capture = new Capture(); + BrandProfitSharingRequest request = GSON.fromJson("{\"brand_mchid\":\"brand\",\"sub_mchid\":\"store\"," + + "\"appid\":\"app\",\"sub_appid\":\"brand-app\",\"finish\":true," + + "\"receivers\":[{\"type\":\"MERCHANT_ID\",\"account\":\"receiver\",\"amount\":10,\"name\":\"测试名称\"}]}", BrandProfitSharingRequest.class); + BrandProfitSharingService service = capture.service(); + for (int n = 0; n < 2; n++) { + service.profitSharing(request); + Assert.assertEquals(capture.url, BASE + "/orders"); + Assert.assertEquals(capture.method, "postV3WithWechatpaySerial"); + String encrypted = capture.json().getAsJsonArray("receivers").get(0).getAsJsonObject().get("name").getAsString(); + Assert.assertEquals(RsaCryptoUtil.decryptOAEP(encrypted, capture.keys.getPrivate()), "测试名称"); + Assert.assertEquals(capture.json().get("brand_mchid").getAsString(), "brand"); + Assert.assertEquals(capture.json().get("sub_mchid").getAsString(), "store"); + Assert.assertTrue(capture.json().get("finish").getAsBoolean()); + Assert.assertEquals(request.getReceivers().get(0).getName(), "测试名称"); + } + } + + @Test + public void shouldPropagateWechatErrors() throws Exception { + Capture capture = new Capture(); + capture.error = new WxPayException("RESOURCE_NOT_EXISTS"); + try { + capture.service().queryMaxRatio("brand"); + Assert.fail("Expected WeChat error"); + } catch (WxPayException e) { + Assert.assertSame(e, capture.error); + } + } + + @Test + public void shouldVerifyAndDecryptBrandNotification() throws Exception { + Capture capture = new Capture(); + String key = "0123456789abcdef0123456789abcdef"; + capture.config.setApiV3Key(key); + String nonce = "0123456789ab"; + String associated = "profitsharing"; + String plaintext = "{\"sp_mchid\":\"provider\",\"sub_mchid\":\"store\",\"transaction_id\":\"transaction\"," + + "\"order_id\":\"order\",\"out_order_no\":\"out-order\",\"success_time\":\"2026-10-13T10:00:00+08:00\"," + + "\"receiver\":{\"type\":\"MERCHANT_ID\",\"account\":\"receiver\",\"amount\":10,\"description\":\"分账\"}}"; + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, + new javax.crypto.spec.SecretKeySpec(key.getBytes(java.nio.charset.StandardCharsets.UTF_8), "AES"), + new javax.crypto.spec.GCMParameterSpec(128, nonce.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + cipher.updateAAD(associated.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + String encrypted = java.util.Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + String data = "{\"resource\":{\"algorithm\":\"AEAD_AES_256_GCM\",\"nonce\":\"" + nonce + + "\",\"associated_data\":\"" + associated + "\",\"ciphertext\":\"" + encrypted + "\"}}"; + com.github.binarywang.wxpay.bean.notify.SignatureHeader header = new com.github.binarywang.wxpay.bean.notify.SignatureHeader(); + header.setTimeStamp("123456"); + header.setNonce("signature-nonce"); + header.setSerial("PUB_KEY_ID_TEST"); + java.security.Signature signature = java.security.Signature.getInstance("SHA256withRSA"); + signature.initSign(capture.keys.getPrivate()); + signature.update((header.getTimeStamp() + "\n" + header.getNonce() + "\n" + data + "\n").getBytes(java.nio.charset.StandardCharsets.UTF_8)); + header.setSignature(java.util.Base64.getEncoder().encodeToString(signature.sign())); + com.github.binarywang.wxpay.bean.profitsharing.notify.ProfitSharingNotifyV3Result result = capture.service().parseNotifyResult(data, header); + Assert.assertEquals(result.getSpMchid(), "provider"); + Assert.assertEquals(result.getSubMchid(), "store"); + Assert.assertEquals(result.getReceiver().getAmount(), Integer.valueOf(10)); + try { + capture.service().parseNotifyResult(data + " ", header); + Assert.fail("Expected signature rejection"); + } catch (WxPayException e) { + Assert.assertTrue(e.getMessage().contains("验证失败")); + } + } + + @Test(expectedExceptions = WxPayException.class, expectedExceptionsMessageRegExp = ".*签名头.*") + public void shouldRequireNotificationSignatureHeader() throws Exception { + new Capture().service().parseNotifyResult("{}", null); + } + + private static class Capture { + final KeyPair keys; + final WxPayConfig config = new WxPayConfig(); + String method; + String url; + String body; + String response = "{}"; + WxPayException error; + + Capture() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + keys = generator.generateKeyPair(); + config.setVerifier(new PublicCertificateVerifier(keys.getPublic(), "PUB_KEY_ID_TEST")); + } + + JsonObject json() { + return GSON.fromJson(body, JsonObject.class); + } + + BrandProfitSharingService service() { + WxPayService pay = (WxPayService) Proxy.newProxyInstance(WxPayService.class.getClassLoader(), + new Class>[]{WxPayService.class}, (proxy, called, args) -> { + if (called.getName().equals("getPayBaseUrl")) return "https://example.com"; + if (called.getName().equals("getConfig")) return config; + method = called.getName(); + url = (String) args[0]; + body = args.length > 1 ? (args[1] instanceof org.apache.http.client.methods.HttpPost + ? org.apache.http.util.EntityUtils.toString(((org.apache.http.client.methods.HttpPost) args[1]).getEntity(), "UTF-8") + : (String) args[1]) : null; + if (error != null) throw error; + return response; + }); + return new BrandProfitSharingServiceImpl(pay); + } + } +} diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BrandSensitiveLoggingTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BrandSensitiveLoggingTest.java new file mode 100644 index 000000000..dde5bf089 --- /dev/null +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BrandSensitiveLoggingTest.java @@ -0,0 +1,130 @@ +package com.github.binarywang.wxpay.service.impl; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import com.github.binarywang.wxpay.bean.brandprofitsharing.BrandProfitSharingReceiverDeleteRequest; +import com.github.binarywang.wxpay.bean.brandprofitsharing.BrandProfitSharingReceiverRequest; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.v3.auth.Signer; +import com.github.binarywang.wxpay.v3.auth.WxPayCredentials; +import org.apache.http.*; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestWrapper; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicHttpResponse; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.lang.reflect.Proxy; +import java.nio.charset.StandardCharsets; + +/** 验证实际 HTTP 执行器保留明文传输且日志不泄露收款信息。 */ +public class BrandSensitiveLoggingTest { + private static final String NAME = "测试敏感姓名"; + private static final String ACCOUNT = "sensitive-receiver-account"; + + @Test + public void shouldKeepReceiverBodyOutOfTransportLogs() throws Exception { + for (WxPayService pay : new WxPayService[]{new WxPayServiceApacheHttpImpl(), new WxPayServiceHttpComponentsImpl()}) { + for (int route = 0; route < 4; route++) { + for (boolean failure : new boolean[]{false, true}) { + final String[] sent = new String[1]; + CloseableHttpClient client = new CloseableHttpClient() { + @Override + protected CloseableHttpResponse doExecute(HttpHost host, HttpRequest request, HttpContext context) throws IOException { + sent[0] = EntityUtils.toString(((HttpEntityEnclosingRequest) request).getEntity(), StandardCharsets.UTF_8); + BasicHttpResponse response = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), failure ? 400 : 200, "test"); + response.setEntity(new StringEntity(failure + ? "{\"code\":\"PARAM_ERROR\",\"message\":\"" + NAME + ACCOUNT + "\"}" + : "{\"name\":\"" + NAME + "\",\"account\":\"" + ACCOUNT + "\"}", ContentType.APPLICATION_JSON)); + return (CloseableHttpResponse) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class>[]{CloseableHttpResponse.class}, (proxy, method, args) -> + method.getName().equals("close") ? null : method.invoke(response, args)); + } + @Override public void close() { } + @Override public org.apache.http.params.HttpParams getParams() { return null; } + @Override public org.apache.http.conn.ClientConnectionManager getConnectionManager() { return null; } + }; + WxPayConfig config = new WxPayConfig(); + config.setApiV3HttpClient(client); + pay.setConfig(config); + Logger logger = (Logger) LoggerFactory.getLogger(pay.getClass()); + ListAppender