Просмотр исходного кода

Merge branch 'release_toaliyun_real_tt_pay_v2' of https://git.malls.iformall.com/server/formallProject into release_toaliyun_real_push_order

release_toaliyun_real
xhxu 4 лет назад
Родитель
Сommit
fc6097270b
39 измененных файлов: 3687 добавлений и 26 удалений
  1. +27
    -25
      mallinkService/src/main/java/com/iformall/douyin/pay/DouYinPayHelper.java
  2. +48
    -0
      mallinkService/src/main/java/com/iformall/douyin/pay/ProfitSharingV2Service.java
  3. +201
    -0
      mallinkService/src/main/java/com/iformall/douyin/pay/TtPayService.java
  4. +216
    -0
      mallinkService/src/main/java/com/iformall/douyin/pay/exception/TtPayException.java
  5. +240
    -0
      mallinkService/src/main/java/com/iformall/douyin/pay/impl/BaseTtPayServiceImpl.java
  6. +76
    -0
      mallinkService/src/main/java/com/iformall/douyin/pay/impl/ProfitSharingV2ServiceImpl.java
  7. +155
    -0
      mallinkService/src/main/java/com/iformall/douyin/pay/impl/TtPayServiceApacheHttpImpl.java
  8. +11
    -0
      mallinkService/src/main/java/com/iformall/douyin/pay/impl/TtPayServiceImpl.java
  9. +32
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/auth/CertificatesVerifier.java
  10. +35
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/auth/PrivateKeySigner.java
  11. +15
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/auth/Signer.java
  12. +87
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/auth/TtPayCredentials.java
  13. +55
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/auth/TtPayValidator.java
  14. +6
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/auth/Verifier.java
  15. +179
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/config/TtPayConfig.java
  16. +35
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/request/CallBackSettingsRequest.java
  17. +67
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/request/ProfitSharingRequest.java
  18. +73
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtOrderPushDeliveryRequest.java
  19. +38
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayOrderQueryV2Request.java
  20. +36
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayRefundQueryV2Request.java
  21. +107
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayRefundV2Request.java
  22. +456
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtPayUnifiedOrderV2Request.java
  23. +64
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/request/TtRefundAuditMsgRequest.java
  24. +57
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/result/BaseTtPayResult.java
  25. +200
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/result/CreateOrderCallback.java
  26. +32
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/result/OriginNotifyResponse.java
  27. +112
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/result/ProfitSharingResult.java
  28. +138
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/result/RefundOrderCallback.java
  29. +304
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayCpsOrderQueryV2Result.java
  30. +119
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayOrderQueryV2Result.java
  31. +121
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayRefundQueryV2Result.java
  32. +33
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayRefundV2Result.java
  33. +99
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/result/TtPayUnifiedOrderV2Result.java
  34. +12
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/util/Credentials.java
  35. +81
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/util/SignatureExec.java
  36. +70
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/util/TtPayV2HttpClientBuilder.java
  37. +9
    -0
      mallinkService/src/main/java/com/iformall/douyin/payv2/util/Validator.java
  38. +15
    -1
      mallinkService/src/main/java/com/iformall/service/pay/service/pay/douyin/miniApp/TtMiniAppPayAdapterService.java
  39. +26
    -0
      mallinkService/src/main/java/com/iformall/utils/MaUtil.java

+ 27
- 25
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<String,Object> 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());


+ 48
- 0
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 {

/**
* <pre>
* 请求分账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;

/**
* <pre>
* 查询分账接口
* 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;

}

+ 201
- 0
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;


/**
* <pre>
* 支付相关接口.
* Created by Binary Wang on 2016/7/28.
* </pre>
*
*/
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;



/**
* <pre>
* 5. 订单信息接口
*/
TtPayOrderQueryV2Result queryOrderV2(String orderId, String outOrderNo) throws TtPayException;

/**
* <pre>
* 5. 订单信息接口
*/
TtPayOrderQueryV2Result queryOrderV2(TtPayOrderQueryV2Request request) throws TtPayException;

/**
* <pre>
* 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);

/**
* <pre>
* 开发者可用该接口替用户发起退款,开发者发起的退款流程则不会有退款申请回调。
* - 过期自动退说明
* - 过期自动退的退款被拒绝退款后,不会再次发起。
* - 在交易系统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;

/**
* <pre>
* 查询退款
*
*/
TtPayRefundQueryV2Result refundQueryV2(TtPayRefundQueryV2Request request) throws TtPayException;

/**
* 获取微信请求数据,方便接口调用方获取处理.
*
* @return the wx api data
*/
WxPayApiData getWxApiData();

}

+ 216
- 0
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;

/**
* <pre>
* 支付异常结果类
* Created by Binary Wang on 2017-6-6.
* </pre>
*
* @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
);
}
}
}

+ 240
- 0
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.*;

/**
* <pre>
* 支付接口请求抽象实现类
* Created by Binary Wang on 2017-7-8.
* </pre>
*
* @author <a href="https://github.com/binarywang">Binary Wang</a>
*/
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<WxPayApiData> 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<CallBackSettingsRequest> baseTtPayResult = GSON.fromJson(response,
new TypeReference<BaseTtPayResult<CallBackSettingsRequest>>(){}.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<TtPayUnifiedOrderV2Result> baseTtPayResult = GSON.fromJson(response,
new TypeReference<BaseTtPayResult<TtPayUnifiedOrderV2Result>>(){}.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<TtPayOrderQueryV2Result> baseTtPayResult = GSON.fromJson(response,
new TypeReference<BaseTtPayResult<TtPayOrderQueryV2Result>>(){}.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<TtPayCpsOrderQueryV2Result> baseTtPayResult = GSON.fromJson(response,
new TypeReference<BaseTtPayResult<TtPayCpsOrderQueryV2Result>>(){}.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<TtPayRefundV2Result> baseTtPayResult = GSON.fromJson(response,
new TypeReference<BaseTtPayResult<TtPayRefundV2Result>>(){}.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<TtPayRefundQueryV2Result> baseTtPayResult = GSON.fromJson(response,
new TypeReference<BaseTtPayResult<TtPayRefundQueryV2Result>>(){}.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();
}
}

}

+ 76
- 0
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());
}
}

+ 155
- 0
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;


/**
* <pre>
* 支付请求实现类,apache httpclient实现.
* Created by Binary Wang on 2016/7/28.
* </pre>
*
*/
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;
}

}

+ 11
- 0
mallinkService/src/main/java/com/iformall/douyin/pay/impl/TtPayServiceImpl.java Просмотреть файл

@@ -0,0 +1,11 @@
package com.iformall.douyin.pay.impl;

/**
* <pre>
* 支付接口请求实现类,默认使用Apache HttpClient实现
* Created by Binary Wang on 2017-7-8.
* </pre>
*
*/
public class TtPayServiceImpl extends TtPayServiceApacheHttpImpl {
}

+ 32
- 0
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);
}
}

}

+ 35
- 0
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);
}
}
}

+ 15
- 0
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;
}
}
}

+ 87
- 0
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";
}

}

+ 55
- 0
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) : "";
}
}

+ 6
- 0
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);

}

+ 179
- 0
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;
}
}

+ 35
- 0
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;

}

+ 67
- 0
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;

}

+ 73
- 0
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;

/**
* <pre>
* 订单推送核销状态请求对象
* </pre>
*
* @author thinsstar
*/
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class TtOrderPushDeliveryRequest implements Serializable {
private static final long serialVersionUID = 1L;

/**
* <pre>
* 开发者服务交易单号
* </pre>
*/
@SerializedName(value = "out_order_no")
private String outOrderNo;

/**
* <pre>
* out_order_no所关联的需要核销的商品单信息,详细结构见下表。可使用此参数指定核销整单中的特定商品单。
* 长度<=100
* 注意:只有待使用状态的商品单才能核销,已核销、正退款或已退款的将返回错误。
* </pre>
*/
@SerializedName(value = "item_order_list")
private List<ItemOrder> 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;

}
}

+ 38
- 0
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;

/**
* <pre>
* 订单查询请求对象
* </pre>
*
* @author thinsstar
*/
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class TtPayOrderQueryV2Request implements Serializable {
private static final long serialVersionUID = 1L;

/**
* <pre>
* 抖音开平内部交易订单号,长度<64byte,开发者在接收到抖音开平发起的下单交易回调请求的时候,由抖音开平传递给开发者服务,开发者必须确保将该id与自己平台生成的交易单号进行唯一关联
* </pre>
*/
@SerializedName(value = "order_id")
private String orderId;

/**
* <pre>
* 开发者系统生成的订单号,与抖音开平交易单号order_id唯一关联,长度<64byte
* </pre>
*/
@SerializedName(value = "out_order_no")
private String outOrderNo;
}

+ 36
- 0
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;

/**
* <pre>
* 微信支付-申请退款请求参数
* </pre>
*
* @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;

}

+ 107
- 0
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;

/**
* <pre>
* 申请退款请求参数
* </pre>
*
* @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<OrderDetail> 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;

}
}

+ 456
- 0
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;

/**
* <pre>
* 统一下单请求参数对象.
* 参考文档:https://bytedance.feishu.cn/docx/doxcna1ALcy90AGMNdGLoALBeIH#doxcnUUemGYgUYECi8n92wMQQke
* </pre>
*
* @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<Good> 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. 与<valid_start_time,valid_end_time>组合,至少回传一个,否则会下单失败
* 3. 都合法优先使用<valid_start_time,valid_end_time>组合
* 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> goodsDiscountDetail;

/**
* 商品算价结果
*/
@SerializedName(value = "order_discount_detail")
private OrderDiscountDetail order_discount_detail;

/**
* 商品算价结果
*/
@SerializedName(value = "item_discount_detail")
private List<ItemDiscountDetail> 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> 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> 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> 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;

}

}

+ 64
- 0
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;

/**
* <pre>
* 订单推送核销状态请求对象
* </pre>
*
* @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;

}
}

+ 57
- 0
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;

/**
* <pre>
* 结果共用属性类.
* Created by Binary Wang on 2016-10-24.
* </pre>
*
*/
@Data
@NoArgsConstructor
public abstract class BaseTtPayResult<T> 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);
}

}

+ 200
- 0
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<Good> 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<String> itemOrderIdList;

/**
* 是
* 商品id类别,1:poi商品,2:非poi商品
*/
@SerializedName(value = "goods_id_type")
private Integer goodsIdType;

}

}

+ 32
- 0
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;

}

+ 112
- 0
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<Receiver> 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;
}
}

+ 138
- 0
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<String> 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> 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;

}

}

+ 304
- 0
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;

/**
* <pre>
* 查询CPS订单 返回结果对象
* </pre>
*
* @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<CpsItem> 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<RefundItem> 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<DeliveryItem> 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;

}

}

+ 119
- 0
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;

/**
* <pre>
* 查询订单 返回结果对象
* </pre>
*
* @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;

}

+ 121
- 0
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;

/**
* <pre>
* 退款返回结果.
* </pre>
*
* @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> 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;
}
}

+ 33
- 0
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;

/**
* <pre>
* 申请退款返回结果.
* </pre>
*
* @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;

}

+ 99
- 0
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;

/**
* <pre>
* 统一下单请求参数对象.
* </pre>
*
* @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<ItemOrderInfo> 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<String> itemOrderIdList;

/**
* 是
* 商品item_order详细信息,详情见 item_order_detail部分
*/
@SerializedName(value = "item_order_detail")
private List<String> 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;
}



}

+ 12
- 0
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;
}

+ 81
- 0
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;
}

}

+ 70
- 0
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);
}
}

+ 9
- 0
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;
}

+ 15
- 1
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());


+ 26
- 0
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<String,WxPayService> payServiceMap = new ConcurrentHashMap<String,WxPayService>();
private static Map<String,TtPayService> ttpayServiceMap = new ConcurrentHashMap<String,TtPayService>();
private static Map<String,WxMaService> wxMaServiceMap = new ConcurrentHashMap<String,WxMaService>();
private static Map<String,String> wxMaAppServiceKeyMap = new ConcurrentHashMap<String,String>();
@@ -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());


Загрузка…
Отмена
Сохранить