前言
关于核销这个概念,在大部分支付的场景下会存在很多,现在很多平台像美团、大众点评、携程等,你去薅了某个商家的羊毛...准确的说应该是买了某个商家的优惠套餐,会生成一个二维码,当你去店家消费的时候店家会去扫这个二维码进行消费确认,这就是核销; 其次就是请款、还款的一些业务逻辑也涉及到核销的流程,并且有时候这些操作并不是一次性的,需要客户根据自己的实际情况分批进行请款、还款的操作,当然这个金额每次也都是不一样的,直到结清才是真正的核销完毕。
核销业务逻辑
整个业务逻辑大概是这样子的:
- 就是我通过一些渠道生成请款单,请款单上有两个金额字段分别是请款总金额和还款总金额;
- 请款单--->item明细表,一对多关系,当然还会衍生另外几个字段已请款金额、已还款金额、未请款金额、未还款金额,每次根据还款、请款的金额,进行计算;
- 每次请款、还款的金额可以随意,会根据请款单明细的生成时间排序,进行核销,先生成的优先进行核销,直到全部金额核销完毕;
通用核销抽象类代码实现
ini
package com.yt.bms.bill.service;
import cn.hutool.core.util.ReflectUtil;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.util.Lists;
import org.springblade.core.tool.utils.CollectionUtil;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
public abstract class NuclearCancelAbstract<T, K> {
/**
* 更新核销字段
*
* @param t 待核销的类
* @param cancelAmount 核销金额
* @param updateList 待核销的类集合
* @param now 时间
* @param kList 根据此集合获取一些更新的字段
*/
public abstract void setNuclearCancelData(T t, BigDecimal cancelAmount, List<T> updateList, Date now, List<K> kList);
/**
* 核销
*
* @param realCancelList 使用此集合核销
* @param stayCancelList 待核销的集合
* @param uniqueNoField1 realCancelList核销的唯一标识,账单编号,或者订单号,或者运单号
* @param uniqueNoField2 stayCancelList核销的唯一标识,账单编号,或者订单号,或者运单号
* @param realAmountField 使用此字段核销
* @param compareAmountField 对比的金额字段
* @param now 时间
* @return
*/
public List<T> process(List<K> realCancelList, List<T> stayCancelList, String uniqueNoField1, String uniqueNoField2, String realAmountField, String compareAmountField, Date now) {
if (now == null) {
now = new Date();
}
//真实核销的金额
Map<String, BigDecimal> realAmountMap = realCancelList.stream().collect(Collectors.groupingBy(k -> String.valueOf(ReflectUtil.getFieldValue(k, uniqueNoField1)), Collectors.mapping((k -> new BigDecimal(String.valueOf(ReflectUtil.getFieldValue(k, realAmountField)))), Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
//待核销的金额
Map<String, BigDecimal> stayCancelAmountMap = stayCancelList.stream().collect(Collectors.groupingBy(t -> String.valueOf(ReflectUtil.getFieldValue(t, uniqueNoField2)), Collectors.mapping(t -> new BigDecimal(String.valueOf(ReflectUtil.getFieldValue(t, compareAmountField))), Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
List<T> updateList = Lists.newArrayList();
for (Map.Entry<String, BigDecimal> entry : stayCancelAmountMap.entrySet()) {
String uniqueNo = entry.getKey();
BigDecimal totalAmount = entry.getValue();
List<T> willbeCancelList = stayCancelList.stream().filter(t -> String.valueOf(ReflectUtil.getFieldValue(t, uniqueNoField2)).equals(uniqueNo)).collect(Collectors.toList());
if (CollectionUtil.isEmpty(willbeCancelList)) {
continue;
}
final BigDecimal[] paidAmount = {realAmountMap.get(uniqueNo)};
if (paidAmount[0].compareTo(totalAmount) >= 0) {
//核销金额=未核销金额,则一次性全部核销完毕
for (T t : willbeCancelList) {
this.setNuclearCancelData(t, new BigDecimal(String.valueOf(ReflectUtil.getFieldValue(t, compareAmountField))), updateList, now, realCancelList);
updateList.add(t);
}
} else {
//核销金额小于未核销金额
for (T t : willbeCancelList) {
if (paidAmount[0].compareTo(BigDecimal.ZERO) <= 0) {
log.info("金额已核销完毕!");
continue;
}
//核销金额
BigDecimal cancelAmount = BigDecimal.ZERO;
BigDecimal compareAmount = new BigDecimal(String.valueOf(ReflectUtil.getFieldValue(t, compareAmountField)));
if (paidAmount[0].compareTo(compareAmount) >= 0) {
cancelAmount = compareAmount;
} else {
cancelAmount = paidAmount[0];
}
this.setNuclearCancelData(t, cancelAmount, updateList, now, realCancelList);
updateList.add(t);
//每核销一笔,实际金额递减
paidAmount[0] = paidAmount[0].subtract(cancelAmount);
}
}
}
return updateList;
}
}
通用的核销代码进行抽象,并且定义范型,可以针对不同业务场景下的对象实体;
代码解释
- process方法会传进来两个范型的list集合,分别为主表的总核销金额和明细表待核销的金额;
- 接下来根据uniqueNoField1、uniqueNoField2入参进行分组,这两个字段都是实际表里的业务关联主键字段;
- 根据分组后的Map集合循环,拿到核销金额List集合,嵌套循环进行金额比对,虽然是双层for循环,内层循环只会根据paidAmount[0]去判断,永远是第一个需要核销的金额,其实不会影响多少性能;
- 循环if判断核销金额与未核销金额进行比较,如果刚好相等则一次性进行核销,如果核销金额小于未核销金额,则去取paidAmount[0]金额,每次循环后面会进行,paidAmount[0] = paidAmount[0].subtract(cancelAmount); 核销金额递减,能确保需要核销的金额,能根据每次核销的金额进行变化;
- 最后调用setNuclearCancelData方法,组装明细,根据明细进行更新表数据;
总结
这个抽象类的核销方法的封装,在目前业务场景下是可以实现核销逻辑操作的,如果核销逻辑有变化,存在多种规则,还是建议通过规则引擎来实现。