Преглед изворни кода

:art: #3056 【微信支付】优化支付/退款结果解析,增加对V3版本服务商的下单/退款支持

Pursuer丶 пре 1 година
родитељ
комит
21a95e15df

+ 30 - 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayBaseNotifyV3Result.java

@@ -0,0 +1,30 @@
+package com.github.binarywang.wxpay.bean.notify;
+
+/**
+ * 微信支付公用回调
+ *
+ * @author Pursuer
+ * @version 1.0
+ * @date 2023/6/15
+ */
+public interface WxPayBaseNotifyV3Result<T> {
+  /**
+   * 设置原始数据
+   *
+   * @param rawData 原始数据
+   * @author Pursuer
+   * @date 2023/6/15
+   * @since 1.0
+   **/
+  void setRawData(OriginNotifyResponse rawData);
+
+  /**
+   * 解密后的数据
+   *
+   * @param data 解密后的数据
+   * @author Pursuer
+   * @date 2023/6/15
+   * @since 1.0
+   **/
+  void setResult(T data);
+}

+ 1 - 1
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyV3Result.java

@@ -15,7 +15,7 @@ import java.util.List;
  */
 @Data
 @NoArgsConstructor
-public class WxPayOrderNotifyV3Result implements Serializable {
+public class WxPayNotifyV3Result implements Serializable, WxPayBaseNotifyV3Result<WxPayNotifyV3Result.DecryptNotifyResult> {
   private static final long serialVersionUID = -1L;
   /**
    * 源数据

+ 568 - 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayPartnerNotifyV3Result.java

@@ -0,0 +1,568 @@
+package com.github.binarywang.wxpay.bean.notify;
+
+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/wiki/doc/apiv3_partner/apis/chapter4_1_5.shtml
+ *
+ * @author Guo Shuai
+ * @version 1.0
+ * @date 2023/3/2
+ */
+@Data
+@NoArgsConstructor
+public class WxPayPartnerNotifyV3Result implements Serializable, WxPayBaseNotifyV3Result<WxPayPartnerNotifyV3Result.DecryptNotifyResult> {
+  private static final long serialVersionUID = -1L;
+  /**
+   * 源数据
+   */
+  private OriginNotifyResponse rawData;
+  /**
+   * 解密后的数据
+   */
+  private DecryptNotifyResult result;
+
+  @Data
+  @NoArgsConstructor
+  public static class DecryptNotifyResult implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /**
+     * <pre>
+     * 字段名:服务商应用ID
+     * 变量名:spAppid
+     * 是否必填:是
+     * 类型:string[1,32]
+     * 描述:
+     *  由微信生成的应用ID,全局唯一。请求统一下单接口时请注意APPID的应用属性,例如公众号场景下,需使用应用属性为公众号的APPID
+     *  示例值:wxd678efh567hg6787
+     * </pre>
+     */
+    @SerializedName(value = "sp_appid")
+    protected String spAppid;
+    /**
+     * <pre>
+     * 字段名:服务商商户号
+     * 变量名:spMchid
+     * 是否必填:是
+     * 类型:string[1,32]
+     * 描述:
+     *  服务商商户号,由微信支付生成并下发。
+     *  示例值:1230000109
+     * </pre>
+     */
+    @SerializedName(value = "sp_mchid")
+    protected String spMchid;
+    /**
+     * <pre>
+     * 字段名:子商户应用ID
+     * 变量名:subAppid
+     * 是否必填:否
+     * 类型:string[1,32]
+     * 描述:
+     *  由微信生成的应用ID,全局唯一。请求统一下单接口时请注意APPID的应用属性,例如公众号场景下,需使用应用属性为公众号的APPID
+     *  示例值:wxd678efh567hg6787
+     * </pre>
+     */
+    @SerializedName(value = "sub_appid")
+    protected String subAppid;
+    /**
+     * <pre>
+     * 字段名:子商户商户号
+     * 变量名:subMchid
+     * 是否必填:是
+     * 类型:string[1,32]
+     * 描述:
+     *  子商户商户号,由微信支付生成并下发。
+     *  示例值:1230000109
+     * </pre>
+     */
+    @SerializedName(value = "sub_mchid")
+    protected String subMchid;
+    /**
+     * <pre>
+     * 字段名:商户订单号
+     * 变量名:out_trade_no
+     * 是否必填:是
+     * 类型:string[6,32]
+     * 描述:
+     *  商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一。
+     *  特殊规则:最小字符长度为6
+     *  示例值:1217752501201407033233368018
+     * </pre>
+     */
+    @SerializedName(value = "out_trade_no")
+    private String outTradeNo;
+    /**
+     * <pre>
+     * 字段名:微信支付订单号
+     * 变量名:transaction_id
+     * 是否必填:是
+     * 类型:string[1,32]
+     * 描述:
+     *  微信支付系统生成的订单号。
+     *  示例值:1217752501201407033233368018
+     * </pre>
+     */
+    @SerializedName(value = "transaction_id")
+    private String transactionId;
+    /**
+     * <pre>
+     * 字段名:交易类型
+     * 变量名:trade_type
+     * 是否必填:是
+     * 类型:string[1,16]
+     * 描述:
+     *  交易类型,枚举值:
+     *  JSAPI:公众号支付
+     *  NATIVE:扫码支付
+     *  APP:APP支付
+     *  MICROPAY:付款码支付
+     *  MWEB:H5支付
+     *  FACEPAY:刷脸支付
+     *  示例值:MICROPAY
+     * </pre>
+     */
+    @SerializedName(value = "trade_type")
+    private String tradeType;
+    /**
+     * <pre>
+     * 字段名:交易状态
+     * 变量名:trade_state
+     * 是否必填:是
+     * 类型:string[1,32]
+     * 描述:
+     *  交易状态,枚举值:
+     *  SUCCESS:支付成功
+     *  REFUND:转入退款
+     *  NOTPAY:未支付
+     *  CLOSED:已关闭
+     *  REVOKED:已撤销(付款码支付)
+     *  USERPAYING:用户支付中(付款码支付)
+     *  PAYERROR:支付失败(其他原因,如银行返回失败)
+     *  示例值:SUCCESS
+     * </pre>
+     */
+    @SerializedName(value = "trade_state")
+    private String tradeState;
+    /**
+     * <pre>
+     * 字段名:交易状态描述
+     * 变量名:trade_state_desc
+     * 是否必填:是
+     * 类型:string[1,256]
+     * 描述:
+     *  交易状态描述
+     *  示例值:支付成功
+     * </pre>
+     */
+    @SerializedName(value = "trade_state_desc")
+    private String tradeStateDesc;
+    /**
+     * <pre>
+     * 字段名:付款银行
+     * 变量名:bank_type
+     * 是否必填:是
+     * 类型:string[1,16]
+     * 描述:
+     *  银行类型,采用字符串类型的银行标识。银行标识请参考《银行类型对照表》https://pay.weixin.qq.com/wiki/doc/apiv3/terms_definition/chapter1_1_3.shtml#part-6
+     *  示例值:CMC
+     * </pre>
+     */
+    @SerializedName(value = "bank_type")
+    private String bankType;
+    /**
+     * <pre>
+     * 字段名:附加数据
+     * 变量名:attach
+     * 是否必填:否
+     * 类型:string[1,128]
+     * 描述:
+     *  附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
+     *  示例值:自定义数据
+     * </pre>
+     */
+    @SerializedName(value = "attach")
+    private String attach;
+    /**
+     * <pre>
+     * 字段名:支付完成时间
+     * 变量名:success_time
+     * 是否必填:是
+     * 类型:string[1,64]
+     * 描述:
+     *  支付完成时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。
+     *  示例值:2018-06-08T10:34:56+08:00
+     * </pre>
+     */
+    @SerializedName(value = "success_time")
+    private String successTime;
+    /**
+     * <pre>
+     * 字段名:支付者
+     * 变量名:payer
+     * 是否必填:是
+     * 类型:object
+     * 描述:
+     *  支付者信息
+     * </pre>
+     */
+    private Payer payer;
+    /**
+     * <pre>
+     * 字段名:订单金额
+     * 变量名:amount
+     * 是否必填:否
+     * 类型:object
+     * 描述:
+     *  订单金额信息
+     * </pre>
+     */
+    @SerializedName(value = "amount")
+    private Amount amount;
+    /**
+     * <pre>
+     * 字段名:场景信息
+     * 变量名:scene_info
+     * 是否必填:否
+     * 类型:object
+     * 描述:
+     *  支付场景信息描述
+     * </pre>
+     */
+    @SerializedName(value = "scene_info")
+    private SceneInfo sceneInfo;
+    /**
+     * <pre>
+     * 字段名:优惠功能
+     * 变量名:promotion_detail
+     * 是否必填:否
+     * 类型:array
+     * 描述:
+     *  优惠功能,享受优惠时返回该字段。
+     * </pre>
+     */
+    @SerializedName(value = "promotion_detail")
+    private List<PromotionDetail> promotionDetails;
+  }
+  @Data
+  @NoArgsConstructor
+  public static class Payer implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /**
+     * <pre>
+     * 字段名:用户标识
+     * 变量名:openid
+     * 是否必填:是
+     * 类型:string[1,128]
+     * 描述:
+     *  用户在直连商户appid下的唯一标识。
+     *  示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
+     * </pre>
+     */
+    @SerializedName(value = "openid")
+    private String openid;
+  }
+
+  @Data
+  @NoArgsConstructor
+  public static class Amount implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /**
+     * <pre>
+     * 字段名:总金额
+     * 变量名:total
+     * 是否必填:否
+     * 类型:int
+     * 描述:
+     *  订单总金额,单位为分。
+     *  示例值:100
+     * </pre>
+     */
+    @SerializedName(value = "total")
+    private Integer total;
+    /**
+     * <pre>
+     * 字段名:用户支付金额
+     * 变量名:payer_total
+     * 是否必填:否
+     * 类型:int
+     * 描述:
+     *  用户支付金额,单位为分。
+     *  示例值:100
+     * </pre>
+     */
+    @SerializedName(value = "payer_total")
+    private Integer payerTotal;
+    /**
+     * <pre>
+     * 字段名:货币类型
+     * 变量名:currency
+     * 是否必填:否
+     * 类型:string[1,16]
+     * 描述:
+     *  CNY:人民币,境内商户号仅支持人民币。
+     *  示例值:CNY
+     * </pre>
+     */
+    @SerializedName(value = "currency")
+    private String currency;
+    /**
+     * <pre>
+     * 字段名:用户支付币种
+     * 变量名:payer_currency
+     * 是否必填:否
+     * 类型:string[1,16]
+     * 描述:
+     *  用户支付币种
+     *  示例值: CNY
+     * </pre>
+     */
+    @SerializedName(value = "payer_currency")
+    private String payerCurrency;
+  }
+
+  @Data
+  @NoArgsConstructor
+  public static class SceneInfo implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /**
+     * <pre>
+     * 字段名:商户端设备号
+     * 变量名:device_id
+     * 是否必填:否
+     * 类型:string[1,32]
+     * 描述:
+     *  终端设备号(门店号或收银设备ID)。
+     *  示例值:013467007045764
+     * </pre>
+     */
+    @SerializedName(value = "device_id")
+    private String deviceId;
+  }
+
+  @Data
+  @NoArgsConstructor
+  public static class PromotionDetail implements Serializable {
+    /**
+     * <pre>
+     * 字段名:券ID
+     * 变量名:coupon_id
+     * 是否必填:是
+     * 类型:string[1,32]
+     * 描述:
+     *  券ID
+     *  示例值:109519
+     * </pre>
+     */
+    @SerializedName(value = "coupon_id")
+    private String couponId;
+    /**
+     * <pre>
+     * 字段名:优惠名称
+     * 变量名:name
+     * 是否必填:否
+     * 类型:string[1,64]
+     * 描述:
+     *  优惠名称
+     *  示例值:单品惠-6
+     * </pre>
+     */
+    @SerializedName(value = "name")
+    private String name;
+    /**
+     * <pre>
+     * 字段名:优惠范围
+     * 变量名:scope
+     * 是否必填:否
+     * 类型:string[1,32]
+     * 描述:
+     *  GLOBAL:全场代金券
+     *  SINGLE:单品优惠
+     *  示例值:GLOBAL
+     * </pre>
+     */
+    @SerializedName(value = "scope")
+    private String scope;
+    /**
+     * <pre>
+     * 字段名:优惠类型
+     * 变量名:type
+     * 是否必填:否
+     * 类型:string[1,32]
+     * 描述:
+     *  CASH:充值
+     *  NOCASH:预充值
+     *  示例值:CASH
+     * </pre>
+     */
+    @SerializedName(value = "type")
+    private String type;
+    /**
+     * <pre>
+     * 字段名:优惠券面额
+     * 变量名:amount
+     * 是否必填:是
+     * 类型:int
+     * 描述:
+     *  优惠券面额
+     *  示例值:100
+     * </pre>
+     */
+    @SerializedName(value = "amount")
+    private Integer amount;
+    /**
+     * <pre>
+     * 字段名:活动ID
+     * 变量名:stock_id
+     * 是否必填:否
+     * 类型:string[1,32]
+     * 描述:
+     *  活动ID
+     *  示例值:931386
+     * </pre>
+     */
+    @SerializedName(value = "stock_id")
+    private String stockId;
+    /**
+     * <pre>
+     * 字段名:微信出资
+     * 变量名:wechatpay_contribute
+     * 是否必填:否
+     * 类型:int
+     * 描述:
+     *  微信出资,单位为分
+     *  示例值:0
+     * </pre>
+     */
+    @SerializedName(value = "wechatpay_contribute")
+    private Integer wechatpayContribute;
+    /**
+     * <pre>
+     * 字段名:商户出资
+     * 变量名:merchant_contribute
+     * 是否必填:否
+     * 类型:int
+     * 描述:
+     *  商户出资,单位为分
+     *  示例值:0
+     * </pre>
+     */
+    @SerializedName(value = "merchant_contribute")
+    private Integer merchantContribute;
+    /**
+     * <pre>
+     * 字段名:其他出资
+     * 变量名:other_contribute
+     * 是否必填:否
+     * 类型:int
+     * 描述:
+     *  其他出资,单位为分
+     *  示例值:0
+     * </pre>
+     */
+    @SerializedName(value = "other_contribute")
+    private Integer otherContribute;
+    /**
+     * <pre>
+     * 字段名:优惠币种
+     * 变量名:currency
+     * 是否必填:否
+     * 类型:string[1,16]
+     * 描述:
+     *  CNY:人民币,境内商户号仅支持人民币。
+     *  示例值:CNY
+     * </pre>
+     */
+    @SerializedName(value = "currency")
+    private String currency;
+    /**
+     * <pre>
+     * 字段名:单品列表
+     * 变量名:goods_detail
+     * 是否必填:否
+     * 类型:array
+     * 描述:
+     *  单品列表信息
+     * </pre>
+     */
+    @SerializedName(value = "goods_detail")
+    private List<GoodsDetail> goodsDetails;
+  }
+
+  @Data
+  @NoArgsConstructor
+  public static class GoodsDetail implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /**
+     * <pre>
+     * 字段名:商品编码
+     * 变量名:goods_id
+     * 是否必填:是
+     * 类型:string[1,32]
+     * 描述:
+     *  商品编码
+     *  示例值:M1006
+     * </pre>
+     */
+    @SerializedName(value = "goods_id")
+    private String goodsId;
+    /**
+     * <pre>
+     * 字段名:商品数量
+     * 变量名:quantity
+     * 是否必填:是
+     * 类型:int
+     * 描述:
+     *  用户购买的数量
+     *  示例值:1
+     * </pre>
+     */
+    @SerializedName(value = "quantity")
+    private Integer quantity;
+    /**
+     * <pre>
+     * 字段名:商品单价
+     * 变量名:unit_price
+     * 是否必填:是
+     * 类型:int
+     * 描述:
+     *  商品单价,单位为分
+     *  示例值:100
+     * </pre>
+     */
+    @SerializedName(value = "unit_price")
+    private Integer unitPrice;
+    /**
+     * <pre>
+     * 字段名:商品优惠金额
+     * 变量名:discount_amount
+     * 是否必填:是
+     * 类型:int
+     * 描述:
+     *  商品优惠金额
+     *  示例值:0
+     * </pre>
+     */
+    @SerializedName(value = "discount_amount")
+    private Integer discountAmount;
+    /**
+     * <pre>
+     * 字段名:商品备注
+     * 变量名:goods_remark
+     * 是否必填:否
+     * 类型:string[1,128]
+     * 描述:
+     *  商品备注信息
+     *  示例值:商品备注信息
+     * </pre>
+     */
+    @SerializedName(value = "goods_remark")
+    private String goodsRemark;
+  }
+}

+ 230 - 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayPartnerRefundNotifyV3Result.java

@@ -0,0 +1,230 @@
+package com.github.binarywang.wxpay.bean.notify;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 微信支付服务商退款回调
+ * 文档见:https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_1_11.shtml
+ *
+ * @author Guo Shuai
+ * @version 1.0
+ * @date 2023/3/2
+ */
+@Data
+@NoArgsConstructor
+public class WxPayPartnerRefundNotifyV3Result implements Serializable, WxPayBaseNotifyV3Result<WxPayPartnerRefundNotifyV3Result.DecryptNotifyResult> {
+  private static final long serialVersionUID = -1L;
+  /**
+   * 源数据
+   */
+  private OriginNotifyResponse rawData;
+  /**
+   * 解密后的数据
+   */
+  private DecryptNotifyResult result;
+
+  @Data
+  @NoArgsConstructor
+  public static class DecryptNotifyResult implements Serializable {
+    private static final long serialVersionUID = -1L;
+    /**
+     * <pre>
+     * 字段名:服务商的商户号
+     * 变量名:sub_mchid
+     * 是否必填:是
+     * 类型:string[1, 32]
+     * 描述:
+     *  服务商的商户号,由微信支付生成并下发。
+     *  示例值:1230000109
+     * </pre>
+     */
+    @SerializedName(value = "sp_mchid")
+    private String spMchId;
+    /**
+     * <pre>
+     * 字段名:子商户的商户号
+     * 变量名:sub_mchid
+     * 是否必填:是
+     * 类型:string[1, 32]
+     * 描述:
+     *  子商户商户号,由微信支付生成并下发。
+     *  示例值:1230000109
+     * </pre>
+     */
+    @SerializedName(value = "sub_mchid")
+    private String subMchId;
+    /**
+     * <pre>
+     * 字段名:商户订单号
+     * 变量名:out_trade_no
+     * 是否必填:是
+     * 类型:string[1,32]
+     * 描述:
+     *  返回的商户订单号
+     *  示例值: 1217752501201407033233368018
+     * </pre>
+     */
+    @SerializedName(value = "out_trade_no")
+    private String outTradeNo;
+    /**
+     * <pre>
+     * 字段名:微信支付订单号
+     * 变量名:transaction_id
+     * 是否必填:是
+     * 类型:string[1,32]
+     * 描述:
+     *  微信支付订单号
+     *  示例值: 1217752501201407033233368018
+     * </pre>
+     */
+    @SerializedName(value = "transaction_id")
+    private String transactionId;
+    /**
+     * <pre>
+     * 字段名:商户退款单号
+     * 变量名:out_refund_no
+     * 是否必填:是
+     * 类型:string[1,64]
+     * 描述:
+     *  商户退款单号
+     *  示例值: 1217752501201407033233368018
+     * </pre>
+     */
+    @SerializedName(value = "out_refund_no")
+    private String outRefundNo;
+    /**
+     * <pre>
+     * 字段名:微信支付退款号
+     * 变量名:refund_id
+     * 是否必填:是
+     * 类型:string[1,32]
+     * 描述:
+     *  微信退款单号
+     *  示例值: 1217752501201407033233368018
+     * </pre>
+     */
+    @SerializedName(value = "refund_id")
+    private String refundId;
+    /**
+     * <pre>
+     * 字段名:退款状态
+     * 变量名:refund_status
+     * 是否必填:是
+     * 类型:string[1,16]
+     * 描述:
+     *  退款状态,枚举值:
+     *  SUCCESS:退款成功
+     *  CLOSE:退款关闭
+     *  ABNORMAL:退款异常,退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往【商户平台—>交易中心】,手动处理此笔退款
+     *  示例值:SUCCESS
+     * </pre>
+     */
+    @SerializedName(value = "refund_status")
+    private String refundStatus;
+    /**
+     * <pre>
+     * 字段名:退款成功时间
+     * 变量名:success_time
+     * 是否必填:否
+     * 类型:string[1,64]
+     * 描述:
+     *  1、退款成功时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日13点29分35秒。
+     *  2、当退款状态为退款成功时返回此参数。
+     *  示例值:2018-06-08T10:34:56+08:00
+     * </pre>
+     */
+    @SerializedName(value = "success_time")
+    private String successTime;
+    /**
+     * <pre>
+     * 字段名:退款入账账户
+     * 变量名:user_received_account
+     * 是否必填:是
+     * 类型:string[1,64]
+     * 描述:
+     *  取当前退款单的退款入账方。
+     *  1、退回银行卡:{银行名称}{卡类型}{卡尾号}
+     *  2、退回支付用户零钱: 支付用户零钱
+     *  3、退还商户: 商户基本账户、商户结算银行账户
+     *  4、退回支付用户零钱通:支付用户零钱通
+     *  示例值:招商银行信用卡0403
+     * </pre>
+     */
+    @SerializedName(value = "user_received_account")
+    private String userReceivedAccount;
+    /**
+     * <pre>
+     * 字段名:金额信息
+     * 变量名:amount
+     * 是否必填:是
+     * 类型:object
+     * 描述:
+     *  金额信息
+     * </pre>
+     */
+    @SerializedName(value = "amount")
+    private Amount amount;
+  }
+
+  @Data
+  @NoArgsConstructor
+  public static class Amount implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /**
+     * <pre>
+     * 字段名:订单金额
+     * 变量名:total
+     * 是否必填:是
+     * 类型:int
+     * 描述:
+     *  订单总金额,单位为分,只能为整数,详见支付金额
+     *  示例值:999
+     * </pre>
+     */
+    @SerializedName(value = "total")
+    private Integer total;
+    /**
+     * <pre>
+     * 字段名:退款金额
+     * 变量名:refund
+     * 是否必填:是
+     * 类型:int
+     * 描述:
+     *  退款金额,币种的最小单位,只能为整数,不能超过原订单支付金额,如果有使用券,后台会按比例退。
+     *  示例值:999
+     * </pre>
+     */
+    @SerializedName(value = "refund")
+    private Integer refund;
+    /**
+     * <pre>
+     * 字段名:用户支付金额
+     * 变量名:payer_total
+     * 是否必填:是
+     * 类型:int
+     * 描述:
+     *  用户实际支付金额,单位为分,只能为整数,详见支付金额
+     *  示例值:999
+     * </pre>
+     */
+    @SerializedName(value = "payer_total")
+    private Integer payerTotal;
+    /**
+     * <pre>
+     * 字段名:用户退款金额
+     * 变量名:payer_refund
+     * 是否必填:是
+     * 类型:int
+     * 描述:
+     *  退款给用户的金额,不包含所有优惠券金额
+     *  示例值:999
+     * </pre>
+     */
+    @SerializedName(value = "payer_refund")
+    private Integer payerRefund;
+  }
+}

+ 1 - 1
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyV3Result.java

@@ -14,7 +14,7 @@ import java.io.Serializable;
  */
 @Data
 @NoArgsConstructor
-public class WxPayRefundNotifyV3Result implements Serializable {
+public class WxPayRefundNotifyV3Result implements Serializable, WxPayBaseNotifyV3Result<WxPayRefundNotifyV3Result.DecryptNotifyResult> {
   private static final long serialVersionUID = -1L;
   /**
    * 源数据

+ 55 - 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3Request.java

@@ -0,0 +1,55 @@
+package com.github.binarywang.wxpay.bean.request;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 微信支付服务商退款请求
+ * 文档见:https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_1_9.shtml
+ *
+ * @author Guo Shuai
+ * @version 1.0
+ * @date 2023/3/2
+ */
+@Data
+@NoArgsConstructor
+@Accessors(chain = true)
+public class WxPayPartnerRefundV3Request extends WxPayRefundV3Request implements Serializable {
+  private static final long serialVersionUID = -1L;
+  /**
+   * <pre>
+   * 字段名:子商户的商户号
+   * 变量名:sub_mchid
+   * 是否必填:是
+   * 类型:string[1, 32]
+   * 描述:
+   *  子商户商户号,由微信支付生成并下发。
+   *  示例值:1230000109
+   * </pre>
+   */
+  @SerializedName(value = "sub_mchid")
+  private String subMchId;
+  /**
+   * <pre>
+   * 字段名:退款资金来源
+   * 变量名:funds_account
+   * 是否必填:否
+   * 类型:string[1, 32]
+   * 描述:
+   *  若传递此参数则使用对应的资金账户退款,否则默认使用未结算资金退款(仅对老资金流商户适用)
+   *  示例值:
+   *    UNSETTLED : 未结算资金
+   *    AVAILABLE : 可用余额
+   *    UNAVAILABLE : 不可用余额
+   *    OPERATION : 运营户
+   *    BASIC : 基本账户(含可用余额和不可用余额)
+   * </pre>
+   */
+  @SerializedName(value = "funds_account")
+  private String fundsAccount;
+}

+ 610 - 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerUnifiedOrderV3Request.java

@@ -0,0 +1,610 @@
+package com.github.binarywang.wxpay.bean.request;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 微信支付服务商下单请求对象
+ *
+ * @author Guo Shuai
+ * @version 1.0
+ * @date 2023/3/2
+ */
+@Data
+@NoArgsConstructor
+@Accessors(chain = true)
+public class WxPayPartnerUnifiedOrderV3Request implements Serializable {
+  private static final long serialVersionUID = 1L;
+  /**
+   * <pre>
+   * 字段名:服务商应用ID
+   * 变量名:spAppid
+   * 是否必填:是
+   * 类型:string[1,32]
+   * 描述:
+   *  由微信生成的应用ID,全局唯一。请求统一下单接口时请注意APPID的应用属性,例如公众号场景下,需使用应用属性为公众号的APPID
+   *  示例值:wxd678efh567hg6787
+   * </pre>
+   */
+  @SerializedName(value = "sp_appid")
+  protected String spAppid;
+  /**
+   * <pre>
+   * 字段名:服务商商户号
+   * 变量名:spMchid
+   * 是否必填:是
+   * 类型:string[1,32]
+   * 描述:
+   *  服务商商户号,由微信支付生成并下发。
+   *  示例值:1230000109
+   * </pre>
+   */
+  @SerializedName(value = "sp_mchid")
+  protected String spMchId;
+  /**
+   * <pre>
+   * 字段名:子商户应用ID
+   * 变量名:subAppid
+   * 是否必填:否
+   * 类型:string[1,32]
+   * 描述:
+   *  由微信生成的应用ID,全局唯一。请求统一下单接口时请注意APPID的应用属性,例如公众号场景下,需使用应用属性为公众号的APPID
+   *  示例值:wxd678efh567hg6787
+   * </pre>
+   */
+  @SerializedName(value = "sub_appid")
+  protected String subAppid;
+  /**
+   * <pre>
+   * 字段名:子商户商户号
+   * 变量名:subMchid
+   * 是否必填:是
+   * 类型:string[1,32]
+   * 描述:
+   *  子商户商户号,由微信支付生成并下发。
+   *  示例值:1230000109
+   * </pre>
+   */
+  @SerializedName(value = "sub_mchid")
+  protected String subMchId;
+  /**
+   * <pre>
+   * 字段名:商品描述
+   * 变量名:description
+   * 是否必填:是
+   * 类型:string[1,127]
+   * 描述:
+   *  商品描述
+   *  示例值:Image形象店-深圳腾大-QQ公仔
+   * </pre>
+   */
+  @SerializedName(value = "description")
+  protected String description;
+  /**
+   * <pre>
+   * 字段名:商户订单号
+   * 变量名:out_trade_no
+   * 是否必填:是
+   * 类型:string[6,32]
+   * 描述:
+   *  商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
+   *  示例值:1217752501201407033233368018
+   * </pre>
+   */
+  @SerializedName(value = "out_trade_no")
+  protected String outTradeNo;
+  /**
+   * <pre>
+   * 字段名:交易结束时间
+   * 变量名:time_expire
+   * 是否必填:是
+   * 类型:string[1,64]
+   * 描述:
+   *  订单失效时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。
+   *  示例值:2018-06-08T10:34:56+08:00
+   * </pre>
+   */
+  @SerializedName(value = "time_expire")
+  protected String timeExpire;
+  /**
+   * <pre>
+   * 字段名:附加数据
+   * 变量名:attach
+   * 是否必填:否
+   * 类型:string[1,128]
+   * 描述:
+   *  附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
+   *  示例值:自定义数据
+   * </pre>
+   */
+  @SerializedName(value = "attach")
+  protected String attach;
+  /**
+   * <pre>
+   * 字段名:通知地址
+   * 变量名:notify_url
+   * 是否必填:是
+   * 类型:string[1,256]
+   * 描述:
+   *  通知URL必须为直接可访问的URL,不允许携带查询串,要求必须为https地址。
+   *  格式:URL
+   *  示例值:https://www.weixin.qq.com/wxpay/pay.php
+   * </pre>
+   */
+  @SerializedName(value = "notify_url")
+  private String notifyUrl;
+  /**
+   * <pre>
+   * 字段名:订单优惠标记
+   * 变量名:goods_tag
+   * 是否必填:否
+   * 类型:string[1,256]
+   * 描述:
+   *  订单优惠标记
+   *  示例值:WXG
+   * </pre>
+   */
+  @SerializedName(value = "goods_tag")
+  private String goodsTag;
+  /**
+   * <pre>
+   * 字段名:电子发票入口开放标识
+   * 变量名:support_fapiao
+   * 是否必填:否
+   * 类型:boolean
+   * 描述:传入true时,支付成功消息和支付详情页将出现开票入口。需要在微信支付商户平台或微信公众平台开通电子发票功能,传此字段才可生效。
+   * </pre>
+   */
+  @SerializedName(value = "support_fapiao")
+  private Boolean supportFapiao;
+  /**
+   * <pre>
+   * 字段名:订单金额
+   * 变量名:amount
+   * 是否必填:是
+   * 类型:object
+   * 描述:
+   *  订单金额信息
+   * </pre>
+   */
+  @SerializedName(value = "amount")
+  private Amount amount;
+  /**
+   * <pre>
+   * 字段名:支付者
+   * 变量名:payer
+   * 是否必填:是
+   * 类型:object
+   * 描述:
+   *  支付者信息
+   * </pre>
+   */
+  @SerializedName(value = "payer")
+  private Payer payer;
+  /**
+   * <pre>
+   * 字段名:优惠功能
+   * 变量名:detail
+   * 是否必填:否
+   * 类型:object
+   * 描述:
+   *  优惠功能
+   * </pre>
+   */
+  @SerializedName(value = "detail")
+  private Discount detail;
+  /**
+   * <pre>
+   * 字段名:场景信息
+   * 变量名:scene_info
+   * 是否必填:否
+   * 类型:object
+   * 描述:
+   *  支付场景描述
+   * </pre>
+   */
+  @SerializedName(value = "scene_info")
+  private SceneInfo sceneInfo;
+  /**
+   * <pre>
+   * 字段名:结算信息
+   * 变量名:settle_info
+   * 是否必填:否
+   * 类型:Object
+   * 描述:结算信息
+   * </pre>
+   */
+  @SerializedName(value = "settle_info")
+  private SettleInfo settleInfo;
+
+  @Data
+  @NoArgsConstructor
+  public static class Amount implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /**
+     * <pre>
+     * 字段名:总金额
+     * 变量名:total
+     * 是否必填:是
+     * 类型:int
+     * 描述:
+     *  订单总金额,单位为分。
+     *  示例值:100
+     * </pre>
+     */
+    @SerializedName(value = "total")
+    private Integer total;
+    /**
+     * <pre>
+     * 字段名:币类型
+     * 变量名:currency
+     * 是否必填:否
+     * 类型:string[1,16]
+     * 描述:
+     *  CNY:人民币,境内商户号仅支持人民币。
+     *  示例值:CNY
+     * </pre>
+     */
+    @SerializedName(value = "currency")
+    private String currency;
+  }
+
+  @Data
+  @NoArgsConstructor
+  public static class Payer implements Serializable {
+    private static final long serialVersionUID = -1L;
+    /**
+     * <pre>
+     * spOpenid 和 subOpenid二选一 参考官网
+     * 字段名:服务商用户标识
+     * 变量名:spOpenid
+     * 是否必填:是
+     * 类型:string[1,128]
+     * 描述:
+     *  用户在服务商appid下的唯一标识。
+     *  示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
+     * </pre>
+     */
+    @SerializedName(value = "sp_openid")
+    private String spOpenid;
+    /**
+     * <pre>
+     * 字段名:子商户应用用户标识
+     * 变量名:subOpenid
+     * 是否必填:是
+     * 类型:string[1,128]
+     * 描述:
+     *  用户在子商户appid下的唯一标识。
+     *  示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
+     * </pre>
+     */
+    @SerializedName(value = "sub_openid")
+    private String subOpenid;
+  }
+
+  @Data
+  @NoArgsConstructor
+  public static class Discount implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /**
+     * <pre>
+     * 字段名:订单原价
+     * 变量名:cost_price
+     * 是否必填:否
+     * 类型:int
+     * 描述:
+     *  1、商户侧一张小票订单可能被分多次支付,订单原价用于记录整张小票的交易金额。
+     *  2、当订单原价与支付金额不相等,则不享受优惠。
+     *  3、该字段主要用于防止同一张小票分多次支付,以享受多次优惠的情况,正常支付订单不必上传此参数。
+     *  示例值:608800
+     * </pre>
+     */
+    @SerializedName(value = "cost_price")
+    private Integer costPrice;
+    /**
+     * <pre>
+     * 字段名:商品小票ID
+     * 变量名:invoice_id
+     * 是否必填:否
+     * 类型:string[1,32]
+     * 描述:
+     *  商品小票ID
+     *  示例值:微信123
+     * </pre>
+     */
+    @SerializedName(value = "invoice_id")
+    private String invoiceId;
+    /**
+     * <pre>
+     * 字段名:单品列表
+     * 变量名:goods_detail
+     * 是否必填:否
+     * 类型:array
+     * 描述:
+     *  单品列表信息
+     *  条目个数限制:【1,6000】
+     * </pre>
+     */
+    @SerializedName(value = "goods_detail")
+    private List<GoodsDetail> goodsDetails;
+  }
+
+  @Data
+  @NoArgsConstructor
+  public static class GoodsDetail implements Serializable {
+    private static final long serialVersionUID = -1L;
+    /**
+     * <pre>
+     * 字段名:商户侧商品编码
+     * 变量名:merchant_goods_id
+     * 是否必填:是
+     * 类型:string[1,32]
+     * 描述:
+     *  由半角的大小写字母、数字、中划线、下划线中的一种或几种组成。
+     *  示例值:商品编码
+     * </pre>
+     */
+    @SerializedName(value = "merchant_goods_id")
+    private String merchantGoodsId;
+    /**
+     * <pre>
+     * 字段名:微信侧商品编码
+     * 变量名:wechatpay_goods_id
+     * 是否必填:否
+     * 类型:string[1,32]
+     * 描述:
+     *  微信支付定义的统一商品编号(没有可不传)
+     *  示例值:1001
+     * </pre>
+     */
+    @SerializedName(value = "wechatpay_goods_id")
+    private String wechatpayGoodsId;
+    /**
+     * <pre>
+     * 字段名:商品名称
+     * 变量名:goods_name
+     * 是否必填:否
+     * 类型:string[1,256]
+     * 描述:
+     *  商品的实际名称
+     *  示例值:iPhoneX 256G
+     * </pre>
+     */
+    @SerializedName(value = "goods_name")
+    private String goodsName;
+    /**
+     * <pre>
+     * 字段名:商品数量
+     * 变量名:quantity
+     * 是否必填:是
+     * 类型:int
+     * 描述:
+     *  用户购买的数量
+     *  示例值:1
+     * </pre>
+     */
+    @SerializedName(value = "quantity")
+    private Integer quantity;
+    /**
+     * <pre>
+     * 字段名:商品单价
+     * 变量名:unit_price
+     * 是否必填:是
+     * 类型:int
+     * 描述:
+     *  商品单价,单位为分
+     *  示例值:828800
+     * </pre>
+     */
+    @SerializedName(value = "unit_price")
+    private Integer unitPrice;
+  }
+
+  @Data
+  @NoArgsConstructor
+  public static class SceneInfo implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /**
+     * <pre>
+     * 字段名:用户终端IP
+     * 变量名:payer_client_ip
+     * 是否必填:是
+     * 类型:string[1,45]
+     * 描述:
+     *  用户的客户端IP,支持IPv4和IPv6两种格式的IP地址。
+     *  示例值:14.23.150.211
+     * </pre>
+     */
+    @SerializedName(value = "payer_client_ip")
+    private String payerClientIp;
+    /**
+     * <pre>
+     * 字段名:商户端设备号
+     * 变量名:device_id
+     * 是否必填:否
+     * 类型:string[1,32]
+     * 描述:
+     *  商户端设备号(门店号或收银设备ID)。
+     *  示例值:013467007045764
+     * </pre>
+     */
+    @SerializedName(value = "device_id")
+    private String deviceId;
+    /**
+     * <pre>
+     * 字段名:商户门店信息
+     * 变量名:store_info
+     * 是否必填:否
+     * 类型:object
+     * 描述:
+     *  商户门店信息
+     * </pre>
+     */
+    @SerializedName(value = "store_info")
+    private StoreInfo storeInfo;
+    /**
+     * <pre>
+     * 字段名:H5场景信息
+     * 变量名:h5_info
+     * 是否必填:否(H5支付必填)
+     * 类型:object
+     * 描述:
+     *  H5场景信息
+     * </pre>
+     */
+    @SerializedName(value = "h5_info")
+    private H5Info h5Info;
+  }
+
+  @Data
+  @NoArgsConstructor
+  public static class StoreInfo implements Serializable {
+    private static final long serialVersionUID = -1L;
+    /**
+     * <pre>
+     * 字段名:门店编号
+     * 变量名:id
+     * 是否必填:是
+     * 类型:string[1,32]
+     * 描述:
+     *  商户侧门店编号
+     *  示例值:0001
+     * </pre>
+     */
+    @SerializedName(value = "id")
+    private String id;
+    /**
+     * <pre>
+     * 字段名:门店名称
+     * 变量名:name
+     * 是否必填:否
+     * 类型:string[1,256]
+     * 描述:
+     *  商户侧门店名称
+     *  示例值:腾讯大厦分店
+     * </pre>
+     */
+    @SerializedName(value = "name")
+    private String name;
+    /**
+     * <pre>
+     * 字段名:地区编码
+     * 变量名:area_code
+     * 是否必填:否
+     * 类型:string[1,32]
+     * 描述: 地区编码, <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/applyments/chapter4_1.shtml">详细请见省市区编号对照表</a>。
+     * 示例值:440305
+     * </pre>
+     */
+    @SerializedName(value = "area_code")
+    private String areaCode;
+    /**
+     * <pre>
+     * 字段名:详细地址
+     * 变量名:address
+     * 是否必填:是
+     * 类型:string[1,512]
+     * 描述:
+     *  详细的商户门店地址
+     *  示例值:广东省深圳市南山区科技中一道10000号
+     * </pre>
+     */
+    @SerializedName(value = "address")
+    private String address;
+  }
+
+  @Data
+  @NoArgsConstructor
+  public static class H5Info implements Serializable {
+    private static final long serialVersionUID = -1L;
+    /**
+     * <pre>
+     * 字段名:场景类型
+     * 变量名:type
+     * 是否必填:是
+     * 类型:string[1,32]
+     * 描述:
+     *  场景类型
+     *  示例值:iOS, Android, Wap
+     * </pre>
+     */
+    @SerializedName(value = "type")
+    private String type;
+    /**
+     * <pre>
+     * 字段名:应用名称
+     * 变量名:app_name
+     * 是否必填:否
+     * 类型:string[1,64]
+     * 描述:
+     *  应用名称
+     *  示例值:王者荣耀
+     * </pre>
+     */
+    @SerializedName(value = "app_name")
+    private String appName;
+    /**
+     * <pre>
+     * 字段名:网站URL
+     * 变量名:app_url
+     * 是否必填:否
+     * 类型:string[1,128]
+     * 描述:
+     *  网站URL
+     *  示例值:https://pay.qq.com
+     * </pre>
+     */
+    @SerializedName(value = "app_url")
+    private String appUrl;
+    /**
+     * <pre>
+     * 字段名:iOS平台BundleID
+     * 变量名:bundle_id
+     * 是否必填:否
+     * 类型:string[1,128]
+     * 描述:
+     *  iOS平台BundleID
+     *  示例值:com.tencent.wzryiOS
+     * </pre>
+     */
+    @SerializedName(value = "bundle_id")
+    private String bundleId;
+    /**
+     * <pre>
+     * 字段名:Android平台PackageName
+     * 变量名:package_name
+     * 是否必填:否
+     * 类型:string[1,128]
+     * 描述:
+     *  Android平台PackageName
+     *  示例值:com.tencent.tmgp.sgame
+     * </pre>
+     */
+    @SerializedName(value = "package_name")
+    private String packageName;
+  }
+
+  @Data
+  @NoArgsConstructor
+  public static class SettleInfo implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /**
+     * <pre>
+     * 字段名:是否指定分账
+     * 变量名:profit_sharing
+     * 是否必填:否
+     * 类型:boolean
+     * 描述:
+     *  是否指定分账
+     *  示例值:false
+     * </pre>
+     */
+    @SerializedName(value = "profit_sharing")
+    private Boolean profitSharing;
+  }
+}

+ 61 - 4
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java

@@ -169,7 +169,7 @@ public interface WxPayService {
    * <p>
    * 部分字段会包含敏感信息,所以在提交前需要在请求头中会包含"Wechatpay-Serial"信息
    *
-   * @param url        请求地址
+   * @param url 请求地址
    * @return 返回请求结果字符串 string
    * @throws WxPayException the wx pay exception
    */
@@ -551,6 +551,27 @@ public interface WxPayService {
   <T> T createOrderV3(TradeTypeEnum tradeType, WxPayUnifiedOrderV3Request request) throws WxPayException;
 
   /**
+   * 服务商模式调用统一下单接口,并组装生成支付所需参数对象.
+   *
+   * @param <T>       请使用{@link com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result}里的内部类或字段
+   * @param tradeType the trade type
+   * @param request   统一下单请求参数
+   * @return 返回 {@link com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result}里的内部类或字段
+   * @throws WxPayException the wx pay exception
+   */
+  <T> T createPartnerOrderV3(TradeTypeEnum tradeType, WxPayPartnerUnifiedOrderV3Request request) throws WxPayException;
+
+  /**
+   * 在发起微信支付前,需要调用统一下单接口,获取"预支付交易会话标识"
+   *
+   * @param tradeType the trade type
+   * @param request   请求对象,注意一些参数如spAppid、spMchid等不用设置,方法内会自动从配置对象中获取到(前提是对应配置中已经设置)
+   * @return the wx pay unified order result
+   * @throws WxPayException the wx pay exception
+   */
+  WxPayUnifiedOrderV3Result unifiedPartnerOrderV3(TradeTypeEnum tradeType, WxPayPartnerUnifiedOrderV3Request request) throws WxPayException;
+
+  /**
    * 在发起微信支付前,需要调用统一下单接口,获取"预支付交易会话标识"
    *
    * @param tradeType the trade type
@@ -802,15 +823,39 @@ public interface WxPayService {
   WxPayOrderNotifyResult parseOrderNotifyResult(String xmlData, String signType) throws WxPayException;
 
   /**
-   * 解析支付结果v3通知.
+   * 解析支付结果v3通知. 直连商户模式
+   * 详见https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_5.shtml
+   *
+   * @param notifyData 通知数据
+   * @param header     通知头部数据,不传则表示不校验头
+   * @return the wx pay order notify result
+   * @throws WxPayException the wx pay exception
+   */
+  WxPayNotifyV3Result parseOrderNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException;
+
+  /**
+   * 服务商模式解析支付结果v3通知.
+   * 详见https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_1_5.shtml
+   *
+   * @param notifyData 通知数据
+   * @param header     通知头部数据,不传则表示不校验头
+   * @return the wx pay order notify result
+   * @throws WxPayException the wx pay exception
+   */
+  WxPayPartnerNotifyV3Result parsePartnerOrderNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException;
+
+  /**
+   * 支付服务商和直连商户两种模式
    * 详见https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_5.shtml
    *
    * @param notifyData 通知数据
    * @param header     通知头部数据,不传则表示不校验头
+   * @param resultType 结果类型
+   * @param dataType   结果数据类型
    * @return the wx pay order notify result
    * @throws WxPayException the wx pay exception
    */
-  WxPayOrderNotifyV3Result parseOrderNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException;
+  <T extends WxPayBaseNotifyV3Result<E>, E> T baseParseOrderNotifyV3Result(String notifyData, SignatureHeader header, Class<T> resultType, Class<E> dataType) throws WxPayException;
 
   /**
    * <pre>
@@ -836,7 +881,7 @@ public interface WxPayService {
   WxPayRefundNotifyResult parseRefundNotifyResult(String xmlData) throws WxPayException;
 
   /**
-   * 解析退款结果通知
+   * 解析直连商户退款结果通知
    * 详见https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_16&index=9
    *
    * @param notifyData 通知数据
@@ -847,6 +892,17 @@ public interface WxPayService {
   WxPayRefundNotifyV3Result parseRefundNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException;
 
   /**
+   * 解析服务商模式退款结果通知
+   * 详见https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_1_11.shtml
+   *
+   * @param notifyData 通知数据
+   * @param header     通知头部数据,不传则表示不校验头
+   * @return the wx pay refund notify result
+   * @throws WxPayException the wx pay exception
+   */
+  WxPayPartnerRefundNotifyV3Result parsePartnerRefundNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException;
+
+  /**
    * 解析扫码支付回调通知
    * 详见https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4
    *
@@ -1380,6 +1436,7 @@ public interface WxPayService {
 
   /**
    * 获取服务商支付分服务类
+   *
    * @return the partner pay score service
    */
   PartnerPayScoreService getPartnerPayScoreService();

+ 52 - 23
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java

@@ -31,6 +31,7 @@ import lombok.Getter;
 import lombok.Setter;
 import me.chanjar.weixin.common.error.WxRuntimeException;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.reflect.ConstructorUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -343,7 +344,17 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
   }
 
   @Override
-  public WxPayOrderNotifyV3Result parseOrderNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException {
+  public WxPayNotifyV3Result parseOrderNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException {
+    return baseParseOrderNotifyV3Result(notifyData, header, WxPayNotifyV3Result.class, WxPayNotifyV3Result.DecryptNotifyResult.class);
+  }
+
+  @Override
+  public WxPayPartnerNotifyV3Result parsePartnerOrderNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException {
+    return this.baseParseOrderNotifyV3Result(notifyData, header, WxPayPartnerNotifyV3Result.class, WxPayPartnerNotifyV3Result.DecryptNotifyResult.class);
+  }
+
+  @Override
+  public <T extends WxPayBaseNotifyV3Result<E>, E> T baseParseOrderNotifyV3Result(String notifyData, SignatureHeader header, Class<T> resultType, Class<E> dataType) throws WxPayException {
     if (Objects.nonNull(header) && !this.verifyNotifySign(header, notifyData)) {
       throw new WxPayException("非法请求,头部信息验证失败");
     }
@@ -355,12 +366,12 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
     String apiV3Key = this.getConfig().getApiV3Key();
     try {
       String result = AesUtils.decryptToString(associatedData, nonce, cipherText, apiV3Key);
-      WxPayOrderNotifyV3Result.DecryptNotifyResult decryptNotifyResult = GSON.fromJson(result, WxPayOrderNotifyV3Result.DecryptNotifyResult.class);
-      WxPayOrderNotifyV3Result notifyResult = new WxPayOrderNotifyV3Result();
+      E decryptNotifyResult = GSON.fromJson(result, dataType);
+      T notifyResult = ConstructorUtils.invokeConstructor(resultType);
       notifyResult.setRawData(response);
       notifyResult.setResult(decryptNotifyResult);
       return notifyResult;
-    } catch (GeneralSecurityException | IOException e) {
+    } catch (Exception e) {
       throw new WxPayException("解析报文异常!", e);
     }
   }
@@ -409,25 +420,12 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
 
   @Override
   public WxPayRefundNotifyV3Result parseRefundNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException {
-    if (Objects.nonNull(header) && !this.verifyNotifySign(header, notifyData)) {
-      throw new WxPayException("非法请求,头部信息验证失败");
-    }
-    OriginNotifyResponse response = GSON.fromJson(notifyData, OriginNotifyResponse.class);
-    OriginNotifyResponse.Resource resource = response.getResource();
-    String cipherText = resource.getCiphertext();
-    String associatedData = resource.getAssociatedData();
-    String nonce = resource.getNonce();
-    String apiV3Key = this.getConfig().getApiV3Key();
-    try {
-      String result = AesUtils.decryptToString(associatedData, nonce, cipherText, apiV3Key);
-      WxPayRefundNotifyV3Result.DecryptNotifyResult decryptNotifyResult = GSON.fromJson(result, WxPayRefundNotifyV3Result.DecryptNotifyResult.class);
-      WxPayRefundNotifyV3Result notifyResult = new WxPayRefundNotifyV3Result();
-      notifyResult.setRawData(response);
-      notifyResult.setResult(decryptNotifyResult);
-      return notifyResult;
-    } catch (GeneralSecurityException | IOException e) {
-      throw new WxPayException("解析报文异常!", e);
-    }
+    return this.baseParseOrderNotifyV3Result(notifyData, header, WxPayRefundNotifyV3Result.class, WxPayRefundNotifyV3Result.DecryptNotifyResult.class);
+  }
+
+  @Override
+  public WxPayPartnerRefundNotifyV3Result parsePartnerRefundNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException {
+    return this.baseParseOrderNotifyV3Result(notifyData, header, WxPayPartnerRefundNotifyV3Result.class, WxPayPartnerRefundNotifyV3Result.DecryptNotifyResult.class);
   }
 
   @Override
@@ -666,6 +664,37 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
   }
 
   @Override
+  public <T> T createPartnerOrderV3(TradeTypeEnum tradeType, WxPayPartnerUnifiedOrderV3Request request) throws WxPayException {
+    WxPayUnifiedOrderV3Result result = this.unifiedPartnerOrderV3(tradeType, request);
+    //获取应用ID
+    String appId = StringUtils.isBlank(request.getSubAppid()) ? request.getSpAppid() : request.getSubAppid();
+    return result.getPayInfo(tradeType, appId, request.getSubMchId(), this.getConfig().getPrivateKey());
+  }
+
+  @Override
+  public WxPayUnifiedOrderV3Result unifiedPartnerOrderV3(TradeTypeEnum tradeType, WxPayPartnerUnifiedOrderV3Request request) throws WxPayException {
+    if (StringUtils.isBlank(request.getSpAppid())) {
+      request.setSpAppid(this.getConfig().getAppId());
+    }
+    if (StringUtils.isBlank(request.getSpMchId())) {
+      request.setSpMchId(this.getConfig().getMchId());
+    }
+    if (StringUtils.isBlank(request.getNotifyUrl())) {
+      request.setNotifyUrl(this.getConfig().getNotifyUrl());
+    }
+    if (StringUtils.isBlank(request.getSubAppid())) {
+      request.setSubAppid(this.getConfig().getSubAppId());
+    }
+    if (StringUtils.isBlank(request.getSubMchId())) {
+      request.setSubMchId(this.getConfig().getSubMchId());
+    }
+
+    String url = this.getPayBaseUrl() + tradeType.getPartnerUrl();
+    String response = this.postV3(url, GSON.toJson(request));
+    return GSON.fromJson(response, WxPayUnifiedOrderV3Result.class);
+  }
+
+  @Override
   public WxPayUnifiedOrderV3Result unifiedOrderV3(TradeTypeEnum tradeType, WxPayUnifiedOrderV3Request request) throws WxPayException {
     if (StringUtils.isBlank(request.getAppid())) {
       request.setAppid(this.getConfig().getAppId());

+ 1 - 1
weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImplTest.java

@@ -796,7 +796,7 @@ public class BaseWxPayServiceImplTest {
     log.info("请求头参数为:timestamp:{} nonce:{} serialNo:{} signature:{}", timestamp, nonce, serialNo, signature);
 
     // V2版本请参考com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResultTest里的单元测试
-    final WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result = this.payService.parseOrderNotifyV3Result(RequestUtils.readData(request),
+    final WxPayNotifyV3Result wxPayOrderNotifyV3Result = this.payService.parseOrderNotifyV3Result(RequestUtils.readData(request),
       new SignatureHeader(timestamp, nonce, signature, serialNo));
     log.info(GSON.toJson(wxPayOrderNotifyV3Result));