35 changed files with 1476 additions and 65 deletions
@ -0,0 +1,69 @@ |
|||
package com.bnyer.common.core.domain; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.IdType; |
|||
import com.baomidou.mybatisplus.annotation.TableField; |
|||
import com.baomidou.mybatisplus.annotation.TableId; |
|||
import com.baomidou.mybatisplus.annotation.TableName; |
|||
import io.swagger.annotations.ApiModel; |
|||
import io.swagger.annotations.ApiModelProperty; |
|||
import java.util.Date; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Getter; |
|||
import lombok.NoArgsConstructor; |
|||
import lombok.Setter; |
|||
import lombok.ToString; |
|||
|
|||
/** |
|||
* @author :WXC |
|||
* @description : |
|||
*/ |
|||
/** |
|||
* 抖音支付配置表 |
|||
*/ |
|||
@ApiModel(value="com-bnyer-common-core-domain-PayDypayConfig") |
|||
@Getter |
|||
@Setter |
|||
@ToString |
|||
@AllArgsConstructor |
|||
@NoArgsConstructor |
|||
@TableName(value = "pay_dypay_config") |
|||
public class DypayConfig extends BaseDomain { |
|||
/** |
|||
* appid |
|||
*/ |
|||
@TableField(value = "appid") |
|||
@ApiModelProperty(value="appid") |
|||
private String appid; |
|||
|
|||
/** |
|||
* 秘钥 |
|||
*/ |
|||
@TableField(value = "salt") |
|||
@ApiModelProperty(value="秘钥") |
|||
private String salt; |
|||
|
|||
/** |
|||
* 令牌 |
|||
*/ |
|||
@TableField(value = "token") |
|||
@ApiModelProperty(value="令牌") |
|||
private String token; |
|||
|
|||
/** |
|||
* 回调地址url |
|||
*/ |
|||
@TableField(value = "backurl") |
|||
@ApiModelProperty(value="回调地址url") |
|||
private String backurl; |
|||
|
|||
/** |
|||
* 帐号状态(0正常 1停用) |
|||
*/ |
|||
@TableField(value = "status") |
|||
@ApiModelProperty(value="帐号状态(0正常 1停用)") |
|||
private String status; |
|||
|
|||
@TableField(value = "remark") |
|||
@ApiModelProperty(value="") |
|||
private String remark; |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
package com.bnyer.common.core.domain; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.IdType; |
|||
import com.baomidou.mybatisplus.annotation.TableField; |
|||
import com.baomidou.mybatisplus.annotation.TableId; |
|||
import com.baomidou.mybatisplus.annotation.TableName; |
|||
import io.swagger.annotations.ApiModel; |
|||
import io.swagger.annotations.ApiModelProperty; |
|||
import java.util.Date; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Getter; |
|||
import lombok.NoArgsConstructor; |
|||
import lombok.Setter; |
|||
import lombok.ToString; |
|||
|
|||
/** |
|||
* @author :WXC |
|||
* @description : |
|||
*/ |
|||
/** |
|||
* 快手支付配置表 |
|||
*/ |
|||
@ApiModel(value="com-bnyer-common-core-domain-PayKspayConfig") |
|||
@Getter |
|||
@Setter |
|||
@ToString |
|||
@AllArgsConstructor |
|||
@NoArgsConstructor |
|||
@TableName(value = "pay_kspay_config") |
|||
public class KspayConfig extends BaseDomain { |
|||
/** |
|||
* appid |
|||
*/ |
|||
@TableField(value = "appid") |
|||
@ApiModelProperty(value="appid") |
|||
private String appid; |
|||
|
|||
/** |
|||
* 秘钥 |
|||
*/ |
|||
@TableField(value = "secret") |
|||
@ApiModelProperty(value="秘钥") |
|||
private String secret; |
|||
|
|||
/** |
|||
* 回调地址url |
|||
*/ |
|||
@TableField(value = "backurl") |
|||
@ApiModelProperty(value="回调地址url") |
|||
private String backurl; |
|||
|
|||
/** |
|||
* 帐号状态(0正常 1停用) |
|||
*/ |
|||
@TableField(value = "status") |
|||
@ApiModelProperty(value="帐号状态(0正常 1停用)") |
|||
private String status; |
|||
|
|||
@TableField(value = "remark") |
|||
@ApiModelProperty(value="") |
|||
private String remark; |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
package com.bnyer.pay.config; |
|||
|
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.http.client.ClientHttpRequestFactory; |
|||
import org.springframework.http.client.SimpleClientHttpRequestFactory; |
|||
import org.springframework.web.client.RestTemplate; |
|||
|
|||
/** |
|||
* @author chengkun |
|||
* @date 2022/4/28 17:27 |
|||
*/ |
|||
@Configuration |
|||
public class RestTemplateConfiguration { |
|||
@Bean |
|||
public RestTemplate restTemplate(ClientHttpRequestFactory factory) { |
|||
return new RestTemplate(factory); |
|||
} |
|||
|
|||
@Bean |
|||
public ClientHttpRequestFactory simpleClientHttpRequestFactory(){ |
|||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); |
|||
factory.setConnectTimeout(15000); |
|||
factory.setReadTimeout(10000); |
|||
return factory; |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
package com.bnyer.pay.constant; |
|||
|
|||
/** |
|||
* @author :WXC |
|||
* @Date :2023/04/23 |
|||
* @description :抖音支付常量池 |
|||
*/ |
|||
public class DYPayConstants { |
|||
/** |
|||
* 登陆 |
|||
*/ |
|||
public static final String CODE_2_SESSION = "https://developer.toutiao.com/api/apps/v2/jscode2session"; |
|||
/** |
|||
* 生成预支付单 |
|||
*/ |
|||
public static final String CREATE_ORDER = "https://developer.toutiao.com/api/apps/ecpay/v1/create_order"; |
|||
/** |
|||
* 分账 |
|||
*/ |
|||
public static final String SETTLE = "https://developer.toutiao.com/api/apps/ecpay/v1/settle"; |
|||
|
|||
/** |
|||
* 退款 |
|||
*/ |
|||
public static final String CREATE_REFUND = "https://developer.toutiao.com/api/apps/ecpay/v1/create_refund"; |
|||
} |
|||
|
|||
@ -0,0 +1,50 @@ |
|||
package com.bnyer.pay.constant; |
|||
|
|||
/** |
|||
* @author :WXC |
|||
* @Date :2023/04/23 |
|||
* @description : 快手支付常量池 |
|||
*/ |
|||
public class KSPayConstants { |
|||
/** |
|||
* https://mp.kuaishou.com/docs/develop/server/code2Session.html
|
|||
* code获取openId sessionKey |
|||
*/ |
|||
public static final String CODE_2_SESSION = "https://open.kuaishou.com/oauth2/mp/code2session"; |
|||
|
|||
/** |
|||
* https://mp.kuaishou.com/docs/develop/server/getAccessToken.html
|
|||
* 接口调用凭证 |
|||
*/ |
|||
public static final String GET_ACCESS_TOKEN = "https://open.kuaishou.com/oauth2/access_token"; |
|||
|
|||
/** |
|||
* https://mp.kuaishou.com/docs/develop/server/epay/interfaceDefinition.html
|
|||
* 预下单接口 |
|||
*/ |
|||
public static final String CREATE_ORDER = "https://open.kuaishou.com/openapi/mp/developer/epay/create_order"; |
|||
|
|||
/** |
|||
* 退款 |
|||
* https://mp.kuaishou.com/docs/develop/server/epay/interfaceDefinition.html#_1-3%E6%94%AF%E4%BB%98%E5%9B%9E%E8%B0%83
|
|||
*/ |
|||
public static final String APPLY_REFUND = "https://open.kuaishou.com/openapi/mp/developer/epay/apply_refund"; |
|||
|
|||
/** |
|||
* 结算 |
|||
* https://mp.kuaishou.com/docs/develop/server/epay/interfaceDefinition.html#_1-3%E6%94%AF%E4%BB%98%E5%9B%9E%E8%B0%83
|
|||
*/ |
|||
public static final String APPLY_SETTLE = "https://open.kuaishou.com/openapi/mp/developer/epay/settle"; |
|||
|
|||
/** |
|||
* 超时时间 |
|||
*/ |
|||
public static final int expireTime = 1800; |
|||
|
|||
|
|||
/** |
|||
* https://mp.kuaishou.com/docs/operate/platformAgreement/epayServiceCharge.html
|
|||
* 商品类目编号 |
|||
*/ |
|||
public static final int GOODS_TYPE_VIP = 1273; |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
package com.bnyer.pay.controller; |
|||
|
|||
import com.alibaba.fastjson.JSON; |
|||
import com.alibaba.fastjson.JSONObject; |
|||
import com.bnyer.common.core.enums.EnumPayType; |
|||
import com.bnyer.pay.design.factory.PayFactory; |
|||
import com.bnyer.pay.design.strategy.IPayStrategy; |
|||
import io.swagger.annotations.Api; |
|||
import io.swagger.annotations.ApiOperation; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.ResponseBody; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
|
|||
/** |
|||
* @author :WXC |
|||
* @Date :2023/04/24 |
|||
* @description : |
|||
*/ |
|||
@Api(value = "抖音支付相关接口",tags = "抖音支付相关接口") |
|||
@RestController |
|||
@Slf4j |
|||
public class DYPayController { |
|||
|
|||
/** |
|||
* 抖音支付结果通知 |
|||
*/ |
|||
@ApiOperation(value = "抖音支付结果通知") |
|||
@ResponseBody |
|||
@PostMapping("/dypayBack") |
|||
public JSONObject dyPayNotify(@RequestBody JSONObject object, HttpServletRequest request) { |
|||
log.info("抖音支付异步通知开始==============》{}", object); |
|||
IPayStrategy payStrategy = PayFactory.getInstance().getConcreteStrategy(EnumPayType.DY_PAY.getType()); |
|||
String payNotify = payStrategy.parsePayNotify(JSON.toJSONString(object)); |
|||
return JSON.parseObject(payNotify); |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
package com.bnyer.pay.controller; |
|||
|
|||
import com.alibaba.fastjson.JSON; |
|||
import com.alibaba.fastjson.JSONObject; |
|||
import com.alibaba.fastjson.serializer.SerializerFeature; |
|||
import com.bnyer.common.core.enums.EnumPayType; |
|||
import com.bnyer.pay.design.factory.PayFactory; |
|||
import com.bnyer.pay.design.strategy.IPayStrategy; |
|||
import io.swagger.annotations.Api; |
|||
import io.swagger.annotations.ApiOperation; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.codec.digest.DigestUtils; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.ResponseBody; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
|
|||
/** |
|||
* @author :WXC |
|||
* @Date :2023/04/24 |
|||
* @description : |
|||
*/ |
|||
@Api(value = "快手支付相关接口",tags = "快手支付相关接口") |
|||
@RestController |
|||
@Slf4j |
|||
public class KSPayController { |
|||
|
|||
/** |
|||
* 快手支付结果通知 |
|||
*/ |
|||
@ApiOperation(value = "快手支付结果通知") |
|||
@ResponseBody |
|||
@PostMapping("/kspayBack") |
|||
public JSONObject ksPayNotify(@RequestBody JSONObject object, HttpServletRequest request) { |
|||
log.info("快手微信支付异步通知开始==============》{}", object); |
|||
log.info("快手微信支付kwaisign==============》{}", request.getHeader("kwaisign")); |
|||
String kwaisign = request.getHeader("kwaisign"); |
|||
JSONObject jsonObject = new JSONObject(); |
|||
jsonObject.put("data",object); |
|||
jsonObject.put("sign",kwaisign); |
|||
String string = JSONObject.toJSONString(jsonObject); |
|||
IPayStrategy payStrategy = PayFactory.getInstance().getConcreteStrategy(EnumPayType.KS_PAY.getType()); |
|||
String payNotify = payStrategy.parsePayNotify(string); |
|||
return JSON.parseObject(payNotify); |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,215 @@ |
|||
package com.bnyer.pay.design.strategy; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.hutool.core.date.DateUtil; |
|||
import com.alibaba.fastjson.JSON; |
|||
import com.alibaba.fastjson.JSONObject; |
|||
import com.alibaba.fastjson.serializer.SerializerFeature; |
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.bnyer.common.core.context.SecurityContextHolder; |
|||
import com.bnyer.common.core.domain.DypayConfig; |
|||
import com.bnyer.common.core.domain.KspayConfig; |
|||
import com.bnyer.common.core.enums.EnumPayType; |
|||
import com.bnyer.common.core.enums.ResponseEnum; |
|||
import com.bnyer.common.core.exception.ServiceException; |
|||
import com.bnyer.common.core.utils.StringUtils; |
|||
import com.bnyer.pay.constant.DYPayConstants; |
|||
import com.bnyer.pay.constant.KSPayConstants; |
|||
import com.bnyer.pay.dto.EditPayInfoNotifyDto; |
|||
import com.bnyer.pay.dto.PayNotifyCheckDto; |
|||
import com.bnyer.pay.dto.UnifiedOrderDto; |
|||
import com.bnyer.pay.enums.EnumPayConfigStatus; |
|||
import com.bnyer.pay.mapper.DypayConfigMapper; |
|||
import com.bnyer.pay.mapper.KspayConfigMapper; |
|||
import com.bnyer.pay.service.PayInfoService; |
|||
import com.bnyer.pay.utils.DYPayUtil; |
|||
import com.bnyer.pay.utils.KSPayUtil; |
|||
import com.bnyer.pay.utils.PayRestTemplateUtil; |
|||
import com.bnyer.pay.vo.PayInOrderVo; |
|||
import com.github.binarywang.wxpay.bean.request.BaseWxPayRequest; |
|||
import com.github.binarywang.wxpay.bean.result.BaseWxPayResult; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.codec.digest.DigestUtils; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.math.BigDecimal; |
|||
import java.util.Date; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.TreeMap; |
|||
|
|||
/** |
|||
* @author :WXC |
|||
* @Date :2023/04/23 |
|||
* @description : |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
public class DYPayStrategy extends AbstractPayStrategy{ |
|||
|
|||
@Autowired |
|||
private DYPayUtil dyPayUtil; |
|||
|
|||
@Autowired |
|||
private PayRestTemplateUtil payRestTemplateUtil; |
|||
|
|||
@Autowired |
|||
private PayInfoService payInfoService; |
|||
|
|||
@Autowired |
|||
private DypayConfigMapper dypayConfigMapper; |
|||
|
|||
@Override |
|||
public PayInOrderVo unifiedOrder(UnifiedOrderDto dto) { |
|||
try { |
|||
List<DypayConfig> dypayConfigList = dypayConfigMapper.selectList(new LambdaQueryWrapper<DypayConfig>().eq(DypayConfig::getStatus, EnumPayConfigStatus.ENABLE.getCode())); |
|||
if (CollUtil.isEmpty(dypayConfigList)){ |
|||
throw new ServiceException(ResponseEnum.PAY_CONFIG_ERROR); |
|||
} |
|||
DypayConfig dypayConfig = dypayConfigList.get(0); |
|||
String appId = dypayConfig.getAppid(); |
|||
String backurl = dypayConfig.getBackurl(); |
|||
String salt = dypayConfig.getSalt(); |
|||
//加签验签的参数需要排序
|
|||
Map<String, Object> params = new TreeMap<>(); |
|||
//小程序APPID
|
|||
params.put("app_id",appId); |
|||
//开发者侧的订单号。需保证同一小程序下不可重复
|
|||
params.put("out_order_no", dto.getPayId()); |
|||
//支付价格。单位为[分],取值范围:[1,10000000000] 100元 = 100*100 分
|
|||
params.put("total_amount", BaseWxPayRequest.yuanToFen(dto.getPayAmount())); |
|||
//商品描述。
|
|||
params.put("subject", dto.getGoodsSubject()); |
|||
//商品详情
|
|||
params.put("body", dto.getGoodsDesc()); |
|||
//订单过期时间(秒) 5min-2day
|
|||
params.put("valid_time", 1800); |
|||
//开发者自定义字段,回调原样回传。超过最大长度会被截断
|
|||
params.put("cp_extra", "xx平台充值"); |
|||
//通知地址
|
|||
params.put("notify_url", backurl); |
|||
//签名,详见https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/server/ecpay/TE
|
|||
String sign = dyPayUtil.getSign(params,salt); |
|||
params.put("sign", sign); |
|||
|
|||
//以JSON格式拼好以下参数发送请求
|
|||
JSONObject payJson = new JSONObject(); |
|||
payJson.put("app_id", appId); |
|||
payJson.put("out_order_no", dto.getPayId()); |
|||
//此处需要传入一个数值类型,string会报错。。
|
|||
payJson.put("total_amount", BaseWxPayRequest.yuanToFen(dto.getPayAmount())); |
|||
payJson.put("subject",dto.getGoodsSubject()); |
|||
payJson.put("body", dto.getGoodsDesc()); |
|||
payJson.put("valid_time", 1800); |
|||
payJson.put("sign", sign); |
|||
//payJson.put("cp_extra", "开发者自定义字段");
|
|||
payJson.put("notify_url",backurl); |
|||
log.info("请求参数{}", payJson); |
|||
//预下单接口
|
|||
String result = payRestTemplateUtil.dyPostRequest(payJson, DYPayConstants.CREATE_ORDER); |
|||
log.info("=================================="); |
|||
log.info("抖音预下单result{}", result); |
|||
log.info("=================================="); |
|||
if (!"".equals(result)) { |
|||
JSONObject jsonObject = JSONObject.parseObject(result); |
|||
String errNo = jsonObject.getString("err_no"); |
|||
if ("0".equals(errNo)) { |
|||
JSONObject data = jsonObject.getJSONObject("data"); |
|||
String orderId = data.getString("order_id"); |
|||
String orderToken = data.getString("order_token"); |
|||
if (null != orderToken && null != orderId) { |
|||
//保存预下单信息
|
|||
PayInOrderVo payInOrderVo = new PayInOrderVo(); |
|||
//把order_no和order_info_token返回前端用于调起收银台
|
|||
payInOrderVo.setPayId(dto.getPayId()); |
|||
payInOrderVo.setPayOrderId(orderId); |
|||
payInOrderVo.setOrderToken(orderToken); |
|||
return payInOrderVo; |
|||
} else { |
|||
return null; |
|||
} |
|||
} else { |
|||
log.error("抖音支付:统一下单接口调用失败,payId:{},error{}", dto.getPayId(), jsonObject.getString("error_msg")); |
|||
throw new ServiceException(ResponseEnum.PAY_FAILS); |
|||
} |
|||
} else { |
|||
log.error("抖音支付:统一下单接口调用失败,payId:{},error{}", dto.getPayId(), "请求结果返回为空"); |
|||
throw new ServiceException(ResponseEnum.PAY_FAILS); |
|||
} |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
log.error("抖音支付:支付异常,payId:{},error{}", dto.getPayId(), e.getMessage()); |
|||
throw new ServiceException(ResponseEnum.PAY_FAILS); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public String parsePayNotify(String params) { |
|||
List<DypayConfig> dypayConfigList = dypayConfigMapper.selectList(new LambdaQueryWrapper<DypayConfig>() |
|||
.eq(DypayConfig::getStatus, EnumPayConfigStatus.ENABLE.getCode())); |
|||
if (CollUtil.isEmpty(dypayConfigList)){ |
|||
throw new ServiceException(ResponseEnum.PAY_CONFIG_ERROR); |
|||
} |
|||
DypayConfig dypayConfig = dypayConfigList.get(0); |
|||
String appId = dypayConfig.getAppid(); |
|||
String token = dypayConfig.getToken(); |
|||
JSONObject object = JSONObject.parseObject(params); |
|||
//随机数
|
|||
String nonce = object.getString("nonce"); |
|||
//时间戳
|
|||
Integer timestamp = object.getInteger("timestamp"); |
|||
//签名
|
|||
String msgSignature = object.getString("msg_signature"); |
|||
//订单信息的json字符串
|
|||
String message = object.getString("msg"); |
|||
JSONObject msgJsonObj = JSON.parseObject(message); |
|||
//校验回调签名
|
|||
String signMessage = dyPayUtil.getCallbackSignature(timestamp, nonce, message,token); |
|||
//签名校验
|
|||
if (msgSignature.equals(signMessage)) { |
|||
//固定值SUCCESS
|
|||
String status = msgJsonObj.getString("status"); |
|||
//开发者侧的订单号
|
|||
String outOrderNo = msgJsonObj.getString("cp_orderno"); |
|||
//支付渠道: 1-微信支付,2-支付宝支付,10-抖音支付
|
|||
String payChannel = msgJsonObj.getString("way"); |
|||
//支付金额,单位为分
|
|||
Integer totalAmount = msgJsonObj.getInteger("total_amount"); |
|||
//支付渠道侧PC单号,支付页面可见(微信支付宝侧的订单号)
|
|||
String paymentOrderNo = msgJsonObj.getString("payment_order_no"); |
|||
//抖音侧订单号
|
|||
String dyOrderId = msgJsonObj.getString("order_id"); |
|||
//这里无论回调失败还是成功,都需要都各个业务层去处理相关逻辑
|
|||
if("success".equals(status)){ |
|||
//处理业务
|
|||
PayNotifyCheckDto payNotifyCheckDto = new PayNotifyCheckDto(); |
|||
payNotifyCheckDto.setPayType(EnumPayType.DY_PAY); |
|||
payNotifyCheckDto.setPayAmount(BaseWxPayResult.fenToYuan(totalAmount)); |
|||
payNotifyCheckDto.setPayId(outOrderNo); |
|||
payNotifyCheckDto.setMsg("success"); |
|||
String notifyCheck = super.payNotifyCheck(payNotifyCheckDto); |
|||
if (StringUtils.isNotBlank(notifyCheck)){ |
|||
return notifyCheck; |
|||
} |
|||
log.info("抖音支付回调,交易正常:封装参数修改内部支付单信息"); |
|||
EditPayInfoNotifyDto editPayInfoNotifyDto = new EditPayInfoNotifyDto(); |
|||
editPayInfoNotifyDto.setPayAmount(BaseWxPayResult.fenToYuan(totalAmount)); |
|||
editPayInfoNotifyDto.setPayTime(DateUtil.formatDateTime(new Date())); |
|||
editPayInfoNotifyDto.setAppId(appId); |
|||
editPayInfoNotifyDto.setPayId(outOrderNo); |
|||
editPayInfoNotifyDto.setPayNo(dyOrderId); |
|||
editPayInfoNotifyDto.setTradeNo(paymentOrderNo); |
|||
editPayInfoNotifyDto.setPayChannel(payChannel); |
|||
payInfoService.editPayInfoNotify(editPayInfoNotifyDto); |
|||
//正确处理后返回以下内容格式通知小程序平台不再持续回调
|
|||
return super.buildNotifyCheckResultMsg(EnumPayType.DY_PAY,true,"success"); |
|||
}else { |
|||
log.info("抖音支付回调不是支付成功不做处理:支付状态:{},payId:{}",status,outOrderNo); |
|||
} |
|||
} else { |
|||
log.info("抖音支付回调签名校验失败,payId:{}",msgJsonObj.getString("cp_orderno")); |
|||
} |
|||
return super.buildNotifyCheckResultMsg(EnumPayType.DY_PAY,true,"business fail"); |
|||
} |
|||
} |
|||
@ -0,0 +1,214 @@ |
|||
package com.bnyer.pay.design.strategy; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.hutool.core.collection.CollectionUtil; |
|||
import cn.hutool.core.date.DateUtil; |
|||
import com.alibaba.fastjson.JSON; |
|||
import com.alibaba.fastjson.JSONObject; |
|||
import com.alibaba.fastjson.serializer.SerializerFeature; |
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.bnyer.common.core.context.SecurityContextHolder; |
|||
import com.bnyer.common.core.domain.KspayConfig; |
|||
import com.bnyer.common.core.enums.EnumPayType; |
|||
import com.bnyer.common.core.enums.ResponseEnum; |
|||
import com.bnyer.common.core.exception.ServiceException; |
|||
import com.bnyer.common.core.utils.StringUtils; |
|||
import com.bnyer.pay.constant.KSPayConstants; |
|||
import com.bnyer.pay.dto.EditPayInfoNotifyDto; |
|||
import com.bnyer.pay.dto.PayNotifyCheckDto; |
|||
import com.bnyer.pay.dto.UnifiedOrderDto; |
|||
import com.bnyer.pay.enums.EnumPayConfigStatus; |
|||
import com.bnyer.pay.mapper.KspayConfigMapper; |
|||
import com.bnyer.pay.service.PayInfoService; |
|||
import com.bnyer.pay.utils.KSPayUtil; |
|||
import com.bnyer.pay.utils.PayRestTemplateUtil; |
|||
import com.bnyer.pay.vo.PayInOrderVo; |
|||
import com.github.binarywang.wxpay.bean.request.BaseWxPayRequest; |
|||
import com.github.binarywang.wxpay.bean.result.BaseWxPayResult; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.codec.digest.DigestUtils; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.util.Date; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.TreeMap; |
|||
|
|||
/** |
|||
* @author :WXC |
|||
* @Date :2023/04/23 |
|||
* @description : |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
public class KSPayStrategy extends AbstractPayStrategy{ |
|||
|
|||
@Autowired |
|||
private KSPayUtil ksPayUtil; |
|||
|
|||
@Autowired |
|||
private PayRestTemplateUtil payRestTemplateUtil; |
|||
|
|||
@Autowired |
|||
private PayInfoService payInfoService; |
|||
|
|||
@Autowired |
|||
private KspayConfigMapper kspayConfigMapper; |
|||
|
|||
@Override |
|||
public PayInOrderVo unifiedOrder(UnifiedOrderDto dto) { |
|||
try { |
|||
List<KspayConfig> kspayConfigList = kspayConfigMapper.selectList(new LambdaQueryWrapper<KspayConfig>().eq(KspayConfig::getStatus, EnumPayConfigStatus.ENABLE.getCode())); |
|||
if (CollUtil.isEmpty(kspayConfigList)){ |
|||
throw new ServiceException(ResponseEnum.PAY_CONFIG_ERROR); |
|||
} |
|||
KspayConfig kspayConfig = kspayConfigList.get(0); |
|||
String openId = SecurityContextHolder.get("ksCode"); |
|||
String appId = kspayConfig.getAppid(); |
|||
String backurl = kspayConfig.getBackurl(); |
|||
String secret = kspayConfig.getSecret(); |
|||
//加签验签的参数需要排序
|
|||
Map<String, Object> params = new TreeMap<>(); |
|||
//小程序APPID
|
|||
params.put("app_id", appId); |
|||
//开发者侧的订单号。需保证同一小程序下不可重复
|
|||
params.put("out_order_no", dto.getPayId()); |
|||
//快手用户在当前小程序的open_id,可通过login操作获取
|
|||
params.put("open_id", openId); |
|||
//用户支付金额,单位为[分]。不允许传非整数的数值。
|
|||
params.put("total_amount", BaseWxPayRequest.yuanToFen(dto.getPayAmount())); |
|||
//商品描述。
|
|||
params.put("subject", dto.getGoodsSubject()); |
|||
//商品详情
|
|||
params.put("detail", dto.getGoodsDesc()); |
|||
//商品类型,不同商品类目的编号见 担保支付商品类目编号
|
|||
params.put("type", dto.getGoodsType()); |
|||
//订单过期时间,单位秒,300s - 172800s
|
|||
params.put("expire_time", KSPayConstants.expireTime); |
|||
//开发者自定义字段,回调原样回传。超过最大长度会被截断
|
|||
// params.put("attach", "支付测试");
|
|||
//通知地址
|
|||
params.put("notify_url", backurl); |
|||
String sign = ksPayUtil.calcSign(params, secret); |
|||
|
|||
JSONObject payJson = new JSONObject(); |
|||
payJson.put("out_order_no", dto.getPayId()); |
|||
payJson.put("open_id", openId); |
|||
payJson.put("total_amount", BaseWxPayRequest.yuanToFen(dto.getPayAmount())); |
|||
payJson.put("subject", dto.getGoodsSubject()); |
|||
payJson.put("detail", dto.getGoodsDesc()); |
|||
payJson.put("type", dto.getGoodsType()); |
|||
payJson.put("expire_time", 1800); |
|||
payJson.put("sign", sign); |
|||
// payJson.put("attach", "测试支付");
|
|||
payJson.put("notify_url", dto.getCurrDate()); |
|||
log.info("请求参数{}", payJson); |
|||
//预下单接口
|
|||
String ksPayAccessToken = payRestTemplateUtil.ksPostRequestUrlencoded(kspayConfig); |
|||
String result = payRestTemplateUtil.ksPostRequestJson(payJson, KSPayConstants.CREATE_ORDER, appId, ksPayAccessToken); |
|||
log.info("=================================="); |
|||
log.info("快手预下单result{}", result); |
|||
log.info("=================================="); |
|||
if (!"".equals(result)) { |
|||
JSONObject jsonObject = JSONObject.parseObject(result); |
|||
String resultCode = jsonObject.getString("result"); |
|||
if ("1".equals(resultCode)) { |
|||
JSONObject data = jsonObject.getJSONObject("order_info"); |
|||
String orderNo = data.getString("order_no"); |
|||
String orderToken = data.getString("order_info_token"); |
|||
if (null != orderToken && null != orderNo) { |
|||
//保存预下单信息
|
|||
PayInOrderVo payInOrderVo = new PayInOrderVo(); |
|||
//把order_no和order_info_token返回前端用于调起收银台
|
|||
payInOrderVo.setPayId(dto.getPayId()); |
|||
payInOrderVo.setPayOrderId(orderNo); |
|||
payInOrderVo.setOrderToken(orderToken); |
|||
return payInOrderVo; |
|||
} else { |
|||
return null; |
|||
} |
|||
} else { |
|||
log.error("快手支付:统一下单接口调用失败,payId:{},error{}", dto.getPayId(), jsonObject.getString("error_msg")); |
|||
throw new ServiceException(ResponseEnum.PAY_FAILS); |
|||
} |
|||
} else { |
|||
log.error("快手支付:统一下单接口调用失败,payId:{},error{}", dto.getPayId(), "请求结果返回为空"); |
|||
throw new ServiceException(ResponseEnum.PAY_FAILS); |
|||
} |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
log.error("快手支付:支付异常,payId:{},error{}", dto.getPayId(), e.getMessage()); |
|||
throw new ServiceException(ResponseEnum.PAY_FAILS); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public String parsePayNotify(String params) { |
|||
List<KspayConfig> kspayConfigList = kspayConfigMapper.selectList(new LambdaQueryWrapper<KspayConfig>().eq(KspayConfig::getStatus, EnumPayConfigStatus.ENABLE.getCode())); |
|||
if (CollUtil.isEmpty(kspayConfigList)){ |
|||
throw new ServiceException(ResponseEnum.PAY_CONFIG_ERROR); |
|||
} |
|||
KspayConfig kspayConfig = kspayConfigList.get(0); |
|||
String appId = kspayConfig.getAppid(); |
|||
String secret = kspayConfig.getSecret(); |
|||
JSONObject jsonObject = JSONObject.parseObject(params); |
|||
JSONObject object = jsonObject.getJSONObject("data"); |
|||
String kwaisign = jsonObject.getString("sign"); |
|||
String jsonString = JSONObject.toJSONString(object, SerializerFeature.WRITE_MAP_NULL_FEATURES, SerializerFeature.QuoteFieldNames); |
|||
log.info("jsonString:" + jsonString); |
|||
if (null != kwaisign) { |
|||
jsonString = jsonString + secret; |
|||
//签名校验
|
|||
if (kwaisign.equals(DigestUtils.md5Hex(jsonString))) { |
|||
//当前回调消息的唯一ID,在同一个消息多次通知时,保持一致。
|
|||
String messageId = object.getString("message_id"); |
|||
JSONObject data = object.getJSONObject("data"); |
|||
//支付渠道。取值:UNKNOWN - 未知|WECHAT-微信|ALIPAY-支付宝
|
|||
String channel = data.getString("channel"); |
|||
//订单支付状态。 取值: PROCESSING-处理中|SUCCESS-成功|FAILED-失败
|
|||
String status = data.getString("status"); |
|||
//订单金额
|
|||
Integer orderAmount = data.getInteger("order_amount"); |
|||
//用户侧支付页交易单号
|
|||
String tradeNo = data.getString("trade_no"); |
|||
//商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
|
|||
String outOrderNo = data.getString("out_order_no"); |
|||
//快手小程序平台订单号。
|
|||
String ksOrderNo = data.getString("ks_order_no"); |
|||
//回调支付成功
|
|||
if ("SUCCESS".equals(status)) { |
|||
//处理业务
|
|||
PayNotifyCheckDto payNotifyCheckDto = new PayNotifyCheckDto(); |
|||
payNotifyCheckDto.setPayType(EnumPayType.KS_PAY); |
|||
payNotifyCheckDto.setPayAmount(BaseWxPayResult.fenToYuan(orderAmount)); |
|||
payNotifyCheckDto.setPayId(outOrderNo); |
|||
payNotifyCheckDto.setMsg(messageId); |
|||
String notifyCheck = super.payNotifyCheck(payNotifyCheckDto); |
|||
if (StringUtils.isNotBlank(notifyCheck)){ |
|||
return notifyCheck; |
|||
} |
|||
log.info("快手支付回调,交易正常:封装参数修改内部支付单信息"); |
|||
EditPayInfoNotifyDto editPayInfoNotifyDto = new EditPayInfoNotifyDto(); |
|||
editPayInfoNotifyDto.setPayAmount(BaseWxPayResult.fenToYuan(orderAmount)); |
|||
editPayInfoNotifyDto.setPayTime(DateUtil.formatDateTime(new Date())); |
|||
editPayInfoNotifyDto.setAppId(appId); |
|||
editPayInfoNotifyDto.setPayId(outOrderNo); |
|||
editPayInfoNotifyDto.setPayNo(ksOrderNo); |
|||
editPayInfoNotifyDto.setTradeNo(tradeNo); |
|||
editPayInfoNotifyDto.setPayChannel(channel); |
|||
payInfoService.editPayInfoNotify(editPayInfoNotifyDto); |
|||
//正确处理后返回以下内容格式通知小程序平台不再持续回调
|
|||
return super.buildNotifyCheckResultMsg(EnumPayType.KS_PAY,true,messageId); |
|||
}else { |
|||
log.info("快手支付回调不是支付成功不做处理:支付状态:{},payId:{}",status,object.getString("trade_no")); |
|||
} |
|||
} else { |
|||
log.info("快手支付回调签名校验失败,payId:{}",object.getString("trade_no")); |
|||
} |
|||
} else { |
|||
log.info("快手支付回调参数丢失,payId:{}",object.getString("trade_no")); |
|||
} |
|||
return super.buildNotifyCheckResultMsg(EnumPayType.KS_PAY,true,"business fail"); |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
package com.bnyer.pay.dto; |
|||
|
|||
import lombok.Getter; |
|||
import lombok.NoArgsConstructor; |
|||
import lombok.Setter; |
|||
import org.apache.ibatis.annotations.Param; |
|||
|
|||
/** |
|||
* @author :WXC |
|||
* @Date :2023/04/24 |
|||
* @description : |
|||
*/ |
|||
@Getter |
|||
@Setter |
|||
@NoArgsConstructor |
|||
public class EditPayInfoSingleDto { |
|||
/** |
|||
* 支付单号:商户侧 |
|||
*/ |
|||
private String payId; |
|||
/** |
|||
* 支付单号:第三方返回 |
|||
*/ |
|||
private String payNo; |
|||
/** |
|||
* 快手用户侧订单号/抖音支付宝微信侧单号:第三方返回 |
|||
*/ |
|||
private String tradeNo; |
|||
/** |
|||
* 支付时间 |
|||
*/ |
|||
private String payTime; |
|||
/** |
|||
* 支付渠道 |
|||
*/ |
|||
private String payChannel; |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
package com.bnyer.pay.mapper; |
|||
|
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
|||
import com.bnyer.common.core.domain.DypayConfig; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
|
|||
/** |
|||
* @author :WXC |
|||
* @description : |
|||
*/ |
|||
@Mapper |
|||
public interface DypayConfigMapper extends BaseMapper<DypayConfig> { |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
package com.bnyer.pay.mapper; |
|||
|
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
|||
import com.bnyer.common.core.domain.KspayConfig; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
|
|||
/** |
|||
* @author :WXC |
|||
* @description : |
|||
*/ |
|||
@Mapper |
|||
public interface KspayConfigMapper extends BaseMapper<KspayConfig> { |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
package com.bnyer.pay.service; |
|||
|
|||
import com.bnyer.common.core.domain.DypayConfig; |
|||
import com.baomidou.mybatisplus.extension.service.IService; |
|||
/** |
|||
* @author :WXC |
|||
* @description : |
|||
*/ |
|||
public interface DypayConfigService extends IService<DypayConfig>{ |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
package com.bnyer.pay.service; |
|||
|
|||
import com.bnyer.common.core.domain.KspayConfig; |
|||
import com.baomidou.mybatisplus.extension.service.IService; |
|||
/** |
|||
* @author :WXC |
|||
* @description : |
|||
*/ |
|||
public interface KspayConfigService extends IService<KspayConfig>{ |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
package com.bnyer.pay.service.impl; |
|||
|
|||
import org.springframework.stereotype.Service; |
|||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
|||
import com.bnyer.common.core.domain.DypayConfig; |
|||
import com.bnyer.pay.mapper.DypayConfigMapper; |
|||
import com.bnyer.pay.service.DypayConfigService; |
|||
/** |
|||
* @author :WXC |
|||
* @description : |
|||
*/ |
|||
@Service |
|||
public class DypayConfigServiceImpl extends ServiceImpl<DypayConfigMapper, DypayConfig> implements DypayConfigService { |
|||
|
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
package com.bnyer.pay.service.impl; |
|||
|
|||
import org.springframework.stereotype.Service; |
|||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
|||
import com.bnyer.common.core.domain.KspayConfig; |
|||
import com.bnyer.pay.mapper.KspayConfigMapper; |
|||
import com.bnyer.pay.service.KspayConfigService; |
|||
/** |
|||
* @author :WXC |
|||
* @description : |
|||
*/ |
|||
@Service |
|||
public class KspayConfigServiceImpl extends ServiceImpl<KspayConfigMapper, KspayConfig> implements KspayConfigService { |
|||
|
|||
} |
|||
@ -0,0 +1,138 @@ |
|||
package com.bnyer.pay.utils; |
|||
|
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import javax.crypto.BadPaddingException; |
|||
import javax.crypto.Cipher; |
|||
import javax.crypto.IllegalBlockSizeException; |
|||
import javax.crypto.NoSuchPaddingException; |
|||
import javax.crypto.spec.IvParameterSpec; |
|||
import javax.crypto.spec.SecretKeySpec; |
|||
import java.nio.charset.StandardCharsets; |
|||
import java.security.InvalidAlgorithmParameterException; |
|||
import java.security.InvalidKeyException; |
|||
import java.security.MessageDigest; |
|||
import java.security.NoSuchAlgorithmException; |
|||
import java.util.*; |
|||
|
|||
/** |
|||
* @author :WXC |
|||
* @Date :2023/04/23 |
|||
* @description :抖音支付工具类 |
|||
*/ |
|||
@Component |
|||
public class DYPayUtil { |
|||
/** |
|||
* 发起请求时的签名 |
|||
*/ |
|||
public String getSign(Map<String, Object> paramsMap,String salt) { |
|||
List<String> paramsArr = new ArrayList<>(); |
|||
for (Map.Entry<String, Object> entry : paramsMap.entrySet()) { |
|||
String key = entry.getKey(); |
|||
if (key.equals("other_settle_params")) { |
|||
continue; |
|||
} |
|||
String value = entry.getValue().toString(); |
|||
|
|||
value = value.trim(); |
|||
if (value.startsWith("\"") && value.endsWith("\"") && value.length() > 1) { |
|||
value = value.substring(1, value.length() - 1); |
|||
} |
|||
value = value.trim(); |
|||
if (value.equals("") || value.equals("null")) { |
|||
continue; |
|||
} |
|||
switch (key) { |
|||
// 字段用于标识身份,不参与签名
|
|||
case "app_id": |
|||
case "thirdparty_id": |
|||
case "sign": |
|||
break; |
|||
default: |
|||
paramsArr.add(value); |
|||
break; |
|||
} |
|||
} |
|||
// 支付密钥值
|
|||
paramsArr.add(salt); |
|||
Collections.sort(paramsArr); |
|||
StringBuilder signStr = new StringBuilder(); |
|||
String sep = ""; |
|||
for (String s : paramsArr) { |
|||
signStr.append(sep).append(s); |
|||
sep = "&"; |
|||
} |
|||
return md5FromStr(signStr.toString()); |
|||
} |
|||
|
|||
public String md5FromStr(String inStr) { |
|||
MessageDigest md5; |
|||
try { |
|||
md5 = MessageDigest.getInstance("MD5"); |
|||
} catch (NoSuchAlgorithmException e) { |
|||
e.printStackTrace(); |
|||
return ""; |
|||
} |
|||
|
|||
byte[] byteArray = inStr.getBytes(StandardCharsets.UTF_8); |
|||
byte[] md5Bytes = md5.digest(byteArray); |
|||
StringBuilder hexValue = new StringBuilder(); |
|||
for (byte md5Byte : md5Bytes) { |
|||
int val = ((int) md5Byte) & 0xff; |
|||
if (val < 16) { |
|||
hexValue.append("0"); |
|||
} |
|||
hexValue.append(Integer.toHexString(val)); |
|||
} |
|||
return hexValue.toString(); |
|||
} |
|||
|
|||
/** |
|||
* 回调验证签名 |
|||
*/ |
|||
public String getCallbackSignature(int timestamp, String nonce, String msg,String token) { |
|||
List<String> sortedString = new ArrayList<>(); |
|||
sortedString.add(String.valueOf(timestamp)); |
|||
sortedString.add(nonce); |
|||
sortedString.add(msg); |
|||
sortedString.add(token); |
|||
Collections.sort(sortedString); |
|||
StringBuilder sb = new StringBuilder(); |
|||
sortedString.forEach(sb::append); |
|||
return getSha1(sb.toString().getBytes()); |
|||
} |
|||
|
|||
public String getSha1(byte[] input) { |
|||
MessageDigest mDigest; |
|||
try { |
|||
mDigest = MessageDigest.getInstance("SHA1"); |
|||
} catch (NoSuchAlgorithmException e) { |
|||
e.printStackTrace(); |
|||
return ""; |
|||
} |
|||
byte[] result = mDigest.digest(input); |
|||
StringBuilder sb = new StringBuilder(); |
|||
for (byte b : result) { |
|||
sb.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1)); |
|||
} |
|||
return sb.toString(); |
|||
} |
|||
|
|||
/** |
|||
* 手机号登陆信息解密 |
|||
*/ |
|||
public String decrypt(String encryptedData, String sessionKey, String iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidKeyException { |
|||
Base64.Decoder decoder = Base64.getDecoder(); |
|||
byte[] sessionKeyBytes = decoder.decode(sessionKey); |
|||
byte[] ivBytes = decoder.decode(iv); |
|||
byte[] encryptedBytes = decoder.decode(encryptedData); |
|||
|
|||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); |
|||
SecretKeySpec skeySpec = new SecretKeySpec(sessionKeyBytes, "AES"); |
|||
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes); |
|||
cipher.init(Cipher.DECRYPT_MODE, skeySpec, ivSpec); |
|||
byte[] ret = cipher.doFinal(encryptedBytes); |
|||
return new String(ret); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,133 @@ |
|||
package com.bnyer.pay.utils; |
|||
|
|||
import com.google.common.base.Joiner; |
|||
import com.google.common.base.Strings; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.codec.binary.Base64; |
|||
import org.apache.commons.codec.digest.DigestUtils; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import javax.crypto.Cipher; |
|||
import javax.crypto.spec.IvParameterSpec; |
|||
import javax.crypto.spec.SecretKeySpec; |
|||
import java.nio.charset.StandardCharsets; |
|||
import java.util.LinkedHashMap; |
|||
import java.util.Map; |
|||
import java.util.stream.Collectors; |
|||
|
|||
import static javax.crypto.Cipher.DECRYPT_MODE; |
|||
|
|||
/** |
|||
* @author :WXC |
|||
* @Date :2023/04/23 |
|||
* @description :快手支付工具类 |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
public class KSPayUtil { |
|||
|
|||
/** |
|||
* 快手小程序返回的加密数据的解密函数 |
|||
* |
|||
* @param sessionKey 有效的sessionKey,通过 login code 置换 |
|||
* @param encryptedData 返回的加密数据(base64编码) |
|||
* @param iv 返回的加密IV(base64编码) |
|||
* @return 返回解密的字符串数据 |
|||
*/ |
|||
public String decrypt(String sessionKey, String encryptedData, String iv) { |
|||
// Base64解码数据
|
|||
byte[] aesKey = Base64.decodeBase64(sessionKey); |
|||
byte[] ivBytes = Base64.decodeBase64(iv); |
|||
byte[] cipherBytes = Base64.decodeBase64(encryptedData); |
|||
|
|||
byte[] plainBytes = decrypt0(aesKey, ivBytes, cipherBytes); |
|||
|
|||
return new String(plainBytes, StandardCharsets.UTF_8); |
|||
} |
|||
|
|||
/** |
|||
* AES解密函数. 使用 AES/CBC/PKCS5Padding 模式 |
|||
* |
|||
* @param aesKey 密钥,长度16 |
|||
* @param iv 偏移量,长度16 |
|||
* @param cipherBytes 密文信息 |
|||
* @return 明文 |
|||
*/ |
|||
private byte[] decrypt0(byte[] aesKey, byte[] iv, byte[] cipherBytes) { |
|||
SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES"); |
|||
IvParameterSpec ivSpec = new IvParameterSpec(iv); |
|||
try { |
|||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); |
|||
cipher.init(DECRYPT_MODE, keySpec, ivSpec); |
|||
return cipher.doFinal(cipherBytes); |
|||
} catch (Exception e) { |
|||
log.error("decrypt error:{}", e.getMessage()); |
|||
throw new RuntimeException(e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* https://mp.kuaishou.com/docs/develop/server/epay/interfaceDefinition.html
|
|||
* <p> |
|||
* https://mp.kuaishou.com/docs/develop/server/payment/serverSignature.html
|
|||
* 支付签名 |
|||
*/ |
|||
|
|||
public String buildMd5(Map<String, String> dataMap, String appSecret) { |
|||
String signStr = genSignStr(dataMap); |
|||
return DigestUtils.md5Hex(signStr + appSecret); |
|||
} |
|||
|
|||
private String genSignStr(Map<String, String> data) { |
|||
StringBuilder sb = new StringBuilder(); |
|||
data.keySet().stream().sorted() |
|||
.filter(key -> StringUtils.isNotBlank(key) && StringUtils.isNotEmpty(data.get(key))) |
|||
.forEach(key -> { |
|||
sb.append(key); |
|||
sb.append("="); |
|||
sb.append(data.get(key)); |
|||
sb.append("&"); |
|||
}); |
|||
if (sb.length() > 0) { |
|||
sb.deleteCharAt(sb.length() - 1); |
|||
} |
|||
|
|||
return sb.toString(); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 获取参数 Map 的签名结果 |
|||
* https://mp.kuaishou.com/docs/develop/server/epay/appendix.html
|
|||
* |
|||
* @param signParamsMap 含义见上述示例 |
|||
* @return 返回签名结果 |
|||
*/ |
|||
public String calcSign(Map<String, Object> signParamsMap, String secret) { |
|||
// 去掉 value 为空的
|
|||
Map<String, Object> trimmedParamMap = signParamsMap.entrySet() |
|||
.stream() |
|||
.filter(item -> !Strings.isNullOrEmpty(item.getValue().toString())) |
|||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); |
|||
|
|||
// 按照字母排序
|
|||
Map<String, Object> sortedParamMap = trimmedParamMap.entrySet() |
|||
.stream() |
|||
.sorted(Map.Entry.comparingByKey()) |
|||
.collect(Collectors.toMap( |
|||
Map.Entry::getKey, |
|||
Map.Entry::getValue, |
|||
(oldValue, newValue) -> oldValue, LinkedHashMap::new)); |
|||
|
|||
// 组装成待签名字符串。(注,引用了guava工具)
|
|||
String paramStr = Joiner.on("&").withKeyValueSeparator("=").join(sortedParamMap.entrySet()); |
|||
String signStr = paramStr + secret; |
|||
|
|||
// 生成签名返回。(注,引用了commons-codec工具)
|
|||
return DigestUtils.md5Hex(signStr); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,107 @@ |
|||
package com.bnyer.pay.utils; |
|||
|
|||
import com.alibaba.fastjson.JSONObject; |
|||
import com.bnyer.common.core.domain.KspayConfig; |
|||
import com.bnyer.pay.constant.KSPayConstants; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.http.HttpEntity; |
|||
import org.springframework.http.HttpHeaders; |
|||
import org.springframework.http.MediaType; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.web.client.RestTemplate; |
|||
|
|||
import java.text.MessageFormat; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* @author :WXC |
|||
* @Date :2023/04/24 |
|||
* @description : |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
public class PayRestTemplateUtil { |
|||
|
|||
@Autowired |
|||
private RestTemplate restTemplate; |
|||
|
|||
/** |
|||
* 快手小程序post请求 |
|||
* code2session |
|||
*/ |
|||
public String ksPostRequestUrlencoded(JSONObject jsonObject, String url) { |
|||
String result = ""; |
|||
try { |
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
HttpEntity<String> formEntity = new HttpEntity<>(MessageFormat.format("js_code={0}&app_id={1}&&app_secret={2}", jsonObject.get("js_code"), jsonObject.get("appid"), jsonObject.get("secret")), headers); |
|||
result = restTemplate.postForObject(url, formEntity, String.class); |
|||
} catch (Exception e) { |
|||
log.error("快手小程序post请求异常{}", url); |
|||
log.error("post请求异常:{}", e.getMessage()); |
|||
e.printStackTrace(); |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* 快手小程序获取accessToken |
|||
*/ |
|||
public String ksPostRequestUrlencoded(KspayConfig kspayConfig) { |
|||
String result = ""; |
|||
try { |
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
HttpEntity<String> formEntity = new HttpEntity<>(MessageFormat.format("app_id={0}&app_secret={1}&&grant_type={2}" |
|||
, kspayConfig.getAppid(), kspayConfig.getSecret(), "client_credentials"), headers); |
|||
result = restTemplate.postForObject(KSPayConstants.GET_ACCESS_TOKEN, formEntity, String.class); |
|||
} catch (Exception e) { |
|||
log.error("快手小程序post请求异常{}", KSPayConstants.GET_ACCESS_TOKEN); |
|||
log.error("post请求异常:{}", e.getMessage()); |
|||
e.printStackTrace(); |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* 快手 |
|||
* 支付 退款 结算 |
|||
*/ |
|||
public String ksPostRequestJson(JSONObject jsonObject, String url, String appId, String accessToken) { |
|||
String result = ""; |
|||
try { |
|||
Map<String, String> map = new HashMap<>(); |
|||
map.put("app_id", appId); |
|||
map.put("access_token", accessToken); |
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_JSON); |
|||
HttpEntity<Object> formEntity = new HttpEntity<>(jsonObject, headers); |
|||
result = restTemplate.postForObject(url + "/?app_id={app_id}&access_token={access_token}", formEntity, String.class, map); |
|||
} catch (Exception e) { |
|||
log.error("快手小程序post请求异常{}", url); |
|||
log.error("post请求异常:{}", e.getMessage()); |
|||
e.printStackTrace(); |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* 抖音小程序post请求 |
|||
*/ |
|||
public String dyPostRequest(JSONObject jsonObject, String url) { |
|||
String result = ""; |
|||
try { |
|||
HttpHeaders headers = new HttpHeaders(); |
|||
//所有的请求需要用JSON格式发送
|
|||
headers.setContentType(MediaType.APPLICATION_JSON); |
|||
HttpEntity<Object> formEntity = new HttpEntity<>(jsonObject, headers); |
|||
result = restTemplate.postForObject(url, formEntity, String.class); |
|||
} catch (Exception e) { |
|||
log.error("抖音小程序post请求异常{}", url); |
|||
e.printStackTrace(); |
|||
} |
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
|||
<mapper namespace="com.bnyer.pay.mapper.DypayConfigMapper"> |
|||
<resultMap id="BaseResultMap" type="com.bnyer.common.core.domain.DypayConfig"> |
|||
<!--@mbg.generated--> |
|||
<!--@Table pay_dypay_config--> |
|||
<id column="id" jdbcType="BIGINT" property="id" /> |
|||
<result column="appid" jdbcType="VARCHAR" property="appid" /> |
|||
<result column="salt" jdbcType="VARCHAR" property="salt" /> |
|||
<result column="token" jdbcType="VARCHAR" property="token" /> |
|||
<result column="backurl" jdbcType="VARCHAR" property="backurl" /> |
|||
<result column="status" jdbcType="CHAR" property="status" /> |
|||
<result column="remark" jdbcType="VARCHAR" property="remark" /> |
|||
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" /> |
|||
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" /> |
|||
</resultMap> |
|||
<sql id="Base_Column_List"> |
|||
<!--@mbg.generated--> |
|||
id, appid,salt, token, backurl, `status`, remark, create_time, update_time |
|||
</sql> |
|||
</mapper> |
|||
@ -0,0 +1,20 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
|||
<mapper namespace="com.bnyer.pay.mapper.KspayConfigMapper"> |
|||
<resultMap id="BaseResultMap" type="com.bnyer.common.core.domain.KspayConfig"> |
|||
<!--@mbg.generated--> |
|||
<!--@Table pay_kspay_config--> |
|||
<id column="id" jdbcType="BIGINT" property="id" /> |
|||
<result column="appid" jdbcType="VARCHAR" property="appid" /> |
|||
<result column="secret" jdbcType="VARCHAR" property="secret" /> |
|||
<result column="backurl" jdbcType="VARCHAR" property="backurl" /> |
|||
<result column="status" jdbcType="CHAR" property="status" /> |
|||
<result column="remark" jdbcType="VARCHAR" property="remark" /> |
|||
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" /> |
|||
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" /> |
|||
</resultMap> |
|||
<sql id="Base_Column_List"> |
|||
<!--@mbg.generated--> |
|||
id, appid, secret, backurl, `status`, remark, create_time, update_time |
|||
</sql> |
|||
</mapper> |
|||
Loading…
Reference in new issue