|
|
@@ -1,12 +1,13 @@
|
|
|
package com.ylx.fareSetting.service.impl;
|
|
|
|
|
|
import cn.hutool.core.collection.CollUtil;
|
|
|
-import cn.hutool.core.util.CoordinateUtil;
|
|
|
-import cn.hutool.core.util.NumberUtil;
|
|
|
import cn.hutool.core.util.ObjectUtil;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
+import com.ylx.common.core.domain.entity.SysDictData;
|
|
|
import com.ylx.common.exception.ServiceException;
|
|
|
+import com.ylx.common.utils.DictUtils;
|
|
|
+import com.ylx.common.utils.DistanceUtil;
|
|
|
import com.ylx.fareSetting.domian.MaProjectFareSetting;
|
|
|
import com.ylx.fareSetting.domian.dto.FareCalculateDTO;
|
|
|
import com.ylx.fareSetting.domian.vo.FareCalculateResultVO;
|
|
|
@@ -21,6 +22,8 @@ import org.springframework.stereotype.Service;
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.time.LocalDateTime;
|
|
|
import java.time.LocalTime;
|
|
|
import java.util.List;
|
|
|
import java.util.Optional;
|
|
|
@@ -35,106 +38,192 @@ public class MaProjectFareSettingServiceImpl extends ServiceImpl<MaProjectFareSe
|
|
|
@Resource
|
|
|
private TAddressService addressService;
|
|
|
|
|
|
+ @Override
|
|
|
public FareCalculateResultVO calculateFare(FareCalculateDTO dto) {
|
|
|
FareCalculateResultVO result = new FareCalculateResultVO();
|
|
|
|
|
|
// 1.获取商户的默认地址
|
|
|
- TAddress address = this.addressService.getOne(new LambdaQueryWrapper<TAddress>().eq(TAddress::getMerchantId, dto.getMerchantId()).eq(TAddress::getType, 1));
|
|
|
+ TAddress address = this.addressService.getOne(new LambdaQueryWrapper<TAddress>()
|
|
|
+ .eq(TAddress::getMerchantId, dto.getMerchantId())
|
|
|
+ .eq(TAddress::getType, 1));
|
|
|
if (ObjectUtil.isNull(address)) {
|
|
|
throw new ServiceException("无法获取商户的默认地址");
|
|
|
}
|
|
|
|
|
|
- // 1. 获取商户车费配置
|
|
|
+ // 2. 计算直线距离(公里)
|
|
|
+ String distanceStr = DistanceUtil.formatDistanceInKilometers(
|
|
|
+ dto.getLongitude(), dto.getLatitude(),
|
|
|
+ address.getLongitude(), address.getLatitude()
|
|
|
+ );
|
|
|
+ double straightLineKm;
|
|
|
+ if ("未知".equals(distanceStr)) {
|
|
|
+ throw new ServiceException("无法获取客户地址信息");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ straightLineKm = Double.parseDouble(distanceStr);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.error("距离字符串解析失败: {}", distanceStr, e);
|
|
|
+ throw new ServiceException("距离数据异常");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3.根据时间段判断是否白天时间段
|
|
|
+ LocalDateTime appointmentStartTime = dto.getAppointmentStartTime();
|
|
|
+ boolean isDay = isDayTimePeriod(appointmentStartTime);
|
|
|
+
|
|
|
+ // 4. 获取商户车费配置
|
|
|
LambdaQueryWrapper<MaProjectFareSetting> wrapper = new LambdaQueryWrapper<>();
|
|
|
wrapper.eq(MaProjectFareSetting::getMerchantId, dto.getMerchantId())
|
|
|
.eq(MaProjectFareSetting::getIsDelete, 0);
|
|
|
- List<MaProjectFareSetting> list = this.baseMapper.selectList(wrapper);
|
|
|
+ List<MaProjectFareSetting> configList = this.baseMapper.selectList(wrapper);
|
|
|
|
|
|
MaProjectFareSetting finalConfig = null;
|
|
|
|
|
|
- if (CollUtil.isNotEmpty(list)) {
|
|
|
- if (list.size() == 1 && ObjectUtil.equals(1, list.get(0).getIsUnified())) {
|
|
|
- // 【情况A】集合数量为1 -> 是统一配置
|
|
|
- // 直接取唯一的一条作为最终配置
|
|
|
- log.info("商户[{}]仅有一条配置,判定为统一设置模式", dto.getMerchantId());
|
|
|
- finalConfig = list.get(0);
|
|
|
+ if (CollUtil.isNotEmpty(configList)) {
|
|
|
+ // 先查找是否存在统一配置 (isUnified = 1)
|
|
|
+ Optional<MaProjectFareSetting> unifiedOpt = configList.stream()
|
|
|
+ .filter(c -> ObjectUtil.equals(1, c.getIsUnified()))
|
|
|
+ .findFirst();
|
|
|
|
|
|
+ if (unifiedOpt.isPresent()) {
|
|
|
+ finalConfig = unifiedOpt.get();
|
|
|
} else {
|
|
|
- // 【情况B】集合数量大于1 -> 不是统一配置(独立项目配置)
|
|
|
- // 需要根据传入的 projectId 从列表中筛选出对应的项目配置
|
|
|
- log.info("商户[{}]存在多条配置,根据projectId[{}]进行匹配", dto.getMerchantId(), dto.getProjectId());
|
|
|
+ // 查找匹配的项目配置
|
|
|
+ log.info("商户[{}]不存在统一配置,根据projectId[{}]进行匹配", dto.getMerchantId(), dto.getProjectId());
|
|
|
|
|
|
- // 使用 Stream API 过滤出当前项目对应的配置
|
|
|
- Optional<MaProjectFareSetting> projectConfigOpt = list.stream()
|
|
|
+ Optional<MaProjectFareSetting> projectConfigOpt = configList.stream()
|
|
|
.filter(item -> item.getProjectId() != null && item.getProjectId().equals(dto.getProjectId()))
|
|
|
.findFirst();
|
|
|
|
|
|
if (projectConfigOpt.isPresent()) {
|
|
|
+ // 找到了对应项目的配置
|
|
|
finalConfig = projectConfigOpt.get();
|
|
|
- } else {
|
|
|
- // 可选:如果没找到对应项目的配置,是否回退到第一条?或者报错?
|
|
|
- // 这里假设必须精确匹配,否则抛异常或给默认值
|
|
|
- throw new ServiceException("未找到该项目[" + dto.getProjectId() + "]的专属车费配置");
|
|
|
}
|
|
|
}
|
|
|
- } else {
|
|
|
- // 【情况C】商户无配置 -> 获取后台城市车费设置
|
|
|
- log.info("商户[{}]无配置,尝试获取城市[{}]的默认配置", dto.getMerchantId(), dto.getCityCode());
|
|
|
- // TODO: 调用获取城市配置的 Service/Method
|
|
|
- // finalConfig = cityFareService.getDefaultByCity(dto.getCityCode());
|
|
|
- TFareSettingVo fareSetting = fareSettingService.getFareSetting("", dto.getCityCode());
|
|
|
}
|
|
|
|
|
|
- // 此时 finalConfig 应该已经有值了,继续后续的距离计算逻辑...
|
|
|
if (ObjectUtil.isNull(finalConfig)) {
|
|
|
throw new ServiceException("无法获取有效的车费计算规则");
|
|
|
}
|
|
|
|
|
|
- // 2. 判断时间段 (白天 vs 夜间)
|
|
|
- // 假设白天是 7:30 - 19:30,其余为夜间 (具体逻辑根据你的业务定)
|
|
|
- LocalTime time = dto.getAppointmentStartTime().toLocalTime();
|
|
|
- boolean isDayTime = !time.isBefore(LocalTime.of(7, 30)) && time.isBefore(LocalTime.of(19, 30));
|
|
|
-
|
|
|
- BigDecimal freeKm = isDayTime ? finalConfig.getDayFreeKm() : finalConfig.getNightFreeKm();
|
|
|
- result.setFreeDistanceKm(freeKm);
|
|
|
-
|
|
|
-
|
|
|
- // 3. 计算实际距离 (调用地图API)
|
|
|
-//
|
|
|
-// // 参数顺序:纬度(lat), 经度(lon)
|
|
|
-//
|
|
|
-//// double distance = CoordinateUtil(37.870000, 112.550000, 37.880000, 112.560000);
|
|
|
-//
|
|
|
-//// 返回值单位是 米 (m),转换为公里
|
|
|
-// double distanceKm = NumberUtil.div(distance, 1000, 2); // 保留2位小数
|
|
|
-//
|
|
|
-// double distanceInMeters = this.baseMapper.calculateDistance(
|
|
|
-// dto.getLatitude(), dto.getLongitude(),
|
|
|
-// targetLat, targetLon
|
|
|
-// );
|
|
|
-//
|
|
|
-// BigDecimal actualKm = new BigDecimal(distanceInMeters).divide(new BigDecimal("1000"), 2, RoundingMode.HALF_UP);
|
|
|
-// result.setActualDistanceKm(actualKm);
|
|
|
-//
|
|
|
-// // 4. 计算费用
|
|
|
-// BigDecimal freeKm = result.getFreeDistanceKm();
|
|
|
-// BigDecimal exceedKm = actualKm.subtract(freeKm);
|
|
|
-//
|
|
|
-// // 如果小于0,说明在免费范围内
|
|
|
-// if (exceedKm.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
-// result.setExceedDistanceKm(BigDecimal.ZERO);
|
|
|
-// result.setEstimatedFare(BigDecimal.ZERO);
|
|
|
-// result.setIsFree(true);
|
|
|
-// result.setMessage(String.format("距离 %.2f km,在 %.2f km 免费范围内", actualKm, freeKm));
|
|
|
-// } else {
|
|
|
-// result.setExceedDistanceKm(exceedKm);
|
|
|
-// // 费用 = 超出距离 * 单价
|
|
|
-// BigDecimal fare = exceedKm.multiply(UNIT_PRICE).setScale(2, RoundingMode.HALF_UP);
|
|
|
-// result.setEstimatedFare(fare);
|
|
|
-// result.setIsFree(false);
|
|
|
-// result.setMessage(String.format("超出免费里程 %.2f km,需支付车费", exceedKm));
|
|
|
-// }
|
|
|
+ // 商户配置的免车费距离(用于扣减)
|
|
|
+ BigDecimal merchantFreeKm = isDay ? finalConfig.getDayFreeKm() : finalConfig.getNightFreeKm();
|
|
|
+
|
|
|
+ if (ObjectUtil.isNull(merchantFreeKm) || merchantFreeKm.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ merchantFreeKm = BigDecimal.ZERO;
|
|
|
+ log.info("商户[{}]配置的免车费距离为 null 或 <= 0, 视为 0", dto.getMerchantId());
|
|
|
+ } else {
|
|
|
+ log.info("商户[{}]使用配置ID={}, 免费公里数: {}", dto.getMerchantId(), finalConfig.getId(), merchantFreeKm);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 计算【打车距离】(即计费里程)
|
|
|
+ BigDecimal straightLineBigDecimal = new BigDecimal(straightLineKm).setScale(6, RoundingMode.HALF_UP);
|
|
|
+ BigDecimal effectiveDistance = straightLineBigDecimal.subtract(merchantFreeKm);
|
|
|
+ if (effectiveDistance.compareTo(BigDecimal.ZERO) < 0) {
|
|
|
+ effectiveDistance = BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+ log.info("直线距离 {} km, 商户免车费距离 {} km -> 打车距离 {} km",
|
|
|
+ straightLineKm, merchantFreeKm, effectiveDistance);
|
|
|
+
|
|
|
+ // 5. 获取城市车费规则(用于最终计费)
|
|
|
+ TFareSettingVo cityFare = fareSettingService.getFareSetting(appointmentStartTime.toString(), dto.getCityCode());
|
|
|
+ if (ObjectUtil.isNull(cityFare)) {
|
|
|
+ throw new ServiceException("未找到城市[" + dto.getCityCode() + "]的车费配置");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 使用城市规则计算费用
|
|
|
+ BigDecimal baseFare = cityFare.getBaseFare(); // 起步价
|
|
|
+ BigDecimal baseDistance = cityFare.getBaseDistance(); // 起步距离(公里)
|
|
|
+ BigDecimal additionalFarePer = cityFare.getAdditionalFarePer(); // 超出后每公里价格
|
|
|
+
|
|
|
+ BigDecimal estimatedFare;
|
|
|
+ boolean isFree;
|
|
|
+
|
|
|
+ // 如果打车距离为 0,表示全程被商户免车费覆盖,直接免费
|
|
|
+ if (effectiveDistance.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ estimatedFare = BigDecimal.ZERO;
|
|
|
+ isFree = true;
|
|
|
+ } else {
|
|
|
+ // 打车距离 > 0,才进入城市计费逻辑
|
|
|
+ BigDecimal exceedDistance = effectiveDistance.subtract(baseDistance);
|
|
|
+ if (exceedDistance.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ // 在起步距离内(但打车距离 > 0)
|
|
|
+ estimatedFare = baseFare;
|
|
|
+ isFree = false;
|
|
|
+ } else {
|
|
|
+ // 超出起步距离
|
|
|
+ BigDecimal extraFare = exceedDistance.multiply(additionalFarePer).setScale(2, RoundingMode.HALF_UP);
|
|
|
+ estimatedFare = baseFare.add(extraFare).setScale(2, RoundingMode.HALF_UP);
|
|
|
+ isFree = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7. 设置结果
|
|
|
+ result.setActualDistanceKm(straightLineBigDecimal.setScale(2, RoundingMode.HALF_UP)); // 原始直线距离(展示用)
|
|
|
+ result.setEstimatedFare(estimatedFare);
|
|
|
+ result.setIsFree(isFree);
|
|
|
|
|
|
return result;
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断预约时间是否属于白天时间段
|
|
|
+ *
|
|
|
+ * @param appointmentStartTime 预约开始时间
|
|
|
+ * @return true表示是白天时间段,false表示不是白天时间段(可能是夜间或其他时间段)
|
|
|
+ */
|
|
|
+ public boolean isDayTimePeriod(LocalDateTime appointmentStartTime) {
|
|
|
+ // 提取预约时间的小时和分钟,仅用于时间段比较
|
|
|
+ LocalTime appointmentTime = appointmentStartTime.toLocalTime();
|
|
|
+
|
|
|
+ // 查询白天时间段配置
|
|
|
+ List<SysDictData> dayTimeRanges = DictUtils.getSortedDictCache("day_time");
|
|
|
+
|
|
|
+ if (CollUtil.isNotEmpty(dayTimeRanges) && dayTimeRanges.size() >= 2) {
|
|
|
+ // 获取开始时间和结束时间
|
|
|
+ LocalTime dayStartTime = parseTime(CollUtil.getFirst(dayTimeRanges).getDictValue()); // 如 7:30
|
|
|
+ LocalTime dayEndTime = parseTime(CollUtil.getLast(dayTimeRanges).getDictValue()); // 如 19:30
|
|
|
+
|
|
|
+ // 判断是否在白天时间段范围内
|
|
|
+ return isTimeInRange(appointmentTime, dayStartTime, dayEndTime);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有找到白天时间段配置,默认返回false
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析时间字符串为LocalTime
|
|
|
+ */
|
|
|
+ private LocalTime parseTime(String timeStr) {
|
|
|
+ // 处理可能的格式差异,比如"7:30"转为"07:30"
|
|
|
+ if (!timeStr.contains(":")) {
|
|
|
+ throw new IllegalArgumentException("无效的时间格式: " + timeStr);
|
|
|
+ }
|
|
|
+
|
|
|
+ String[] parts = timeStr.split(":");
|
|
|
+ if (parts.length == 2) {
|
|
|
+ int hour = Integer.parseInt(parts[0]);
|
|
|
+ int minute = Integer.parseInt(parts[1]);
|
|
|
+ return LocalTime.of(hour, minute);
|
|
|
+ } else {
|
|
|
+ throw new IllegalArgumentException("无效的时间格式: " + timeStr);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断时间是否在时间段范围内(不跨越午夜)
|
|
|
+ */
|
|
|
+ private boolean isTimeInRange(LocalTime targetTime, LocalTime startTime, LocalTime endTime) {
|
|
|
+ // 如果开始时间小于结束时间(同一天内)
|
|
|
+ if (startTime.isBefore(endTime)) {
|
|
|
+ return (targetTime.equals(startTime) || targetTime.isAfter(startTime)) &&
|
|
|
+ (targetTime.equals(endTime) || targetTime.isBefore(endTime));
|
|
|
+ } else if (startTime.isAfter(endTime)) {
|
|
|
+ // 如果开始时间大于结束时间(跨越午夜),这应该是夜间时间段
|
|
|
+ return false;
|
|
|
+ } else {
|
|
|
+ // 如果开始时间等于结束时间
|
|
|
+ return targetTime.equals(startTime);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|