支付系统中更新业务数据的正确方式,如何做到支付和业务状态隔离?事务如何控制?

在《定时任务查单、关单、退款功能实战》一文中实现了退款申请、退款通知回调接口的功能,同支付功能一样,退款也需要定时查询退款单的状态,这一篇主要实现以下3个功能点:

  • 定时任务查退款单:出现网络异常时,可通过主动定时任务查单来确定退款的实际状态。每隔30秒查询创建超过5分钟未处理的退款单进行处理。

  • 补充支付、退款用到的枚举状态类。

  • 业务数据如何做到和微信支付相关的表的事务隔离,并添加异常处理。

定时任务查询退款单

每隔30秒查询创建超过5分钟未处理的退款单进行处理。
在WxPayTask类中 添加如下方法:

 /**
* 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单
*/

@Scheduled(cron = "0/30 * * * * ?")
@PostMapping(value = "/refundConfirm")
public void refundConfirm() throws Exception {
log.info("refundConfirm 被执行......");

//找出申请退款超过5分钟并且未成功的退款单
List<DepositRefundInfo> refundInfoList = refundInfoService.getNoRefundOrderByDuration(-5);

for (DepositRefundInfo refundInfo : refundInfoList) {
String refundNo = refundInfo.getRefundNo();
log.warn("超时未退款的退款单号 ===> {}", refundNo);

//核实订单状态:调用微信支付查询退款接口
wxPayService.checkRefundStatus(refundNo);
}
}

上述代码中,首先找出 ,申请退款超过5分钟并且未成功的退款单:

  @Override
public List<DepositRefundInfo> getNoRefundOrderByDuration(int minutes) throws Exception {
//minutes分钟之前的时间
Date time = DataTypeUtils.getTimeSec(minutes);
// Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
// Date time = DataTypeUtils.parseRFC3339(instant.toString(),"yyyy-MM-ddTHH:mm:ss.XXX");
// Date time1 = DataTypeUtils.parseDate(DataTypeUtils.formatDateTime(time));
QueryWrapper<DepositRefundInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("refund_status", WxRefundStatus.PROCESSING.getType());
queryWrapper.le("create_time",time);
return baseMapper.selectList(queryWrapper);
}

在退款下单创建退款单记录时,默认存的状态为退款中的状态即processing,如果是此状态说明还未返回实际的退款状态,或者说退款申请成功了,但是系统的状态还未更新,这种情况就要调用微信的查单接口查询实际的退款状态,以便更新到我们的系统中。

循环查询的退款单列表,针对每一条处理中的退款单进行处理,如果状态异常就更新退款申请单状态为异常,如果退款成功则更新退款单和退款申请单的状态为成功,并更新订单表的已退金额,退款结果,订单状态。具体的处理逻辑如下:

 /**
* 根据退款单号核实退款单状态
*
* @param refundNo
* @return
*/

@Transactional(rollbackFor = Exception.class)
@Override
public void checkRefundStatus(String refundNo) throws Exception {

log.info("定时任务核实退款单");
log.warn("根据退款单号核实退款单状态 ===> {}", refundNo);
//组装json请求体字符串
Gson gson = new Gson();

//获取退款通知回调锁
RLock lockRefundNotify = redisson.getLock(LockType.REFUND_NOTIFY_LOCK.getType());

log.info("定时任务执行...获取退款通知回调锁");

try {
if (lockRefundNotify.tryLock(5, 30, TimeUnit.SECONDS)) {
//调用微信退款单查单接口
Refund refund = this.queryRefund(refundNo);
if (refund == null) {
log.warn("查询退款单接口出现异常,返回null");
return;
}
String plainText = gson.toJson(refund);
//获取微信退款状态
String status = refund.getStatus().name();
String orderNo = refund.getOutTradeNo();
//获取退款单的实际金额
long payerRefund = refund.getAmount().getPayerRefund();

//获取订单状态,如果订单状态还未处理,说明系统还未接收到微信的通知,如果不是未处理,说明已经处理过了
// 所以一定要保证,查单或回调后更新订单的状态为实际退款状态
DepositOrderInfo orderInfo = depositOrderInfoService.getOrderStatus(orderNo);

if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderInfo.getOrderStatus())) {
return;
}

if (WxRefundStatus.SUCCESS.getType().equals(status)) {

log.warn("核实订单已退款成功 ===> {}", refundNo);
/**
* 如果本笔退款成功则 需要判断 当前订单的已退金额+本次退款的金额是否=订单的总金额,如果未退完则
* 更新订单表中退款状态为部分退款,如果退完则更新为已全部退完,并更新已退金额
*/


//如果确认退款成功,则更新订单状态和已退金额
depositOrderInfoService.updateOrderRefundInfoByOrderNo(orderNo, payerRefund);
//更新退款单
depositRefundInfoService.updateRefund(refund);

log.info("退款成功!开始更新业务数据");
//更新业务数据
TradeInfoInVo tradeInfoInVo = new TradeInfoInVo();
tradeInfoInVo.setPayTradeNo(refundNo);
tradeInfoInVo.setEventType(TradeLogType.REFUND.getCode());
tradeInfoInVo.setTradeAmount(payerRefund);
//退款成功时间
tradeInfoInVo.setTradeTime(refund.getSuccessTime());
tradeInfoInVo.setUserId(orderInfo.getUserId());
//成功
tradeInfoInVo.setTradeStatus(WxRefundStatus.SUCCESS.getType());
this.updateBusiDataByPayInfo(tradeInfoInVo);

}

// 退款异常(订单状态更新为退款异常,如果没有退完,下次再退又更新为退款中)
if (WxRefundStatus.ABNORMAL.getType().equals(status)) {

log.warn("核实订单退款异常 ===> {}", refundNo);
//如果确认退款成功,则更新订单状态为退款异常(此状态只能代表上一次退款异常的状态)
depositOrderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
//更新退款单
depositRefundInfoService.updateRefund(refund);
//更新退款申请单
TradeInfoInVo tradeInfoInVo = new TradeInfoInVo();
tradeInfoInVo.setPayTradeNo(refundNo);
tradeInfoInVo.setEventType(TradeLogType.REFUND.getCode());
tradeInfoInVo.setTradeAmount(payerRefund);
tradeInfoInVo.setUserId(orderInfo.getUserId());
tradeInfoInVo.setTradeStatus(WxRefundStatus.ABNORMAL.getType());
//退款失败的时候业务处理时间
tradeInfoInVo.setTradeTime(refund.getCreateTime());
appDepositRefundApplicationService.updateRefundApplicationInfo(tradeInfoInVo);
}
}
} catch (Exception e) {
log.error("定时任务查询退款单出错" + e.getMessage());
throw new RuntimeException("定时任务查询退款单出错");
} finally {
if (lockRefundNotify.isHeldByCurrentThread()) {
log.info("定时任务执行--退款通知回调锁释放");
//要主动释放锁
lockRefundNotify.unlock();
}
}
}

上述代码中更新退款单的状态时,这里要注意的是 退款回调时拿到的响应数据(RefundNotification)和查退款单返回的响应数据(Refund)不是同一个对象,所以在更新退款单的公共方法中使用了泛型。方法如下:

// 接口
<T> void updateRefund(T refundResult) throws Exception;
//实现
@Override
public <T> void updateRefund(T refundResult) throws Exception {

Gson gson = new Gson();
assert refundResult!=null;
String content = gson.toJson(refundResult);

//转换成map,泛型处理
HashMap<String,Object> resultMap = gson.fromJson(content,HashMap.class);

//根据退款单编号修改退款单
QueryWrapper<DepositRefundInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("refund_no", resultMap.get("out_refund_no"));

//设置要修改的字段
DepositRefundInfo refundInfo = new DepositRefundInfo();
//微信支付退款单号
refundInfo.setRefundId(resultMap.get("refund_id").toString());

//查询退款和申请退款中的返回参数
if(resultMap.get("status") != null){
//退款状态
refundInfo.setRefundStatus(resultMap.get("status").toString());
//将全部响应结果(查单或退款下单返回)存入
refundInfo.setContentReturn(content);
}
//退款回调中的回调参数
if(resultMap.get("refund_status") != null){
//回调给的退款状态
refundInfo.setRefundStatus(resultMap.get("refund_status").toString());
//回调结果存放
refundInfo.setContentNotify(content);
}
if(resultMap.get("create_time") != null){
//退款受理时间,主要用于退款失败的情况下使用
Date createTime = DataTypeUtils.parseRFC3339(resultMap.get("create_time").toString());
String createTimeStr = DataTypeUtils.formatDateTime(createTime);
refundInfo.setRefundTime(DataTypeUtils.parseDate(createTimeStr));
}
if(resultMap.get("success_time") != null){
//退款成功时间
Date dateTime = DataTypeUtils.parseRFC3339(resultMap.get("success_time").toString());
String dateTimeStr = DataTypeUtils.formatDateTime(dateTime);
refundInfo.setRefundTime(DataTypeUtils.parseDate(dateTimeStr));
}
//更新退款单
this.update(refundInfo, queryWrapper);
}

枚举类

这些类主要枚举了订单的本地状态,微信返回的支付状态、微信退款状态,redis分布式锁的粒度。分别如下:

微信通知回调uri

@AllArgsConstructor
@Getter
public enum WxNotifyType {

/**
* 支付通知
*/

NATIVE_NOTIFY("api/wx-pay/jsapi/notify"),

/**
* 退款结果通知
*/

REFUND_NOTIFY("/api/wx-pay/refunds/notify");

/**
* 类型
*/

private final String type;
}

微信返回的退款状态

@AllArgsConstructor
@Getter
public enum WxRefundStatus {

/**
* 退款成功
*/

SUCCESS("SUCCESS"),

/**
* 退款关闭
*/

CLOSED("CLOSED"),

/**
* 退款处理中
*/

PROCESSING("PROCESSING"),

/**
* 退款异常
*/

ABNORMAL("ABNORMAL");

/**
* 类型
*/

private final String type;
}

订单实时的交易状态

@AllArgsConstructor
@Getter
public enum WxTradeState {

/**
* 支付成功
*/

SUCCESS("SUCCESS"),

/**
* 未支付
*/

NOTPAY("NOTPAY"),

/**
* 已关闭
*/

CLOSED("CLOSED"),

/**
* 转入退款
*/

REFUND("REFUND");

/**
* 类型
*/

private final String type;
}

系统内部订单状态

@AllArgsConstructor
@Getter
public enum OrderStatus {
/**
* 未支付
*/

NOTPAY("NOTPAY"),

/**
* 支付成功
*/

SUCCESS("SUCCESS"),

/**
* 支付失败
*/

PAYERROR("PAYERROR"),

/**
* 已关闭-超时已关闭
*/

CLOSED("CLOSED"),

/**
* 已取消-用户已取消
*/

CANCEL("CANCEL"),

/**
* 部分退款(只退了一部分)
*/


REFUND_PART("REFUND_PART"),

/**
* 退款中
*/

REFUND_PROCESSING("REFUND_PROCESSING"),

/**
* 未退 -默认值
*/

NO_REFUND("NO_REFUND"),

/**
* 已退款(全部退完)
*/

REFUND_SUCCESS("REFUND_SUCCESS"),

/**
* 退款异常
*/

REFUND_ABNORMAL("REFUND_ABNORMAL");

/**
* 类型
*/

private final String type;
}

业务日志类型枚举

@AllArgsConstructor
@Getter
public enum TradeLogType {
/**
* 更新金额异常
*/

AMOUNT("AMOUNT","更新金额"),

/**
* 插入使用记录异常
*/

USERECORD("USERECORD","插入使用记录"),

/**
* 支付
*/

PAY("PAY","支付"),

/**
* 退款
*/

REFUND("REFUND","退款"),

/**
* 冻结
*/


DJ("DJ","冻结"),

/**
* 解冻
*/

JD("JD", "解冻"),

/**
* 违约扣减
*/

WYKJ("KJ", "违约扣减"),

/**
* REFUND_APPLICATION
*/

REFUND_APPLICATION("REFUND_APPLICATION", "退款申请单"),

/**
* 竞价结束定时器自动解冻出现异常
*/

DSQYC("DSQYC", "竞价结束定时器异常");

/**
* 类型
*/

private final String code;
private final String type;
}

redisson锁枚举

@AllArgsConstructor
@Getter
public enum LockType {

/**
* 更新余额使用锁
*/

AMOUNT_LOCK("AMOUNT_LOCK","更新余额锁"),

/**
* 更新余额使用锁
*/

BID_LOCK("BID_LOCK","竞价锁"),

/**
* 支付回调使用的锁
*/

PAY_NOTIFY_LOCK("PAY_NOTIFY_LOCK","支付回调锁"),

/**
* 退款回调使用的锁
*/

REFUND_NOTIFY_LOCK("REFUND_NOTIFY_LOCK","退款回调锁"),

/**
* 已退金额锁
*/

YT_MONEY_LOCK("YT_MONEY_LOCK","已退金额锁");

/**
* 类型
*/

private final String type;
/**
* 描述
*/

private final String desc;
}

业务异常日志类型枚举

public enum PayErrorLogType {

//操作类型
PAY("PAY", "PAY", "handlePayError"),//支付
REFUND("REFUND", "REFUND", "handleRefundError"),//提现
//业务错误码
AMOUNT("AMOUNT", "AMOUNT", "handleAmountError"),//更新余额
USERECORD("USERECORD", "USERECORD", "handleUserecordError"),//插入使用记录
REFUND_APPLICATION("REFUND_APPLICATION", "REFUND_APPLICATION", "handleRefundApplicationError"),//更新退款申请单
DSQYC("DSQYC", "DSQYC", "handleTimerError");
private String key;

private String value;

// 服务名称
private String methodName;

private PayErrorLogType(String key, String value, String methodName) {
this.key = key;
this.value = value;
this.methodName = methodName;
}

public String getKey() {
return this.key;
}

public String getValue() {
return this.value;
}

public String getMethodName() {
return this.methodName;
}

@Override
public String toString() {
return this.name();

}

//通过属性获取对象
public static PayErrorLogType getPayErrorLogTypeBykey(String key) {

for(PayErrorLogType at : PayErrorLogType.values()) {
if(key.equals(at.getKey()) ) {
return at;
}
}

return null;

}

public static String getPayErrorLogTypeValueByKey(String key) {

for(PayErrorLogType at : PayErrorLogType.values()) {
if(key.equals(at.getKey())) {
return at.getValue();
}
}
return null;
}

public static String getMethonNameEnumValueByKey(String key) {

for(PayErrorLogType at : PayErrorLogType.values()) {
if(key.equals(at.getKey())) {
return at.getMethodName();
}
}
return null;
}
}

业务数据更新

当完成微信支付及退款的

在前面的微信支付回调、定时任务查单、微信退款回调、定时任务查退款单 逻辑的最后都添加了保证金业务数据的更新逻辑,如下代码块:

//更新业务数据
TradeInfoInVo tradeInfoInVo = new TradeInfoInVo();
tradeInfoInVo.setPayTradeNo(refundNo);
tradeInfoInVo.setEventType(TradeLogType.REFUND.getCode());
tradeInfoInVo.setTradeAmount(payerRefund);
//退款成功时间
tradeInfoInVo.setTradeTime(refund.getSuccessTime());
tradeInfoInVo.setUserId(orderInfo.getUserId());
//成功
tradeInfoInVo.setTradeStatus(WxRefundStatus.SUCCESS.getType());
this.updateBusiDataByPayInfo(tradeInfoInVo);

目前的业务逻辑有两种,一种是支付要更新保证金相关的表,一个是退款也要更新保证金相关的表。而支付和退款本身也有更新订单状态和退款单或支付流水的业务。为了保证更新业务数据和微信支付退款的业务隔离开来,在this.updateBusiDataByPayInfo(tradeInfoInVo);中使用了开启异步任务的方式来处理业务数据,因为使用异步任务后就会涉及多线程,而多线程时事务不共享的。这里需要注意的是要保证下游的数据库能支撑住,因为异步任务越多越会对数据库造成压力。
this.updateBusiDataByPayInfo(tradeInfoInVo);具体的代码如下:

private void updateBusiDataByPayInfo(TradeInfoInVo tradeInfoInVo) {

//开启新任务、新事务完成余额更新,涉及多线程事务不共享
Runnable newTask = () -> {
log.info("更新余额异步任务开始,当前线程名字为:" + Thread.currentThread().getName());

//获取更新余额的锁
RLock lockAmount = redisson.getLock(LockType.AMOUNT_LOCK.getType());
//加锁,阻塞式等待,没有拿到锁等着,默认使用30s有效期,使用看门狗
lockAmount.lock();
try {
log.info("拿到更新余额锁===");
//更新金额和使用记录
appDepositAmountService.updateAmountInfo(tradeInfoInVo);
} catch (Exception e) {
e.printStackTrace();
log.error("更新余额信息出现异常===");
} finally {
log.info("更新余额锁释放===");
//释放锁
lockAmount.unlock();
}
log.info("更新余额异步任务结束");
};
THREAD_POOL_EXECUTOR.execute(newTask);
}

线程池的定义如下:

//定义线程池
private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR;

static {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("my-pool-%d")
.build();

THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(), threadFactory);
}
@Override
public void updateAmountInfo(TradeInfoInVo tradeInfoInVo) {

//根据交易编号、用户id、事件类型判断保证金记录是否已经插入,如果已经插入则不重复执行(避免回调和定时查单并发)
DepositUseRecord depositUseRecord = appDepositUseRecordService.queryDepositUseRecord(tradeInfoInVo);
if (depositUseRecord!=null){
log.warn("交易编号为{}的交易单已经更新业务数据,交易类型为{},用户为{}",tradeInfoInVo.getPayTradeNo(),tradeInfoInVo.getEventType(),tradeInfoInVo.getUserId());
return;
}
try {
//在这里异常捕获,外层无法接收到异常。做到和父方法的事务隔离。只要写了try catch就会使程序向下执行,所以要添加return给终止
this.updateAmount(tradeInfoInVo);
}catch (Exception e){
log.warn("更新保证金金额出现异常");
// 根据user_id查询更新后的金额
tradeInfoInVo.setEventDesc("更新保证金金额出现异常:当前用户userId为"+tradeInfoInVo.getUserId()+"" +
"的用户"+tradeInfoInVo.getEventType()+"了"+tradeInfoInVo.getTradeAmount()+"分,但是保证金的现存出现更新异常");
//更新余额异常则插入异常记录
tradeInfoInVo.setEventCode(TradeLogType.AMOUNT.getCode());
depositPayErrorLogService.insertTradeErrorInfo(tradeInfoInVo);
return;
}
try{
//更新余额 成功后插入使用记录表,记录充值过程
appDepositUseRecordService.insertUserRecord(tradeInfoInVo);
}catch (Exception e){
e.printStackTrace();
log.warn("插入保证金使用记录出现异常");
//更新余额异常则插入异常记录
tradeInfoInVo.setEventCode(TradeLogType.USERECORD.getCode());
tradeInfoInVo.setEventDesc("插入保证金使用记录出现异常,关联的交易单号为"+tradeInfoInVo.getPayTradeNo());
depositPayErrorLogService.insertTradeErrorInfo(tradeInfoInVo);
return;
}
//当前交易类型如果是退款需要更新退款申请单的信息
if (tradeInfoInVo.getEventType().equals(TradeLogType.REFUND.getCode())) {
try {
//更新退款申请单状态和实际退款时间
appDepositRefundApplicationService.updateRefundApplicationInfo(tradeInfoInVo);
} catch (Exception e) {
e.printStackTrace();
log.warn("更新退款申请单出现异常");
//更新余额异常则插入异常记录
tradeInfoInVo.setEventCode(TradeLogType.REFUND_APPLICATION.getCode());
tradeInfoInVo.setEventDesc("更新退款申请单出现异常");
depositPayErrorLogService.insertTradeErrorInfo(tradeInfoInVo);
}
}
}

上述业务代码中分为了3个部分

(1)更新保证金余额
(2)插入保证金使用记录
(3)更新退款申请单状态

这3个部分的处理本应该放到一个事务中进行,但是这里没加事务,而是使用了自定义异常处理,如果出现异常就会插入异常记录表,这里根据实际业务情况自定义。这里需要注意合理的使用try catch 和return 。

总结

具体更新业务数据这里就不详细说明了,本文主要目的是给大家讲解一下支付功能和实际业务的操作的设计思路,并完成退款的功能。最重要的是如何做到支付api与业务数据更新之间的事务隔离,如何处理具体的业务数据状态。

好了,到这里支付系统的核心功能就完成了,如果大家想实现完整的支付系统,可以查看往期支付相关的文章,将每一篇串联起来才具有参考意义。


原文始发于微信公众号(小核桃编程):支付系统中更新业务数据的正确方式,如何做到支付和业务状态隔离?事务如何控制?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/215969.html

(0)
李, 若俞的头像李, 若俞

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!