1
0
mirror of synced 2025-11-06 04:20:53 +08:00

🆕 #3618 【微信支付】增加境外微信支付的支持

This commit is contained in:
Copilot
2025-10-04 01:44:56 +08:00
committed by GitHub
parent a6825a62bb
commit ca567ce310
7 changed files with 508 additions and 0 deletions

View File

@@ -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)

View File

@@ -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;
/**
* <pre>
* 境外微信支付统一下单请求参数对象.
* 参考文档https://pay.weixin.qq.com/doc/global/v3/zh/4013014223
* </pre>
*
* @author Binary Wang
*/
@Data
@NoArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class WxPayUnifiedOrderV3GlobalRequest extends WxPayUnifiedOrderV3Request implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:交易类型
* 变量名trade_type
* 是否必填:是
* 类型string[1,16]
* 描述:
* 交易类型,取值如下:
* JSAPI--JSAPI支付
* NATIVE--Native支付
* APP--APP支付
* H5--H5支付
* 示例值JSAPI
* </pre>
*/
@SerializedName(value = "trade_type")
private String tradeType;
/**
* <pre>
* 字段名:商户类目
* 变量名merchant_category_code
* 是否必填:是
* 类型string[1,32]
* 描述:
* 商户类目,境外商户必填
* 示例值5812
* </pre>
*/
@SerializedName(value = "merchant_category_code")
private String merchantCategoryCode;
}

View File

@@ -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;
}

View File

@@ -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 {
*/
<T> T createPartnerOrderV3(TradeTypeEnum tradeType, WxPayPartnerUnifiedOrderV3Request request) throws WxPayException;
/**
* 境外微信支付调用统一下单接口,并组装生成支付所需参数对象.
*
* @param <T> 请使用{@link WxPayUnifiedOrderV3Result}里的内部类或字段
* @param tradeType the global trade type
* @param request 境外统一下单请求参数
* @return 返回 {@link WxPayUnifiedOrderV3Result}里的内部类或字段
* @throws WxPayException the wx pay exception
*/
<T> T createOrderV3Global(GlobalTradeTypeEnum tradeType, WxPayUnifiedOrderV3GlobalRequest request) throws WxPayException;
/**
* 在发起微信支付前,需要调用统一下单接口,获取"预支付交易会话标识"
*
@@ -660,6 +672,16 @@ public interface WxPayService {
*/
WxPayUnifiedOrderV3Result unifiedOrderV3(TradeTypeEnum tradeType, WxPayUnifiedOrderV3Request request) throws WxPayException;
/**
* 境外微信支付在发起支付前,需要调用统一下单接口,获取"预支付交易会话标识"
*
* @param tradeType the global trade type
* @param request 境外请求对象注意一些参数如appid、mchid等不用设置方法内会自动从配置对象中获取到前提是对应配置中已经设置
* @return the wx pay unified order result
* @throws WxPayException the wx pay exception
*/
WxPayUnifiedOrderV3Result unifiedOrderV3Global(GlobalTradeTypeEnum tradeType, WxPayUnifiedOrderV3GlobalRequest request) throws WxPayException;
/**
* <pre>
* 合单支付API(APP支付、JSAPI支付、H5支付、NATIVE支付).

View File

@@ -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> 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())) {

View File

@@ -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"));
}
}
}

View File

@@ -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;
}
}