From c950069d91bdc0203cf6638bb4e5b10031420ef6 Mon Sep 17 00:00:00 2001 From: xhxu Date: Mon, 30 May 2022 09:51:57 +0800 Subject: [PATCH] //tt pay v2 --- .../iformall/douyin/pay/DouYinPayHelper.java | 52 +- .../douyin/pay/ProfitSharingV2Service.java | 48 ++ .../com/iformall/douyin/pay/TtPayService.java | 201 ++++++++ .../douyin/pay/exception/TtPayException.java | 216 +++++++++ .../douyin/pay/impl/BaseTtPayServiceImpl.java | 240 +++++++++ .../pay/impl/ProfitSharingV2ServiceImpl.java | 76 +++ .../pay/impl/TtPayServiceApacheHttpImpl.java | 155 ++++++ .../douyin/pay/impl/TtPayServiceImpl.java | 11 + .../payv2/auth/CertificatesVerifier.java | 32 ++ .../douyin/payv2/auth/PrivateKeySigner.java | 35 ++ .../iformall/douyin/payv2/auth/Signer.java | 15 + .../douyin/payv2/auth/TtPayCredentials.java | 87 ++++ .../douyin/payv2/auth/TtPayValidator.java | 55 +++ .../iformall/douyin/payv2/auth/Verifier.java | 6 + .../douyin/payv2/config/TtPayConfig.java | 179 +++++++ .../request/CallBackSettingsRequest.java | 35 ++ .../payv2/request/ProfitSharingRequest.java | 67 +++ .../request/TtOrderPushDeliveryRequest.java | 73 +++ .../request/TtPayOrderQueryV2Request.java | 38 ++ .../request/TtPayRefundQueryV2Request.java | 36 ++ .../payv2/request/TtPayRefundV2Request.java | 107 ++++ .../request/TtPayUnifiedOrderV2Request.java | 456 ++++++++++++++++++ .../request/TtRefundAuditMsgRequest.java | 64 +++ .../douyin/payv2/result/BaseTtPayResult.java | 57 +++ .../payv2/result/CreateOrderCallback.java | 200 ++++++++ .../payv2/result/OriginNotifyResponse.java | 32 ++ .../payv2/result/ProfitSharingResult.java | 112 +++++ .../payv2/result/RefundOrderCallback.java | 138 ++++++ .../result/TtPayCpsOrderQueryV2Result.java | 304 ++++++++++++ .../payv2/result/TtPayOrderQueryV2Result.java | 119 +++++ .../result/TtPayRefundQueryV2Result.java | 121 +++++ .../payv2/result/TtPayRefundV2Result.java | 33 ++ .../result/TtPayUnifiedOrderV2Result.java | 99 ++++ .../douyin/payv2/util/Credentials.java | 12 + .../douyin/payv2/util/SignatureExec.java | 81 ++++ .../payv2/util/TtPayV2HttpClientBuilder.java | 70 +++ .../iformall/douyin/payv2/util/Validator.java | 9 + .../miniApp/TtMiniAppPayAdapterService.java | 16 +- .../main/java/com/iformall/utils/MaUtil.java | 26 + 39 files changed, 3687 insertions(+), 26 deletions(-) create mode 100644 mallinkService/src/main/java/com/iformall/douyin/pay/ProfitSharingV2Service.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/pay/TtPayService.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/pay/exception/TtPayException.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/pay/impl/BaseTtPayServiceImpl.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/pay/impl/ProfitSharingV2ServiceImpl.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/pay/impl/TtPayServiceApacheHttpImpl.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/pay/impl/TtPayServiceImpl.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/auth/CertificatesVerifier.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/auth/PrivateKeySigner.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/auth/Signer.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/auth/TtPayCredentials.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/auth/TtPayValidator.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/auth/Verifier.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/config/TtPayConfig.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/request/CallBackSettingsRequest.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/request/ProfitSharingRequest.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtOrderPushDeliveryRequest.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayOrderQueryV2Request.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayRefundQueryV2Request.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayRefundV2Request.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayUnifiedOrderV2Request.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtRefundAuditMsgRequest.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/result/BaseTtPayResult.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/result/CreateOrderCallback.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/result/OriginNotifyResponse.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/result/ProfitSharingResult.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/result/RefundOrderCallback.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayCpsOrderQueryV2Result.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayOrderQueryV2Result.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayRefundQueryV2Result.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayRefundV2Result.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayUnifiedOrderV2Result.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/util/Credentials.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/util/SignatureExec.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/util/TtPayV2HttpClientBuilder.java create mode 100644 mallinkService/src/main/java/com/iformall/douyin/payv2/util/Validator.java diff --git a/mallinkService/src/main/java/com/iformall/douyin/pay/DouYinPayHelper.java b/mallinkService/src/main/java/com/iformall/douyin/pay/DouYinPayHelper.java index 779de588d..3d687fb31 100644 --- a/mallinkService/src/main/java/com/iformall/douyin/pay/DouYinPayHelper.java +++ b/mallinkService/src/main/java/com/iformall/douyin/pay/DouYinPayHelper.java @@ -6,7 +6,6 @@ import java.security.NoSuchAlgorithmException; import java.util.*; import com.iformall.common.ErrorCode; -import com.iformall.douyin.pay.enums.AppAddSubMerchantUrlType; import com.iformall.douyin.pay.orderQuery.QueryMerchantResult; import com.iformall.douyin.pay.orderQuery.QueryRefundResult; import com.iformall.douyin.pay.orderQuery.QuerySettleResult; @@ -22,7 +21,6 @@ import com.iformall.utils.HttpUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import springfox.documentation.spring.web.json.Json; /** * 抖音小程序支付 @@ -356,48 +354,52 @@ public class DouYinPayHelper { public static void main(String[] args) { //测试 // String mcuid = "70777649963225930610";//商户号 - String appid = "ttef6599d8705b49b101"; - String salt = "G8DdHfRcEPXB8kHQsnpVWvhLfMC7MZQ72aweTei8";//支付密钥 +// String appid = "ttef6599d8705b49b101"; +// String salt = "G8DdHfRcEPXB8kHQsnpVWvhLfMC7MZQ72aweTei8";//支付密钥 //生产 // String mcuid = "69739487619282066230"; -// String appid = "tt8183f73e1ebb053701"; -// String salt = "rtQwyvhZHMuMYrY6GBm8DnhvEu0fuJUVHYS2KP6P"; + String appid = "ttc0cdb6705ff345f301"; + String salt = "FhGHClzfdDEPW6Cq4hw2GyoynJbpeq6wLQyM5iub"; - AppAddSubMerchant appAddSubMerchant = new AppAddSubMerchant(); - appAddSubMerchant.setAppId("ttef6599d8705b49b101"); - appAddSubMerchant.setSalt("G8DdHfRcEPXB8kHQsnpVWvhLfMC7MZQ72aweTei8"); - appAddSubMerchant.setSubMerchantId("666438479039619072"); - appAddSubMerchant.setUrlType(2); - AppAddSubMerchantResult appAddSubMerchantResult = appAddSubMerchant(appAddSubMerchant); - System.out.println(appAddSubMerchantResult.toString()); +// AppAddSubMerchant appAddSubMerchant = new AppAddSubMerchant(); +// appAddSubMerchant.setAppId("ttef6599d8705b49b101"); +// appAddSubMerchant.setSalt("G8DdHfRcEPXB8kHQsnpVWvhLfMC7MZQ72aweTei8"); +// appAddSubMerchant.setSubMerchantId("666438479039619072"); +// appAddSubMerchant.setUrlType(2); +// AppAddSubMerchantResult appAddSubMerchantResult = appAddSubMerchant(appAddSubMerchant); +// System.out.println(appAddSubMerchantResult.toString()); // Settle settle = new Settle(); // settle.setAppId(appid); // settle.setSalt(salt); ///** -// * {"settle_params":"[{\"amount\":19,\"merchant_uid\":\"69798058370993871733\"}]", -// * "settle_desc":"客户提现。", -// * "sign":"94fa5f559880870f2fda53c67b236a3f", -// * "out_settle_no":"572578660822212608", -// * "out_order_no":"572308600038809600", -// * "app_id":"tt9da4eb19029c2bb301"} +// * {settle_params=[{"amount":6859,"merchant_uid":"70878359464980830803"}], +// * settle_desc=自主完结分账, sign=9acc782f9196bb45823fdbe7e14b24df, +// * out_settle_no=687210710707761152, +// * out_order_no=687209074036342784, app_id=ttc0cdb6705ff345f301} +// * response:{"err_no":6006,"err_tips":"其他分账方分账比例超限","settle_no":""} +// * +// * request:{settle_params=[{"amount":6900,"merchant_uid":"70878359464980830803"}], +// * settle_desc=自主完结分账, sign=d3541104ea0a6234f96ea753aa514f7c, +// * out_settle_no=687210746524631040, out_order_no=687208992638238720, app_id=ttc0cdb6705ff345f301} +// * response:{"err_no":6006,"err_tips":"其他分账方分账比例超限","settle_no":""} // */ -// settle.setOutSettleNo("57257866081118888"); -// settle.setOutOrderNo("613967802405908480"); +// settle.setOutSettleNo("687210746524631040"); +// settle.setOutOrderNo("687208992638238720"); // settle.setSettleDesc("手动处理。"); // List list = new ArrayList<>(); // Map settleParams = new HashMap<>(); -// settleParams.put("merchant_uid",mcuid); -// settleParams.put("amount",5); +// settleParams.put("merchant_uid","70878359464980830803"); +// settleParams.put("amount",6859); // list.add(settleParams); // settle.setSettleParams(JSON.toJSONString(list)); // SettleResult settle1 = settle(settle); // System.out.println(settle1.toString()); -// QuerySettleResult querySettleResult = settleQuery(appid, salt, "57257866081118888", null); -// System.out.println(querySettleResult.toString()); + QuerySettleResult querySettleResult = settleQuery(appid, salt, "687210746524631040", null); + System.out.println(querySettleResult.toString()); // OrderQueryResult orderQueryResult = orderQuery(appid, salt, "613879951804915712", null); // System.out.println(orderQueryResult.toString()); diff --git a/mallinkService/src/main/java/com/iformall/douyin/pay/ProfitSharingV2Service.java b/mallinkService/src/main/java/com/iformall/douyin/pay/ProfitSharingV2Service.java new file mode 100644 index 000000000..5eec923a7 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/pay/ProfitSharingV2Service.java @@ -0,0 +1,48 @@ +package com.iformall.douyin.pay; + +import com.iformall.douyin.pay.exception.TtPayException; +import com.iformall.douyin.payv2.request.ProfitSharingRequest; +import com.iformall.douyin.payv2.result.ProfitSharingResult; + +/** + * 支付V2-资金应用-分账 + * https://bytedance.feishu.cn/docx/doxcnj21j74jHyXEq6Ybg3AzOxh + * @author pg 2021-6-23 + * @date 2021-6-23 + */ +public interface ProfitSharingV2Service { + + /** + *
+   * 请求分账API
+   * 接口简介
+   * 一笔订单完成后,开发者可以通过分账接口将这笔订单产生的资金结算给各个分账方。
+   * - 结算规则统一为核销D+3;
+   *   - CPS订单比较特殊,统一结算规则是核销D+3
+   *   - 结算规则为核销D+3,表示订单到达“终态”3天后可以分账,“终态”表示所有的券/商品已核销或已退款
+   * - 如对上述结算规则有特殊需求,请联系对应行业经理(餐饮@Xinru Chen 酒旅@Jie Zhou @Yuekai Zhao 到综@Yankai Ouyang )
+   * - 若分账失败,请参考文档进行自查分账失败如何自查
+   * 核销D+3
+   * - 例如用户的一笔订单买了5张团购券,其中3张先核销,剩余2张再退款,最终退款完成的时间就是订单到达终态的时间。订单到达终态3天后,则可以发起分账
+   *
+   * 返回 小程序分账id,系统会确保不超过64个字节(数字+ASCII)
+   *
+   */
+  String profitSharing(ProfitSharingRequest request) throws TtPayException;
+
+  /**
+   * 
+   * 查询分账接口
+   * out_order_no string 否 开发者交易订单id,长度<64byte,数字ASCII字符
+   * out_settle_no string 否 开发者分账单id,长度<64byte,数字、ASCII字符
+   * order_id string 否 抖音开平侧订单id,长度<64byte,数字、ASCII
+   * settle_id string 否 抖音开平侧分账单id,长度<64byte,数字、ASCII
+   * - 查询优先级: settle_id > order_id > out_settle_no > out_order_no,4个参数选填一个
+   *   - 例如:请求填写了settle_id和order_id,服务只会按settle_id来查询,忽略order_id
+   *   - 如果未查询到结果,会返回空数组
+   * - 注意:对于担保交易和交易1.0的订单,如果需要根据order_id/out_order_no查询订单的分账记录,建议使用order_id(抖音开平侧支付单id查询),若使用out_order_no可能存在查询不到的情况
+   *
+   */
+  ProfitSharingResult getProfitSharingResult(String order_id) throws TtPayException;
+
+}
diff --git a/mallinkService/src/main/java/com/iformall/douyin/pay/TtPayService.java b/mallinkService/src/main/java/com/iformall/douyin/pay/TtPayService.java
new file mode 100644
index 000000000..0a0a70c70
--- /dev/null
+++ b/mallinkService/src/main/java/com/iformall/douyin/pay/TtPayService.java
@@ -0,0 +1,201 @@
+package com.iformall.douyin.pay;
+
+import com.github.binarywang.wxpay.bean.WxPayApiData;
+import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader;
+import com.iformall.douyin.pay.exception.TtPayException;
+import com.iformall.douyin.payv2.config.TtPayConfig;
+import com.iformall.douyin.payv2.request.*;
+import com.iformall.douyin.payv2.result.*;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpRequestBase;
+
+
+/**
+ * 
+ * 支付相关接口.
+ * Created by Binary Wang on 2016/7/28.
+ * 
+ * + */ +public interface TtPayService { + + /** + * 获取微信支付请求url前缀,沙箱环境可能不一样. + * + * @return the pay base url + */ + String getPayBaseUrl(); + + + /** + * 发送post请求,得到响应字符串. + * + * @param url 请求地址 + * @param requestStr 请求信息 + * @return 返回请求结果字符串 string + * @throws TtPayException the tt pay exception + */ + String postV2(String url, String requestStr) throws TtPayException; + + /** + * 发送post请求,得到响应字符串. + * + * @param url 请求地址 + * @param httpPost 请求信息 + * @return 返回请求结果字符串 string + * @throws TtPayException the tt pay exception + */ + String postV2(String url, HttpPost httpPost) throws TtPayException; + + /** + * 发送http请求,得到响应字符串. + * + * @param url 请求地址 + * @param httpRequest 请求信息,可以是put,post,get,delete等请求 + * @return 返回请求结果字符串 string + * @throws TtPayException the tt pay exception + */ + String requestV2(String url, HttpRequestBase httpRequest) throws TtPayException; + + /** + * 发送get V2请求,得到响应字符串. + * + * @param url 请求地址 + * @return 返回请求结果字符串 string + * @throws TtPayException the tt pay exception + */ + String getV2(String url) throws TtPayException; + + /** + * 获取分账服务类. + * + * @return the ent pay service + */ + ProfitSharingV2Service getProfitSharingV2Service(); + + + /** + * 2.1 交易回调设置接口 + */ + boolean callbackSettings(CallBackSettingsRequest settings) throws TtPayException; + + + /** + * 2.2 查询交易回调设置接口 + */ + CallBackSettingsRequest querySettings() throws TtPayException; + + + /** + * 解析预下单回调 + * + */ + CreateOrderCallback parseOrderNotifyV2Result(String notifyData, SignatureHeader header) throws TtPayException; + + + /** + * 交易系统2.0模版下单依赖入口前端js-api,如果用户没有更新抖音,就没法使用模版下单,开发者可用该接口替用户发起下单,开发者发起的下单流程则不会有预下单回调,当低版本减少后,会下掉该接口,请不要应用于其它场景,否则会影响业务(对于poi商品,目前只支持团购券) + * @param request + * @return + * @throws TtPayException + */ + TtPayUnifiedOrderV2Result createOrderV2(TtPayUnifiedOrderV2Request request) throws TtPayException; + + + + /** + *
+   * 5. 订单信息接口
+   */
+  TtPayOrderQueryV2Result queryOrderV2(String orderId, String outOrderNo) throws TtPayException;
+
+  /**
+   * 
+   * 5. 订单信息接口
+   */
+  TtPayOrderQueryV2Result queryOrderV2(TtPayOrderQueryV2Request request) throws TtPayException;
+
+  /**
+   * 
+   * 5. cps订单信息接口
+   */
+  TtPayCpsOrderQueryV2Result queryCpsOrderV2(String orderId, String outOrderNo) throws TtPayException;
+
+  /**
+   * 推送核销状态(非闭环核销)
+   */
+  boolean pushDelivery(TtOrderPushDeliveryRequest request) throws TtPayException;
+
+
+  /**
+   * 获取配置.
+   *
+   * @return the config
+   */
+  TtPayConfig getConfig();
+
+  /**
+   * 设置配置对象.
+   *
+   * @param config the config
+   */
+  void setConfig(TtPayConfig config);
+
+  /**
+   * 
+   * 开发者可用该接口替用户发起退款,开发者发起的退款流程则不会有退款申请回调。
+   * - 过期自动退说明
+   *   - 过期自动退的退款被拒绝退款后,不会再次发起。
+   *   - 在交易系统2.0产生的订单,如果用户购买的商品是过期退商品,交易系统2.0会自动发起退款,创建退款单,退款流程和用户发起退款流程相同。
+   *   - 发起过期自动退条件
+   *     - 非POI 商品
+   *       - 前端传入合法的商品过期时间
+   *       - 前端传入的goodsLabels中包含过期退标签
+   *     - POI 商品
+   *       - POI 商品设置了过期时间
+   *       - POI 商品的 sub_title包含过期退标签
+   *   - 旧订单系统不会自动发起过期自动退,仍需开发者处理。
+   *     - 过期自动退的退款单由系统创建,因此 退款申请回调和状态通知回调 cp_extra 会是空值。
+   * - 外部退款单号说明
+   *   - 已经在担保支付或者交易1.0使用过的外部退款单号,请不要在交易2.0使用,当交易2.0系统发现该外部退款单号已经在担保支付或者交易1.0创建过退款单号时,会拒绝创建退款单。
+   *   - 已经升级到交易2.0的接口之后,不支持再降级使用交易1.0,否则产生的后果由开发者承担。
+   */
+  TtPayRefundV2Result refundV2(TtPayRefundV2Request request) throws TtPayException;
+
+  /**
+   * 5. 退款申请回调
+   * 当用户从交易的模板组件发起退款后,抖音开平交易系统会给向开发者的服务发起请求,用于通知开发者用户发起退款,开发者响应时需要返回必填的信息。
+   * 注意
+   * - 退款单的创建是在退款申请回调之前,退款申请回调请求开发者服务失败,也不会在C端展示错误,退款申请回调请求失败后会进行重试。
+   * - 请务必确保退款申请回调请求能按照正确的格式和参数要求返回。
+   * - 请求返回的外部退款单号也务必确保在同一小程序内不会重复,否则也会被认为请求失败。
+   * - 开发者返回的err_no 不为 0 也会认为请求失败,会进行重试,请确保请求成功,有拒绝退款的场景请在退款审核阶段拒绝退款。
+   * - 如果一直请求开发者服务失败,退款单将无法被推进到退款审核阶段,请务必确保响应无问题。
+   * - 退款申请回调务必做好幂等处理,相同的系统退款单号重复请求应当被视为相同的一次退款。
+   *
+   */
+  RefundOrderCallback refundOrderNotifyV2Result(String notifyData, SignatureHeader header) throws TtPayException;
+
+  /**
+   * 退款审核结果同步
+   * @param request
+   * @return
+   * @throws TtPayException
+   */
+  boolean merchantAuditCallback(TtRefundAuditMsgRequest request) throws TtPayException;
+
+  /**
+   * 
+   * 查询退款
+   *
+   */
+  TtPayRefundQueryV2Result refundQueryV2(TtPayRefundQueryV2Request request) throws TtPayException;
+
+  /**
+   * 获取微信请求数据,方便接口调用方获取处理.
+   *
+   * @return the wx api data
+   */
+  WxPayApiData getWxApiData();
+
+}
diff --git a/mallinkService/src/main/java/com/iformall/douyin/pay/exception/TtPayException.java b/mallinkService/src/main/java/com/iformall/douyin/pay/exception/TtPayException.java
new file mode 100644
index 000000000..c73d12b21
--- /dev/null
+++ b/mallinkService/src/main/java/com/iformall/douyin/pay/exception/TtPayException.java
@@ -0,0 +1,216 @@
+package com.iformall.douyin.pay.exception;
+
+import com.github.binarywang.wxpay.bean.result.BaseWxPayResult;
+import com.google.common.base.Joiner;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 
+ * 支付异常结果类
+ * Created by Binary Wang on 2017-6-6.
+ * 
+ * + * @author BinaryWang + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class TtPayException extends Exception { + + private static final long serialVersionUID = -1246675502834889819L; + /** + * 自定义错误讯息. + */ + private String customErrorMsg; + /** + * 返回状态码. + */ + private String returnCode; + /** + * 返回信息. + */ + private String returnMsg; + + /** + * 业务结果. + */ + private String resultCode; + + /** + * 错误代码. + */ + private String errCode; + + /** + * 错误代码描述. + */ + private String errCodeDes; + + /** + * 微信支付返回的结果xml字符串. + */ + private String xmlString; + + /** + * Instantiates a new Wx pay exception. + * + * @param customErrorMsg the custom error msg + */ + public TtPayException(String customErrorMsg) { + super(customErrorMsg); + this.customErrorMsg = customErrorMsg; + } + + /** + * Instantiates a new Wx pay exception. + * + * @param customErrorMsg the custom error msg + * @param tr the tr + */ + public TtPayException(String customErrorMsg, Throwable tr) { + super(customErrorMsg, tr); + this.customErrorMsg = customErrorMsg; + } + + private TtPayException(Builder builder) { + super(builder.buildErrorMsg()); + returnCode = builder.returnCode; + returnMsg = builder.returnMsg; + resultCode = builder.resultCode; + errCode = builder.errCode; + errCodeDes = builder.errCodeDes; + xmlString = builder.xmlString; + } + + /** + * 通过BaseWxPayResult生成异常对象. + * + * @param payBaseResult the pay base result + * @return the wx pay exception + */ + public static TtPayException from(BaseWxPayResult payBaseResult) { + return TtPayException.newBuilder() + .xmlString(payBaseResult.getXmlString()) + .returnMsg(payBaseResult.getReturnMsg()) + .returnCode(payBaseResult.getReturnCode()) + .resultCode(payBaseResult.getResultCode()) + .errCode(payBaseResult.getErrCode()) + .errCodeDes(payBaseResult.getErrCodeDes()) + .build(); + } + + /** + * New builder builder. + * + * @return the builder + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * The type Builder. + */ + public static final class Builder { + private String returnCode; + private String returnMsg; + private String resultCode; + private String errCode; + private String errCodeDes; + private String xmlString; + + private Builder() { + } + + /** + * Return code builder. + * + * @param returnCode the return code + * @return the builder + */ + public Builder returnCode(String returnCode) { + this.returnCode = returnCode; + return this; + } + + /** + * Return msg builder. + * + * @param returnMsg the return msg + * @return the builder + */ + public Builder returnMsg(String returnMsg) { + this.returnMsg = returnMsg; + return this; + } + + /** + * Result code builder. + * + * @param resultCode the result code + * @return the builder + */ + public Builder resultCode(String resultCode) { + this.resultCode = resultCode; + return this; + } + + /** + * Err code builder. + * + * @param errCode the err code + * @return the builder + */ + public Builder errCode(String errCode) { + this.errCode = errCode; + return this; + } + + /** + * Err code des builder. + * + * @param errCodeDes the err code des + * @return the builder + */ + public Builder errCodeDes(String errCodeDes) { + this.errCodeDes = errCodeDes; + return this; + } + + /** + * Xml string builder. + * + * @param xmlString the xml string + * @return the builder + */ + public Builder xmlString(String xmlString) { + this.xmlString = xmlString; + return this; + } + + /** + * Build wx pay exception. + * + * @return the wx pay exception + */ + public TtPayException build() { + return new TtPayException(this); + } + + /** + * Build error msg string. + * + * @return the string + */ + public String buildErrorMsg() { + return Joiner.on(",").skipNulls().join( + returnCode == null ? null : String.format("返回代码:[%s]", returnCode), + returnMsg == null ? null : String.format("返回信息:[%s]", returnMsg), + resultCode == null ? null : String.format("结果代码:[%s]", resultCode), + errCode == null ? null : String.format("错误代码:[%s]", errCode), + errCodeDes == null ? null : String.format("错误详情:[%s]", errCodeDes), + xmlString == null ? null : "返回的原始报文:\n" + xmlString + ); + } + } +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/pay/impl/BaseTtPayServiceImpl.java b/mallinkService/src/main/java/com/iformall/douyin/pay/impl/BaseTtPayServiceImpl.java new file mode 100644 index 000000000..cd189ff05 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/pay/impl/BaseTtPayServiceImpl.java @@ -0,0 +1,240 @@ +package com.iformall.douyin.pay.impl; + +import com.alibaba.fastjson.TypeReference; +import com.github.binarywang.wxpay.bean.WxPayApiData; +import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.v3.util.AesUtils; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.iformall.douyin.pay.ProfitSharingV2Service; +import com.iformall.douyin.pay.TtPayService; +import com.iformall.douyin.pay.exception.TtPayException; +import com.iformall.douyin.payv2.config.TtPayConfig; +import com.iformall.douyin.payv2.request.*; +import com.iformall.douyin.payv2.result.*; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.*; + +/** + *
+ *  支付接口请求抽象实现类
+ * Created by Binary Wang on 2017-7-8.
+ * 
+ * + * @author Binary Wang + */ +public abstract class BaseTtPayServiceImpl implements TtPayService { + private static final String TOTAL_FUND_COUNT = "资金流水总笔数"; + /** + * The Log. + */ + final Logger log = LoggerFactory.getLogger(this.getClass()); + + private static final Gson GSON = new GsonBuilder().create(); + /** + * The constant wxApiData. + */ + static ThreadLocal wxApiData = new ThreadLocal<>(); + + private ProfitSharingV2Service profitSharingV2Service = new ProfitSharingV2ServiceImpl(this); + + /** + * The Config. + */ + protected TtPayConfig config; + + @Override + public ProfitSharingV2Service getProfitSharingV2Service() { + return profitSharingV2Service; + } + + @Override + public TtPayConfig getConfig() { + return this.config; + } + + @Override + public void setConfig(TtPayConfig config) { + this.config = config; + } + + @Override + public String getPayBaseUrl() { + return this.getConfig().getPayBaseUrl(); + } + + @Override + public boolean callbackSettings(CallBackSettingsRequest request) throws TtPayException { + String url = String.format("%s/api/apps/trade/v2/settings", this.getPayBaseUrl()); + String response = this.postV2(url, GSON.toJson(request)); + return GSON.fromJson(response, BaseTtPayResult.class).isSuccess(); + } + + @Override + public CallBackSettingsRequest querySettings() throws TtPayException { + String url = String.format("%s/api/apps/trade/v2/query_settings", this.getPayBaseUrl()); + String response = this.postV2(url,""); + BaseTtPayResult baseTtPayResult = GSON.fromJson(response, + new TypeReference>(){}.getType()); + if(baseTtPayResult.isSuccess()){ + return baseTtPayResult.getData(); + }else{ + throw new TtPayException(baseTtPayResult.getErrTips()); + } + } + + @Override + public CreateOrderCallback parseOrderNotifyV2Result(String notifyData, SignatureHeader header) throws TtPayException { + if (Objects.nonNull(header) && !this.verifyNotifySign(header, notifyData)) { + throw new TtPayException("非法请求,头部信息验证失败"); + } + OriginNotifyResponse response = GSON.fromJson(notifyData, OriginNotifyResponse.class); + try { + CreateOrderCallback createOrderCallback = GSON.fromJson(response.getMsg(), CreateOrderCallback.class); + return createOrderCallback; + } catch (Exception e) { + throw new TtPayException("解析报文异常!", e); + } + } + + /** + * 校验通知签名 + * + * @param header 通知头信息 + * @param data 通知数据 + * @return true:校验通过 false:校验不通过 + */ + private boolean verifyNotifySign(SignatureHeader header, String data) { + String beforeSign = String.format("%s\n%s\n%s\n", + header.getTimeStamp(), + header.getNonce(), + data); + return this.getConfig().getVerifier().verify( + beforeSign.getBytes(StandardCharsets.UTF_8), header.getSigned()); + } + + @Override + public TtPayUnifiedOrderV2Result createOrderV2(TtPayUnifiedOrderV2Request request) throws TtPayException{ + String url = String.format("%s/api/apps/trade/v2/create_order", this.getPayBaseUrl()); + String response = this.postV2(url, GSON.toJson(request)); + BaseTtPayResult baseTtPayResult = GSON.fromJson(response, + new TypeReference>(){}.getType()); + if(baseTtPayResult.isSuccess()){ + return baseTtPayResult.getData(); + }else{ + throw new TtPayException(baseTtPayResult.getErrTips()); + } + } + + @Override + public TtPayOrderQueryV2Result queryOrderV2(String orderId, String outOrderNo) throws TtPayException { + TtPayOrderQueryV2Request request = new TtPayOrderQueryV2Request(); + request.setOutOrderNo(StringUtils.trimToNull(outOrderNo)); + request.setOrderId(StringUtils.trimToNull(orderId)); + return this.queryOrderV2(request); + } + + @Override + public TtPayOrderQueryV2Result queryOrderV2(TtPayOrderQueryV2Request request) throws TtPayException { + String url = String.format("%s/api/apps/trade/v2/query_order", this.getPayBaseUrl()); + String response = this.postV2(url, GSON.toJson(request)); + BaseTtPayResult baseTtPayResult = GSON.fromJson(response, + new TypeReference>(){}.getType()); + if(baseTtPayResult.isSuccess()){ + return baseTtPayResult.getData(); + }else{ + throw new TtPayException(baseTtPayResult.getErrTips()); + } + } + + public TtPayCpsOrderQueryV2Result queryCpsOrderV2(String orderId, String outOrderNo) throws TtPayException{ + String url = String.format("%s/api/apps/trade/v2/query_cps", this.getPayBaseUrl()); + TtPayOrderQueryV2Request request = new TtPayOrderQueryV2Request(); + request.setOutOrderNo(StringUtils.trimToNull(outOrderNo)); + request.setOrderId(StringUtils.trimToNull(orderId)); + String response = this.postV2(url, GSON.toJson(request)); + BaseTtPayResult baseTtPayResult = GSON.fromJson(response, + new TypeReference>(){}.getType()); + if(baseTtPayResult.isSuccess()){ + return baseTtPayResult.getData(); + }else{ + throw new TtPayException(baseTtPayResult.getErrTips()); + } + } + + + @Override + public boolean pushDelivery(TtOrderPushDeliveryRequest request) throws TtPayException{ + String url = String.format("%s/api/apps/trade/v2/push_delivery", this.getPayBaseUrl()); + String response = this.postV2(url, GSON.toJson(request)); + return GSON.fromJson(response, BaseTtPayResult.class).isSuccess(); + + } + + @Override + public TtPayRefundV2Result refundV2(TtPayRefundV2Request request) throws TtPayException { + String url = String.format("%s/api/apps/trade/v2/create_refund", this.getPayBaseUrl()); + String response = this.postV2(url, GSON.toJson(request)); + BaseTtPayResult baseTtPayResult = GSON.fromJson(response, + new TypeReference>(){}.getType()); + if(baseTtPayResult.isSuccess()){ + return baseTtPayResult.getData(); + }else{ + throw new TtPayException(baseTtPayResult.getErrTips()); + } + } + + @Override + public RefundOrderCallback refundOrderNotifyV2Result(String notifyData, SignatureHeader header) throws TtPayException { + if (Objects.nonNull(header) && !this.verifyNotifySign(header, notifyData)) { + throw new TtPayException("非法请求,头部信息验证失败"); + } + OriginNotifyResponse response = GSON.fromJson(notifyData, OriginNotifyResponse.class); + try { + RefundOrderCallback refundOrderCallback = GSON.fromJson(response.getMsg(), RefundOrderCallback.class); + return refundOrderCallback; + } catch (Exception e) { + throw new TtPayException("解析报文异常!", e); + } + } + + @Override + public boolean merchantAuditCallback(TtRefundAuditMsgRequest request) throws TtPayException{ + String url = String.format("%s/api/apps/trade/v2/merchant_audit_callback", this.getPayBaseUrl()); + String response = this.postV2(url, GSON.toJson(request)); + return GSON.fromJson(response, BaseTtPayResult.class).isSuccess(); + } + + @Override + public TtPayRefundQueryV2Result refundQueryV2(TtPayRefundQueryV2Request request) throws TtPayException{ + String url = String.format("%s/api/apps/trade/v2/query_refund", this.getPayBaseUrl()); + String response = this.postV2(url, GSON.toJson(request)); + BaseTtPayResult baseTtPayResult = GSON.fromJson(response, + new TypeReference>(){}.getType()); + if(baseTtPayResult.isSuccess()){ + return baseTtPayResult.getData(); + }else{ + throw new TtPayException(baseTtPayResult.getErrTips()); + } + } + + + @Override + public WxPayApiData getWxApiData() { + try { + return wxApiData.get(); + } finally { + //一般来说,接口请求会在一个线程内进行,这种情况下,每个线程get的会是之前所存入的数据, + // 但以防万一有同一线程多次请求的问题,所以每次获取完数据后移除对应数据 + wxApiData.remove(); + } + } + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/pay/impl/ProfitSharingV2ServiceImpl.java b/mallinkService/src/main/java/com/iformall/douyin/pay/impl/ProfitSharingV2ServiceImpl.java new file mode 100644 index 000000000..f54c2af61 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/pay/impl/ProfitSharingV2ServiceImpl.java @@ -0,0 +1,76 @@ +package com.iformall.douyin.pay.impl; + +import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.iformall.douyin.pay.ProfitSharingV2Service; +import com.iformall.douyin.pay.TtPayService; +import com.iformall.douyin.pay.exception.TtPayException; +import com.iformall.douyin.payv2.auth.Verifier; +import com.iformall.douyin.payv2.request.ProfitSharingRequest; +import com.iformall.douyin.payv2.result.BaseTtPayResult; +import com.iformall.douyin.payv2.result.ProfitSharingResult; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.util.json.GsonHelper; + +import java.nio.charset.StandardCharsets; + +/** + * 支付V2-资金应用-分账Service + * + * @author pg 2021-6-23 + * @version 1.0 + */ +@Slf4j +@RequiredArgsConstructor +public class ProfitSharingV2ServiceImpl implements ProfitSharingV2Service { + private static final Gson GSON = new GsonBuilder().create(); + private final TtPayService payService; + + @Override + public String profitSharing(ProfitSharingRequest request) throws TtPayException { + String url = String.format("%s/api/apps/trade/v2/create_settle", this.payService.getPayBaseUrl()); + String result = this.payService.postV2(url, GSON.toJson(request)); + BaseTtPayResult baseTtPayResult = GSON.fromJson(result, BaseTtPayResult.class); + if(baseTtPayResult.isSuccess()){ + Object data = baseTtPayResult.getData(); + JsonObject asJsonObject = GSON.toJsonTree(data).getAsJsonObject(); + return GsonHelper.getString(asJsonObject,"settle_id"); + }else{ + throw new TtPayException(baseTtPayResult.getErrTips()); + } + } + + @Override + public ProfitSharingResult getProfitSharingResult(String order_id) throws TtPayException { + String url = String.format("%s/api/apps/trade/v2/query_settle", this.payService.getPayBaseUrl()); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("order_id",order_id); + String result = this.payService.postV2(url,jsonObject.toString()); + return GSON.fromJson(result, ProfitSharingResult.class); + } + + + + /** + * 校验通知签名 + * + * @param header 通知头信息 + * @param data 通知数据 + * @return true:校验通过 false:校验不通过 + */ + private boolean verifyNotifySign(SignatureHeader header, String data) throws TtPayException { + String beforeSign = String.format("%s\n%s\n%s\n", + header.getTimeStamp(), + header.getNonce(), + data); + Verifier verifier = this.payService.getConfig().getVerifier(); + if (verifier == null) { + throw new TtPayException("检验对象为空"); + } + return verifier.verify( + beforeSign.getBytes(StandardCharsets.UTF_8), header.getSigned()); + } +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/pay/impl/TtPayServiceApacheHttpImpl.java b/mallinkService/src/main/java/com/iformall/douyin/pay/impl/TtPayServiceApacheHttpImpl.java new file mode 100644 index 000000000..e6082e374 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/pay/impl/TtPayServiceApacheHttpImpl.java @@ -0,0 +1,155 @@ +package com.iformall.douyin.pay.impl; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.iformall.douyin.pay.exception.TtPayException; +import me.chanjar.weixin.common.util.json.GsonParser; +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; + + +/** + *
+ * 支付请求实现类,apache httpclient实现.
+ * Created by Binary Wang on 2016/7/28.
+ * 
+ * + */ +public class TtPayServiceApacheHttpImpl extends BaseTtPayServiceImpl { + + @Override + public String postV2(String url, String requestStr) throws TtPayException { + CloseableHttpClient httpClient = this.createApiV2HttpClient(); + HttpPost httpPost = this.createHttpPost(url, requestStr); + httpPost.addHeader("Accept", "application/json"); + httpPost.addHeader("Content-Type", "application/json"); + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + //v3已经改为通过状态码判断200 204 成功 + int statusCode = response.getStatusLine().getStatusCode(); + //post方法有可能会没有返回值的情况 + String responseString; + if (response.getEntity() == null) { + responseString = null; + } else { + responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + } + if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode) { + this.log.info("\n【请求地址】:{}\n【请求数据】:{}\n【响应数据】:{}", url, requestStr, responseString); + return responseString; + } else { + //有错误提示信息返回 + JsonObject jsonObject = GsonParser.parse(responseString); + throw convertException(jsonObject); + } + } catch (Exception e) { + this.log.error("\n【请求地址】:{}\n【请求数据】:{}\n【异常信息】:{}", url, requestStr, e.getMessage()); + throw (e instanceof TtPayException) ? (TtPayException) e : new TtPayException(e.getMessage(), e); + } finally { + httpPost.releaseConnection(); + } + + + } + + @Override + public String postV2(String url, HttpPost httpPost) throws TtPayException { + return this.requestV2(url, httpPost); + } + + @Override + public String requestV2(String url, HttpRequestBase httpRequest) throws TtPayException { + httpRequest.setConfig(RequestConfig.custom() + .setConnectionRequestTimeout(this.getConfig().getHttpConnectionTimeout()) + .setConnectTimeout(this.getConfig().getHttpConnectionTimeout()) + .setSocketTimeout(this.getConfig().getHttpTimeout()) + .build()); + + CloseableHttpClient httpClient = this.createApiV2HttpClient(); + try (CloseableHttpResponse response = httpClient.execute(httpRequest)) { + //v3已经改为通过状态码判断200 204 成功 + int statusCode = response.getStatusLine().getStatusCode(); + //post方法有可能会没有返回值的情况 + String responseString; + if (response.getEntity() == null) { + responseString = null; + } else { + responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + } + if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode) { + this.log.info("\n【请求地址】:{}\n【响应数据】:{}", url, responseString); + return responseString; + } else { + //有错误提示信息返回 + JsonObject jsonObject = GsonParser.parse(responseString); + throw convertException(jsonObject); + } + } catch (Exception e) { + this.log.error("\n【请求地址】:{}\n【异常信息】:{}", url, e.getMessage()); + throw (e instanceof TtPayException) ? (TtPayException) e : new TtPayException(e.getMessage(), e); + } finally { + httpRequest.releaseConnection(); + } + } + + @Override + public String getV2(String url) throws TtPayException { + HttpGet httpGet = new HttpGet(url); + httpGet.addHeader("Accept", "application/json"); + httpGet.addHeader("Content-Type", "application/json"); + return this.requestV2(url.toString(), httpGet); + } + + + private CloseableHttpClient createApiV2HttpClient() throws TtPayException { + CloseableHttpClient apiV3HttpClient = this.getConfig().getApiV2HttpClient(); + if (null == apiV3HttpClient) { + return this.getConfig().initApiV2HttpClient(); + } + return apiV3HttpClient; + } + + private StringEntity createEntry(String requestStr) { + try { + return new StringEntity(new String(requestStr.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1)); + } catch (UnsupportedEncodingException e) { + //cannot happen + this.log.error(e.getMessage(), e); + return null; + } + } + + private HttpPost createHttpPost(String url, String requestStr) { + HttpPost httpPost = new HttpPost(url); + httpPost.setEntity(this.createEntry(requestStr)); + + httpPost.setConfig(RequestConfig.custom() + .setConnectionRequestTimeout(this.getConfig().getHttpConnectionTimeout()) + .setConnectTimeout(this.getConfig().getHttpConnectionTimeout()) + .setSocketTimeout(this.getConfig().getHttpTimeout()) + .build()); + + return httpPost; + } + + private TtPayException convertException(JsonObject jsonObject) { + //todo 这里考虑使用新的适用于V2的异常 + JsonElement codeElement = jsonObject.get("code"); + String code = codeElement == null ? null : codeElement.getAsString(); + String message = jsonObject.get("message").getAsString(); + TtPayException ttPayException = new TtPayException(message); + ttPayException.setErrCode(code); + ttPayException.setErrCodeDes(message); + return ttPayException; + } + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/pay/impl/TtPayServiceImpl.java b/mallinkService/src/main/java/com/iformall/douyin/pay/impl/TtPayServiceImpl.java new file mode 100644 index 000000000..aa137ba85 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/pay/impl/TtPayServiceImpl.java @@ -0,0 +1,11 @@ +package com.iformall.douyin.pay.impl; + +/** + *
+ * 支付接口请求实现类,默认使用Apache HttpClient实现
+ * Created by Binary Wang on 2017-7-8.
+ * 
+ * + */ +public class TtPayServiceImpl extends TtPayServiceApacheHttpImpl { +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/CertificatesVerifier.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/CertificatesVerifier.java new file mode 100644 index 000000000..874ff2b6d --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/CertificatesVerifier.java @@ -0,0 +1,32 @@ +package com.iformall.douyin.payv2.auth; + +import me.chanjar.weixin.common.error.WxRuntimeException; + +import java.security.*; +import java.util.Base64; + +public class CertificatesVerifier implements Verifier { + + private PublicKey apiKey; + + public CertificatesVerifier(PublicKey apiKey) { + this.apiKey = apiKey; + } + + @Override + public boolean verify(byte[] message, String signature) { + try { + Signature sign = Signature.getInstance("SHA256withRSA"); + sign.initVerify(this.apiKey); + sign.update(message); + return sign.verify(Base64.getDecoder().decode(signature)); + } catch (NoSuchAlgorithmException e) { + throw new WxRuntimeException("当前Java环境不支持SHA256withRSA", e); + } catch (SignatureException e) { + throw new WxRuntimeException("签名验证过程发生了错误", e); + } catch (InvalidKeyException e) { + throw new WxRuntimeException("无效的证书", e); + } + } + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/PrivateKeySigner.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/PrivateKeySigner.java new file mode 100644 index 000000000..7f46f9218 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/PrivateKeySigner.java @@ -0,0 +1,35 @@ +package com.iformall.douyin.payv2.auth; + +import me.chanjar.weixin.common.error.WxRuntimeException; + +import java.security.*; +import java.util.Base64; + +public class PrivateKeySigner implements Signer { + private String certificateSerialNumber; + + private PrivateKey privateKey; + + public PrivateKeySigner(String serialNumber, PrivateKey privateKey) { + this.certificateSerialNumber = serialNumber; + this.privateKey = privateKey; + } + + @Override + public SignatureResult sign(byte[] message) { + try { + Signature sign = Signature.getInstance("SHA256withRSA"); + sign.initSign(privateKey); + sign.update(message); + + return new SignatureResult( + Base64.getEncoder().encodeToString(sign.sign()), certificateSerialNumber); + } catch (NoSuchAlgorithmException e) { + throw new WxRuntimeException("当前Java环境不支持SHA256withRSA", e); + } catch (SignatureException e) { + throw new WxRuntimeException("签名计算失败", e); + } catch (InvalidKeyException e) { + throw new WxRuntimeException("无效的私钥", e); + } + } +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/Signer.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/Signer.java new file mode 100644 index 000000000..0d8156eb0 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/Signer.java @@ -0,0 +1,15 @@ +package com.iformall.douyin.payv2.auth; + +public interface Signer { + SignatureResult sign(byte[] message); + + class SignatureResult { + String sign; + String certificateSerialNumber; + + public SignatureResult(String sign, String serialNumber) { + this.sign = sign; + this.certificateSerialNumber = serialNumber; + } + } +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/TtPayCredentials.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/TtPayCredentials.java new file mode 100644 index 000000000..73c7485db --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/TtPayCredentials.java @@ -0,0 +1,87 @@ +package com.iformall.douyin.payv2.auth; + +import com.iformall.douyin.payv2.util.Credentials; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.client.methods.HttpRequestWrapper; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; + +@Slf4j +public class TtPayCredentials implements Credentials { + private static final String SYMBOLS = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final SecureRandom RANDOM = new SecureRandom(); + protected String appId; + protected Signer signer; + + public TtPayCredentials(String appId, Signer signer) { + this.appId = appId; + this.signer = signer; + } + + public String getAppId() { + return appId; + } + + protected long generateTimestamp() { + return System.currentTimeMillis() / 1000; + } + + protected String generateNonceStr() { + char[] nonceChars = new char[32]; + for (int index = 0; index < nonceChars.length; ++index) { + nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length())); + } + return new String(nonceChars); + } + + @Override + public final String getSchema() { + return "SHA256-RSA2048"; + } + + @Override + public final String getToken(HttpRequestWrapper request) throws IOException { + String nonceStr = generateNonceStr(); + long timestamp = generateTimestamp(); + + String message = buildMessage(nonceStr, timestamp, request); + log.debug("authorization message=[{}]", message); + + Signer.SignatureResult signature = signer.sign(message.getBytes(StandardCharsets.UTF_8)); + + String token = "appid=\"" + getAppId() + "\"," + + "nonce_str=\"" + nonceStr + "\"," + + "timestamp=\"" + timestamp + "\"," + + "key_version=\"" + signature.certificateSerialNumber + "\"," + + "signature=\"" + signature.sign + "\""; + log.debug("authorization token=[{}]", token); + + return token; + } + + protected final String buildMessage(String nonce, long timestamp, HttpRequestWrapper request) + throws IOException { + URI uri = request.getURI(); + String canonicalUrl = uri.getRawPath(); + if (uri.getQuery() != null) { + canonicalUrl += "?" + uri.getRawQuery(); + } + + String body = ""; + // PATCH,POST,PUT + body = EntityUtils.toString(((HttpEntityEnclosingRequest) request).getEntity()); + + return request.getRequestLine().getMethod() + "\n" + + canonicalUrl + "\n" + + timestamp + "\n" + + nonce + "\n" + + body + "\n"; + } + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/TtPayValidator.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/TtPayValidator.java new file mode 100644 index 000000000..07414a20e --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/TtPayValidator.java @@ -0,0 +1,55 @@ +package com.iformall.douyin.payv2.auth; + +import com.iformall.douyin.payv2.util.Validator; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.ContentType; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; + +@Slf4j +public class TtPayValidator implements Validator { + private Verifier verifier; + + public TtPayValidator(Verifier verifier) { + this.verifier = verifier; + } + + @Override + public final boolean validate(CloseableHttpResponse response) throws IOException { + if (!ContentType.APPLICATION_JSON.getMimeType().equals(ContentType.parse(String.valueOf(response.getFirstHeader("Content-Type").getValue())).getMimeType())) { + return true; + } + + Header sign = response.getFirstHeader("Byte-Signature"); + Header timestamp = response.getFirstHeader("Byte-Timestamp"); + Header nonce = response.getFirstHeader("Byte-Nonce-Str"); + + // todo: check timestamp + if (timestamp == null || nonce == null || sign == null) { + return false; + } + + String message = buildMessage(response); + return verifier.verify(message.getBytes("utf-8"), sign.getValue()); + } + + protected final String buildMessage(CloseableHttpResponse response) throws IOException { + String timestamp = response.getFirstHeader("Byte-Timestamp").getValue(); + String nonce = response.getFirstHeader("Byte-Nonce-Str").getValue(); + + String body = getResponseBody(response); + return timestamp + "\n" + + nonce + "\n" + + body + "\n"; + } + + protected final String getResponseBody(CloseableHttpResponse response) throws IOException { + HttpEntity entity = response.getEntity(); + + return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : ""; + } +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/Verifier.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/Verifier.java new file mode 100644 index 000000000..fc3dfa1cb --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/auth/Verifier.java @@ -0,0 +1,6 @@ +package com.iformall.douyin.payv2.auth; + +public interface Verifier { + boolean verify(byte[] message, String signature); + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/config/TtPayConfig.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/config/TtPayConfig.java new file mode 100644 index 000000000..e4b6f3fa2 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/config/TtPayConfig.java @@ -0,0 +1,179 @@ +package com.iformall.douyin.payv2.config; + +import com.github.binarywang.wxpay.v3.util.PemUtils; +import com.iformall.douyin.pay.exception.TtPayException; +import com.iformall.douyin.payv2.auth.CertificatesVerifier; +import com.iformall.douyin.payv2.auth.TtPayValidator; +import com.iformall.douyin.payv2.auth.Verifier; +import com.iformall.douyin.payv2.util.TtPayV2HttpClientBuilder; +import jodd.util.ResourcesUtil; +import lombok.Data; +import lombok.SneakyThrows; +import org.apache.commons.lang3.RegExUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.impl.client.CloseableHttpClient; +import java.io.*; +import java.net.URL; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * 支付配置 + * + * @author Binary Wang + */ +@Data +public class TtPayConfig { + private static final String DEFAULT_PAY_BASE_URL = "https://developer.toutiao.com"; + private static final String PROBLEM_MSG = "证书文件【%s】有问题,请核实!"; + private static final String NOT_FOUND_MSG = "证书文件【%s】不存在,请核实!"; + + /** + * 支付接口请求地址域名部分. + */ + private String payBaseUrl = DEFAULT_PAY_BASE_URL; + + /** + * http请求连接超时时间. + */ + private int httpConnectionTimeout = 5000; + + /** + * http请求数据读取等待时间. + */ + private int httpTimeout = 10000; + + /** + * appid. + */ + private String appId; + + /** + * apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径. + */ + private String privateKeyPath; + + /** + * api 秘钥值. + */ + private String apiKey; + + private Verifier verifier; + + @SneakyThrows + public Verifier getVerifier() { + if (verifier == null) { + //当改对象为null时,初始化api v3的请求头 + initApiV2HttpClient(); + } + return verifier; + } + + /** + * apiV2 证书序列号值 + */ + private String certSerialNo; + + private CloseableHttpClient apiV2HttpClient; + /** + * 私钥信息 + */ + private PrivateKey privateKey; + + /** + * 返回所设置的微信支付接口请求地址域名. + * + * @return 微信支付接口请求地址域名 + */ + public String getPayBaseUrl() { + if (StringUtils.isEmpty(this.payBaseUrl)) { + return DEFAULT_PAY_BASE_URL; + } + + return this.payBaseUrl; + } + + + /** + * 初始化api v2请求头 自动签名验签 + * + * @return org.apache.http.impl.client.CloseableHttpClient + * @author doger.wang + **/ + public CloseableHttpClient initApiV2HttpClient() throws TtPayException { + String privateKeyPath = this.getPrivateKeyPath(); + String apiKey = this.getApiKey(); + if (StringUtils.isBlank(privateKeyPath)) { + throw new TtPayException("请确保privateKeyPath已设置"); + } + if (StringUtils.isBlank(apiKey)) { + throw new TtPayException("请确保apiKey值已设置"); + } + InputStream keyInputStream = this.loadConfigInputStream(privateKeyPath); + try { + PrivateKey privateKey = PemUtils.loadPrivateKey(keyInputStream); + PublicKey publicKey = PemUtils.loadPublicKey(new ByteArrayInputStream(apiKey.getBytes("utf-8"))); + CertificatesVerifier certificatesVerifier = new CertificatesVerifier(publicKey); + + CloseableHttpClient httpClient = TtPayV2HttpClientBuilder.create() + .withAppId(appId, certSerialNo, privateKey) + .withValidator(new TtPayValidator(certificatesVerifier)) + .build(); + this.apiV2HttpClient = httpClient; + this.privateKey = privateKey; + this.verifier = certificatesVerifier; + + return httpClient; + } catch (Exception e) { + throw new TtPayException("v2请求构造异常!", e); + } + } + + /** + * 从配置路径 加载配置 信息(支持 classpath、本地路径、网络url) + * @param configPath 配置路径 + * @return + * @throws TtPayException + */ + private InputStream loadConfigInputStream(String configPath) throws TtPayException { + InputStream inputStream; + final String prefix = "classpath:"; + String fileHasProblemMsg = String.format(PROBLEM_MSG, configPath); + String fileNotFoundMsg = String.format(NOT_FOUND_MSG, configPath); + if (configPath.startsWith(prefix)) { + String path = RegExUtils.removeFirst(configPath, prefix); + if (!path.startsWith("/")) { + path = "/" + path; + } + try { + inputStream = ResourcesUtil.getResourceAsStream(path); + if (inputStream == null) { + throw new TtPayException(fileNotFoundMsg); + } + } catch (Exception e) { + throw new TtPayException(fileNotFoundMsg, e); + } + } else if (configPath.startsWith("http://") || configPath.startsWith("https://")) { + try { + inputStream = new URL(configPath).openStream(); + if (inputStream == null) { + throw new TtPayException(fileNotFoundMsg); + } + } catch (IOException e) { + throw new TtPayException(fileNotFoundMsg, e); + } + } else { + try { + File file = new File(configPath); + if (!file.exists()) { + throw new TtPayException(fileNotFoundMsg); + } + + inputStream = new FileInputStream(file); + } catch (IOException e) { + throw new TtPayException(fileHasProblemMsg, e); + } + } + return inputStream; + } +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/request/CallBackSettingsRequest.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/CallBackSettingsRequest.java new file mode 100644 index 000000000..354f5b552 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/CallBackSettingsRequest.java @@ -0,0 +1,35 @@ +package com.iformall.douyin.payv2.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; + +/** + * + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class CallBackSettingsRequest implements Serializable { + private static final long serialVersionUID = -1L; + /** + * https回调地址,预下单请求地址,长度限制<=512字节 + */ + @SerializedName(value = "create_order_callback") + private String createOrderCallback; + /** + * https回调地址,退款请求地址,长度限制<=512字节 + */ + @SerializedName(value = "refund_callback") + private String refundCallback; + /** + * https回调地址,长度限制<=256字节。闭环核销方案核销组件上展示的二维码所对应的跳转地址。开发者可使用这个参数设置app_id维度的订单跳转链接。 + */ + @SerializedName(value = "delivery_qrcode_redirect") + private String deliveryQrcodeRedirect; + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/request/ProfitSharingRequest.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/ProfitSharingRequest.java new file mode 100644 index 000000000..196bc02b2 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/ProfitSharingRequest.java @@ -0,0 +1,67 @@ +package com.iformall.douyin.payv2.request; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 请求分账API请求实体 + * + * @author pg + * @date 2021-6-24 + */ +@Data +@Builder(builderMethodName = "newBuilder") +@NoArgsConstructor +@AllArgsConstructor +public class ProfitSharingRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 开发者侧订单id,长度限制<64字节 + */ + @SerializedName("out_order_no") + private String outOrderNo; + + /** + * 开发者侧分账单id,分账请求的唯一标识,长度限制<64字节 + */ + @SerializedName("out_settle_no") + private String outSettleNo; + + /** + * 分账描述,长度<512字节 + */ + @SerializedName("settle_desc") + private String settleDesc; + + /** + * 其他分账方(除卖家之外的),json字符串,长度<512字节 + * - 示例,"[{\"merchant_uid\":\"分账方商户号1\",\"amount\":100}]" + * merchant_uid string 是 + * 进件商户id(除了卖家以外的其他分账方) + * amount int64 是 + * 分账金额,单位[分],amount>0 + */ + @SerializedName("settle_params") + private String settleParams; + + /** + * 透传字段,开发者自定义,<=2048字节,不支持二进制 + */ + @SerializedName("cp_extra") + private String cpExtra; + + /** + * 分账结果通知地址,必须是https类型, + * 若不填,默认使用在开发者平台-担保交易配置页面设置的回调地址 + */ + @SerializedName("notify_url") + private String notifyUrl; + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtOrderPushDeliveryRequest.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtOrderPushDeliveryRequest.java new file mode 100644 index 000000000..8affbef19 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtOrderPushDeliveryRequest.java @@ -0,0 +1,73 @@ +package com.iformall.douyin.payv2.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 thinsstar + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class TtOrderPushDeliveryRequest implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 开发者服务交易单号
+   * 
+ */ + @SerializedName(value = "out_order_no") + private String outOrderNo; + + /** + *
+   * out_order_no所关联的需要核销的商品单信息,详细结构见下表。可使用此参数指定核销整单中的特定商品单。
+   * 长度<=100
+   * 注意:只有待使用状态的商品单才能核销,已核销、正退款或已退款的将返回错误。
+   * 
+ */ + @SerializedName(value = "item_order_list") + private List itemOrderList; + + /** + * 是否整单核销。此参数设为true则将out_order_no的所有商品单核销。use_all和item_list必须有一个有效值,要么整单核销,要么指定商品单核销。 + * 注意:此参数设为true时只有out_order_no的所有商品单都为待使用状态才能成功核销,如果有商品单状态不对将导致不能整单核销,会返回错误。 + */ + @SerializedName(value = "use_all") + private Boolean useAll; + + /** + * 核销的商铺poi信息,详细结构见下表,最多1024个字节。 + * 注意:此字段为json string + * poi_info 结构,poi商品必填所有字段(强校验),小程序商品也尽量传入(不校验) + * shop_name string poi商品必填 + * 核销店铺名称,参考商铺同步接口中的店铺名称(name) + * ext_valid_shop_id string poi商品必填 + * 核销门店的外部店铺id,参考商铺同步接口中的接入方店铺id(supplier_ext_id) + * valid_poi_id_str string poi商品必填 + * 核销门店对应的抖音poi_id,参考商铺同步接口中的抖音poi id(poi_id) + */ + @SerializedName(value = "poi_info") + private String poiInfo; + + + @Data + @NoArgsConstructor + public static class ItemOrder implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName(value = "item_order_id") + private String itemOrderId; + + } +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayOrderQueryV2Request.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayOrderQueryV2Request.java new file mode 100644 index 000000000..4a6196a86 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayOrderQueryV2Request.java @@ -0,0 +1,38 @@ +package com.iformall.douyin.payv2.request; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + *
+ * 订单查询请求对象
+ * 
+ * + * @author thinsstar + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class TtPayOrderQueryV2Request implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+   * 抖音开平内部交易订单号,长度<64byte,开发者在接收到抖音开平发起的下单交易回调请求的时候,由抖音开平传递给开发者服务,开发者必须确保将该id与自己平台生成的交易单号进行唯一关联
+   * 
+ */ + @SerializedName(value = "order_id") + private String orderId; + + /** + *
+   * 开发者系统生成的订单号,与抖音开平交易单号order_id唯一关联,长度<64byte
+   * 
+ */ + @SerializedName(value = "out_order_no") + private String outOrderNo; +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayRefundQueryV2Request.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayRefundQueryV2Request.java new file mode 100644 index 000000000..5af00d555 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayRefundQueryV2Request.java @@ -0,0 +1,36 @@ +package com.iformall.douyin.payv2.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 thinsstar + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class TtPayRefundQueryV2Request implements Serializable { + private static final long serialVersionUID = -1L; + + /** + * 抖音开平内部交易退款单号,长度<= 64byte,开发者必须确保将该id与自己平台生成的交易单号进行唯一关联 + */ + @SerializedName(value = "refund_id") + private String refund_id; + + /** + * 开发者系统生成的退款单号,与抖音开平退款单号refund_id 唯一关联,长度<= 64byte + */ + @SerializedName(value = "out_refund_no") + private String outRefundNo; + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayRefundV2Request.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayRefundV2Request.java new file mode 100644 index 000000000..968e7cc77 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayRefundV2Request.java @@ -0,0 +1,107 @@ +package com.iformall.douyin.payv2.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 + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class TtPayRefundV2Request implements Serializable { + private static final long serialVersionUID = -1L; + + /** + * 是 + * 开发者侧订单号,长度 <= 64 byte + */ + @SerializedName(value = "out_order_no") + private String outOrderNo; + + /** + * 是 + * 开发者侧退款单号,长度 <= 64 byte + */ + @SerializedName(value = "out_refund_no") + private String outRefundNo; + + /** + * 否 + * 开发者自定义透传字段,不支持二进制,长度 <= 2048 byte + */ + @SerializedName(value = "cp_extra") + private String cpExtra; + + /** + * 是 + * 退款单的跳转的schema + */ + @SerializedName(value = "order_entry_schema") + private PageEntry orderEntrySchema; + + /** + * 否 + * 退款结果通知地址,必须是https类型, + * 若不填,默认使用在开发者平台-担保交易配置页面设置的退款回调地址 + */ + @SerializedName(value = "notify_url") + private String notifyUrl; + + /** + * 否(新订单发起退款必传) + * + * 需要发起退款的商品单信息(新订单发起退款必传) + */ + @SerializedName(value = "item_order_detail") + private List itemOrderDetail; + + /** + * 否(旧订单发起退款必传) + * 退款总金额 (旧订单发起退款必传) + */ + @SerializedName(value = "refund_total_amount") + private Integer refundTotalAmount; + + @Data + @NoArgsConstructor + public static class PageEntry implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 路径 没有前导的“/” + */ + @SerializedName(value = "path") + private String path; + + /** + * 参数 自定义的json结构 + */ + @SerializedName(value = "params") + private String params; + + } + + + @Data + @NoArgsConstructor + public static class OrderDetail implements Serializable { + private static final long serialVersionUID = -1L; + + /** + * 商品单号,参见前文"对于重要id字段的说明" + */ + @SerializedName(value = "item_order_id") + private String itemOrderId; + + } +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayUnifiedOrderV2Request.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayUnifiedOrderV2Request.java new file mode 100644 index 000000000..c24e50a9f --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayUnifiedOrderV2Request.java @@ -0,0 +1,456 @@ +package com.iformall.douyin.payv2.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://bytedance.feishu.cn/docx/doxcna1ALcy90AGMNdGLoALBeIH#doxcnUUemGYgUYECi8n92wMQQke
+ * 
+ * + * @author thinsstar + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class TtPayUnifiedOrderV2Request implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 是 + * 商品信息,详情见 goods_list说明 + */ + @SerializedName(value = "goods_list") + protected List goodsList; + + /** + * 是 + * 订单总价,单位(分) + */ + @SerializedName(value = "total_amount") + private Integer totalAmount; + + /** + * 否 + * 用户手机号 + */ + @SerializedName(value = "phone_num") + private String phoneNum; + + /** + * 否 + * 用户姓名 + */ + @SerializedName(value = "contact_name") + private String contactName; + + /** + * 否 + * 下单备注信息,长度 <= 2048byte + */ + @SerializedName(value = "extra") + private String extra; + + /** + * 是 + * 用户OpenID + */ + @SerializedName(value = "open_id") + private String openId; + + /** + * 否 + * 支付结果通知地址,必须是https类型。若不填,默认使用在开发者平台-担保交易配置页面设置的支付回调地址 + */ + @SerializedName(value = "pay_notify_url") + private String payNotifyUrl; + + /** + * 是 + * 开发者的单号,长度 <= 64 byte + */ + @SerializedName(value = "out_order_no") + private String outOrderNo; + + /** + * 否 + * 支付超时时间,单位秒,例如300表示300秒后过期;不传或传0会使用默认值300 + */ + @SerializedName(value = "pay_expire_seconds") + private Integer payExpireSeconds; + + /** + * 是 + * 订单详情页 + */ + @SerializedName(value = "order_entry_schema") + private PageEntry orderEntrySchema; + + /** + * 否 + * 开发者自定义透传字段,不支持二进制,长度 <= 2048 byte + */ + @SerializedName(value = "cp_extra") + private PageEntry cpExtra; + + /** + * 否 + * 折扣金额,单位(分) + */ + @SerializedName(value = "discount_amount") + private Integer discountAmount; + + /** + * 否 + * 营销算价结果信息, + */ + @SerializedName(value = "price_calculation_detail") + private PriceDetail priceCalculationDetail; + + + + @Data + @NoArgsConstructor + public static class Good implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 否 + * - 非poi商品必传 + * 商品图片链接,长度 <= 512 byte + */ + @SerializedName(value = "goods_image") + private String goodsImage; + + /** + * 否 + * - 非poi商品必传 + * 商品标题/商品名称,长度 <= 256 byte + */ + @SerializedName(value = "goods_title") + private String goodsTitle; + + /** + * 否 + * - 非poi商品必传 + * 商品标签,最多设置三个标签,例如:随时退|免预约|提前3日预约 (“|”是中文类型),详见type的合法值 + */ + @SerializedName(value = "labels") + private String labels; + + /** + * 否 + * 使用规则,如 “周一至周日可用”、“周一至周五可用”、“非节假日可用”,默认“周一至周日可用” + */ + @SerializedName(value = "date_rule") + private String dateRule; + + /** + * 否 + * - 非poi商品必传 + * 商品价格,单位(分) + */ + @SerializedName(value = "price") + private Integer price; + + /** + * 是 + * 商品数量 + */ + @SerializedName(value = "quantity") + private Integer quantity; + + /** + * 是 + * 商品id + */ + @SerializedName(value = "goods_id") + private String goodsId; + + /** + * 是 + * 商品id类别,poi商品传 1,非poi商品传 2 + */ + @SerializedName(value = "goods_id_type") + private Integer goodsIdType; + + /** + * 否 + * 商品详情页 + */ + @SerializedName(value = "goods_page") + private PageEntry goodsPage; + + /** + * 否 + * - 非poi商品必传 + * 券的有效期,注意: + * 1. 非poi商品必传,poi商品会从poi库里查询有效期信息,不会使用开发者传的数据 + * 2. 如果是非poi商品,每个goods_id都要传券的有效期信息,否则会下单失败 + */ + @SerializedName(value = "order_valid_time") + private OrderValid orderValidTime; + + /** + * 否 + * 折扣金额,单位(分) + */ + @SerializedName(value = "discount_amount") + private Integer discountAmount; + + } + + + @Data + @NoArgsConstructor + public static class PageEntry implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 路径 没有前导的“/” + */ + @SerializedName(value = "path") + private String path; + + /** + * 参数 自定义的json结构 + */ + @SerializedName(value = "params") + private String params; + + } + + @Data + @NoArgsConstructor + public static class OrderValid implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 券的有效期开始时间,单位毫秒,须大于0 + */ + @SerializedName(value = "valid_start_time") + private Long validStartTime; + + /** + * 券的有效期结束时间,单位毫秒,须大于0,且须大于valid_start_time和当前时间 + */ + @SerializedName(value = "valid_end_time") + private Long validEndTime; + + /** + * 1. 券的相对有效时间,单位毫秒,须大于0 + * 2. 与组合,至少回传一个,否则会下单失败 + * 3. 都合法优先使用组合 + * 4. 当valid_duration 有效时,券的有效期开始时间 S=订单支付完成时间,券的有效期结束时间 E=1天 + 向下按天截断(S+valid_duration) + * eg:valid_duration = 86400000ms(一天) S = 2021.1.1 6:00 E = 2021.1.3 00:00 + */ + @SerializedName(value = "valid_duration") + private Long validDuration; + + } + + @Data + @NoArgsConstructor + public static class PriceDetail implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 是 + * 算价类型,1-商户单维度,item单价格我们按分摊比例计算, + * 2-item单维度,item单价格由开发者计算 + */ + @SerializedName(value = "calculation_type") + private Integer calculationType; + + /** + * 商品算价结果 + */ + @SerializedName(value = "goods_discount_detail") + private List goodsDiscountDetail; + + /** + * 商品算价结果 + */ + @SerializedName(value = "order_discount_detail") + private OrderDiscountDetail order_discount_detail; + + /** + * 商品算价结果 + */ + @SerializedName(value = "item_discount_detail") + private List item_discount_detail; + + } + + @Data + @NoArgsConstructor + public static class GoodsDiscountDetail implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 是 + * 商品id,此处为课程id,注意这里是string类型 + */ + @SerializedName(value = "goods_id") + private String goodsId; + + /** + * 是 + * 购买数量 + */ + @SerializedName(value = "quantity") + private Integer quantity; + + /** + * 是 + * 商品总价,单位:分 + */ + @SerializedName(value = "total_amount") + private Integer totalAmount; + + /** + * 是 + * 该商品总优惠金额,该商品的实付金额=total_amount-discount_amount + */ + @SerializedName(value = "discountAmount") + private Integer discountAmount; + + /** + * 营销明细 + */ + @SerializedName(value = "marketing_detail") + private List marketingDetail; + + } + + @Data + @NoArgsConstructor + public static class OrderDiscountDetail implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 是 + * 订单维度总优惠金额,单位: 分 + */ + @SerializedName(value = "order_total_discount_amount") + private Integer orderTotalDiscountAmount; + + /** + * 是 + * 商品(sku)维度总优惠金额,单位: 分 + */ + @SerializedName(value = "goods_total_discount_amount") + private Integer goodsTotalDiscountAmount; + + /** + * 营销明细 + */ + @SerializedName(value = "marketing_detail") + private List marketingDetail; + + } + + @Data + @NoArgsConstructor + public static class ItemDiscountDetail implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 是 + * 商品id,此处为课程id,注意这里是string类型 + */ + @SerializedName(value = "goods_id") + private String goodsId; + + /** + * 是 + * 商品总价,单位:分 + */ + @SerializedName(value = "total_amount") + private Integer totalAmount; + + /** + * 是 + * 该商品总优惠金额,该商品的实付金额=total_amount-discount_amount + */ + @SerializedName(value = "discountAmount") + private Integer discountAmount; + + /** + * 营销明细 + */ + @SerializedName(value = "marketing_detail") + private List marketingDetail; + + } + + @Data + @NoArgsConstructor + public static class MarketingDetail implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 是 + * 营销id(用户身份id,优惠券id,积分id或者活动id) + */ + @SerializedName(value = "id") + private String id; + + /** + * 是 + * + * 营销类型: + * 1: 用户身份 + * 2: 优惠券 + * 3: 积分(暂不开放) + * 4: 活动 + */ + @SerializedName(value = "type") + private Integer type; + + /** + * 是 + * 该营销策略优惠金额,单位: 分 + */ + @SerializedName(value = "discountAmount") + private Integer discountAmount; + + /** + * 营销名称 + */ + @SerializedName(value = "title") + private String title; + + /** + * 营销备注 + */ + @SerializedName(value = "note") + private String note; + + /** + * 优惠范围 + */ + @SerializedName(value = "discount_range") + private Integer discountRange; + + /** + * 营销子类型 + */ + @SerializedName(value = "subtype") + private String subtype; + + /** + * 不同type含义不同,若type=3,value则为积分值 + */ + @SerializedName(value = "value") + private Integer value; + + } + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtRefundAuditMsgRequest.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtRefundAuditMsgRequest.java new file mode 100644 index 000000000..f5429273b --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtRefundAuditMsgRequest.java @@ -0,0 +1,64 @@ +package com.iformall.douyin.payv2.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 thinsstar + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class TtRefundAuditMsgRequest implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 开发者侧退款单号 + */ + @SerializedName(value = "out_refund_no") + private String outRefundNo; + + /** + * 审核状态,1-同意退款 2-不同意退款 + */ + @SerializedName(value = "refund_audit_status") + private Integer refundAuditStatus; + + /** + * 不同意退款信息(不同意退款时必填),长度 <= 512 byte + */ + @SerializedName(value = "deny_message") + private String denyMessage; + + /** + * 核销的商铺poi信息,详细结构见下表,最多1024个字节。 + * shop_name string poi商品必填 + * 核销店铺名称,参考商铺同步接口中的店铺名称(name) + * ext_valid_shop_id string poi商品必填 + * 核销门店的外部店铺id,参考商铺同步接口中的接入方店铺id(supplier_ext_id) + * valid_poi_id_str string poi商品必填 + * 核销门店对应的抖音poi_id,参考商铺同步接口中的抖音poi id(poi_id) + */ + @SerializedName(value = "poi_info") + private Boolean poiInfo; + + + @Data + @NoArgsConstructor + public static class ItemOrder implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName(value = "item_order_id") + private String itemOrderId; + + } +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/result/BaseTtPayResult.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/BaseTtPayResult.java new file mode 100644 index 000000000..3eeb545d6 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/BaseTtPayResult.java @@ -0,0 +1,57 @@ +package com.iformall.douyin.payv2.result; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.ss.formula.functions.T; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; + +/** + *
+ * 结果共用属性类.
+ * Created by Binary Wang on 2016-10-24.
+ * 
+ * + */ +@Data +@NoArgsConstructor +public abstract class BaseTtPayResult implements Serializable { + + /** + * 返回状态码. + */ + @SerializedName("err_no") + protected Integer errNo; + + public boolean isSuccess() { + return errNo.intValue() == 0; + } + /** + * 返回信息. + */ + @SerializedName("err_tips") + protected String errTips; + + @SerializedName("data") + private T data; + + /** + * Gets logger. + * + * @return the logger + */ + protected Logger getLogger() { + return LoggerFactory.getLogger(this.getClass()); + } + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/result/CreateOrderCallback.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/CreateOrderCallback.java new file mode 100644 index 000000000..fff1db2dc --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/CreateOrderCallback.java @@ -0,0 +1,200 @@ +package com.iformall.douyin.payv2.result; + +import cn.binarywang.wx.miniapp.bean.WxMaGetLiveInfo; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + * 结果通知. + * + * @author thinsstar + */ +@Data +@NoArgsConstructor +public class CreateOrderCallback implements Serializable { + + private static final long serialVersionUID = -1L; + + /** + * 是 + * 抖音开平侧生成的订单号 + */ + @SerializedName(value = "order_id") + private String orderId; + + /** + * 是 + * 商品信息,详情见 goods参数 部分 + */ + @SerializedName(value = "goods") + private List goods; + + /** + * 是 + * 订单总价格,单位[分] + * 用户实付金额=total_amount-discount + */ + @SerializedName(value = "total_amount") + private Integer totalAmount; + + /** + * 是 + * 订单折扣,单位[分] + */ + @SerializedName(value = "discount") + private Integer discount; + + /** + * 否 + * 预下单时开发者定义的透传信息 + */ + @SerializedName(value = "cp_extra") + private String cpExtra; + + /** + * 是 + * 订单创建时间,13位时间戳,毫秒 + */ + @SerializedName(value = "create_order_time") + private Long createOrderTime; + + /** + * 是 + * 用户OpenID + */ + @SerializedName(value = "open_id") + private String openId; + + /** + * 否 + * 用户手机号 + */ + @SerializedName(value = "phone_num") + private String phoneNum; + + /** + * 否 + * 联系人姓名 + */ + @SerializedName(value = "contact_name") + private String contactName; + + /** + * 是 + * 小程序id + */ + @SerializedName(value = "app_id") + private String appId; + + /** + * 否 + * 用户的 UnionID + */ + @SerializedName(value = "union_id") + private String unionId; + + /** + * 是 + * 核销类型,开平告知此订单是否可走闭环核销判断结果。 + * 0-非闭环核销,开发者自行处理券码生成及展示,通过push_delivery接口推送核销状态,1-闭环核销,开平负责生券,开发者使用核销组件展示,使用验券准备和验券接口核销。 + */ + @SerializedName(value = "delivery_type") + private Integer deliveryType; + + @Data + @NoArgsConstructor + public static class Good implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 是 + * 商品图片链接 + */ + @SerializedName(value = "img_url") + private String imgUrl; + + /** + * 是 + * 商品标题 + */ + @SerializedName(value = "title") + private String title; + + /** + * 是 + * 商品副标题 + */ + @SerializedName(value = "sub_title") + private String subTitle; + + /** + * 是 + * + * 商品标签,对应poi多门店SPU同步的sub_title,例如:随时退|免预约|提前3日预约 (“|”是中文类型) + */ + @SerializedName(value = "labels") + private String labels; + + /** + * 是 + * 券的可用时间(目前仅用于展示),例如:“周一至周五可用”、“非节假日可用” + */ + @SerializedName(value = "date_rule") + private String dateRule; + + /** + * 是 + * 商品原价,单位[分] + */ + @SerializedName(value = "origin_price") + private Integer originPrice; + + /** + * 是 + * 商品去掉折扣后的单价,单位[分] + */ + @SerializedName(value = "price") + private Integer price; + + /** + * 是 + * 购买的商品数量 + */ + @SerializedName(value = "quantity") + private Integer quantity; + + /** + * 否 + * 对应门店的Poi ID + */ + @SerializedName(value = "poi_id") + private String poiId; + + /** + * 是 + * 商品id + */ + @SerializedName(value = "goods_id") + private String goodsId; + + /** + * 是 + * item_order_id列表,id个数与quantity一致 + */ + @SerializedName(value = "item_order_id_list") + private List itemOrderIdList; + + /** + * 是 + * 商品id类别,1:poi商品,2:非poi商品 + */ + @SerializedName(value = "goods_id_type") + private Integer goodsIdType; + + } + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/result/OriginNotifyResponse.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/OriginNotifyResponse.java new file mode 100644 index 000000000..aa9e338f3 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/OriginNotifyResponse.java @@ -0,0 +1,32 @@ +package com.iformall.douyin.payv2.result; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +public class OriginNotifyResponse implements Serializable { + + private static final long serialVersionUID = 1L; + + @SerializedName(value = "msg") + private String msg; + + /** + * 枚举值: + * pre_create_order 预下单回调 + * pre_create_refund 退款审核回调 + */ + @SerializedName(value = "type") + private String type; + + /** + * 固定值:"2.0" callback版本,用于开发者识别回调参数的变更 + */ + @SerializedName(value = "version") + private String version; + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/result/ProfitSharingResult.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/ProfitSharingResult.java new file mode 100644 index 000000000..f3d47906f --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/ProfitSharingResult.java @@ -0,0 +1,112 @@ +package com.iformall.douyin.payv2.result; + + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 请求分账API返回的分账结果实体 + * + * @author pg + * @date 2021-6-24 + */ +@Data +public class ProfitSharingResult implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 返回状态码. + */ + @SerializedName("err_no") + protected Integer errNo; + + public boolean isSuccess() { + return errNo.intValue() == 0; + } + /** + * 返回信息. + */ + @SerializedName("err_tips") + protected String errTips; + + /** + * 订单的分账结果 + */ + @SerializedName("data") + private List data; + + @Data + public static class Receiver implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 开发者交易订单id,长度<=64byte,数字、ASCII字符组成 + */ + @SerializedName("out_order_no") + private String outOrderNo; + + /** + * 开发者分账单id,长度<=64byte,数字、ASCII字符组成 + */ + @SerializedName("out_settle_no") + private String outSettleNo; + + /** + * 小程序侧交易订单id,长度<=64byte,数字、ASCII字符组成 + */ + @SerializedName("order_id") + private String orderId; + + /** + * 小程序侧分账单id,长度<=64byte,数字、ASCII字符组成 + */ + @SerializedName("settle_id") + private String settleId; + + /** + * 分账金额,单位[分] + */ + @SerializedName("settle_amount") + private Integer settleAmount; + + /** + * 分账状态:INIT,PROCESSING,SUCCESS,FAIL + */ + @SerializedName("settle_status") + private String settleStatus; + + /** + * 分账详情,如“商户号ABC-分成金额(分)100” + */ + @SerializedName("settle_detail") + private String settleDetail; + + /** + * 分账时间,13位时间戳,毫秒 + */ + @SerializedName("settle_time") + private Long settleTime; + + /** + * 手续费,单位[分] + */ + @SerializedName("rake") + private Integer rake; + + /** + * 佣金,单位[分] + */ + @SerializedName("commission") + private Integer commission; + + /** + * 透传字段,开发者自定义,<=2048字节,不支持二进制数据 + */ + @SerializedName("cp_extra") + private String cpExtra; + } +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/result/RefundOrderCallback.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/RefundOrderCallback.java new file mode 100644 index 000000000..37a6ed17f --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/RefundOrderCallback.java @@ -0,0 +1,138 @@ +package com.iformall.douyin.payv2.result; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + * 结果通知. + * + * @author + */ +@Data +@NoArgsConstructor +public class RefundOrderCallback implements Serializable { + + private static final long serialVersionUID = -1L; + + /** + * 小程序的appid + */ + @SerializedName(value = "app_id") + private String appId; + + /** + * 用户openid + */ + @SerializedName(value = "open_id") + private String openId; + + /** + * 抖音开平侧退款单号 + */ + @SerializedName(value = "refund_id") + private String refundId; + + /** + * 抖音开平侧订单号 + */ + @SerializedName(value = "order_id") + private String orderId; + + /** + * 开发者侧订单号 + */ + @SerializedName(value = "out_order_no") + private String outOrderNo; + + /** + * 退款总金额,单位[分] + */ + @SerializedName(value = "refund_total_amount") + private Integer refundTotalAmount; + + /** + * 退款审核的最后期限,超过该期限无需商家审核,自动退款,13位时间戳,毫秒 + */ + @SerializedName(value = "refund_audit_deadline") + private Long refundAuditDeadline; + + /** + * 退款创建时间,13位时间戳,毫秒 + */ + @SerializedName(value = "create_refund_time") + private Long createRefundTime; + + /** + * 退款来源:1-用户发起退款 3-过期自动退 + */ + @SerializedName(value = "refund_source") + private Integer refundSource; + + /** + * cp 自定义字段,不支持二进制,长度 <= 2048byte + */ + @SerializedName(value = "cp_extra") + private String cpExtra; + + /** + * 退款原因,退款原因有多个 + */ + @SerializedName(value = "refund_reason") + private List refundReason; + + /** + *退款补充说明 + */ + @SerializedName(value = "refund_description") + private String refundDescription; + + /** + * 退款商品单信息 (新系统订单退款必填) + */ + @SerializedName(value = "refund_item_detail") + private RefundItemDetail refundItemDetail; + + + @Data + @NoArgsConstructor + public static class RefundItemDetail implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 用户需要退款多少个商品单 + */ + @SerializedName(value = "item_order_quantity") + private Integer itemOrderQuantity; + + /** + * 本次退款的商品单列表 + */ + @SerializedName(value = "item_order_detail") + private List itemOrderDetail; + + } + + @Data + @NoArgsConstructor + public static class ItemOrderDetail implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 用户需要退款多少个商品单 + */ + @SerializedName(value = "item_order_id") + private String itemOrderId; + + /** + * 用户需要退款多少个商品单 + */ + @SerializedName(value = "refund_amount") + private Integer refundAmount; + + } + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayCpsOrderQueryV2Result.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayCpsOrderQueryV2Result.java new file mode 100644 index 000000000..e990914f7 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayCpsOrderQueryV2Result.java @@ -0,0 +1,304 @@ +package com.iformall.douyin.payv2.result; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + *
+ *  查询CPS订单 返回结果对象
+ * 
+ * + * @author thinsstar + */ +@Data +@NoArgsConstructor +public class TtPayCpsOrderQueryV2Result implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 抖音开平侧订单号 + */ + @SerializedName(value = "order_id") + private String orderId; + + /** + * 开发者侧订单号,与order_id一一对应 + */ + @SerializedName(value = "out_order_no") + private String outOrderNo; + + /** + *cps订单支付相关信息 + */ + @SerializedName(value = "payment_info") + private PaymentInfo paymentInfo; + + /** + *cps相关信息 + */ + @SerializedName(value = "cps_info") + private CpsInfo cpsInfo; + + /** + *cps订单已退款记录信息 + * - refund_info字段结构(只展示已退款记录,退款失败和退款中不展示) + */ + @SerializedName(value = "refund_info") + private RefundInfo refundInfo; + + /** + *cps订单已核销记录信息 + * - delivery_info字段结构(只展示已核销记录) + */ + @SerializedName(value = "delivery_info") + private DeliveryInfo deliveryInfo; + + @Data + @NoArgsConstructor + public static class PaymentInfo implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 订单实际支付金额,单位[分] + */ + @SerializedName(value = "total_fee") + private Integer totalFee; + + /** + * 订单状态, + * INIT 初始状态 + * PROCESS 订单处理中 + * SUCCESS:成功 + * FAIL:失败 + * TIMEOUT:用户超时未支付 + */ + @SerializedName(value = "order_status") + private String orderStatus; + + /** + * 支付时间,格式:2021-12-12 00:00:00 + */ + @SerializedName(value = "pay_time") + private String payTime; + + /** + * 支付渠道枚举,1:微信,2:支付宝 + */ + @SerializedName(value = "pay_channel") + private Integer payChannel; + + /** + * 渠道支付单号,如微信的支付单号 + */ + @SerializedName(value = "channel_pay_id") + private String channelPayId; + + /** + * 卖家商户号id + */ + @SerializedName(value = "seller_uid") + private String sellerUid; + + /** + * 预下单时开发者定义的透传信息 + */ + @SerializedName(value = "cp_extra") + private String cpExtra; + + /** + * 结果描述信息,如失败原因 + */ + @SerializedName(value = "message") + private String message; + } + + @Data + @NoArgsConstructor + public static class CpsInfo implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 订单总佣金,单位分 + */ + @SerializedName(value = "total_commission_amount") + private Integer totalCommissionAmount; + + /** + * 订单状态, + * INIT 初始状态 + * PROCESS 订单处理中 + * SUCCESS:成功 + * FAIL:失败 + * TIMEOUT:用户超时未支付 + */ + @SerializedName(value = "cps_item_list") + private List cps_item_list; + } + + @Data + @NoArgsConstructor + public static class CpsItem implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 分佣比例,万分位 + */ + @SerializedName(value = "commission_rate") + private Integer commissionRate; + + /** + * 分佣用户ID + */ + @SerializedName(value = "commission_user_id") + private Long commissionUserId; + + /** + * 佣金,单位分 + */ + @SerializedName(value = "commission_amount") + private Integer commissionAmount; + + /** + * 分佣类型,1 短视频, 2 直播间 + */ + @SerializedName(value = "source_type") + private Integer sourceType; + + /** + * 短视频/直播间ID + */ + @SerializedName(value = "item_id") + private Long itemId; + + /** + * 售价,单位分 + */ + @SerializedName(value = "sell_amount") + private Integer sellAmount; + + /** + * cps订单状态,交易系统2.0为子单状态 + * 0-未支付, + * 1-已支付(待使用); + * 2-已退款(不分佣); + * 3-已核销(已分佣) + */ + @SerializedName(value = "status") + private Integer status; + + @SerializedName(value = "item_order_id") + private String itemOrderId; + + } + + @Data + @NoArgsConstructor + public static class RefundInfo implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 订单退款总金额,单位分,没有已退款记录,为0 + */ + @SerializedName(value = "total_refund_amount") + private Integer total_refund_amount; + + /** + * 分佣用户ID + */ + @SerializedName(value = "refund_items") + private List refund_items; + + } + + @Data + @NoArgsConstructor + public static class RefundItem implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 开发者系统生成的退款单号,与抖音开平退款单号refund_id 唯一关联 + */ + @SerializedName(value = "out_refund_no") + private String outRefundNo; + + /** + * 退款金额,单位[分] + */ + @SerializedName(value = "refund_amount") + private Integer refundAmount; + + /** + * 显示退款状态,默认“已退款” + */ + @SerializedName(value = "refundStatus") + private String refundStatus; + + /** + * 退款时间,13位毫秒时间戳,只有已退款才有退款时间 + */ + @SerializedName(value = "refunded_at") + private Long refunded_at; + + /** + * 抖音开平侧的商品单号,只存在交易2.0系统 + */ + @SerializedName(value = "item_order_id") + private String itemOrderId; + + } + + @Data + @NoArgsConstructor + public static class DeliveryInfo implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 订单核销总金额,单位分,没有核销记录,为0 + */ + @SerializedName(value = "total_delivery_amount") + private Integer totalDeliveryAmount; + + /** + * 分佣用户ID + */ + @SerializedName(value = "delivery_items") + private List deliveryItems; + + } + + @Data + @NoArgsConstructor + public static class DeliveryItem implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 核销金额,单位[分] + */ + @SerializedName(value = "delivery_amount") + private Integer deliveryAmount; + + /** + * 显示核销状态,默认“已核销” + */ + @SerializedName(value = "delivery_status") + private String deliveryStatus; + + /** + * 退款时间,13位毫秒时间戳,只有已退款才有退款时间 + */ + @SerializedName(value = "delivery_at") + private Long deliveryAt; + + /** + * 抖音开平侧的商品单号,只存在交易2.0系统 + */ + @SerializedName(value = "item_order_id") + private String itemOrderId; + + } + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayOrderQueryV2Result.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayOrderQueryV2Result.java new file mode 100644 index 000000000..64fd25204 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayOrderQueryV2Result.java @@ -0,0 +1,119 @@ +package com.iformall.douyin.payv2.result; + +import com.google.gson.annotations.SerializedName; +import io.swagger.models.auth.In; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + *
+ *  查询订单 返回结果对象
+ * 
+ * + * @author thinsstar + */ +@Data +@NoArgsConstructor +public class TtPayOrderQueryV2Result implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 抖音开平侧订单号 + */ + @SerializedName(value = "order_id") + private String orderId; + + /** + * 开发者侧订单号,与order_id一一对应 + */ + @SerializedName(value = "out_order_no") + private String outOrderNo; + + /** + * 已退款金额,单位[分] + */ + @SerializedName(value = "refund_amount") + private Integer refundAmount; + + /** + * 已分账金额,单位[分] + */ + @SerializedName(value = "settle_amount") + private Integer settleAmount; + + /** + * 订单实际支付金额,单位[分] + */ + @SerializedName(value = "total_fee") + private Integer totalFee; + + /** + * 订单状态, + * INIT 初始状态 + * PROCESS 订单处理中 + * SUCCESS:成功 + * FAIL:失败 + * TIMEOUT:用户超时未支付 + */ + @SerializedName(value = "order_status") + private String orderStatus; + + /** + * 支付时间,格式:2021-12-12 00:00:00 + */ + @SerializedName(value = "pay_time") + private String payTime; + + /** + * 支付渠道枚举,1:微信,2:支付宝 + */ + @SerializedName(value = "pay_channel") + private Integer payChannel; + + /** + * 渠道支付单号,如微信的支付单号 + */ + @SerializedName(value = "channel_pay_id") + private String channelPayId; + + /** + * 卖家商户号id + */ + @SerializedName(value = "seller_uid") + private String sellerUid; + + /** + * 视频id + */ + @SerializedName(value = "item_id") + private String itemId; + + /** + * 预下单时开发者定义的透传信息 + */ + @SerializedName(value = "cp_extra") + private String cpExtra; + + /** + * 结果描述信息,如失败原因 + */ + @SerializedName(value = "message") + private String message; + /** + * 担保支付单id + */ + @SerializedName(value = "payment_order_id") + private Integer paymentOrderId; + + /** + * 订单核销类型: + * 0-非闭环核销,开发者自行处理券码生成及展示,通过push_delivery接口推送核销状态。 + * 1-闭环核销,开平负责生券,开发者使用核销组件展示,使用验券准备和验券接口核销。 + */ + @SerializedName(value = "delivery_type") + private Integer deliveryType; + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayRefundQueryV2Result.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayRefundQueryV2Result.java new file mode 100644 index 000000000..5b09f1cae --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayRefundQueryV2Result.java @@ -0,0 +1,121 @@ +package com.iformall.douyin.payv2.result; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + *
+ * 退款返回结果.
+ * 
+ * + * @author + */ +@Data +@NoArgsConstructor +public class TtPayRefundQueryV2Result implements Serializable { + private static final long serialVersionUID = -1L; + + /** + * 系统退款单号,开放平台生成的退款单号 + */ + @SerializedName(value = "refund_id") + private String refundId; + + /** + * 开发者系统生成的退款单号,与抖音开平退款单号refund_id 唯一关联 + */ + @SerializedName(value = "out_refund_no") + private String outRefundNo; + + /** + * 退款金额,单位[分] + */ + @SerializedName(value = "refund_total_amount") + private Integer refundTotalAmount; + + /** + * 退款状态 + * 退款中- PROCESSING,已退款- SUCCESS,退款失败- FAIL + */ + @SerializedName(value = "refund_status") + private String refundStatus; + + /** + * 退款时间,13位毫秒时间戳,只有已退款才有退款时间 + */ + @SerializedName(value = "refund_at") + private Long refundAt; + + /** + * 退款结果信息,非商家拒绝退款导致的退款失败,可以通过该字段了解退款失败原因 + */ + @SerializedName(value = "message") + private String message; + + /** + * 系统订单信息,开放平台生成的订单号 + */ + @SerializedName(value = "order_id") + private String orderId; + + /** + * 商品单信息 (新系统订单退款才有的信息) + */ + @SerializedName(value = "item_order_detail") + private List itemOrderDetail; + + /** + * 退款审核信息(在新系统发起的退款单才有) + */ + @SerializedName(value = "merchant_audit_detail") + private MerchantAuditDetail merchantAuditDetail; + + + @Data + @NoArgsConstructor + public static class itemOrderDetail implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 抖音开平侧的商品单号 + */ + @SerializedName(value = "item_order_id") + private String itemOrderId; + + /** + * 该商品单退款金额,单位[分] + */ + @SerializedName(value = "refund_amount") + private Integer refundAmount; + + } + + @Data + @NoArgsConstructor + public static class MerchantAuditDetail implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 退款审核的最后期限,过期无需审核,自动退款,13位unix时间戳,精度:毫秒 + */ + @SerializedName(value = "refund_audit_deadline") + private Long refundAuditDeadline; + + /** + * 退款审核状态 + * INIT-初始化 TOAUDIT-待审核 AGREE-同意 DENY-拒绝 OVERTIME-超时未审核自动同意 + */ + @SerializedName(value = "audit_status") + private String auditStatus; + + /** + * 不同意退款信息(不同意退款时必填),长度 <= 512 byte + */ + @SerializedName(value = "deny_message") + private String denyMessage; + } +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayRefundV2Result.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayRefundV2Result.java new file mode 100644 index 000000000..f04760030 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayRefundV2Result.java @@ -0,0 +1,33 @@ +package com.iformall.douyin.payv2.result; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + *
+ * 申请退款返回结果.
+ * 
+ * + * @author thinsstar + */ +@Data +@NoArgsConstructor +public class TtPayRefundV2Result implements Serializable { + private static final long serialVersionUID = -1L; + + /** + * 抖音开放平台交易系统内部退款单号 + */ + @SerializedName(value = "refund_id") + private String refundId; + + /** + * 退款审核的最后期限,过期无需审核,自动退款,13位unix时间戳,精度:毫秒 + */ + @SerializedName(value = "refund_audit_deadline") + private Long refundAuditDeadline; + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayUnifiedOrderV2Result.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayUnifiedOrderV2Result.java new file mode 100644 index 000000000..dc669e2a1 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayUnifiedOrderV2Result.java @@ -0,0 +1,99 @@ +package com.iformall.douyin.payv2.result; + +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 thinsstar + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class TtPayUnifiedOrderV2Result implements Serializable { + private static final long serialVersionUID = 1L; + /** + * 是 + * 抖音开平侧生成的订单号 + */ + @SerializedName(value = "order_id") + protected String orderId; + /** + * 是 + * 开发者系统生成的订单号 + */ + @SerializedName(value = "out_order_no") + protected String outOrderNo; + /** + * 是 + * 吊起收银台的支付订单号 + */ + @SerializedName(value = "pay_order_id") + protected String payOrderId; + /** + * 是 + * 吊起收银台的token + */ + @SerializedName(value = "pay_order_token") + protected String payOrderToken; + + /** + * 是 + * 商品item_order信息,详情见 item_order_info_list部分 + */ + @SerializedName(value = "item_order_info_list") + protected List itemOrderInfoList; + + @Data + @NoArgsConstructor + public static class ItemOrderInfo implements Serializable { + private static final long serialVersionUID = 1L; + + @SerializedName(value = "goods_id") + private String goodsId; + + /** + * 是 + * item_order_id列表,id个数与下单时对应goods_id的quantity一致 + */ + @SerializedName(value = "item_order_id_list") + private List itemOrderIdList; + + /** + * 是 + * 商品item_order详细信息,详情见 item_order_detail部分 + */ + @SerializedName(value = "item_order_detail") + private List itemOrderDetail; + + } + + @Data + @NoArgsConstructor + public static class ItemOrderDetail implements Serializable { + private static final long serialVersionUID = -1L; + + /** + * item单id + */ + @SerializedName(value = "item_order_id") + private String itemOrderId; + + /** + * 商品优惠后价格 + */ + @SerializedName(value = "price") + private Integer price; + } + + + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/util/Credentials.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/util/Credentials.java new file mode 100644 index 000000000..0021a68bd --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/util/Credentials.java @@ -0,0 +1,12 @@ +package com.iformall.douyin.payv2.util; + +import org.apache.http.client.methods.HttpRequestWrapper; + +import java.io.IOException; + +public interface Credentials { + + String getSchema(); + + String getToken(HttpRequestWrapper request) throws IOException; +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/util/SignatureExec.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/util/SignatureExec.java new file mode 100644 index 000000000..9179bf670 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/util/SignatureExec.java @@ -0,0 +1,81 @@ +package com.iformall.douyin.payv2.util; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpException; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpExecutionAware; +import org.apache.http.client.methods.HttpRequestWrapper; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.entity.BufferedHttpEntity; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.execchain.ClientExecChain; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; + +public class SignatureExec implements ClientExecChain { + final ClientExecChain mainExec; + final Credentials credentials; + final Validator validator; + + SignatureExec(Credentials credentials, Validator validator, ClientExecChain mainExec) { + this.credentials = credentials; + this.validator = validator; + this.mainExec = mainExec; + } + + protected HttpEntity newRepeatableEntity(HttpEntity entity) throws IOException { + byte[] content = EntityUtils.toByteArray(entity); + ByteArrayEntity newEntity = new ByteArrayEntity(content); + newEntity.setContentEncoding(entity.getContentEncoding()); + newEntity.setContentType(entity.getContentType()); + + return newEntity; + } + + protected void convertToRepeatableResponseEntity(CloseableHttpResponse response) throws IOException { + HttpEntity entity = response.getEntity(); + if (entity != null && !entity.isRepeatable()) { + response.setEntity(newRepeatableEntity(entity)); + } + } + + protected void convertToRepeatableRequestEntity(HttpRequestWrapper request) throws IOException { + if (request instanceof HttpEntityEnclosingRequest) { + HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); + if (entity != null) { + ((HttpEntityEnclosingRequest) request).setEntity(new BufferedHttpEntity(entity)); + } + } + } + + @Override + public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request, + HttpClientContext context, HttpExecutionAware execAware) throws IOException, HttpException { + return executeWithSignature(route, request, context, execAware); + } + + private CloseableHttpResponse executeWithSignature(HttpRoute route, HttpRequestWrapper request, + HttpClientContext context, HttpExecutionAware execAware) throws IOException, HttpException { + // 添加认证信息 + request.addHeader("Byte-Authorization", + credentials.getSchema() + " " + credentials.getToken(request)); + + // 执行 + CloseableHttpResponse response = mainExec.execute(route, request, context, execAware); + + // 对成功应答验签 + StatusLine statusLine = response.getStatusLine(); + if (statusLine.getStatusCode() >= 200 && statusLine.getStatusCode() < 300) { + convertToRepeatableResponseEntity(response); + if (!validator.validate(response)) { + throw new HttpException("应答的支付签名验证失败"); + } + } + return response; + } + +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/util/TtPayV2HttpClientBuilder.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/util/TtPayV2HttpClientBuilder.java new file mode 100644 index 000000000..fde3552e7 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/util/TtPayV2HttpClientBuilder.java @@ -0,0 +1,70 @@ +package com.iformall.douyin.payv2.util; + +import com.iformall.douyin.payv2.auth.*; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.execchain.ClientExecChain; + +import java.security.PrivateKey; +import java.security.PublicKey; + +public class TtPayV2HttpClientBuilder extends HttpClientBuilder { + private Credentials credentials; + private Validator validator; + + static final String OS = System.getProperty("os.name") + "/" + System.getProperty("os.version"); + static final String VERSION = System.getProperty("java.version"); + + private TtPayV2HttpClientBuilder() { + super(); + + String userAgent = String.format( + "WechatPay-Apache-HttpClient/%s (%s) Java/%s", + getClass().getPackage().getImplementationVersion(), + OS, + VERSION == null ? "Unknown" : VERSION); + setUserAgent(userAgent); + } + + public static TtPayV2HttpClientBuilder create() { + return new TtPayV2HttpClientBuilder(); + } + + public TtPayV2HttpClientBuilder withAppId(String appId, String serialNo, PrivateKey privateKey) { + this.credentials = + new TtPayCredentials(appId, new PrivateKeySigner(serialNo, privateKey)); + return this; + } + + public TtPayV2HttpClientBuilder withCredentials(Credentials credentials) { + this.credentials = credentials; + return this; + } + + public TtPayV2HttpClientBuilder withPublicKey(PublicKey publicKey) { + this.validator = new TtPayValidator(new CertificatesVerifier(publicKey)); + return this; + } + + public TtPayV2HttpClientBuilder withValidator(Validator validator) { + this.validator = validator; + return this; + } + + @Override + public CloseableHttpClient build() { + if (credentials == null) { + throw new IllegalArgumentException("缺少身份认证信息"); + } + if (validator == null) { + throw new IllegalArgumentException("缺少签名验证信息"); + } + + return super.build(); + } + + @Override + protected ClientExecChain decorateProtocolExec(final ClientExecChain requestExecutor) { + return new SignatureExec(this.credentials, this.validator, requestExecutor); + } +} diff --git a/mallinkService/src/main/java/com/iformall/douyin/payv2/util/Validator.java b/mallinkService/src/main/java/com/iformall/douyin/payv2/util/Validator.java new file mode 100644 index 000000000..465d706f6 --- /dev/null +++ b/mallinkService/src/main/java/com/iformall/douyin/payv2/util/Validator.java @@ -0,0 +1,9 @@ +package com.iformall.douyin.payv2.util; + +import org.apache.http.client.methods.CloseableHttpResponse; + +import java.io.IOException; + +public interface Validator { + boolean validate(CloseableHttpResponse response) throws IOException; +} diff --git a/mallinkService/src/main/java/com/iformall/service/pay/service/pay/douyin/miniApp/TtMiniAppPayAdapterService.java b/mallinkService/src/main/java/com/iformall/service/pay/service/pay/douyin/miniApp/TtMiniAppPayAdapterService.java index 28c2e97e6..60de0552e 100644 --- a/mallinkService/src/main/java/com/iformall/service/pay/service/pay/douyin/miniApp/TtMiniAppPayAdapterService.java +++ b/mallinkService/src/main/java/com/iformall/service/pay/service/pay/douyin/miniApp/TtMiniAppPayAdapterService.java @@ -8,8 +8,11 @@ import com.iformall.domain.vo.WxMerchantVo; import com.iformall.domain.vo.WxOrderCouponVo; import com.iformall.douyin.miniapp.api.TtMaService; import com.iformall.douyin.pay.DouYinPayHelper; +import com.iformall.douyin.pay.TtPayService; import com.iformall.douyin.pay.preOrder.CreatePreOrderResult; import com.iformall.douyin.pay.preOrder.DouYinCreatePreOrder; +import com.iformall.douyin.payv2.request.TtPayUnifiedOrderV2Request; +import com.iformall.douyin.payv2.result.TtPayUnifiedOrderV2Result; import com.iformall.enums.*; import com.iformall.mapper.TtMerchantPoiMapper; import com.iformall.mapper.WxAppinfoMapper; @@ -21,6 +24,7 @@ import com.iformall.service.pay.service.pay.entity.PayAdapterResult; import com.iformall.service.pay.service.pay.entity.PayQueryAdapterResult; import com.iformall.utils.Constant; import com.iformall.utils.DateUtils; +import com.iformall.utils.MaUtil; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.json.WxGsonBuilder; @@ -42,9 +46,19 @@ public class TtMiniAppPayAdapterService extends BaseTtPayAdapterService implemen @Autowired private TtMerchantPoiMapper ttMerchantPoiMapper; + @Autowired + private MaUtil maUtil; + @Override public PayAdapterResult pay(WxPayAccount payAccount, WxPayOrder record,WxComposeOrder composeOrder,String productName,EnumPayShare isShare,WxAppinfo appInfo,Date currentDate, PayExtraParam params) throws Exception { - DouYinCreatePreOrder preOrder = new DouYinCreatePreOrder(); + // 新下单接口 +// TtPayService ttPayService = maUtil.getTtPayService(appInfo, payAccount); +// TtPayUnifiedOrderV2Request request = new TtPayUnifiedOrderV2Request(); +// +// +// TtPayUnifiedOrderV2Result result = ttPayService.createOrderV2(request); + + DouYinCreatePreOrder preOrder = new DouYinCreatePreOrder(); preOrder.setAppId(appInfo.getAppId()); preOrder.setSercrect(appInfo.getSecret()); preOrder.setSalt(payAccount.getApiKey()); diff --git a/mallinkService/src/main/java/com/iformall/utils/MaUtil.java b/mallinkService/src/main/java/com/iformall/utils/MaUtil.java index f38626ef4..124d3ec73 100644 --- a/mallinkService/src/main/java/com/iformall/utils/MaUtil.java +++ b/mallinkService/src/main/java/com/iformall/utils/MaUtil.java @@ -16,6 +16,9 @@ import java.util.Date; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import com.iformall.douyin.pay.TtPayService; +import com.iformall.douyin.pay.impl.TtPayServiceImpl; +import com.iformall.douyin.payv2.config.TtPayConfig; import com.iformall.douyin.web.api.TtWebService; import com.iformall.douyin.web.api.impl.TtWebServiceImpl; import com.iformall.douyin.web.config.TtWebDefaultConfigImpl; @@ -42,6 +45,7 @@ public class MaUtil { private static Map payServiceMap = new ConcurrentHashMap(); + private static Map ttpayServiceMap = new ConcurrentHashMap(); private static Map wxMaServiceMap = new ConcurrentHashMap(); private static Map wxMaAppServiceKeyMap = new ConcurrentHashMap(); @@ -119,6 +123,28 @@ public class MaUtil { return service; } + public TtPayService getTtPayService(WxAppinfo appinfo, WxPayAccount payAccount) { + TtPayConfig config = new TtPayConfig(); + config.setAppId(appinfo.getAppId()); + + config.setPrivateKeyPath(payAccount.getPrivateKeyPath());//apiclient_key.pem 平台私钥 + config.setCertSerialNo(payAccount.getCertSerialNo());//平台私钥序列号 + config.setApiKey(payAccount.getApiV3Key());//平台私钥 + + TtPayService service = ttpayServiceMap.get(appinfo.getAppId()); + if ( null == service ) { + synchronized("getTtPayServiceBlock"+appinfo.getAppId()) { + service = ttpayServiceMap.get(appinfo.getAppId()); + if (null == service) { + service = new TtPayServiceImpl(); + service.setConfig(config); + ttpayServiceMap.put(appinfo.getAppId(), service); + } + } + } + return service; + } + private TtMaService getTtappService(WxAppinfo appinfo) { WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl(); config.setAppid(appinfo.getAppId());