Skip to content

Add overseas WeChat Pay support with GlobalTradeTypeEnum and new API methods #3653

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions weixin-java-pay/OVERSEAS_PAY.md
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ public String toString() {
* </pre>
*/
@XStreamAlias("refund_recv_accout")
private String refundRecvAccout;
private String refundRecvAccount;

/**
* <pre>
Expand Down Expand Up @@ -324,7 +324,7 @@ public void loadXML(Document d) {
settlementRefundFee = readXmlInteger(d, "settlement_refund_fee");
refundStatus = readXmlString(d, "refund_status");
successTime = readXmlString(d, "success_time");
refundRecvAccout = readXmlString(d, "refund_recv_accout");
refundRecvAccount = readXmlString(d, "refund_recv_accout");
refundAccount = readXmlString(d, "refund_account");
refundRequestSource = readXmlString(d, "refund_request_source");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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;
Expand Down Expand Up @@ -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;

/**
* 在发起微信支付前,需要调用统一下单接口,获取"预支付交易会话标识"
*
Expand All @@ -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支付).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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;
Expand Down Expand Up @@ -746,6 +747,14 @@ public <T> T createPartnerOrderV3(TradeTypeEnum tradeType, WxPayPartnerUnifiedOr
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())) {
Expand Down Expand Up @@ -790,6 +799,28 @@ public WxPayUnifiedOrderV3Result unifiedOrderV3(TradeTypeEnum tradeType, WxPayUn
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())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public void testFromXMLFastMode() throws WxPayException {
refundNotifyResult.loadReqInfo(xmlDecryptedReqInfo);
assertEquals(refundNotifyResult.getReqInfo().getRefundFee().intValue(), 15);
assertEquals(refundNotifyResult.getReqInfo().getRefundStatus(), "SUCCESS");
assertEquals(refundNotifyResult.getReqInfo().getRefundRecvAccout(), "用户零钱");
assertEquals(refundNotifyResult.getReqInfo().getRefundRecvAccount(), "用户零钱");
System.out.println(refundNotifyResult);
} finally {
XmlConfig.fastMode = false;
Expand Down
Loading