| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- 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<MaProjectFareSettingMapper, MaProjectFareSetting> 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<TAddress>()
- .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<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);
- }
- }
- /**
- * 保存免车费设置信息
- *
- * @param
- * @param dto
- * @return
- */
- @Override
- public void saveOrUpdateFee(DriverFeeDTO dto){
- // 1. 基础校验:检查是否有空值 (对应UI中的“判断必填项是否为空”)
- validateInput(dto);
- if (dto.getMode() == 1) {
- // --- 模式一:统一设置 ---
- // 逻辑:删除该用户所有具体的项目配置,只保留一条 categoryId=null 的记录
- baseMapper.delete(new LambdaQueryWrapper<MaProjectFareSetting>()
- .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<MaProjectFareSetting>()
- .eq(MaProjectFareSetting::getMerchantId, dto.getMerchantId()));
- List<MaProjectFareSetting> 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<MaProjectFareSetting> wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(MaProjectFareSetting::getMerchantId, merchantId)
- .eq(MaProjectFareSetting::getIsDelete, 0);
- List<MaProjectFareSetting> configList = this.baseMapper.selectList(wrapper);
- MaProjectFareSetting finalConfig = null;
- 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 {
- // 查找匹配的项目配置
- log.info("商户[{}]不存在统一配置,根据projectId[{}]进行匹配", merchantId, projectId);
- Optional<MaProjectFareSetting> 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;
- }
- }
|