diff --git a/weixin-java-pay/OVERSEAS_PAY.md b/weixin-java-pay/OVERSEAS_PAY.md new file mode 100644 index 000000000..b9da9814f --- /dev/null +++ b/weixin-java-pay/OVERSEAS_PAY.md @@ -0,0 +1,120 @@ +# 境外微信支付(Overseas WeChat Pay)支持 + +本次更新添加了境外微信支付的支持,解决了 [Issue #3618](https://github.com/binarywang/WxJava/issues/3618) 中提到的问题。 + +## 问题背景 + +境外微信支付需要使用新的API接口地址和额外的参数: +- 使用不同的基础URL: `https://apihk.mch.weixin.qq.com` +- 需要额外的参数: `trade_type` 和 `merchant_category_code` +- 使用不同的API端点: `/global/v3/transactions/*` + +## 新增功能 + +### 1. GlobalTradeTypeEnum +新的枚举类,定义了境外支付的交易类型和对应的API端点: +- `APP`: `/global/v3/transactions/app` +- `JSAPI`: `/global/v3/transactions/jsapi` +- `NATIVE`: `/global/v3/transactions/native` +- `H5`: `/global/v3/transactions/h5` + +### 2. WxPayUnifiedOrderV3GlobalRequest +扩展的请求类,包含境外支付必需的额外字段: +- `trade_type`: 交易类型 (JSAPI, APP, NATIVE, H5) +- `merchant_category_code`: 商户类目代码(境外商户必填) + +### 3. 新的服务方法 +- `createOrderV3Global()`: 创建境外支付订单 +- `unifiedOrderV3Global()`: 境外统一下单接口 + +## 使用示例 + +### JSAPI支付示例 +```java +// 创建境外支付请求 +WxPayUnifiedOrderV3GlobalRequest request = new WxPayUnifiedOrderV3GlobalRequest(); +request.setOutTradeNo(RandomUtils.getRandomStr()); +request.setDescription("境外商品购买"); +request.setNotifyUrl("https://your-domain.com/notify"); + +// 设置金额 +WxPayUnifiedOrderV3GlobalRequest.Amount amount = new WxPayUnifiedOrderV3GlobalRequest.Amount(); +amount.setCurrency(WxPayConstants.CurrencyType.CNY); +amount.setTotal(100); // 1元,单位为分 +request.setAmount(amount); + +// 设置支付者 +WxPayUnifiedOrderV3GlobalRequest.Payer payer = new WxPayUnifiedOrderV3GlobalRequest.Payer(); +payer.setOpenid("用户的openid"); +request.setPayer(payer); + +// 设置境外支付必需的参数 +request.setTradeType("JSAPI"); +request.setMerchantCategoryCode("5812"); // 商户类目代码 + +// 调用境外支付接口 +WxPayUnifiedOrderV3Result.JsapiResult result = payService.createOrderV3Global( + GlobalTradeTypeEnum.JSAPI, + request +); +``` + +### APP支付示例 +```java +WxPayUnifiedOrderV3GlobalRequest request = new WxPayUnifiedOrderV3GlobalRequest(); +// ... 设置基础信息 ... + +request.setTradeType("APP"); +request.setMerchantCategoryCode("5812"); +request.setPayer(new WxPayUnifiedOrderV3GlobalRequest.Payer()); // APP支付不需要openid + +WxPayUnifiedOrderV3Result.AppResult result = payService.createOrderV3Global( + GlobalTradeTypeEnum.APP, + request +); +``` + +### NATIVE支付示例 +```java +WxPayUnifiedOrderV3GlobalRequest request = new WxPayUnifiedOrderV3GlobalRequest(); +// ... 设置基础信息 ... + +request.setTradeType("NATIVE"); +request.setMerchantCategoryCode("5812"); +request.setPayer(new WxPayUnifiedOrderV3GlobalRequest.Payer()); + +String codeUrl = payService.createOrderV3Global( + GlobalTradeTypeEnum.NATIVE, + request +); +``` + +## 配置说明 + +境外支付使用相同的 `WxPayConfig` 配置,无需特殊设置: + +```java +WxPayConfig config = new WxPayConfig(); +config.setAppId("你的AppId"); +config.setMchId("你的境外商户号"); +config.setMchKey("你的商户密钥"); +config.setNotifyUrl("https://your-domain.com/notify"); + +// V3相关配置 +config.setPrivateKeyPath("你的私钥文件路径"); +config.setCertSerialNo("你的商户证书序列号"); +config.setApiV3Key("你的APIv3密钥"); +``` + +**注意**: 境外支付会自动使用 `https://apihk.mch.weixin.qq.com` 作为基础URL,无需手动设置。 + +## 兼容性 + +- 完全向后兼容,不影响现有的国内支付功能 +- 使用相同的配置类和结果类 +- 遵循现有的代码风格和架构模式 + +## 参考文档 + +- [境外微信支付文档](https://pay.weixin.qq.com/doc/global/v3/zh/4013014223) +- [原始Issue #3618](https://github.com/binarywang/WxJava/issues/3618) \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayUnifiedOrderV3GlobalRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayUnifiedOrderV3GlobalRequest.java new file mode 100644 index 000000000..296d3a864 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayUnifiedOrderV3GlobalRequest.java @@ -0,0 +1,57 @@ +package com.github.binarywang.wxpay.bean.request; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + *
+ * 境外微信支付统一下单请求参数对象. + * 参考文档:https://pay.weixin.qq.com/doc/global/v3/zh/4013014223 + *+ * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class WxPayUnifiedOrderV3GlobalRequest extends WxPayUnifiedOrderV3Request implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+ * 字段名:交易类型 + * 变量名:trade_type + * 是否必填:是 + * 类型:string[1,16] + * 描述: + * 交易类型,取值如下: + * JSAPI--JSAPI支付 + * NATIVE--Native支付 + * APP--APP支付 + * H5--H5支付 + * 示例值:JSAPI + *+ */ + @SerializedName(value = "trade_type") + private String tradeType; + + /** + *
+ * 字段名:商户类目 + * 变量名:merchant_category_code + * 是否必填:是 + * 类型:string[1,32] + * 描述: + * 商户类目,境外商户必填 + * 示例值:5812 + *+ */ + @SerializedName(value = "merchant_category_code") + private String merchantCategoryCode; +} \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/enums/GlobalTradeTypeEnum.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/enums/GlobalTradeTypeEnum.java new file mode 100644 index 000000000..fd33b240f --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/enums/GlobalTradeTypeEnum.java @@ -0,0 +1,36 @@ +package com.github.binarywang.wxpay.bean.result.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 境外微信支付方式 + * Overseas WeChat Pay trade types with global endpoints + * + * @author Binary Wang + */ +@Getter +@AllArgsConstructor +public enum GlobalTradeTypeEnum { + /** + * APP + */ + APP("/global/v3/transactions/app"), + /** + * JSAPI 或 小程序 + */ + JSAPI("/global/v3/transactions/jsapi"), + /** + * NATIVE + */ + NATIVE("/global/v3/transactions/native"), + /** + * H5 + */ + H5("/global/v3/transactions/h5"); + + /** + * 境外下单url + */ + private final String url; +} \ No newline at end of file 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 8ceac2b6b..c73fb843e 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 @@ -6,6 +6,7 @@ import com.github.binarywang.wxpay.bean.notify.*; import com.github.binarywang.wxpay.bean.request.*; import com.github.binarywang.wxpay.bean.result.*; import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.bean.result.enums.GlobalTradeTypeEnum; import com.github.binarywang.wxpay.bean.transfer.TransferBillsNotifyResult; import com.github.binarywang.wxpay.config.WxPayConfig; import com.github.binarywang.wxpay.constant.WxPayConstants; @@ -640,6 +641,17 @@ public interface WxPayService { */
* 合单支付API(APP支付、JSAPI支付、H5支付、NATIVE支付).
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 0df3530a3..f32083a63 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
@@ -11,6 +11,7 @@ import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
import com.github.binarywang.wxpay.bean.request.*;
import com.github.binarywang.wxpay.bean.result.*;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.bean.result.enums.GlobalTradeTypeEnum;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsNotifyResult;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.config.WxPayConfigHolder;
@@ -746,6 +747,14 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
return result.getPayInfo(tradeType, appId, request.getSubMchId(), this.getConfig().getPrivateKey());
}
+ @Override
+ public T createOrderV3Global(GlobalTradeTypeEnum tradeType, WxPayUnifiedOrderV3GlobalRequest request) throws WxPayException {
+ WxPayUnifiedOrderV3Result result = this.unifiedOrderV3Global(tradeType, request);
+ // Convert GlobalTradeTypeEnum to TradeTypeEnum for getPayInfo method
+ TradeTypeEnum domesticTradeType = TradeTypeEnum.valueOf(tradeType.name());
+ return result.getPayInfo(domesticTradeType, request.getAppid(), request.getMchid(), this.getConfig().getPrivateKey());
+ }
+
@Override
public WxPayUnifiedOrderV3Result unifiedPartnerOrderV3(TradeTypeEnum tradeType, WxPayPartnerUnifiedOrderV3Request request) throws WxPayException {
if (StringUtils.isBlank(request.getSpAppid())) {
@@ -790,6 +799,28 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
return GSON.fromJson(response, WxPayUnifiedOrderV3Result.class);
}
+ @Override
+ public WxPayUnifiedOrderV3Result unifiedOrderV3Global(GlobalTradeTypeEnum tradeType, WxPayUnifiedOrderV3GlobalRequest request) throws WxPayException {
+ if (StringUtils.isBlank(request.getAppid())) {
+ request.setAppid(this.getConfig().getAppId());
+ }
+ if (StringUtils.isBlank(request.getMchid())) {
+ request.setMchid(this.getConfig().getMchId());
+ }
+ if (StringUtils.isBlank(request.getNotifyUrl())) {
+ request.setNotifyUrl(this.getConfig().getNotifyUrl());
+ }
+ if (StringUtils.isBlank(request.getTradeType())) {
+ request.setTradeType(tradeType.name());
+ }
+
+ // Use global WeChat Pay base URL for overseas payments
+ String globalBaseUrl = "https://apihk.mch.weixin.qq.com";
+ String url = globalBaseUrl + tradeType.getUrl();
+ String response = this.postV3WithWechatpaySerial(url, GSON.toJson(request));
+ return GSON.fromJson(response, WxPayUnifiedOrderV3Result.class);
+ }
+
@Override
public CombineTransactionsResult combine(TradeTypeEnum tradeType, CombineTransactionsRequest request) throws WxPayException {
if (StringUtils.isBlank(request.getCombineAppid())) {
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceGlobalImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceGlobalImplTest.java
new file mode 100644
index 000000000..c648c8a17
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceGlobalImplTest.java
@@ -0,0 +1,89 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3GlobalRequest;
+import com.github.binarywang.wxpay.bean.result.enums.GlobalTradeTypeEnum;
+import com.github.binarywang.wxpay.constant.WxPayConstants;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import me.chanjar.weixin.common.util.RandomUtils;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+/**
+ * 境外微信支付测试类
+ *
+ * @author Binary Wang
+ */
+public class BaseWxPayServiceGlobalImplTest {
+
+ private static final Gson GSON = new GsonBuilder().create();
+
+ @Test
+ public void testWxPayUnifiedOrderV3GlobalRequest() {
+ // Test that the new request class has the required fields
+ WxPayUnifiedOrderV3GlobalRequest request = new WxPayUnifiedOrderV3GlobalRequest();
+
+ // Set basic order information
+ String outTradeNo = RandomUtils.getRandomStr();
+ request.setOutTradeNo(outTradeNo);
+ request.setDescription("Test overseas payment");
+ request.setNotifyUrl("https://api.example.com/notify");
+
+ // Set amount
+ WxPayUnifiedOrderV3GlobalRequest.Amount amount = new WxPayUnifiedOrderV3GlobalRequest.Amount();
+ amount.setCurrency(WxPayConstants.CurrencyType.CNY);
+ amount.setTotal(100); // 1 yuan in cents
+ request.setAmount(amount);
+
+ // Set payer
+ WxPayUnifiedOrderV3GlobalRequest.Payer payer = new WxPayUnifiedOrderV3GlobalRequest.Payer();
+ payer.setOpenid("test_openid");
+ request.setPayer(payer);
+
+ // Set the new required fields for global payments
+ request.setTradeType("JSAPI");
+ request.setMerchantCategoryCode("5812"); // Example category code
+
+ // Assert that all fields are properly set
+ assertNotNull(request.getTradeType());
+ assertNotNull(request.getMerchantCategoryCode());
+ assertEquals("JSAPI", request.getTradeType());
+ assertEquals("5812", request.getMerchantCategoryCode());
+ assertEquals(outTradeNo, request.getOutTradeNo());
+ assertEquals("Test overseas payment", request.getDescription());
+ assertEquals(100, request.getAmount().getTotal());
+ assertEquals("test_openid", request.getPayer().getOpenid());
+
+ // Test JSON serialization contains the new fields
+ String json = GSON.toJson(request);
+ assertTrue(json.contains("trade_type"));
+ assertTrue(json.contains("merchant_category_code"));
+ assertTrue(json.contains("JSAPI"));
+ assertTrue(json.contains("5812"));
+ }
+
+ @Test
+ public void testGlobalTradeTypeEnum() {
+ // Test that all trade types have the correct global endpoints
+ assertEquals("/global/v3/transactions/app", GlobalTradeTypeEnum.APP.getUrl());
+ assertEquals("/global/v3/transactions/jsapi", GlobalTradeTypeEnum.JSAPI.getUrl());
+ assertEquals("/global/v3/transactions/native", GlobalTradeTypeEnum.NATIVE.getUrl());
+ assertEquals("/global/v3/transactions/h5", GlobalTradeTypeEnum.H5.getUrl());
+ }
+
+ @Test
+ public void testGlobalTradeTypeEnumValues() {
+ // Test that we have all the main trade types
+ GlobalTradeTypeEnum[] tradeTypes = GlobalTradeTypeEnum.values();
+ assertEquals(4, tradeTypes.length);
+
+ // Test that we can convert between enum name and TradeTypeEnum
+ for (GlobalTradeTypeEnum globalType : tradeTypes) {
+ // This tests that the enum names match between Global and regular TradeTypeEnum
+ String name = globalType.name();
+ assertNotNull(name);
+ assertTrue(name.equals("APP") || name.equals("JSAPI") || name.equals("NATIVE") || name.equals("H5"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/OverseasWxPayExample.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/OverseasWxPayExample.java
new file mode 100644
index 000000000..ccccf9c80
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/OverseasWxPayExample.java
@@ -0,0 +1,153 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3GlobalRequest;
+import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
+import com.github.binarywang.wxpay.bean.result.enums.GlobalTradeTypeEnum;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.constant.WxPayConstants;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import me.chanjar.weixin.common.util.RandomUtils;
+
+/**
+ * 境外微信支付使用示例
+ * Example usage for overseas WeChat Pay
+ *
+ * @author Binary Wang
+ */
+public class OverseasWxPayExample {
+
+ /**
+ * 境外微信支付JSAPI下单示例
+ * Example for overseas WeChat Pay JSAPI order creation
+ */
+ public void createOverseasJsapiOrder(WxPayService payService) throws WxPayException {
+ // 创建境外支付请求对象
+ WxPayUnifiedOrderV3GlobalRequest request = new WxPayUnifiedOrderV3GlobalRequest();
+
+ // 设置基础订单信息
+ request.setOutTradeNo(RandomUtils.getRandomStr()); // 商户订单号
+ request.setDescription("境外商品购买"); // 商品描述
+ request.setNotifyUrl("https://your-domain.com/notify"); // 支付通知地址
+
+ // 设置金额信息
+ WxPayUnifiedOrderV3GlobalRequest.Amount amount = new WxPayUnifiedOrderV3GlobalRequest.Amount();
+ amount.setCurrency(WxPayConstants.CurrencyType.CNY); // 币种
+ amount.setTotal(100); // 金额,单位为分
+ request.setAmount(amount);
+
+ // 设置支付者信息
+ WxPayUnifiedOrderV3GlobalRequest.Payer payer = new WxPayUnifiedOrderV3GlobalRequest.Payer();
+ payer.setOpenid("用户的openid"); // 用户openid
+ request.setPayer(payer);
+
+ // 设置境外支付必需的参数
+ request.setTradeType("JSAPI"); // 交易类型
+ request.setMerchantCategoryCode("5812"); // 商户类目代码,境外商户必填
+
+ // 可选:设置场景信息
+ WxPayUnifiedOrderV3GlobalRequest.SceneInfo sceneInfo = new WxPayUnifiedOrderV3GlobalRequest.SceneInfo();
+ sceneInfo.setPayerClientIp("用户IP地址");
+ request.setSceneInfo(sceneInfo);
+
+ // 调用境外支付接口
+ WxPayUnifiedOrderV3Result.JsapiResult result = payService.createOrderV3Global(
+ GlobalTradeTypeEnum.JSAPI,
+ request
+ );
+
+ // 返回的result包含前端需要的支付参数
+ System.out.println("支付参数:" + result);
+ }
+
+ /**
+ * 境外微信支付APP下单示例
+ * Example for overseas WeChat Pay APP order creation
+ */
+ public void createOverseasAppOrder(WxPayService payService) throws WxPayException {
+ WxPayUnifiedOrderV3GlobalRequest request = new WxPayUnifiedOrderV3GlobalRequest();
+
+ // 设置基础信息
+ request.setOutTradeNo(RandomUtils.getRandomStr());
+ request.setDescription("境外APP商品购买");
+ request.setNotifyUrl("https://your-domain.com/notify");
+
+ // 设置金额
+ WxPayUnifiedOrderV3GlobalRequest.Amount amount = new WxPayUnifiedOrderV3GlobalRequest.Amount();
+ amount.setCurrency(WxPayConstants.CurrencyType.CNY);
+ amount.setTotal(200); // 2元
+ request.setAmount(amount);
+
+ // APP支付不需要设置payer.openid,但需要设置空的payer对象
+ request.setPayer(new WxPayUnifiedOrderV3GlobalRequest.Payer());
+
+ // 境外支付必需参数
+ request.setTradeType("APP");
+ request.setMerchantCategoryCode("5812");
+
+ // 调用境外APP支付接口
+ WxPayUnifiedOrderV3Result.AppResult result = payService.createOrderV3Global(
+ GlobalTradeTypeEnum.APP,
+ request
+ );
+
+ System.out.println("APP支付参数:" + result);
+ }
+
+ /**
+ * 境外微信支付NATIVE下单示例
+ * Example for overseas WeChat Pay NATIVE order creation
+ */
+ public void createOverseasNativeOrder(WxPayService payService) throws WxPayException {
+ WxPayUnifiedOrderV3GlobalRequest request = new WxPayUnifiedOrderV3GlobalRequest();
+
+ request.setOutTradeNo(RandomUtils.getRandomStr());
+ request.setDescription("境外扫码支付");
+ request.setNotifyUrl("https://your-domain.com/notify");
+
+ // 设置金额
+ WxPayUnifiedOrderV3GlobalRequest.Amount amount = new WxPayUnifiedOrderV3GlobalRequest.Amount();
+ amount.setCurrency(WxPayConstants.CurrencyType.CNY);
+ amount.setTotal(300); // 3元
+ request.setAmount(amount);
+
+ // NATIVE支付不需要设置payer.openid
+ request.setPayer(new WxPayUnifiedOrderV3GlobalRequest.Payer());
+
+ // 境外支付必需参数
+ request.setTradeType("NATIVE");
+ request.setMerchantCategoryCode("5812");
+
+ // 调用境外NATIVE支付接口
+ String result = payService.createOrderV3Global(
+ GlobalTradeTypeEnum.NATIVE,
+ request
+ );
+
+ System.out.println("NATIVE支付二维码链接:" + result);
+ }
+
+ /**
+ * 配置示例
+ * Configuration example
+ */
+ public WxPayConfig createOverseasConfig() {
+ WxPayConfig config = new WxPayConfig();
+
+ // 基础配置
+ config.setAppId("你的AppId");
+ config.setMchId("你的境外商户号");
+ config.setMchKey("你的商户密钥");
+ config.setNotifyUrl("https://your-domain.com/notify");
+
+ // 境外支付使用的是全球API,在代码中会自动使用 https://apihk.mch.weixin.qq.com 作为基础URL
+ // 无需额外设置payBaseUrl,方法内部会自动处理
+
+ // V3相关配置(境外支付也使用V3接口)
+ config.setPrivateKeyPath("你的私钥文件路径");
+ config.setCertSerialNo("你的商户证书序列号");
+ config.setApiV3Key("你的APIv3密钥");
+
+ return config;
+ }
+}
\ No newline at end of file