package com.xiaopeng.order.utils;
|
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.time.LocalTime;
|
import java.util.HashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
/**
|
* 出租车计费引擎服务
|
*/
|
public class TaxiPricingService {
|
|
public static void main(String[] args) {
|
|
}
|
|
// 1. 定义规则模型
|
public static class PricingRule {
|
String ruleName; // 时段名称(如:高峰期、深夜档、普通时段A)
|
String ruleType; // 类型:NORMAL, PEAK, NIGHT, HOLIDAY
|
LocalTime start;
|
LocalTime end;
|
BigDecimal perKmRate; // 每公里价格
|
BigDecimal perMinRate; // 每分钟价格
|
|
// 起步价相关(仅用于订单开始时的首个规则判定)
|
BigDecimal baseFare;
|
BigDecimal baseKm;
|
BigDecimal baseMin;
|
|
public PricingRule(String name, String type, String start, String end, double kmRate, double minRate, double bf, double bk, double bm) {
|
this.ruleName = name;
|
this.ruleType = type;
|
this.start = LocalTime.parse(start);
|
this.end = LocalTime.parse(end);
|
this.perKmRate = BigDecimal.valueOf(kmRate);
|
this.perMinRate = BigDecimal.valueOf(minRate);
|
this.baseFare = BigDecimal.valueOf(bf);
|
this.baseKm = BigDecimal.valueOf(bk);
|
this.baseMin = BigDecimal.valueOf(bm);
|
}
|
|
// 跨天匹配核心算法
|
public boolean contains(LocalTime now) {
|
if (start.isBefore(end)) {
|
return !now.isBefore(start) && !now.isAfter(end);
|
} else { // 处理跨天,如 22:00 - 05:00
|
return !now.isBefore(start) || !now.isAfter(end);
|
}
|
}
|
}
|
|
/**
|
* 计算服务
|
*/
|
public Map<String, BigDecimal> calculateOrderFee(
|
List<GpsPoint> points,
|
PricingRule holidayRule,
|
PricingRule peakRule,
|
PricingRule nightRule,
|
List<PricingRule> normalRules,
|
List<LongDistStep> longSteps,
|
boolean isHoliday) {
|
|
if (points == null || points.size() < 2) return new HashMap<>();
|
|
// 结果存放:Key为规则名称,Value为该时段产生的超出金额
|
Map<String, BigDecimal> report = new HashMap<>();
|
|
// 1. 初始化起步参数 (以第一条定位的时间点决定起步规则)
|
GpsPoint startPoint = points.get(0);
|
PricingRule initialRule = findMatchedRule(startPoint, isHoliday, holidayRule, peakRule, nightRule, normalRules);
|
|
report.put("起步价(" + initialRule.ruleName + ")", initialRule.baseFare);
|
|
// 剩余可抵扣起步额度
|
BigDecimal remainBaseMeters = initialRule.baseKm.multiply(new BigDecimal("1000"));
|
BigDecimal remainBaseMillis = initialRule.baseMin.multiply(new BigDecimal("60000"));
|
BigDecimal totalDistanceMeters = BigDecimal.ZERO;
|
|
// 2. 循环处理每一段定位轨迹
|
for (int i = 1; i < points.size(); i++) {
|
GpsPoint curr = points.get(i);
|
GpsPoint prev = points.get(i - 1);
|
|
// 段增量
|
BigDecimal segM = BigDecimal.valueOf(curr.getDistance());
|
BigDecimal segMs = BigDecimal.valueOf(curr.getLocatetime() - prev.getLocatetime());
|
totalDistanceMeters = totalDistanceMeters.add(segM);
|
|
// 匹配当前定位点所属的规则
|
PricingRule activeRule = findMatchedRule(curr, isHoliday, holidayRule, peakRule, nightRule, normalRules);
|
String key = activeRule.ruleName;
|
|
// A. 扣除起步里程后的计费里程
|
BigDecimal billableM = BigDecimal.ZERO;
|
if (remainBaseMeters.compareTo(BigDecimal.ZERO) > 0) {
|
if (segM.compareTo(remainBaseMeters) > 0) {
|
billableM = segM.subtract(remainBaseMeters);
|
remainBaseMeters = BigDecimal.ZERO;
|
} else {
|
remainBaseMeters = remainBaseMeters.subtract(segM);
|
}
|
} else {
|
billableM = segM;
|
}
|
|
// B. 扣除起步时长后的计费时长
|
BigDecimal billableMs = BigDecimal.ZERO;
|
if (remainBaseMillis.compareTo(BigDecimal.ZERO) > 0) {
|
if (segMs.compareTo(remainBaseMillis) > 0) {
|
billableMs = segMs.subtract(remainBaseMillis);
|
remainBaseMillis = BigDecimal.ZERO;
|
} else {
|
remainBaseMillis = remainBaseMillis.subtract(segMs);
|
}
|
} else {
|
billableMs = segMs;
|
}
|
|
// C. 计算当前段金额 = (里程费) + (时长费) + (远途费)
|
BigDecimal distFee = billableM.divide(new BigDecimal("1000"), 10, RoundingMode.HALF_UP).multiply(activeRule.perKmRate);
|
BigDecimal timeFee = billableMs.divide(new BigDecimal("60000"), 10, RoundingMode.HALF_UP).multiply(activeRule.perMinRate);
|
BigDecimal extraLongFee = calculateLongDistExtra(totalDistanceMeters, billableM, longSteps);
|
|
BigDecimal segTotal = distFee.add(timeFee).add(extraLongFee);
|
|
// D. 归纳到对应规则
|
report.put(key, report.getOrDefault(key, BigDecimal.ZERO).add(segTotal));
|
}
|
|
// 3. 格式化输出:保留两位小数
|
return formatResult(report);
|
}
|
|
/**
|
* 优先级判定逻辑:节假日 > 高峰期 > 夜间 > 普通1 > 普通2...
|
*/
|
private PricingRule findMatchedRule(GpsPoint p, boolean isHoliday, PricingRule h, PricingRule pk, PricingRule n, List<PricingRule> normals) {
|
if (isHoliday) return h;
|
|
// 将毫秒转为当地时间进行匹配
|
LocalTime time = new java.sql.Time(p.getLocatetime()).toLocalTime();
|
|
if (pk.contains(time)) return pk;
|
if (n.contains(time)) return n;
|
|
for (PricingRule rule : normals) {
|
if (rule.contains(time)) return rule;
|
}
|
|
return normals.get(0); // 兜底逻辑
|
}
|
|
private BigDecimal calculateLongDistExtra(BigDecimal totalM, BigDecimal segM, List<LongDistStep> steps) {
|
if (segM.compareTo(BigDecimal.ZERO) <= 0) return BigDecimal.ZERO;
|
BigDecimal totalKm = totalM.divide(new BigDecimal("1000"), 10, RoundingMode.HALF_UP);
|
BigDecimal rate = BigDecimal.ZERO;
|
for (LongDistStep step : steps) {
|
if (totalKm.compareTo(step.kmThreshold) > 0) {
|
rate = step.addPrice;
|
break;
|
}
|
}
|
return segM.divide(new BigDecimal("1000"), 10, RoundingMode.HALF_UP).multiply(rate);
|
}
|
|
private Map<String, BigDecimal> formatResult(Map<String, BigDecimal> map) {
|
Map<String, BigDecimal> formatted = new HashMap<>();
|
map.forEach((k, v) -> formatted.put(k, v.setScale(2, RoundingMode.HALF_UP)));
|
return formatted;
|
}
|
|
// 远途阶梯配置类
|
public static class LongDistStep {
|
BigDecimal kmThreshold;
|
BigDecimal addPrice;
|
public LongDistStep(double km, double price) {
|
this.kmThreshold = BigDecimal.valueOf(km);
|
this.addPrice = BigDecimal.valueOf(price);
|
}
|
}
|
}
|