feat 云闪付对接

This commit is contained in:
bootx
2024-03-06 23:23:59 +08:00
parent cfbd7e2e32
commit f6cd4b0a46
12 changed files with 303 additions and 29 deletions

View File

@@ -1,11 +1,16 @@
2.0.3: 2.0.3:
- [ ] 云闪付接入 - [ ] 云闪付接入
- [ ] 支付
- [ ] 退款
- [ ] 同步
- [ ] 对账
- [ ] 回调
- [x] 手动触发通知消息发送 - [x] 手动触发通知消息发送
- [x] 退款操作支持重试 - [x] 退款操作支持重试
- [ ] 退款操作支持关闭
- [x] 通知任务增加订单的状态类型,例如订单关闭、成功、失败等 - [x] 通知任务增加订单的状态类型,例如订单关闭、成功、失败等
- [ ] 支付流程也改为先落库后支付情况, 避免极端情况导致掉单 - [ ] 支付流程也改为先落库后支付情况, 避免极端情况导致掉单
- [ ] 优化发起请求时IP的获取逻辑
2.0.x 版本内容 2.0.x 版本内容
- [ ] 增加各类日志记录,例如钱包的各项操作 - [ ] 增加各类日志记录,例如钱包的各项操作

View File

@@ -0,0 +1,20 @@
package cn.bootx.platform.daxpay.param.channel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 云闪付支付参数
* @author xxm
* @since 2024/3/6
*/
@Data
@Schema(title = "云闪付支付参数")
public class UnionPayParam {
@Schema(description = "微信openId")
private String openId;
@Schema(description = "授权码(主动扫描用户的付款码)")
private String authCode;
}

View File

@@ -2,10 +2,7 @@ package cn.bootx.platform.daxpay.param.pay;
import cn.bootx.platform.daxpay.code.PayChannelEnum; import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.code.PayWayEnum; import cn.bootx.platform.daxpay.code.PayWayEnum;
import cn.bootx.platform.daxpay.param.channel.AliPayParam; import cn.bootx.platform.daxpay.param.channel.*;
import cn.bootx.platform.daxpay.param.channel.VoucherPayParam;
import cn.bootx.platform.daxpay.param.channel.WalletPayParam;
import cn.bootx.platform.daxpay.param.channel.WeChatPayParam;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -47,6 +44,7 @@ public class PayChannelParam {
* @see WeChatPayParam * @see WeChatPayParam
* @see VoucherPayParam * @see VoucherPayParam
* @see WalletPayParam * @see WalletPayParam
* @see UnionPayParam
*/ */
@Schema(description = "附加支付参数") @Schema(description = "附加支付参数")
private Map<String,Object> channelParam; private Map<String,Object> channelParam;

View File

@@ -2,7 +2,6 @@ package cn.bootx.platform.daxpay.util;
import cn.bootx.platform.common.core.util.LocalDateTimeUtil; import cn.bootx.platform.common.core.util.LocalDateTimeUtil;
import cn.bootx.platform.daxpay.code.PayChannelEnum; import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.entity.RefundableInfo;
import cn.bootx.platform.daxpay.exception.pay.PayAmountAbnormalException; import cn.bootx.platform.daxpay.exception.pay.PayAmountAbnormalException;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException; import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.param.pay.PayChannelParam; import cn.bootx.platform.daxpay.param.pay.PayChannelParam;
@@ -14,7 +13,6 @@ import lombok.experimental.UtilityClass;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.List; import java.util.List;
import java.util.Objects;
/** /**
* 支付工具类 * 支付工具类
@@ -61,19 +59,26 @@ public class PayUtil {
} }
/** /**
* 获取支付宝的过期时间 * 获取支付宝的过期时间 yyyy-MM-dd HH:mm:ss
*/ */
public String getAliTimeExpire(LocalDateTime dateTime) { public String getAliTimeExpire(LocalDateTime dateTime) {
return LocalDateTimeUtil.format(dateTime, DatePattern.NORM_DATETIME_PATTERN); return LocalDateTimeUtil.format(dateTime, DatePattern.NORM_DATETIME_PATTERN);
} }
/** /**
* 获取微信的过期时间 * 获取微信的过期时间 yyyyMMddHHmmss
*/ */
public String getWxExpiredTime(LocalDateTime dateTime) { public String getWxExpiredTime(LocalDateTime dateTime) {
return LocalDateTimeUtil.format(dateTime, DatePattern.PURE_DATETIME_PATTERN); return LocalDateTimeUtil.format(dateTime, DatePattern.PURE_DATETIME_PATTERN);
} }
/**
* 获取云闪付的过期时间 yyyyMMddHHmmss
*/
public String getUnionExpiredTime(LocalDateTime dateTime) {
return LocalDateTimeUtil.format(dateTime, DatePattern.PURE_DATETIME_PATTERN);
}
/** /**
* 获取支付单的超时时间 * 获取支付单的超时时间
*/ */

View File

@@ -92,6 +92,7 @@
<artifactId>IJPay-WxPay</artifactId> <artifactId>IJPay-WxPay</artifactId>
<version>${IJPay.version}</version> <version>${IJPay.version}</version>
</dependency> </dependency>
<!-- 微信支付 wxjava --> <!-- 微信支付 wxjava -->
<dependency> <dependency>
<groupId>com.github.binarywang</groupId> <groupId>com.github.binarywang</groupId>
@@ -99,6 +100,13 @@
<version>${wxjava.version}</version> <version>${wxjava.version}</version>
</dependency> </dependency>
<!-- 云闪付 -->
<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-UnionPay</artifactId>
<version>${IJPay.version}</version>
</dependency>
<!-- 微信工具包 --> <!-- 微信工具包 -->
<dependency> <dependency>
<groupId>com.github.binarywang</groupId> <groupId>com.github.binarywang</groupId>

View File

@@ -79,7 +79,7 @@ public class AliPayConfigService {
public AliPayConfig getAndCheckConfig() { public AliPayConfig getAndCheckConfig() {
AliPayConfig alipayConfig = this.getConfig(); AliPayConfig alipayConfig = this.getConfig();
if (!alipayConfig.getEnable()){ if (!alipayConfig.getEnable()){
throw new PayFailureException("支付宝支付方式未启用"); throw new PayFailureException("支付宝支付未启用");
} }
return alipayConfig; return alipayConfig;
} }

View File

@@ -2,14 +2,21 @@ package cn.bootx.platform.daxpay.service.core.channel.union.entity;
import cn.bootx.platform.common.core.function.EntityBaseFunction; import cn.bootx.platform.common.core.function.EntityBaseFunction;
import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity; import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity;
import cn.bootx.platform.common.mybatisplus.handler.StringListTypeHandler;
import cn.bootx.platform.daxpay.service.core.channel.union.convert.UnionPayConvert; import cn.bootx.platform.daxpay.service.core.channel.union.convert.UnionPayConvert;
import cn.bootx.platform.daxpay.service.dto.channel.union.UnionPayConfigDto; import cn.bootx.platform.daxpay.service.dto.channel.union.UnionPayConfigDto;
import cn.bootx.table.modify.annotation.DbColumn;
import cn.bootx.table.modify.annotation.DbTable; import cn.bootx.table.modify.annotation.DbTable;
import cn.bootx.table.modify.mysql.annotation.DbMySqlFieldType;
import cn.bootx.table.modify.mysql.constants.MySqlFieldTypeEnum;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.util.List;
/** /**
* 云闪付支付配置 * 云闪付支付配置
* *
@@ -20,9 +27,44 @@ import lombok.experimental.Accessors;
@Data @Data
@DbTable(comment = "云闪付支付配置") @DbTable(comment = "云闪付支付配置")
@Accessors(chain = true) @Accessors(chain = true)
@TableName("pay_union_pay_config") @TableName(value = "pay_union_pay_config",autoResultMap = true)
public class UnionPayConfig extends MpBaseEntity implements EntityBaseFunction<UnionPayConfigDto> { public class UnionPayConfig extends MpBaseEntity implements EntityBaseFunction<UnionPayConfigDto> {
/** 商户号 */
@DbColumn(comment = "商户号")
private String machId;
/** 是否启用, 只影响支付和退款操作 */
@DbColumn(comment = "是否启用")
private Boolean enable;
/** 密钥 */
@DbColumn(comment = "密钥")
private String appKey;
/** 支付网关地址 */
@DbColumn(comment = "支付网关地址")
private String serverUrl;
/**
* 服务器异步通知页面路径, 需要填写本网关服务的地址, 不可以直接填写业务系统的地址
* 1. 需http://或者https://格式的完整路径,
* 2. 不能加?id=123这类自定义参数必须外网可以正常访问
* 3. 消息顺序 银联网关 -> 本网关进行处理 -> 发送消息通知业务系统
*/
@DbColumn(comment = "异步通知路径")
private String notifyUrl;
/** 可用支付方式 */
@DbColumn(comment = "可用支付方式")
@DbMySqlFieldType(MySqlFieldTypeEnum.LONGTEXT)
@TableField(typeHandler = StringListTypeHandler.class)
private List<String> payWays;
/** 备注 */
@DbColumn(comment = "备注")
private String remark;
@Override @Override
public UnionPayConfigDto toDto() { public UnionPayConfigDto toDto() {
return UnionPayConvert.CONVERT.convert(this); return UnionPayConvert.CONVERT.convert(this);

View File

@@ -1,11 +1,27 @@
package cn.bootx.platform.daxpay.service.core.channel.union.service; package cn.bootx.platform.daxpay.service.core.channel.union.service;
import cn.bootx.platform.common.core.exception.DataNotExistException;
import cn.bootx.platform.common.core.rest.dto.LabelValue;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.service.code.AliPayWay;
import cn.bootx.platform.daxpay.service.core.channel.union.dao.UnionPayConfigManager; import cn.bootx.platform.daxpay.service.core.channel.union.dao.UnionPayConfigManager;
import cn.bootx.platform.daxpay.service.core.channel.union.entity.UnionPayConfig;
import cn.bootx.platform.daxpay.service.core.system.config.service.PayChannelConfigService;
import cn.bootx.platform.daxpay.service.param.channel.alipay.AliPayConfigParam;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/** /**
* 云闪付支付配置
* @author xxm * @author xxm
* @since 2022/3/11 * @since 2022/3/11
*/ */
@@ -13,7 +29,52 @@ import org.springframework.stereotype.Service;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class UnionPayConfigService { public class UnionPayConfigService {
/** 默认云闪付配置的主键ID */
private final static Long ID = 0L;
private final UnionPayConfigManager unionPayConfigManager; private final UnionPayConfigManager unionPayConfigManager;
private final PayChannelConfigService payChannelConfigService;
/**
* 修改
*/
@Transactional(rollbackFor = Exception.class)
public void update(AliPayConfigParam param) {
UnionPayConfig unionPayConfig = unionPayConfigManager.findById(ID).orElseThrow(() -> new DataNotExistException("支付宝配置不存在"));
// 启用或停用
if (!Objects.equals(param.getEnable(), unionPayConfig.getEnable())){
payChannelConfigService.setEnable(PayChannelEnum.UNION_PAY.getCode(), param.getEnable());
}
BeanUtil.copyProperties(param, unionPayConfig, CopyOptions.create().ignoreNullValue());
unionPayConfigManager.updateById(unionPayConfig);
}
/**
* 支付宝支持支付方式
*/
public List<LabelValue> findPayWays() {
return AliPayWay.getPayWays()
.stream()
.map(e -> new LabelValue(e.getName(),e.getCode()))
.collect(Collectors.toList());
}
/**
* 获取支付配置
*/
public UnionPayConfig getConfig(){
return unionPayConfigManager.findById(ID).orElseThrow(() -> new DataNotExistException("支付宝配置不存在"));
}
/**
* 获取并检查支付配置
*/
public UnionPayConfig getAndCheckConfig() {
UnionPayConfig unionPayConfig = this.getConfig();
if (!unionPayConfig.getEnable()){
throw new PayFailureException("云闪付支付未启用");
}
return unionPayConfig;
}
} }

View File

@@ -1,10 +1,30 @@
package cn.bootx.platform.daxpay.service.core.channel.union.service; package cn.bootx.platform.daxpay.service.core.channel.union.service;
import cn.bootx.platform.daxpay.code.PayWayEnum;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.param.channel.UnionPayParam;
import cn.bootx.platform.daxpay.param.pay.PayChannelParam;
import cn.bootx.platform.daxpay.service.code.AliPayWay;
import cn.bootx.platform.daxpay.service.common.context.PayLocal;
import cn.bootx.platform.daxpay.service.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.service.core.channel.union.entity.UnionPayConfig;
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayOrder;
import cn.bootx.platform.daxpay.util.PayUtil;
import cn.hutool.core.collection.CollUtil;
import com.ijpay.core.enums.SignType;
import com.ijpay.core.kit.WxPayKit;
import com.ijpay.unionpay.UnionPayApi;
import com.ijpay.unionpay.enums.ServiceEnum;
import com.ijpay.unionpay.model.UnifiedOrderModel;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Optional;
/** /**
* 云闪付支付
* @author xxm * @author xxm
* @since 2022/3/11 * @since 2022/3/11
*/ */
@@ -13,4 +33,59 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor @RequiredArgsConstructor
public class UnionPayService { public class UnionPayService {
/**
* 支付接口
*/
public void pay(PayOrder payOrder, PayChannelParam payChannelParam, UnionPayParam unionPayParam, UnionPayConfig config){
Integer amount = payChannelParam.getAmount();
String totalFee = String.valueOf(amount);
PayLocal asyncPayInfo = PaymentContextLocal.get().getPayInfo();;
String payBody = null;
PayWayEnum payWayEnum = PayWayEnum.findByCode(payChannelParam.getWay());
// 二维码支付
if (payWayEnum == PayWayEnum.QRCODE){
payBody = this.qrCodePay(totalFee, payOrder, config);
}
asyncPayInfo.setPayBody(payBody);
}
/**
* 扫码支付
*/
public String qrCodePay(String totalFee, PayOrder payOrder, UnionPayConfig config){
Map<String, String> params = UnifiedOrderModel.builder()
.service(ServiceEnum.NATIVE.toString())
.mch_id(config.getMachId())
.out_trade_no(String.valueOf(payOrder.getId()))
.body(payOrder.getTitle())
.total_fee(totalFee)
.time_expire(PayUtil.getUnionExpiredTime(payOrder.getExpiredTime()))
.mch_create_ip("127.0.0.1")
.notify_url(config.getNotifyUrl())
.nonce_str(WxPayKit.generateStr())
.build()
.createSign(config.getAppKey(), SignType.MD5);
String xmlResult = UnionPayApi.execution(config.getServerUrl(), params);
Map<String, String> result = WxPayKit.xmlToMap(xmlResult);
return result.get("code_url");
}
/**
* 支付前检查支付方式是否可用
*/
public void validation(PayChannelParam payChannelParam, UnionPayConfig unionPayConfig) {
if (CollUtil.isEmpty(unionPayConfig.getPayWays())){
throw new PayFailureException("云闪付未配置可用的支付方式");
}
// 发起的支付类型是否在支持的范围内
PayWayEnum payWayEnum = Optional.ofNullable(AliPayWay.findByCode(payChannelParam.getWay()))
.orElseThrow(() -> new PayFailureException("非法的云闪付支付类型"));
if (!unionPayConfig.getPayWays().contains(payWayEnum.getCode())) {
throw new PayFailureException("该云闪付支付方式不可用");
}
}
} }

View File

@@ -1,16 +0,0 @@
package cn.bootx.platform.daxpay.service.core.channel.union.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @author xxm
* @since 2022/3/11
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UnionPaymentService {
}

View File

@@ -63,7 +63,7 @@ public class WeChatPayConfigService {
public WeChatPayConfig getAndCheckConfig(){ public WeChatPayConfig getAndCheckConfig(){
WeChatPayConfig weChatPayConfig = getConfig(); WeChatPayConfig weChatPayConfig = getConfig();
if (!weChatPayConfig.getEnable()){ if (!weChatPayConfig.getEnable()){
throw new PayFailureException("微信支付配置未启用"); throw new PayFailureException("微信支付未启用");
} }
return weChatPayConfig; return weChatPayConfig;
} }

View File

@@ -1,12 +1,28 @@
package cn.bootx.platform.daxpay.service.core.payment.pay.strategy; package cn.bootx.platform.daxpay.service.core.payment.pay.strategy;
import cn.bootx.platform.daxpay.code.PayChannelEnum; import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.exception.pay.PayAmountAbnormalException;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.param.channel.UnionPayParam;
import cn.bootx.platform.daxpay.param.pay.PayChannelParam;
import cn.bootx.platform.daxpay.service.common.context.PayLocal;
import cn.bootx.platform.daxpay.service.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.service.core.channel.union.entity.UnionPayConfig;
import cn.bootx.platform.daxpay.service.core.channel.union.service.UnionPayConfigService;
import cn.bootx.platform.daxpay.service.core.channel.union.service.UnionPayService;
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayChannelOrder;
import cn.bootx.platform.daxpay.service.core.order.pay.service.PayChannelOrderService;
import cn.bootx.platform.daxpay.service.func.AbsPayStrategy; import cn.bootx.platform.daxpay.service.func.AbsPayStrategy;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Map;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE; import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/** /**
@@ -21,14 +37,74 @@ import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROT
@RequiredArgsConstructor @RequiredArgsConstructor
public class UnionPayStrategy extends AbsPayStrategy { public class UnionPayStrategy extends AbsPayStrategy {
private final PayChannelOrderService channelOrderService;
private final UnionPayService unionPayService;
private final UnionPayConfigService unionPayConfigService;
private UnionPayParam unionPayParam;
private UnionPayConfig unionPayConfig;
@Override @Override
public PayChannelEnum getChannel() { public PayChannelEnum getChannel() {
return PayChannelEnum.UNION_PAY; return PayChannelEnum.UNION_PAY;
} }
/**
* 支付前操作
*/
@Override
public void doBeforePayHandler() {
try {
// 支付宝参数验证
Map<String, Object> channelParam = this.getPayChannelParam().getChannelParam();
if (CollUtil.isNotEmpty(channelParam)) {
this.unionPayParam = BeanUtil.toBean(channelParam, UnionPayParam.class);
}
else {
this.unionPayParam = new UnionPayParam();
}
}
catch (JSONException e) {
throw new PayFailureException("支付参数错误");
}
// 检查金额
PayChannelParam payMode = this.getPayChannelParam();
if (payMode.getAmount() <= 0) {
throw new PayAmountAbnormalException();
}
// 检查并获取支付宝支付配置
this.unionPayConfig = unionPayConfigService.getAndCheckConfig();
unionPayService.validation(this.getPayChannelParam(), unionPayConfig);
}
/**
* 发起支付操作
*/
@Override @Override
public void doPayHandler() { public void doPayHandler() {
unionPayService.pay(this.getOrder(), this.getPayChannelParam(), this.unionPayParam, this.unionPayConfig);
}
/**
* 不使用默认的生成通道支付单方法, 异步支付通道的支付订单自己管理
* channelOrderService.switchAsyncPayChannel 进行切换
*/
@Override
public void generateChannelOrder() {
}
/**
* 支付调起成功, 保存或更新通道支付订单
*/
@Override
public void doSuccessHandler() {
PayLocal asyncPayInfo = PaymentContextLocal.get().getPayInfo();
PayChannelOrder payChannelOrder = channelOrderService.switchAsyncPayChannel(this.getOrder(), this.getPayChannelParam());
// 支付完成, 保存记录
if (asyncPayInfo.isPayComplete()) {
// aliRecordService.pay(this.getOrder(), payChannelOrder);
}
} }