feat 新增支付撤销, 支付订单增加撤销状态, 删除无关基础模块

This commit is contained in:
DaxPay
2024-06-05 20:05:25 +08:00
parent 694f150a61
commit 42cfd24558
36 changed files with 529 additions and 211 deletions

View File

@@ -1,28 +1,22 @@
## 单商户
2.0.8: 对账完善和系统优化
- [ ] 细分各种支付异常类和编码
- [ ] 修复功能支付/退款/对账
- [ ] 支持撤销接口
- [ ] 增加转账接口功能
- [ ] 细分各种支付异常类和编码
- [ ] 增加对账修复功能拆
- [ ] DEMO增加获取微信OpenID和支付宝OpenId功能
- [ ] 管理端界面支持扫码绑定对账接收方功能
- [ ] 对账提供外部接口调用
- [ ] 下载系统账单
- [ ] 分账结果通知处理
- [ ] 支付宝通知
- [ ] 微信通知
- [ ] 通知记录保存
- [ ] 增加收单收银台功能
- [ ] 增加资金对账单功能
- [ ] 支付通道两个独立的配置进行合并为一个
- [ ]持撤销接口
- [ ] 支付和退款达到终态不可以再回退回之前的状态, 只能添加差错单进行处理
2.0.8: 转账和功能功能优化
- [ ] 支持转账操作, 通过支付通道专有参数进行实现, 转账时只能单个通道进行操作
- [ ]付和退款达到终态不可以再回退回之前的状态
- [x] 修复支付关闭参数名称不正确问题
2.0.9: 消息通知和功能功能优化
- [ ] 将系统通知消息重构为类似支付宝应用通知的方式
- [ ] 支付成功回调后, 如果订单已超时, 则进入待退款订单中,提示进行退款,或者自动退款
- [ ] 新增支付单预警功能, 处理支付单与网关状态不一致且无法自动修复的情况
2.1.x 版本内容
2.1.x 版本内容
- [ ] 新增支付单预警功能, 处理支付单与网关状态不一致且无法自动修复的情况
- [ ] 差错单据处理
- [ ] 特殊退款接口
- [ ] 统计报表功能

View File

@@ -1,30 +0,0 @@
package cn.bootx.platform.baseapi.dto.chinaword;
import cn.bootx.platform.common.core.rest.dto.BaseDto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 敏感词
* @author xxm
* @since 2023-08-09
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Schema(title = "敏感词")
@Accessors(chain = true)
public class ChinaWordDto extends BaseDto {
@Schema(description = "敏感词")
private String word;
@Schema(description = "分类")
private String type;
@Schema(description = "描述")
private String description;
@Schema(description = "是否启用")
private Boolean enable;
@Schema(description = "是否是白名单名词")
private Boolean white;
}

View File

@@ -1,27 +0,0 @@
package cn.bootx.platform.baseapi.dto.chinaword;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Set;
/**
* 敏感词验证结果
* @author xxm
* @since 2023/8/9
*/
@Data
@Accessors(chain = true)
@Schema(title = "敏感词验证结果")
public class ChinaWordVerifyResult {
@Schema(description = "是否敏感")
private boolean sensitive;
@Schema(description = "敏感词数量")
private int count;
@Schema(description = "去重后的敏感词列表")
private Set<String> sensitiveWords;
@Schema(description = "脱敏后的文本")
private String text;
}

View File

@@ -1,19 +0,0 @@
package cn.bootx.platform.baseapi.param.chinaword;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* 敏感词导入参数
* @author xxm
* @since 2023/8/12
*/
@Data
public class ChinaWordImportParam {
@ExcelProperty(value = "类型")
private String type;
@ExcelProperty("黑/白名单")
private String whiteOrBlack;
@ExcelProperty("敏感词")
private String word;
}

View File

@@ -1,31 +0,0 @@
package cn.bootx.platform.baseapi.param.chinaword;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 敏感词
* @author xxm
* @since 2023-08-09
*/
@Data
@Schema(title = "敏感词")
@Accessors(chain = true)
public class ChinaWordParam {
@Schema(description= "主键")
private Long id;
@Schema(description = "敏感词")
private String word;
@Schema(description = "分类")
private String type;
@Schema(description = "描述")
private String description;
@Schema(description = "是否启用")
private Boolean enable;
@Schema(description = "白名单名词")
private Boolean white;
}

View File

@@ -1,24 +0,0 @@
package cn.bootx.platform.baseapi.param.chinaword;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 敏感词验证参数类
* @author xxm
* @since 2023/8/10
*/
@Data
@Accessors(chain = true)
@Schema(title = "敏感词验证参数类")
public class ChinaWordVerifyParam {
@Schema(description = "文本")
private String text;
@Schema(description = "间隔距离")
private int skip = 0;
@Schema(description = "替换字符")
private char symbol = '*';
}

View File

@@ -0,0 +1,17 @@
package cn.daxpay.single.sdk.model.pay;
import cn.daxpay.single.sdk.net.DaxPayResponseModel;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 支付撤销
* @author xxm
* @since 2024/2/2
*/
@Getter
@Setter
@ToString(callSuper = true)
public class PayCancelModel extends DaxPayResponseModel {
}

View File

@@ -0,0 +1,41 @@
package cn.daxpay.single.sdk.param.pay;
import cn.daxpay.single.sdk.model.pay.PayCancelModel;
import cn.daxpay.single.sdk.net.DaxPayRequest;
import cn.daxpay.single.sdk.response.DaxPayResult;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONUtil;
import lombok.Getter;
import lombok.Setter;
/**
* 支付撤销参数
* @author xxm
* @since 2023/12/17
*/
@Getter
@Setter
public class PayCancelParam extends DaxPayRequest<PayCancelModel> {
/** 订单号 */
private String orderNo;
/** 商户订单号 */
private String bizOrderNo;
/**
* 方法请求路径
*/
@Override
public String path() {
return "/unipay/cancel";
}
/**
* 将请求返回结果反序列化为实体类
*/
@Override
public DaxPayResult<PayCancelModel> toModel(String json) {
return JSONUtil.toBean(json, new TypeReference<DaxPayResult<PayCancelModel>>() {}, false);
}
}

View File

@@ -5,23 +5,22 @@ import cn.daxpay.single.sdk.net.DaxPayRequest;
import cn.daxpay.single.sdk.response.DaxPayResult;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONUtil;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.*;
/**
* 支付关闭参数
* @author xxm
* @since 2023/12/17
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Getter
@Setter
public class PayCloseParam extends DaxPayRequest<PayCloseModel> {
/** 订单号 */
private String orderNo;
/** 商户订单号 */
private String bizTradeNo;
private String bizOrderNo;
/**
* 方法请求路径

View File

@@ -13,7 +13,6 @@ import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.LocalDateTime;
@@ -24,7 +23,6 @@ import java.time.LocalDateTime;
*/
@Getter
@Setter
@ToString(callSuper = true)
public class PayParam extends DaxPayRequest<PayModel> {
/** 商户订单号 */

View File

@@ -1,9 +1,11 @@
package cn.daxpay.single.sdk.payment;
import cn.daxpay.single.sdk.code.SignTypeEnum;
import cn.daxpay.single.sdk.model.pay.PayCancelModel;
import cn.daxpay.single.sdk.model.pay.PayCloseModel;
import cn.daxpay.single.sdk.net.DaxPayConfig;
import cn.daxpay.single.sdk.net.DaxPayKit;
import cn.daxpay.single.sdk.param.pay.PayCancelParam;
import cn.daxpay.single.sdk.param.pay.PayCloseParam;
import cn.daxpay.single.sdk.response.DaxPayResult;
import cn.hutool.json.JSONUtil;
@@ -37,4 +39,13 @@ public class PayCloseOrderTest {
DaxPayResult<PayCloseModel> execute = DaxPayKit.execute(param);
System.out.println(JSONUtil.toJsonStr(execute));
}
@Test
public void cancel(){
PayCancelParam param = new PayCancelParam();
param.setOrderNo("DEVP24060518083863000001");
param.setClientIp("127.0.0.1");
DaxPayResult<PayCancelModel> execute = DaxPayKit.execute(param);
System.out.println(JSONUtil.toJsonStr(execute));
}
}

View File

@@ -18,6 +18,7 @@ public enum PayStatusEnum {
PROGRESS("progress","支付中"),
SUCCESS("success","成功"),
CLOSE("close","支付关闭"),
CANCEL("cancel","支付撤销"),
REFUNDING("refunding","退款中"),
PARTIAL_REFUND("partial_refund","部分退款"),
REFUNDED("refunded","全部退款"),

View File

@@ -12,6 +12,8 @@ public interface PaymentApiCode {
String REFUND = "refund";
/** 关闭订单 */
String CLOSE = "close";
/** 撤销订单 */
String CANCEL = "cancel";
/** 分账 */
String ALLOCATION = "allocation";
/** 转账 */

View File

@@ -1,25 +0,0 @@
package cn.daxpay.single.entity;
import cn.daxpay.single.code.PayChannelEnum;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 可退款信息(不持久化,直接保存为json)
* @author xxm
* @since 2023/12/18
*/
@Data
@Accessors(chain = true)
public class RefundableInfo {
/**
* 通道
* @see PayChannelEnum#getCode()
*/
private String channel;
/**
* 可退款金额
*/
private Integer amount;
}

View File

@@ -16,9 +16,11 @@ import lombok.EqualsAndHashCode;
@Schema(title = "支付订单撤销")
public class PayCancelParam extends PaymentCommonParam {
/** 订单号 */
@Schema(description = "订单号")
private String orderNo;
/** 商户订单号 */
@Schema(description = "商户订单号")
private String bizOrderNo;
}

View File

@@ -21,5 +21,5 @@ public class PayCloseParam extends PaymentCommonParam {
/** 商户订单号 */
@Schema(description = "商户订单号")
private String bizTradeNo;
private String bizOrderNo;
}

View File

@@ -0,0 +1,19 @@
package cn.daxpay.single.result.pay;
import cn.daxpay.single.result.PaymentCommonResult;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 支付关闭响应参数
* @author xxm
* @since 2024/4/23
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "支付撤销响应参数")
public class PayCancelResult extends PaymentCommonResult {
}

View File

@@ -2,16 +2,19 @@ package cn.daxpay.single.gateway.controller;
import cn.bootx.platform.common.core.annotation.IgnoreAuth;
import cn.daxpay.single.code.PaymentApiCode;
import cn.daxpay.single.param.payment.pay.PayCancelParam;
import cn.daxpay.single.param.payment.pay.PayCloseParam;
import cn.daxpay.single.param.payment.pay.PayParam;
import cn.daxpay.single.param.payment.refund.RefundParam;
import cn.daxpay.single.param.payment.transfer.TransferParam;
import cn.daxpay.single.result.DaxResult;
import cn.daxpay.single.result.pay.PayCancelResult;
import cn.daxpay.single.result.pay.PayCloseResult;
import cn.daxpay.single.result.pay.PayResult;
import cn.daxpay.single.result.pay.RefundResult;
import cn.daxpay.single.service.annotation.PaymentSign;
import cn.daxpay.single.service.annotation.InitPaymentContext;
import cn.daxpay.single.service.core.payment.cancel.service.PayCancelService;
import cn.daxpay.single.service.core.payment.close.service.PayCloseService;
import cn.daxpay.single.service.core.payment.pay.service.PayService;
import cn.daxpay.single.service.core.payment.refund.service.RefundService;
@@ -38,6 +41,7 @@ public class UniPayController {
private final PayService payService;
private final RefundService refundService;
private final PayCloseService payCloseService;
private final PayCancelService payCancelService;
@PaymentSign
@InitPaymentContext(PaymentApiCode.PAY)
@@ -55,6 +59,14 @@ public class UniPayController {
return DaxRes.ok(payCloseService.close(param));
}
@PaymentSign
@InitPaymentContext(PaymentApiCode.CANCEL)
@Operation(summary = "支付撤销接口")
@PostMapping("/cancel")
public DaxResult<PayCancelResult> cancel(@RequestBody PayCancelParam param){
return DaxRes.ok(payCancelService.cancel(param));
}
@PaymentSign
@InitPaymentContext(PaymentApiCode.REFUND)
@Operation(summary = "统一退款接口")

View File

@@ -0,0 +1,25 @@
package cn.daxpay.single.service.code;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 订单关闭类型
* @author xxm
* @since 2024/6/5
*/
@Getter
@AllArgsConstructor
public enum PayCloseTypeEnum {
/**
* 订单关闭
*/
CLOSE("close", "订单关闭"),
/**
* 订单撤销
*/
CANCEL("cancel", "订单撤销"),
;
final String code;
final String name;
}

View File

@@ -1,24 +1,24 @@
package cn.daxpay.single.service.core.channel.alipay.service;
import cn.bootx.platform.common.spring.exception.RetryableException;
import cn.daxpay.single.code.PaySyncStatusEnum;
import cn.daxpay.single.service.code.AliPayCode;
import cn.daxpay.single.service.core.payment.sync.result.PayRemoteSyncResult;
import cn.daxpay.single.service.core.order.pay.entity.PayOrder;
import cn.daxpay.single.exception.pay.PayFailureException;
import cn.daxpay.single.service.code.AliPayCode;
import cn.daxpay.single.service.core.order.pay.entity.PayOrder;
import cn.daxpay.single.service.core.payment.sync.result.PayRemoteSyncResult;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradeCancelModel;
import com.alipay.api.domain.AlipayTradeCloseModel;
import com.alipay.api.response.AlipayTradeCancelResponse;
import com.alipay.api.response.AlipayTradeCloseResponse;
import com.ijpay.alipay.AliPayApi;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* 支付宝支付取消和支付关闭
* 支付宝支付撤销和支付关闭
*
* @author xxm
* @since 2021/4/20
@@ -30,15 +30,13 @@ public class AliPayCloseService {
private final AliPaySyncService aliPaySyncService;
/**
* 关闭支付 此处使用交易关闭接口, 支付宝支持 交易关闭 和 交易撤销 两种关闭订单的方式, 区别如下
* 关闭支付 此处使用交易关闭接口
* 交易关闭: 只有订单在未支付的状态下才可以进行关闭, 商户不需要额外申请就有此接口的权限
* 交易撤销: 如果用户支付成功,会将此订单资金退还给用户. 限制时间为1天过了24小时该接口无法再使用。可以视为一个特殊的接口, 需要专门签约这个接口的权限
* <p>
* 1. 如果未查询到支付单,视为支付关闭,
* 2. 如果返回"当前交易状态不支持此操作", 同步网关支付状态, 判断网关是否已经被关闭
*
*/
@Retryable(value = RetryableException.class)
public void close(PayOrder payOrder) {
AlipayTradeCloseModel model = new AlipayTradeCloseModel();
model.setOutTradeNo(payOrder.getOrderNo());
@@ -65,6 +63,38 @@ public class AliPayCloseService {
}
}
/**
* 交易撤销: 如果用户支付成功,会将此订单资金退还给用户. 限制时间为1天过了24小时该接口无法再使用。
* 可以视为一个特殊的接口, 需要专门签约这个接口的权限
*/
public void cancel(PayOrder payOrder) {
AlipayTradeCancelModel model = new AlipayTradeCancelModel();
model.setOutTradeNo(payOrder.getOrderNo());
try {
AlipayTradeCancelResponse response = AliPayApi.tradeCancelToResponse(model);
if (!Objects.equals(AliPayCode.SUCCESS, response.getCode())) {
if (!Objects.equals(AliPayCode.SUCCESS, response.getCode())) {
// 如果返回"当前交易状态不支持此操作", 同步网关支付状态, 判断网关是否已经被关闭
if (Objects.equals(response.getSubCode(),AliPayCode.ACQ_TRADE_STATUS_ERROR)){
if (this.syncStatus(payOrder)){
return;
}
}
// 返回"交易不存在"视同关闭成功
if (Objects.equals(response.getSubCode(),AliPayCode.ACQ_TRADE_NOT_EXIST)){
return;
}
log.error("网关返回关闭失败: {}", response.getSubMsg());
throw new PayFailureException(response.getSubMsg());
}
}
} catch (AlipayApiException e) {
log.error("关闭订单失败:", e);
throw new PayFailureException("关闭订单失败");
}
}
/**
* 关闭失败后, 获取支付网关的状态, 如果是关闭返回true, 其他情况抛出异常

View File

@@ -1,10 +1,10 @@
package cn.daxpay.single.service.core.channel.wechat.service;
import cn.bootx.platform.common.spring.exception.RetryableException;
import cn.daxpay.single.exception.pay.PayFailureException;
import cn.daxpay.single.service.code.WeChatPayCode;
import cn.daxpay.single.service.core.channel.wechat.entity.WeChatPayConfig;
import cn.daxpay.single.service.core.order.pay.entity.PayOrder;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import com.ijpay.core.enums.SignType;
import com.ijpay.core.kit.WxPayKit;
@@ -12,9 +12,9 @@ import com.ijpay.wxpay.WxPayApi;
import com.ijpay.wxpay.model.CloseOrderModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import java.io.ByteArrayInputStream;
import java.util.Map;
/**
@@ -31,7 +31,6 @@ public class WeChatPayCloseService {
/**
* 关闭支付, 微信对已经关闭的支付单也可以重复关闭
*/
@Retryable(value = RetryableException.class)
public void close(PayOrder payOrder, WeChatPayConfig weChatPayConfig) {
// 只有部分需要调用微信网关进行关闭
Map<String, String> params = CloseOrderModel.builder()
@@ -47,6 +46,32 @@ public class WeChatPayCloseService {
this.verifyErrorMsg(result);
}
/**
* 撤销接口
*/
public void cancel(PayOrder payOrder, WeChatPayConfig weChatPayConfig){
// 只有部分需要调用微信网关进行关闭
Map<String, String> params = CloseOrderModel.builder()
.appid(weChatPayConfig.getWxAppId())
.mch_id(weChatPayConfig.getWxMchId())
.out_trade_no(payOrder.getOrderNo())
.nonce_str(WxPayKit.generateStr())
.build()
.createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256);
// 获取证书文件
if (StrUtil.isBlank(weChatPayConfig.getP12())){
String errorMsg = "微信p.12证书未配置,无法进行退款";
throw new PayFailureException(errorMsg);
}
byte[] fileBytes = Base64.decode(weChatPayConfig.getP12());
ByteArrayInputStream inputStream = new ByteArrayInputStream(fileBytes);
// 证书密码为 微信商户号
String xmlResult = WxPayApi.orderReverse(params,inputStream,weChatPayConfig.getWxMchId());
Map<String, String> result = WxPayKit.xmlToMap(xmlResult);
this.verifyErrorMsg(result);
}
/**
* 验证错误信息
*/

View File

@@ -2,6 +2,7 @@ package cn.daxpay.single.service.core.order.pay.entity;
import cn.bootx.platform.common.core.function.EntityBaseFunction;
import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity;
import cn.daxpay.single.code.PayMethodEnum;
import cn.daxpay.single.code.PayOrderAllocStatusEnum;
import cn.daxpay.single.code.PayChannelEnum;
import cn.daxpay.single.code.PayStatusEnum;
@@ -71,6 +72,7 @@ public class PayOrder extends MpBaseEntity implements EntityBaseFunction<PayOrde
/**
* 支付方式
* @see PayMethodEnum
*/
@DbColumn(comment = "支付方式")
private String method;

View File

@@ -28,7 +28,10 @@ public class PayOrderService {
private final PayExpiredTimeService expiredTimeService;
// 支付完成常量集合
private final List<String> ORDER_FINISH = Arrays.asList(PayStatusEnum.CLOSE.getCode(), PayStatusEnum.SUCCESS.getCode());
private final List<String> ORDER_FINISH = Arrays.asList(
PayStatusEnum.CLOSE.getCode(),
PayStatusEnum.CANCEL.getCode(),
PayStatusEnum.SUCCESS.getCode());
/**
* 查询

View File

@@ -0,0 +1,38 @@
package cn.daxpay.single.service.core.payment.cancel.factory;
import cn.daxpay.single.code.PayChannelEnum;
import cn.daxpay.single.exception.pay.PayUnsupportedMethodException;
import cn.daxpay.single.service.core.payment.cancel.strategy.AliPayCancelStrategy;
import cn.daxpay.single.service.core.payment.cancel.strategy.WeChatPayCancelStrategy;
import cn.daxpay.single.service.func.AbsPayCancelStrategy;
import cn.hutool.extra.spring.SpringUtil;
import lombok.experimental.UtilityClass;
/**
* 订单撤销策略工厂
* @author xxm
* @since 2024/6/5
*/
@UtilityClass
public class PayCancelStrategyFactory {
/**
* 根据传入的支付通道创建策略
* @return 支付策略
*/
public static AbsPayCancelStrategy create(String channel) {
PayChannelEnum channelEnum = PayChannelEnum.findByCode(channel);
AbsPayCancelStrategy strategy;
switch (channelEnum) {
case ALI:
strategy = SpringUtil.getBean(AliPayCancelStrategy.class);
break;
case WECHAT:
strategy = SpringUtil.getBean(WeChatPayCancelStrategy.class);
break;
default:
throw new PayUnsupportedMethodException();
}
return strategy;
}
}

View File

@@ -0,0 +1,113 @@
package cn.daxpay.single.service.core.payment.cancel.service;
import cn.bootx.platform.common.core.exception.RepetitiveOperationException;
import cn.daxpay.single.code.PayStatusEnum;
import cn.daxpay.single.exception.pay.PayFailureException;
import cn.daxpay.single.param.payment.pay.PayCancelParam;
import cn.daxpay.single.result.pay.PayCancelResult;
import cn.daxpay.single.service.code.PayCloseTypeEnum;
import cn.daxpay.single.service.common.local.PaymentContextLocal;
import cn.daxpay.single.service.core.order.pay.entity.PayOrder;
import cn.daxpay.single.service.core.order.pay.service.PayOrderQueryService;
import cn.daxpay.single.service.core.order.pay.service.PayOrderService;
import cn.daxpay.single.service.core.payment.cancel.factory.PayCancelStrategyFactory;
import cn.daxpay.single.service.core.payment.notice.service.ClientNoticeService;
import cn.daxpay.single.service.core.record.close.entity.PayCloseRecord;
import cn.daxpay.single.service.core.record.close.service.PayCloseRecordService;
import cn.daxpay.single.service.func.AbsPayCancelStrategy;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Objects;
/**
* 支付关闭和撤销服务
* @author xxm
* @since 2023/12/18
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PayCancelService {
private final PayOrderService payOrderService;
private final PayOrderQueryService payOrderQueryService;
private final PayCloseRecordService payCloseRecordService;
private final ClientNoticeService clientsService;;
private final LockTemplate lockTemplate;
/**
* 撤销支付
*/
public PayCancelResult cancel(PayCancelParam param){
PayOrder payOrder = payOrderQueryService.findByBizOrOrderNo(param.getOrderNo(), param.getBizOrderNo())
.orElseThrow(() -> new PayFailureException("支付订单不存在"));
LockInfo lock = lockTemplate.lock("payment:cancel:" + payOrder.getId(),10000, 50);
if (Objects.isNull(lock)){
throw new RepetitiveOperationException("支付订单已在撤销中,请勿重复发起");
}
try {
PayCancelResult result = new PayCancelResult();
// 状态检查, 只有支付中可以进行撤销支付
if (!Objects.equals(payOrder.getStatus(), PayStatusEnum.PROGRESS.getCode())) {
throw new PayFailureException("订单不是支付中, 无法进行撤销订单");
}
try {
AbsPayCancelStrategy strategy = PayCancelStrategyFactory.create(payOrder.getChannel());
// 设置支付订单
strategy.setOrder(payOrder);
// 撤销前准备
strategy.doBeforeCancelHandler();
// 执行撤销策略
strategy.doCancelHandler();
// 成功处理
this.successHandler(payOrder);
// 返回结果
return result;
} catch (Exception e) {
log.error("撤销订单失败, id: {}:", payOrder.getId());
log.error("撤销订单失败:", e);
// 记录撤销失败的记录
this.saveRecord(payOrder, false, e.getMessage());
throw new PayFailureException("撤销订单失败");
}
} finally {
lockTemplate.releaseLock(lock);
}
}
/**
* 成功后处理方法
*/
private void successHandler(PayOrder payOrder){
// 撤销订单
payOrder.setStatus(PayStatusEnum.CANCEL.getCode())
.setCloseTime(LocalDateTime.now());
payOrderService.updateById(payOrder);
// 发送通知
clientsService.registerPayNotice(payOrder,null);
this.saveRecord(payOrder,true,null);
}
/**
* 保存撤销记录
*/
private void saveRecord(PayOrder payOrder, boolean closed, String errMsg){
String clientIp = PaymentContextLocal.get()
.getRequestInfo()
.getClientIp();
PayCloseRecord record = new PayCloseRecord()
.setOrderNo(payOrder.getOrderNo())
.setBizOrderNo(payOrder.getBizOrderNo())
.setChannel(payOrder.getChannel())
.setCloseType(PayCloseTypeEnum.CANCEL.getCode())
.setClosed(closed)
.setErrorMsg(errMsg)
.setClientIp(clientIp);
payCloseRecordService.saveRecord(record);
}
}

View File

@@ -0,0 +1,51 @@
package cn.daxpay.single.service.core.payment.cancel.strategy;
import cn.daxpay.single.code.PayChannelEnum;
import cn.daxpay.single.service.core.channel.alipay.entity.AliPayConfig;
import cn.daxpay.single.service.core.channel.alipay.service.AliPayCloseService;
import cn.daxpay.single.service.core.channel.alipay.service.AliPayConfigService;
import cn.daxpay.single.service.func.AbsPayCancelStrategy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**
* 支付宝撤销策略
* @author xxm
* @since 2024/6/5
*/
@Slf4j
@Scope(SCOPE_PROTOTYPE)
@Service
@RequiredArgsConstructor
public class AliPayCancelStrategy extends AbsPayCancelStrategy {
private final AliPayConfigService alipayConfigService;
private final AliPayCloseService aliPayCloseService;
@Override
public PayChannelEnum getChannel() {
return PayChannelEnum.ALI;
}
/**
* 关闭前的处理方式
*/
@Override
public void doBeforeCancelHandler() {
AliPayConfig config = alipayConfigService.getConfig();
alipayConfigService.initConfig(config);
}
/**
* 关闭操作
*/
@Override
public void doCancelHandler() {
aliPayCloseService.cancel(this.getOrder());
}
}

View File

@@ -0,0 +1,58 @@
package cn.daxpay.single.service.core.payment.cancel.strategy;
import cn.daxpay.single.code.PayChannelEnum;
import cn.daxpay.single.code.PayMethodEnum;
import cn.daxpay.single.service.core.channel.wechat.entity.WeChatPayConfig;
import cn.daxpay.single.service.core.channel.wechat.service.WeChatPayCloseService;
import cn.daxpay.single.service.core.channel.wechat.service.WeChatPayConfigService;
import cn.daxpay.single.service.func.AbsPayCancelStrategy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import java.util.Objects;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**
* 微信支付关闭策略
* @author xxm
* @since 2023/12/30
*/
@Slf4j
@Scope(SCOPE_PROTOTYPE)
@Service
@RequiredArgsConstructor
public class WeChatPayCancelStrategy extends AbsPayCancelStrategy {
private final WeChatPayConfigService weChatPayConfigService;
private final WeChatPayCloseService weChatPayCloseService;
private WeChatPayConfig weChatPayConfig;
@Override
public PayChannelEnum getChannel() {
return PayChannelEnum.WECHAT;
}
/**
* 关闭前的处理方式
*/
@Override
public void doBeforeCancelHandler() {
// 非当面付订单不可以关闭
if (!Objects.equals(this.getOrder().getMethod(), PayMethodEnum.BARCODE.getCode())) {
throw new RuntimeException("微信当面付订单才可以被撤销");
}
this.weChatPayConfig = weChatPayConfigService.getConfig();
}
/**
* 关闭操作
*/
@Override
public void doCancelHandler() {
weChatPayCloseService.cancel(this.getOrder(), weChatPayConfig);
}
}

View File

@@ -5,6 +5,7 @@ import cn.daxpay.single.code.PayStatusEnum;
import cn.daxpay.single.exception.pay.PayFailureException;
import cn.daxpay.single.param.payment.pay.PayCloseParam;
import cn.daxpay.single.result.pay.PayCloseResult;
import cn.daxpay.single.service.code.PayCloseTypeEnum;
import cn.daxpay.single.service.common.local.PaymentContextLocal;
import cn.daxpay.single.service.core.order.pay.entity.PayOrder;
import cn.daxpay.single.service.core.order.pay.service.PayOrderQueryService;
@@ -43,7 +44,7 @@ public class PayCloseService {
* 关闭支付
*/
public PayCloseResult close(PayCloseParam param){
PayOrder payOrder = payOrderQueryService.findByBizOrOrderNo(param.getOrderNo(), param.getBizTradeNo())
PayOrder payOrder = payOrderQueryService.findByBizOrOrderNo(param.getOrderNo(), param.getBizOrderNo())
.orElseThrow(() -> new PayFailureException("支付订单不存在"));
LockInfo lock = lockTemplate.lock("payment:close:" + payOrder.getId(),10000, 50);
if (Objects.isNull(lock)){
@@ -83,7 +84,6 @@ public class PayCloseService {
// 记录关闭失败的记录
this.saveRecord(payOrder, false, e.getMessage());
throw new PayFailureException("关闭订单失败");
}
}
@@ -111,6 +111,7 @@ public class PayCloseService {
.setOrderNo(payOrder.getOrderNo())
.setBizOrderNo(payOrder.getBizOrderNo())
.setChannel(payOrder.getChannel())
.setCloseType(PayCloseTypeEnum.CLOSE.getCode())
.setClosed(closed)
.setErrorMsg(errMsg)
.setClientIp(clientIp);

View File

@@ -200,7 +200,7 @@ public class PayAssistService {
throw new PayFailureException("已经支付成功,请勿重新支付");
}
// 支付失败类型状态
List<String> tradesStatus = Arrays.asList(FAIL.getCode(), CLOSE.getCode());
List<String> tradesStatus = Arrays.asList(FAIL.getCode(), CLOSE.getCode(), CANCEL.getCode());
if (tradesStatus.contains(payOrder.getStatus())) {
throw new PayFailureException("支付失败或已经被关闭");
}

View File

@@ -83,6 +83,7 @@ public class RefundAssistService {
List<String> tradesStatus = Arrays.asList(
PayStatusEnum.PROGRESS.getCode(),
PayStatusEnum.CLOSE.getCode(),
PayStatusEnum.CANCEL.getCode(),
PayStatusEnum.REFUNDED.getCode(),
PayStatusEnum.REFUNDING.getCode(),
PayStatusEnum.FAIL.getCode());

View File

@@ -74,8 +74,7 @@ public class PayRepairService {
repairResult.setAfterPayStatus(PayStatusEnum.CLOSE);
break;
case PROGRESS:
this.waitPay(order);
repairResult.setAfterPayStatus(PayStatusEnum.PROGRESS);
// TODO 保存为异常订单
break;
case CLOSE_GATEWAY:
this.closeRemote(order, repairStrategy);
@@ -93,17 +92,6 @@ public class PayRepairService {
return repairResult;
}
/**
* 变更未待支付
* TODO 后期保存为异常订单
*/
private void waitPay(PayOrder order) {
// 修改订单支付状态为待支付
order.setStatus(PayStatusEnum.PROGRESS.getCode())
.setPayTime(null)
.setCloseTime(null);
payOrderService.updateById(order);
}
/**
* 变更为已支付

View File

@@ -33,7 +33,6 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -163,7 +162,7 @@ public class PaySyncService {
未找到订单可能是发起支付用户未操作、支付已关闭、交易未找到三种情况
所以需要根据本地订单不同的状态进行特殊处理, 此处视为支付已关闭、交易未找到这两种, 处理方式相同, 都作为支付关闭处理
*/
List<String> payCloseEnums = Collections.singletonList(PayStatusEnum.CLOSE.getCode());
List<String> payCloseEnums = Arrays.asList(PayStatusEnum.CLOSE.getCode(), PayStatusEnum.CANCEL.getCode());
List<PaySyncStatusEnum> syncClose = Arrays.asList(CLOSED, NOT_FOUND, NOT_FOUND_UNKNOWN);
if (payCloseEnums.contains(orderStatus) && syncClose.contains(syncStatus)){
return true;

View File

@@ -3,6 +3,7 @@ package cn.daxpay.single.service.core.record.close.entity;
import cn.bootx.platform.common.core.function.EntityBaseFunction;
import cn.bootx.platform.common.mybatisplus.base.MpCreateEntity;
import cn.daxpay.single.code.PayChannelEnum;
import cn.daxpay.single.service.code.PayCloseTypeEnum;
import cn.daxpay.single.service.core.record.close.convert.PayCloseRecordConvert;
import cn.daxpay.single.service.dto.record.close.PayCloseRecordDto;
import cn.bootx.table.modify.annotation.DbColumn;
@@ -39,6 +40,13 @@ public class PayCloseRecord extends MpCreateEntity implements EntityBaseFunction
@DbColumn(comment = "关闭的支付通道")
private String channel;
/**
* 关闭类型 关闭/撤销
* @see PayCloseTypeEnum
*/
@DbColumn(comment = "关闭类型")
private String closeType;
/**
* 是否关闭成功
*/

View File

@@ -3,6 +3,7 @@ package cn.daxpay.single.service.dto.record.close;
import cn.bootx.platform.common.core.rest.dto.BaseDto;
import cn.daxpay.single.code.PayChannelEnum;
import cn.bootx.table.modify.annotation.DbColumn;
import cn.daxpay.single.service.code.PayCloseTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -34,6 +35,13 @@ public class PayCloseRecordDto extends BaseDto {
@DbColumn(comment = "关闭的异步支付通道")
private String channel;
/**
* 关闭类型 关闭/撤销
* @see PayCloseTypeEnum
*/
@DbColumn(comment = "关闭类型")
private String closeType;
/**
* 是否关闭成功
*/

View File

@@ -0,0 +1,28 @@
package cn.daxpay.single.service.func;
import cn.daxpay.single.service.core.order.pay.entity.PayOrder;
import lombok.Getter;
import lombok.Setter;
/**
* 支付订单撤销接口
* @author xxm
* @since 2024/6/5
*/
@Getter
@Setter
public abstract class AbsPayCancelStrategy implements PayStrategy {
/** 支付订单 */
private PayOrder order = null;
/**
* 撤销前的处理方式
*/
public void doBeforeCancelHandler() {}
/**
* 撤销操作
*/
public abstract void doCancelHandler();
}

View File

@@ -38,7 +38,7 @@ public class PayCloseRecordQuery extends QueryOrder {
* 是否关闭成功
*/
@DbColumn(comment = "是否关闭成功")
private boolean closed;
private Boolean closed;
/** 错误码 */
@DbColumn(comment = "错误码")