配送页面字段内容调整

This commit is contained in:
2026-03-19 15:37:39 +08:00
parent d322fe830e
commit 8f1ab4e9c6
13 changed files with 548 additions and 237 deletions

View File

@@ -102,7 +102,7 @@ public class VehicleTypeController extends BaseController
*/ */
@PreAuthorize("@ss.hasPermi('document:type:remove')") @PreAuthorize("@ss.hasPermi('document:type:remove')")
@Log(title = "车型定义", businessType = BusinessType.DELETE) @Log(title = "车型定义", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}") @DeleteMapping("/{ids:\\d+}")
public AjaxResult remove(@PathVariable Long[] ids) public AjaxResult remove(@PathVariable Long[] ids)
{ {
return toAjax(vehicleTypeService.deleteVehicleTypeByIds(ids)); return toAjax(vehicleTypeService.deleteVehicleTypeByIds(ids));

View File

@@ -152,7 +152,7 @@ public class DeliveryOrder extends BaseEntity {
private String orderStatus; private String orderStatus;
/** 车辆类型ID */ /** 车辆类型ID */
private Long vehicleTypeId; private String vehicleTypeId;
/** 车辆类型名称 */ /** 车辆类型名称 */
@Excel(name = "车辆类型名称") @Excel(name = "车辆类型名称")
@@ -312,8 +312,8 @@ public class DeliveryOrder extends BaseEntity {
public String getOrderStatus() { return orderStatus; } public String getOrderStatus() { return orderStatus; }
public void setOrderStatus(String orderStatus) { this.orderStatus = orderStatus; } public void setOrderStatus(String orderStatus) { this.orderStatus = orderStatus; }
public Long getVehicleTypeId() { return vehicleTypeId; } public String getVehicleTypeId() { return vehicleTypeId; }
public void setVehicleTypeId(Long vehicleTypeId) { this.vehicleTypeId = vehicleTypeId; } public void setVehicleTypeId(String vehicleTypeId) { this.vehicleTypeId = vehicleTypeId; }
public String getVehicleTypeName() { return vehicleTypeName; } public String getVehicleTypeName() { return vehicleTypeName; }
public void setVehicleTypeName(String vehicleTypeName) { this.vehicleTypeName = vehicleTypeName; } public void setVehicleTypeName(String vehicleTypeName) { this.vehicleTypeName = vehicleTypeName; }

View File

@@ -26,5 +26,6 @@ public class CalcSuggestFeeDTO {
private BigDecimal distanceKm; private BigDecimal distanceKm;
/** 指定车型ID可选传入则按该车型计算建议费用 */ /** 指定车型ID可选传入则按该车型计算建议费用 */
private Long vehicleTypeId; private String vehicleTypeId;
} }

View File

@@ -83,7 +83,7 @@ public class DeliveryOrderCreateDTO {
private String orderStatus; private String orderStatus;
/** 车型 ID */ /** 车型 ID */
private Long vehicleTypeId; private String vehicleTypeId;
/** 车型名称 */ /** 车型名称 */
private String vehicleTypeName; private String vehicleTypeName;

View File

@@ -1,4 +1,3 @@
// src/main/java/com/delivery/project/document/domain/vo/CalcSuggestFeeVO.java
package com.delivery.project.document.domain.vo; package com.delivery.project.document.domain.vo;
import lombok.Data; import lombok.Data;
@@ -6,58 +5,93 @@ import java.math.BigDecimal;
import java.util.List; import java.util.List;
/** /**
* 计算建议费用 - 响应体 VO * 计算建议费用 返回VO
* 返回推荐/选定的车型、单价、建议费用,以及候选车型列表。
*/ */
@Data @Data
public class CalcSuggestFeeVO { public class CalcSuggestFeeVO {
/** 选定/推荐车型ID */ /** 是否存在可用车型 */
private Long vehicleTypeId;
/** 选定/推荐车型名称 */
private String vehicleTypeName;
/** 每公里单价(单位:元/公里) */
private BigDecimal unitPricePerKm;
/** 建议费用单位四舍五入保留2位小数 */
private BigDecimal suggestFee;
/** 错误信息 */
private String errorMessage;
/** 是否有适配车型 */
private Boolean hasSuitableType; private Boolean hasSuitableType;
/** 候选车型列表(按价格/容量排序 */ /** 错误信息(无车型时返回 */
private String errorMessage;
/** 推荐总费用 */
private BigDecimal suggestFee;
/** 总车辆数 */
private Integer totalVehicleCount;
/** 货物重量 */
private BigDecimal totalWeightTon;
/** 货物体积 */
private BigDecimal totalVolumeM3;
/** 推荐车辆组合方案 */
private List<VehiclePlanVO> bestPlan;
/** 候选车型列表 */
private List<VehicleTypeOptionVO> candidates; private List<VehicleTypeOptionVO> 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 @Data
public static class VehicleTypeOptionVO { public static class VehicleTypeOptionVO {
/** 车型ID */ /** 车型ID */
private Long id; private String id;
/** 车型名称 */ /** 车型名称 */
private String name; private String name;
/** 每公里单价(单位:元/公里) */ /** 每公里单价 */
private BigDecimal unitPricePerKm; private BigDecimal unitPricePerKm;
/** 承重下限(单位:吨,含) */ /** 最小承重 */
private BigDecimal weightMinTon; private BigDecimal weightMinTon;
/** 承重上限(单位:吨,含) */ /** 最大承重 */
private BigDecimal weightMaxTon; private BigDecimal weightMaxTon;
/** 载方下限(单位:立方米,含) */ /** 最小体积 */
private BigDecimal volumeMinM3; private BigDecimal volumeMinM3;
/** 载方上限(单位:吨,含) */ /** 最大体积 */
private BigDecimal volumeMaxM3; private BigDecimal volumeMaxM3;
} }
} }

View File

@@ -86,7 +86,7 @@ public class DeliveryOrderVo {
private String orderStatus; private String orderStatus;
/** 车型 ID */ /** 车型 ID */
private Long vehicleTypeId; private String vehicleTypeId;
/** 车型名称 */ /** 车型名称 */
private String vehicleTypeName; private String vehicleTypeName;

View File

@@ -113,4 +113,6 @@ public interface DeliveryOrderMapper
/** 配送单VO列表 */ /** 配送单VO列表 */
List<DeliveryOrderVo> selectDeliveryOrderVoList(DeliveryOrder deliveryOrder); List<DeliveryOrderVo> selectDeliveryOrderVoList(DeliveryOrder deliveryOrder);
/** 防止重复创建配送单 */
List<Long> selectExistsRkRecordIds(@Param("rkRecordIds") List<Long> rkRecordIds);
} }

View File

@@ -77,4 +77,8 @@ public interface VehicleTypeMapper
* @return 批量结果 * @return 批量结果
*/ */
List<VehicleType> selectFallbackTypes(BigDecimal w, BigDecimal v); List<VehicleType> selectFallbackTypes(BigDecimal w, BigDecimal v);
/**
* 1 查询所有可用车型
* */
List<VehicleType> selectUsableTypes();
} }

View File

@@ -67,4 +67,6 @@ public interface IVehicleTypeService
* @return * @return
*/ */
CalcSuggestFeeVO calcSuggestFee(CalcSuggestFeeDTO dto); CalcSuggestFeeVO calcSuggestFee(CalcSuggestFeeDTO dto);
} }

View File

@@ -186,17 +186,37 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
throw new ServiceException("物料明细不能为空"); throw new ServiceException("物料明细不能为空");
} }
String billNo = dto.getItems().get(0).getBillNo(); List<DeliveryOrderLineDTO> items = dto.getItems();
boolean fromWms = StringUtils.isNotBlank(billNo);
List<Long> rkRecordIds = dto.getItems().stream() boolean fromWms = items.stream().anyMatch(it -> StringUtils.isNotBlank(it.getBillNo()));
List<Long> rkRecordIds = items.stream()
.map(DeliveryOrderLineDTO::getRkRecordId) .map(DeliveryOrderLineDTO::getRkRecordId)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.distinct() .distinct()
.collect(Collectors.toList()); .collect(Collectors.toList());
if (fromWms && rkRecordIds.isEmpty()) { if (fromWms) {
throw new ServiceException("明细行缺少 rk_record 主键 id"); 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<Long> existIds = deliveryOrderMapper.selectExistsRkRecordIds(rkRecordIds);
if (existIds != null && !existIds.isEmpty()) {
throw new ServiceException("存在已生成配送单的明细不能重复创建明细ID" + existIds);
}
} }
String orderNo = StringUtils.isBlank(dto.getOrderNo()) String orderNo = StringUtils.isBlank(dto.getOrderNo())
@@ -208,19 +228,9 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
Long currentUserId = SecurityUtils.getUserId(); Long currentUserId = SecurityUtils.getUserId();
Long makerId = dto.getMakerId() != null ? dto.getMakerId() : currentUserId; Long makerId = dto.getMakerId() != null ? dto.getMakerId() : currentUserId;
List<DeliveryOrder> rows = new ArrayList<>(); List<DeliveryOrder> rows = new ArrayList<>(items.size());
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");
}
}
for (DeliveryOrderLineDTO it : items) {
DeliveryOrder row = new DeliveryOrder(); DeliveryOrder row = new DeliveryOrder();
row.setOrderNo(orderNo); row.setOrderNo(orderNo);
@@ -244,7 +254,6 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
row.setMakerId(makerId); row.setMakerId(makerId);
row.setReceiveStatus(dto.getReceiveStatus()); row.setReceiveStatus(dto.getReceiveStatus());
row.setReceiveProblem(dto.getReceiveProblem()); row.setReceiveProblem(dto.getReceiveProblem());
row.setOrderStatus(StringUtils.defaultIfBlank(dto.getOrderStatus(), "1")); row.setOrderStatus(StringUtils.defaultIfBlank(dto.getOrderStatus(), "1"));
row.setVehicleTypeId(dto.getVehicleTypeId()); row.setVehicleTypeId(dto.getVehicleTypeId());
@@ -255,10 +264,8 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
row.setTotalKm(dto.getTotalKm()); row.setTotalKm(dto.getTotalKm());
row.setRemark(dto.getRemark()); row.setRemark(dto.getRemark());
// 🔥 关键改造点
row.setRkRecordId(it.getRkRecordId()); row.setRkRecordId(it.getRkRecordId());
row.setBillNo(it.getBillNo()); row.setBillNo(it.getBillNo());
row.setXmMs(it.getXmMs()); row.setXmMs(it.getXmMs());
row.setXmNo(it.getXmNo()); row.setXmNo(it.getXmNo());
row.setWlNo(it.getWlNo()); row.setWlNo(it.getWlNo());
@@ -276,7 +283,10 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
} }
if (!rows.isEmpty()) { if (!rows.isEmpty()) {
deliveryOrderMapper.batchInsert(rows); int insertRows = deliveryOrderMapper.batchInsert(rows);
if (insertRows <= 0) {
throw new ServiceException("配送单保存失败");
}
} }
if (fromWms) { if (fromWms) {

View File

@@ -1,228 +1,473 @@
package com.delivery.project.document.service.impl; 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.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.util.ArrayList; import java.util.*;
import java.util.Comparator; import java.util.stream.Collectors;
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;
/**
* 车型定义Service业务层处理
*
* @author delivery
* @date 2025-10-23
*/
@Service @Service
public class VehicleTypeServiceImpl implements IVehicleTypeService public class VehicleTypeServiceImpl implements IVehicleTypeService {
{
@Autowired @Autowired
private VehicleTypeMapper vehicleTypeMapper; private VehicleTypeMapper vehicleTypeMapper;
/**
* 查询车型定义
*
* @param id 车型定义主键
* @return 车型定义
*/
@Override @Override
public VehicleType selectVehicleTypeById(Long id) public VehicleType selectVehicleTypeById(Long id) {
{
return vehicleTypeMapper.selectVehicleTypeById(id); return vehicleTypeMapper.selectVehicleTypeById(id);
} }
/**
* 查询车型定义列表
*
* @param vehicleType 车型定义
* @return 车型定义
*/
@Override @Override
public List<VehicleType> selectVehicleTypeList(VehicleType vehicleType) public List<VehicleType> selectVehicleTypeList(VehicleType vehicleType) {
{
return vehicleTypeMapper.selectVehicleTypeList(vehicleType); return vehicleTypeMapper.selectVehicleTypeList(vehicleType);
} }
/**
* 新增车型定义
*
* @param vehicleType 车型定义
* @return 结果
*/
@Override @Override
public int insertVehicleType(VehicleType vehicleType) public int insertVehicleType(VehicleType vehicleType) {
{
vehicleType.setCreateTime(DateUtils.getNowDate());
return vehicleTypeMapper.insertVehicleType(vehicleType); return vehicleTypeMapper.insertVehicleType(vehicleType);
} }
/**
* 修改车型定义
*
* @param vehicleType 车型定义
* @return 结果
*/
@Override @Override
public int updateVehicleType(VehicleType vehicleType) public int updateVehicleType(VehicleType vehicleType) {
{
vehicleType.setUpdateTime(DateUtils.getNowDate());
return vehicleTypeMapper.updateVehicleType(vehicleType); return vehicleTypeMapper.updateVehicleType(vehicleType);
} }
/**
* 批量删除车型定义
*
* @param ids 需要删除的车型定义主键
* @return 结果
*/
@Override @Override
public int deleteVehicleTypeByIds(Long[] ids) public int deleteVehicleTypeByIds(Long[] ids) {
{
return vehicleTypeMapper.deleteVehicleTypeByIds(ids); return vehicleTypeMapper.deleteVehicleTypeByIds(ids);
} }
/**
* 删除车型定义信息
*
* @param id 车型定义主键
* @return 结果
*/
@Override @Override
public int deleteVehicleTypeById(Long id) public int deleteVehicleTypeById(Long id) {
{
return vehicleTypeMapper.deleteVehicleTypeById(id); return vehicleTypeMapper.deleteVehicleTypeById(id);
} }
/** /**
* 计算建议运费,推荐车型 * 计算建议费用
* @param dto
* @return
*/ */
@Override @Override
public CalcSuggestFeeVO calcSuggestFee(CalcSuggestFeeDTO dto) public CalcSuggestFeeVO calcSuggestFee(CalcSuggestFeeDTO dto) {
{
// ========== 0) 入参兜底 ==========
BigDecimal w = nvl(dto.getWeightTon()); // 货物重量(吨)
BigDecimal v = nvl(dto.getVolumeM3()); // 货物体积(立方米)
BigDecimal km = nvl(dto.getDistanceKm()); // 行程公里数(公里)
// ========== 1) 候选车型:严格匹配,若无则兜底匹配 ========== BigDecimal weight = nvl(dto.getWeightTon());
List<VehicleType> candidates = vehicleTypeMapper.selectMatchTypes(w, v); BigDecimal volume = nvl(dto.getVolumeM3());
if (candidates == null || candidates.isEmpty()) { BigDecimal km = nvl(dto.getDistanceKm());
candidates = vehicleTypeMapper.selectFallbackTypes(w, v);
if (km.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("公里数必须大于0");
} }
// ========== 2) 检查是否存在适配车型 ========== List<VehicleType> vehicleTypes = vehicleTypeMapper.selectUsableTypes();
if (candidates == null || candidates.isEmpty()) {
// 如果没有适配车型,检查系统中是否至少存在一个车型配置 if (vehicleTypes == null || vehicleTypes.isEmpty()) {
List<VehicleType> allVehicleTypes = vehicleTypeMapper.selectVehicleTypeList(new VehicleType()); throw new ServiceException("没有配置车型");
if (allVehicleTypes == null || allVehicleTypes.isEmpty()) { }
CalcSuggestFeeVO vo = new CalcSuggestFeeVO();
vo.setErrorMessage("系统中暂无车型配置,请先配置车型信息。"); // 构建算法节点
vo.setHasSuitableType(false); List<VehicleNode> nodes = vehicleTypes.stream()
return vo; .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<VehicleNode> nodes,
int index,
BigDecimal weight,
BigDecimal volume,
BigDecimal fee,
int vehicleCount,
List<VehiclePlan> 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<VehiclePlan> 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);
} }
// 找到最大承载能力的车型 dfs(nodes,
VehicleType maxCapacityType = allVehicleTypes.stream() index + 1,
.filter(type -> "0".equals(type.getStatus()) && !"1".equals(type.getIsDelete())) weight.add(addWeight),
.max(Comparator volume.add(addVolume),
.comparing(VehicleType::getWeightMaxTon, Comparator.nullsLast(BigDecimal::compareTo)) fee.add(addFee),
.thenComparing(VehicleType::getVolumeMaxM3, Comparator.nullsLast(BigDecimal::compareTo))) vehicleCount + i,
.orElse(null); newPlan,
ctx);
}
}
if (maxCapacityType != null) { /**
// 检查最大容量车型是否仍无法满足要求 * 计算最多车辆数
String errorMsg = buildNoMatchErrorMessage(w, v, maxCapacityType); */
CalcSuggestFeeVO vo = new CalcSuggestFeeVO(); private int calculateMaxCount(VehicleNode node,
vo.setErrorMessage(errorMsg); BigDecimal weight,
vo.setHasSuitableType(false); BigDecimal volume) {
// 仍然返回候选车型列表供参考
List<CalcSuggestFeeVO.VehicleTypeOptionVO> list = new ArrayList<>(allVehicleTypes.size()); 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<CalcSuggestFeeVO.VehiclePlanVO> 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<VehicleType> 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<VehicleType> enabledTypes = new ArrayList<>();
if (allVehicleTypes != null) {
for (VehicleType t : allVehicleTypes) { for (VehicleType t : allVehicleTypes) {
if ("0".equals(t.getStatus()) && !"1".equals(t.getIsDelete())) { if ("0".equals(t.getStatus()) && !"1".equals(t.getIsDelete())) {
CalcSuggestFeeVO.VehicleTypeOptionVO o = new CalcSuggestFeeVO.VehicleTypeOptionVO(); enabledTypes.add(t);
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);
return vo;
} else {
CalcSuggestFeeVO vo = new CalcSuggestFeeVO();
vo.setErrorMessage("未找到适配车型,请检查车型配置或输入参数。");
vo.setHasSuitableType(false);
return vo;
} }
}
// 稳定排序:单价升序 -> 承重上限升序 -> 载方上限升序 -> id 升序(数据库已排序,这里再兜一层) // 4. 筛选满足条件的车型
candidates.sort(Comparator List<VehicleType> candidates = new ArrayList<>();
.comparing(VehicleType::getUnitPricePerKm, Comparator.nullsLast(BigDecimal::compareTo)) for (VehicleType t : enabledTypes) {
.thenComparing(VehicleType::getWeightMaxTon, Comparator.nullsLast(BigDecimal::compareTo)) if (t.getWeightMaxTon() != null
.thenComparing(VehicleType::getVolumeMaxM3, Comparator.nullsLast(BigDecimal::compareTo)) && t.getVolumeMaxM3() != null
.thenComparing(VehicleType::getId, Comparator.nullsLast(Long::compareTo))); && t.getWeightMaxTon().compareTo(recommendMaxWeight) >= 0
&& t.getVolumeMaxM3().compareTo(recommendMaxVolume) >= 0) {
candidates.add(t);
}
}
// 5. 如果没有符合条件的,返回全部启用车型
if (candidates.isEmpty()) {
candidates = enabledTypes;
}
List<CalcSuggestFeeVO.VehicleTypeOptionVO> 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<CalcSuggestFeeVO.VehicleTypeOptionVO> 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 系统推荐 ========== // ========== 3) 选定车型:前端指定 or 系统推荐 ==========
VehicleType chosen; List<VehicleType> chosenList;
if (dto.getVehicleTypeId() != null) {
// 前端点名车型:优先在候选中找,找不到则直接按主键查 if (StringUtils.isNotBlank(dto.getVehicleTypeId())) {
chosen = candidates.stream()
.filter(t -> dto.getVehicleTypeId().equals(t.getId())) List<Long> idList = Arrays.stream(dto.getVehicleTypeId().split(","))
.findFirst() .map(String::trim)
.orElseGet(() -> vehicleTypeMapper.selectVehicleTypeById(dto.getVehicleTypeId())); .filter(s -> !s.isEmpty())
if (chosen == null) { .map(Long::valueOf)
throw new RuntimeException("指定车型不存在或已被禁用id=" + dto.getVehicleTypeId()); .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); // 有适配车型
// 候选项 // // ========== 5) 组装返回 ==========
List<CalcSuggestFeeVO.VehicleTypeOptionVO> list = new ArrayList<>(candidates.size()); // CalcSuggestFeeVO vo = new CalcSuggestFeeVO();
for (VehicleType t : candidates) { // vo.setVehicleTypeId(chosen.getId());
CalcSuggestFeeVO.VehicleTypeOptionVO o = new CalcSuggestFeeVO.VehicleTypeOptionVO(); // vo.setVehicleTypeName(chosen.getTypeName());
o.setId(t.getId()); // vo.setUnitPricePerKm(price);
o.setName(t.getTypeName()); // vo.setHasSuitableType(true); // 有适配车型
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);
return vo; 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<VehiclePlan> bestPlan;
SearchContext(BigDecimal w, BigDecimal v) {
targetWeight = w;
targetVolume = v;
}
}
/** /**
* 构建无匹配车型的错误信息 * 构建无匹配车型的错误信息
*/ */
@@ -235,23 +480,18 @@ public class VehicleTypeServiceImpl implements IVehicleTypeService
if (weightExceeds && volumeExceeds) { if (weightExceeds && volumeExceeds) {
sb.append("货物重量(").append(weight).append("吨)超过最大承重(") sb.append("货物重量(").append(weight).append("吨)超过最大承重(")
.append(maxCapacityType.getWeightMaxTon()).append("吨),且货物体积(") .append(maxCapacityType.getWeightMaxTon()).append("吨),且货物体积(")
.append(volume).append("立方米)超过最大载方(") .append(volume).append("立方米)超过最大载方(")
.append(maxCapacityType.getVolumeMaxM3()).append("立方米)。"); .append(maxCapacityType.getVolumeMaxM3()).append("立方米)。");
} else if (weightExceeds) { } else if (weightExceeds) {
sb.append("货物重量(").append(weight).append("吨)超过最大承重(") sb.append("货物重量(").append(weight).append("吨)超过最大承重(")
.append(maxCapacityType.getWeightMaxTon()).append("吨)。"); .append(maxCapacityType.getWeightMaxTon()).append("吨)。");
} else if (volumeExceeds) { } else if (volumeExceeds) {
sb.append("货物体积(").append(volume).append("立方米)超过最大载方(") sb.append("货物体积(").append(volume).append("立方米)超过最大载方(")
.append(maxCapacityType.getVolumeMaxM3()).append("立方米)。"); .append(maxCapacityType.getVolumeMaxM3()).append("立方米)。");
} }
sb.append("最大承载车型为:").append(maxCapacityType.getTypeName()); sb.append("最大承载车型为:").append(maxCapacityType.getTypeName());
return sb.toString(); return sb.toString();
} }
}
private static BigDecimal nvl(BigDecimal x) {
return x == null ? BigDecimal.ZERO : x;
}
}

View File

@@ -553,7 +553,7 @@
<!-- 车型 --> <!-- 车型 -->
<if test="vehicleTypeId != null"> <if test="vehicleTypeId != null">
AND dor.vehicle_type_id = #{vehicleTypeId} AND FIND_IN_SET(#{vehicleTypeId}, dor.vehicle_type_id)
</if> </if>
<if test="vehicleTypeName != null and vehicleTypeName != ''"> <if test="vehicleTypeName != null and vehicleTypeName != ''">
AND dor.vehicle_type_name LIKE CONCAT('%', #{vehicleTypeName}, '%') AND dor.vehicle_type_name LIKE CONCAT('%', #{vehicleTypeName}, '%')
@@ -945,6 +945,7 @@
<!-- 车辆类型 --> <!-- 车辆类型 -->
<if test="q.vehicleTypeId != null"> <if test="q.vehicleTypeId != null">
AND FIND_IN_SET(#{q.vehicleTypeId}, dor.vehicle_type_id)
AND dor.vehicle_type_id = #{q.vehicleTypeId} AND dor.vehicle_type_id = #{q.vehicleTypeId}
</if> </if>
@@ -1054,4 +1055,16 @@
GROUP BY DATE_FORMAT(dor.delivery_date, '%Y-%m') GROUP BY DATE_FORMAT(dor.delivery_date, '%Y-%m')
ORDER BY statMonth ASC ORDER BY statMonth ASC
</select> </select>
<select id="selectExistsRkRecordIds" resultType="java.lang.Long">
SELECT DISTINCT rk_record_id
FROM delivery_order
WHERE is_delete = '0'
AND rk_record_id IS NOT NULL
AND order_status IN ('1','2','3')
AND rk_record_id IN
<foreach collection="rkRecordIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
</mapper> </mapper>

View File

@@ -140,5 +140,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and volume_max_m3 &gt;= #{v} and volume_max_m3 &gt;= #{v}
order by unit_price_per_km asc, weight_max_ton asc, volume_max_m3 asc, id asc order by unit_price_per_km asc, weight_max_ton asc, volume_max_m3 asc, id asc
</select> </select>
<select id="selectUsableTypes" resultMap="VehicleTypeResult">
SELECT *
FROM vehicle_type
WHERE status = '0'
AND is_delete = '0'
</select>
</mapper> </mapper>