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