MaProjectFareSettingServiceImpl.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. package com.ylx.fareSetting.service.impl;
  2. import cn.hutool.core.collection.CollUtil;
  3. import cn.hutool.core.util.ObjectUtil;
  4. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  5. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  6. import com.ylx.common.core.domain.entity.SysDictData;
  7. import com.ylx.common.exception.ServiceException;
  8. import com.ylx.common.utils.DictUtils;
  9. import com.ylx.common.utils.DistanceUtil;
  10. import com.ylx.fareSetting.domian.MaProjectFareSetting;
  11. import com.ylx.fareSetting.domian.dto.FareCalculateDTO;
  12. import com.ylx.fareSetting.domian.vo.FareCalculateResultVO;
  13. import com.ylx.fareSetting.mapper.MaProjectFareSettingMapper;
  14. import com.ylx.fareSetting.service.IMaProjectFareSettingService;
  15. import com.ylx.massage.domain.TAddress;
  16. import com.ylx.massage.domain.TFareFreeRule;
  17. import com.ylx.massage.domain.dto.DriverFeeDTO;
  18. import com.ylx.massage.domain.vo.TFareSettingVo;
  19. import com.ylx.massage.service.TAddressService;
  20. import com.ylx.massage.service.TFareSettingService;
  21. import lombok.extern.slf4j.Slf4j;
  22. import org.springframework.stereotype.Service;
  23. import org.springframework.util.CollectionUtils;
  24. import javax.annotation.Resource;
  25. import java.math.BigDecimal;
  26. import java.math.RoundingMode;
  27. import java.time.LocalDateTime;
  28. import java.time.LocalTime;
  29. import java.util.ArrayList;
  30. import java.util.List;
  31. import java.util.Optional;
  32. @Slf4j
  33. @Service
  34. public class MaProjectFareSettingServiceImpl extends ServiceImpl<MaProjectFareSettingMapper, MaProjectFareSetting> implements IMaProjectFareSettingService {
  35. @Resource
  36. private TFareSettingService fareSettingService;
  37. @Resource
  38. private TAddressService addressService;
  39. @Override
  40. public FareCalculateResultVO calculateFare(FareCalculateDTO dto) {
  41. FareCalculateResultVO result = new FareCalculateResultVO();
  42. // 1.获取商户的默认地址
  43. TAddress address = this.addressService.getOne(new LambdaQueryWrapper<TAddress>()
  44. .eq(TAddress::getMerchantId, dto.getMerchantId())
  45. .eq(TAddress::getIsDefault, 1)
  46. .eq(TAddress::getIsDelete, 0));
  47. if (ObjectUtil.isNull(address)) {
  48. throw new ServiceException("无法获取商户的默认地址");
  49. }
  50. // 2. 计算直线距离(公里)
  51. String distanceStr = DistanceUtil.formatDistanceInKilometers(
  52. dto.getLatitude(),dto.getLongitude(),
  53. address.getLatitude(),address.getLongitude()
  54. );
  55. double straightLineKm;
  56. if ("未知".equals(distanceStr)) {
  57. throw new ServiceException("无法获取客户地址信息");
  58. }
  59. try {
  60. straightLineKm = Double.parseDouble(distanceStr);
  61. } catch (NumberFormatException e) {
  62. log.error("距离字符串解析失败: {}", distanceStr, e);
  63. throw new ServiceException("距离数据异常");
  64. }
  65. // 3.根据时间段判断是否白天时间段
  66. LocalDateTime appointmentStartTime = dto.getAppointmentStartTime();
  67. boolean isDay = isDayTimePeriod(appointmentStartTime);
  68. // 4. 获取商户车费配置
  69. BigDecimal merchantFreeKm = getMerchantFreeKm(dto.getMerchantId(), dto.getProjectId(), isDay);
  70. // 5. 计算【打车距离】(即计费里程)
  71. BigDecimal straightLineBigDecimal = new BigDecimal(straightLineKm).setScale(6, RoundingMode.HALF_UP);
  72. BigDecimal effectiveDistance = straightLineBigDecimal.subtract(merchantFreeKm);
  73. if (effectiveDistance.compareTo(BigDecimal.ZERO) < 0) {
  74. effectiveDistance = BigDecimal.ZERO;
  75. }
  76. log.info("直线距离 {} km, 商户免车费距离 {} km -> 打车距离 {} km",
  77. straightLineKm, merchantFreeKm, effectiveDistance);
  78. // 6. 获取城市车费规则(用于最终计费)
  79. TFareSettingVo cityFare = fareSettingService.getFareSetting(appointmentStartTime, dto.getCityCode());
  80. if (ObjectUtil.isNull(cityFare)) {
  81. throw new ServiceException("未找到城市[" + dto.getCityCode() + "]的车费配置");
  82. }
  83. // 7. 使用城市规则计算费用
  84. BigDecimal baseFare = cityFare.getBaseFare(); // 起步价
  85. BigDecimal baseDistance = cityFare.getBaseDistance(); // 起步距离(公里)
  86. BigDecimal additionalFarePer = cityFare.getAdditionalFarePer(); // 超出后每公里价格
  87. BigDecimal estimatedFare;
  88. boolean isFree;
  89. // 如果打车距离为 0,表示全程被商户免车费覆盖,直接免费
  90. if (effectiveDistance.compareTo(BigDecimal.ZERO) <= 0) {
  91. estimatedFare = BigDecimal.ZERO;
  92. isFree = true;
  93. } else {
  94. // 打车距离 > 0,才进入城市计费逻辑
  95. BigDecimal exceedDistance = effectiveDistance.subtract(baseDistance);
  96. if (exceedDistance.compareTo(BigDecimal.ZERO) <= 0) {
  97. // 在起步距离内(但打车距离 > 0)
  98. estimatedFare = baseFare;
  99. isFree = false;
  100. } else {
  101. // 超出起步距离
  102. BigDecimal extraFare = exceedDistance.multiply(additionalFarePer).setScale(2, RoundingMode.HALF_UP);
  103. estimatedFare = baseFare.add(extraFare).setScale(2, RoundingMode.HALF_UP);
  104. isFree = false;
  105. }
  106. }
  107. // 8. 设置结果
  108. result.setFreeKm(merchantFreeKm);
  109. result.setBaseFare(baseFare);
  110. result.setBaseDistance(baseDistance);
  111. result.setAdditionalFarePer(additionalFarePer);
  112. result.setActualDistanceKm(straightLineBigDecimal.setScale(2, RoundingMode.HALF_UP)); // 原始直线距离(展示用)
  113. result.setEstimatedFare(estimatedFare);
  114. result.setIsFree(isFree);
  115. return result;
  116. }
  117. /**
  118. * 判断预约时间是否属于白天时间段
  119. *
  120. * @param appointmentStartTime 预约开始时间
  121. * @return true表示是白天时间段,false表示不是白天时间段(可能是夜间或其他时间段)
  122. */
  123. @Override
  124. public boolean isDayTimePeriod(LocalDateTime appointmentStartTime) {
  125. // 提取预约时间的小时和分钟,仅用于时间段比较
  126. LocalTime appointmentTime = appointmentStartTime.toLocalTime();
  127. // 查询白天时间段配置
  128. List<SysDictData> dayTimeRanges = DictUtils.getSortedDictCache("day_time");
  129. if (CollUtil.isNotEmpty(dayTimeRanges) && dayTimeRanges.size() >= 2) {
  130. // 获取开始时间和结束时间
  131. LocalTime dayStartTime = parseTime(CollUtil.getFirst(dayTimeRanges).getDictValue()); // 如 7:30
  132. LocalTime dayEndTime = parseTime(CollUtil.getLast(dayTimeRanges).getDictValue()); // 如 19:30
  133. // 判断是否在白天时间段范围内
  134. return isTimeInRange(appointmentTime, dayStartTime, dayEndTime);
  135. }
  136. // 如果没有找到白天时间段配置,默认返回false
  137. return false;
  138. }
  139. /**
  140. * 解析时间字符串为LocalTime
  141. */
  142. private LocalTime parseTime(String timeStr) {
  143. // 处理可能的格式差异,比如"7:30"转为"07:30"
  144. if (!timeStr.contains(":")) {
  145. throw new IllegalArgumentException("无效的时间格式: " + timeStr);
  146. }
  147. String[] parts = timeStr.split(":");
  148. if (parts.length == 2) {
  149. int hour = Integer.parseInt(parts[0]);
  150. int minute = Integer.parseInt(parts[1]);
  151. return LocalTime.of(hour, minute);
  152. } else {
  153. throw new IllegalArgumentException("无效的时间格式: " + timeStr);
  154. }
  155. }
  156. /**
  157. * 判断时间是否在时间段范围内(不跨越午夜)
  158. */
  159. private boolean isTimeInRange(LocalTime targetTime, LocalTime startTime, LocalTime endTime) {
  160. // 如果开始时间小于结束时间(同一天内)
  161. if (startTime.isBefore(endTime)) {
  162. return (targetTime.equals(startTime) || targetTime.isAfter(startTime)) &&
  163. (targetTime.equals(endTime) || targetTime.isBefore(endTime));
  164. } else if (startTime.isAfter(endTime)) {
  165. // 如果开始时间大于结束时间(跨越午夜),这应该是夜间时间段
  166. return false;
  167. } else {
  168. // 如果开始时间等于结束时间
  169. return targetTime.equals(startTime);
  170. }
  171. }
  172. /**
  173. * 保存免车费设置信息
  174. *
  175. * @param
  176. * @param dto
  177. * @return
  178. */
  179. @Override
  180. public void saveOrUpdateFee(DriverFeeDTO dto){
  181. // 1. 基础校验:检查是否有空值 (对应UI中的“判断必填项是否为空”)
  182. validateInput(dto);
  183. if (dto.getMode() == 1) {
  184. // --- 模式一:统一设置 ---
  185. // 逻辑:删除该用户所有具体的项目配置,只保留一条 categoryId=null 的记录
  186. baseMapper.delete(new LambdaQueryWrapper<MaProjectFareSetting>()
  187. .eq(MaProjectFareSetting::getMerchantId, dto.getMerchantId()));
  188. MaProjectFareSetting config = new MaProjectFareSetting();
  189. config.setMerchantId(dto.getMerchantId());
  190. config.setProjectId(null);
  191. config.setDayFreeKm(dto.getUnifiedConfig().getDayFreeKm());
  192. config.setNightFreeKm(dto.getUnifiedConfig().getNightFreeKm());
  193. baseMapper.insert(config);
  194. } else if (dto.getMode() == 2) {
  195. // --- 模式二:按项目设置 ---
  196. // 逻辑:先删除旧数据,再批量插入新数据
  197. baseMapper.delete(new LambdaQueryWrapper<MaProjectFareSetting>()
  198. .eq(MaProjectFareSetting::getMerchantId, dto.getMerchantId()));
  199. List<MaProjectFareSetting> list = new ArrayList<>();
  200. for (DriverFeeDTO.CategoryFeeItem item : dto.getCategoryConfigs()) {
  201. MaProjectFareSetting config = new MaProjectFareSetting();
  202. config.setMerchantId(dto.getMerchantId());
  203. config.setIsUnified(0);
  204. config.setProjectId(item.getCategoryId());
  205. config.setProjectName(item.getCategoryName());
  206. config.setDayFreeKm(item.getDayFreeKm());
  207. config.setNightFreeKm(item.getNightFreeKm());
  208. list.add(config);
  209. }
  210. // 批量保存
  211. for (MaProjectFareSetting config : list) {
  212. baseMapper.insert(config);
  213. }
  214. }
  215. }
  216. /**
  217. * 校验输入是否合法 (对应UI中的 Toast 提示逻辑)
  218. */
  219. private void validateInput(DriverFeeDTO dto) {
  220. if (dto.getMode() == 1) {
  221. if (dto.getUnifiedConfig() == null ||
  222. dto.getUnifiedConfig().getDayFreeKm() == null ||
  223. dto.getUnifiedConfig().getNightFreeKm() == null) {
  224. throw new RuntimeException("请输入完整的统一设置里程");
  225. }
  226. } else if (dto.getMode() == 2) {
  227. if (CollectionUtils.isEmpty(dto.getCategoryConfigs())) {
  228. throw new RuntimeException("请至少配置一个项目的免车费");
  229. }
  230. for (DriverFeeDTO.CategoryFeeItem item : dto.getCategoryConfigs()) {
  231. if (item.getDayFreeKm() == null || item.getNightFreeKm() == null) {
  232. throw new RuntimeException("分类ID[" + item.getCategoryId() + "]的里程设置不完整");
  233. }
  234. }
  235. }
  236. }
  237. /**
  238. * 获取商户当前适用的免车费距离
  239. *
  240. * @param merchantId 商户ID
  241. * @param projectId 项目ID
  242. * @param isDay 是否为白天 (true: 白天, false: 夜间)
  243. * @return 适用的免车费距离,如果未配置或配置无效则返回 BigDecimal.ZERO
  244. */
  245. @Override
  246. public BigDecimal getMerchantFreeKm(Long merchantId, Long projectId, boolean isDay) {
  247. LambdaQueryWrapper<MaProjectFareSetting> wrapper = new LambdaQueryWrapper<>();
  248. wrapper.eq(MaProjectFareSetting::getMerchantId, merchantId)
  249. .eq(MaProjectFareSetting::getIsDelete, 0);
  250. List<MaProjectFareSetting> configList = this.baseMapper.selectList(wrapper);
  251. MaProjectFareSetting finalConfig = null;
  252. if (CollUtil.isNotEmpty(configList)) {
  253. // 先查找是否存在统一配置 (isUnified = 1)
  254. Optional<MaProjectFareSetting> unifiedOpt = configList.stream()
  255. .filter(c -> ObjectUtil.equals(1, c.getIsUnified()))
  256. .findFirst();
  257. if (unifiedOpt.isPresent()) {
  258. finalConfig = unifiedOpt.get();
  259. } else {
  260. // 查找匹配的项目配置
  261. log.info("商户[{}]不存在统一配置,根据projectId[{}]进行匹配", merchantId, projectId);
  262. Optional<MaProjectFareSetting> projectConfigOpt = configList.stream()
  263. .filter(item -> item.getProjectId() != null && item.getProjectId().equals(projectId))
  264. .findFirst();
  265. if (projectConfigOpt.isPresent()) {
  266. // 找到了对应项目的配置
  267. finalConfig = projectConfigOpt.get();
  268. }
  269. }
  270. }
  271. BigDecimal merchantFreeKm = BigDecimal.ZERO;
  272. if (ObjectUtil.isNotNull(finalConfig)) {
  273. // 商户配置的免车费距离(用于扣减)
  274. merchantFreeKm = isDay ? finalConfig.getDayFreeKm() : finalConfig.getNightFreeKm();
  275. if (ObjectUtil.isNull(merchantFreeKm) || merchantFreeKm.compareTo(BigDecimal.ZERO) <= 0) {
  276. merchantFreeKm = BigDecimal.ZERO;
  277. log.info("商户[{}]配置的免车费距离为 null 或 <= 0, 视为 0", merchantId);
  278. } else {
  279. log.info("商户[{}]使用配置ID={}, 免费公里数: {}", merchantId, finalConfig.getId(), merchantFreeKm);
  280. }
  281. }
  282. return merchantFreeKm;
  283. }
  284. }