| @@ -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()); | |||
| @@ -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; | |||
| } | |||
| @@ -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(); | |||
| } | |||
| @@ -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 | |||
| ); | |||
| } | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| } | |||
| @@ -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()); | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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 { | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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"; | |||
| } | |||
| } | |||
| @@ -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) : ""; | |||
| } | |||
| } | |||
| @@ -0,0 +1,6 @@ | |||
| package com.iformall.douyin.payv2.auth; | |||
| public interface Verifier { | |||
| boolean verify(byte[] message, String signature); | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -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()); | |||
| @@ -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()); | |||