1
0
mirror of synced 2025-12-17 13:08:02 +08:00

🆕 #3688 【微信支付】 实现预约扣费服务的相关接口

This commit is contained in:
Copilot
2025-11-15 17:12:21 +08:00
committed by GitHub
parent 69a2ab9cc7
commit 092e992817
17 changed files with 1754 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
# 微信支付预约扣费功能使用说明
## 概述
微信支付预约扣费功能(连续包月功能)允许商户在用户授权的情况下,按照约定的时间和金额,自动从用户的支付账户中扣取费用。主要适用于连续包月、订阅服务等场景。
## 功能特性
- **预约扣费**:创建未来某个时间点的扣费计划
- **查询预约**:查询已创建的扣费计划状态
- **取消预约**:取消已创建的扣费计划
- **立即扣费**:立即执行扣费操作
- **扣费记录查询**:查询历史扣费记录
## 快速开始
### 1. 获取服务实例
```java
// 通过 WxPayService 获取预约扣费服务
SubscriptionBillingService subscriptionService = wxPayService.getSubscriptionBillingService();
```
### 2. 创建预约扣费
```java
// 创建预约扣费请求
SubscriptionScheduleRequest request = new SubscriptionScheduleRequest();
request.setOutTradeNo("subscription_" + System.currentTimeMillis());
request.setOpenid("用户的openid");
request.setDescription("腾讯视频VIP会员");
request.setScheduleTime("2024-09-01T10:00:00+08:00");
// 设置扣费金额
SubscriptionAmount amount = new SubscriptionAmount();
amount.setTotal(3000); // 30元单位为分
amount.setCurrency("CNY");
request.setAmount(amount);
// 设置扣费计划(可选)
BillingPlan billingPlan = new BillingPlan();
billingPlan.setPlanType("MONTHLY"); // 按月扣费
billingPlan.setPeriod(1); // 每1个月
billingPlan.setTotalCount(12); // 总共12次
request.setBillingPlan(billingPlan);
// 发起预约扣费
SubscriptionScheduleResult result = subscriptionService.scheduleSubscription(request);
System.out.println("预约扣费ID: " + result.getSubscriptionId());
```
### 3. 查询预约扣费
```java
// 通过预约扣费ID查询
String subscriptionId = "从预约扣费结果中获取的ID";
SubscriptionQueryResult queryResult = subscriptionService.querySubscription(subscriptionId);
System.out.println("预约状态: " + queryResult.getStatus());
```
### 4. 取消预约扣费
```java
// 创建取消请求
SubscriptionCancelRequest cancelRequest = new SubscriptionCancelRequest();
cancelRequest.setSubscriptionId(subscriptionId);
cancelRequest.setCancelReason("用户主动取消");
// 取消预约扣费
SubscriptionCancelResult cancelResult = subscriptionService.cancelSubscription(cancelRequest);
System.out.println("取消结果: " + cancelResult.getStatus());
```
### 5. 立即扣费
```java
// 创建立即扣费请求
SubscriptionInstantBillingRequest instantRequest = new SubscriptionInstantBillingRequest();
instantRequest.setOutTradeNo("instant_" + System.currentTimeMillis());
instantRequest.setOpenid("用户的openid");
instantRequest.setDescription("补扣上月会员费");
// 设置扣费金额
SubscriptionAmount instantAmount = new SubscriptionAmount();
instantAmount.setTotal(3000); // 30元
instantAmount.setCurrency("CNY");
instantRequest.setAmount(instantAmount);
// 执行立即扣费
SubscriptionInstantBillingResult instantResult = subscriptionService.instantBilling(instantRequest);
System.out.println("扣费结果: " + instantResult.getTradeState());
```
### 6. 查询扣费记录
```java
// 创建查询请求
SubscriptionTransactionQueryRequest queryRequest = new SubscriptionTransactionQueryRequest();
queryRequest.setOpenid("用户的openid");
queryRequest.setBeginTime("2024-08-01T00:00:00+08:00");
queryRequest.setEndTime("2024-08-31T23:59:59+08:00");
queryRequest.setLimit(20);
queryRequest.setOffset(0);
// 查询扣费记录
SubscriptionTransactionQueryResult transactionResult = subscriptionService.queryTransactions(queryRequest);
System.out.println("总记录数: " + transactionResult.getTotalCount());
for (SubscriptionTransactionQueryResult.SubscriptionTransaction transaction : transactionResult.getData()) {
System.out.println("订单号: " + transaction.getOutTradeNo() + ", 状态: " + transaction.getTradeState());
}
```
## 扣费计划类型
- `MONTHLY`:按月扣费
- `WEEKLY`:按周扣费
- `DAILY`:按日扣费
- `YEARLY`:按年扣费
## 预约状态说明
- `SCHEDULED`:已预约
- `CANCELLED`:已取消
- `EXECUTED`:已执行
- `FAILED`:执行失败
## 交易状态说明
- `SUCCESS`:支付成功
- `REFUND`:转入退款
- `NOTPAY`:未支付
- `CLOSED`:已关闭
- `REVOKED`:已撤销(刷卡支付)
- `USERPAYING`:用户支付中
- `PAYERROR`:支付失败
## 注意事项
1. **用户授权**:使用预约扣费功能前,需要用户在微信内完成签约授权
2. **商户资质**:需要具备相应的业务资质才能开通此功能
3. **金额限制**:扣费金额需要在签约模板规定的范围内
4. **频率限制**API调用有频率限制请注意控制调用频次
5. **异常处理**建议对所有API调用进行异常处理
## 相关文档
- [微信支付预约扣费API文档](https://pay.weixin.qq.com/doc/v3/merchant/4012161105)
- [微信支付开发指南](https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml)
## 示例完整代码
```java
import com.github.binarywang.wxpay.service.SubscriptionBillingService;
import com.github.binarywang.wxpay.bean.subscriptionbilling.*;
public class SubscriptionBillingExample {
private SubscriptionBillingService subscriptionService;
public void example() throws Exception {
// 1. 创建预约扣费
SubscriptionScheduleRequest request = new SubscriptionScheduleRequest();
request.setOutTradeNo("subscription_" + System.currentTimeMillis());
request.setOpenid("用户openid");
request.setDescription("VIP会员续费");
request.setScheduleTime("2024-09-01T10:00:00+08:00");
SubscriptionAmount amount = new SubscriptionAmount();
amount.setTotal(3000);
amount.setCurrency("CNY");
request.setAmount(amount);
BillingPlan plan = new BillingPlan();
plan.setPlanType("MONTHLY");
plan.setPeriod(1);
plan.setTotalCount(12);
request.setBillingPlan(plan);
SubscriptionScheduleResult result = subscriptionService.scheduleSubscription(request);
// 2. 查询预约状态
SubscriptionQueryResult query = subscriptionService.querySubscription(result.getSubscriptionId());
// 3. 如需取消
if ("SCHEDULED".equals(query.getStatus())) {
SubscriptionCancelRequest cancelReq = new SubscriptionCancelRequest();
cancelReq.setSubscriptionId(result.getSubscriptionId());
cancelReq.setCancelReason("用户取消");
SubscriptionCancelResult cancelResult = subscriptionService.cancelSubscription(cancelReq);
}
}
}
```

View File

@@ -0,0 +1,110 @@
package com.github.binarywang.wxpay.bean.subscriptionbilling;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 扣费计划信息
* <pre>
* 文档地址https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* </pre>
*
* @author Binary Wang
*/
@Data
@NoArgsConstructor
public class BillingPlan implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:计划类型
* 变量名plan_type
* 是否必填:是
* 类型string(32)
* 描述:
* 扣费计划类型
* MONTHLY按月扣费
* WEEKLY按周扣费
* DAILY按日扣费
* YEARLY按年扣费
* 示例值MONTHLY
* </pre>
*/
@SerializedName("plan_type")
private String planType;
/**
* <pre>
* 字段名:扣费周期
* 变量名period
* 是否必填:是
* 类型int
* 描述:
* 扣费周期配合plan_type使用
* 例如plan_type为MONTHLYperiod为1表示每1个月扣费一次
* 示例值1
* </pre>
*/
@SerializedName("period")
private Integer period;
/**
* <pre>
* 字段名:总扣费次数
* 变量名total_count
* 是否必填:否
* 类型int
* 描述:
* 总扣费次数,不填表示无限次扣费
* 示例值12
* </pre>
*/
@SerializedName("total_count")
private Integer totalCount;
/**
* <pre>
* 字段名:已扣费次数
* 变量名executed_count
* 是否必填:否
* 类型int
* 描述:
* 已扣费次数,查询时返回
* 示例值2
* </pre>
*/
@SerializedName("executed_count")
private Integer executedCount;
/**
* <pre>
* 字段名:计划开始时间
* 变量名start_time
* 是否必填:否
* 类型string(32)
* 描述:
* 计划开始时间遵循rfc3339标准格式格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
* 示例值2018-06-08T10:34:56+08:00
* </pre>
*/
@SerializedName("start_time")
private String startTime;
/**
* <pre>
* 字段名:计划结束时间
* 变量名end_time
* 是否必填:否
* 类型string(32)
* 描述:
* 计划结束时间遵循rfc3339标准格式格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
* 示例值2019-06-08T10:34:56+08:00
* </pre>
*/
@SerializedName("end_time")
private String endTime;
}

View File

@@ -0,0 +1,49 @@
package com.github.binarywang.wxpay.bean.subscriptionbilling;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 预约扣费金额信息
* <pre>
* 文档地址https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* </pre>
*
* @author Binary Wang
*/
@Data
@NoArgsConstructor
public class SubscriptionAmount implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:总金额
* 变量名total
* 是否必填:是
* 类型int
* 描述:
* 订单总金额,单位为分
* 示例值100
* </pre>
*/
@SerializedName("total")
private Integer total;
/**
* <pre>
* 字段名:货币类型
* 变量名currency
* 是否必填:否
* 类型string(16)
* 描述:
* CNY人民币境内商户号仅支持人民币
* 示例值CNY
* </pre>
*/
@SerializedName("currency")
private String currency;
}

View File

@@ -0,0 +1,49 @@
package com.github.binarywang.wxpay.bean.subscriptionbilling;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 取消预约扣费请求参数
* <pre>
* 文档地址https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* </pre>
*
* @author Binary Wang
*/
@Data
@NoArgsConstructor
public class SubscriptionCancelRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名预约扣费ID
* 变量名subscription_id
* 是否必填:是
* 类型string(64)
* 描述:
* 微信支付预约扣费ID
* 示例值1217752501201407033233368018
* </pre>
*/
@SerializedName("subscription_id")
private String subscriptionId;
/**
* <pre>
* 字段名:取消原因
* 变量名cancel_reason
* 是否必填:否
* 类型string(256)
* 描述:
* 取消原因描述
* 示例值:用户主动取消
* </pre>
*/
@SerializedName("cancel_reason")
private String cancelReason;
}

View File

@@ -0,0 +1,77 @@
package com.github.binarywang.wxpay.bean.subscriptionbilling;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 取消预约扣费响应结果
* <pre>
* 文档地址https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* </pre>
*
* @author Binary Wang
*/
@Data
@NoArgsConstructor
public class SubscriptionCancelResult implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名预约扣费ID
* 变量名subscription_id
* 是否必填:是
* 类型string(64)
* 描述:
* 微信支付预约扣费ID
* 示例值1217752501201407033233368018
* </pre>
*/
@SerializedName("subscription_id")
private String subscriptionId;
/**
* <pre>
* 字段名:预约状态
* 变量名status
* 是否必填:是
* 类型string(32)
* 描述:
* 预约状态取消后应为CANCELLED
* 示例值CANCELLED
* </pre>
*/
@SerializedName("status")
private String status;
/**
* <pre>
* 字段名:取消时间
* 变量名cancel_time
* 是否必填:是
* 类型string(32)
* 描述:
* 取消时间遵循rfc3339标准格式
* 示例值2018-06-08T10:34:56+08:00
* </pre>
*/
@SerializedName("cancel_time")
private String cancelTime;
/**
* <pre>
* 字段名:取消原因
* 变量名cancel_reason
* 是否必填:否
* 类型string(256)
* 描述:
* 取消原因描述
* 示例值:用户主动取消
* </pre>
*/
@SerializedName("cancel_reason")
private String cancelReason;
}

View File

@@ -0,0 +1,104 @@
package com.github.binarywang.wxpay.bean.subscriptionbilling;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 立即扣费请求参数
* <pre>
* 文档地址https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* </pre>
*
* @author Binary Wang
*/
@Data
@NoArgsConstructor
public class SubscriptionInstantBillingRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:商户订单号
* 变量名out_trade_no
* 是否必填:是
* 类型string(32)
* 描述:
* 商户系统内部订单号只能是数字、大小写字母_-*且在同一个商户号下唯一
* 示例值1217752501201407033233368018
* </pre>
*/
@SerializedName("out_trade_no")
private String outTradeNo;
/**
* <pre>
* 字段名:用户标识
* 变量名openid
* 是否必填:是
* 类型string(128)
* 描述:
* 用户在直连商户appid下的唯一标识
* 示例值oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
* </pre>
*/
@SerializedName("openid")
private String openid;
/**
* <pre>
* 字段名:订单描述
* 变量名description
* 是否必填:是
* 类型string(127)
* 描述:
* 订单描述
* 示例值:腾讯充值中心-QQ会员充值
* </pre>
*/
@SerializedName("description")
private String description;
/**
* <pre>
* 字段名:扣费金额
* 变量名amount
* 是否必填:是
* 类型object
* 描述:
* 扣费金额信息
* </pre>
*/
@SerializedName("amount")
private SubscriptionAmount amount;
/**
* <pre>
* 字段名:通知地址
* 变量名notify_url
* 是否必填:否
* 类型string(256)
* 描述:
* 异步接收微信支付结果通知的回调地址通知url必须为外网可访问的url不能携带参数
* 示例值https://www.weixin.qq.com/wxpay/pay.php
* </pre>
*/
@SerializedName("notify_url")
private String notifyUrl;
/**
* <pre>
* 字段名:附加数据
* 变量名attach
* 是否必填:否
* 类型string(128)
* 描述:
* 附加数据在查询API和支付通知中原样返回可作为自定义参数使用
* 示例值:自定义数据
* </pre>
*/
@SerializedName("attach")
private String attach;
}

View File

@@ -0,0 +1,111 @@
package com.github.binarywang.wxpay.bean.subscriptionbilling;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 立即扣费响应结果
* <pre>
* 文档地址https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* </pre>
*
* @author Binary Wang
*/
@Data
@NoArgsConstructor
public class SubscriptionInstantBillingResult implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:微信支付订单号
* 变量名transaction_id
* 是否必填:是
* 类型string(32)
* 描述:
* 微信支付系统生成的订单号
* 示例值1217752501201407033233368018
* </pre>
*/
@SerializedName("transaction_id")
private String transactionId;
/**
* <pre>
* 字段名:商户订单号
* 变量名out_trade_no
* 是否必填:是
* 类型string(32)
* 描述:
* 商户系统内部订单号
* 示例值1217752501201407033233368018
* </pre>
*/
@SerializedName("out_trade_no")
private String outTradeNo;
/**
* <pre>
* 字段名:交易状态
* 变量名trade_state
* 是否必填:是
* 类型string(32)
* 描述:
* 交易状态
* SUCCESS支付成功
* REFUND转入退款
* NOTPAY未支付
* CLOSED已关闭
* REVOKED已撤销(刷卡支付)
* USERPAYING用户支付中
* PAYERROR支付失败
* 示例值SUCCESS
* </pre>
*/
@SerializedName("trade_state")
private String tradeState;
/**
* <pre>
* 字段名:交易状态描述
* 变量名trade_state_desc
* 是否必填:是
* 类型string(256)
* 描述:
* 交易状态描述
* 示例值:支付成功
* </pre>
*/
@SerializedName("trade_state_desc")
private String tradeStateDesc;
/**
* <pre>
* 字段名:支付完成时间
* 变量名success_time
* 是否必填:否
* 类型string(32)
* 描述:
* 支付完成时间遵循rfc3339标准格式
* 示例值2018-06-08T10:34:56+08:00
* </pre>
*/
@SerializedName("success_time")
private String successTime;
/**
* <pre>
* 字段名:扣费金额
* 变量名amount
* 是否必填:是
* 类型object
* 描述:
* 扣费金额信息
* </pre>
*/
@SerializedName("amount")
private SubscriptionAmount amount;
}

View File

@@ -0,0 +1,177 @@
package com.github.binarywang.wxpay.bean.subscriptionbilling;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 预约扣费查询结果
* <pre>
* 文档地址https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* </pre>
*
* @author Binary Wang
*/
@Data
@NoArgsConstructor
public class SubscriptionQueryResult implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名预约扣费ID
* 变量名subscription_id
* 是否必填:是
* 类型string(64)
* 描述:
* 微信支付预约扣费ID
* 示例值1217752501201407033233368018
* </pre>
*/
@SerializedName("subscription_id")
private String subscriptionId;
/**
* <pre>
* 字段名:商户订单号
* 变量名out_trade_no
* 是否必填:是
* 类型string(32)
* 描述:
* 商户系统内部订单号
* 示例值1217752501201407033233368018
* </pre>
*/
@SerializedName("out_trade_no")
private String outTradeNo;
/**
* <pre>
* 字段名:用户标识
* 变量名openid
* 是否必填:是
* 类型string(128)
* 描述:
* 用户在直连商户appid下的唯一标识
* 示例值oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
* </pre>
*/
@SerializedName("openid")
private String openid;
/**
* <pre>
* 字段名:订单描述
* 变量名description
* 是否必填:是
* 类型string(127)
* 描述:
* 订单描述
* 示例值:腾讯充值中心-QQ会员充值
* </pre>
*/
@SerializedName("description")
private String description;
/**
* <pre>
* 字段名:预约状态
* 变量名status
* 是否必填:是
* 类型string(32)
* 描述:
* 预约状态
* SCHEDULED已预约
* CANCELLED已取消
* EXECUTED已执行
* FAILED执行失败
* 示例值SCHEDULED
* </pre>
*/
@SerializedName("status")
private String status;
/**
* <pre>
* 字段名:预约扣费时间
* 变量名schedule_time
* 是否必填:是
* 类型string(32)
* 描述:
* 预约扣费的时间遵循rfc3339标准格式
* 示例值2018-06-08T10:34:56+08:00
* </pre>
*/
@SerializedName("schedule_time")
private String scheduleTime;
/**
* <pre>
* 字段名:创建时间
* 变量名create_time
* 是否必填:是
* 类型string(32)
* 描述:
* 预约创建时间遵循rfc3339标准格式
* 示例值2018-06-08T10:34:56+08:00
* </pre>
*/
@SerializedName("create_time")
private String createTime;
/**
* <pre>
* 字段名:更新时间
* 变量名update_time
* 是否必填:否
* 类型string(32)
* 描述:
* 预约更新时间遵循rfc3339标准格式
* 示例值2018-06-08T10:34:56+08:00
* </pre>
*/
@SerializedName("update_time")
private String updateTime;
/**
* <pre>
* 字段名:预约扣费金额
* 变量名amount
* 是否必填:是
* 类型object
* 描述:
* 预约扣费金额信息
* </pre>
*/
@SerializedName("amount")
private SubscriptionAmount amount;
/**
* <pre>
* 字段名:扣费计划
* 变量名billing_plan
* 是否必填:否
* 类型object
* 描述:
* 扣费计划信息
* </pre>
*/
@SerializedName("billing_plan")
private BillingPlan billingPlan;
/**
* <pre>
* 字段名:附加数据
* 变量名attach
* 是否必填:否
* 类型string(128)
* 描述:
* 附加数据
* 示例值:自定义数据
* </pre>
*/
@SerializedName("attach")
private String attach;
}

View File

@@ -0,0 +1,131 @@
package com.github.binarywang.wxpay.bean.subscriptionbilling;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 预约扣费请求参数
* <pre>
* 文档地址https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* </pre>
*
* @author Binary Wang
*/
@Data
@NoArgsConstructor
public class SubscriptionScheduleRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:商户订单号
* 变量名out_trade_no
* 是否必填:是
* 类型string(32)
* 描述:
* 商户系统内部订单号只能是数字、大小写字母_-*且在同一个商户号下唯一
* 示例值1217752501201407033233368018
* </pre>
*/
@SerializedName("out_trade_no")
private String outTradeNo;
/**
* <pre>
* 字段名:用户标识
* 变量名openid
* 是否必填:是
* 类型string(128)
* 描述:
* 用户在直连商户appid下的唯一标识
* 示例值oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
* </pre>
*/
@SerializedName("openid")
private String openid;
/**
* <pre>
* 字段名:订单描述
* 变量名description
* 是否必填:是
* 类型string(127)
* 描述:
* 订单描述
* 示例值:腾讯充值中心-QQ会员充值
* </pre>
*/
@SerializedName("description")
private String description;
/**
* <pre>
* 字段名:预约扣费金额
* 变量名amount
* 是否必填:是
* 类型object
* 描述:
* 预约扣费金额信息
* </pre>
*/
@SerializedName("amount")
private SubscriptionAmount amount;
/**
* <pre>
* 字段名:预约扣费时间
* 变量名schedule_time
* 是否必填:是
* 类型string(32)
* 描述:
* 预约扣费的时间遵循rfc3339标准格式格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
* 示例值2018-06-08T10:34:56+08:00
* </pre>
*/
@SerializedName("schedule_time")
private String scheduleTime;
/**
* <pre>
* 字段名:扣费计划
* 变量名billing_plan
* 是否必填:否
* 类型object
* 描述:
* 扣费计划信息,用于连续包月等场景
* </pre>
*/
@SerializedName("billing_plan")
private BillingPlan billingPlan;
/**
* <pre>
* 字段名:通知地址
* 变量名notify_url
* 是否必填:否
* 类型string(256)
* 描述:
* 异步接收微信支付结果通知的回调地址通知url必须为外网可访问的url不能携带参数
* 示例值https://www.weixin.qq.com/wxpay/pay.php
* </pre>
*/
@SerializedName("notify_url")
private String notifyUrl;
/**
* <pre>
* 字段名:附加数据
* 变量名attach
* 是否必填:否
* 类型string(128)
* 描述:
* 附加数据在查询API和支付通知中原样返回可作为自定义参数使用
* 示例值:自定义数据
* </pre>
*/
@SerializedName("attach")
private String attach;
}

View File

@@ -0,0 +1,121 @@
package com.github.binarywang.wxpay.bean.subscriptionbilling;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 预约扣费响应结果
* <pre>
* 文档地址https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* </pre>
*
* @author Binary Wang
*/
@Data
@NoArgsConstructor
public class SubscriptionScheduleResult implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名预约扣费ID
* 变量名subscription_id
* 是否必填:是
* 类型string(64)
* 描述:
* 微信支付预约扣费ID
* 示例值1217752501201407033233368018
* </pre>
*/
@SerializedName("subscription_id")
private String subscriptionId;
/**
* <pre>
* 字段名:商户订单号
* 变量名out_trade_no
* 是否必填:是
* 类型string(32)
* 描述:
* 商户系统内部订单号
* 示例值1217752501201407033233368018
* </pre>
*/
@SerializedName("out_trade_no")
private String outTradeNo;
/**
* <pre>
* 字段名:预约状态
* 变量名status
* 是否必填:是
* 类型string(32)
* 描述:
* 预约状态
* SCHEDULED已预约
* CANCELLED已取消
* EXECUTED已执行
* FAILED执行失败
* 示例值SCHEDULED
* </pre>
*/
@SerializedName("status")
private String status;
/**
* <pre>
* 字段名:预约扣费时间
* 变量名schedule_time
* 是否必填:是
* 类型string(32)
* 描述:
* 预约扣费的时间遵循rfc3339标准格式
* 示例值2018-06-08T10:34:56+08:00
* </pre>
*/
@SerializedName("schedule_time")
private String scheduleTime;
/**
* <pre>
* 字段名:创建时间
* 变量名create_time
* 是否必填:是
* 类型string(32)
* 描述:
* 预约创建时间遵循rfc3339标准格式
* 示例值2018-06-08T10:34:56+08:00
* </pre>
*/
@SerializedName("create_time")
private String createTime;
/**
* <pre>
* 字段名:预约扣费金额
* 变量名amount
* 是否必填:是
* 类型object
* 描述:
* 预约扣费金额信息
* </pre>
*/
@SerializedName("amount")
private SubscriptionAmount amount;
/**
* <pre>
* 字段名:扣费计划
* 变量名billing_plan
* 是否必填:否
* 类型object
* 描述:
* 扣费计划信息
* </pre>
*/
@SerializedName("billing_plan")
private BillingPlan billingPlan;
}

View File

@@ -0,0 +1,91 @@
package com.github.binarywang.wxpay.bean.subscriptionbilling;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 查询扣费记录请求参数
* <pre>
* 文档地址https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* </pre>
*
* @author Binary Wang
*/
@Data
@NoArgsConstructor
public class SubscriptionTransactionQueryRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:用户标识
* 变量名openid
* 是否必填:否
* 类型string(128)
* 描述:
* 用户在直连商户appid下的唯一标识
* 示例值oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
* </pre>
*/
@SerializedName("openid")
private String openid;
/**
* <pre>
* 字段名:开始时间
* 变量名begin_time
* 是否必填:否
* 类型string(32)
* 描述:
* 查询开始时间遵循rfc3339标准格式格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
* 示例值2018-06-08T10:34:56+08:00
* </pre>
*/
@SerializedName("begin_time")
private String beginTime;
/**
* <pre>
* 字段名:结束时间
* 变量名end_time
* 是否必填:否
* 类型string(32)
* 描述:
* 查询结束时间遵循rfc3339标准格式格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
* 示例值2018-06-08T10:34:56+08:00
* </pre>
*/
@SerializedName("end_time")
private String endTime;
/**
* <pre>
* 字段名:分页大小
* 变量名limit
* 是否必填:否
* 类型int
* 描述:
* 分页大小不超过50
* 示例值20
* </pre>
*/
@SerializedName("limit")
private Integer limit;
/**
* <pre>
* 字段名:分页偏移量
* 变量名offset
* 是否必填:否
* 类型int
* 描述:
* 分页偏移量
* 示例值0
* </pre>
*/
@SerializedName("offset")
private Integer offset;
}

View File

@@ -0,0 +1,190 @@
package com.github.binarywang.wxpay.bean.subscriptionbilling;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 查询扣费记录响应结果
* <pre>
* 文档地址https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* </pre>
*
* @author Binary Wang
*/
@Data
@NoArgsConstructor
public class SubscriptionTransactionQueryResult implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:总数量
* 变量名total_count
* 是否必填:是
* 类型int
* 描述:
* 符合条件的记录总数量
* 示例值100
* </pre>
*/
@SerializedName("total_count")
private Integer totalCount;
/**
* <pre>
* 字段名:扣费记录列表
* 变量名data
* 是否必填:是
* 类型array
* 描述:
* 扣费记录列表
* </pre>
*/
@SerializedName("data")
private List<SubscriptionTransaction> data;
/**
* 扣费记录
*/
@Data
@NoArgsConstructor
public static class SubscriptionTransaction implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:微信支付订单号
* 变量名transaction_id
* 是否必填:是
* 类型string(32)
* 描述:
* 微信支付系统生成的订单号
* 示例值1217752501201407033233368018
* </pre>
*/
@SerializedName("transaction_id")
private String transactionId;
/**
* <pre>
* 字段名:商户订单号
* 变量名out_trade_no
* 是否必填:是
* 类型string(32)
* 描述:
* 商户系统内部订单号
* 示例值1217752501201407033233368018
* </pre>
*/
@SerializedName("out_trade_no")
private String outTradeNo;
/**
* <pre>
* 字段名预约扣费ID
* 变量名subscription_id
* 是否必填:否
* 类型string(64)
* 描述:
* 微信支付预约扣费ID预约扣费产生的交易才有此字段
* 示例值1217752501201407033233368018
* </pre>
*/
@SerializedName("subscription_id")
private String subscriptionId;
/**
* <pre>
* 字段名:交易状态
* 变量名trade_state
* 是否必填:是
* 类型string(32)
* 描述:
* 交易状态
* SUCCESS支付成功
* REFUND转入退款
* NOTPAY未支付
* CLOSED已关闭
* REVOKED已撤销(刷卡支付)
* USERPAYING用户支付中
* PAYERROR支付失败
* 示例值SUCCESS
* </pre>
*/
@SerializedName("trade_state")
private String tradeState;
/**
* <pre>
* 字段名:支付完成时间
* 变量名success_time
* 是否必填:否
* 类型string(32)
* 描述:
* 支付完成时间遵循rfc3339标准格式
* 示例值2018-06-08T10:34:56+08:00
* </pre>
*/
@SerializedName("success_time")
private String successTime;
/**
* <pre>
* 字段名:扣费金额
* 变量名amount
* 是否必填:是
* 类型object
* 描述:
* 扣费金额信息
* </pre>
*/
@SerializedName("amount")
private SubscriptionAmount amount;
/**
* <pre>
* 字段名:用户标识
* 变量名openid
* 是否必填:是
* 类型string(128)
* 描述:
* 用户在直连商户appid下的唯一标识
* 示例值oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
* </pre>
*/
@SerializedName("openid")
private String openid;
/**
* <pre>
* 字段名:订单描述
* 变量名description
* 是否必填:是
* 类型string(127)
* 描述:
* 订单描述
* 示例值:腾讯充值中心-QQ会员充值
* </pre>
*/
@SerializedName("description")
private String description;
/**
* <pre>
* 字段名:附加数据
* 变量名attach
* 是否必填:否
* 类型string(128)
* 描述:
* 附加数据
* 示例值:自定义数据
* </pre>
*/
@SerializedName("attach")
private String attach;
}
}

View File

@@ -0,0 +1,105 @@
package com.github.binarywang.wxpay.service;
import com.github.binarywang.wxpay.bean.subscriptionbilling.*;
import com.github.binarywang.wxpay.exception.WxPayException;
/**
* 微信支付-预约扣费服务 (连续包月功能)
* <pre>
* 微信支付预约扣费功能,支持商户在用户授权的情况下,
* 按照约定的时间和金额,自动从用户的支付账户中扣取费用。
* 主要用于连续包月、订阅服务等场景。
*
* 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* </pre>
*
* @author Binary Wang
* created on 2024-08-31
*/
public interface SubscriptionBillingService {
/**
* 预约扣费
* <pre>
* 商户可以通过该接口预约未来某个时间点的扣费。
* 适用于连续包月、订阅服务等场景。
*
* 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* 请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/schedule
* 请求方式: POST
* 是否需要证书: 是
* </pre>
*
* @param request 预约扣费请求参数
* @return 预约扣费结果
* @throws WxPayException 微信支付异常
*/
SubscriptionScheduleResult scheduleSubscription(SubscriptionScheduleRequest request) throws WxPayException;
/**
* 查询预约扣费
* <pre>
* 商户可以通过该接口查询已预约的扣费信息。
*
* 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* 请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/schedule/{subscription_id}
* 请求方式: GET
* </pre>
*
* @param subscriptionId 预约扣费ID
* @return 预约扣费查询结果
* @throws WxPayException 微信支付异常
*/
SubscriptionQueryResult querySubscription(String subscriptionId) throws WxPayException;
/**
* 取消预约扣费
* <pre>
* 商户可以通过该接口取消已预约的扣费。
*
* 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* 请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/schedule/{subscription_id}/cancel
* 请求方式: POST
* 是否需要证书: 是
* </pre>
*
* @param request 取消预约扣费请求参数
* @return 取消预约扣费结果
* @throws WxPayException 微信支付异常
*/
SubscriptionCancelResult cancelSubscription(SubscriptionCancelRequest request) throws WxPayException;
/**
* 立即扣费
* <pre>
* 商户可以通过该接口立即执行扣费操作。
* 通常用于补扣失败的费用或者特殊情况下的即时扣费。
*
* 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* 请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/instant-billing
* 请求方式: POST
* 是否需要证书: 是
* </pre>
*
* @param request 立即扣费请求参数
* @return 立即扣费结果
* @throws WxPayException 微信支付异常
*/
SubscriptionInstantBillingResult instantBilling(SubscriptionInstantBillingRequest request) throws WxPayException;
/**
* 查询扣费记录
* <pre>
* 商户可以通过该接口查询扣费记录。
*
* 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* 请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/transactions
* 请求方式: GET
* </pre>
*
* @param request 查询扣费记录请求参数
* @return 扣费记录查询结果
* @throws WxPayException 微信支付异常
*/
SubscriptionTransactionQueryResult queryTransactions(SubscriptionTransactionQueryRequest request) throws WxPayException;
}

View File

@@ -337,6 +337,13 @@ public interface WxPayService {
*/
BrandMerchantTransferService getBrandMerchantTransferService();
/**
* 获取微信支付预约扣费服务类 (连续包月功能)
*
* @return the subscription billing service
*/
SubscriptionBillingService getSubscriptionBillingService();
/**
* 设置企业付款服务类,允许开发者自定义实现类.
*

View File

@@ -133,6 +133,9 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
@Getter
private final BrandMerchantTransferService brandMerchantTransferService = new BrandMerchantTransferServiceImpl(this);
@Getter
private final SubscriptionBillingService subscriptionBillingService = new SubscriptionBillingServiceImpl(this);
@Getter
private final BusinessOperationTransferService businessOperationTransferService = new BusinessOperationTransferServiceImpl(this);

View File

@@ -0,0 +1,91 @@
package com.github.binarywang.wxpay.service.impl;
import com.github.binarywang.wxpay.bean.subscriptionbilling.*;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.SubscriptionBillingService;
import com.github.binarywang.wxpay.service.WxPayService;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 微信支付-预约扣费服务实现 (连续包月功能)
* <pre>
* 微信支付预约扣费功能,支持商户在用户授权的情况下,
* 按照约定的时间和金额,自动从用户的支付账户中扣取费用。
* 主要用于连续包月、订阅服务等场景。
*
* 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
* </pre>
*
* @author Binary Wang
* created on 2024-08-31
*/
@Slf4j
@RequiredArgsConstructor
public class SubscriptionBillingServiceImpl implements SubscriptionBillingService {
private static final Gson GSON = new GsonBuilder().create();
private final WxPayService payService;
@Override
public SubscriptionScheduleResult scheduleSubscription(SubscriptionScheduleRequest request) throws WxPayException {
String url = String.format("%s/v3/subscription-billing/schedule", this.payService.getPayBaseUrl());
String response = this.payService.postV3(url, GSON.toJson(request));
return GSON.fromJson(response, SubscriptionScheduleResult.class);
}
@Override
public SubscriptionQueryResult querySubscription(String subscriptionId) throws WxPayException {
String url = String.format("%s/v3/subscription-billing/schedule/%s", this.payService.getPayBaseUrl(), subscriptionId);
String response = this.payService.getV3(url);
return GSON.fromJson(response, SubscriptionQueryResult.class);
}
@Override
public SubscriptionCancelResult cancelSubscription(SubscriptionCancelRequest request) throws WxPayException {
String url = String.format("%s/v3/subscription-billing/schedule/%s/cancel",
this.payService.getPayBaseUrl(), request.getSubscriptionId());
String response = this.payService.postV3(url, GSON.toJson(request));
return GSON.fromJson(response, SubscriptionCancelResult.class);
}
@Override
public SubscriptionInstantBillingResult instantBilling(SubscriptionInstantBillingRequest request) throws WxPayException {
String url = String.format("%s/v3/subscription-billing/instant-billing", this.payService.getPayBaseUrl());
String response = this.payService.postV3(url, GSON.toJson(request));
return GSON.fromJson(response, SubscriptionInstantBillingResult.class);
}
@Override
public SubscriptionTransactionQueryResult queryTransactions(SubscriptionTransactionQueryRequest request) throws WxPayException {
String url = String.format("%s/v3/subscription-billing/transactions", this.payService.getPayBaseUrl());
StringBuilder queryString = new StringBuilder();
if (request.getOpenid() != null) {
queryString.append("openid=").append(request.getOpenid()).append("&");
}
if (request.getBeginTime() != null) {
queryString.append("begin_time=").append(request.getBeginTime()).append("&");
}
if (request.getEndTime() != null) {
queryString.append("end_time=").append(request.getEndTime()).append("&");
}
if (request.getLimit() != null) {
queryString.append("limit=").append(request.getLimit()).append("&");
}
if (request.getOffset() != null) {
queryString.append("offset=").append(request.getOffset()).append("&");
}
if (queryString.length() > 0) {
// Remove trailing &
queryString.setLength(queryString.length() - 1);
url += "?" + queryString.toString();
}
String response = this.payService.getV3(url);
return GSON.fromJson(response, SubscriptionTransactionQueryResult.class);
}
}

View File

@@ -0,0 +1,144 @@
package com.github.binarywang.wxpay.service.impl;
import com.github.binarywang.wxpay.bean.subscriptionbilling.*;
import com.github.binarywang.wxpay.service.SubscriptionBillingService;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.testbase.ApiTestModule;
import com.google.inject.Inject;
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
/**
* 微信支付预约扣费服务测试类
* <p>
* 注意:由于预约扣费功能需要用户授权和实际的签约关系,
* 这些测试主要用于验证接口调用的正确性,而不是功能的完整性。
* 实际测试需要在具有有效签约关系的环境中进行。
* </p>
*
* @author Binary Wang
*/
@Test(enabled = false) // 默认关闭,需要实际环境配置才能测试
@Guice(modules = ApiTestModule.class)
public class SubscriptionBillingServiceImplTest {
@Inject
private WxPayService wxPayService;
@Test
public void testScheduleSubscription() {
try {
SubscriptionBillingService service = this.wxPayService.getSubscriptionBillingService();
SubscriptionScheduleRequest request = new SubscriptionScheduleRequest();
request.setOutTradeNo("test_subscription_" + System.currentTimeMillis());
request.setOpenid("test_openid");
request.setDescription("测试预约扣费");
request.setScheduleTime("2024-09-01T10:00:00+08:00");
SubscriptionAmount amount = new SubscriptionAmount();
amount.setTotal(100); // 1元单位分
amount.setCurrency("CNY");
request.setAmount(amount);
BillingPlan billingPlan = new BillingPlan();
billingPlan.setPlanType("MONTHLY");
billingPlan.setPeriod(1);
billingPlan.setTotalCount(12);
request.setBillingPlan(billingPlan);
SubscriptionScheduleResult result = service.scheduleSubscription(request);
System.out.println("预约扣费结果:" + result.toString());
assert result.getSubscriptionId() != null;
assert "SCHEDULED".equals(result.getStatus());
} catch (Exception e) {
// 预期会因为测试环境没有有效的签约关系而失败
System.out.println("预约扣费测试异常(预期):" + e.getMessage());
}
}
@Test
public void testQuerySubscription() {
try {
SubscriptionBillingService service = this.wxPayService.getSubscriptionBillingService();
SubscriptionQueryResult result = service.querySubscription("test_subscription_id");
System.out.println("查询预约扣费结果:" + result.toString());
} catch (Exception e) {
// 预期会因为测试数据不存在而失败
System.out.println("查询预约扣费测试异常(预期):" + e.getMessage());
}
}
@Test
public void testCancelSubscription() {
try {
SubscriptionBillingService service = this.wxPayService.getSubscriptionBillingService();
SubscriptionCancelRequest request = new SubscriptionCancelRequest();
request.setSubscriptionId("test_subscription_id");
request.setCancelReason("测试取消");
SubscriptionCancelResult result = service.cancelSubscription(request);
System.out.println("取消预约扣费结果:" + result.toString());
assert "CANCELLED".equals(result.getStatus());
} catch (Exception e) {
// 预期会因为测试数据不存在而失败
System.out.println("取消预约扣费测试异常(预期):" + e.getMessage());
}
}
@Test
public void testInstantBilling() {
try {
SubscriptionBillingService service = this.wxPayService.getSubscriptionBillingService();
SubscriptionInstantBillingRequest request = new SubscriptionInstantBillingRequest();
request.setOutTradeNo("test_instant_" + System.currentTimeMillis());
request.setOpenid("test_openid");
request.setDescription("测试立即扣费");
SubscriptionAmount amount = new SubscriptionAmount();
amount.setTotal(100); // 1元单位分
amount.setCurrency("CNY");
request.setAmount(amount);
SubscriptionInstantBillingResult result = service.instantBilling(request);
System.out.println("立即扣费结果:" + result.toString());
assert result.getTransactionId() != null;
} catch (Exception e) {
// 预期会因为测试环境没有有效的签约关系而失败
System.out.println("立即扣费测试异常(预期):" + e.getMessage());
}
}
@Test
public void testQueryTransactions() {
try {
SubscriptionBillingService service = this.wxPayService.getSubscriptionBillingService();
SubscriptionTransactionQueryRequest request = new SubscriptionTransactionQueryRequest();
request.setOpenid("test_openid");
request.setBeginTime("2024-08-01T00:00:00+08:00");
request.setEndTime("2024-08-31T23:59:59+08:00");
request.setLimit(20);
request.setOffset(0);
SubscriptionTransactionQueryResult result = service.queryTransactions(request);
System.out.println("查询扣费记录结果:" + result.toString());
assert result.getTotalCount() != null;
} catch (Exception e) {
// 预期会因为测试环境数据问题而失败
System.out.println("查询扣费记录测试异常(预期):" + e.getMessage());
}
}
}