package com.ylx.fareSetting.service.impl; import cn.hutool.core.collection.CollUtil; 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; import com.ylx.fareSetting.mapper.MaProjectFareSettingMapper; import com.ylx.fareSetting.service.IMaProjectFareSettingService; import com.ylx.massage.domain.TAddress; import com.ylx.massage.domain.TFareFreeRule; import com.ylx.massage.domain.dto.DriverFeeDTO; import com.ylx.massage.domain.vo.TFareSettingVo; import com.ylx.massage.service.TAddressService; import com.ylx.massage.service.TFareSettingService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import java.util.Optional; @Slf4j @Service public class MaProjectFareSettingServiceImpl extends ServiceImpl implements IMaProjectFareSettingService { @Resource private TFareSettingService fareSettingService; @Resource private TAddressService addressService; @Override public FareCalculateResultVO calculateFare(FareCalculateDTO dto) { FareCalculateResultVO result = new FareCalculateResultVO(); // 1.获取商户的默认地址 TAddress address = this.addressService.getOne(new LambdaQueryWrapper() .eq(TAddress::getMerchantId, dto.getMerchantId()) .eq(TAddress::getIsDefault, 1) .eq(TAddress::getIsDelete, 0)); if (ObjectUtil.isNull(address)) { throw new ServiceException("无法获取商户的默认地址"); } // 2. 计算直线距离(公里) String distanceStr = DistanceUtil.formatDistanceInKilometers( dto.getLatitude(),dto.getLongitude(), address.getLatitude(),address.getLongitude() ); 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. 获取商户车费配置 BigDecimal merchantFreeKm = getMerchantFreeKm(dto.getMerchantId(), dto.getProjectId(), isDay); // 5. 计算【打车距离】(即计费里程) 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); // 6. 获取城市车费规则(用于最终计费) TFareSettingVo cityFare = fareSettingService.getFareSetting(appointmentStartTime, dto.getCityCode()); if (ObjectUtil.isNull(cityFare)) { throw new ServiceException("未找到城市[" + dto.getCityCode() + "]的车费配置"); } // 7. 使用城市规则计算费用 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; } } // 8. 设置结果 result.setFreeKm(merchantFreeKm); result.setBaseFare(baseFare); result.setBaseDistance(baseDistance); result.setAdditionalFarePer(additionalFarePer); result.setActualDistanceKm(straightLineBigDecimal.setScale(2, RoundingMode.HALF_UP)); // 原始直线距离(展示用) result.setEstimatedFare(estimatedFare); result.setIsFree(isFree); return result; } /** * 判断预约时间是否属于白天时间段 * * @param appointmentStartTime 预约开始时间 * @return true表示是白天时间段,false表示不是白天时间段(可能是夜间或其他时间段) */ @Override public boolean isDayTimePeriod(LocalDateTime appointmentStartTime) { // 提取预约时间的小时和分钟,仅用于时间段比较 LocalTime appointmentTime = appointmentStartTime.toLocalTime(); // 查询白天时间段配置 List 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); } } /** * 保存免车费设置信息 * * @param * @param dto * @return */ @Override public void saveOrUpdateFee(DriverFeeDTO dto){ // 1. 基础校验:检查是否有空值 (对应UI中的“判断必填项是否为空”) validateInput(dto); if (dto.getMode() == 1) { // --- 模式一:统一设置 --- // 逻辑:删除该用户所有具体的项目配置,只保留一条 categoryId=null 的记录 baseMapper.delete(new LambdaQueryWrapper() .eq(MaProjectFareSetting::getMerchantId, dto.getMerchantId())); MaProjectFareSetting config = new MaProjectFareSetting(); config.setMerchantId(dto.getMerchantId()); config.setProjectId(null); config.setDayFreeKm(dto.getUnifiedConfig().getDayFreeKm()); config.setNightFreeKm(dto.getUnifiedConfig().getNightFreeKm()); baseMapper.insert(config); } else if (dto.getMode() == 2) { // --- 模式二:按项目设置 --- // 逻辑:先删除旧数据,再批量插入新数据 baseMapper.delete(new LambdaQueryWrapper() .eq(MaProjectFareSetting::getMerchantId, dto.getMerchantId())); List list = new ArrayList<>(); for (DriverFeeDTO.CategoryFeeItem item : dto.getCategoryConfigs()) { MaProjectFareSetting config = new MaProjectFareSetting(); config.setMerchantId(dto.getMerchantId()); config.setIsUnified(0); config.setProjectId(item.getCategoryId()); config.setProjectName(item.getCategoryName()); config.setDayFreeKm(item.getDayFreeKm()); config.setNightFreeKm(item.getNightFreeKm()); list.add(config); } // 批量保存 for (MaProjectFareSetting config : list) { baseMapper.insert(config); } } } /** * 校验输入是否合法 (对应UI中的 Toast 提示逻辑) */ private void validateInput(DriverFeeDTO dto) { if (dto.getMode() == 1) { if (dto.getUnifiedConfig() == null || dto.getUnifiedConfig().getDayFreeKm() == null || dto.getUnifiedConfig().getNightFreeKm() == null) { throw new RuntimeException("请输入完整的统一设置里程"); } } else if (dto.getMode() == 2) { if (CollectionUtils.isEmpty(dto.getCategoryConfigs())) { throw new RuntimeException("请至少配置一个项目的免车费"); } for (DriverFeeDTO.CategoryFeeItem item : dto.getCategoryConfigs()) { if (item.getDayFreeKm() == null || item.getNightFreeKm() == null) { throw new RuntimeException("分类ID[" + item.getCategoryId() + "]的里程设置不完整"); } } } } /** * 获取商户当前适用的免车费距离 * * @param merchantId 商户ID * @param projectId 项目ID * @param isDay 是否为白天 (true: 白天, false: 夜间) * @return 适用的免车费距离,如果未配置或配置无效则返回 BigDecimal.ZERO */ @Override public BigDecimal getMerchantFreeKm(Long merchantId, Long projectId, boolean isDay) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(MaProjectFareSetting::getMerchantId, merchantId) .eq(MaProjectFareSetting::getIsDelete, 0); List configList = this.baseMapper.selectList(wrapper); MaProjectFareSetting finalConfig = null; if (CollUtil.isNotEmpty(configList)) { // 先查找是否存在统一配置 (isUnified = 1) Optional unifiedOpt = configList.stream() .filter(c -> ObjectUtil.equals(1, c.getIsUnified())) .findFirst(); if (unifiedOpt.isPresent()) { finalConfig = unifiedOpt.get(); } else { // 查找匹配的项目配置 log.info("商户[{}]不存在统一配置,根据projectId[{}]进行匹配", merchantId, projectId); Optional projectConfigOpt = configList.stream() .filter(item -> item.getProjectId() != null && item.getProjectId().equals(projectId)) .findFirst(); if (projectConfigOpt.isPresent()) { // 找到了对应项目的配置 finalConfig = projectConfigOpt.get(); } } } BigDecimal merchantFreeKm = BigDecimal.ZERO; if (ObjectUtil.isNotNull(finalConfig)) { // 商户配置的免车费距离(用于扣减) merchantFreeKm = isDay ? finalConfig.getDayFreeKm() : finalConfig.getNightFreeKm(); if (ObjectUtil.isNull(merchantFreeKm) || merchantFreeKm.compareTo(BigDecimal.ZERO) <= 0) { merchantFreeKm = BigDecimal.ZERO; log.info("商户[{}]配置的免车费距离为 null 或 <= 0, 视为 0", merchantId); } else { log.info("商户[{}]使用配置ID={}, 免费公里数: {}", merchantId, finalConfig.getId(), merchantFreeKm); } } return merchantFreeKm; } }