From f1e7868ae19d80281d160d054ed342f9a3d7858b Mon Sep 17 00:00:00 2001 From: wenshijun Date: Fri, 5 Dec 2025 16:03:29 +0800 Subject: [PATCH] =?UTF-8?q?=E9=85=8D=E9=80=81=E7=B3=BB=E7=BB=9F=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=BC=80=E5=8F=911205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 51 +++- .../framework/config/SecurityConfig.java | 1 + .../controller/DeliveryOrderController.java | 4 +- .../controller/VehicleTypeController.java | 5 +- .../document/domain/DeliveryOrder.java | 62 ++++- .../domain/dto/CalcSuggestFeeDTO.java | 6 + .../domain/dto/DeliveryExecuteBindDTO.java | 9 + .../domain/dto/DeliveryOrderCreateDTO.java | 58 +++-- .../domain/dto/DeliveryOrderLineDTO.java | 43 +++- .../document/domain/vo/CalcSuggestFeeVO.java | 8 +- .../domain/vo/DeliveryOrderDetailVO.java | 21 ++ .../service/IDeliveryOrderService.java | 3 +- .../document/service/IVehicleTypeService.java | 2 +- .../impl/DeliveryAttachmentServiceImpl.java | 69 ++++-- .../impl/DeliveryOrderServiceImpl.java | 107 ++++++--- .../document/service/impl/MtdServiceImpl.java | 45 +++- .../service/impl/VehicleTypeServiceImpl.java | 88 ++++++- .../project/ocr/config/QwenProperties.java | 51 ++++ .../project/ocr/controller/OcrController.java | 70 ++++++ .../project/ocr/service/QwenOcrService.java | 187 +++++++++++++++ src/main/resources/application.yml | 29 ++- .../mybatis/document/DeliveryOrderMapper.xml | 226 ++++++++++++------ 22 files changed, 956 insertions(+), 189 deletions(-) create mode 100644 src/main/java/com/delivery/project/document/domain/vo/DeliveryOrderDetailVO.java create mode 100644 src/main/java/com/delivery/project/ocr/config/QwenProperties.java create mode 100644 src/main/java/com/delivery/project/ocr/controller/OcrController.java create mode 100644 src/main/java/com/delivery/project/ocr/service/QwenOcrService.java diff --git a/pom.xml b/pom.xml index 02d3c56..a55173a 100644 --- a/pom.xml +++ b/pom.xml @@ -34,12 +34,21 @@ 4.1.2 6.8.3 2.3 + 9.0.108 1.2.13 5.7.14 5.3.39 7.1.4 + + + 3.1.0 + + + UTF-8 + 1.8 + 1.8 @@ -180,7 +189,7 @@ - + io.swagger swagger-models @@ -239,12 +248,45 @@ + org.projectlombok lombok 1.18.38 + + + com.aliyun + ocr_api20210707 + ${aliyun.ocr.version} + + + + + com.google.zxing + core + 3.5.3 + + + com.google.zxing + javase + 3.5.3 + + + + + com.alibaba + dashscope-sdk-java + 2.16.2 + + + org.slf4j + slf4j-simple + + + + @@ -253,13 +295,6 @@ org.springframework.boot spring-boot-maven-plugin - - true diff --git a/src/main/java/com/delivery/framework/config/SecurityConfig.java b/src/main/java/com/delivery/framework/config/SecurityConfig.java index ffd3d48..1135314 100644 --- a/src/main/java/com/delivery/framework/config/SecurityConfig.java +++ b/src/main/java/com/delivery/framework/config/SecurityConfig.java @@ -118,6 +118,7 @@ public class SecurityConfig "/delivery/**", "/document/vehicle/**", "/document/type/**", + "/ocr/**", "/document/location/**", "/document/mtd/**", "/document/info/**", diff --git a/src/main/java/com/delivery/project/document/controller/DeliveryOrderController.java b/src/main/java/com/delivery/project/document/controller/DeliveryOrderController.java index 8c7ad59..68c6114 100644 --- a/src/main/java/com/delivery/project/document/controller/DeliveryOrderController.java +++ b/src/main/java/com/delivery/project/document/controller/DeliveryOrderController.java @@ -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) { diff --git a/src/main/java/com/delivery/project/document/controller/VehicleTypeController.java b/src/main/java/com/delivery/project/document/controller/VehicleTypeController.java index bc35374..9fc8353 100644 --- a/src/main/java/com/delivery/project/document/controller/VehicleTypeController.java +++ b/src/main/java/com/delivery/project/document/controller/VehicleTypeController.java @@ -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); } diff --git a/src/main/java/com/delivery/project/document/domain/DeliveryOrder.java b/src/main/java/com/delivery/project/document/domain/DeliveryOrder.java index 3beb8c8..3246a24 100644 --- a/src/main/java/com/delivery/project/document/domain/DeliveryOrder.java +++ b/src/main/java/com/delivery/project/document/domain/DeliveryOrder.java @@ -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 attachments; + /** 查询用:多状态筛选 */ private List 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 getAttachments() { return attachments; } public void setAttachments(List attachments) { this.attachments = attachments; } + public List getOrderStatusList() { return orderStatusList; } + public void setOrderStatusList(List 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 getOrderStatusList() { - return orderStatusList; - } - public void setOrderStatusList(List 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()) diff --git a/src/main/java/com/delivery/project/document/domain/dto/CalcSuggestFeeDTO.java b/src/main/java/com/delivery/project/document/domain/dto/CalcSuggestFeeDTO.java index 6e46a3a..69443aa 100644 --- a/src/main/java/com/delivery/project/document/domain/dto/CalcSuggestFeeDTO.java +++ b/src/main/java/com/delivery/project/document/domain/dto/CalcSuggestFeeDTO.java @@ -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(可选;传入则按该车型计算建议费用) */ diff --git a/src/main/java/com/delivery/project/document/domain/dto/DeliveryExecuteBindDTO.java b/src/main/java/com/delivery/project/document/domain/dto/DeliveryExecuteBindDTO.java index 7b9ebc2..529bd25 100644 --- a/src/main/java/com/delivery/project/document/domain/dto/DeliveryExecuteBindDTO.java +++ b/src/main/java/com/delivery/project/document/domain/dto/DeliveryExecuteBindDTO.java @@ -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 attachments; diff --git a/src/main/java/com/delivery/project/document/domain/dto/DeliveryOrderCreateDTO.java b/src/main/java/com/delivery/project/document/domain/dto/DeliveryOrderCreateDTO.java index f5624eb..684b65f 100644 --- a/src/main/java/com/delivery/project/document/domain/dto/DeliveryOrderCreateDTO.java +++ b/src/main/java/com/delivery/project/document/domain/dto/DeliveryOrderCreateDTO.java @@ -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; // ==================== 行明细(多物料行) ==================== diff --git a/src/main/java/com/delivery/project/document/domain/dto/DeliveryOrderLineDTO.java b/src/main/java/com/delivery/project/document/domain/dto/DeliveryOrderLineDTO.java index 4b26984..b9e06a9 100644 --- a/src/main/java/com/delivery/project/document/domain/dto/DeliveryOrderLineDTO.java +++ b/src/main/java/com/delivery/project/document/domain/dto/DeliveryOrderLineDTO.java @@ -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; } + diff --git a/src/main/java/com/delivery/project/document/domain/vo/CalcSuggestFeeVO.java b/src/main/java/com/delivery/project/document/domain/vo/CalcSuggestFeeVO.java index 8a7cb63..b46a1b1 100644 --- a/src/main/java/com/delivery/project/document/domain/vo/CalcSuggestFeeVO.java +++ b/src/main/java/com/delivery/project/document/domain/vo/CalcSuggestFeeVO.java @@ -24,6 +24,12 @@ public class CalcSuggestFeeVO { /** 建议费用(单位:元,四舍五入保留2位小数) */ private BigDecimal suggestFee; + /** 错误信息 */ + private String errorMessage; + + /** 是否有适配车型 */ + private Boolean hasSuitableType; + /** 候选车型列表(按价格/容量排序) */ private List candidates; @@ -51,7 +57,7 @@ public class CalcSuggestFeeVO { /** 载方下限(单位:立方米,含) */ private BigDecimal volumeMinM3; - /** 载方上限(单位:立方米,含) */ + /** 载方上限(单位:吨,含) */ private BigDecimal volumeMaxM3; } } diff --git a/src/main/java/com/delivery/project/document/domain/vo/DeliveryOrderDetailVO.java b/src/main/java/com/delivery/project/document/domain/vo/DeliveryOrderDetailVO.java new file mode 100644 index 0000000..90ac0cc --- /dev/null +++ b/src/main/java/com/delivery/project/document/domain/vo/DeliveryOrderDetailVO.java @@ -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; + +} diff --git a/src/main/java/com/delivery/project/document/service/IDeliveryOrderService.java b/src/main/java/com/delivery/project/document/service/IDeliveryOrderService.java index 1dc603c..4d95300 100644 --- a/src/main/java/com/delivery/project/document/service/IDeliveryOrderService.java +++ b/src/main/java/com/delivery/project/document/service/IDeliveryOrderService.java @@ -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 listByOrderNo(String orderNo); + List listByOrderNo(String orderNo); diff --git a/src/main/java/com/delivery/project/document/service/IVehicleTypeService.java b/src/main/java/com/delivery/project/document/service/IVehicleTypeService.java index 5c4a0c2..8b05e56 100644 --- a/src/main/java/com/delivery/project/document/service/IVehicleTypeService.java +++ b/src/main/java/com/delivery/project/document/service/IVehicleTypeService.java @@ -62,7 +62,7 @@ public interface IVehicleTypeService public int deleteVehicleTypeById(Long id); /** - * 计算建议运费 + * 计算建议运费,推荐车型 * @param dto * @return */ diff --git a/src/main/java/com/delivery/project/document/service/impl/DeliveryAttachmentServiceImpl.java b/src/main/java/com/delivery/project/document/service/impl/DeliveryAttachmentServiceImpl.java index 1593738..1ca76a0 100644 --- a/src/main/java/com/delivery/project/document/service/impl/DeliveryAttachmentServiceImpl.java +++ b/src/main/java/com/delivery/project/document/service/impl/DeliveryAttachmentServiceImpl.java @@ -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 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 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 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 rkInfoIdList, int isDelivery) { String url = wisdomBaseUrl + "/wisdom/stock/updateDeliveryStatus"; Map 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"; diff --git a/src/main/java/com/delivery/project/document/service/impl/DeliveryOrderServiceImpl.java b/src/main/java/com/delivery/project/document/service/impl/DeliveryOrderServiceImpl.java index cfd11e1..eefb625 100644 --- a/src/main/java/com/delivery/project/document/service/impl/DeliveryOrderServiceImpl.java +++ b/src/main/java/com/delivery/project/document/service/impl/DeliveryOrderServiceImpl.java @@ -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 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 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 rkInfoIds, int isDelivery) { String url = wisdomBaseUrl + "/wisdom/stock/updateDeliveryStatus"; Map 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 listByOrderNo(String orderNo) { - return deliveryOrderMapper.selectByOrderNo(orderNo); + public List listByOrderNo(String orderNo) { + + // 1. 直接查出带附件、带 makerUserName 的明细列表 + // 使用的是 resultMap="DeliveryOrderWithAttachResult" + List list = deliveryOrderMapper.selectByOrderNo(orderNo); + if (list == null || list.isEmpty()) { + return Collections.emptyList(); + } + + // 2. 转成 VO 列表(继承自 DeliveryOrder,自动带上 makerId / makerUserName / attachments 等所有字段) + List 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; } diff --git a/src/main/java/com/delivery/project/document/service/impl/MtdServiceImpl.java b/src/main/java/com/delivery/project/document/service/impl/MtdServiceImpl.java index b421cd1..739f331 100644 --- a/src/main/java/com/delivery/project/document/service/impl/MtdServiceImpl.java +++ b/src/main/java/com/delivery/project/document/service/impl/MtdServiceImpl.java @@ -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 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 list = mtdMapper.selectByWlNos(dto.getWlNos()); + List 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(); diff --git a/src/main/java/com/delivery/project/document/service/impl/VehicleTypeServiceImpl.java b/src/main/java/com/delivery/project/document/service/impl/VehicleTypeServiceImpl.java index 7258113..b8bb742 100644 --- a/src/main/java/com/delivery/project/document/service/impl/VehicleTypeServiceImpl.java +++ b/src/main/java/com/delivery/project/document/service/impl/VehicleTypeServiceImpl.java @@ -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 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 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 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; } diff --git a/src/main/java/com/delivery/project/ocr/config/QwenProperties.java b/src/main/java/com/delivery/project/ocr/config/QwenProperties.java new file mode 100644 index 0000000..1c3d94a --- /dev/null +++ b/src/main/java/com/delivery/project/ocr/config/QwenProperties.java @@ -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; + } +} diff --git a/src/main/java/com/delivery/project/ocr/controller/OcrController.java b/src/main/java/com/delivery/project/ocr/controller/OcrController.java new file mode 100644 index 0000000..73d57fe --- /dev/null +++ b/src/main/java/com/delivery/project/ocr/controller/OcrController.java @@ -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 extractErpOrder(@RequestParam("file") MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "上传文件不能为空"); + } + + String erpOrderNo = qwenOcrService.extractErpOrderNo(file); + + Map 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 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 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 + } +} diff --git a/src/main/java/com/delivery/project/ocr/service/QwenOcrService.java b/src/main/java/com/delivery/project/ocr/service/QwenOcrService.java new file mode 100644 index 0000000..2d02a03 --- /dev/null +++ b/src/main/java/com/delivery/project/ocr/service/QwenOcrService.java @@ -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 root = new HashMap(); + root.put("model", qwenProperties.getModel()); + + List> messages = new ArrayList>(); + Map message = new HashMap(); + message.put("role", "user"); + + List contentList = new ArrayList(); + + // 图片部分 + Map imagePart = new HashMap(); + imagePart.put("type", "image_url"); + Map imageUrlMap = new HashMap(); + imageUrlMap.put("url", dataUrl); + imagePart.put("image_url", imageUrlMap); + contentList.add(imagePart); + + // 文本提示部分 + Map textPart = new HashMap(); + 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 nums = new ArrayList(); + + 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); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 75777c1..2fa9780 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -52,8 +52,8 @@ spring: active: druid servlet: multipart: - max-file-size: 10MB - max-request-size: 20MB + max-file-size: 20MB + max-request-size: 50MB devtools: restart: enabled: false @@ -128,3 +128,28 @@ upload: wisdom: base-url: http://192.168.1.251:8086 + +# =============== OCR 服务核心配置 =============== +ocr: + enabled: true + + # 阿里云OCR配置 + aliyun: + + access-key: LTAI5tMi6fjhiQfDdzNpWavx + secret-key: PpDsoh11EptdQXcFDm9HkTB4Us4cd7 + + # 阿里云文档“服务接入点”里的正式 OCR endpoint + endpoint: ocr-api.cn-hangzhou.aliyuncs.com + region-id: cn-hangzhou + + # 服务类型:通用文字识别 RecognizeGeneral + service-type: general + + # 通义千问VL模型配置 +qwen: + # 重要:请替换为有效的通义千问API密钥 + # 注意:生产环境中应通过环境变量等方式配置,避免暴露API密钥 + api-key: "sk-3dfb3f7b049d4010bddaff3ceee08f94" + base-url: "https://dashscope.aliyuncs.com/compatible-mode/v1" + model: "qwen-vl-ocr-latest" diff --git a/src/main/resources/mybatis/document/DeliveryOrderMapper.xml b/src/main/resources/mybatis/document/DeliveryOrderMapper.xml index 657ef44..c7291a9 100644 --- a/src/main/resources/mybatis/document/DeliveryOrderMapper.xml +++ b/src/main/resources/mybatis/document/DeliveryOrderMapper.xml @@ -11,6 +11,16 @@ + + + + + + + + + + @@ -31,7 +41,7 @@ - + @@ -96,58 +106,78 @@ select - dor.id, dor.order_no, - dor.xm_ms, dor.xm_no, dor.wl_no, dor.wl_ms, dor.real_qty, dor.dw, dor.sap_no, dor.gys_mc, - dor.remark, dor.origin_name, dor.origin_lng, dor.origin_lat, - dor.dest_name, dor.dest_lng, dor.dest_lat, - dor.delivery_date, dor.plate_no, dor.order_status, - dor.driver_name, dor.driver_phone, + dor.id, + dor.order_no, + dor.bill_no_ck, + dor.rk_info_id, + dor.maker_id, + -- 制单人用户名(连表 sys_user) + su.user_name AS maker_user_name, + dor.receive_status, + dor.receive_problem, + dor.xm_ms, dor.xm_no, dor.wl_no, dor.wl_ms, dor.real_qty, dor.dw, dor.sap_no, dor.gys_mc, + dor.remark, + dor.origin_name, dor.origin_lng, dor.origin_lat, + dor.dest_name, dor.dest_lng, dor.dest_lat, + dor.delivery_date, dor.plate_no, dor.order_status, + dor.driver_name, dor.driver_phone, + dor.shipper_name, dor.shipper_phone, + dor.receiver_name, dor.receiver_phone, dor.receiver_org_name, + dor.delivery_ton, + dor.goods_size, + -- 车型外键与名称 + dor.vehicle_type_id, dor.vehicle_type_name, + -- 费用/里程 + dor.suggest_fee, dor.actual_fee, dor.toll_fee, dor.total_km, + dor.create_by, dor.create_time, dor.update_by, dor.update_time, dor.is_delete, - dor.shipper_name, dor.shipper_phone, - dor.receiver_name, dor.receiver_phone, dor.receiver_org_name, - dor.delivery_ton, - dor.goods_size, - -- 车型外键与名称 - dor.vehicle_type_id, dor.vehicle_type_name, - -- 费用/里程 - dor.suggest_fee, dor.actual_fee, dor.toll_fee, dor.total_km, - dor.create_by, dor.create_time, dor.update_by, dor.update_time, dor.is_delete, - - -- 附件列(att_ 前缀) - da.id AS att_id, - da.order_no AS att_order_no, - da.scene AS att_scene, - da.biz_type AS att_biz_type, - da.url AS att_url, - da.status AS att_status, - da.sort_no AS att_sort_no, - da.remark AS att_remark, - da.create_by AS att_create_by, - da.create_time AS att_create_time, - da.update_by AS att_update_by, - da.update_time AS att_update_time, - da.is_delete AS att_is_delete + -- 附件列(att_ 前缀) + da.id AS att_id, + da.order_no AS att_order_no, + da.scene AS att_scene, + da.biz_type AS att_biz_type, + da.url AS att_url, + da.status AS att_status, + da.sort_no AS att_sort_no, + da.remark AS att_remark, + da.create_by AS att_create_by, + da.create_time AS att_create_time, + da.update_by AS att_update_by, + da.update_time AS att_update_time, + da.is_delete AS att_is_delete from delivery_order dor - left join delivery_attachment da - on da.order_no = dor.order_no - and da.is_delete = '0' + left join delivery_attachment da + on da.order_no = dor.order_no + and da.is_delete = '0' + + left join sys_user su + on su.user_id = dor.maker_id - select id, order_no, xm_ms, xm_no, wl_no, wl_ms, real_qty, dw, sap_no, gys_mc, remark, - origin_name, origin_lng, origin_lat, dest_name, dest_lng, dest_lat, - delivery_date, plate_no, - -- 司机 - driver_name, driver_phone, - shipper_name, shipper_phone, receiver_name, - receiver_phone, receiver_org_name, delivery_ton, goods_size, order_status, - -- 车型外键与名称 - vehicle_type_id, vehicle_type_name, - -- 费用/里程 - suggest_fee, actual_fee, toll_fee, total_km, - create_by, create_time, update_by, update_time, is_delete + select + id, + order_no, + bill_no_ck, + rk_info_id, + maker_id, + receive_status, + receive_problem, + xm_ms, xm_no, wl_no, wl_ms, real_qty, dw, sap_no, gys_mc, remark, + origin_name, origin_lng, origin_lat, + dest_name, dest_lng, dest_lat, + delivery_date, plate_no, + -- 司机 + driver_name, driver_phone, + shipper_name, shipper_phone, receiver_name, + receiver_phone, receiver_org_name, delivery_ton, goods_size, order_status, + -- 车型外键与名称 + vehicle_type_id, vehicle_type_name, + -- 费用/里程 + suggest_fee, actual_fee, toll_fee, total_km, + create_by, create_time, update_by, update_time, is_delete from delivery_order @@ -166,6 +196,14 @@ + + + and dor.maker_id = #{makerId} + + + and dor.receive_status = #{receiveStatus} + + and dor.xm_ms = #{xmMs} and dor.xm_no = #{xmNo} and dor.wl_no = #{wlNo} @@ -175,10 +213,14 @@ and dor.sap_no = #{sapNo} and dor.gys_mc = #{gysMc} - and dor.origin_name like concat('%', #{originName}, '%') + + and dor.origin_name like concat('%', #{originName}, '%') + and dor.origin_lng = #{originLng} and dor.origin_lat = #{originLat} - and dor.dest_name like concat('%', #{destName}, '%') + + and dor.dest_name like concat('%', #{destName}, '%') + and dor.dest_lng = #{destLng} and dor.dest_lat = #{destLat} @@ -186,18 +228,34 @@ and dor.plate_no = #{plateNo} - and dor.driver_name like concat('%', #{driverName}, '%') - and dor.driver_phone = #{driverPhone} + + and dor.driver_name like concat('%', #{driverName}, '%') + + + and dor.driver_phone = #{driverPhone} + - and dor.shipper_name like concat('%', #{shipperName}, '%') - and dor.shipper_phone = #{shipperPhone} - and dor.receiver_name like concat('%', #{receiverName}, '%') - and dor.receiver_phone = #{receiverPhone} - and dor.receiver_org_name like concat('%', #{receiverOrgName}, '%') + + and dor.shipper_name like concat('%', #{shipperName}, '%') + + + and dor.shipper_phone = #{shipperPhone} + + + and dor.receiver_name like concat('%', #{receiverName}, '%') + + + and dor.receiver_phone = #{receiverPhone} + + + and dor.receiver_org_name like concat('%', #{receiverOrgName}, '%') + and dor.delivery_ton = #{deliveryTon} and dor.goods_size = #{goodsSize} - and dor.order_status = #{orderStatus} + + and dor.order_status = #{orderStatus} + and dor.vehicle_type_id = #{vehicleTypeId} @@ -250,7 +308,7 @@ - + AND dor.order_status = #{orderStatus} @@ -267,21 +325,23 @@ ORDER BY MAX(dor.create_time) DESC - - + + WHERE dor.order_no = #{orderNo} + AND (dor.is_delete = '0' OR dor.is_delete = 0 OR dor.is_delete IS NULL) + ORDER BY dor.id - + @@ -289,10 +349,18 @@ - + insert into delivery_order order_no, + bill_no_ck, + rk_info_id, + maker_id, + receive_status, + receive_problem, + xm_ms, xm_no, wl_no, @@ -339,6 +407,12 @@ #{orderNo,jdbcType=VARCHAR}, + #{billNoCk}, + #{rkInfoId}, + #{makerId}, + #{receiveStatus}, + #{receiveProblem}, + #{xmMs}, #{xmNo}, #{wlNo}, @@ -388,7 +462,7 @@ INSERT INTO delivery_order - (order_no, bill_no_ck, + (order_no, bill_no_ck, rk_info_id, maker_id, receive_status, receive_problem, xm_ms, xm_no, wl_no, wl_ms, real_qty, dw, sap_no, gys_mc, remark, origin_name, origin_lng, origin_lat, dest_name, dest_lng, dest_lat, @@ -404,6 +478,10 @@ ( #{it.orderNo}, #{it.billNoCk}, + #{it.rkInfoId}, + #{it.makerId}, + #{it.receiveStatus}, + #{it.receiveProblem}, #{it.xmMs}, #{it.xmNo}, #{it.wlNo}, #{it.wlMs}, #{it.realQty}, #{it.dw}, #{it.sapNo}, #{it.gysMc}, #{it.remark}, @@ -424,11 +502,17 @@ - - + UPDATE delivery_order + bill_no_ck = #{billNoCk}, + rk_info_id = #{rkInfoId}, + maker_id = #{makerId}, + receive_status = #{receiveStatus}, + receive_problem = #{receiveProblem}, + xm_ms = #{xmMs}, xm_no = #{xmNo}, wl_no = #{wlNo}, @@ -479,11 +563,11 @@ - + delete from delivery_order where id = #{id} - + delete from delivery_order where id in #{id}