配送系统功能开发1205

This commit is contained in:
2025-12-05 16:03:29 +08:00
parent aa53f26d63
commit f1e7868ae1
22 changed files with 956 additions and 189 deletions

View File

@@ -118,6 +118,7 @@ public class SecurityConfig
"/delivery/**",
"/document/vehicle/**",
"/document/type/**",
"/ocr/**",
"/document/location/**",
"/document/mtd/**",
"/document/info/**",

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -25,6 +25,25 @@ public class DeliveryOrder extends BaseEntity {
@Excel(name = "配送单据号")
private String orderNo;
/** rk_info主键ID */
@Excel(name = "rk_info主键ID")
private Long rkInfoId;
/** 制单人用户ID */
@Excel(name = "制单人ID")
private Long makerId;
/** 制单人用户名(不入库,用于返回给前端) */
private String makerUserName;
/** 接收物资状态1数量齐全状态完好 2存在问题 */
@Excel(name = "接收物资状态", readConverterExp = "1=数量齐全状态完好,2=存在问题")
private Integer receiveStatus; // *** 修改:从 String 改为 Integer ***
/** 接收物资问题描述 */
@Excel(name = "存在问题描述")
private String receiveProblem;
/** 出库单据号 */
@Excel(name = "出库单据号")
private String billNoCk;
@@ -148,9 +167,11 @@ public class DeliveryOrder extends BaseEntity {
/** 连表查询用:附件列表 */
private List<DeliveryAttachment> attachments;
/** 查询用:多状态筛选 */
private List<String> orderStatusList;
// ===================== 费用与里程 =====================
/** 建议费用(按车型单价*里程的推荐值) */
@Excel(name = "建议费用")
private BigDecimal suggestFee;
@@ -174,8 +195,33 @@ public class DeliveryOrder extends BaseEntity {
public String getOrderNo() { return orderNo; }
public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
public Long getRkInfoId() { return rkInfoId; }
public void setRkInfoId(Long rkInfoId) { this.rkInfoId = rkInfoId; }
public String getMakerUserName() {
return makerUserName;
}
public void setMakerUserName(String makerUserName) {
this.makerUserName = makerUserName;
}
public Long getMakerId() { return makerId; }
public void setMakerId(Long makerId) { this.makerId = makerId; }
public Integer getReceiveStatus() { // *** 修改 ***
return receiveStatus;
}
public void setReceiveStatus(Integer receiveStatus) { // *** 修改 ***
this.receiveStatus = receiveStatus;
}
public String getReceiveProblem() { return receiveProblem; }
public void setReceiveProblem(String receiveProblem) { this.receiveProblem = receiveProblem; }
public String getBillNoCk() { return billNoCk; }
public void setBillNoCk(String billNoCk) { this.billNoCk = billNoCk; }
public String getXmMs() { return xmMs; }
public void setXmMs(String xmMs) { this.xmMs = xmMs; }
@@ -266,6 +312,9 @@ public class DeliveryOrder extends BaseEntity {
public List<DeliveryAttachment> getAttachments() { return attachments; }
public void setAttachments(List<DeliveryAttachment> attachments) { this.attachments = attachments; }
public List<String> getOrderStatusList() { return orderStatusList; }
public void setOrderStatusList(List<String> orderStatusList) { this.orderStatusList = orderStatusList; }
public BigDecimal getSuggestFee() { return suggestFee; }
public void setSuggestFee(BigDecimal suggestFee) { this.suggestFee = suggestFee; }
@@ -278,19 +327,18 @@ public class DeliveryOrder extends BaseEntity {
public BigDecimal getTotalKm() { return totalKm; }
public void setTotalKm(BigDecimal totalKm) { this.totalKm = totalKm; }
public List<String> getOrderStatusList() {
return orderStatusList;
}
public void setOrderStatusList(List<String> orderStatusList) {
this.orderStatusList = orderStatusList;
}
// ===================== toString =====================
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("orderNo", getOrderNo())
.append("rkInfoId", getRkInfoId())
.append("makerId", getMakerId())
.append("makerUserName", getMakerUserName())
.append("receiveStatus", getReceiveStatus()) // *** 类型已是 Integer ***
.append("receiveProblem", getReceiveProblem())
.append("billNoCk", getBillNoCk())
.append("xmMs", getXmMs())
.append("xmNo", getXmNo())

View File

@@ -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可选传入则按该车型计算建议费用 */

View File

@@ -31,6 +31,15 @@ public class DeliveryExecuteBindDTO {
private BigDecimal actualFee; // 实际费用
private BigDecimal tollFee; // 高速费用
/**
* 接收物资状态:
* 0 待确认1 齐全完好2 存在问题
*/
private Integer receiveStatus;
/** 当 receiveStatus = 2 时,必须填写问题描述 */
private String receiveProblem;
/** 本次执行的附件条目URL 列表) */
@NotNull
private List<DeliveryAttachItemDTO> attachments;

View File

@@ -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;
// ==================== 行明细(多物料行) ====================

View File

@@ -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;
}

View File

@@ -24,6 +24,12 @@ public class CalcSuggestFeeVO {
/** 建议费用单位四舍五入保留2位小数 */
private BigDecimal suggestFee;
/** 错误信息 */
private String errorMessage;
/** 是否有适配车型 */
private Boolean hasSuitableType;
/** 候选车型列表(按价格/容量排序) */
private List<VehicleTypeOptionVO> candidates;
@@ -51,7 +57,7 @@ public class CalcSuggestFeeVO {
/** 载方下限(单位:立方米,含) */
private BigDecimal volumeMinM3;
/** 载方上限(单位:立方米,含) */
/** 载方上限(单位:,含) */
private BigDecimal volumeMaxM3;
}
}

View File

@@ -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;
}

View File

@@ -5,6 +5,7 @@ import com.delivery.project.document.domain.DeliveryOrder;
import com.delivery.project.document.domain.dto.DeliveryOrderCreateDTO;
import com.delivery.project.document.domain.dto.DeliveryOrderSaveDTO;
import com.delivery.project.document.domain.vo.DeliveryBillVO;
import com.delivery.project.document.domain.vo.DeliveryOrderDetailVO;
import com.delivery.project.document.domain.vo.DeliveryOrderGroupVO;
/**
@@ -92,7 +93,7 @@ public interface IDeliveryOrderService
* @return
*/
/** 详情:按单号查行 */
List<DeliveryOrder> listByOrderNo(String orderNo);
List<DeliveryOrderDetailVO> listByOrderNo(String orderNo);

View File

@@ -62,7 +62,7 @@ public interface IVehicleTypeService
public int deleteVehicleTypeById(Long id);
/**
* 计算建议运费
* 计算建议运费,推荐车型
* @param dto
* @return
*/

View File

@@ -7,6 +7,7 @@ import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
import com.alibaba.fastjson2.JSON;
import com.delivery.common.exception.ServiceException;
@@ -139,25 +140,32 @@ public class DeliveryAttachmentServiceImpl implements IDeliveryAttachmentService
@Override
@Transactional(rollbackFor = Exception.class)
public int executeBind(DeliveryExecuteBindDTO dto) {
// 1) 校验订单存在
// 1) 校验订单存在(一个单号多行)
List<DeliveryOrder> existList = deliveryOrderMapper.selectDeliveryOrderByOrderNo(dto.getOrderNo());
if (existList == null || existList.isEmpty()) {
throw new ServiceException("配送单不存在:" + dto.getOrderNo());
}
String billNoCk = existList.get(0).getBillNoCk();
// ====== 从配送单中拿到所有 rk_info_id 列表 ======
List<Long> rkInfoIdList = existList.stream()
.map(DeliveryOrder::getRkInfoId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (StringUtils.isBlank(billNoCk)) {
throw new ServiceException("配送单未绑定出库单号,无法回写库存状态!");
if (rkInfoIdList.isEmpty()) {
throw new ServiceException("配送单未绑定 rk_info_id,无法回写库存状态!");
}
// ====================================================
if (dto.getAttachments() == null || dto.getAttachments().isEmpty()) {
throw new ServiceException("附件列表不能为空");
}
// 2) 批量插入附件(只传 URL
List<DeliveryAttachment> list = new ArrayList<>();
String username = getUsername();
// String username = "大爷的!!!";
String username = getUsername();
for (DeliveryAttachItemDTO it : dto.getAttachments()) {
if (it == null) {
continue;
@@ -215,24 +223,36 @@ public class DeliveryAttachmentServiceImpl implements IDeliveryAttachmentService
patch.setTollFee(dto.getTollFee());
}
Integer receiveStatus = dto.getReceiveStatus();
if (receiveStatus == null) {
throw new ServiceException("完成配送时必须选择接收物资状态");
}
if (receiveStatus != 0 && receiveStatus != 1 && receiveStatus != 2) {
throw new ServiceException("接收物资状态不合法");
}
patch.setReceiveStatus(receiveStatus);
if (receiveStatus == 2) {
// 有问题必须写说明
if (StringUtils.isBlank(dto.getReceiveProblem())) {
throw new ServiceException("存在问题时必须填写问题描述");
}
patch.setReceiveProblem(dto.getReceiveProblem());
}
patch.setOrderStatus("3"); // 已完成
}
deliveryOrderMapper.updateDeliveryOrder(patch);
// 4) ⭐ 如果是 DEST 场景,远程调用 WMS把 rk_info.is_delivery 改成 3
// 4) ⭐ 如果是 DEST 场景,远程调用 WMS这些 rk_info 记录的 is_delivery 改成 3
if ("DEST".equals(scene)) {
if (StringUtils.isBlank(billNoCk)) {
throw new ServiceException("配送单缺少对应的出库单号 billNoCk无法回写库存状态");
}
// rkInfoMapper.updateDeliveryStatus(billNoCk, 3);
boolean ok = updateWmsIsDelivery(billNoCk, 3);
// 这里已经不再按 billNoCk 整单更新,而是按 rk_info_id 列表更新
boolean ok = updateWmsIsDeliveryByIds(rkInfoIdList, 3);
if (!ok) {
// 让整个事务回滚,附件 + 配送单状态都撤回
throw new ServiceException("回写 WMS 配送状态失败,出库单号" + billNoCk);
throw new ServiceException("回写 WMS 配送状态失败,rk_info_id 列表" + rkInfoIdList);
}
}
@@ -240,24 +260,28 @@ public class DeliveryAttachmentServiceImpl implements IDeliveryAttachmentService
}
/**
* 远程调用智慧实物管理系统,更新 rk_info.is_delivery 状态
* 远程调用智慧实物管理系统, rk_info 主键ID列表更新 is_delivery 状态
* 约定请求体结构:
* {
* "rkInfoIdList": [1, 2, 3],
* "isDelivery": 3
* }
*/
private boolean updateWmsIsDelivery(String billNoCk, int isDelivery) {
private boolean updateWmsIsDeliveryByIds(List<Long> rkInfoIdList, int isDelivery) {
String url = wisdomBaseUrl + "/wisdom/stock/updateDeliveryStatus";
Map<String, Object> body = new HashMap<>();
body.put("billNoCk", billNoCk);
body.put("ids", rkInfoIdList);
body.put("isDelivery", isDelivery);
String json = JSON.toJSONString(body);
try {
// 用你项目里的 HttpUtils 发 JSON POST
String resp = HttpUtils.sendPost(url, json, MediaType.APPLICATION_JSON_VALUE);
if (StringUtils.isBlank(resp)) {
log.error("WMS 更新失败响应为空url={} billNoCk={}", url, billNoCk);
log.error("WMS 更新失败响应为空url={} rkInfoIdList={}", url, rkInfoIdList);
return false;
}
@@ -269,16 +293,17 @@ public class DeliveryAttachmentServiceImpl implements IDeliveryAttachmentService
String msg = (result == null)
? "响应为空"
: String.valueOf(result.get(AjaxResult.MSG_TAG));
log.error("WMS 更新失败,billNoCk={},原因={}", billNoCk, msg);
log.error("WMS 更新失败,rkInfoIdList={},原因={}", rkInfoIdList, msg);
return false;
}
} catch (Exception e) {
log.error("调用 WMS 接口异常,billNoCk={}error={}", billNoCk, e.getMessage(), e);
log.error("调用 WMS 接口异常,rkInfoIdList={}error={}", rkInfoIdList, e.getMessage(), e);
return false;
}
}
// 保存目录 D:\delivery
private static final String BASE_PATH = "D:/delivery";

View File

@@ -20,6 +20,7 @@ import com.delivery.project.document.domain.dto.DeliveryOrderCreateDTO;
import com.delivery.project.document.domain.dto.DeliveryOrderLineDTO;
import com.delivery.project.document.domain.dto.DeliveryOrderSaveDTO;
import com.delivery.project.document.domain.vo.DeliveryBillVO;
import com.delivery.project.document.domain.vo.DeliveryOrderDetailVO;
import com.delivery.project.document.domain.vo.DeliveryOrderGroupVO;
import com.delivery.project.document.mapper.DeliveryAttachmentMapper;
import com.delivery.project.document.mapper.MtdMapper;
@@ -51,8 +52,6 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
@Autowired
private DeliveryAttachmentMapper deliveryAttachmentMapper;
@Autowired
private RkInfoMapper rkInfoMapper;
@Autowired
private MtdMapper mtdMapper;
@@ -185,34 +184,67 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
public String createOrder(DeliveryOrderCreateDTO dto) {
// ========== 0. 参数校验 ==========
if (dto == null || dto.getItems() == null || dto.getItems().isEmpty()) {
throw new ServiceException("物料明细不能为空");
}
// 所有明细应是同一出库单据号,这里简单从第一条取出
// 取第一条,仍然要求同一出库单据号(暂不支持多出库单合并)
String billNoCk = dto.getItems().get(0).getBillNoCk();
if (billNoCk == null || billNoCk.trim().isEmpty()) {
if (StringUtils.isBlank(billNoCk)) {
throw new ServiceException("明细行缺少出库单据号 billNoCk");
}
// 收集 rk_info 主键 id用于回写 WMS
List<Long> rkInfoIds = dto.getItems().stream()
.map(DeliveryOrderLineDTO::getRkInfoId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (rkInfoIds.isEmpty()) {
throw new ServiceException("明细行缺少 rk_info 主键 id无法回写配送状态");
}
// ========== 1. 生成配送单号 ==========
String orderNo = (dto.getOrderNo() == null || dto.getOrderNo().trim().isEmpty())
String orderNo = StringUtils.isBlank(dto.getOrderNo())
? "DO" + DateUtils.dateTimeNow("yyyyMMddHHmmssSSS")
: dto.getOrderNo().trim();
Date now = DateUtils.getNowDate();
String username = SecurityUtils.getUsername();
Long currentUserId = null;
try {
currentUserId = SecurityUtils.getUserId();
} catch (Exception e) {
log.warn("获取当前登录用户ID失败{}", e.getMessage());
}
// 制单人用户ID优先用前端传的 makerId没有则回退到当前登录用户
Long makerId = dto.getMakerId();
if (makerId == null) {
if (currentUserId == null) {
throw new ServiceException("无法确定制单人用户ID请重新登录后重试");
}
makerId = currentUserId;
}
List<DeliveryOrder> rows = new ArrayList<>();
// ========== 2. 遍历每一条物料明细 ==========
// ========== 2. 遍历每一条物料明细,组装行记录 ==========
for (DeliveryOrderLineDTO it : dto.getItems()) {
// 这里如果允许多个出库单合并配送,可以去掉这个校验
// 仍然限制同一出库单(你后续要支持多出库单再放开)
if (!billNoCk.equals(it.getBillNoCk())) {
throw new ServiceException("当前接口暂不支持多张出库单合并配送,请确保所有明细 billNoCk 相同");
}
if (it.getRkInfoId() == null) {
throw new ServiceException("明细行缺少 rk_info 主键 id");
}
DeliveryOrder row = new DeliveryOrder();
// 2.1 公共头部字段(整单一致)
@@ -225,6 +257,7 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
row.setDestLat(dto.getDestLat());
row.setDeliveryDate(dto.getDeliveryDate());
row.setPlateNo(dto.getPlateNo());
row.setShipperName(dto.getShipperName());
row.setShipperPhone(dto.getShipperPhone());
row.setReceiverName(dto.getReceiverName());
@@ -232,9 +265,15 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
row.setReceiverOrgName(dto.getReceiverOrgName());
row.setDeliveryTon(dto.getDeliveryTon());
row.setGoodsSize(dto.getGoodsSize());
row.setOrderStatus(
StringUtils.isBlank(dto.getOrderStatus()) ? "1" : dto.getOrderStatus().trim()
);
// 制单人用户ID
row.setMakerId(makerId);
// 配送状态:前端不传时默认 1已接单 / 待起运)
String orderStatus = StringUtils.isBlank(dto.getOrderStatus())
? "1" : dto.getOrderStatus().trim();
row.setOrderStatus(orderStatus);
row.setVehicleTypeId(dto.getVehicleTypeId());
row.setVehicleTypeName(dto.getVehicleTypeName());
row.setSuggestFee(dto.getSuggestFee());
@@ -244,6 +283,7 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
row.setRemark(dto.getRemark());
// 2.2 明细字段
row.setRkInfoId(it.getRkInfoId()); // 关联 rk_info 主键ID
row.setBillNoCk(it.getBillNoCk());
row.setXmMs(it.getXmMs());
row.setXmNo(it.getXmNo());
@@ -263,17 +303,16 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
}
// ========== 3. 批量落库 ==========
if (!rows.isEmpty()) {
deliveryOrderMapper.batchInsert(rows);
}
// rkInfoMapper.updateDeliveryStatus(billNoCk, 2);
// ========== 4. 回写 WMS rk_info.id 更新 is_delivery = 2配送中 ==========
// ========== 4. 回写 WMSrk_info.is_delivery = 2配送中 ==========
// 按出库单据号整单回写
boolean ok = updateWmsIsDelivery(billNoCk, 2);
boolean ok = updateWmsIsDeliveryByIds(rkInfoIds, 2);
if (!ok) {
// 这里直接抛异常让当前事务回滚避免出现“配送单已生成WMS 状态没改”的脏数据
// 回写失败,整单回滚,避免脏数据
throw new ServiceException("已生成配送单,但回写 WMS 配送状态失败");
}
@@ -281,34 +320,29 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
}
/**
* 远程调用 WMS出库单据号修改 rk_info.is_delivery 状态
* 远程调用 WMS按 rk_info 主键 ID 列表修改 is_delivery 状态
*
* 请求方式示例:
* POST ${delivery.wms-base-url}/delivery/rkInfo/updateDeliveryStatus
* Body: { "billNoCk": "CK202511200001", "isDelivery": 2 }
* POST ${delivery.wisdom-base-url}/wisdom/stock/updateDeliveryStatus
* Body: { "ids": [1, 2, 3], "isDelivery": 2 }
*/
private boolean updateWmsIsDelivery(String billNoCk, int isDelivery) {
private boolean updateWmsIsDeliveryByIds(List<Long> rkInfoIds, int isDelivery) {
String url = wisdomBaseUrl + "/wisdom/stock/updateDeliveryStatus";
Map<String, Object> map = new HashMap<>();
map.put("billNoCk", billNoCk);
map.put("ids", rkInfoIds);
map.put("isDelivery", isDelivery);
String json = JSON.toJSONString(map);
try {
// 发送 JSON POST你刚加的 sendJsonPost
String resp = HttpUtils.sendJsonPost(url, json);
// 解析为 AjaxResult
AjaxResult result = JSON.parseObject(resp, AjaxResult.class);
if (result != null && result.isSuccess()) {
// code == 200
return true;
} else {
// 取 msg从 Map 里按 key 取
String msg = (result != null)
? String.valueOf(result.get(AjaxResult.MSG_TAG))
: "响应为空";
@@ -317,7 +351,7 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
}
} catch (Exception e) {
log.error("WMS 调用异常 billNoCk={} error={}", billNoCk, e.getMessage(), e);
log.error("WMS 调用异常 rkInfoIds={} error={}", rkInfoIds, e.getMessage(), e);
return false;
}
}
@@ -329,8 +363,24 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
}
@Override
public List<DeliveryOrder> listByOrderNo(String orderNo) {
return deliveryOrderMapper.selectByOrderNo(orderNo);
public List<DeliveryOrderDetailVO> listByOrderNo(String orderNo) {
// 1. 直接查出带附件、带 makerUserName 的明细列表
// 使用的是 resultMap="DeliveryOrderWithAttachResult"
List<DeliveryOrder> list = deliveryOrderMapper.selectByOrderNo(orderNo);
if (list == null || list.isEmpty()) {
return Collections.emptyList();
}
// 2. 转成 VO 列表(继承自 DeliveryOrder自动带上 makerId / makerUserName / attachments 等所有字段)
List<DeliveryOrderDetailVO> result = new ArrayList<>(list.size());
for (DeliveryOrder o : list) {
DeliveryOrderDetailVO vo = new DeliveryOrderDetailVO();
BeanUtils.copyProperties(o, vo);
result.add(vo);
}
return result;
}
/**
@@ -419,7 +469,6 @@ public class DeliveryOrderServiceImpl implements IDeliveryOrderService
}
}
}
return list;
}

View File

@@ -143,7 +143,37 @@ public class MtdServiceImpl implements IMtdService
@Override
public TotalWvVO calcTotalWv(CalcTotalWvDTO dto) {
if (dto == null || dto.getWlNos() == null || dto.getWlNos().isEmpty()) {
if (dto == null) {
TotalWvVO empty = new TotalWvVO();
empty.setTotalWeightKg(BigDecimal.ZERO);
empty.setTotalVolumeM3(BigDecimal.ZERO);
return empty;
}
// 使用wlNos和items列表来统计物料
Map<String, BigDecimal> wlNoQtyMap = new LinkedHashMap<>();
// 如果提供了items列表优先使用items包含具体数量
if (dto.getItems() != null && !dto.getItems().isEmpty()) {
for (CalcTotalWvDTO.Item item : dto.getItems()) {
if (item.getWlNo() != null && !item.getWlNo().trim().isEmpty()) {
BigDecimal qty = item.getQty();
if (qty == null || qty.compareTo(BigDecimal.ZERO) <= 0) {
qty = BigDecimal.ONE; // 默认为1
}
wlNoQtyMap.put(item.getWlNo(), wlNoQtyMap.getOrDefault(item.getWlNo(), BigDecimal.ZERO).add(qty));
}
}
} else if (dto.getWlNos() != null && !dto.getWlNos().isEmpty()) {
// 如果只有wlNos列表则每个物料号计数为1
for (String wlNo : dto.getWlNos()) {
if (wlNo != null && !wlNo.trim().isEmpty()) {
wlNoQtyMap.put(wlNo, wlNoQtyMap.getOrDefault(wlNo, BigDecimal.ZERO).add(BigDecimal.ONE));
}
}
}
if (wlNoQtyMap.isEmpty()) {
TotalWvVO empty = new TotalWvVO();
empty.setTotalWeightKg(BigDecimal.ZERO);
empty.setTotalVolumeM3(BigDecimal.ZERO);
@@ -151,17 +181,20 @@ public class MtdServiceImpl implements IMtdService
}
// 查询数据库中所有匹配的物料信息
List<Mtd> list = mtdMapper.selectByWlNos(dto.getWlNos());
List<Mtd> list = mtdMapper.selectByWlNos(new ArrayList<>(wlNoQtyMap.keySet()));
BigDecimal totalWeight = BigDecimal.ZERO;
BigDecimal totalVolume = BigDecimal.ZERO;
for (Mtd m : list) {
BigDecimal w = m.getWeightKg() == null ? BigDecimal.ZERO : m.getWeightKg();
BigDecimal v = m.getVolumeM3() == null ? BigDecimal.ZERO : m.getVolumeM3();
if (m.getWlNo() != null && wlNoQtyMap.containsKey(m.getWlNo())) {
BigDecimal qty = wlNoQtyMap.get(m.getWlNo());
BigDecimal w = m.getWeightKg() == null ? BigDecimal.ZERO : m.getWeightKg();
BigDecimal v = m.getVolumeM3() == null ? BigDecimal.ZERO : m.getVolumeM3();
totalWeight = totalWeight.add(w);
totalVolume = totalVolume.add(v);
totalWeight = totalWeight.add(w.multiply(qty));
totalVolume = totalVolume.add(v.multiply(qty));
}
}
TotalWvVO vo = new TotalWvVO();

View File

@@ -100,6 +100,11 @@ public class VehicleTypeServiceImpl implements IVehicleTypeService
return vehicleTypeMapper.deleteVehicleTypeById(id);
}
/**
* 计算建议运费,推荐车型
* @param dto
* @return
*/
@Override
public CalcSuggestFeeVO calcSuggestFee(CalcSuggestFeeDTO dto)
{
@@ -113,8 +118,55 @@ public class VehicleTypeServiceImpl implements IVehicleTypeService
if (candidates == null || candidates.isEmpty()) {
candidates = vehicleTypeMapper.selectFallbackTypes(w, v);
}
// ========== 2) 检查是否存在适配车型 ==========
if (candidates == null || candidates.isEmpty()) {
throw new RuntimeException("未找到适配车型,检查车型配置或输入参数。");
// 如果没有适配车型,检查系统中是否至少存在一个车型配置
List<VehicleType> allVehicleTypes = vehicleTypeMapper.selectVehicleTypeList(new VehicleType());
if (allVehicleTypes == null || allVehicleTypes.isEmpty()) {
CalcSuggestFeeVO vo = new CalcSuggestFeeVO();
vo.setErrorMessage("系统中暂无车型配置,请先配置车型信息。");
vo.setHasSuitableType(false);
return vo;
}
// 找到最大承载能力的车型
VehicleType maxCapacityType = allVehicleTypes.stream()
.filter(type -> "0".equals(type.getStatus()) && !"1".equals(type.getIsDelete()))
.max(Comparator
.comparing(VehicleType::getWeightMaxTon, Comparator.nullsLast(BigDecimal::compareTo))
.thenComparing(VehicleType::getVolumeMaxM3, Comparator.nullsLast(BigDecimal::compareTo)))
.orElse(null);
if (maxCapacityType != null) {
// 检查最大容量车型是否仍无法满足要求
String errorMsg = buildNoMatchErrorMessage(w, v, maxCapacityType);
CalcSuggestFeeVO vo = new CalcSuggestFeeVO();
vo.setErrorMessage(errorMsg);
vo.setHasSuitableType(false);
// 仍然返回候选车型列表供参考
List<CalcSuggestFeeVO.VehicleTypeOptionVO> list = new ArrayList<>(allVehicleTypes.size());
for (VehicleType t : allVehicleTypes) {
if ("0".equals(t.getStatus()) && !"1".equals(t.getIsDelete())) {
CalcSuggestFeeVO.VehicleTypeOptionVO o = new CalcSuggestFeeVO.VehicleTypeOptionVO();
o.setId(t.getId());
o.setName(t.getTypeName());
o.setUnitPricePerKm(t.getUnitPricePerKm());
o.setWeightMinTon(t.getWeightMinTon());
o.setWeightMaxTon(t.getWeightMaxTon());
o.setVolumeMinM3(t.getVolumeMinM3());
o.setVolumeMaxM3(t.getVolumeMaxM3());
list.add(o);
}
}
vo.setCandidates(list);
return vo;
} else {
CalcSuggestFeeVO vo = new CalcSuggestFeeVO();
vo.setErrorMessage("未找到适配车型,请检查车型配置或输入参数。");
vo.setHasSuitableType(false);
return vo;
}
}
// 稳定排序:单价升序 -> 承重上限升序 -> 载方上限升序 -> id 升序(数据库已排序,这里再兜一层)
@@ -124,7 +176,7 @@ public class VehicleTypeServiceImpl implements IVehicleTypeService
.thenComparing(VehicleType::getVolumeMaxM3, Comparator.nullsLast(BigDecimal::compareTo))
.thenComparing(VehicleType::getId, Comparator.nullsLast(Long::compareTo)));
// ========== 2) 选定车型:前端指定 or 系统推荐 ==========
// ========== 3) 选定车型:前端指定 or 系统推荐 ==========
VehicleType chosen;
if (dto.getVehicleTypeId() != null) {
// 前端点名车型:优先在候选中找,找不到则直接按主键查
@@ -139,19 +191,20 @@ public class VehicleTypeServiceImpl implements IVehicleTypeService
chosen = candidates.get(0);
}
// ========== 3) 计算建议费用:单价 × 公里数保留2位 ==========
// ========== 4) 计算建议费用:单价 × 公里数保留2位 ==========
BigDecimal price = chosen.getUnitPricePerKm();
if (price == null) {
throw new RuntimeException("车型【" + chosen.getTypeName() + "】未配置每公里单价unit_price_per_km 为空)");
}
BigDecimal fee = price.multiply(km).setScale(2, RoundingMode.HALF_UP);
// ========== 4) 组装返回 ==========
// ========== 5) 组装返回 ==========
CalcSuggestFeeVO vo = new CalcSuggestFeeVO();
vo.setVehicleTypeId(chosen.getId());
vo.setVehicleTypeName(chosen.getTypeName());
vo.setUnitPricePerKm(price);
vo.setSuggestFee(fee);
vo.setHasSuitableType(true); // 有适配车型
// 候选项
List<CalcSuggestFeeVO.VehicleTypeOptionVO> list = new ArrayList<>(candidates.size());
@@ -170,6 +223,33 @@ public class VehicleTypeServiceImpl implements IVehicleTypeService
return vo;
}
/**
* 构建无匹配车型的错误信息
*/
private String buildNoMatchErrorMessage(BigDecimal weight, BigDecimal volume, VehicleType maxCapacityType) {
StringBuilder sb = new StringBuilder();
sb.append("无适配车型,原因:");
boolean weightExceeds = weight.compareTo(maxCapacityType.getWeightMaxTon()) > 0;
boolean volumeExceeds = volume.compareTo(maxCapacityType.getVolumeMaxM3()) > 0;
if (weightExceeds && volumeExceeds) {
sb.append("货物重量(").append(weight).append("吨)超过最大承重(")
.append(maxCapacityType.getWeightMaxTon()).append("吨),且货物体积(")
.append(volume).append("立方米)超过最大载方(")
.append(maxCapacityType.getVolumeMaxM3()).append("立方米)。");
} else if (weightExceeds) {
sb.append("货物重量(").append(weight).append("吨)超过最大承重(")
.append(maxCapacityType.getWeightMaxTon()).append("吨)。");
} else if (volumeExceeds) {
sb.append("货物体积(").append(volume).append("立方米)超过最大载方(")
.append(maxCapacityType.getVolumeMaxM3()).append("立方米)。");
}
sb.append("最大承载车型为:").append(maxCapacityType.getTypeName());
return sb.toString();
}
private static BigDecimal nvl(BigDecimal x) {
return x == null ? BigDecimal.ZERO : x;
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,70 @@
package com.delivery.project.ocr.controller;
import com.delivery.project.ocr.service.QwenOcrService;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/ocr")
public class OcrController {
@Autowired
private QwenOcrService qwenOcrService;
/**
* 方式一:上传文件(本地测试)
*/
@PostMapping("/extractErp")
public Map<String, Object> extractErpOrder(@RequestParam("file") MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "上传文件不能为空");
}
String erpOrderNo = qwenOcrService.extractErpOrderNo(file);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("found", StringUtils.hasText(erpOrderNo));
result.put("erpOrderNo", erpOrderNo);
return result;
}
/**
* 方式二:智慧实物管理系统调用 —— 直接传 Base64 图片
*
* JSON 示例:
* {
* "imageBase64": "data:image/jpeg;base64,xxxxxx"
* }
*/
@PostMapping("/extractErpByBase64")
public Map<String, Object> extractErpByBase64(@RequestBody OcrBase64Request request) {
if (request == null || !StringUtils.hasText(request.getImageBase64())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "imageBase64 不能为空");
}
String erpOrderNo = qwenOcrService.extractErpOrderNoFromBase64(request.getImageBase64());
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("found", StringUtils.hasText(erpOrderNo));
result.put("erpOrderNo", erpOrderNo);
return result;
}
/**
* 接收 Base64 JSON 的 DTO
*/
@Data
public static class OcrBase64Request {
private String imageBase64; // 支持带前缀的 data:image/jpeg;base64,xxxx
}
}

View File

@@ -0,0 +1,187 @@
package com.delivery.project.ocr.service;
import com.delivery.project.ocr.config.QwenProperties;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import okhttp3.*;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
@RequiredArgsConstructor
public class QwenOcrService {
private final QwenProperties qwenProperties;
private final ObjectMapper objectMapper = new ObjectMapper();
// OkHttp 可复用并加超时
private final OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(25, TimeUnit.SECONDS)
.writeTimeout(25, TimeUnit.SECONDS)
.build();
/**
* MultipartFile → OCR
*/
public String extractErpOrderNo(MultipartFile file) {
try {
byte[] bytes = file.getBytes();
String fileName = file.getOriginalFilename();
String contentType = file.getContentType();
return doExtractErpOrderNo(bytes, fileName, contentType);
} catch (IOException e) {
throw new RuntimeException("读取上传图片失败:" + e.getMessage(), e);
}
}
/**
* Base64 → OCR
* 支持含前缀data:image/jpeg;base64,xxxx
* 也支持纯 Base64 字串
*/
public String extractErpOrderNoFromBase64(String base64Str) {
// 1. 去掉 data:image/jpeg;base64, 前缀
String cleanBase64 = base64Str;
String contentType = "image/jpeg";
if (base64Str.startsWith("data:")) {
int commaIndex = base64Str.indexOf(",");
String meta = base64Str.substring(5, commaIndex); // image/jpeg;base64
cleanBase64 = base64Str.substring(commaIndex + 1);
int semiIndex = meta.indexOf(";");
if (semiIndex > 0) {
contentType = meta.substring(0, semiIndex);
} else {
contentType = meta;
}
}
byte[] bytes = Base64.getDecoder().decode(cleanBase64);
return doExtractErpOrderNo(bytes, "upload.jpg", contentType);
}
/**
* 核心 OCR 调用逻辑
*/
private String doExtractErpOrderNo(byte[] bytes, String fileName, String contentType) {
try {
if (contentType == null || contentType.isEmpty()) {
contentType = "image/jpeg";
}
// bytes → base64 → data-url
String base64 = Base64.getEncoder().encodeToString(bytes);
String dataUrl = "data:" + contentType + ";base64," + base64;
// ------------ 拼接 Qwen 请求体 ------------
Map<String, Object> root = new HashMap<String, Object>();
root.put("model", qwenProperties.getModel());
List<Map<String, Object>> messages = new ArrayList<Map<String, Object>>();
Map<String, Object> message = new HashMap<String, Object>();
message.put("role", "user");
List<Object> contentList = new ArrayList<Object>();
// 图片部分
Map<String, Object> imagePart = new HashMap<String, Object>();
imagePart.put("type", "image_url");
Map<String, Object> imageUrlMap = new HashMap<String, Object>();
imageUrlMap.put("url", dataUrl);
imagePart.put("image_url", imageUrlMap);
contentList.add(imagePart);
// 文本提示部分
Map<String, Object> textPart = new HashMap<String, Object>();
textPart.put("type", "text");
textPart.put("text",
"你是一个票据识别助手,请严格按以下要求识别:\n" +
"1. 找到“采购订单号(ERP)”对应的数字。\n" +
"2. 如果其下方是条形码,请返回条形码下面的数字。\n" +
"3. 图片中可能同时存在 ERP 与“采购订单号(电商交易专区)”,必须忽略电商交易专区的编号。\n" +
"4. 只返回数字,不要返回任何文字、标点或空格。\n" +
"5. 如果找不到,请返回空字符串。"
);
contentList.add(textPart);
message.put("content", contentList);
messages.add(message);
root.put("messages", messages);
String json = objectMapper.writeValueAsString(root);
// ⚠ 这里参数顺序:先 MediaType 再 jsonJDK8 + OkHttp3 正确用法)
RequestBody body = RequestBody.create(MediaType.parse("application/json"), json);
String url = qwenProperties.getBaseUrl() + "/chat/completions";
Request request = new Request.Builder()
.url(url)
.header("Authorization", "Bearer " + qwenProperties.getApiKey())
.header("Content-Type", "application/json")
.post(body)
.build();
Response response = httpClient.newCall(request).execute();
try {
if (!response.isSuccessful()) {
String errorBody = response.body() != null ? response.body().string() : "no error body";
throw new RuntimeException("调用通义千问失败:" + response.code() + ",错误信息:" + errorBody);
}
String resp = Objects.requireNonNull(response.body()).string();
JsonNode rootNode = objectMapper.readTree(resp);
JsonNode choices = rootNode.get("choices");
if (choices == null || !choices.isArray() || choices.size() == 0) {
return "";
}
String content = choices.get(0).get("message").get("content").asText("");
return extractDigits(content);
} finally {
response.close();
}
} catch (Exception e) {
throw new RuntimeException("调用通义千问异常:" + e.getMessage(), e);
}
}
/**
* 正则提取最像 ERP 的数字串
*/
private String extractDigits(String text) {
if (text == null || text.isEmpty()) {
return "";
}
Matcher m = Pattern.compile("\\d+").matcher(text);
List<String> nums = new ArrayList<String>();
while (m.find()) {
nums.add(m.group());
}
if (nums.isEmpty()) {
return "";
}
// 根据长度筛选一遍(可以根据你实际 ERP 号长度调整)
for (String s : nums) {
if (s.length() >= 8 && s.length() <= 12) {
return s;
}
}
// 兜底:返回第一段数字
return nums.get(0);
}
}