订单折扣金额分摊算法|代金券分摊|收银系统|积分分摊|分摊|精度问题|按比例分配|钱分摊|钱分配

一个金额分摊 的算法,将折扣分摊按比例(细单实收在总体的占比 )到各个细单中。

此算法需要达到以下要求:

  1. 折扣金额接近细单总额,甚至折扣金额等于细单金额,某些时候甚至超过细单总额,要保证实收不为负数。
  2. 复杂度O(n)

...

写这个算法的初衷,就是因为现在网上的分摊算法,都没有考虑到最后一项不够减、只循环一次、折扣金额接近总额...

用例:

细单1:8.91

细单2:21.09

细单3:0.01

三个细单总和是 30.01

折扣金额:30

按比例分摊后,应该只有一项是 0.01

废话不多,直接上代码:

细单对象:

java 复制代码
	/**
     * 细单类
     */
    @Data
    public static class Detail {

        /**
         * 用来标识记录
         */
        private Long id;

        /**
         * 总额
         */
        private BigDecimal money;
    }

分摊算法:(忽略了金额从小到大排序,后续补上)

java 复制代码
    /**
     * 分摊
     *
     * @param detailList    细单
     * @param discountMoney 折扣
     * @return 新的细单集合
     */
    public static List<Detail> allocateDiscountMoney(List<Detail> detailList, BigDecimal discountMoney) {

        // 分摊总金额
        BigDecimal allocatedAmountTotal = discountMoney;
        // 剩余分摊金额
        BigDecimal leftAllocatedAmount = allocatedAmountTotal;
        // 订单总实收
        BigDecimal orderTotalAmount = detailList.stream().map(Detail::getMoney).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
        // 结果集
        List<Detail> resultList = new ArrayList<>();
        for (int i = 0; i < detailList.size(); i++) {
            // 结果
            Detail resultDetail = new Detail();
            BeanUtils.copyProperties(detailList.get(i), resultDetail);
            BigDecimal money = resultDetail.getMoney();
            // 占比比例=自身实收/实收总额
            BigDecimal proportion = money.divide(orderTotalAmount, 10, RoundingMode.UP);
            // 分摊金额 = 总分摊金额*占比比例
            BigDecimal allocatedMoney = allocatedAmountTotal.multiply(proportion);
            // 折扣分摊金额向上取整,将精度差异提前吸收,此举使得最后一项足够吸收剩余折扣金额
            allocatedMoney = allocatedMoney.setScale(2, RoundingMode.UP);
            // 是否该订单最后一条商品 或者 已经不够分摊
            if (i == detailList.size() - 1 || leftAllocatedAmount.subtract(allocatedMoney).compareTo(BigDecimal.ZERO) <= 0) {
                allocatedMoney = leftAllocatedAmount;
            }
            // 防止订单金额负数(若最后一项执行此逻辑,则导致总金额有误)
            if (money.subtract(allocatedMoney).compareTo(BigDecimal.ZERO) < 0) {
                allocatedMoney = money;
            }
            // 单个商品分摊后的金额
            BigDecimal goodsActualMoneyAfterAllocated = money.subtract(allocatedMoney);
            // 累减已分摊金额
            leftAllocatedAmount = leftAllocatedAmount.subtract(allocatedMoney);
            resultDetail.setMoney(goodsActualMoneyAfterAllocated);
            resultList.add(resultDetail);
        }
        return resultList;
    }

测试类:

java 复制代码
    public static void main1() {
        List<Detail> detailList = new ArrayList<>();
        //
        Detail detail = new Detail();
        detail.setId(1L);
        detail.setMoney(new BigDecimal("8.91"));
        detailList.add(detail);
        //
        Detail detail2 = new Detail();
        detail2.setId(2L);
        detail2.setMoney(new BigDecimal("21.07"));
        detailList.add(detail2);
        //
        Detail detail3 = new Detail();
        detail3.setId(3L);
        detail3.setMoney(new BigDecimal("0.01"));
        detailList.add(detail3);

        System.out.println("分摊前:" + JSON.toJSONString(detailList));

        List<Detail> allocated = allocateDiscountMoney(detailList, new BigDecimal("30"));

        System.out.println("分摊后:" + JSON.toJSONString(allocated));
    }

问题:为什么每一项算分摊金额都是向上取整?

答:除最后一项外的每一项的折扣分摊算多了,最后一项就分摊得少,保证最后一项一定够分摊,前面的项在迭代时可以做金额如果不够分摊的兜底处理。而如果这么做,前面的不先兜底,后面的如果不够分摊是需要再往前找项来帮忙分摊的,复杂度就比较高。

~~

折扣金额的分摊,是反向 的,其实正向 的分摊也一并适用,并且逻辑是等价的。

例如:

细单1:8.91

细单2:21.09

细单3:0.01

三个细单总和是 30.01

折扣金额:30

我们也可以看做最终金额为 0.01,用0.01来分摊。

java 复制代码
/**
     * 分摊
     *
     * @param detailList    细单
     * @param tgtTotalMoney 待分摊的目标总金额
     * @return 新的细单集合
     */
    public static List<Detail> allocateTgtTotalMoney(List<Detail> detailList, BigDecimal tgtTotalMoney) {

        // 分摊总金额
        BigDecimal allocatedAmountTotal = tgtTotalMoney;
        // 剩余分摊金额
        BigDecimal leftAllocatedAmount = allocatedAmountTotal;
        // 订单总实收
        BigDecimal orderTotalAmount = detailList.stream().map(Detail::getMoney).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
        // 结果集
        List<Detail> resultList = new ArrayList<>();
        for (int i = 0; i < detailList.size(); i++) {
            // 结果
            Detail resultDetail = new Detail();
            BeanUtils.copyProperties(detailList.get(i), resultDetail);
            BigDecimal money = resultDetail.getMoney();
            // 占比比例=自身实收/实收总额
            BigDecimal proportion = money.divide(orderTotalAmount, 10, RoundingMode.UP);
            // 分摊金额 = 总分摊金额*占比比例
            BigDecimal allocatedMoney = allocatedAmountTotal.multiply(proportion);
            // 折扣分摊金额向上取整,将精度差异提前吸收,此举使得最后一项足够吸收剩余折扣金额
            allocatedMoney = allocatedMoney.setScale(2, RoundingMode.UP);
            // 是否该订单最后一条商品 或者 已经不够分摊
            if (i == detailList.size() - 1 || leftAllocatedAmount.subtract(allocatedMoney).compareTo(BigDecimal.ZERO) <= 0) {
                allocatedMoney = leftAllocatedAmount;
            }
            // 累减已分摊金额
            leftAllocatedAmount = leftAllocatedAmount.subtract(allocatedMoney);
            resultDetail.setMoney(allocatedMoney);
            resultList.add(resultDetail);
        }
        return resultList;
    }

测试类:

java 复制代码
    public static void main2() {
        List<Detail> detailList = new ArrayList<>();
        //
        Detail detail = new Detail();
        detail.setId(1L);
        detail.setMoney(new BigDecimal("8.91"));
        detailList.add(detail);
        //
        Detail detail2 = new Detail();
        detail2.setId(2L);
        detail2.setMoney(new BigDecimal("21.07"));
        detailList.add(detail2);
        //
        Detail detail3 = new Detail();
        detail3.setId(3L);
        detail3.setMoney(new BigDecimal("0.01"));
        detailList.add(detail3);

        System.out.println("分摊前:" + JSON.toJSONString(detailList));

        List<Detail> allocated = allocateTgtTotalMoney(detailList, new BigDecimal("0.1"));

        System.out.println("分摊后:" + JSON.toJSONString(allocated));
    }

算法缺点(隐患):在折扣分摊的算法中,在需要保证每一项细单金额大于0的场景下,此算法需要谨慎使用,因为可能会把金额分摊为0元,但其实这也很难避免,因为总价30.01,折扣30,有很多项都会是0,只能说还有改进的空间,可以进行改动(例如向上取整)以保证在概率上出现0的情况少点。而且需要进行从小到大排序。

对你有帮助的话,点赞、收藏、评论、关注,谢谢各位大佬了~

相关推荐
CoovallyAIHub6 分钟前
中科大DSAI Lab团队多篇论文入选ICCV 2025,推动三维视觉与泛化感知技术突破
深度学习·算法·计算机视觉
沐怡旸8 分钟前
【底层机制】std::shared_ptr解决的痛点?是什么?如何实现?如何正确用?
c++·面试
Java中文社群16 分钟前
有点意思!Java8后最有用新特性排行榜!
java·后端·面试
代码匠心24 分钟前
从零开始学Flink:数据源
java·大数据·后端·flink
间彧30 分钟前
Spring Boot项目中如何自定义线程池
java
间彧1 小时前
Java线程池详解与实战指南
java
用户298698530141 小时前
Java 使用 Spire.PDF 将PDF文档转换为Word格式
java·后端
NAGNIP1 小时前
Serverless 架构下的大模型框架落地实践
算法·架构
moonlifesudo1 小时前
半开区间和开区间的两个二分模版
算法
渣哥1 小时前
ConcurrentHashMap 1.7 vs 1.8:分段锁到 CAS+红黑树的演进与性能差异
java