业务让我实现通用核销逻辑

前言

关于核销这个概念,在大部分支付的场景下会存在很多,现在很多平台像美团、大众点评、携程等,你去薅了某个商家的羊毛...准确的说应该是买了某个商家的优惠套餐,会生成一个二维码,当你去店家消费的时候店家会去扫这个二维码进行消费确认,这就是核销; 其次就是请款、还款的一些业务逻辑也涉及到核销的流程,并且有时候这些操作并不是一次性的,需要客户根据自己的实际情况分批进行请款、还款的操作,当然这个金额每次也都是不一样的,直到结清才是真正的核销完毕。

核销业务逻辑

整个业务逻辑大概是这样子的:

  1. 就是我通过一些渠道生成请款单,请款单上有两个金额字段分别是请款总金额和还款总金额;
  2. 请款单--->item明细表,一对多关系,当然还会衍生另外几个字段已请款金额、已还款金额、未请款金额、未还款金额,每次根据还款、请款的金额,进行计算;
  3. 每次请款、还款的金额可以随意,会根据请款单明细的生成时间排序,进行核销,先生成的优先进行核销,直到全部金额核销完毕;

通用核销抽象类代码实现

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方法,组装明细,根据明细进行更新表数据;

总结

这个抽象类的核销方法的封装,在目前业务场景下是可以实现核销逻辑操作的,如果核销逻辑有变化,存在多种规则,还是建议通过规则引擎来实现。

相关推荐
tg-zm8899964 小时前
2025返利商城源码/挂机自动收益可二开多语言/自定义返利比例/三级分销理财商城
java·mysql·php·laravel·1024程序员节
X***C8624 小时前
SpringBoot:几种常用的接口日期格式化方法
java·spring boot·后端
i***t9194 小时前
Spring Boot项目接收前端参数的11种方式
前端·spring boot·后端
前端达人4 小时前
你的App消息推送为什么石沉大海?看Service Worker源码我终于懂了
java·开发语言
小光学长4 小时前
基于ssm的宠物交易系统的设计与实现850mb48h(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·前端·数据库
编程大师哥4 小时前
vxe-table 透视表分组汇总及排序基础配置
java
8***84824 小时前
spring security 超详细使用教程(接入springboot、前后端分离)
java·spring boot·spring
o***74174 小时前
基于SpringBoot的DeepSeek-demo 深度求索-demo 支持流式输出、历史记录
spring boot·后端·lua
9***J6284 小时前
Spring Boot项目集成Redisson 原始依赖与 Spring Boot Starter 的流程
java·spring boot·后端
S***q1925 小时前
Rust在系统工具中的内存安全给代码上了三道保险锁。但正是这种“编译期的严苛”,换来了运行时的安心。比如这段代码:
开发语言·后端·rust