下单的过程中我们会根据配送方式的不同来计算相应的运费,并将每个商品分摊到的运费金额放到订单的items表里,如果按照相应的比例进行分摊,就会存在四舍五入的小数分摊不均,比如10元运费3件商品,每个就会分摊到3.33,这样就会有0.01的误差存在,为了避免这样的误差存在,我们会记录下前几次分摊到的金额值总和,最后一个用总运费金额减去之前的总和,这样减避免了误差的存在。订单发生退货的时候就会根据退货数量不同来决定退还给用户多少金额。逻辑代码如下,假设订单总运费100元,所有商品的价格均取两位小数

    1. import lombok.Getter;
    2. import lombok.Setter;
    3. import java.math.BigDecimal;
    4. @Getter
    5. @Setter
    6. public class TestVo {
    7. private BigDecimal a;
    8. private BigDecimal b;
    9. public TestVo(BigDecimal a, BigDecimal b) {
    10. this.a = a;
    11. this.b = b;
    12. }
    13. }
    1. public static void main(String[] args) {
    2. BigDecimal bigDecimal = new BigDecimal("1600");
    3. List<TestVo> list = new ArrayList<>();
    4. list.add(new TestVo(new BigDecimal(700), null));
    5. list.add(new TestVo(new BigDecimal(400), null));
    6. list.add(new TestVo(new BigDecimal(500), null));
    7. saveReceivedAmount(bigDecimal, list);
    8. System.out.println(JSON.toJSONString(list));
    9. }
    10. private static void saveReceivedAmount(BigDecimal amount, List<TestVo> list) {
    11. AtomicReference<BigDecimal> atomicReceivedAmount = new AtomicReference<>();
    12. if(amount != null){
    13. atomicReceivedAmount.set(amount);
    14. }
    15. int len = list.size();
    16. for (int i = 0; i < len; i++) {
    17. TestVo vo = list.get(i);
    18. if(amount != null ){
    19. if(len >= 2){
    20. BigDecimal decimal = atomicReceivedAmount.get();
    21. if(decimal.compareTo(vo.getA()) <= 0){
    22. vo.setB(decimal);
    23. atomicReceivedAmount.set(new BigDecimal("0"));
    24. }else {
    25. if(i == len-1){
    26. vo.setB(decimal);
    27. }else{
    28. vo.setB(vo.getA());
    29. atomicReceivedAmount.set(decimal.subtract(vo.getA()));
    30. }
    31. }
    32. }else{
    33. //回款金额
    34. vo.setB(amount);
    35. }
    36. }else{
    37. vo.setB(null);
    38. }
    39. }
    40. }

    分摊

    1. /**
    2. * 按户数分摊方式
    3. * 分摊计算采用截取小数精确位数后的小数值BigDecimal.ROUND_DOWN
    4. * 目的:指定金额200元,平均分配给6户,分摊前后金额相等
    5. */
    6. private static void hsFt() {
    7. BigDecimal[] repair_shou_Amt = new BigDecimal[6];// 每个分户应承担维修金额
    8. BigDecimal fact_repair_Amt = new BigDecimal("0");// 维修对象的实际费用(不含零头)
    9. //待分摊金额
    10. BigDecimal ft_amt = new BigDecimal("200");
    11. //分户总数
    12. int count = repair_shou_Amt.length;
    13. for(int i = 0; i < count; i++) {
    14. //求每个分户的承担金额=分摊金额/总户数
    15. repair_shou_Amt[i] =ft_amt.divide(new BigDecimal(count), 2, BigDecimal.ROUND_DOWN); //截掉精度之后的小数位的值
    16. //实际分摊承担总金额---小数点截取后的总金额
    17. fact_repair_Amt = fact_repair_Amt.add(repair_shou_Amt[i]);
    18. }
    19. // 零头 = 分摊实际承担金额-待分摊金额
    20. BigDecimal repair_oddment_amt = fact_repair_Amt.subtract(ft_amt);
    21. // 算出要参与分摊零头的户数-截掉精度后的小数位的值累加后的每个值假设最大也只可能为0.00999999999999999999无限接近0.01
    22. int repair_int = repair_oddment_amt.multiply(new BigDecimal("100")).abs().intValue();
    23. // 把维修金额零头分摊到分户上,如果参与维修零头的户数大于零或者小于总户数,则进行分摊,否则报错
    24. if( (repair_int < count) && (repair_int > 0) ) {
    25. // 如果零头小于零,则说明有repair_int个分户承担的金额少一分钱0.01,需要加一分钱
    26. if( repair_oddment_amt.compareTo(new BigDecimal("0")) < 0 ) {
    27. for(int i = 0; i < repair_int; i++) {
    28. repair_shou_Amt[i] = repair_shou_Amt[i].add(new BigDecimal("0.01"));
    29. }
    30. } else {
    31. // 如果零头大于零,则说明有repair_int个分户承担的金额多一分钱0.01,需要减去一分钱
    32. for(int i = 0; i < repair_int; i++) {
    33. repair_shou_Amt[i] = repair_shou_Amt[i].subtract(new BigDecimal("0.01"));
    34. }
    35. }
    36. } else if( repair_int == 0 ) {
    37. // 零头如果为零,不需要处理
    38. } else {
    39. System.out.println("非法操作!");
    40. }
    41. //分摊金额
    42. System.out.println(JSON.toJSONString(repair_shou_Amt));
    43. }
    1. BigDecimal.add() 增加金额无效,需要赋值 bigDecimal = BigDecimal.add();
    2. 在调用方法事务中,尽量少用FeignClient,因为在事务内,查询数据可能,不是事务生效后的数据,所以要用Mapper或者Service服务。

      1. 错误示例:<br />
      1. /**
      2. * 查询订单的未支付金额
      3. * @param salesHeadCode
      4. * @param mallSalesContractCode
      5. * @return
      6. */
      7. public BigDecimal getNotPayAmt(String salesHeadCode, String mallSalesContractCode) {
      8. log.info("查询订单的未支付金额入参salesHeadCode:{}, mallSalesContractCode:{}", salesHeadCode, mallSalesContractCode);
      9. // 查询签收金额(应付金额)
      10. BigDecimal shouldPayAmt = new BigDecimal(0);
      11. BigDecimal paiedAmt = new BigDecimal(0);
      12. List<String> salesHeadCodes = Arrays.asList(salesHeadCode);
      13. ResponseBO<List<DeliveryOrderProductVO>> deliveryOrderProductVOs = deliveryOrderFeignClient.calReceiptedAmount(salesHeadCodes);
      14. ResponseBO<List<com.vd.canary.obmp.transport.api.response.returnorder.ReturnOrderProductVO>> returnOrderProductVOs = returnOrderFeignClient.calReturnedAmount(salesHeadCodes);
      15. if(ObjectUtil.isNotEmpty(deliveryOrderProductVOs) && CollectionUtil.isNotEmpty(deliveryOrderProductVOs.getData())){
      16. DeliveryOrderProductVO deliveryOrderProductVO = deliveryOrderProductVOs.getData().get(0);
      17. shouldPayAmt = deliveryOrderProductVO.getSalesTotalAmount();
      18. }
      19. if(ObjectUtil.isNotEmpty(returnOrderProductVOs) && CollectionUtil.isNotEmpty(returnOrderProductVOs.getData())){
      20. com.vd.canary.obmp.transport.api.response.returnorder.ReturnOrderProductVO returnOrderProductVO = returnOrderProductVOs.getData().get(0);
      21. shouldPayAmt = shouldPayAmt.subtract(returnOrderProductVO.getSalesReturnAmount());
      22. }
      23. ArmReceiptQueryPageReq armReceiptQueryPageReq = new ArmReceiptQueryPageReq();
      24. armReceiptQueryPageReq.setSaleContractCode(mallSalesContractCode);
      25. armReceiptQueryPageReq.setPageNum(1);
      26. armReceiptQueryPageReq.setPageSize(200);
      27. //TODO 在@Transactional事务中,不要调用本服务的FeignClient的方法,会导致查询数据不是最新的或者不是最新更新的
      28. ResponseBO<ArmReceiptPageResp> armReceiptPageRespResponseBO = armReceiptHeadFeignClient.queryPageReceiptList(armReceiptQueryPageReq);
      29. log.info("查询订单的支付信息申请结算已支付金额:{}, salesHeadCode:{}", JSON.toJSONString(armReceiptPageRespResponseBO), salesHeadCode);
      30. List<String> onLinePaymentChannel = Arrays.asList("1", "5", "6", "7");
      31. if(ObjectUtil.isNotEmpty(armReceiptPageRespResponseBO) && ObjectUtil.isNotEmpty(armReceiptPageRespResponseBO.getData()) && CollectionUtil.isNotEmpty(armReceiptPageRespResponseBO.getData().getList())){
      32. List<ArmReceiptPageVO> list = armReceiptPageRespResponseBO.getData().getList();
      33. List<ArmReceiptPageVO> voList = armReceiptPageRespResponseBO.getData().getList();
      34. paiedAmt = list.stream().filter(ar -> "20".equals(ar.getAnnounceStatus())).map(ArmReceiptPageVO::getReceiptedAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
      35. log.info("查询订单的未支付金额paiedAmt:{}, salesHeadCode:{}", paiedAmt, salesHeadCode);
      36. // 需要加上未认领的,但是支付方式是企业网银,支付宝、微信的金额(在途金额)
      37. BigDecimal onlinePayAmtB = voList.stream().filter(ar -> "10".equals(ar.getAnnounceStatus()) && onLinePaymentChannel.contains(ar.getPaymentChannel())).map(ArmReceiptPageVO::getTotalAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
      38. log.info("查询订单的未支付金额onlinePayAmt:{}, salesHeadCode:{}", onlinePayAmtB, salesHeadCode);
      39. //TODO 此地方没有将金额赋值给 paiedAmt 导致存在问题
      40. paiedAmt.add(onlinePayAmtB == null ? new BigDecimal(0) : onlinePayAmtB);
      41. log.info("查询签收金额(应付金额):{}", paiedAmt);
      42. }
      43. BigDecimal notPayAmt = shouldPayAmt.subtract(paiedAmt);
      44. log.info("查询订单的未支付金额出参shouldPayAmt:{}, paiedAmt:{}, notPayAmt:{}, salesHeadCode:{}", shouldPayAmt, paiedAmt, notPayAmt, salesHeadCode);
      45. return notPayAmt;
      46. }

      正确示例:

      1. /**
      2. * 查询订单的未支付金额
      3. * @param salesHeadCode
      4. * @param mallSalesContractCode
      5. * @return
      6. */
      7. public BigDecimal getNotPayAmt(String salesHeadCode, String mallSalesContractCode) {
      8. log.info("查询订单的未支付金额入参salesHeadCode:{}, mallSalesContractCode:{}", salesHeadCode, mallSalesContractCode);
      9. // 查询签收金额(应付金额)
      10. BigDecimal shouldPayAmt = new BigDecimal(0);
      11. BigDecimal paiedAmt = new BigDecimal(0);
      12. List<String> salesHeadCodes = Arrays.asList(salesHeadCode);
      13. ResponseBO<List<DeliveryOrderProductVO>> deliveryOrderProductVOs = deliveryOrderFeignClient.calReceiptedAmount(salesHeadCodes);
      14. ResponseBO<List<com.vd.canary.obmp.transport.api.response.returnorder.ReturnOrderProductVO>> returnOrderProductVOs = returnOrderFeignClient.calReturnedAmount(salesHeadCodes);
      15. if(ObjectUtil.isNotEmpty(deliveryOrderProductVOs) && CollectionUtil.isNotEmpty(deliveryOrderProductVOs.getData())){
      16. DeliveryOrderProductVO deliveryOrderProductVO = deliveryOrderProductVOs.getData().get(0);
      17. shouldPayAmt = deliveryOrderProductVO.getSalesTotalAmount();
      18. }
      19. if(ObjectUtil.isNotEmpty(returnOrderProductVOs) && CollectionUtil.isNotEmpty(returnOrderProductVOs.getData())){
      20. com.vd.canary.obmp.transport.api.response.returnorder.ReturnOrderProductVO returnOrderProductVO = returnOrderProductVOs.getData().get(0);
      21. shouldPayAmt = shouldPayAmt.subtract(returnOrderProductVO.getSalesReturnAmount());
      22. }
      23. ArmReceiptQueryPageReq armReceiptQueryPageReq = new ArmReceiptQueryPageReq();
      24. armReceiptQueryPageReq.setSaleContractCode(mallSalesContractCode);
      25. armReceiptQueryPageReq.setPageNum(1);
      26. armReceiptQueryPageReq.setPageSize(200);
      27. ResponseBO<ArmReceiptPageResp> armReceiptPageRespResponseBO = armReceiptHeadService.queryPageReceiptList(armReceiptQueryPageReq);
      28. log.info("查询订单的支付信息申请结算已支付金额:{}, salesHeadCode:{}", JSON.toJSONString(armReceiptPageRespResponseBO), salesHeadCode);
      29. List<String> onLinePaymentChannel = Arrays.asList("1", "5", "6", "7");
      30. if(ObjectUtil.isNotEmpty(armReceiptPageRespResponseBO) && ObjectUtil.isNotEmpty(armReceiptPageRespResponseBO.getData()) && CollectionUtil.isNotEmpty(armReceiptPageRespResponseBO.getData().getList())){
      31. List<ArmReceiptPageVO> list = armReceiptPageRespResponseBO.getData().getList();
      32. List<ArmReceiptPageVO> voList = armReceiptPageRespResponseBO.getData().getList();
      33. paiedAmt = list.stream().filter(ar -> "20".equals(ar.getAnnounceStatus())).map(ArmReceiptPageVO::getReceiptedAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
      34. log.info("查询订单的未支付金额paiedAmt:{}, salesHeadCode:{}", paiedAmt, salesHeadCode);
      35. // 需要加上未认领的,但是支付方式是企业网银,支付宝、微信的金额(在途金额)
      36. BigDecimal onlinePayAmtB = voList.stream().filter(ar -> "10".equals(ar.getAnnounceStatus()) && onLinePaymentChannel.contains(ar.getPaymentChannel())).map(ArmReceiptPageVO::getTotalAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
      37. log.info("查询订单的未支付金额onlinePayAmt:{}, salesHeadCode:{}", onlinePayAmtB, salesHeadCode);
      38. paiedAmt = paiedAmt.add(onlinePayAmtB == null ? new BigDecimal(0) : onlinePayAmtB);
      39. log.info("查询签收金额(应付金额):{}", paiedAmt);
      40. }
      41. BigDecimal notPayAmt = shouldPayAmt.subtract(paiedAmt);
      42. log.info("查询订单的未支付金额出参shouldPayAmt:{}, paiedAmt:{}, notPayAmt:{}, salesHeadCode:{}", shouldPayAmt, paiedAmt, notPayAmt, salesHeadCode);
      43. return notPayAmt;
      44. }
    3. Java中byte是8位带符号的二进制整数,Go中byte是8位无符号的二进制整数。

      问题: 有符号整数溢出,无符号整数的环绕,无符号整数的减法问题,无符号整数和带符号整数混用