配送系统功能开发1205
This commit is contained in:
@@ -118,6 +118,7 @@ public class SecurityConfig
|
||||
"/delivery/**",
|
||||
"/document/vehicle/**",
|
||||
"/document/type/**",
|
||||
"/ocr/**",
|
||||
"/document/location/**",
|
||||
"/document/mtd/**",
|
||||
"/document/info/**",
|
||||
|
||||
@@ -122,7 +122,7 @@ public class DeliveryOrderController extends BaseController
|
||||
}
|
||||
|
||||
|
||||
/** 新增配送单据:同一单号多行写入 */
|
||||
/** 发布配送单据:同一单号多行写入 */
|
||||
@PreAuthorize("@ss.hasPermi('document:order:add')")
|
||||
@Log(title = "配送单据", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/batch")
|
||||
@@ -141,7 +141,7 @@ public class DeliveryOrderController extends BaseController
|
||||
}
|
||||
|
||||
/** 详情:按单号查询所有行 */
|
||||
@PreAuthorize("@ss.hasPermi('document:order:query')")
|
||||
// @PreAuthorize("@ss.hasPermi('document:order:query')")
|
||||
@GetMapping("/detail/{orderNo}")
|
||||
public AjaxResult detail(@PathVariable String orderNo) {
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.delivery.project.document.domain.dto.CalcSuggestFeeDTO;
|
||||
import com.delivery.project.document.domain.vo.CalcSuggestFeeVO;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
@@ -15,6 +16,7 @@ import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import javax.validation.Valid;
|
||||
import com.delivery.framework.aspectj.lang.annotation.Log;
|
||||
import com.delivery.framework.aspectj.lang.enums.BusinessType;
|
||||
import com.delivery.project.document.domain.VehicleType;
|
||||
@@ -32,6 +34,7 @@ import com.delivery.framework.web.page.TableDataInfo;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/document/type")
|
||||
@Validated
|
||||
public class VehicleTypeController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
@@ -107,7 +110,7 @@ public class VehicleTypeController extends BaseController
|
||||
|
||||
/** 计算建议费用(支持前端指定车型重算) */
|
||||
@PostMapping("/fee")
|
||||
public AjaxResult calcSuggestFee(@RequestBody CalcSuggestFeeDTO dto) {
|
||||
public AjaxResult calcSuggestFee(@Valid @RequestBody CalcSuggestFeeDTO dto) {
|
||||
CalcSuggestFeeVO vo = vehicleTypeService.calcSuggestFee(dto);
|
||||
return success(vo);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,25 @@ public class DeliveryOrder extends BaseEntity {
|
||||
@Excel(name = "配送单据号")
|
||||
private String orderNo;
|
||||
|
||||
/** rk_info主键ID */
|
||||
@Excel(name = "rk_info主键ID")
|
||||
private Long rkInfoId;
|
||||
|
||||
/** 制单人用户ID */
|
||||
@Excel(name = "制单人ID")
|
||||
private Long makerId;
|
||||
|
||||
/** 制单人用户名(不入库,用于返回给前端) */
|
||||
private String makerUserName;
|
||||
|
||||
/** 接收物资状态(1数量齐全状态完好 2存在问题) */
|
||||
@Excel(name = "接收物资状态", readConverterExp = "1=数量齐全状态完好,2=存在问题")
|
||||
private Integer receiveStatus; // *** 修改:从 String 改为 Integer ***
|
||||
|
||||
/** 接收物资问题描述 */
|
||||
@Excel(name = "存在问题描述")
|
||||
private String receiveProblem;
|
||||
|
||||
/** 出库单据号 */
|
||||
@Excel(name = "出库单据号")
|
||||
private String billNoCk;
|
||||
@@ -148,9 +167,11 @@ public class DeliveryOrder extends BaseEntity {
|
||||
/** 连表查询用:附件列表 */
|
||||
private List<DeliveryAttachment> attachments;
|
||||
|
||||
/** 查询用:多状态筛选 */
|
||||
private List<String> orderStatusList;
|
||||
|
||||
// ===================== 费用与里程 =====================
|
||||
|
||||
/** 建议费用(按车型单价*里程的推荐值) */
|
||||
@Excel(name = "建议费用")
|
||||
private BigDecimal suggestFee;
|
||||
@@ -174,8 +195,33 @@ public class DeliveryOrder extends BaseEntity {
|
||||
|
||||
public String getOrderNo() { return orderNo; }
|
||||
public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
|
||||
|
||||
public Long getRkInfoId() { return rkInfoId; }
|
||||
public void setRkInfoId(Long rkInfoId) { this.rkInfoId = rkInfoId; }
|
||||
|
||||
public String getMakerUserName() {
|
||||
return makerUserName;
|
||||
}
|
||||
public void setMakerUserName(String makerUserName) {
|
||||
this.makerUserName = makerUserName;
|
||||
}
|
||||
|
||||
public Long getMakerId() { return makerId; }
|
||||
public void setMakerId(Long makerId) { this.makerId = makerId; }
|
||||
|
||||
public Integer getReceiveStatus() { // *** 修改 ***
|
||||
return receiveStatus;
|
||||
}
|
||||
public void setReceiveStatus(Integer receiveStatus) { // *** 修改 ***
|
||||
this.receiveStatus = receiveStatus;
|
||||
}
|
||||
|
||||
public String getReceiveProblem() { return receiveProblem; }
|
||||
public void setReceiveProblem(String receiveProblem) { this.receiveProblem = receiveProblem; }
|
||||
|
||||
public String getBillNoCk() { return billNoCk; }
|
||||
public void setBillNoCk(String billNoCk) { this.billNoCk = billNoCk; }
|
||||
|
||||
public String getXmMs() { return xmMs; }
|
||||
public void setXmMs(String xmMs) { this.xmMs = xmMs; }
|
||||
|
||||
@@ -266,6 +312,9 @@ public class DeliveryOrder extends BaseEntity {
|
||||
public List<DeliveryAttachment> getAttachments() { return attachments; }
|
||||
public void setAttachments(List<DeliveryAttachment> attachments) { this.attachments = attachments; }
|
||||
|
||||
public List<String> getOrderStatusList() { return orderStatusList; }
|
||||
public void setOrderStatusList(List<String> orderStatusList) { this.orderStatusList = orderStatusList; }
|
||||
|
||||
public BigDecimal getSuggestFee() { return suggestFee; }
|
||||
public void setSuggestFee(BigDecimal suggestFee) { this.suggestFee = suggestFee; }
|
||||
|
||||
@@ -278,19 +327,18 @@ public class DeliveryOrder extends BaseEntity {
|
||||
public BigDecimal getTotalKm() { return totalKm; }
|
||||
public void setTotalKm(BigDecimal totalKm) { this.totalKm = totalKm; }
|
||||
|
||||
public List<String> getOrderStatusList() {
|
||||
return orderStatusList;
|
||||
}
|
||||
public void setOrderStatusList(List<String> orderStatusList) {
|
||||
this.orderStatusList = orderStatusList;
|
||||
}
|
||||
|
||||
// ===================== toString =====================
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
|
||||
.append("id", getId())
|
||||
.append("orderNo", getOrderNo())
|
||||
.append("rkInfoId", getRkInfoId())
|
||||
.append("makerId", getMakerId())
|
||||
.append("makerUserName", getMakerUserName())
|
||||
.append("receiveStatus", getReceiveStatus()) // *** 类型已是 Integer ***
|
||||
.append("receiveProblem", getReceiveProblem())
|
||||
.append("billNoCk", getBillNoCk())
|
||||
.append("xmMs", getXmMs())
|
||||
.append("xmNo", getXmNo())
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
package com.delivery.project.document.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.DecimalMax;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
@@ -11,12 +13,16 @@ import java.math.BigDecimal;
|
||||
@Data
|
||||
public class CalcSuggestFeeDTO {
|
||||
/** 货物重量(单位:吨) */
|
||||
@DecimalMin(value = "0.00", message = "货物重量不能小于0")
|
||||
private BigDecimal weightTon;
|
||||
|
||||
/** 货物体积(单位:立方米) */
|
||||
@DecimalMin(value = "0.00", message = "货物体积不能小于0")
|
||||
private BigDecimal volumeM3;
|
||||
|
||||
/** 行程距离(单位:公里) */
|
||||
@DecimalMin(value = "0.00", message = "行程距离不能小于0")
|
||||
@DecimalMax(value = "999999.99", message = "行程距离不能超过999999.99公里")
|
||||
private BigDecimal distanceKm;
|
||||
|
||||
/** 指定车型ID(可选;传入则按该车型计算建议费用) */
|
||||
|
||||
@@ -31,6 +31,15 @@ public class DeliveryExecuteBindDTO {
|
||||
private BigDecimal actualFee; // 实际费用
|
||||
private BigDecimal tollFee; // 高速费用
|
||||
|
||||
/**
|
||||
* 接收物资状态:
|
||||
* 0 待确认,1 齐全完好,2 存在问题
|
||||
*/
|
||||
private Integer receiveStatus;
|
||||
|
||||
/** 当 receiveStatus = 2 时,必须填写问题描述 */
|
||||
private String receiveProblem;
|
||||
|
||||
/** 本次执行的附件条目(URL 列表) */
|
||||
@NotNull
|
||||
private List<DeliveryAttachItemDTO> attachments;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.delivery.project.document.domain.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -17,14 +16,9 @@ import java.util.List;
|
||||
@Data
|
||||
public class DeliveryOrderCreateDTO {
|
||||
|
||||
/**
|
||||
* 配送单据号,可传可不传;
|
||||
* 不传时由后端自动生成(格式如 DO202510280001)
|
||||
*/
|
||||
/** 配送单据号(可空,后台自动生成) */
|
||||
private String orderNo;
|
||||
|
||||
// ==================== 头部信息(整单通用) ====================
|
||||
|
||||
/** 起始地点名称 */
|
||||
private String originName;
|
||||
|
||||
@@ -43,56 +37,70 @@ public class DeliveryOrderCreateDTO {
|
||||
/** 目的地点纬度 */
|
||||
private BigDecimal destLat;
|
||||
|
||||
/** 配送日期(yyyy-MM-dd 格式) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
/** 配送日期 */
|
||||
private Date deliveryDate;
|
||||
|
||||
/** 配送车辆车牌号 */
|
||||
/** 车牌号 */
|
||||
private String plateNo;
|
||||
|
||||
/** 发货人姓名 */
|
||||
/** 发货人名称 */
|
||||
private String shipperName;
|
||||
|
||||
/** 发货人联系电话 */
|
||||
/** 发货人联系方式 */
|
||||
private String shipperPhone;
|
||||
|
||||
/** 收货人姓名 */
|
||||
/** 接收人名称 */
|
||||
private String receiverName;
|
||||
|
||||
/** 收货人联系电话 */
|
||||
/** 接收人联系方式 */
|
||||
private String receiverPhone;
|
||||
|
||||
/** 收货单位名称 */
|
||||
/** 接收单位 */
|
||||
private String receiverOrgName;
|
||||
|
||||
/** 配送吨位(单位:吨) */
|
||||
/** 配送吨位 */
|
||||
private BigDecimal deliveryTon;
|
||||
|
||||
/** 货物体积(单位:立方米) */
|
||||
/** 货物尺寸 */
|
||||
private BigDecimal goodsSize;
|
||||
|
||||
/** 单据状态(默认 0:待发货,1:起运,2:送达) */
|
||||
/**
|
||||
* 接收物资状态:
|
||||
* 1:数量齐全、状态完好(默认)
|
||||
* 2:存在问题
|
||||
*/
|
||||
private Integer receiveStatus; // *** 修改:String -> Integer ***
|
||||
|
||||
/**
|
||||
* 接收物资问题描述(receiveStatus = 2 时必填)
|
||||
*/
|
||||
private String receiveProblem;
|
||||
|
||||
/** 制单人ID(前端可传;如果为空则后台自动取当前登录用户ID) */
|
||||
private Long makerId;
|
||||
|
||||
/** 配送状态(默认:1 已接单 / 后续会调整) */
|
||||
private String orderStatus;
|
||||
|
||||
/** 车辆类型ID(外键,对应 vehicle_type 表主键) */
|
||||
/** 车型 ID */
|
||||
private Long vehicleTypeId;
|
||||
|
||||
/** 车辆类型名称(如:小型面包车、9.6米货车等) */
|
||||
/** 车型名称 */
|
||||
private String vehicleTypeName;
|
||||
|
||||
/** 系统建议运费(根据车型和里程计算) */
|
||||
/** 建议费用 */
|
||||
private BigDecimal suggestFee;
|
||||
|
||||
/** 实际运费(人工调整或司机确认后) */
|
||||
/** 实际费用 */
|
||||
private BigDecimal actualFee;
|
||||
|
||||
/** 过路费(收费站、高速费等额外费用) */
|
||||
/** 高速费用 */
|
||||
private BigDecimal tollFee;
|
||||
|
||||
/** 配送总里程(单位:公里) */
|
||||
/** 总公里数 */
|
||||
private BigDecimal totalKm;
|
||||
|
||||
/** 备注说明(可填写其他补充信息) */
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
// ==================== 行明细(多物料行) ====================
|
||||
|
||||
@@ -6,13 +6,38 @@ import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class DeliveryOrderLineDTO {
|
||||
private String billNoCk; // 出库单据号
|
||||
private String xmMs; // 项目描述
|
||||
private String xmNo; // 项目号
|
||||
private String wlNo; // 物料号
|
||||
private String wlMs; // 物料描述
|
||||
private BigDecimal realQty; // 实际入库数量
|
||||
private String dw; // 计量单位
|
||||
private String sapNo; // SAP订单编号
|
||||
private String gysMc; // 供应商名称
|
||||
|
||||
/**
|
||||
* 对应智慧实物系统 rk_info 表的主键 ID
|
||||
* detailList 里的 id 就是这个值
|
||||
*/
|
||||
private Long rkInfoId;
|
||||
|
||||
/** 出库单据号 */
|
||||
private String billNoCk;
|
||||
|
||||
/** 项目描述 */
|
||||
private String xmMs;
|
||||
|
||||
/** 项目号 */
|
||||
private String xmNo;
|
||||
|
||||
/** 物料号 */
|
||||
private String wlNo;
|
||||
|
||||
/** 物料描述 */
|
||||
private String wlMs;
|
||||
|
||||
/** 实际数量 */
|
||||
private BigDecimal realQty;
|
||||
|
||||
/** 计量单位 */
|
||||
private String dw;
|
||||
|
||||
/** SAP订单编号 */
|
||||
private String sapNo;
|
||||
|
||||
/** 供应商名称 */
|
||||
private String gysMc;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,12 @@ public class CalcSuggestFeeVO {
|
||||
/** 建议费用(单位:元,四舍五入保留2位小数) */
|
||||
private BigDecimal suggestFee;
|
||||
|
||||
/** 错误信息 */
|
||||
private String errorMessage;
|
||||
|
||||
/** 是否有适配车型 */
|
||||
private Boolean hasSuitableType;
|
||||
|
||||
/** 候选车型列表(按价格/容量排序) */
|
||||
private List<VehicleTypeOptionVO> candidates;
|
||||
|
||||
@@ -51,7 +57,7 @@ public class CalcSuggestFeeVO {
|
||||
/** 载方下限(单位:立方米,含) */
|
||||
private BigDecimal volumeMinM3;
|
||||
|
||||
/** 载方上限(单位:立方米,含) */
|
||||
/** 载方上限(单位:吨,含) */
|
||||
private BigDecimal volumeMaxM3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.delivery.project.document.domain.vo;
|
||||
|
||||
import com.delivery.project.document.domain.DeliveryOrder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 配送单据详情 VO(带图片)
|
||||
* 继承 DeliveryOrder,只做返回展示使用,不参与持久化映射。
|
||||
*/
|
||||
@Data
|
||||
public class DeliveryOrderDetailVO extends DeliveryOrder {
|
||||
|
||||
/** 当前登录用户ID(只做前端展示用) */
|
||||
private Long makerId;
|
||||
|
||||
/** 当前登录用户名(只做前端展示用) */
|
||||
private String makerUserName;
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.delivery.project.document.domain.DeliveryOrder;
|
||||
import com.delivery.project.document.domain.dto.DeliveryOrderCreateDTO;
|
||||
import com.delivery.project.document.domain.dto.DeliveryOrderSaveDTO;
|
||||
import com.delivery.project.document.domain.vo.DeliveryBillVO;
|
||||
import com.delivery.project.document.domain.vo.DeliveryOrderDetailVO;
|
||||
import com.delivery.project.document.domain.vo.DeliveryOrderGroupVO;
|
||||
|
||||
/**
|
||||
@@ -92,7 +93,7 @@ public interface IDeliveryOrderService
|
||||
* @return
|
||||
*/
|
||||
/** 详情:按单号查行 */
|
||||
List<DeliveryOrder> listByOrderNo(String orderNo);
|
||||
List<DeliveryOrderDetailVO> listByOrderNo(String orderNo);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ public interface IVehicleTypeService
|
||||
public int deleteVehicleTypeById(Long id);
|
||||
|
||||
/**
|
||||
* 计算建议运费
|
||||
* 计算建议运费,推荐车型
|
||||
* @param dto
|
||||
* @return
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.delivery.common.exception.ServiceException;
|
||||
@@ -139,25 +140,32 @@ public class DeliveryAttachmentServiceImpl implements IDeliveryAttachmentService
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int executeBind(DeliveryExecuteBindDTO dto) {
|
||||
// 1) 校验订单存在
|
||||
// 1) 校验订单存在(一个单号多行)
|
||||
List<DeliveryOrder> existList = deliveryOrderMapper.selectDeliveryOrderByOrderNo(dto.getOrderNo());
|
||||
if (existList == null || existList.isEmpty()) {
|
||||
throw new ServiceException("配送单不存在:" + dto.getOrderNo());
|
||||
}
|
||||
|
||||
String billNoCk = existList.get(0).getBillNoCk();
|
||||
// ====== 从配送单中拿到所有 rk_info_id 列表 ======
|
||||
List<Long> rkInfoIdList = existList.stream()
|
||||
.map(DeliveryOrder::getRkInfoId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (StringUtils.isBlank(billNoCk)) {
|
||||
throw new ServiceException("配送单未绑定出库单号,无法回写库存状态!");
|
||||
if (rkInfoIdList.isEmpty()) {
|
||||
throw new ServiceException("配送单未绑定 rk_info_id,无法回写库存状态!");
|
||||
}
|
||||
// ====================================================
|
||||
|
||||
if (dto.getAttachments() == null || dto.getAttachments().isEmpty()) {
|
||||
throw new ServiceException("附件列表不能为空");
|
||||
}
|
||||
|
||||
// 2) 批量插入附件(只传 URL)
|
||||
List<DeliveryAttachment> list = new ArrayList<>();
|
||||
String username = getUsername();
|
||||
// String username = "大爷的!!!";
|
||||
|
||||
String username = getUsername();
|
||||
for (DeliveryAttachItemDTO it : dto.getAttachments()) {
|
||||
if (it == null) {
|
||||
continue;
|
||||
@@ -215,24 +223,36 @@ public class DeliveryAttachmentServiceImpl implements IDeliveryAttachmentService
|
||||
patch.setTollFee(dto.getTollFee());
|
||||
}
|
||||
|
||||
Integer receiveStatus = dto.getReceiveStatus();
|
||||
if (receiveStatus == null) {
|
||||
throw new ServiceException("完成配送时必须选择接收物资状态");
|
||||
}
|
||||
if (receiveStatus != 0 && receiveStatus != 1 && receiveStatus != 2) {
|
||||
throw new ServiceException("接收物资状态不合法");
|
||||
}
|
||||
patch.setReceiveStatus(receiveStatus);
|
||||
|
||||
if (receiveStatus == 2) {
|
||||
// 有问题必须写说明
|
||||
if (StringUtils.isBlank(dto.getReceiveProblem())) {
|
||||
throw new ServiceException("存在问题时必须填写问题描述");
|
||||
}
|
||||
patch.setReceiveProblem(dto.getReceiveProblem());
|
||||
}
|
||||
|
||||
patch.setOrderStatus("3"); // 已完成
|
||||
}
|
||||
|
||||
deliveryOrderMapper.updateDeliveryOrder(patch);
|
||||
|
||||
// 4) ⭐ 如果是 DEST 场景,远程调用 WMS,把 rk_info.is_delivery 改成 3
|
||||
// 4) ⭐ 如果是 DEST 场景,远程调用 WMS,把这些 rk_info 记录的 is_delivery 改成 3
|
||||
if ("DEST".equals(scene)) {
|
||||
|
||||
if (StringUtils.isBlank(billNoCk)) {
|
||||
throw new ServiceException("配送单缺少对应的出库单号 billNoCk,无法回写库存状态!");
|
||||
}
|
||||
|
||||
// rkInfoMapper.updateDeliveryStatus(billNoCk, 3);
|
||||
|
||||
boolean ok = updateWmsIsDelivery(billNoCk, 3);
|
||||
// 这里已经不再按 billNoCk 整单更新,而是按 rk_info_id 列表更新
|
||||
boolean ok = updateWmsIsDeliveryByIds(rkInfoIdList, 3);
|
||||
if (!ok) {
|
||||
// 让整个事务回滚,附件 + 配送单状态都撤回
|
||||
throw new ServiceException("回写 WMS 配送状态失败,出库单号:" + billNoCk);
|
||||
throw new ServiceException("回写 WMS 配送状态失败,rk_info_id 列表:" + rkInfoIdList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,24 +260,28 @@ public class DeliveryAttachmentServiceImpl implements IDeliveryAttachmentService
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程调用智慧实物管理系统,更新 rk_info.is_delivery 状态
|
||||
* 远程调用智慧实物管理系统,按 rk_info 主键ID列表更新 is_delivery 状态
|
||||
* 约定请求体结构:
|
||||
* {
|
||||
* "rkInfoIdList": [1, 2, 3],
|
||||
* "isDelivery": 3
|
||||
* }
|
||||
*/
|
||||
private boolean updateWmsIsDelivery(String billNoCk, int isDelivery) {
|
||||
private boolean updateWmsIsDeliveryByIds(List<Long> rkInfoIdList, int isDelivery) {
|
||||
|
||||
String url = wisdomBaseUrl + "/wisdom/stock/updateDeliveryStatus";
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("billNoCk", billNoCk);
|
||||
body.put("ids", rkInfoIdList);
|
||||
body.put("isDelivery", isDelivery);
|
||||
|
||||
String json = JSON.toJSONString(body);
|
||||
|
||||
try {
|
||||
// 用你项目里的 HttpUtils 发 JSON POST
|
||||
String resp = HttpUtils.sendPost(url, json, MediaType.APPLICATION_JSON_VALUE);
|
||||
|
||||
if (StringUtils.isBlank(resp)) {
|
||||
log.error("WMS 更新失败,响应为空,url={} billNoCk={}", url, billNoCk);
|
||||
log.error("WMS 更新失败,响应为空,url={} rkInfoIdList={}", url, rkInfoIdList);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -269,16 +293,17 @@ public class DeliveryAttachmentServiceImpl implements IDeliveryAttachmentService
|
||||
String msg = (result == null)
|
||||
? "响应为空"
|
||||
: String.valueOf(result.get(AjaxResult.MSG_TAG));
|
||||
log.error("WMS 更新失败,billNoCk={},原因={}", billNoCk, msg);
|
||||
log.error("WMS 更新失败,rkInfoIdList={},原因={}", rkInfoIdList, msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("调用 WMS 接口异常,billNoCk={},error={}", billNoCk, e.getMessage(), e);
|
||||
log.error("调用 WMS 接口异常,rkInfoIdList={},error={}", rkInfoIdList, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 保存目录 D:\delivery
|
||||
private static final String BASE_PATH = "D:/delivery";
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.delivery.project.document.domain.dto.DeliveryOrderCreateDTO;
|
||||
import com.delivery.project.document.domain.dto.DeliveryOrderLineDTO;
|
||||
import com.delivery.project.document.domain.dto.DeliveryOrderSaveDTO;
|
||||
import com.delivery.project.document.domain.vo.DeliveryBillVO;
|
||||
import com.delivery.project.document.domain.vo.DeliveryOrderDetailVO;
|
||||
import com.delivery.project.document.domain.vo.DeliveryOrderGroupVO;
|
||||
import com.delivery.project.document.mapper.DeliveryAttachmentMapper;
|
||||
import com.delivery.project.document.mapper.MtdMapper;
|
||||
@@ -51,8 +52,6 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
|
||||
@Autowired
|
||||
private DeliveryAttachmentMapper deliveryAttachmentMapper;
|
||||
|
||||
@Autowired
|
||||
private RkInfoMapper rkInfoMapper;
|
||||
|
||||
@Autowired
|
||||
private MtdMapper mtdMapper;
|
||||
@@ -185,34 +184,67 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
|
||||
public String createOrder(DeliveryOrderCreateDTO dto) {
|
||||
|
||||
// ========== 0. 参数校验 ==========
|
||||
|
||||
if (dto == null || dto.getItems() == null || dto.getItems().isEmpty()) {
|
||||
throw new ServiceException("物料明细不能为空");
|
||||
}
|
||||
|
||||
// 所有明细应是同一出库单据号,这里简单从第一条取出
|
||||
// 取第一条,仍然要求同一出库单据号(暂不支持多出库单合并)
|
||||
String billNoCk = dto.getItems().get(0).getBillNoCk();
|
||||
if (billNoCk == null || billNoCk.trim().isEmpty()) {
|
||||
if (StringUtils.isBlank(billNoCk)) {
|
||||
throw new ServiceException("明细行缺少出库单据号 billNoCk");
|
||||
}
|
||||
|
||||
// 收集 rk_info 主键 id(用于回写 WMS)
|
||||
List<Long> rkInfoIds = dto.getItems().stream()
|
||||
.map(DeliveryOrderLineDTO::getRkInfoId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (rkInfoIds.isEmpty()) {
|
||||
throw new ServiceException("明细行缺少 rk_info 主键 id,无法回写配送状态");
|
||||
}
|
||||
|
||||
// ========== 1. 生成配送单号 ==========
|
||||
String orderNo = (dto.getOrderNo() == null || dto.getOrderNo().trim().isEmpty())
|
||||
|
||||
String orderNo = StringUtils.isBlank(dto.getOrderNo())
|
||||
? "DO" + DateUtils.dateTimeNow("yyyyMMddHHmmssSSS")
|
||||
: dto.getOrderNo().trim();
|
||||
|
||||
Date now = DateUtils.getNowDate();
|
||||
String username = SecurityUtils.getUsername();
|
||||
Long currentUserId = null;
|
||||
try {
|
||||
currentUserId = SecurityUtils.getUserId();
|
||||
} catch (Exception e) {
|
||||
log.warn("获取当前登录用户ID失败:{}", e.getMessage());
|
||||
}
|
||||
|
||||
// 制单人用户ID:优先用前端传的 makerId,没有则回退到当前登录用户
|
||||
Long makerId = dto.getMakerId();
|
||||
if (makerId == null) {
|
||||
if (currentUserId == null) {
|
||||
throw new ServiceException("无法确定制单人用户ID,请重新登录后重试");
|
||||
}
|
||||
makerId = currentUserId;
|
||||
}
|
||||
|
||||
List<DeliveryOrder> rows = new ArrayList<>();
|
||||
|
||||
// ========== 2. 遍历每一条物料明细 ==========
|
||||
// ========== 2. 遍历每一条物料明细,组装行记录 ==========
|
||||
|
||||
for (DeliveryOrderLineDTO it : dto.getItems()) {
|
||||
|
||||
// 这里如果允许多个出库单合并配送,可以去掉这个校验
|
||||
// 仍然限制同一出库单(你后续要支持多出库单再放开)
|
||||
if (!billNoCk.equals(it.getBillNoCk())) {
|
||||
throw new ServiceException("当前接口暂不支持多张出库单合并配送,请确保所有明细 billNoCk 相同");
|
||||
}
|
||||
|
||||
if (it.getRkInfoId() == null) {
|
||||
throw new ServiceException("明细行缺少 rk_info 主键 id");
|
||||
}
|
||||
|
||||
DeliveryOrder row = new DeliveryOrder();
|
||||
|
||||
// 2.1 公共头部字段(整单一致)
|
||||
@@ -225,6 +257,7 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
|
||||
row.setDestLat(dto.getDestLat());
|
||||
row.setDeliveryDate(dto.getDeliveryDate());
|
||||
row.setPlateNo(dto.getPlateNo());
|
||||
|
||||
row.setShipperName(dto.getShipperName());
|
||||
row.setShipperPhone(dto.getShipperPhone());
|
||||
row.setReceiverName(dto.getReceiverName());
|
||||
@@ -232,9 +265,15 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
|
||||
row.setReceiverOrgName(dto.getReceiverOrgName());
|
||||
row.setDeliveryTon(dto.getDeliveryTon());
|
||||
row.setGoodsSize(dto.getGoodsSize());
|
||||
row.setOrderStatus(
|
||||
StringUtils.isBlank(dto.getOrderStatus()) ? "1" : dto.getOrderStatus().trim()
|
||||
);
|
||||
|
||||
// 制单人用户ID
|
||||
row.setMakerId(makerId);
|
||||
|
||||
// 配送状态:前端不传时默认 1(已接单 / 待起运)
|
||||
String orderStatus = StringUtils.isBlank(dto.getOrderStatus())
|
||||
? "1" : dto.getOrderStatus().trim();
|
||||
row.setOrderStatus(orderStatus);
|
||||
|
||||
row.setVehicleTypeId(dto.getVehicleTypeId());
|
||||
row.setVehicleTypeName(dto.getVehicleTypeName());
|
||||
row.setSuggestFee(dto.getSuggestFee());
|
||||
@@ -244,6 +283,7 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
|
||||
row.setRemark(dto.getRemark());
|
||||
|
||||
// 2.2 明细字段
|
||||
row.setRkInfoId(it.getRkInfoId()); // 关联 rk_info 主键ID
|
||||
row.setBillNoCk(it.getBillNoCk());
|
||||
row.setXmMs(it.getXmMs());
|
||||
row.setXmNo(it.getXmNo());
|
||||
@@ -263,17 +303,16 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
|
||||
}
|
||||
|
||||
// ========== 3. 批量落库 ==========
|
||||
|
||||
if (!rows.isEmpty()) {
|
||||
deliveryOrderMapper.batchInsert(rows);
|
||||
}
|
||||
|
||||
// rkInfoMapper.updateDeliveryStatus(billNoCk, 2);
|
||||
// ========== 4. 回写 WMS:按 rk_info.id 更新 is_delivery = 2(配送中) ==========
|
||||
|
||||
// ========== 4. 回写 WMS:rk_info.is_delivery = 2(配送中) ==========
|
||||
// 按出库单据号整单回写
|
||||
boolean ok = updateWmsIsDelivery(billNoCk, 2);
|
||||
boolean ok = updateWmsIsDeliveryByIds(rkInfoIds, 2);
|
||||
if (!ok) {
|
||||
// 这里直接抛异常,让当前事务回滚,避免出现“配送单已生成,WMS 状态没改”的脏数据
|
||||
// 回写失败,整单回滚,避免脏数据
|
||||
throw new ServiceException("已生成配送单,但回写 WMS 配送状态失败");
|
||||
}
|
||||
|
||||
@@ -281,34 +320,29 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程调用 WMS,按出库单据号修改 rk_info.is_delivery 状态
|
||||
* 远程调用 WMS,按 rk_info 主键 ID 列表修改 is_delivery 状态
|
||||
*
|
||||
* 请求方式示例:
|
||||
* POST ${delivery.wms-base-url}/delivery/rkInfo/updateDeliveryStatus
|
||||
* Body: { "billNoCk": "CK202511200001", "isDelivery": 2 }
|
||||
* POST ${delivery.wisdom-base-url}/wisdom/stock/updateDeliveryStatus
|
||||
* Body: { "ids": [1, 2, 3], "isDelivery": 2 }
|
||||
*/
|
||||
private boolean updateWmsIsDelivery(String billNoCk, int isDelivery) {
|
||||
private boolean updateWmsIsDeliveryByIds(List<Long> rkInfoIds, int isDelivery) {
|
||||
|
||||
String url = wisdomBaseUrl + "/wisdom/stock/updateDeliveryStatus";
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("billNoCk", billNoCk);
|
||||
map.put("ids", rkInfoIds);
|
||||
map.put("isDelivery", isDelivery);
|
||||
|
||||
String json = JSON.toJSONString(map);
|
||||
|
||||
try {
|
||||
// 发送 JSON POST(你刚加的 sendJsonPost)
|
||||
String resp = HttpUtils.sendJsonPost(url, json);
|
||||
|
||||
// 解析为 AjaxResult
|
||||
AjaxResult result = JSON.parseObject(resp, AjaxResult.class);
|
||||
|
||||
if (result != null && result.isSuccess()) {
|
||||
// code == 200
|
||||
return true;
|
||||
} else {
|
||||
// 取 msg:从 Map 里按 key 取
|
||||
String msg = (result != null)
|
||||
? String.valueOf(result.get(AjaxResult.MSG_TAG))
|
||||
: "响应为空";
|
||||
@@ -317,7 +351,7 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("WMS 调用异常 billNoCk={} error={}", billNoCk, e.getMessage(), e);
|
||||
log.error("WMS 调用异常 rkInfoIds={} error={}", rkInfoIds, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -329,8 +363,24 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DeliveryOrder> listByOrderNo(String orderNo) {
|
||||
return deliveryOrderMapper.selectByOrderNo(orderNo);
|
||||
public List<DeliveryOrderDetailVO> listByOrderNo(String orderNo) {
|
||||
|
||||
// 1. 直接查出带附件、带 makerUserName 的明细列表
|
||||
// 使用的是 resultMap="DeliveryOrderWithAttachResult"
|
||||
List<DeliveryOrder> list = deliveryOrderMapper.selectByOrderNo(orderNo);
|
||||
if (list == null || list.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 2. 转成 VO 列表(继承自 DeliveryOrder,自动带上 makerId / makerUserName / attachments 等所有字段)
|
||||
List<DeliveryOrderDetailVO> result = new ArrayList<>(list.size());
|
||||
for (DeliveryOrder o : list) {
|
||||
DeliveryOrderDetailVO vo = new DeliveryOrderDetailVO();
|
||||
BeanUtils.copyProperties(o, vo);
|
||||
result.add(vo);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -419,7 +469,6 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
@@ -143,7 +143,37 @@ public class MtdServiceImpl implements IMtdService
|
||||
|
||||
@Override
|
||||
public TotalWvVO calcTotalWv(CalcTotalWvDTO dto) {
|
||||
if (dto == null || dto.getWlNos() == null || dto.getWlNos().isEmpty()) {
|
||||
if (dto == null) {
|
||||
TotalWvVO empty = new TotalWvVO();
|
||||
empty.setTotalWeightKg(BigDecimal.ZERO);
|
||||
empty.setTotalVolumeM3(BigDecimal.ZERO);
|
||||
return empty;
|
||||
}
|
||||
|
||||
// 使用wlNos和items列表来统计物料
|
||||
Map<String, BigDecimal> wlNoQtyMap = new LinkedHashMap<>();
|
||||
|
||||
// 如果提供了items列表,优先使用items(包含具体数量)
|
||||
if (dto.getItems() != null && !dto.getItems().isEmpty()) {
|
||||
for (CalcTotalWvDTO.Item item : dto.getItems()) {
|
||||
if (item.getWlNo() != null && !item.getWlNo().trim().isEmpty()) {
|
||||
BigDecimal qty = item.getQty();
|
||||
if (qty == null || qty.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
qty = BigDecimal.ONE; // 默认为1
|
||||
}
|
||||
wlNoQtyMap.put(item.getWlNo(), wlNoQtyMap.getOrDefault(item.getWlNo(), BigDecimal.ZERO).add(qty));
|
||||
}
|
||||
}
|
||||
} else if (dto.getWlNos() != null && !dto.getWlNos().isEmpty()) {
|
||||
// 如果只有wlNos列表,则每个物料号计数为1
|
||||
for (String wlNo : dto.getWlNos()) {
|
||||
if (wlNo != null && !wlNo.trim().isEmpty()) {
|
||||
wlNoQtyMap.put(wlNo, wlNoQtyMap.getOrDefault(wlNo, BigDecimal.ZERO).add(BigDecimal.ONE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wlNoQtyMap.isEmpty()) {
|
||||
TotalWvVO empty = new TotalWvVO();
|
||||
empty.setTotalWeightKg(BigDecimal.ZERO);
|
||||
empty.setTotalVolumeM3(BigDecimal.ZERO);
|
||||
@@ -151,17 +181,20 @@ public class MtdServiceImpl implements IMtdService
|
||||
}
|
||||
|
||||
// 查询数据库中所有匹配的物料信息
|
||||
List<Mtd> list = mtdMapper.selectByWlNos(dto.getWlNos());
|
||||
List<Mtd> list = mtdMapper.selectByWlNos(new ArrayList<>(wlNoQtyMap.keySet()));
|
||||
|
||||
BigDecimal totalWeight = BigDecimal.ZERO;
|
||||
BigDecimal totalVolume = BigDecimal.ZERO;
|
||||
|
||||
for (Mtd m : list) {
|
||||
BigDecimal w = m.getWeightKg() == null ? BigDecimal.ZERO : m.getWeightKg();
|
||||
BigDecimal v = m.getVolumeM3() == null ? BigDecimal.ZERO : m.getVolumeM3();
|
||||
if (m.getWlNo() != null && wlNoQtyMap.containsKey(m.getWlNo())) {
|
||||
BigDecimal qty = wlNoQtyMap.get(m.getWlNo());
|
||||
BigDecimal w = m.getWeightKg() == null ? BigDecimal.ZERO : m.getWeightKg();
|
||||
BigDecimal v = m.getVolumeM3() == null ? BigDecimal.ZERO : m.getVolumeM3();
|
||||
|
||||
totalWeight = totalWeight.add(w);
|
||||
totalVolume = totalVolume.add(v);
|
||||
totalWeight = totalWeight.add(w.multiply(qty));
|
||||
totalVolume = totalVolume.add(v.multiply(qty));
|
||||
}
|
||||
}
|
||||
|
||||
TotalWvVO vo = new TotalWvVO();
|
||||
|
||||
@@ -100,6 +100,11 @@ public class VehicleTypeServiceImpl implements IVehicleTypeService
|
||||
return vehicleTypeMapper.deleteVehicleTypeById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算建议运费,推荐车型
|
||||
* @param dto
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public CalcSuggestFeeVO calcSuggestFee(CalcSuggestFeeDTO dto)
|
||||
{
|
||||
@@ -113,8 +118,55 @@ public class VehicleTypeServiceImpl implements IVehicleTypeService
|
||||
if (candidates == null || candidates.isEmpty()) {
|
||||
candidates = vehicleTypeMapper.selectFallbackTypes(w, v);
|
||||
}
|
||||
|
||||
// ========== 2) 检查是否存在适配车型 ==========
|
||||
if (candidates == null || candidates.isEmpty()) {
|
||||
throw new RuntimeException("未找到适配车型,请检查车型配置或输入参数。");
|
||||
// 如果没有适配车型,检查系统中是否至少存在一个车型配置
|
||||
List<VehicleType> allVehicleTypes = vehicleTypeMapper.selectVehicleTypeList(new VehicleType());
|
||||
if (allVehicleTypes == null || allVehicleTypes.isEmpty()) {
|
||||
CalcSuggestFeeVO vo = new CalcSuggestFeeVO();
|
||||
vo.setErrorMessage("系统中暂无车型配置,请先配置车型信息。");
|
||||
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);
|
||||
|
||||
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());
|
||||
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);
|
||||
}
|
||||
}
|
||||
vo.setCandidates(list);
|
||||
return vo;
|
||||
} else {
|
||||
CalcSuggestFeeVO vo = new CalcSuggestFeeVO();
|
||||
vo.setErrorMessage("未找到适配车型,请检查车型配置或输入参数。");
|
||||
vo.setHasSuitableType(false);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
||||
// 稳定排序:单价升序 -> 承重上限升序 -> 载方上限升序 -> id 升序(数据库已排序,这里再兜一层)
|
||||
@@ -124,7 +176,7 @@ public class VehicleTypeServiceImpl implements IVehicleTypeService
|
||||
.thenComparing(VehicleType::getVolumeMaxM3, Comparator.nullsLast(BigDecimal::compareTo))
|
||||
.thenComparing(VehicleType::getId, Comparator.nullsLast(Long::compareTo)));
|
||||
|
||||
// ========== 2) 选定车型:前端指定 or 系统推荐 ==========
|
||||
// ========== 3) 选定车型:前端指定 or 系统推荐 ==========
|
||||
VehicleType chosen;
|
||||
if (dto.getVehicleTypeId() != null) {
|
||||
// 前端点名车型:优先在候选中找,找不到则直接按主键查
|
||||
@@ -139,19 +191,20 @@ public class VehicleTypeServiceImpl implements IVehicleTypeService
|
||||
chosen = candidates.get(0);
|
||||
}
|
||||
|
||||
// ========== 3) 计算建议费用:单价 × 公里数,保留2位 ==========
|
||||
// ========== 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);
|
||||
|
||||
// ========== 4) 组装返回 ==========
|
||||
// ========== 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());
|
||||
@@ -170,6 +223,33 @@ public class VehicleTypeServiceImpl implements IVehicleTypeService
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建无匹配车型的错误信息
|
||||
*/
|
||||
private String buildNoMatchErrorMessage(BigDecimal weight, BigDecimal volume, VehicleType maxCapacityType) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("无适配车型,原因:");
|
||||
|
||||
boolean weightExceeds = weight.compareTo(maxCapacityType.getWeightMaxTon()) > 0;
|
||||
boolean volumeExceeds = volume.compareTo(maxCapacityType.getVolumeMaxM3()) > 0;
|
||||
|
||||
if (weightExceeds && volumeExceeds) {
|
||||
sb.append("货物重量(").append(weight).append("吨)超过最大承重(")
|
||||
.append(maxCapacityType.getWeightMaxTon()).append("吨),且货物体积(")
|
||||
.append(volume).append("立方米)超过最大载方(")
|
||||
.append(maxCapacityType.getVolumeMaxM3()).append("立方米)。");
|
||||
} else if (weightExceeds) {
|
||||
sb.append("货物重量(").append(weight).append("吨)超过最大承重(")
|
||||
.append(maxCapacityType.getWeightMaxTon()).append("吨)。");
|
||||
} else if (volumeExceeds) {
|
||||
sb.append("货物体积(").append(volume).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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.delivery.project.ocr.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "qwen")
|
||||
public class QwenProperties {
|
||||
|
||||
/**
|
||||
* 通义千问 API Key
|
||||
*/
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* OpenAI 兼容 base url,例如:
|
||||
* https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
*/
|
||||
private String baseUrl;
|
||||
|
||||
/**
|
||||
* 模型名称,例如:qwen-vl-ocr-latest
|
||||
*/
|
||||
private String model;
|
||||
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public void setApiKey(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
public String getBaseUrl() {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public void setModel(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.delivery.project.ocr.controller;
|
||||
|
||||
import com.delivery.project.ocr.service.QwenOcrService;
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/ocr")
|
||||
public class OcrController {
|
||||
|
||||
@Autowired
|
||||
private QwenOcrService qwenOcrService;
|
||||
|
||||
/**
|
||||
* 方式一:上传文件(本地测试)
|
||||
*/
|
||||
@PostMapping("/extractErp")
|
||||
public Map<String, Object> extractErpOrder(@RequestParam("file") MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "上传文件不能为空");
|
||||
}
|
||||
|
||||
String erpOrderNo = qwenOcrService.extractErpOrderNo(file);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", true);
|
||||
result.put("found", StringUtils.hasText(erpOrderNo));
|
||||
result.put("erpOrderNo", erpOrderNo);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 方式二:智慧实物管理系统调用 —— 直接传 Base64 图片
|
||||
*
|
||||
* JSON 示例:
|
||||
* {
|
||||
* "imageBase64": "data:image/jpeg;base64,xxxxxx"
|
||||
* }
|
||||
*/
|
||||
@PostMapping("/extractErpByBase64")
|
||||
public Map<String, Object> extractErpByBase64(@RequestBody OcrBase64Request request) {
|
||||
if (request == null || !StringUtils.hasText(request.getImageBase64())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "imageBase64 不能为空");
|
||||
}
|
||||
|
||||
String erpOrderNo = qwenOcrService.extractErpOrderNoFromBase64(request.getImageBase64());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", true);
|
||||
result.put("found", StringUtils.hasText(erpOrderNo));
|
||||
result.put("erpOrderNo", erpOrderNo);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收 Base64 JSON 的 DTO
|
||||
*/
|
||||
@Data
|
||||
public static class OcrBase64Request {
|
||||
private String imageBase64; // 支持带前缀的 data:image/jpeg;base64,xxxx
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package com.delivery.project.ocr.service;
|
||||
|
||||
import com.delivery.project.ocr.config.QwenProperties;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import okhttp3.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class QwenOcrService {
|
||||
|
||||
private final QwenProperties qwenProperties;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// OkHttp 可复用并加超时
|
||||
private final OkHttpClient httpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(25, TimeUnit.SECONDS)
|
||||
.writeTimeout(25, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* MultipartFile → OCR
|
||||
*/
|
||||
public String extractErpOrderNo(MultipartFile file) {
|
||||
try {
|
||||
byte[] bytes = file.getBytes();
|
||||
String fileName = file.getOriginalFilename();
|
||||
String contentType = file.getContentType();
|
||||
return doExtractErpOrderNo(bytes, fileName, contentType);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("读取上传图片失败:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 → OCR
|
||||
* 支持含前缀:data:image/jpeg;base64,xxxx
|
||||
* 也支持纯 Base64 字串
|
||||
*/
|
||||
public String extractErpOrderNoFromBase64(String base64Str) {
|
||||
|
||||
// 1. 去掉 data:image/jpeg;base64, 前缀
|
||||
String cleanBase64 = base64Str;
|
||||
String contentType = "image/jpeg";
|
||||
|
||||
if (base64Str.startsWith("data:")) {
|
||||
int commaIndex = base64Str.indexOf(",");
|
||||
String meta = base64Str.substring(5, commaIndex); // image/jpeg;base64
|
||||
cleanBase64 = base64Str.substring(commaIndex + 1);
|
||||
|
||||
int semiIndex = meta.indexOf(";");
|
||||
if (semiIndex > 0) {
|
||||
contentType = meta.substring(0, semiIndex);
|
||||
} else {
|
||||
contentType = meta;
|
||||
}
|
||||
}
|
||||
|
||||
byte[] bytes = Base64.getDecoder().decode(cleanBase64);
|
||||
|
||||
return doExtractErpOrderNo(bytes, "upload.jpg", contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心 OCR 调用逻辑
|
||||
*/
|
||||
private String doExtractErpOrderNo(byte[] bytes, String fileName, String contentType) {
|
||||
try {
|
||||
if (contentType == null || contentType.isEmpty()) {
|
||||
contentType = "image/jpeg";
|
||||
}
|
||||
|
||||
// bytes → base64 → data-url
|
||||
String base64 = Base64.getEncoder().encodeToString(bytes);
|
||||
String dataUrl = "data:" + contentType + ";base64," + base64;
|
||||
|
||||
// ------------ 拼接 Qwen 请求体 ------------
|
||||
|
||||
Map<String, Object> root = new HashMap<String, Object>();
|
||||
root.put("model", qwenProperties.getModel());
|
||||
|
||||
List<Map<String, Object>> messages = new ArrayList<Map<String, Object>>();
|
||||
Map<String, Object> message = new HashMap<String, Object>();
|
||||
message.put("role", "user");
|
||||
|
||||
List<Object> contentList = new ArrayList<Object>();
|
||||
|
||||
// 图片部分
|
||||
Map<String, Object> imagePart = new HashMap<String, Object>();
|
||||
imagePart.put("type", "image_url");
|
||||
Map<String, Object> imageUrlMap = new HashMap<String, Object>();
|
||||
imageUrlMap.put("url", dataUrl);
|
||||
imagePart.put("image_url", imageUrlMap);
|
||||
contentList.add(imagePart);
|
||||
|
||||
// 文本提示部分
|
||||
Map<String, Object> textPart = new HashMap<String, Object>();
|
||||
textPart.put("type", "text");
|
||||
textPart.put("text",
|
||||
"你是一个票据识别助手,请严格按以下要求识别:\n" +
|
||||
"1. 找到“采购订单号(ERP)”对应的数字。\n" +
|
||||
"2. 如果其下方是条形码,请返回条形码下面的数字。\n" +
|
||||
"3. 图片中可能同时存在 ERP 与“采购订单号(电商交易专区)”,必须忽略电商交易专区的编号。\n" +
|
||||
"4. 只返回数字,不要返回任何文字、标点或空格。\n" +
|
||||
"5. 如果找不到,请返回空字符串。"
|
||||
);
|
||||
contentList.add(textPart);
|
||||
|
||||
message.put("content", contentList);
|
||||
messages.add(message);
|
||||
root.put("messages", messages);
|
||||
|
||||
String json = objectMapper.writeValueAsString(root);
|
||||
|
||||
// ⚠ 这里参数顺序:先 MediaType 再 json(JDK8 + OkHttp3 正确用法)
|
||||
RequestBody body = RequestBody.create(MediaType.parse("application/json"), json);
|
||||
String url = qwenProperties.getBaseUrl() + "/chat/completions";
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer " + qwenProperties.getApiKey())
|
||||
.header("Content-Type", "application/json")
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
Response response = httpClient.newCall(request).execute();
|
||||
try {
|
||||
if (!response.isSuccessful()) {
|
||||
String errorBody = response.body() != null ? response.body().string() : "no error body";
|
||||
throw new RuntimeException("调用通义千问失败:" + response.code() + ",错误信息:" + errorBody);
|
||||
}
|
||||
|
||||
String resp = Objects.requireNonNull(response.body()).string();
|
||||
JsonNode rootNode = objectMapper.readTree(resp);
|
||||
JsonNode choices = rootNode.get("choices");
|
||||
if (choices == null || !choices.isArray() || choices.size() == 0) {
|
||||
return "";
|
||||
}
|
||||
String content = choices.get(0).get("message").get("content").asText("");
|
||||
return extractDigits(content);
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("调用通义千问异常:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 正则提取最像 ERP 的数字串
|
||||
*/
|
||||
private String extractDigits(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Matcher m = Pattern.compile("\\d+").matcher(text);
|
||||
List<String> nums = new ArrayList<String>();
|
||||
|
||||
while (m.find()) {
|
||||
nums.add(m.group());
|
||||
}
|
||||
if (nums.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 根据长度筛选一遍(可以根据你实际 ERP 号长度调整)
|
||||
for (String s : nums) {
|
||||
if (s.length() >= 8 && s.length() <= 12) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
// 兜底:返回第一段数字
|
||||
return nums.get(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user