Преглед изворни кода

订单发货单列表以及所有接口操作日志修改

Pengxy пре 3 месеци
родитељ
комит
ec298619e8

+ 5 - 5
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/LogRecordConstants.java

@@ -36,7 +36,7 @@ public interface LogRecordConstants {
     String ORDER_SHIPMENT_CREATE_SUB_TYPE = "创建发货单";
     String ORDER_SHIPMENT_CREATE_SUB_TYPE = "创建发货单";
     String ORDER_SHIPMENT_CREATE_SUCCESS = "创建了发货单【{{#shipment.id}}】";
     String ORDER_SHIPMENT_CREATE_SUCCESS = "创建了发货单【{{#shipment.id}}】";
     String ORDER_SHIPMENT_UPDATE_SUB_TYPE = "更新发货单";
     String ORDER_SHIPMENT_UPDATE_SUB_TYPE = "更新发货单";
-    String ORDER_SHIPMENT_UPDATE_SUCCESS = "更新了发货单【{{#shipment.id}}】: {_DIFF{#updateReqVO}}";
+    String ORDER_SHIPMENT_UPDATE_SUCCESS = "更新了发货单【{{#shipment.id}}】: {_DIFF{#updateReqVO}}{{#itemsChangeLog}}";
     String ORDER_SHIPMENT_DELETE_SUB_TYPE = "删除发货单";
     String ORDER_SHIPMENT_DELETE_SUB_TYPE = "删除发货单";
     String ORDER_SHIPMENT_DELETE_SUCCESS = "删除了发货单【{{#shipment.id}}】";
     String ORDER_SHIPMENT_DELETE_SUCCESS = "删除了发货单【{{#shipment.id}}】";
 
 
@@ -50,11 +50,11 @@ public interface LogRecordConstants {
 
 
     String SHIPPING_PLAN_TYPE = "ORDER 出货计划";
     String SHIPPING_PLAN_TYPE = "ORDER 出货计划";
     String SHIPPING_PLAN_CREATE_SUB_TYPE = "创建出货计划";
     String SHIPPING_PLAN_CREATE_SUB_TYPE = "创建出货计划";
-    String SHIPPING_PLAN_CREATE_SUCCESS = "创建了出货计划【{{#shippingPlan.recId}}】";
+    String SHIPPING_PLAN_CREATE_SUCCESS = "创建了出货计划【{{#shippingPlan.lotSerial}}】";
     String SHIPPING_PLAN_UPDATE_SUB_TYPE = "更新出货计划";
     String SHIPPING_PLAN_UPDATE_SUB_TYPE = "更新出货计划";
-    String SHIPPING_PLAN_UPDATE_SUCCESS = "更新了出货计划【{{#shippingPlan.recId}}】: {_DIFF{#updateReqVO}}";
+    String SHIPPING_PLAN_UPDATE_SUCCESS = "更新了出货计划【{{#shippingPlan.lotSerial}}】: {_DIFF{#updateReqVO}}{{#itemsChangeLog}}";
     String SHIPPING_PLAN_DELETE_SUB_TYPE = "删除出货计划";
     String SHIPPING_PLAN_DELETE_SUB_TYPE = "删除出货计划";
-    String SHIPPING_PLAN_DELETE_SUCCESS = "删除了出货计划【{{#shippingPlan.recId}}】";
+    String SHIPPING_PLAN_DELETE_SUCCESS = "删除了出货计划【{{#shippingPlan.lotSerial}}】";
 
 
     // ======================= SALES_ORDER 销售订单 =======================
     // ======================= SALES_ORDER 销售订单 =======================
 
 
@@ -62,7 +62,7 @@ public interface LogRecordConstants {
     String SALES_ORDER_CREATE_SUB_TYPE = "创建销售订单";
     String SALES_ORDER_CREATE_SUB_TYPE = "创建销售订单";
     String SALES_ORDER_CREATE_SUCCESS = "创建了销售订单【{{#salesOrder.billNo}}】";
     String SALES_ORDER_CREATE_SUCCESS = "创建了销售订单【{{#salesOrder.billNo}}】";
     String SALES_ORDER_UPDATE_SUB_TYPE = "更新销售订单";
     String SALES_ORDER_UPDATE_SUB_TYPE = "更新销售订单";
-    String SALES_ORDER_UPDATE_SUCCESS = "更新了销售订单【{{#salesOrder.billNo}}】: {_DIFF{#updateReqVO}}";
+    String SALES_ORDER_UPDATE_SUCCESS = "更新了销售订单【{{#salesOrder.billNo}}】: {_DIFF{#updateReqVO}}{{#itemsChangeLog}}";
     String SALES_ORDER_DELETE_SUB_TYPE = "删除销售订单";
     String SALES_ORDER_DELETE_SUB_TYPE = "删除销售订单";
     String SALES_ORDER_DELETE_SUCCESS = "删除了销售订单,ID:{{#ids}}";
     String SALES_ORDER_DELETE_SUCCESS = "删除了销售订单,ID:{{#ids}}";
 
 

+ 1 - 1
yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/controller/admin/delivery/vo/ShipmentRespVO.java

@@ -24,7 +24,7 @@ public class ShipmentRespVO {
     private LocalDateTime shipDate;
     private LocalDateTime shipDate;
 
 
     @Schema(description = "发货时间")
     @Schema(description = "发货时间")
-    private String shipTime;
+    private Integer shipTime;
 
 
     @Schema(description = "状态")
     @Schema(description = "状态")
     private String status;
     private String status;

+ 1 - 1
yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/controller/admin/delivery/vo/ShipmentSaveReqVO.java

@@ -30,7 +30,7 @@ public class ShipmentSaveReqVO {
 
 
     @Schema(description = "发货时间")
     @Schema(description = "发货时间")
     @DiffLogField(name = "发货时间")
     @DiffLogField(name = "发货时间")
-    private String shipTime;
+    private Integer shipTime;
 
 
     @Schema(description = "状态")
     @Schema(description = "状态")
     @DiffLogField(name = "状态")
     @DiffLogField(name = "状态")

+ 1 - 1
yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/dal/dataobject/ShipmentMasterDO.java

@@ -32,7 +32,7 @@ public class ShipmentMasterDO {
     private LocalDateTime shipDate;
     private LocalDateTime shipDate;
 
 
     @TableField("ShipTime")
     @TableField("ShipTime")
-    private String shipTime;
+    private Integer shipTime;
 
 
     @TableField("Status")
     @TableField("Status")
     private String status;
     private String status;

+ 118 - 2
yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/service/impl/OrderDeliveryServiceImpl.java

@@ -183,6 +183,25 @@ public class OrderDeliveryServiceImpl implements OrderDeliveryService {
             success = SHIPPING_PLAN_UPDATE_SUCCESS)
             success = SHIPPING_PLAN_UPDATE_SUCCESS)
     public void updateShippingPlan(ShippingPlanSaveReqVO updateReqVO) {
     public void updateShippingPlan(ShippingPlanSaveReqVO updateReqVO) {
         ShippingPlanDO exists = validateShippingPlanExists(updateReqVO.getRecId());
         ShippingPlanDO exists = validateShippingPlanExists(updateReqVO.getRecId());
+        
+        // 查询旧的明细数据,用于日志对比
+        List<ShippingPlanDetailDO> oldDetails = shippingPlanDetailMapper.selectDetailsByPlanId(exists.getRecId());
+        // 旧数据(不含items,避免框架自动对比列表)
+        ShippingPlanSaveReqVO oldData = BeanUtils.toBean(exists, ShippingPlanSaveReqVO.class);
+        // 统一日期格式:将LocalDateTime转为yyyy-MM-dd字符串
+        if (exists.getShippingDate() != null) {
+            oldData.setShippingDate(exists.getShippingDate().toLocalDate().toString());
+        }
+        oldData.setItems(null);
+        
+        // 新数据副本(不含items,用于主表DIFF对比)
+        ShippingPlanSaveReqVO newDataForDiff = BeanUtils.toBean(updateReqVO, ShippingPlanSaveReqVO.class);
+        // 统一日期格式:只保留yyyy-MM-dd部分
+        if (newDataForDiff.getShippingDate() != null && newDataForDiff.getShippingDate().length() > 10) {
+            newDataForDiff.setShippingDate(newDataForDiff.getShippingDate().substring(0, 10));
+        }
+        newDataForDiff.setItems(null);
+        
         ShippingPlanDO update = OrderDeliveryConvert.INSTANCE.convert(updateReqVO);
         ShippingPlanDO update = OrderDeliveryConvert.INSTANCE.convert(updateReqVO);
         update.setRecId(exists.getRecId());
         update.setRecId(exists.getRecId());
         update.setUpdateTime(LocalDateTime.now());
         update.setUpdateTime(LocalDateTime.now());
@@ -200,9 +219,106 @@ public class OrderDeliveryServiceImpl implements OrderDeliveryService {
         shippingPlanDetailMapper.deleteByPlanId(exists.getRecId());
         shippingPlanDetailMapper.deleteByPlanId(exists.getRecId());
         saveDetails(update, updateReqVO.getItems());
         saveDetails(update, updateReqVO.getItems());
 
 
-        // 记录操作日志上下文
-        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(exists, ShippingPlanSaveReqVO.class));
+        // 手动生成明细变更日志(按recId匹配)
+        String itemsChangeLog = buildShippingPlanItemsChangeLog(oldDetails, updateReqVO.getItems());
+
+        // 记录操作日志上下文(使用不含items的副本进行DIFF)
+        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, oldData);
+        LogRecordContext.putVariable("updateReqVO", newDataForDiff);
         LogRecordContext.putVariable("shippingPlan", exists);
         LogRecordContext.putVariable("shippingPlan", exists);
+        LogRecordContext.putVariable("itemsChangeLog", itemsChangeLog);
+    }
+
+    /**
+     * 构建出货计划明细变更日志(按recId匹配新旧数据)
+     */
+    private String buildShippingPlanItemsChangeLog(List<ShippingPlanDetailDO> oldDetails, 
+            List<ShippingPlanSaveReqVO.ShippingPlanDetailSaveReqVO> newItems) {
+        StringBuilder log = new StringBuilder();
+        if (CollUtil.isEmpty(newItems)) {
+            newItems = List.of();
+        }
+        
+        // 构建旧数据Map(按recId)
+        java.util.Map<Long, ShippingPlanDetailDO> oldMap = oldDetails.stream()
+                .filter(e -> e.getRecId() != null)
+                .collect(java.util.stream.Collectors.toMap(ShippingPlanDetailDO::getRecId, e -> e));
+        
+        // 构建新数据Map(按recId)
+        java.util.Map<Long, ShippingPlanSaveReqVO.ShippingPlanDetailSaveReqVO> newMap = newItems.stream()
+                .filter(e -> e.getRecId() != null)
+                .collect(java.util.stream.Collectors.toMap(ShippingPlanSaveReqVO.ShippingPlanDetailSaveReqVO::getRecId, e -> e));
+        
+        // 1. 检查更新的明细(recId相同)
+        for (ShippingPlanSaveReqVO.ShippingPlanDetailSaveReqVO newItem : newItems) {
+            if (newItem.getRecId() != null && oldMap.containsKey(newItem.getRecId())) {
+                ShippingPlanDetailDO oldItem = oldMap.get(newItem.getRecId());
+                String changes = compareShippingPlanDetail(oldItem, newItem);
+                if (!changes.isEmpty()) {
+                    log.append(";更新了明细行【").append(oldItem.getItemNum()).append("】: ").append(changes);
+                }
+            }
+        }
+        
+        // 2. 检查新增的明细(recId为空或recId不在旧数据中)
+        for (ShippingPlanSaveReqVO.ShippingPlanDetailSaveReqVO newItem : newItems) {
+            if (newItem.getRecId() == null || !oldMap.containsKey(newItem.getRecId())) {
+                log.append(";新增了明细行【").append(newItem.getItemNum()).append("】");
+            }
+        }
+        
+        // 3. 检查删除的明细(旧recId不在新数据中)
+        for (ShippingPlanDetailDO oldItem : oldDetails) {
+            if (oldItem.getRecId() != null && !newMap.containsKey(oldItem.getRecId())) {
+                log.append(";删除了明细行【").append(oldItem.getItemNum()).append("】");
+            }
+        }
+        
+        return log.toString();
+    }
+
+    /**
+     * 对比单条出货计划明细的变更
+     */
+    private String compareShippingPlanDetail(ShippingPlanDetailDO oldItem, 
+            ShippingPlanSaveReqVO.ShippingPlanDetailSaveReqVO newItem) {
+        StringBuilder changes = new StringBuilder();
+        
+        // 使用compareTo比较BigDecimal,避免精度差异导致误判
+        if (!compareBigDecimal(oldItem.getQty(), newItem.getQty())) {
+            changes.append("【订单数量】从【").append(formatBigDecimal(oldItem.getQty())).append("】修改为【").append(formatBigDecimal(newItem.getQty())).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getItemName(), newItem.getItemName())) {
+            changes.append("【物料名称】从【").append(oldItem.getItemName()).append("】修改为【").append(newItem.getItemName()).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getSpecification(), newItem.getSpecification())) {
+            changes.append("【规格型号】从【").append(oldItem.getSpecification()).append("】修改为【").append(newItem.getSpecification()).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getRemark(), newItem.getRemark())) {
+            changes.append("【备注】从【").append(oldItem.getRemark()).append("】修改为【").append(newItem.getRemark()).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getStatus(), newItem.getStatus())) {
+            changes.append("【状态】从【").append(oldItem.getStatus()).append("】修改为【").append(newItem.getStatus()).append("】;");
+        }
+        
+        return changes.toString();
+    }
+    
+    /**
+     * 比较两个BigDecimal是否相等(忽略精度差异)
+     */
+    private boolean compareBigDecimal(java.math.BigDecimal a, java.math.BigDecimal b) {
+        if (a == null && b == null) return true;
+        if (a == null || b == null) return false;
+        return a.compareTo(b) == 0;
+    }
+    
+    /**
+     * 格式化BigDecimal,去除尾部多余的0
+     */
+    private String formatBigDecimal(java.math.BigDecimal value) {
+        if (value == null) return "空";
+        return value.stripTrailingZeros().toPlainString();
     }
     }
 
 
     @Override
     @Override

+ 99 - 2
yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/service/impl/OrderServiceImpl.java

@@ -79,15 +79,112 @@ public class OrderServiceImpl implements OrderService {
             success = SALES_ORDER_UPDATE_SUCCESS)
             success = SALES_ORDER_UPDATE_SUCCESS)
     public void updateOrder(OrderSaveReqVO updateReqVO) {
     public void updateOrder(OrderSaveReqVO updateReqVO) {
         OrderDO exists = validateExists(updateReqVO.getId());
         OrderDO exists = validateExists(updateReqVO.getId());
+        
+        // 查询旧的明细数据,用于日志对比
+        List<OrderEntryDO> oldEntries = orderEntryMapper.selectListByOrderId(exists.getId());
+        // 旧数据(不含items,避免框架自动对比列表)
+        OrderSaveReqVO oldData = BeanUtils.toBean(exists, OrderSaveReqVO.class);
+        oldData.setItems(null);
+        
+        // 新数据副本(不含items,用于主表DIFF对比)
+        OrderSaveReqVO newDataForDiff = BeanUtils.toBean(updateReqVO, OrderSaveReqVO.class);
+        newDataForDiff.setItems(null);
+        
         OrderDO update = OrderConvert.INSTANCE.convert(updateReqVO);
         OrderDO update = OrderConvert.INSTANCE.convert(updateReqVO);
         update.setId(exists.getId());
         update.setId(exists.getId());
         orderMapper.updateById(update);
         orderMapper.updateById(update);
         // 明细:传入 id 匹配更新;传入 id 不存在则新增;未传入但数据库存在则删除
         // 明细:传入 id 匹配更新;传入 id 不存在则新增;未传入但数据库存在则删除
         upsertEntries(update, updateReqVO.getItems());
         upsertEntries(update, updateReqVO.getItems());
 
 
-        // 记录操作日志上下文
-        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(exists, OrderSaveReqVO.class));
+        // 手动生成明细变更日志(按id匹配)
+        String itemsChangeLog = buildItemsChangeLog(oldEntries, updateReqVO.getItems());
+
+        // 记录操作日志上下文(使用不含items的副本进行DIFF)
+        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, oldData);
+        LogRecordContext.putVariable("updateReqVO", newDataForDiff);
         LogRecordContext.putVariable("salesOrder", exists);
         LogRecordContext.putVariable("salesOrder", exists);
+        LogRecordContext.putVariable("itemsChangeLog", itemsChangeLog);
+    }
+
+    /**
+     * 构建明细变更日志(按id匹配新旧数据)
+     */
+    private String buildItemsChangeLog(List<OrderEntryDO> oldEntries, List<OrderEntrySaveReqVO> newItems) {
+        StringBuilder log = new StringBuilder();
+        
+        // 构建旧数据Map(按id)
+        Map<Long, OrderEntryDO> oldMap = oldEntries.stream()
+                .filter(e -> e.getId() != null)
+                .collect(Collectors.toMap(OrderEntryDO::getId, e -> e));
+        
+        // 构建新数据Map(按id)
+        Map<Long, OrderEntrySaveReqVO> newMap = newItems.stream()
+                .filter(e -> e.getId() != null)
+                .collect(Collectors.toMap(OrderEntrySaveReqVO::getId, e -> e));
+        
+        // 1. 检查更新的明细(id相同)
+        for (OrderEntrySaveReqVO newItem : newItems) {
+            if (newItem.getId() != null && oldMap.containsKey(newItem.getId())) {
+                OrderEntryDO oldItem = oldMap.get(newItem.getId());
+                String changes = compareEntry(oldItem, newItem);
+                if (!changes.isEmpty()) {
+                    log.append(";更新了明细行【").append(oldItem.getEntrySeq()).append("】: ").append(changes);
+                }
+            }
+        }
+        
+        // 2. 检查新增的明细(id为空或id不在旧数据中)
+        for (OrderEntrySaveReqVO newItem : newItems) {
+            if (newItem.getId() == null || !oldMap.containsKey(newItem.getId())) {
+                log.append(";新增了明细行【").append(newItem.getItemNumber()).append("】");
+            }
+        }
+        
+        // 3. 检查删除的明细(旧id不在新数据中)
+        for (OrderEntryDO oldItem : oldEntries) {
+            if (oldItem.getId() != null && !newMap.containsKey(oldItem.getId())) {
+                log.append(";删除了明细行【").append(oldItem.getEntrySeq()).append("】产品【").append(oldItem.getItemNumber()).append("】");
+            }
+        }
+        
+        return log.toString();
+    }
+
+    /**
+     * 对比单条明细的变更
+     */
+    private String compareEntry(OrderEntryDO oldItem, OrderEntrySaveReqVO newItem) {
+        StringBuilder changes = new StringBuilder();
+        
+        if (!java.util.Objects.equals(oldItem.getDate(), newItem.getDate())) {
+            changes.append("【最终交货日期】从【").append(oldItem.getDate()).append("】修改为【").append(newItem.getDate()).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getEntrySeq(), newItem.getEntrySeq())) {
+            changes.append("【行号】从【").append(oldItem.getEntrySeq()).append("】修改为【").append(newItem.getEntrySeq()).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getItemName(), newItem.getItemName())) {
+            changes.append("【产品名称】从【").append(oldItem.getItemName()).append("】修改为【").append(newItem.getItemName()).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getItemNumber(), newItem.getItemNumber())) {
+            changes.append("【产品编码】从【").append(oldItem.getItemNumber()).append("】修改为【").append(newItem.getItemNumber()).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getPlanDate(), newItem.getPlanDate())) {
+            changes.append("【客户要求交期】从【").append(oldItem.getPlanDate()).append("】修改为【").append(newItem.getPlanDate()).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getQty(), newItem.getQty())) {
+            changes.append("【订单数量】从【").append(oldItem.getQty()).append("】修改为【").append(newItem.getQty()).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getRemark(), newItem.getRemark())) {
+            changes.append("【备注】从【").append(oldItem.getRemark()).append("】修改为【").append(newItem.getRemark()).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getSpecification(), newItem.getSpecification())) {
+            changes.append("【规格型号】从【").append(oldItem.getSpecification()).append("】修改为【").append(newItem.getSpecification()).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getUnit(), newItem.getUnit())) {
+            changes.append("【单位】从【").append(oldItem.getUnit()).append("】修改为【").append(newItem.getUnit()).append("】;");
+        }
+        
+        return changes.toString();
     }
     }
 
 
     @Override
     @Override

+ 170 - 5
yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/service/impl/ShipmentServiceImpl.java

@@ -93,6 +93,25 @@ public class ShipmentServiceImpl implements ShipmentService {
         master.setCreateTime(LocalDateTime.now());
         master.setCreateTime(LocalDateTime.now());
         master.setStatus("O"); // 默认状态为Open
         master.setStatus("O"); // 默认状态为Open
         
         
+        // 设置ShipTime默认值(数据库int类型,不能为空)
+        if (master.getShipTime() == null) {
+            master.setShipTime(0);
+        }
+        
+        // 设置数值字段默认值(数据库NOT NULL)
+        if (master.getGrossWeight() == null) {
+            master.setGrossWeight(java.math.BigDecimal.ZERO);
+        }
+        if (master.getNetWeight() == null) {
+            master.setNetWeight(java.math.BigDecimal.ZERO);
+        }
+        if (master.getVolume() == null) {
+            master.setVolume(java.math.BigDecimal.ZERO);
+        }
+        if (master.getQtyToShip() == null) {
+            master.setQtyToShip(java.math.BigDecimal.ZERO);
+        }
+        
         // 设置必填字段默认值
         // 设置必填字段默认值
         if (StrUtil.isBlank(master.getShipFromId())) {
         if (StrUtil.isBlank(master.getShipFromId())) {
             master.setShipFromId(domain); // 默认使用domain作为发货地
             master.setShipFromId(domain); // 默认使用domain作为发货地
@@ -135,6 +154,36 @@ public class ShipmentServiceImpl implements ShipmentService {
             throw exception(SHIPMENT_STATUS_CLOSED);
             throw exception(SHIPMENT_STATUS_CLOSED);
         }
         }
 
 
+        // 查询旧的明细数据,用于日志对比
+        List<ShipmentDetailDO> oldDetails = shipmentDetailMapper.selectDetailsByMasterId(exists.getId(), exists.getDomain());
+        // 旧数据(不含items,避免框架自动对比列表)
+        ShipmentSaveReqVO oldData = BeanUtils.toBean(exists, ShipmentSaveReqVO.class);
+        // 统一日期格式:将LocalDateTime转为yyyy-MM-dd字符串
+        if (exists.getShipDate() != null) {
+            oldData.setShipDate(exists.getShipDate().toLocalDate().toString());
+        }
+        // 清除不需要比较的字段(避免格式差异导致误判)
+        oldData.setShipTime(null);
+        oldData.setGrossWeight(null);
+        oldData.setNetWeight(null);
+        oldData.setVolume(null);
+        oldData.setQtyToShip(null);
+        oldData.setItems(null);
+        
+        // 新数据副本(不含items,用于主表DIFF对比)
+        ShipmentSaveReqVO newDataForDiff = BeanUtils.toBean(updateReqVO, ShipmentSaveReqVO.class);
+        // 统一日期格式:只保留yyyy-MM-dd部分
+        if (newDataForDiff.getShipDate() != null && newDataForDiff.getShipDate().length() > 10) {
+            newDataForDiff.setShipDate(newDataForDiff.getShipDate().substring(0, 10));
+        }
+        // 清除不需要比较的字段
+        newDataForDiff.setShipTime(null);
+        newDataForDiff.setGrossWeight(null);
+        newDataForDiff.setNetWeight(null);
+        newDataForDiff.setVolume(null);
+        newDataForDiff.setQtyToShip(null);
+        newDataForDiff.setItems(null);
+
         // 更新主表
         // 更新主表
         ShipmentMasterDO update = convertToMasterDO(updateReqVO);
         ShipmentMasterDO update = convertToMasterDO(updateReqVO);
         update.setRecId(exists.getRecId());
         update.setRecId(exists.getRecId());
@@ -147,9 +196,125 @@ public class ShipmentServiceImpl implements ShipmentService {
             saveDetails(exists, updateReqVO.getItems());
             saveDetails(exists, updateReqVO.getItems());
         }
         }
 
 
-        // 记录操作日志上下文
-        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(exists, ShipmentSaveReqVO.class));
+        // 手动生成明细变更日志(按recId匹配)
+        String itemsChangeLog = buildShipmentItemsChangeLog(oldDetails, updateReqVO.getItems());
+
+        // 记录操作日志上下文(使用不含items的副本进行DIFF)
+        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, oldData);
+        LogRecordContext.putVariable("updateReqVO", newDataForDiff);
         LogRecordContext.putVariable("shipment", exists);
         LogRecordContext.putVariable("shipment", exists);
+        LogRecordContext.putVariable("itemsChangeLog", itemsChangeLog);
+    }
+
+    /**
+     * 构建发货单明细变更日志(按recId匹配新旧数据)
+     */
+    private String buildShipmentItemsChangeLog(List<ShipmentDetailDO> oldDetails, 
+            List<ShipmentSaveReqVO.ShipmentDetailSaveReqVO> newItems) {
+        StringBuilder log = new StringBuilder();
+        if (CollUtil.isEmpty(newItems)) {
+            newItems = List.of();
+        }
+        
+        // 构建旧数据Map(按recId)
+        Map<Long, ShipmentDetailDO> oldMap = oldDetails.stream()
+                .filter(e -> e.getRecId() != null)
+                .collect(Collectors.toMap(ShipmentDetailDO::getRecId, e -> e));
+        
+        // 构建新数据Map(按recId)
+        Map<Long, ShipmentSaveReqVO.ShipmentDetailSaveReqVO> newMap = newItems.stream()
+                .filter(e -> e.getRecId() != null)
+                .collect(Collectors.toMap(ShipmentSaveReqVO.ShipmentDetailSaveReqVO::getRecId, e -> e));
+        
+        // 1. 检查更新的明细(recId相同)
+        for (ShipmentSaveReqVO.ShipmentDetailSaveReqVO newItem : newItems) {
+            if (newItem.getRecId() != null && oldMap.containsKey(newItem.getRecId())) {
+                ShipmentDetailDO oldItem = oldMap.get(newItem.getRecId());
+                String changes = compareShipmentDetail(oldItem, newItem);
+                if (!changes.isEmpty()) {
+                    log.append(";更新了明细行【").append(oldItem.getLine()).append("】: ").append(changes);
+                }
+            }
+        }
+        
+        // 2. 检查新增的明细(recId为空或recId不在旧数据中)
+        for (ShipmentSaveReqVO.ShipmentDetailSaveReqVO newItem : newItems) {
+            if (newItem.getRecId() == null || !oldMap.containsKey(newItem.getRecId())) {
+                log.append(";新增了明细行【").append(newItem.getContainerItem()).append("】");
+            }
+        }
+        
+        // 3. 检查删除的明细(旧recId不在新数据中)
+        for (ShipmentDetailDO oldItem : oldDetails) {
+            if (oldItem.getRecId() != null && !newMap.containsKey(oldItem.getRecId())) {
+                log.append(";删除了明细行【").append(oldItem.getLine()).append("】物料【").append(oldItem.getContainerItem()).append("】");
+            }
+        }
+        
+        return log.toString();
+    }
+
+    /**
+     * 对比单条发货单明细的变更
+     */
+    private String compareShipmentDetail(ShipmentDetailDO oldItem, 
+            ShipmentSaveReqVO.ShipmentDetailSaveReqVO newItem) {
+        StringBuilder changes = new StringBuilder();
+        
+        // 使用compareTo比较BigDecimal,避免精度差异导致误判
+        if (!compareBigDecimal(oldItem.getQtyToShip(), newItem.getQtyToShip())) {
+            changes.append("【发运数量】从【").append(formatBigDecimal(oldItem.getQtyToShip())).append("】修改为【").append(formatBigDecimal(newItem.getQtyToShip())).append("】;");
+        }
+        if (!compareBigDecimal(oldItem.getPickingQty(), newItem.getPickingQty())) {
+            changes.append("【挑选数量】从【").append(formatBigDecimal(oldItem.getPickingQty())).append("】修改为【").append(formatBigDecimal(newItem.getPickingQty())).append("】;");
+        }
+        if (!compareBigDecimal(oldItem.getRealQty(), newItem.getRealQty())) {
+            changes.append("【实际数量】从【").append(formatBigDecimal(oldItem.getRealQty())).append("】修改为【").append(formatBigDecimal(newItem.getRealQty())).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getContainerItem(), newItem.getContainerItem())) {
+            changes.append("【物料编号】从【").append(oldItem.getContainerItem()).append("】修改为【").append(newItem.getContainerItem()).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getDescr(), newItem.getDescr())) {
+            changes.append("【描述】从【").append(oldItem.getDescr()).append("】修改为【").append(newItem.getDescr()).append("】;");
+        }
+        if (!java.util.Objects.equals(nullToEmpty(oldItem.getLocation()), nullToEmpty(newItem.getLocation()))) {
+            changes.append("【库位】从【").append(nullToEmpty(oldItem.getLocation())).append("】修改为【").append(nullToEmpty(newItem.getLocation())).append("】;");
+        }
+        if (!java.util.Objects.equals(nullToEmpty(oldItem.getLotSerial()), nullToEmpty(newItem.getLotSerial()))) {
+            changes.append("【批次号】从【").append(nullToEmpty(oldItem.getLotSerial())).append("】修改为【").append(nullToEmpty(newItem.getLotSerial())).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getRemark(), newItem.getRemark())) {
+            changes.append("【备注】从【").append(oldItem.getRemark()).append("】修改为【").append(newItem.getRemark()).append("】;");
+        }
+        if (!java.util.Objects.equals(oldItem.getStatus(), newItem.getStatus())) {
+            changes.append("【状态】从【").append(oldItem.getStatus()).append("】修改为【").append(newItem.getStatus()).append("】;");
+        }
+        
+        return changes.toString();
+    }
+    
+    /**
+     * 比较两个BigDecimal是否相等(忽略精度差异)
+     */
+    private boolean compareBigDecimal(java.math.BigDecimal a, java.math.BigDecimal b) {
+        if (a == null && b == null) return true;
+        if (a == null || b == null) return false;
+        return a.compareTo(b) == 0;
+    }
+    
+    /**
+     * 格式化BigDecimal,去除尾部多余的0
+     */
+    private String formatBigDecimal(java.math.BigDecimal value) {
+        if (value == null) return "空";
+        return value.stripTrailingZeros().toPlainString();
+    }
+    
+    /**
+     * 将null转为空字符串
+     */
+    private String nullToEmpty(String value) {
+        return value == null ? "" : value;
     }
     }
 
 
     @Override
     @Override
@@ -203,9 +368,9 @@ public class ShipmentServiceImpl implements ShipmentService {
             detail.setUm(item.getUm());
             detail.setUm(item.getUm());
             detail.setLocation(item.getLocation());
             detail.setLocation(item.getLocation());
             detail.setLotSerial(item.getLotSerial());
             detail.setLotSerial(item.getLotSerial());
-            detail.setQtyToShip(item.getQtyToShip());
-            detail.setPickingQty(item.getPickingQty());
-            detail.setRealQty(item.getRealQty());
+            detail.setQtyToShip(item.getQtyToShip() != null ? item.getQtyToShip() : java.math.BigDecimal.ZERO);
+            detail.setPickingQty(item.getPickingQty() != null ? item.getPickingQty() : java.math.BigDecimal.ZERO);
+            detail.setRealQty(item.getRealQty() != null ? item.getRealQty() : java.math.BigDecimal.ZERO);
             detail.setStatus(item.getStatus());
             detail.setStatus(item.getStatus());
             detail.setRemark(item.getRemark());
             detail.setRemark(item.getRemark());
             detail.setShType("SH");
             detail.setShType("SH");

+ 24 - 5
yudao-ui/yudao-ui-admin-vue3/src/views/jiaohuo/components/ShipmentForm.vue

@@ -133,7 +133,14 @@
         </el-table-column>
         </el-table-column>
         <el-table-column prop="ordLine" label="订单项次" width="90">
         <el-table-column prop="ordLine" label="订单项次" width="90">
           <template #default="{ row }">
           <template #default="{ row }">
-            <el-input v-if="mode !== 'view'" v-model="row.ordLine" size="small" />
+            <el-input-number
+              v-if="mode !== 'view'"
+              v-model="row.ordLine"
+              :min="0"
+              :controls="false"
+              size="small"
+              style="width: 100%"
+            />
             <span v-else>{{ row.ordLine }}</span>
             <span v-else>{{ row.ordLine }}</span>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
@@ -340,7 +347,7 @@ const loadShippingPlanData = (plans) => {
 
 
     formData.items = plans.map((plan) => ({
     formData.items = plans.map((plan) => ({
       ordNbr: plan.billNo || '',
       ordNbr: plan.billNo || '',
-      ordLine: plan.ordLine || '',
+      ordLine: plan.ordLine ? Number(plan.ordLine) : null,
       containerItem: plan.itemNum || '',
       containerItem: plan.itemNum || '',
       descr: plan.itemName || '',
       descr: plan.itemName || '',
       um: plan.um || '',
       um: plan.um || '',
@@ -366,8 +373,20 @@ const loadData = async (id) => {
       formData.soldTo = data.soldTo || ''
       formData.soldTo = data.soldTo || ''
       // 转换日期为时间戳
       // 转换日期为时间戳
       if (data.shipDate) {
       if (data.shipDate) {
-        const dateStr = String(data.shipDate).substring(0, 10)
-        formData.shipDate = new Date(dateStr).getTime()
+        // 处理各种日期格式:ISO字符串、数组、时间戳
+        let timestamp = null
+        if (Array.isArray(data.shipDate)) {
+          // Java LocalDateTime 序列化为数组 [year, month, day, hour, minute, second]
+          const [year, month, day] = data.shipDate
+          timestamp = new Date(year, month - 1, day).getTime()
+        } else if (typeof data.shipDate === 'string') {
+          // ISO 字符串格式
+          const dateStr = data.shipDate.substring(0, 10)
+          timestamp = new Date(dateStr).getTime()
+        } else if (typeof data.shipDate === 'number') {
+          timestamp = data.shipDate
+        }
+        formData.shipDate = timestamp && !isNaN(timestamp) ? timestamp : null
       } else {
       } else {
         formData.shipDate = null
         formData.shipDate = null
       }
       }
@@ -391,7 +410,7 @@ const resetForm = () => {
 const handleAddRow = () => {
 const handleAddRow = () => {
   formData.items.push({
   formData.items.push({
     ordNbr: formData.ordNbr || '',
     ordNbr: formData.ordNbr || '',
-    ordLine: '',
+    ordLine: null,
     containerItem: '',
     containerItem: '',
     descr: '',
     descr: '',
     um: '',
     um: '',