我这里的阶梯收费是针对水费而来的,我们的阶梯缴费方案如下,默认水价是一个阶梯,然后每一个阶梯都有一个起始吨位和一个阶梯价格。
功能:我们的抄表任务需要每天抄写一次水表(远程表),抄表的同时对水表用量和余额进行一次计算。我们的缴费方案分为以年为周期和以月为周期。
阶梯收费逻辑:这里所有的结束吨位都是下一个阶梯的起始吨位,并且结束吨位是闭区间。例如第1阶梯起始吨位是2吨,那么默认阶梯的范围就是[0-2],第1阶梯范围就是(2-∞]; 如果再增加一个阶梯,第2阶梯的起始吨位是4,那么第一阶梯的范围(2-4],第二阶梯的范围就是(4-∞]。依次类推
那么我们如何计算出每天的精准的水费呢?
我们的代码基础逻辑:首先我们先将第一阶梯的起始吨位取出,然后判断总用水量,是不是小于等于第一阶梯。如果小于等于第一阶梯,那么我们的当天的水费就处于第一个阶梯。然后遍历每个阶梯通过总用量来计算,当天的水费处于哪一个阶梯。
如下代码就是普通的获取阶梯费用的逻辑:
public BigDecimal getTiersAmountCommon(
PlanContentDTO planContentDTO, //缴费方案体
List<LadderDTO> tiers, //阶梯价格
BigDecimal totalWaterVolume, // 总用水量(用于判断阶梯)
BigDecimal currentDayUsage // 当天用水量(用于计算费用)
) {
//计算的金额
BigDecimal amount = BigDecimal.ZERO;
if(totalWaterVolume == null || currentDayUsage == null){
return amount.setScale(2, RoundingMode.HALF_UP);
}
//获取缴费方案的默认价格
BigDecimal defaultPrice = BigDecimal.valueOf(Double.parseDouble(planContentDTO.getPrice()));
//获取缴费方案的第一阶梯的起始吨位
BigDecimal oneStartAmount = BigDecimal.valueOf(Double.parseDouble(tiers.get(0).getStartAmount()));
//判断总水量是否大于第一阶梯的起始吨位
if(totalWaterVolume.compareTo(oneStartAmount) <= 0){
//如果小于等于就是使用默认价格计算当前水费
amount = currentDayUsage.multiply(defaultPrice);
}else{
//判断总用量属于哪一个阶梯价格
BigDecimal currentPrice = BigDecimal.ZERO;
for (LadderDTO tier : tiers) {
//拿到当前阶梯的起始吨位
BigDecimal startAmount = BigDecimal.valueOf(Double.parseDouble(tier.getStartAmount()));
//判断使用到了那个吨位,并给当前价格赋值(最后的值,就是所处的阶梯)
if (totalWaterVolume.compareTo(startAmount) > 0) {
currentPrice = BigDecimal.valueOf(Double.parseDouble(tier.getPrice()));
}
}
amount = currentPrice.multiply(currentDayUsage);
}
return amount.setScale(2, RoundingMode.HALF_UP);
}
乍一看觉得没有什么毛病,其实毛病大的去了
进阶代码逻辑:那么这里起始有一个问题,需要我们去思考,其实不管我们的阶梯收费是以年为周期,还是以月为周期。我们的总用量都是用来判断使用哪个阶段价格计算水费的标准。但是存在一种不可忽略的一点就是可能存在当天用量存在两个阶梯分界处,甚至更小的处于多个阶梯都有可能。
修改过后的逻辑:
public BigDecimal getTiersAmountAdvanced(
PlanContentDTO planContentDTO, //缴费方案体
List<LadderDTO> tiers, //阶梯价格
BigDecimal totalWaterVolume, // 总用水量(用于判断阶梯)
BigDecimal currentDayUsage // 当天用水量(用于计算费用)
) {
//这里所有的结束吨位都是下一个阶梯的起始吨位,并且结束吨位是闭区间,例如1阶梯起始吨位是2吨,那么默认价格的范围就是[0-2],第1阶梯范围就是(2-∞]
//如果在增加一个阶梯,第2阶梯的起始吨位是4,那么第一阶梯的范围(2-4],第二阶梯的范围就是(4-∞]
//给计算的金额赋初值
if(totalWaterVolume == null || currentDayUsage == null){
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
}
BigDecimal amount = BigDecimal.ZERO;
//获取缴费方案的默认价格
BigDecimal defaultPrice = BigDecimal.valueOf(Double.parseDouble(planContentDTO.getPrice()));
//获取缴费方案的第一阶梯的起始吨位(及默认价格的结束吨位)
BigDecimal oneStartAmount = BigDecimal.valueOf(Double.parseDouble(tiers.get(0).getStartAmount()));
//当前阶梯的价格
BigDecimal currentPrice = BigDecimal.ZERO;
//默认上一次档位的档位都是-1档及默认档
int currentTier = 0;
int lastTier = -1;
//判断总水量是否大于第一阶梯的起始吨位
if(totalWaterVolume.compareTo(oneStartAmount) <= 0){
//总量在默认档位
//那么今天的费用就是当日用量*默认价格
amount = currentDayUsage.multiply(defaultPrice);
}else{
//总量不在默认档位
//总用量减去当前天用量,计算得到上次支付水费时候的用量
BigDecimal subtractVolume = totalWaterVolume.subtract(currentDayUsage);
//判断总用量处于哪一个阶梯
for (int i = 0; i < tiers.size(); i++){
//拿到当前阶梯的起始吨位
BigDecimal startAmount = BigDecimal.valueOf(Double.parseDouble(tiers.get(i).getStartAmount()));
//判断使用到了那个吨位,并给当前价格赋值(最后的值,就是所处的阶梯)
if (totalWaterVolume.compareTo(startAmount) > 0) {
currentPrice = BigDecimal.valueOf(Double.parseDouble(tiers.get(i).getPrice()));
currentTier = i;
}
if (subtractVolume.compareTo(startAmount) > 0) {
lastTier = i;
}
}
BigDecimal settledVolume = BigDecimal.ZERO;
//如果上一次在默认当 并且总用量不在默认档
if(lastTier == -1){
//如果上次吨位小于第一阶梯吨位,证明还有默认档的费用没有收取完
if(subtractVolume.compareTo(oneStartAmount) <= 0){
//默认档没有收完的钱 = 第一阶梯起始吨位 - 上一次的吨位 = 默认未收吨位,再使用默认默认未收吨位*默认价格
amount = amount.add(defaultPrice.multiply(oneStartAmount.subtract(subtractVolume)));
//今日已经结算的吨位增加
settledVolume = settledVolume.add(oneStartAmount.subtract(subtractVolume));
}
//如果今天总吨位所处的阶梯大于第一阶梯(最少第二阶段,所以等于第一阶段的时候钱需要都算进去)
if(currentTier > 0){
//如果是1就是第二档,需要将第一档的金额结清
//先把其他不在当前阶梯的钱都计算出来
for (int i = 0; i < currentTier; i++) {
BigDecimal tierStartAmount = BigDecimal.valueOf(Double.parseDouble(tiers.get(i).getStartAmount()));
BigDecimal tierPrice = BigDecimal.valueOf(Double.parseDouble(tiers.get(i).getPrice()));
BigDecimal nextStartAmount = BigDecimal.valueOf(Double.parseDouble(tiers.get(i + 1).getStartAmount()));
amount = amount.add(tierPrice.multiply(nextStartAmount.subtract(tierStartAmount)));
settledVolume = settledVolume.add(nextStartAmount.subtract(tierStartAmount));
}
//最后在加上在这个阶梯的费用
amount = amount.add(currentPrice.multiply(currentDayUsage.subtract(settledVolume)));
}else{
//如果当前正好处在第一阶梯,当日是用来量 - 第一阶梯起始吨位 = 该阶梯使用吨位
BigDecimal onePrice = BigDecimal.valueOf(Double.parseDouble(tiers.get(0).getPrice()));
amount = amount.add(onePrice.multiply(currentDayUsage.subtract(oneStartAmount)));
}
}else{
if(currentTier == lastTier){
//相同就不可能没有有没有结算过的金额,因为有一个总的总用量判断他是在哪个阶梯,如果总的都在默认就直接用默认价格计算了
amount = amount.add(currentPrice.multiply(currentDayUsage));
}else if(currentTier > lastTier){
//如果当前档位和上一次结算的档位不同,那么需要计算上一次结算的档位和当前档位之间的水量
//这个也是到不了当前阶梯,只是充上一次的阶梯开始将没有收的阶梯费用收完
for (int i = lastTier; i < currentTier; i++) {
//第一个就是到的上次价格的阶梯,然后就可以把上次价格阶梯没有结算的吨位,计算完
BigDecimal tierPrice = BigDecimal.valueOf(Double.parseDouble(tiers.get(i).getPrice()));
BigDecimal tierStartAmount = BigDecimal.valueOf(Double.parseDouble(tiers.get(i).getStartAmount()));
BigDecimal nextStartAmount = BigDecimal.valueOf(Double.parseDouble(tiers.get(i + 1).getStartAmount()));
if(i == lastTier){
//如果i>0,并且i等于上一次抄表的档位,那就需要用上次抄表的吨位-上次抄表所处的阶梯吨位=还剩多少吨位没有收到
amount = amount.add(tierPrice.multiply(subtractVolume.subtract(tierStartAmount)));
settledVolume = settledVolume.add(subtractVolume.subtract(tierStartAmount));
}else{
//这个上次收费没有达到的吨位,直接使用下一阶梯的起始吨位-这一阶梯的起始吨位,得到这一阶梯的范围,然后乘以阶梯价格
amount = amount.add(tierPrice.multiply(nextStartAmount.subtract(tierStartAmount)));
settledVolume = settledVolume.add(nextStartAmount.subtract(tierStartAmount));
}
}
amount = amount.add(currentPrice.multiply(currentDayUsage.subtract(settledVolume)));
}
}
}
return amount.setScale(2, RoundingMode.HALF_UP);
}
附上我写的测试类:
@Test
public void testGetTiersAmount() {
// 创建阶梯信息
List<LadderDTO> tiers = new ArrayList<>();
LadderDTO ladder1 = new LadderDTO();
ladder1.setStartAmount("2");
ladder1.setPrice("2.2");
tiers.add(ladder1);
LadderDTO ladder2 = new LadderDTO();
ladder2.setStartAmount("4");
ladder2.setPrice("2.3");
tiers.add(ladder2);
LadderDTO ladder3 = new LadderDTO();
ladder3.setStartAmount("6");
ladder3.setPrice("2.4");
tiers.add(ladder3);
// 创建计划内容对象
PlanContentDTO planContentDTO = new PlanContentDTO();
planContentDTO.setPrice("2.1");
// 创建收费计划对象
ChargingPlan chargingPlan = new ChargingPlan();
chargingPlan.setIsMinGuarantee(0); // 可以设置为1进行保底水量的测试
chargingPlan.setMinGuarantee(1);
chargingPlan.setChargingCycle(1);
chargingPlan.setMinPrice(BigDecimal.valueOf(10)); // 设置一个假设的保底价格
//按照每日扣费和按照每天扣费一致
BigDecimal waterVolume5 = BigDecimal.valueOf(6.19);
BigDecimal user = BigDecimal.valueOf(5.19);
assertEquals(new BigDecimal("0.00"), getTiersAmountAdvanced(chargingPlan, planContentDTO, tiers, null ,null));
assertEquals(new BigDecimal("11.56"), getTiersAmountAdvanced(chargingPlan, planContentDTO, tiers, waterVolume5,user));
}