From 84358d8141e910da77bd7fa8672a2fc9ca80a8ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:50:43 +0000 Subject: [PATCH 1/3] Initial plan From dcbc337f46a650bca63640b04b8c6a08e659d0ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 31 Aug 2025 15:03:04 +0000 Subject: [PATCH 2/3] =?UTF-8?q?Implement=20WeChat=20Pay=20V3=20subscriptio?= =?UTF-8?q?n=20billing=20functionality=20(=E9=A2=84=E7=BA=A6=E6=89=A3?= =?UTF-8?q?=E8=B4=B9=E5=8A=9F=E8=83=BD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../bean/subscriptionbilling/BillingPlan.java | 110 ++++++++++ .../SubscriptionAmount.java | 49 +++++ .../SubscriptionCancelRequest.java | 49 +++++ .../SubscriptionCancelResult.java | 77 +++++++ .../SubscriptionInstantBillingRequest.java | 104 ++++++++++ .../SubscriptionInstantBillingResult.java | 111 ++++++++++ .../SubscriptionQueryResult.java | 177 ++++++++++++++++ .../SubscriptionScheduleRequest.java | 131 ++++++++++++ .../SubscriptionScheduleResult.java | 121 +++++++++++ .../SubscriptionTransactionQueryRequest.java | 91 +++++++++ .../SubscriptionTransactionQueryResult.java | 190 ++++++++++++++++++ .../service/SubscriptionBillingService.java | 105 ++++++++++ .../wxpay/service/WxPayService.java | 7 + .../service/impl/BaseWxPayServiceImpl.java | 3 + .../impl/SubscriptionBillingServiceImpl.java | 91 +++++++++ .../SubscriptionBillingServiceImplTest.java | 144 +++++++++++++ 16 files changed, 1560 insertions(+) create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/BillingPlan.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionAmount.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionCancelRequest.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionCancelResult.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionInstantBillingRequest.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionInstantBillingResult.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionQueryResult.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionScheduleRequest.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionScheduleResult.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionTransactionQueryRequest.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionTransactionQueryResult.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/SubscriptionBillingService.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/SubscriptionBillingServiceImpl.java create mode 100644 weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/SubscriptionBillingServiceImplTest.java diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/BillingPlan.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/BillingPlan.java new file mode 100644 index 0000000000..b664f4cb7b --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/BillingPlan.java @@ -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; + +/** + * 扣费计划信息 + *
+ *   文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 
+ * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +public class BillingPlan implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:计划类型
+   * 变量名:plan_type
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  扣费计划类型
+   *  MONTHLY:按月扣费
+   *  WEEKLY:按周扣费
+   *  DAILY:按日扣费
+   *  YEARLY:按年扣费
+   *  示例值:MONTHLY
+   * 
+ */ + @SerializedName("plan_type") + private String planType; + + /** + *
+   * 字段名:扣费周期
+   * 变量名:period
+   * 是否必填:是
+   * 类型:int
+   * 描述:
+   *  扣费周期,配合plan_type使用
+   *  例如:plan_type为MONTHLY,period为1,表示每1个月扣费一次
+   *  示例值:1
+   * 
+ */ + @SerializedName("period") + private Integer period; + + /** + *
+   * 字段名:总扣费次数
+   * 变量名:total_count
+   * 是否必填:否
+   * 类型:int
+   * 描述:
+   *  总扣费次数,不填表示无限次扣费
+   *  示例值:12
+   * 
+ */ + @SerializedName("total_count") + private Integer totalCount; + + /** + *
+   * 字段名:已扣费次数
+   * 变量名:executed_count
+   * 是否必填:否
+   * 类型:int
+   * 描述:
+   *  已扣费次数,查询时返回
+   *  示例值:2
+   * 
+ */ + @SerializedName("executed_count") + private Integer executedCount; + + /** + *
+   * 字段名:计划开始时间
+   * 变量名:start_time
+   * 是否必填:否
+   * 类型:string(32)
+   * 描述:
+   *  计划开始时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
+   *  示例值:2018-06-08T10:34:56+08:00
+   * 
+ */ + @SerializedName("start_time") + private String startTime; + + /** + *
+   * 字段名:计划结束时间
+   * 变量名:end_time
+   * 是否必填:否
+   * 类型:string(32)
+   * 描述:
+   *  计划结束时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
+   *  示例值:2019-06-08T10:34:56+08:00
+   * 
+ */ + @SerializedName("end_time") + private String endTime; +} \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionAmount.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionAmount.java new file mode 100644 index 0000000000..c778a8ecb6 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionAmount.java @@ -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; + +/** + * 预约扣费金额信息 + *
+ *   文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 
+ * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +public class SubscriptionAmount implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:总金额
+   * 变量名:total
+   * 是否必填:是
+   * 类型:int
+   * 描述:
+   *  订单总金额,单位为分
+   *  示例值:100
+   * 
+ */ + @SerializedName("total") + private Integer total; + + /** + *
+   * 字段名:货币类型
+   * 变量名:currency
+   * 是否必填:否
+   * 类型:string(16)
+   * 描述:
+   *  CNY:人民币,境内商户号仅支持人民币
+   *  示例值:CNY
+   * 
+ */ + @SerializedName("currency") + private String currency; +} \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionCancelRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionCancelRequest.java new file mode 100644 index 0000000000..233d756f03 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionCancelRequest.java @@ -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; + +/** + * 取消预约扣费请求参数 + *
+ *   文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 
+ * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +public class SubscriptionCancelRequest implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:预约扣费ID
+   * 变量名:subscription_id
+   * 是否必填:是
+   * 类型:string(64)
+   * 描述:
+   *  微信支付预约扣费ID
+   *  示例值:1217752501201407033233368018
+   * 
+ */ + @SerializedName("subscription_id") + private String subscriptionId; + + /** + *
+   * 字段名:取消原因
+   * 变量名:cancel_reason
+   * 是否必填:否
+   * 类型:string(256)
+   * 描述:
+   *  取消原因描述
+   *  示例值:用户主动取消
+   * 
+ */ + @SerializedName("cancel_reason") + private String cancelReason; +} \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionCancelResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionCancelResult.java new file mode 100644 index 0000000000..74ca22f130 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionCancelResult.java @@ -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; + +/** + * 取消预约扣费响应结果 + *
+ *   文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 
+ * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +public class SubscriptionCancelResult implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:预约扣费ID
+   * 变量名:subscription_id
+   * 是否必填:是
+   * 类型:string(64)
+   * 描述:
+   *  微信支付预约扣费ID
+   *  示例值:1217752501201407033233368018
+   * 
+ */ + @SerializedName("subscription_id") + private String subscriptionId; + + /** + *
+   * 字段名:预约状态
+   * 变量名:status
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  预约状态,取消后应为CANCELLED
+   *  示例值:CANCELLED
+   * 
+ */ + @SerializedName("status") + private String status; + + /** + *
+   * 字段名:取消时间
+   * 变量名:cancel_time
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  取消时间,遵循rfc3339标准格式
+   *  示例值:2018-06-08T10:34:56+08:00
+   * 
+ */ + @SerializedName("cancel_time") + private String cancelTime; + + /** + *
+   * 字段名:取消原因
+   * 变量名:cancel_reason
+   * 是否必填:否
+   * 类型:string(256)
+   * 描述:
+   *  取消原因描述
+   *  示例值:用户主动取消
+   * 
+ */ + @SerializedName("cancel_reason") + private String cancelReason; +} \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionInstantBillingRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionInstantBillingRequest.java new file mode 100644 index 0000000000..2b5a3dec37 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionInstantBillingRequest.java @@ -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; + +/** + * 立即扣费请求参数 + *
+ *   文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 
+ * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +public class SubscriptionInstantBillingRequest implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:商户订单号
+   * 变量名:out_trade_no
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
+   *  示例值:1217752501201407033233368018
+   * 
+ */ + @SerializedName("out_trade_no") + private String outTradeNo; + + /** + *
+   * 字段名:用户标识
+   * 变量名:openid
+   * 是否必填:是
+   * 类型:string(128)
+   * 描述:
+   *  用户在直连商户appid下的唯一标识
+   *  示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
+   * 
+ */ + @SerializedName("openid") + private String openid; + + /** + *
+   * 字段名:订单描述
+   * 变量名:description
+   * 是否必填:是
+   * 类型:string(127)
+   * 描述:
+   *  订单描述
+   *  示例值:腾讯充值中心-QQ会员充值
+   * 
+ */ + @SerializedName("description") + private String description; + + /** + *
+   * 字段名:扣费金额
+   * 变量名:amount
+   * 是否必填:是
+   * 类型:object
+   * 描述:
+   *  扣费金额信息
+   * 
+ */ + @SerializedName("amount") + private SubscriptionAmount amount; + + /** + *
+   * 字段名:通知地址
+   * 变量名:notify_url
+   * 是否必填:否
+   * 类型:string(256)
+   * 描述:
+   *  异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数
+   *  示例值:https://www.weixin.qq.com/wxpay/pay.php
+   * 
+ */ + @SerializedName("notify_url") + private String notifyUrl; + + /** + *
+   * 字段名:附加数据
+   * 变量名:attach
+   * 是否必填:否
+   * 类型:string(128)
+   * 描述:
+   *  附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
+   *  示例值:自定义数据
+   * 
+ */ + @SerializedName("attach") + private String attach; +} \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionInstantBillingResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionInstantBillingResult.java new file mode 100644 index 0000000000..ac34307cd4 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionInstantBillingResult.java @@ -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; + +/** + * 立即扣费响应结果 + *
+ *   文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 
+ * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +public class SubscriptionInstantBillingResult implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:微信支付订单号
+   * 变量名:transaction_id
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  微信支付系统生成的订单号
+   *  示例值:1217752501201407033233368018
+   * 
+ */ + @SerializedName("transaction_id") + private String transactionId; + + /** + *
+   * 字段名:商户订单号
+   * 变量名:out_trade_no
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  商户系统内部订单号
+   *  示例值:1217752501201407033233368018
+   * 
+ */ + @SerializedName("out_trade_no") + private String outTradeNo; + + /** + *
+   * 字段名:交易状态
+   * 变量名:trade_state
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  交易状态
+   *  SUCCESS:支付成功
+   *  REFUND:转入退款
+   *  NOTPAY:未支付
+   *  CLOSED:已关闭
+   *  REVOKED:已撤销(刷卡支付)
+   *  USERPAYING:用户支付中
+   *  PAYERROR:支付失败
+   *  示例值:SUCCESS
+   * 
+ */ + @SerializedName("trade_state") + private String tradeState; + + /** + *
+   * 字段名:交易状态描述
+   * 变量名:trade_state_desc
+   * 是否必填:是
+   * 类型:string(256)
+   * 描述:
+   *  交易状态描述
+   *  示例值:支付成功
+   * 
+ */ + @SerializedName("trade_state_desc") + private String tradeStateDesc; + + /** + *
+   * 字段名:支付完成时间
+   * 变量名:success_time
+   * 是否必填:否
+   * 类型:string(32)
+   * 描述:
+   *  支付完成时间,遵循rfc3339标准格式
+   *  示例值:2018-06-08T10:34:56+08:00
+   * 
+ */ + @SerializedName("success_time") + private String successTime; + + /** + *
+   * 字段名:扣费金额
+   * 变量名:amount
+   * 是否必填:是
+   * 类型:object
+   * 描述:
+   *  扣费金额信息
+   * 
+ */ + @SerializedName("amount") + private SubscriptionAmount amount; +} \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionQueryResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionQueryResult.java new file mode 100644 index 0000000000..17e2c7dc19 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionQueryResult.java @@ -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; + +/** + * 预约扣费查询结果 + *
+ *   文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 
+ * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +public class SubscriptionQueryResult implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:预约扣费ID
+   * 变量名:subscription_id
+   * 是否必填:是
+   * 类型:string(64)
+   * 描述:
+   *  微信支付预约扣费ID
+   *  示例值:1217752501201407033233368018
+   * 
+ */ + @SerializedName("subscription_id") + private String subscriptionId; + + /** + *
+   * 字段名:商户订单号
+   * 变量名:out_trade_no
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  商户系统内部订单号
+   *  示例值:1217752501201407033233368018
+   * 
+ */ + @SerializedName("out_trade_no") + private String outTradeNo; + + /** + *
+   * 字段名:用户标识
+   * 变量名:openid
+   * 是否必填:是
+   * 类型:string(128)
+   * 描述:
+   *  用户在直连商户appid下的唯一标识
+   *  示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
+   * 
+ */ + @SerializedName("openid") + private String openid; + + /** + *
+   * 字段名:订单描述
+   * 变量名:description
+   * 是否必填:是
+   * 类型:string(127)
+   * 描述:
+   *  订单描述
+   *  示例值:腾讯充值中心-QQ会员充值
+   * 
+ */ + @SerializedName("description") + private String description; + + /** + *
+   * 字段名:预约状态
+   * 变量名:status
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  预约状态
+   *  SCHEDULED:已预约
+   *  CANCELLED:已取消
+   *  EXECUTED:已执行
+   *  FAILED:执行失败
+   *  示例值:SCHEDULED
+   * 
+ */ + @SerializedName("status") + private String status; + + /** + *
+   * 字段名:预约扣费时间
+   * 变量名:schedule_time
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  预约扣费的时间,遵循rfc3339标准格式
+   *  示例值:2018-06-08T10:34:56+08:00
+   * 
+ */ + @SerializedName("schedule_time") + private String scheduleTime; + + /** + *
+   * 字段名:创建时间
+   * 变量名:create_time
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  预约创建时间,遵循rfc3339标准格式
+   *  示例值:2018-06-08T10:34:56+08:00
+   * 
+ */ + @SerializedName("create_time") + private String createTime; + + /** + *
+   * 字段名:更新时间
+   * 变量名:update_time
+   * 是否必填:否
+   * 类型:string(32)
+   * 描述:
+   *  预约更新时间,遵循rfc3339标准格式
+   *  示例值:2018-06-08T10:34:56+08:00
+   * 
+ */ + @SerializedName("update_time") + private String updateTime; + + /** + *
+   * 字段名:预约扣费金额
+   * 变量名:amount
+   * 是否必填:是
+   * 类型:object
+   * 描述:
+   *  预约扣费金额信息
+   * 
+ */ + @SerializedName("amount") + private SubscriptionAmount amount; + + /** + *
+   * 字段名:扣费计划
+   * 变量名:billing_plan
+   * 是否必填:否
+   * 类型:object
+   * 描述:
+   *  扣费计划信息
+   * 
+ */ + @SerializedName("billing_plan") + private BillingPlan billingPlan; + + /** + *
+   * 字段名:附加数据
+   * 变量名:attach
+   * 是否必填:否
+   * 类型:string(128)
+   * 描述:
+   *  附加数据
+   *  示例值:自定义数据
+   * 
+ */ + @SerializedName("attach") + private String attach; +} \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionScheduleRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionScheduleRequest.java new file mode 100644 index 0000000000..51cf5aebd1 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionScheduleRequest.java @@ -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; + +/** + * 预约扣费请求参数 + *
+ *   文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 
+ * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +public class SubscriptionScheduleRequest implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:商户订单号
+   * 变量名:out_trade_no
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
+   *  示例值:1217752501201407033233368018
+   * 
+ */ + @SerializedName("out_trade_no") + private String outTradeNo; + + /** + *
+   * 字段名:用户标识
+   * 变量名:openid
+   * 是否必填:是
+   * 类型:string(128)
+   * 描述:
+   *  用户在直连商户appid下的唯一标识
+   *  示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
+   * 
+ */ + @SerializedName("openid") + private String openid; + + /** + *
+   * 字段名:订单描述
+   * 变量名:description
+   * 是否必填:是
+   * 类型:string(127)
+   * 描述:
+   *  订单描述
+   *  示例值:腾讯充值中心-QQ会员充值
+   * 
+ */ + @SerializedName("description") + private String description; + + /** + *
+   * 字段名:预约扣费金额
+   * 变量名:amount
+   * 是否必填:是
+   * 类型:object
+   * 描述:
+   *  预约扣费金额信息
+   * 
+ */ + @SerializedName("amount") + private SubscriptionAmount amount; + + /** + *
+   * 字段名:预约扣费时间
+   * 变量名:schedule_time
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  预约扣费的时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
+   *  示例值:2018-06-08T10:34:56+08:00
+   * 
+ */ + @SerializedName("schedule_time") + private String scheduleTime; + + /** + *
+   * 字段名:扣费计划
+   * 变量名:billing_plan
+   * 是否必填:否
+   * 类型:object
+   * 描述:
+   *  扣费计划信息,用于连续包月等场景
+   * 
+ */ + @SerializedName("billing_plan") + private BillingPlan billingPlan; + + /** + *
+   * 字段名:通知地址
+   * 变量名:notify_url
+   * 是否必填:否
+   * 类型:string(256)
+   * 描述:
+   *  异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数
+   *  示例值:https://www.weixin.qq.com/wxpay/pay.php
+   * 
+ */ + @SerializedName("notify_url") + private String notifyUrl; + + /** + *
+   * 字段名:附加数据
+   * 变量名:attach
+   * 是否必填:否
+   * 类型:string(128)
+   * 描述:
+   *  附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
+   *  示例值:自定义数据
+   * 
+ */ + @SerializedName("attach") + private String attach; +} \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionScheduleResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionScheduleResult.java new file mode 100644 index 0000000000..fc0f9f6615 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionScheduleResult.java @@ -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; + +/** + * 预约扣费响应结果 + *
+ *   文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 
+ * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +public class SubscriptionScheduleResult implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:预约扣费ID
+   * 变量名:subscription_id
+   * 是否必填:是
+   * 类型:string(64)
+   * 描述:
+   *  微信支付预约扣费ID
+   *  示例值:1217752501201407033233368018
+   * 
+ */ + @SerializedName("subscription_id") + private String subscriptionId; + + /** + *
+   * 字段名:商户订单号
+   * 变量名:out_trade_no
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  商户系统内部订单号
+   *  示例值:1217752501201407033233368018
+   * 
+ */ + @SerializedName("out_trade_no") + private String outTradeNo; + + /** + *
+   * 字段名:预约状态
+   * 变量名:status
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  预约状态
+   *  SCHEDULED:已预约
+   *  CANCELLED:已取消
+   *  EXECUTED:已执行
+   *  FAILED:执行失败
+   *  示例值:SCHEDULED
+   * 
+ */ + @SerializedName("status") + private String status; + + /** + *
+   * 字段名:预约扣费时间
+   * 变量名:schedule_time
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  预约扣费的时间,遵循rfc3339标准格式
+   *  示例值:2018-06-08T10:34:56+08:00
+   * 
+ */ + @SerializedName("schedule_time") + private String scheduleTime; + + /** + *
+   * 字段名:创建时间
+   * 变量名:create_time
+   * 是否必填:是
+   * 类型:string(32)
+   * 描述:
+   *  预约创建时间,遵循rfc3339标准格式
+   *  示例值:2018-06-08T10:34:56+08:00
+   * 
+ */ + @SerializedName("create_time") + private String createTime; + + /** + *
+   * 字段名:预约扣费金额
+   * 变量名:amount
+   * 是否必填:是
+   * 类型:object
+   * 描述:
+   *  预约扣费金额信息
+   * 
+ */ + @SerializedName("amount") + private SubscriptionAmount amount; + + /** + *
+   * 字段名:扣费计划
+   * 变量名:billing_plan
+   * 是否必填:否
+   * 类型:object
+   * 描述:
+   *  扣费计划信息
+   * 
+ */ + @SerializedName("billing_plan") + private BillingPlan billingPlan; +} \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionTransactionQueryRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionTransactionQueryRequest.java new file mode 100644 index 0000000000..17b3681cea --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionTransactionQueryRequest.java @@ -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; + +/** + * 查询扣费记录请求参数 + *
+ *   文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 
+ * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +public class SubscriptionTransactionQueryRequest implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:用户标识
+   * 变量名:openid
+   * 是否必填:否
+   * 类型:string(128)
+   * 描述:
+   *  用户在直连商户appid下的唯一标识
+   *  示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
+   * 
+ */ + @SerializedName("openid") + private String openid; + + /** + *
+   * 字段名:开始时间
+   * 变量名:begin_time
+   * 是否必填:否
+   * 类型:string(32)
+   * 描述:
+   *  查询开始时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
+   *  示例值:2018-06-08T10:34:56+08:00
+   * 
+ */ + @SerializedName("begin_time") + private String beginTime; + + /** + *
+   * 字段名:结束时间
+   * 变量名:end_time
+   * 是否必填:否
+   * 类型:string(32)
+   * 描述:
+   *  查询结束时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
+   *  示例值:2018-06-08T10:34:56+08:00
+   * 
+ */ + @SerializedName("end_time") + private String endTime; + + /** + *
+   * 字段名:分页大小
+   * 变量名:limit
+   * 是否必填:否
+   * 类型:int
+   * 描述:
+   *  分页大小,不超过50
+   *  示例值:20
+   * 
+ */ + @SerializedName("limit") + private Integer limit; + + /** + *
+   * 字段名:分页偏移量
+   * 变量名:offset
+   * 是否必填:否
+   * 类型:int
+   * 描述:
+   *  分页偏移量
+   *  示例值:0
+   * 
+ */ + @SerializedName("offset") + private Integer offset; +} \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionTransactionQueryResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionTransactionQueryResult.java new file mode 100644 index 0000000000..75fff11a22 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionTransactionQueryResult.java @@ -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; + +/** + * 查询扣费记录响应结果 + *
+ *   文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 
+ * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +public class SubscriptionTransactionQueryResult implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:总数量
+   * 变量名:total_count
+   * 是否必填:是
+   * 类型:int
+   * 描述:
+   *  符合条件的记录总数量
+   *  示例值:100
+   * 
+ */ + @SerializedName("total_count") + private Integer totalCount; + + /** + *
+   * 字段名:扣费记录列表
+   * 变量名:data
+   * 是否必填:是
+   * 类型:array
+   * 描述:
+   *  扣费记录列表
+   * 
+ */ + @SerializedName("data") + private List data; + + /** + * 扣费记录 + */ + @Data + @NoArgsConstructor + public static class SubscriptionTransaction implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+     * 字段名:微信支付订单号
+     * 变量名:transaction_id
+     * 是否必填:是
+     * 类型:string(32)
+     * 描述:
+     *  微信支付系统生成的订单号
+     *  示例值:1217752501201407033233368018
+     * 
+ */ + @SerializedName("transaction_id") + private String transactionId; + + /** + *
+     * 字段名:商户订单号
+     * 变量名:out_trade_no
+     * 是否必填:是
+     * 类型:string(32)
+     * 描述:
+     *  商户系统内部订单号
+     *  示例值:1217752501201407033233368018
+     * 
+ */ + @SerializedName("out_trade_no") + private String outTradeNo; + + /** + *
+     * 字段名:预约扣费ID
+     * 变量名:subscription_id
+     * 是否必填:否
+     * 类型:string(64)
+     * 描述:
+     *  微信支付预约扣费ID,预约扣费产生的交易才有此字段
+     *  示例值:1217752501201407033233368018
+     * 
+ */ + @SerializedName("subscription_id") + private String subscriptionId; + + /** + *
+     * 字段名:交易状态
+     * 变量名:trade_state
+     * 是否必填:是
+     * 类型:string(32)
+     * 描述:
+     *  交易状态
+     *  SUCCESS:支付成功
+     *  REFUND:转入退款
+     *  NOTPAY:未支付
+     *  CLOSED:已关闭
+     *  REVOKED:已撤销(刷卡支付)
+     *  USERPAYING:用户支付中
+     *  PAYERROR:支付失败
+     *  示例值:SUCCESS
+     * 
+ */ + @SerializedName("trade_state") + private String tradeState; + + /** + *
+     * 字段名:支付完成时间
+     * 变量名:success_time
+     * 是否必填:否
+     * 类型:string(32)
+     * 描述:
+     *  支付完成时间,遵循rfc3339标准格式
+     *  示例值:2018-06-08T10:34:56+08:00
+     * 
+ */ + @SerializedName("success_time") + private String successTime; + + /** + *
+     * 字段名:扣费金额
+     * 变量名:amount
+     * 是否必填:是
+     * 类型:object
+     * 描述:
+     *  扣费金额信息
+     * 
+ */ + @SerializedName("amount") + private SubscriptionAmount amount; + + /** + *
+     * 字段名:用户标识
+     * 变量名:openid
+     * 是否必填:是
+     * 类型:string(128)
+     * 描述:
+     *  用户在直连商户appid下的唯一标识
+     *  示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
+     * 
+ */ + @SerializedName("openid") + private String openid; + + /** + *
+     * 字段名:订单描述
+     * 变量名:description
+     * 是否必填:是
+     * 类型:string(127)
+     * 描述:
+     *  订单描述
+     *  示例值:腾讯充值中心-QQ会员充值
+     * 
+ */ + @SerializedName("description") + private String description; + + /** + *
+     * 字段名:附加数据
+     * 变量名:attach
+     * 是否必填:否
+     * 类型:string(128)
+     * 描述:
+     *  附加数据
+     *  示例值:自定义数据
+     * 
+ */ + @SerializedName("attach") + private String attach; + } +} \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/SubscriptionBillingService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/SubscriptionBillingService.java new file mode 100644 index 0000000000..e662e4dd4f --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/SubscriptionBillingService.java @@ -0,0 +1,105 @@ +package com.github.binarywang.wxpay.service; + +import com.github.binarywang.wxpay.bean.subscriptionbilling.*; +import com.github.binarywang.wxpay.exception.WxPayException; + +/** + * 微信支付-预约扣费服务 (连续包月功能) + *
+ *   微信支付预约扣费功能,支持商户在用户授权的情况下,
+ *   按照约定的时间和金额,自动从用户的支付账户中扣取费用。
+ *   主要用于连续包月、订阅服务等场景。
+ *   
+ *   文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 
+ * + * @author Binary Wang + * created on 2024-08-31 + */ +public interface SubscriptionBillingService { + + /** + * 预约扣费 + *
+   *   商户可以通过该接口预约未来某个时间点的扣费。
+   *   适用于连续包月、订阅服务等场景。
+   *   
+   *   文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+   *   请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/schedule
+   *   请求方式: POST
+   *   是否需要证书: 是
+   * 
+ * + * @param request 预约扣费请求参数 + * @return 预约扣费结果 + * @throws WxPayException 微信支付异常 + */ + SubscriptionScheduleResult scheduleSubscription(SubscriptionScheduleRequest request) throws WxPayException; + + /** + * 查询预约扣费 + *
+   *   商户可以通过该接口查询已预约的扣费信息。
+   *   
+   *   文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+   *   请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/schedule/{subscription_id}
+   *   请求方式: GET
+   * 
+ * + * @param subscriptionId 预约扣费ID + * @return 预约扣费查询结果 + * @throws WxPayException 微信支付异常 + */ + SubscriptionQueryResult querySubscription(String subscriptionId) throws WxPayException; + + /** + * 取消预约扣费 + *
+   *   商户可以通过该接口取消已预约的扣费。
+   *   
+   *   文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+   *   请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/schedule/{subscription_id}/cancel
+   *   请求方式: POST
+   *   是否需要证书: 是
+   * 
+ * + * @param request 取消预约扣费请求参数 + * @return 取消预约扣费结果 + * @throws WxPayException 微信支付异常 + */ + SubscriptionCancelResult cancelSubscription(SubscriptionCancelRequest request) throws WxPayException; + + /** + * 立即扣费 + *
+   *   商户可以通过该接口立即执行扣费操作。
+   *   通常用于补扣失败的费用或者特殊情况下的即时扣费。
+   *   
+   *   文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+   *   请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/instant-billing
+   *   请求方式: POST
+   *   是否需要证书: 是
+   * 
+ * + * @param request 立即扣费请求参数 + * @return 立即扣费结果 + * @throws WxPayException 微信支付异常 + */ + SubscriptionInstantBillingResult instantBilling(SubscriptionInstantBillingRequest request) throws WxPayException; + + /** + * 查询扣费记录 + *
+   *   商户可以通过该接口查询扣费记录。
+   *   
+   *   文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+   *   请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/transactions
+   *   请求方式: GET
+   * 
+ * + * @param request 查询扣费记录请求参数 + * @return 扣费记录查询结果 + * @throws WxPayException 微信支付异常 + */ + SubscriptionTransactionQueryResult queryTransactions(SubscriptionTransactionQueryRequest request) throws WxPayException; +} \ 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 8ceac2b6ba..828aff0b7f 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 @@ -316,6 +316,13 @@ public interface WxPayService { */ BrandMerchantTransferService getBrandMerchantTransferService(); + /** + * 获取微信支付预约扣费服务类 (连续包月功能) + * + * @return the subscription billing service + */ + SubscriptionBillingService getSubscriptionBillingService(); + /** * 设置企业付款服务类,允许开发者自定义实现类. * 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 5057ef2b6b..b02abfa90d 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 @@ -128,6 +128,9 @@ public abstract class BaseWxPayServiceImpl implements WxPayService { @Getter private final BrandMerchantTransferService brandMerchantTransferService = new BrandMerchantTransferServiceImpl(this); + @Getter + private final SubscriptionBillingService subscriptionBillingService = new SubscriptionBillingServiceImpl(this); + protected Map configMap = new ConcurrentHashMap<>(); @Override diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/SubscriptionBillingServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/SubscriptionBillingServiceImpl.java new file mode 100644 index 0000000000..45c1a9f0d2 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/SubscriptionBillingServiceImpl.java @@ -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; + +/** + * 微信支付-预约扣费服务实现 (连续包月功能) + *
+ *   微信支付预约扣费功能,支持商户在用户授权的情况下,
+ *   按照约定的时间和金额,自动从用户的支付账户中扣取费用。
+ *   主要用于连续包月、订阅服务等场景。
+ *   
+ *   文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 
+ * + * @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); + } +} \ No newline at end of file diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/SubscriptionBillingServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/SubscriptionBillingServiceImplTest.java new file mode 100644 index 0000000000..21143a47d1 --- /dev/null +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/SubscriptionBillingServiceImplTest.java @@ -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; + +/** + * 微信支付预约扣费服务测试类 + *

+ * 注意:由于预约扣费功能需要用户授权和实际的签约关系, + * 这些测试主要用于验证接口调用的正确性,而不是功能的完整性。 + * 实际测试需要在具有有效签约关系的环境中进行。 + *

+ * + * @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()); + } + } +} \ No newline at end of file From 9e6ad5cf95ce177225192dbba4ee2751ccd8edf2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 31 Aug 2025 15:04:10 +0000 Subject: [PATCH 3/3] Add comprehensive usage documentation for subscription billing functionality Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- weixin-java-pay/SUBSCRIPTION_BILLING_USAGE.md | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 weixin-java-pay/SUBSCRIPTION_BILLING_USAGE.md diff --git a/weixin-java-pay/SUBSCRIPTION_BILLING_USAGE.md b/weixin-java-pay/SUBSCRIPTION_BILLING_USAGE.md new file mode 100644 index 0000000000..acc8f269b8 --- /dev/null +++ b/weixin-java-pay/SUBSCRIPTION_BILLING_USAGE.md @@ -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); + } + } +} +``` \ No newline at end of file