配送页面字段内容调整
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -26,5 +26,6 @@ public class CalcSuggestFeeDTO {
|
||||
private BigDecimal distanceKm;
|
||||
|
||||
/** 指定车型ID(可选;传入则按该车型计算建议费用) */
|
||||
private Long vehicleTypeId;
|
||||
private String vehicleTypeId;
|
||||
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ public class DeliveryOrderCreateDTO {
|
||||
private String orderStatus;
|
||||
|
||||
/** 车型 ID */
|
||||
private Long vehicleTypeId;
|
||||
private String vehicleTypeId;
|
||||
|
||||
/** 车型名称 */
|
||||
private String vehicleTypeName;
|
||||
|
||||
@@ -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<VehiclePlanVO> bestPlan;
|
||||
|
||||
/** 候选车型列表 */
|
||||
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
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ public class DeliveryOrderVo {
|
||||
private String orderStatus;
|
||||
|
||||
/** 车型 ID */
|
||||
private Long vehicleTypeId;
|
||||
private String vehicleTypeId;
|
||||
|
||||
/** 车型名称 */
|
||||
private String vehicleTypeName;
|
||||
|
||||
@@ -113,4 +113,6 @@ public interface DeliveryOrderMapper
|
||||
|
||||
/** 配送单VO列表 */
|
||||
List<DeliveryOrderVo> selectDeliveryOrderVoList(DeliveryOrder deliveryOrder);
|
||||
/** 防止重复创建配送单 */
|
||||
List<Long> selectExistsRkRecordIds(@Param("rkRecordIds") List<Long> rkRecordIds);
|
||||
}
|
||||
|
||||
@@ -77,4 +77,8 @@ public interface VehicleTypeMapper
|
||||
* @return 批量结果
|
||||
*/
|
||||
List<VehicleType> selectFallbackTypes(BigDecimal w, BigDecimal v);
|
||||
/**
|
||||
* 1 查询所有可用车型
|
||||
* */
|
||||
List<VehicleType> selectUsableTypes();
|
||||
}
|
||||
|
||||
@@ -67,4 +67,6 @@ public interface IVehicleTypeService
|
||||
* @return
|
||||
*/
|
||||
CalcSuggestFeeVO calcSuggestFee(CalcSuggestFeeDTO dto);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -186,18 +186,38 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
|
||||
throw new ServiceException("物料明细不能为空");
|
||||
}
|
||||
|
||||
String billNo = dto.getItems().get(0).getBillNo();
|
||||
boolean fromWms = StringUtils.isNotBlank(billNo);
|
||||
List<DeliveryOrderLineDTO> items = dto.getItems();
|
||||
|
||||
List<Long> rkRecordIds = dto.getItems().stream()
|
||||
boolean fromWms = items.stream().anyMatch(it -> StringUtils.isNotBlank(it.getBillNo()));
|
||||
|
||||
List<Long> rkRecordIds = items.stream()
|
||||
.map(DeliveryOrderLineDTO::getRkRecordId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (fromWms && rkRecordIds.isEmpty()) {
|
||||
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<Long> existIds = deliveryOrderMapper.selectExistsRkRecordIds(rkRecordIds);
|
||||
if (existIds != null && !existIds.isEmpty()) {
|
||||
throw new ServiceException("存在已生成配送单的明细,不能重复创建,明细ID:" + existIds);
|
||||
}
|
||||
}
|
||||
|
||||
String orderNo = StringUtils.isBlank(dto.getOrderNo())
|
||||
? "DO" + DateUtils.dateTimeNow("yyyyMMddHHmmssSSS")
|
||||
@@ -208,19 +228,9 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
|
||||
Long currentUserId = SecurityUtils.getUserId();
|
||||
Long makerId = dto.getMakerId() != null ? dto.getMakerId() : currentUserId;
|
||||
|
||||
List<DeliveryOrder> 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<DeliveryOrder> 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) {
|
||||
|
||||
@@ -1,216 +1,307 @@
|
||||
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<VehicleType> selectVehicleTypeList(VehicleType vehicleType)
|
||||
{
|
||||
public List<VehicleType> 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<VehicleType> 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<VehicleType> allVehicleTypes = vehicleTypeMapper.selectVehicleTypeList(new VehicleType());
|
||||
if (allVehicleTypes == null || allVehicleTypes.isEmpty()) {
|
||||
List<VehicleType> vehicleTypes = vehicleTypeMapper.selectUsableTypes();
|
||||
|
||||
if (vehicleTypes == null || vehicleTypes.isEmpty()) {
|
||||
throw new ServiceException("没有配置车型");
|
||||
}
|
||||
|
||||
// 构建算法节点
|
||||
List<VehicleNode> 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<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,
|
||||
index + 1,
|
||||
weight.add(addWeight),
|
||||
volume.add(addVolume),
|
||||
fee.add(addFee),
|
||||
vehicleCount + i,
|
||||
newPlan,
|
||||
ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算最多车辆数
|
||||
*/
|
||||
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();
|
||||
vo.setErrorMessage("系统中暂无车型配置,请先配置车型信息。");
|
||||
|
||||
if (ctx.bestPlan == null) {
|
||||
vo.setHasSuitableType(false);
|
||||
return vo;
|
||||
}
|
||||
|
||||
// 找到最大承载能力的车型
|
||||
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);
|
||||
vo.setHasSuitableType(true);
|
||||
vo.setSuggestFee(ctx.bestFee);
|
||||
vo.setTotalVehicleCount(ctx.bestVehicleCount);
|
||||
|
||||
if (maxCapacityType != null) {
|
||||
// 检查最大容量车型是否仍无法满足要求
|
||||
String errorMsg = buildNoMatchErrorMessage(w, v, maxCapacityType);
|
||||
CalcSuggestFeeVO vo = new CalcSuggestFeeVO();
|
||||
vo.setErrorMessage(errorMsg);
|
||||
vo.setHasSuitableType(false);
|
||||
// 仍然返回候选车型列表供参考
|
||||
List<CalcSuggestFeeVO.VehicleTypeOptionVO> list = new ArrayList<>(allVehicleTypes.size());
|
||||
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) {
|
||||
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)));
|
||||
|
||||
// ========== 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());
|
||||
// 4. 筛选满足条件的车型
|
||||
List<VehicleType> 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);
|
||||
}
|
||||
} else {
|
||||
chosen = candidates.get(0);
|
||||
}
|
||||
|
||||
// ========== 4) 计算建议费用:单价 × 公里数,保留2位 ==========
|
||||
BigDecimal price = chosen.getUnitPricePerKm();
|
||||
if (price == null) {
|
||||
throw new RuntimeException("车型【" + chosen.getTypeName() + "】未配置每公里单价(unit_price_per_km 为空)");
|
||||
// 5. 如果没有符合条件的,返回全部启用车型
|
||||
if (candidates.isEmpty()) {
|
||||
candidates = enabledTypes;
|
||||
}
|
||||
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<CalcSuggestFeeVO.VehicleTypeOptionVO> list = new ArrayList<>(candidates.size());
|
||||
List<CalcSuggestFeeVO.VehicleTypeOptionVO> list = new ArrayList<>();
|
||||
for (VehicleType t : candidates) {
|
||||
CalcSuggestFeeVO.VehicleTypeOptionVO o = new CalcSuggestFeeVO.VehicleTypeOptionVO();
|
||||
o.setId(t.getId());
|
||||
o.setId(String.valueOf(t.getId()));
|
||||
o.setName(t.getTypeName());
|
||||
o.setUnitPricePerKm(t.getUnitPricePerKm());
|
||||
o.setWeightMinTon(t.getWeightMinTon());
|
||||
@@ -220,9 +311,163 @@ public class VehicleTypeServiceImpl implements IVehicleTypeService
|
||||
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 系统推荐 ==========
|
||||
List<VehicleType> chosenList;
|
||||
|
||||
if (StringUtils.isNotBlank(dto.getVehicleTypeId())) {
|
||||
|
||||
List<Long> 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()
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// // ========== 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<VehiclePlan> bestPlan;
|
||||
|
||||
SearchContext(BigDecimal w, BigDecimal v) {
|
||||
targetWeight = w;
|
||||
targetVolume = v;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 构建无匹配车型的错误信息
|
||||
*/
|
||||
@@ -249,9 +494,4 @@ public class VehicleTypeServiceImpl implements IVehicleTypeService
|
||||
sb.append("最大承载车型为:").append(maxCapacityType.getTypeName());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static BigDecimal nvl(BigDecimal x) {
|
||||
return x == null ? BigDecimal.ZERO : x;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -553,7 +553,7 @@
|
||||
|
||||
<!-- 车型 -->
|
||||
<if test="vehicleTypeId != null">
|
||||
AND dor.vehicle_type_id = #{vehicleTypeId}
|
||||
AND FIND_IN_SET(#{vehicleTypeId}, dor.vehicle_type_id)
|
||||
</if>
|
||||
<if test="vehicleTypeName != null and vehicleTypeName != ''">
|
||||
AND dor.vehicle_type_name LIKE CONCAT('%', #{vehicleTypeName}, '%')
|
||||
@@ -945,6 +945,7 @@
|
||||
|
||||
<!-- 车辆类型 -->
|
||||
<if test="q.vehicleTypeId != null">
|
||||
AND FIND_IN_SET(#{q.vehicleTypeId}, dor.vehicle_type_id)
|
||||
AND dor.vehicle_type_id = #{q.vehicleTypeId}
|
||||
</if>
|
||||
|
||||
@@ -1054,4 +1055,16 @@
|
||||
GROUP BY DATE_FORMAT(dor.delivery_date, '%Y-%m')
|
||||
ORDER BY statMonth ASC
|
||||
</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>
|
||||
|
||||
@@ -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
|
||||
</select>
|
||||
|
||||
<select id="selectUsableTypes" resultMap="VehicleTypeResult">
|
||||
SELECT *
|
||||
FROM vehicle_type
|
||||
WHERE status = '0'
|
||||
AND is_delete = '0'
|
||||
</select>
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user