From 8f1ab4e9c685db5fee554897346969ab002bf928 Mon Sep 17 00:00:00 2001 From: liuyuxin Date: Thu, 19 Mar 2026 15:37:39 +0800 Subject: [PATCH] =?UTF-8?q?=E9=85=8D=E9=80=81=E9=A1=B5=E9=9D=A2=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=86=85=E5=AE=B9=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/VehicleTypeController.java | 2 +- .../document/domain/DeliveryOrder.java | 6 +- .../domain/dto/CalcSuggestFeeDTO.java | 3 +- .../domain/dto/DeliveryOrderCreateDTO.java | 2 +- .../document/domain/vo/CalcSuggestFeeVO.java | 92 ++- .../document/domain/vo/DeliveryOrderVo.java | 2 +- .../document/mapper/DeliveryOrderMapper.java | 2 + .../document/mapper/VehicleTypeMapper.java | 4 + .../document/service/IVehicleTypeService.java | 2 + .../impl/DeliveryOrderServiceImpl.java | 52 +- .../service/impl/VehicleTypeServiceImpl.java | 596 ++++++++++++------ .../mybatis/document/DeliveryOrderMapper.xml | 15 +- .../mybatis/document/VehicleTypeMapper.xml | 7 +- 13 files changed, 548 insertions(+), 237 deletions(-) diff --git a/src/main/java/com/delivery/project/document/controller/VehicleTypeController.java b/src/main/java/com/delivery/project/document/controller/VehicleTypeController.java index 9fc8353..15f7771 100644 --- a/src/main/java/com/delivery/project/document/controller/VehicleTypeController.java +++ b/src/main/java/com/delivery/project/document/controller/VehicleTypeController.java @@ -102,7 +102,7 @@ public class VehicleTypeController extends BaseController */ @PreAuthorize("@ss.hasPermi('document:type:remove')") @Log(title = "车型定义", businessType = BusinessType.DELETE) - @DeleteMapping("/{ids}") + @DeleteMapping("/{ids:\\d+}") public AjaxResult remove(@PathVariable Long[] ids) { return toAjax(vehicleTypeService.deleteVehicleTypeByIds(ids)); diff --git a/src/main/java/com/delivery/project/document/domain/DeliveryOrder.java b/src/main/java/com/delivery/project/document/domain/DeliveryOrder.java index 99b55b8..96a9499 100644 --- a/src/main/java/com/delivery/project/document/domain/DeliveryOrder.java +++ b/src/main/java/com/delivery/project/document/domain/DeliveryOrder.java @@ -152,7 +152,7 @@ public class DeliveryOrder extends BaseEntity { private String orderStatus; /** 车辆类型ID */ - private Long vehicleTypeId; + private String vehicleTypeId; /** 车辆类型名称 */ @Excel(name = "车辆类型名称") @@ -312,8 +312,8 @@ public class DeliveryOrder extends BaseEntity { public String getOrderStatus() { return orderStatus; } public void setOrderStatus(String orderStatus) { this.orderStatus = orderStatus; } - public Long getVehicleTypeId() { return vehicleTypeId; } - public void setVehicleTypeId(Long vehicleTypeId) { this.vehicleTypeId = vehicleTypeId; } + public String getVehicleTypeId() { return vehicleTypeId; } + public void setVehicleTypeId(String vehicleTypeId) { this.vehicleTypeId = vehicleTypeId; } public String getVehicleTypeName() { return vehicleTypeName; } public void setVehicleTypeName(String vehicleTypeName) { this.vehicleTypeName = vehicleTypeName; } diff --git a/src/main/java/com/delivery/project/document/domain/dto/CalcSuggestFeeDTO.java b/src/main/java/com/delivery/project/document/domain/dto/CalcSuggestFeeDTO.java index 69443aa..6f97b0f 100644 --- a/src/main/java/com/delivery/project/document/domain/dto/CalcSuggestFeeDTO.java +++ b/src/main/java/com/delivery/project/document/domain/dto/CalcSuggestFeeDTO.java @@ -26,5 +26,6 @@ public class CalcSuggestFeeDTO { private BigDecimal distanceKm; /** 指定车型ID(可选;传入则按该车型计算建议费用) */ - private Long vehicleTypeId; + private String vehicleTypeId; + } diff --git a/src/main/java/com/delivery/project/document/domain/dto/DeliveryOrderCreateDTO.java b/src/main/java/com/delivery/project/document/domain/dto/DeliveryOrderCreateDTO.java index 684b65f..c8a1e2f 100644 --- a/src/main/java/com/delivery/project/document/domain/dto/DeliveryOrderCreateDTO.java +++ b/src/main/java/com/delivery/project/document/domain/dto/DeliveryOrderCreateDTO.java @@ -83,7 +83,7 @@ public class DeliveryOrderCreateDTO { private String orderStatus; /** 车型 ID */ - private Long vehicleTypeId; + private String vehicleTypeId; /** 车型名称 */ private String vehicleTypeName; diff --git a/src/main/java/com/delivery/project/document/domain/vo/CalcSuggestFeeVO.java b/src/main/java/com/delivery/project/document/domain/vo/CalcSuggestFeeVO.java index b46a1b1..381cc67 100644 --- a/src/main/java/com/delivery/project/document/domain/vo/CalcSuggestFeeVO.java +++ b/src/main/java/com/delivery/project/document/domain/vo/CalcSuggestFeeVO.java @@ -1,4 +1,3 @@ -// src/main/java/com/delivery/project/document/domain/vo/CalcSuggestFeeVO.java package com.delivery.project.document.domain.vo; import lombok.Data; @@ -6,58 +5,93 @@ import java.math.BigDecimal; import java.util.List; /** - * 计算建议费用 - 响应体 VO - * 返回推荐/选定的车型、单价、建议费用,以及候选车型列表。 + * 计算建议费用 返回VO */ @Data public class CalcSuggestFeeVO { - /** 选定/推荐车型ID */ - private Long vehicleTypeId; - - /** 选定/推荐车型名称 */ - private String vehicleTypeName; - - /** 每公里单价(单位:元/公里) */ - private BigDecimal unitPricePerKm; - - /** 建议费用(单位:元,四舍五入保留2位小数) */ - private BigDecimal suggestFee; - - /** 错误信息 */ - private String errorMessage; - - /** 是否有适配车型 */ + /** 是否存在可用车型 */ private Boolean hasSuitableType; - /** 候选车型列表(按价格/容量排序) */ + /** 错误信息(无车型时返回) */ + private String errorMessage; + + /** 推荐总费用 */ + private BigDecimal suggestFee; + + /** 总车辆数 */ + private Integer totalVehicleCount; + + /** 货物重量 */ + private BigDecimal totalWeightTon; + + /** 货物体积 */ + private BigDecimal totalVolumeM3; + + /** 推荐车辆组合方案 */ + private List bestPlan; + + /** 候选车型列表 */ private List candidates; + /** 提示信息(不阻断) */ + private String warningMessage; /** - * 候选车型项 VO - * 表示一条可选车型的基本信息。 + * 推荐车辆组合 + */ + @Data + public static class VehiclePlanVO { + + /** 车型ID */ + private String vehicleTypeId; + + /** 车型名称 */ + private String vehicleTypeName; + + /** 车辆数量 */ + private Integer vehicleCount; + + /** 每公里单价 */ + private BigDecimal unitPricePerKm; + + /** 单车费用 */ + private BigDecimal singleVehicleFee; + + /** 该车型总费用 */ + private BigDecimal totalFee; + + /** 单车最大承重 */ + private BigDecimal weightMaxTon; + + /** 单车最大体积 */ + private BigDecimal volumeMaxM3; + } + + /** + * 候选车型 */ @Data public static class VehicleTypeOptionVO { + /** 车型ID */ - private Long id; + private String id; /** 车型名称 */ private String name; - /** 每公里单价(单位:元/公里) */ + /** 每公里单价 */ private BigDecimal unitPricePerKm; - /** 承重下限(单位:吨,含) */ + /** 最小承重 */ private BigDecimal weightMinTon; - /** 承重上限(单位:吨,含) */ + /** 最大承重 */ private BigDecimal weightMaxTon; - /** 载方下限(单位:立方米,含) */ + /** 最小体积 */ private BigDecimal volumeMinM3; - /** 载方上限(单位:吨,含) */ + /** 最大体积 */ private BigDecimal volumeMaxM3; } -} +} \ No newline at end of file diff --git a/src/main/java/com/delivery/project/document/domain/vo/DeliveryOrderVo.java b/src/main/java/com/delivery/project/document/domain/vo/DeliveryOrderVo.java index 6e2d934..8dcfa42 100644 --- a/src/main/java/com/delivery/project/document/domain/vo/DeliveryOrderVo.java +++ b/src/main/java/com/delivery/project/document/domain/vo/DeliveryOrderVo.java @@ -86,7 +86,7 @@ public class DeliveryOrderVo { private String orderStatus; /** 车型 ID */ - private Long vehicleTypeId; + private String vehicleTypeId; /** 车型名称 */ private String vehicleTypeName; diff --git a/src/main/java/com/delivery/project/document/mapper/DeliveryOrderMapper.java b/src/main/java/com/delivery/project/document/mapper/DeliveryOrderMapper.java index 54e7a27..9529c2a 100644 --- a/src/main/java/com/delivery/project/document/mapper/DeliveryOrderMapper.java +++ b/src/main/java/com/delivery/project/document/mapper/DeliveryOrderMapper.java @@ -113,4 +113,6 @@ public interface DeliveryOrderMapper /** 配送单VO列表 */ List selectDeliveryOrderVoList(DeliveryOrder deliveryOrder); + /** 防止重复创建配送单 */ + List selectExistsRkRecordIds(@Param("rkRecordIds") List rkRecordIds); } diff --git a/src/main/java/com/delivery/project/document/mapper/VehicleTypeMapper.java b/src/main/java/com/delivery/project/document/mapper/VehicleTypeMapper.java index c28dcf5..7630f80 100644 --- a/src/main/java/com/delivery/project/document/mapper/VehicleTypeMapper.java +++ b/src/main/java/com/delivery/project/document/mapper/VehicleTypeMapper.java @@ -77,4 +77,8 @@ public interface VehicleTypeMapper * @return 批量结果 */ List selectFallbackTypes(BigDecimal w, BigDecimal v); + /** + * 1 查询所有可用车型 + * */ + List selectUsableTypes(); } diff --git a/src/main/java/com/delivery/project/document/service/IVehicleTypeService.java b/src/main/java/com/delivery/project/document/service/IVehicleTypeService.java index 8b05e56..4c13b27 100644 --- a/src/main/java/com/delivery/project/document/service/IVehicleTypeService.java +++ b/src/main/java/com/delivery/project/document/service/IVehicleTypeService.java @@ -67,4 +67,6 @@ public interface IVehicleTypeService * @return */ CalcSuggestFeeVO calcSuggestFee(CalcSuggestFeeDTO dto); + + } diff --git a/src/main/java/com/delivery/project/document/service/impl/DeliveryOrderServiceImpl.java b/src/main/java/com/delivery/project/document/service/impl/DeliveryOrderServiceImpl.java index 700d4b6..dcdd30f 100644 --- a/src/main/java/com/delivery/project/document/service/impl/DeliveryOrderServiceImpl.java +++ b/src/main/java/com/delivery/project/document/service/impl/DeliveryOrderServiceImpl.java @@ -186,17 +186,37 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService throw new ServiceException("物料明细不能为空"); } - String billNo = dto.getItems().get(0).getBillNo(); - boolean fromWms = StringUtils.isNotBlank(billNo); + List items = dto.getItems(); - List rkRecordIds = dto.getItems().stream() + boolean fromWms = items.stream().anyMatch(it -> StringUtils.isNotBlank(it.getBillNo())); + + List rkRecordIds = items.stream() .map(DeliveryOrderLineDTO::getRkRecordId) .filter(Objects::nonNull) .distinct() .collect(Collectors.toList()); - if (fromWms && rkRecordIds.isEmpty()) { - throw new ServiceException("明细行缺少 rk_record 主键 id"); + if (fromWms) { + for (DeliveryOrderLineDTO it : items) { + if (StringUtils.isBlank(it.getBillNo())) { + throw new ServiceException("WMS来源明细缺少 billNo"); + } + if (it.getRkRecordId() == null) { + throw new ServiceException("WMS来源明细缺少 rk_record_id"); + } + } + + if (rkRecordIds.isEmpty()) { + throw new ServiceException("明细行缺少 rk_record 主键 id"); + } + } + + // ===== 新增:防止重复创建配送单 ===== + if (!rkRecordIds.isEmpty()) { + List existIds = deliveryOrderMapper.selectExistsRkRecordIds(rkRecordIds); + if (existIds != null && !existIds.isEmpty()) { + throw new ServiceException("存在已生成配送单的明细,不能重复创建,明细ID:" + existIds); + } } String orderNo = StringUtils.isBlank(dto.getOrderNo()) @@ -208,19 +228,9 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService Long currentUserId = SecurityUtils.getUserId(); Long makerId = dto.getMakerId() != null ? dto.getMakerId() : currentUserId; - List rows = new ArrayList<>(); - - for (DeliveryOrderLineDTO it : dto.getItems()) { - - if (fromWms) { - if (!billNo.equals(it.getBillNo())) { - throw new ServiceException("暂不支持多单合并配送"); - } - if (it.getRkRecordId() == null) { - throw new ServiceException("明细缺少 rk_record_id"); - } - } + List rows = new ArrayList<>(items.size()); + for (DeliveryOrderLineDTO it : items) { DeliveryOrder row = new DeliveryOrder(); row.setOrderNo(orderNo); @@ -244,7 +254,6 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService row.setMakerId(makerId); row.setReceiveStatus(dto.getReceiveStatus()); row.setReceiveProblem(dto.getReceiveProblem()); - row.setOrderStatus(StringUtils.defaultIfBlank(dto.getOrderStatus(), "1")); row.setVehicleTypeId(dto.getVehicleTypeId()); @@ -255,10 +264,8 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService row.setTotalKm(dto.getTotalKm()); row.setRemark(dto.getRemark()); - // 🔥 关键改造点 row.setRkRecordId(it.getRkRecordId()); row.setBillNo(it.getBillNo()); - row.setXmMs(it.getXmMs()); row.setXmNo(it.getXmNo()); row.setWlNo(it.getWlNo()); @@ -276,7 +283,10 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService } if (!rows.isEmpty()) { - deliveryOrderMapper.batchInsert(rows); + int insertRows = deliveryOrderMapper.batchInsert(rows); + if (insertRows <= 0) { + throw new ServiceException("配送单保存失败"); + } } if (fromWms) { diff --git a/src/main/java/com/delivery/project/document/service/impl/VehicleTypeServiceImpl.java b/src/main/java/com/delivery/project/document/service/impl/VehicleTypeServiceImpl.java index b8bb742..2ec2cfc 100644 --- a/src/main/java/com/delivery/project/document/service/impl/VehicleTypeServiceImpl.java +++ b/src/main/java/com/delivery/project/document/service/impl/VehicleTypeServiceImpl.java @@ -1,228 +1,473 @@ package com.delivery.project.document.service.impl; +import com.delivery.common.exception.ServiceException; +import com.delivery.common.utils.StringUtils; +import com.delivery.project.document.domain.VehicleType; +import com.delivery.project.document.domain.dto.CalcSuggestFeeDTO; +import com.delivery.project.document.domain.vo.CalcSuggestFeeVO; +import com.delivery.project.document.mapper.VehicleTypeMapper; +import com.delivery.project.document.service.IVehicleTypeService; +import lombok.Data; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import com.delivery.common.utils.DateUtils; -import com.delivery.project.document.domain.dto.CalcSuggestFeeDTO; -import com.delivery.project.document.domain.vo.CalcSuggestFeeVO; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import com.delivery.project.document.mapper.VehicleTypeMapper; -import com.delivery.project.document.domain.VehicleType; -import com.delivery.project.document.service.IVehicleTypeService; +import java.util.*; +import java.util.stream.Collectors; -/** - * 车型定义Service业务层处理 - * - * @author delivery - * @date 2025-10-23 - */ @Service -public class VehicleTypeServiceImpl implements IVehicleTypeService -{ +public class VehicleTypeServiceImpl implements IVehicleTypeService { + @Autowired private VehicleTypeMapper vehicleTypeMapper; - /** - * 查询车型定义 - * - * @param id 车型定义主键 - * @return 车型定义 - */ @Override - public VehicleType selectVehicleTypeById(Long id) - { + public VehicleType selectVehicleTypeById(Long id) { return vehicleTypeMapper.selectVehicleTypeById(id); } - /** - * 查询车型定义列表 - * - * @param vehicleType 车型定义 - * @return 车型定义 - */ @Override - public List selectVehicleTypeList(VehicleType vehicleType) - { + public List selectVehicleTypeList(VehicleType vehicleType) { return vehicleTypeMapper.selectVehicleTypeList(vehicleType); } - /** - * 新增车型定义 - * - * @param vehicleType 车型定义 - * @return 结果 - */ @Override - public int insertVehicleType(VehicleType vehicleType) - { - vehicleType.setCreateTime(DateUtils.getNowDate()); + public int insertVehicleType(VehicleType vehicleType) { return vehicleTypeMapper.insertVehicleType(vehicleType); } - /** - * 修改车型定义 - * - * @param vehicleType 车型定义 - * @return 结果 - */ @Override - public int updateVehicleType(VehicleType vehicleType) - { - vehicleType.setUpdateTime(DateUtils.getNowDate()); + public int updateVehicleType(VehicleType vehicleType) { return vehicleTypeMapper.updateVehicleType(vehicleType); } - /** - * 批量删除车型定义 - * - * @param ids 需要删除的车型定义主键 - * @return 结果 - */ @Override - public int deleteVehicleTypeByIds(Long[] ids) - { + public int deleteVehicleTypeByIds(Long[] ids) { return vehicleTypeMapper.deleteVehicleTypeByIds(ids); } - /** - * 删除车型定义信息 - * - * @param id 车型定义主键 - * @return 结果 - */ @Override - public int deleteVehicleTypeById(Long id) - { + public int deleteVehicleTypeById(Long id) { return vehicleTypeMapper.deleteVehicleTypeById(id); } /** - * 计算建议运费,推荐车型 - * @param dto - * @return + * 计算建议费用 */ @Override - public CalcSuggestFeeVO calcSuggestFee(CalcSuggestFeeDTO dto) - { - // ========== 0) 入参兜底 ========== - BigDecimal w = nvl(dto.getWeightTon()); // 货物重量(吨) - BigDecimal v = nvl(dto.getVolumeM3()); // 货物体积(立方米) - BigDecimal km = nvl(dto.getDistanceKm()); // 行程公里数(公里) + public CalcSuggestFeeVO calcSuggestFee(CalcSuggestFeeDTO dto) { - // ========== 1) 候选车型:严格匹配,若无则兜底匹配 ========== - List candidates = vehicleTypeMapper.selectMatchTypes(w, v); - if (candidates == null || candidates.isEmpty()) { - candidates = vehicleTypeMapper.selectFallbackTypes(w, v); + BigDecimal weight = nvl(dto.getWeightTon()); + BigDecimal volume = nvl(dto.getVolumeM3()); + BigDecimal km = nvl(dto.getDistanceKm()); + + if (km.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("公里数必须大于0"); } - // ========== 2) 检查是否存在适配车型 ========== - if (candidates == null || candidates.isEmpty()) { - // 如果没有适配车型,检查系统中是否至少存在一个车型配置 - List allVehicleTypes = vehicleTypeMapper.selectVehicleTypeList(new VehicleType()); - if (allVehicleTypes == null || allVehicleTypes.isEmpty()) { - CalcSuggestFeeVO vo = new CalcSuggestFeeVO(); - vo.setErrorMessage("系统中暂无车型配置,请先配置车型信息。"); - vo.setHasSuitableType(false); - return vo; + List vehicleTypes = vehicleTypeMapper.selectUsableTypes(); + + if (vehicleTypes == null || vehicleTypes.isEmpty()) { + throw new ServiceException("没有配置车型"); + } + + // 构建算法节点 + List nodes = vehicleTypes.stream() + .map(v -> buildNode(v, km)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + // 按费用排序 + nodes.sort(Comparator.comparing(VehicleNode::getSingleFee)); + + SearchContext ctx = new SearchContext(weight, volume); + + dfs(nodes, + 0, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + 0, + new ArrayList<>(), + ctx); + CalcSuggestFeeVO calcSuggestFeeVO = buildResult(ctx,dto); + + return calcSuggestFeeVO; + } + + /** + * DFS搜索最佳组合(最优费用 + 同价最少车) + */ + private void dfs(List nodes, + int index, + BigDecimal weight, + BigDecimal volume, + BigDecimal fee, + int vehicleCount, + List plan, + SearchContext ctx) { + + // 1. 剪枝:当前费用已经更差 + if (ctx.bestFee != null && fee.compareTo(ctx.bestFee) > 0) { + return; + } + + // 2. 满足需求 + if (weight.compareTo(ctx.targetWeight) >= 0 && + volume.compareTo(ctx.targetVolume) >= 0) { + + if (ctx.bestFee == null + || fee.compareTo(ctx.bestFee) < 0 + || (fee.compareTo(ctx.bestFee) == 0 + && vehicleCount < ctx.bestVehicleCount)) { + + ctx.bestFee = fee; + ctx.bestVehicleCount = vehicleCount; + ctx.bestPlan = new ArrayList<>(plan); + } + return; + } + + // 3. 递归结束 + if (index >= nodes.size()) { + return; + } + + VehicleNode node = nodes.get(index); + + int max = calculateMaxCount(node, ctx.targetWeight, ctx.targetVolume); + + // 4. 从多到少尝试 + for (int i = max; i >= 0; i--) { + + BigDecimal addWeight = node.weightMaxTon.multiply(BigDecimal.valueOf(i)); + BigDecimal addVolume = node.volumeMaxM3.multiply(BigDecimal.valueOf(i)); + BigDecimal addFee = node.singleFee.multiply(BigDecimal.valueOf(i)); + + List newPlan = new ArrayList<>(plan); + + if (i > 0) { + VehiclePlan p = new VehiclePlan(); + p.vehicleTypeId = node.id; + p.vehicleTypeName = node.typeName; + p.count = i; + p.singleFee = node.singleFee; + p.totalFee = addFee; + p.weightMaxTon = node.weightMaxTon; + p.volumeMaxM3 = node.volumeMaxM3; + newPlan.add(p); } - // 找到最大承载能力的车型 - VehicleType maxCapacityType = allVehicleTypes.stream() - .filter(type -> "0".equals(type.getStatus()) && !"1".equals(type.getIsDelete())) - .max(Comparator - .comparing(VehicleType::getWeightMaxTon, Comparator.nullsLast(BigDecimal::compareTo)) - .thenComparing(VehicleType::getVolumeMaxM3, Comparator.nullsLast(BigDecimal::compareTo))) - .orElse(null); + dfs(nodes, + index + 1, + weight.add(addWeight), + volume.add(addVolume), + fee.add(addFee), + vehicleCount + i, + newPlan, + ctx); + } + } - if (maxCapacityType != null) { - // 检查最大容量车型是否仍无法满足要求 - String errorMsg = buildNoMatchErrorMessage(w, v, maxCapacityType); - CalcSuggestFeeVO vo = new CalcSuggestFeeVO(); - vo.setErrorMessage(errorMsg); - vo.setHasSuitableType(false); - // 仍然返回候选车型列表供参考 - List list = new ArrayList<>(allVehicleTypes.size()); + /** + * 计算最多车辆数 + */ + private int calculateMaxCount(VehicleNode node, + BigDecimal weight, + BigDecimal volume) { + + int w = 0; + int v = 0; + + if (node.weightMaxTon.compareTo(BigDecimal.ZERO) > 0) { + w = weight.divide(node.weightMaxTon, 0, RoundingMode.CEILING).intValue(); + } + + if (node.volumeMaxM3.compareTo(BigDecimal.ZERO) > 0) { + v = volume.divide(node.volumeMaxM3, 0, RoundingMode.CEILING).intValue(); + } + + return Math.max(w, v) + 2; + } + + /** + * 构建节点 + */ + private VehicleNode buildNode(VehicleType v, BigDecimal km) { + + if (!"0".equals(v.getStatus()) || "1".equals(v.getIsDelete())) { + return null; + } + + if (v.getUnitPricePerKm() == null) { + return null; + } + + VehicleNode node = new VehicleNode(); + + node.id = v.getId(); + node.typeName = v.getTypeName(); + node.unitPricePerKm = v.getUnitPricePerKm(); + node.singleFee = v.getUnitPricePerKm() + .multiply(km) + .setScale(2, RoundingMode.HALF_UP); + + node.weightMaxTon = nvl(v.getWeightMaxTon()); + node.volumeMaxM3 = nvl(v.getVolumeMaxM3()); + + return node; + } + + /** + * 构建返回结果 + */ + private CalcSuggestFeeVO buildResult(SearchContext ctx,CalcSuggestFeeDTO dto) { + + CalcSuggestFeeVO vo = new CalcSuggestFeeVO(); + + if (ctx.bestPlan == null) { + vo.setHasSuitableType(false); + return vo; + } + + vo.setHasSuitableType(true); + vo.setSuggestFee(ctx.bestFee); + vo.setTotalVehicleCount(ctx.bestVehicleCount); + + List bestPlanList = + ctx.bestPlan.stream() + .map(p -> { + + CalcSuggestFeeVO.VehiclePlanVO item = + new CalcSuggestFeeVO.VehiclePlanVO(); + + item.setVehicleTypeId(String.valueOf(p.vehicleTypeId)); + item.setVehicleTypeName(p.vehicleTypeName); + item.setVehicleCount(p.count); + item.setSingleVehicleFee(p.singleFee); + item.setTotalFee(p.totalFee); + + return item; + + }).collect(Collectors.toList()); + + vo.setBestPlan(bestPlanList); + // ========== 1) 候选车型:严格匹配,若无则兜底匹配 ========== + // 候选车型:同时满足“体积 >= 推荐最大体积” 且 “承重 >= 推荐最大承重”;否则返回全部启用车型 + // 2. 查询全部车型 + List allVehicleTypes = vehicleTypeMapper.selectVehicleTypeList(new VehicleType()); + if (bestPlanList!=null&&bestPlanList.size()==1) { + // 1. 取推荐方案中的最大体积、最大承重 + BigDecimal recommendMaxVolume = ctx.bestPlan.stream() + .map(p -> p.volumeMaxM3) + .filter(Objects::nonNull) + .max(BigDecimal::compareTo) + .orElse(BigDecimal.ZERO); + + BigDecimal recommendMaxWeight = ctx.bestPlan.stream() + .map(p -> p.weightMaxTon) + .filter(Objects::nonNull) + .max(BigDecimal::compareTo) + .orElse(BigDecimal.ZERO); + + + + // 3. 过滤启用且未删除的车型 + List enabledTypes = new ArrayList<>(); + if (allVehicleTypes != null) { for (VehicleType t : allVehicleTypes) { if ("0".equals(t.getStatus()) && !"1".equals(t.getIsDelete())) { - CalcSuggestFeeVO.VehicleTypeOptionVO o = new CalcSuggestFeeVO.VehicleTypeOptionVO(); - o.setId(t.getId()); - o.setName(t.getTypeName()); - o.setUnitPricePerKm(t.getUnitPricePerKm()); - o.setWeightMinTon(t.getWeightMinTon()); - o.setWeightMaxTon(t.getWeightMaxTon()); - o.setVolumeMinM3(t.getVolumeMinM3()); - o.setVolumeMaxM3(t.getVolumeMaxM3()); - list.add(o); + enabledTypes.add(t); } } - vo.setCandidates(list); - return vo; - } else { - CalcSuggestFeeVO vo = new CalcSuggestFeeVO(); - vo.setErrorMessage("未找到适配车型,请检查车型配置或输入参数。"); - vo.setHasSuitableType(false); - return vo; } - } - // 稳定排序:单价升序 -> 承重上限升序 -> 载方上限升序 -> id 升序(数据库已排序,这里再兜一层) - candidates.sort(Comparator - .comparing(VehicleType::getUnitPricePerKm, Comparator.nullsLast(BigDecimal::compareTo)) - .thenComparing(VehicleType::getWeightMaxTon, Comparator.nullsLast(BigDecimal::compareTo)) - .thenComparing(VehicleType::getVolumeMaxM3, Comparator.nullsLast(BigDecimal::compareTo)) - .thenComparing(VehicleType::getId, Comparator.nullsLast(Long::compareTo))); + // 4. 筛选满足条件的车型 + List candidates = new ArrayList<>(); + for (VehicleType t : enabledTypes) { + if (t.getWeightMaxTon() != null + && t.getVolumeMaxM3() != null + && t.getWeightMaxTon().compareTo(recommendMaxWeight) >= 0 + && t.getVolumeMaxM3().compareTo(recommendMaxVolume) >= 0) { + candidates.add(t); + } + } + + // 5. 如果没有符合条件的,返回全部启用车型 + if (candidates.isEmpty()) { + candidates = enabledTypes; + } + List list = new ArrayList<>(); + for (VehicleType t : candidates) { + CalcSuggestFeeVO.VehicleTypeOptionVO o = new CalcSuggestFeeVO.VehicleTypeOptionVO(); + o.setId(String.valueOf(t.getId())); + o.setName(t.getTypeName()); + o.setUnitPricePerKm(t.getUnitPricePerKm()); + o.setWeightMinTon(t.getWeightMinTon()); + o.setWeightMaxTon(t.getWeightMaxTon()); + o.setVolumeMinM3(t.getVolumeMinM3()); + o.setVolumeMaxM3(t.getVolumeMaxM3()); + list.add(o); + } + vo.setCandidates(list); + }else { + List list = new ArrayList<>(); + for (VehicleType t : allVehicleTypes) { + CalcSuggestFeeVO.VehicleTypeOptionVO o = new CalcSuggestFeeVO.VehicleTypeOptionVO(); + o.setId(String.valueOf(t.getId())); + o.setName(t.getTypeName()); + o.setUnitPricePerKm(t.getUnitPricePerKm()); + o.setWeightMinTon(t.getWeightMinTon()); + o.setWeightMaxTon(t.getWeightMaxTon()); + o.setVolumeMinM3(t.getVolumeMinM3()); + o.setVolumeMaxM3(t.getVolumeMaxM3()); + list.add(o); + } + vo.setCandidates(list); + } // ========== 3) 选定车型:前端指定 or 系统推荐 ========== - VehicleType chosen; - if (dto.getVehicleTypeId() != null) { - // 前端点名车型:优先在候选中找,找不到则直接按主键查 - chosen = candidates.stream() - .filter(t -> dto.getVehicleTypeId().equals(t.getId())) - .findFirst() - .orElseGet(() -> vehicleTypeMapper.selectVehicleTypeById(dto.getVehicleTypeId())); - if (chosen == null) { - throw new RuntimeException("指定车型不存在或已被禁用:id=" + dto.getVehicleTypeId()); + List chosenList; + + if (StringUtils.isNotBlank(dto.getVehicleTypeId())) { + + List idList = Arrays.stream(dto.getVehicleTypeId().split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Long::valueOf) + .collect(Collectors.toList()); + + chosenList = allVehicleTypes.stream() + .filter(t -> idList.contains(t.getId())) + .collect(Collectors.toList()); + + // ========== 先判断:前端指定车型组合能否装下 ========== + BigDecimal totalWeightCapacity = BigDecimal.ZERO; + BigDecimal totalVolumeCapacity = BigDecimal.ZERO; + + for (VehicleType t : chosenList) { + totalWeightCapacity = totalWeightCapacity.add( + t.getWeightMaxTon() == null ? BigDecimal.ZERO : t.getWeightMaxTon() + ); + totalVolumeCapacity = totalVolumeCapacity.add( + t.getVolumeMaxM3() == null ? BigDecimal.ZERO : t.getVolumeMaxM3() + ); } - } else { - chosen = candidates.get(0); + + boolean weightEnough = totalWeightCapacity.compareTo(dto.getWeightTon()) >= 0; + boolean volumeEnough = totalVolumeCapacity.compareTo(dto.getVolumeM3()) >= 0; + + if (!weightEnough || !volumeEnough) { + StringBuilder sb = new StringBuilder("所选车型可能无法装下当前货物"); + sb.append(",总承重=").append(totalWeightCapacity).append("吨"); + sb.append(",总载方=").append(totalVolumeCapacity).append("方"); + sb.append(",货物重量=").append(dto.getWeightTon()).append("吨"); + sb.append(",货物体积=").append(dto.getVolumeM3()).append("方"); + + vo.setWarningMessage(sb.toString()); + } + + // ========== 4) 多车型计算建议费用 ========== + BigDecimal totalFee = BigDecimal.ZERO; + + for (VehicleType t : chosenList) { + + if (t.getUnitPricePerKm() == null) { + throw new ServiceException("车型【" + t.getTypeName() + "】未配置单价"); + } + + // 单车费用 + BigDecimal singleFee = t.getUnitPricePerKm() + .multiply(dto.getDistanceKm()) + .setScale(2, RoundingMode.HALF_UP); + + // 这里默认每个指定车型 1 辆 + int count = 1; + + BigDecimal subTotal = singleFee.multiply(BigDecimal.valueOf(count)); + totalFee = totalFee.add(subTotal); + } + + vo.setSuggestFee(totalFee); } - // ========== 4) 计算建议费用:单价 × 公里数,保留2位 ========== - BigDecimal price = chosen.getUnitPricePerKm(); - if (price == null) { - throw new RuntimeException("车型【" + chosen.getTypeName() + "】未配置每公里单价(unit_price_per_km 为空)"); - } - BigDecimal fee = price.multiply(km).setScale(2, RoundingMode.HALF_UP); - // ========== 5) 组装返回 ========== - CalcSuggestFeeVO vo = new CalcSuggestFeeVO(); - vo.setVehicleTypeId(chosen.getId()); - vo.setVehicleTypeName(chosen.getTypeName()); - vo.setUnitPricePerKm(price); - vo.setSuggestFee(fee); - vo.setHasSuitableType(true); // 有适配车型 - // 候选项 - List list = new ArrayList<>(candidates.size()); - for (VehicleType t : candidates) { - CalcSuggestFeeVO.VehicleTypeOptionVO o = new CalcSuggestFeeVO.VehicleTypeOptionVO(); - o.setId(t.getId()); - o.setName(t.getTypeName()); - o.setUnitPricePerKm(t.getUnitPricePerKm()); - o.setWeightMinTon(t.getWeightMinTon()); - o.setWeightMaxTon(t.getWeightMaxTon()); - o.setVolumeMinM3(t.getVolumeMinM3()); - o.setVolumeMaxM3(t.getVolumeMaxM3()); - list.add(o); - } - vo.setCandidates(list); +// // ========== 5) 组装返回 ========== +// CalcSuggestFeeVO vo = new CalcSuggestFeeVO(); +// vo.setVehicleTypeId(chosen.getId()); +// vo.setVehicleTypeName(chosen.getTypeName()); +// vo.setUnitPricePerKm(price); +// vo.setHasSuitableType(true); // 有适配车型 return vo; } + /** + * 空值转0 + */ + private BigDecimal nvl(BigDecimal v) { + return v == null ? BigDecimal.ZERO : v; + } + + /** + * 算法节点 + */ + @Data + private static class VehicleNode { + + Long id; + + String typeName; + + BigDecimal unitPricePerKm; + + BigDecimal singleFee; + + BigDecimal weightMaxTon; + + BigDecimal volumeMaxM3; + } + + /** + * 组合方案 + */ + private static class VehiclePlan { + + + Long vehicleTypeId; + + String vehicleTypeName; + + int count; + + BigDecimal singleFee; + + BigDecimal totalFee; + BigDecimal weightMaxTon; + BigDecimal volumeMaxM3; + } + + /** + * 搜索上下文 + */ + private static class SearchContext { + + BigDecimal targetWeight; + + BigDecimal targetVolume; + + BigDecimal bestFee; + + int bestVehicleCount; + + List bestPlan; + + SearchContext(BigDecimal w, BigDecimal v) { + targetWeight = w; + targetVolume = v; + } + } /** * 构建无匹配车型的错误信息 */ @@ -235,23 +480,18 @@ public class VehicleTypeServiceImpl implements IVehicleTypeService if (weightExceeds && volumeExceeds) { sb.append("货物重量(").append(weight).append("吨)超过最大承重(") - .append(maxCapacityType.getWeightMaxTon()).append("吨),且货物体积(") - .append(volume).append("立方米)超过最大载方(") - .append(maxCapacityType.getVolumeMaxM3()).append("立方米)。"); + .append(maxCapacityType.getWeightMaxTon()).append("吨),且货物体积(") + .append(volume).append("立方米)超过最大载方(") + .append(maxCapacityType.getVolumeMaxM3()).append("立方米)。"); } else if (weightExceeds) { sb.append("货物重量(").append(weight).append("吨)超过最大承重(") - .append(maxCapacityType.getWeightMaxTon()).append("吨)。"); + .append(maxCapacityType.getWeightMaxTon()).append("吨)。"); } else if (volumeExceeds) { sb.append("货物体积(").append(volume).append("立方米)超过最大载方(") - .append(maxCapacityType.getVolumeMaxM3()).append("立方米)。"); + .append(maxCapacityType.getVolumeMaxM3()).append("立方米)。"); } sb.append("最大承载车型为:").append(maxCapacityType.getTypeName()); return sb.toString(); } - - private static BigDecimal nvl(BigDecimal x) { - return x == null ? BigDecimal.ZERO : x; - } - -} +} \ No newline at end of file diff --git a/src/main/resources/mybatis/document/DeliveryOrderMapper.xml b/src/main/resources/mybatis/document/DeliveryOrderMapper.xml index d8a95f8..b93ee29 100644 --- a/src/main/resources/mybatis/document/DeliveryOrderMapper.xml +++ b/src/main/resources/mybatis/document/DeliveryOrderMapper.xml @@ -553,7 +553,7 @@ - AND dor.vehicle_type_id = #{vehicleTypeId} + AND FIND_IN_SET(#{vehicleTypeId}, dor.vehicle_type_id) AND dor.vehicle_type_name LIKE CONCAT('%', #{vehicleTypeName}, '%') @@ -945,6 +945,7 @@ + AND FIND_IN_SET(#{q.vehicleTypeId}, dor.vehicle_type_id) AND dor.vehicle_type_id = #{q.vehicleTypeId} @@ -1054,4 +1055,16 @@ GROUP BY DATE_FORMAT(dor.delivery_date, '%Y-%m') ORDER BY statMonth ASC + + diff --git a/src/main/resources/mybatis/document/VehicleTypeMapper.xml b/src/main/resources/mybatis/document/VehicleTypeMapper.xml index 12778fd..91a5d3c 100644 --- a/src/main/resources/mybatis/document/VehicleTypeMapper.xml +++ b/src/main/resources/mybatis/document/VehicleTypeMapper.xml @@ -140,5 +140,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and volume_max_m3 >= #{v} order by unit_price_per_km asc, weight_max_ton asc, volume_max_m3 asc, id asc - + \ No newline at end of file