From ad9694f7c53d81dec76292b8d329e4dc06e90bc6 Mon Sep 17 00:00:00 2001
From: yuan <123@>
Date: 星期四, 23 四月 2026 14:45:22 +0800
Subject: [PATCH] feat(production): 新增BOM复制功能

---
 src/main/java/com/ruoyi/production/dto/ProductBomDto.java                  |    3 
 src/main/java/com/ruoyi/common/utils/BigDecimalUtils.java                  |  180 ++++++++++++++++++++++++++++++
 src/main/java/com/ruoyi/production/service/impl/ProductBomServiceImpl.java |  111 ++++++++++++++++++
 src/main/java/com/ruoyi/production/controller/ProductBomController.java    |    7 +
 src/main/java/com/ruoyi/production/service/ProductBomService.java          |    2 
 5 files changed, 303 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/ruoyi/common/utils/BigDecimalUtils.java b/src/main/java/com/ruoyi/common/utils/BigDecimalUtils.java
new file mode 100644
index 0000000..8d16cc3
--- /dev/null
+++ b/src/main/java/com/ruoyi/common/utils/BigDecimalUtils.java
@@ -0,0 +1,180 @@
+package com.ruoyi.common.utils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Optional;
+
+/**
+ * @author yuan
+ * @date 2026-04-23 13:10
+ * @description BigDecimal宸ュ叿绫�
+ */
+public class BigDecimalUtils {
+    public static BigDecimal getBigDecimalValue(BigDecimal val) {
+        return Optional.ofNullable(val).orElse(BigDecimal.ZERO);
+    }
+
+    public static BigDecimal of(Object val) {
+        if (val == null) {
+            return null;
+        }
+        try {
+            if (val instanceof Double || val instanceof Float) {
+                return BigDecimal.valueOf(((Number) val).doubleValue());
+            }
+            if (val instanceof Number) {
+                return new BigDecimal(val.toString());
+            }
+            return new BigDecimal(val.toString().trim());
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("鏃犳硶杞崲 " + val + " 涓� BigDecimal绫诲瀷 ", e);
+        }
+    }
+
+    /**
+     * 瀹夊叏杞崲鏂规硶锛屽皢瀵硅薄杞负BigDecimal锛宯ull瑙嗕负0
+     * @param number 鍙互鏄疦umber銆丼tring鎴朆igDecimal
+     * @return 瀵瑰簲鐨凚igDecimal锛宯ull杞负0
+     */
+    public static BigDecimal safe(Object number) {
+        if (number == null) return BigDecimal.ZERO;
+
+        if (number instanceof BigDecimal) return (BigDecimal) number;
+
+        if (number instanceof Number) return BigDecimal.valueOf(((Number) number).doubleValue());
+        try {
+            return new BigDecimal(number.toString());
+        } catch (NumberFormatException e) {
+            return BigDecimal.ZERO;
+        }
+    }
+
+    /**
+     * 鍔犳硶杩愮畻锛堣嚜鍔ㄥ鐞唍ull鍊硷級
+     * @param values 澶氫釜鍔犳暟
+     * @return 鍜�
+     */
+    public static BigDecimal add(Object... values) {
+        return Arrays.stream(values)
+                .map(BigDecimalUtils::safe)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+    }
+
+    /**
+     * 鍑忔硶杩愮畻锛堣嚜鍔ㄥ鐞唍ull鍊硷級
+     * @param minuend 琚噺鏁�
+     * @param subtrahends 鍑忔暟
+     * @return 宸�
+     */
+    public static BigDecimal subtract(Object minuend, Object... subtrahends) {
+        BigDecimal result = safe(minuend);
+        for (Object subtrahend : subtrahends) {
+            result = result.subtract(safe(subtrahend));
+        }
+        return result;
+    }
+
+    /**
+     * 涔樻硶杩愮畻锛堣嚜鍔ㄥ鐞唍ull鍊硷紝浠讳綍鍙傛暟涓簄ull鍒欒繑鍥�0锛�
+     * @param values 澶氫釜涔樻暟
+     * @return 绉�
+     */
+    public static BigDecimal multiply(Object... values) {
+        if (values == null || values.length == 0) {
+            return BigDecimal.ZERO;
+        }
+
+        BigDecimal result = Arrays.stream(values)
+                .map(BigDecimalUtils::safe)
+                .reduce(BigDecimal.ONE, BigDecimal::multiply);
+
+        // 鏍囧噯鍖栫粨鏋滐細濡傛灉鏄�0锛屽幓闄ゆ墍鏈夊皬鏁颁綅锛涘惁鍒欎繚鐣欏師鏈夊皬鏁颁綅
+        return stripTrailingZeros(result);
+    }
+
+    /**
+     * 闄ゆ硶杩愮畻锛堣嚜鍔ㄥ鐞唍ull鍊硷級
+     * @param dividend 琚櫎鏁�
+     * @param divisor 闄ゆ暟
+     * @param scale 灏忔暟浣嶆暟
+     * @param roundingMode 鑸嶅叆妯″紡
+     * @return 鍟�
+     */
+    public static BigDecimal divide(Object dividend, Object divisor,
+                                    int scale, RoundingMode roundingMode) {
+        BigDecimal d1 = safe(dividend);
+        BigDecimal d2 = safe(divisor);
+
+        if (d2.compareTo(BigDecimal.ZERO) == 0) {
+            return BigDecimal.ZERO;
+        }
+        return	stripTrailingZeros(d1.divide(d2, scale, roundingMode));
+    }
+
+    /**
+     * 闄ゆ硶杩愮畻锛堣嚜鍔ㄥ鐞唍ull鍊硷級
+     * @param dividend 琚櫎鏁�
+     * @param divisor 闄ゆ暟
+     * @param scale 灏忔暟浣嶆暟
+     * @param roundingMode 鑸嶅叆妯″紡
+     * @return 鍟�
+     */
+    public static BigDecimal dividePercentage(Object dividend, Object divisor,
+                                              int scale, RoundingMode roundingMode) {
+        BigDecimal d1 = safe(dividend);
+        BigDecimal d2 = safe(divisor);
+
+        if (d2.compareTo(BigDecimal.ZERO) == 0) {
+            return BigDecimal.ZERO;
+        }
+        BigDecimal multiplied = d1.multiply(new BigDecimal("100"));
+        BigDecimal result = multiplied.divide(d2, scale, roundingMode);
+
+        return stripTrailingZeros(result);
+    }
+
+    /**
+     * 姣旇緝涓や釜鏁扮殑澶у皬锛堣嚜鍔ㄥ鐞唍ull鍊硷紝null瑙嗕负0锛�
+     * @param num1 绗竴涓暟
+     * @param num2 绗簩涓暟
+     * @return 1: num1 > num2; 0: num1 = num2; -1: num1 < num2
+     */
+    public static int compare(Object num1, Object num2) {
+        return safe(num1).compareTo(safe(num2));
+    }
+
+    /**
+     * 鍥涜垗浜斿叆锛堣嚜鍔ㄥ鐞唍ull鍊硷級
+     * @param value 鍘熷鍊�
+     * @param scale 淇濈暀灏忔暟浣嶆暟
+     * @return 鍥涜垗浜斿叆鍚庣殑鍊�
+     */
+    public static BigDecimal round(Object value, int scale) {
+        return safe(value).setScale(scale, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 姹傚拰锛堟祦寮忓鐞嗙増锛�
+     * @param numbers 鏁板瓧闆嗗悎
+     * @return 鍜�
+     */
+    public static BigDecimal sum(Collection<?> numbers) {
+        return numbers.stream()
+                .map(BigDecimalUtils::safe)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+    }
+
+    /**
+     * 鏍囧噯鍖朆igDecimal缁撴灉
+     * 1. 濡傛灉鏄�0锛屽幓闄ゆ墍鏈夊皬鏁颁綅
+     * 2. 濡傛灉涓嶆槸0锛屽幓闄ゆ湯灏炬棤鎰忎箟鐨�0
+     */
+    public static BigDecimal stripTrailingZeros(BigDecimal value) {
+        if (value.compareTo(BigDecimal.ZERO) == 0) {
+            return BigDecimal.ZERO;
+        }
+        return new BigDecimal(value.stripTrailingZeros().toPlainString());
+    }
+}
diff --git a/src/main/java/com/ruoyi/production/controller/ProductBomController.java b/src/main/java/com/ruoyi/production/controller/ProductBomController.java
index 3925b5c..79eb6e0 100644
--- a/src/main/java/com/ruoyi/production/controller/ProductBomController.java
+++ b/src/main/java/com/ruoyi/production/controller/ProductBomController.java
@@ -69,6 +69,13 @@
         return productBomService.add(productBom);
     }
 
+    @ApiModelProperty("澶嶅埗BOM")
+    @PostMapping("/copy")
+    @Log(title = "澶嶅埗BOM", businessType = BusinessType.INSERT)
+    public AjaxResult copy(@RequestBody ProductBomDto productBom) {
+        return productBomService.copy(productBom);
+    }
+
     @ApiOperation("鏇存柊BOM")
     @Log(title = "淇敼", businessType = BusinessType.UPDATE)
     @PutMapping("/update")
diff --git a/src/main/java/com/ruoyi/production/dto/ProductBomDto.java b/src/main/java/com/ruoyi/production/dto/ProductBomDto.java
index 30998d3..09ad814 100644
--- a/src/main/java/com/ruoyi/production/dto/ProductBomDto.java
+++ b/src/main/java/com/ruoyi/production/dto/ProductBomDto.java
@@ -17,4 +17,7 @@
 
     //鐗╂枡缂栫爜
     private String materialCode;
+
+    //鐗╂枡缂栫爜
+    private Long copyId;
 }
diff --git a/src/main/java/com/ruoyi/production/service/ProductBomService.java b/src/main/java/com/ruoyi/production/service/ProductBomService.java
index 7ec6d9a..927f4c6 100644
--- a/src/main/java/com/ruoyi/production/service/ProductBomService.java
+++ b/src/main/java/com/ruoyi/production/service/ProductBomService.java
@@ -29,4 +29,6 @@
     void exportBom(HttpServletResponse response, Integer bomId);
 
     AjaxResult update(ProductBom productBom);
+
+    AjaxResult copy(ProductBomDto productBom);
 }
diff --git a/src/main/java/com/ruoyi/production/service/impl/ProductBomServiceImpl.java b/src/main/java/com/ruoyi/production/service/impl/ProductBomServiceImpl.java
index b7b67bb..b91ba31 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductBomServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductBomServiceImpl.java
@@ -2,6 +2,7 @@
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.ruoyi.basic.pojo.Product;
@@ -20,6 +21,7 @@
 import com.ruoyi.production.service.ProductBomService;
 import com.ruoyi.production.service.ProductProcessService;
 import com.ruoyi.production.service.ProductStructureService;
+import org.jetbrains.annotations.NotNull;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -188,6 +190,115 @@
 
     @Override
     @Transactional(rollbackFor = Exception.class)
+    public AjaxResult copy(ProductBomDto productBom) {
+        Long copyId = productBom.getCopyId();
+        if (copyId == null) {
+            throw new ServiceException("澶嶅埗婧怋OM ID涓嶈兘涓虹┖");
+        }
+        ProductBom sourceBom = productBomMapper.selectById(copyId);
+        if (sourceBom == null) {
+            throw new ServiceException("澶嶅埗婧怋OM涓嶅瓨鍦�");
+        }
+
+
+        ProductBom newBom = getProductBom(productBom, sourceBom);
+        productBomMapper.insert(newBom);
+        newBom.setBomNo("BM." + String.format("%05d", newBom.getId()));
+        productBomMapper.updateById(newBom);
+
+
+
+        ProductModel productModel = productModelService.getById(newBom.getProductModelId());
+        if (productModel == null) {
+            throw new ServiceException("閫夋嫨鐨勪骇鍝佹ā鍨嬩笉瀛樺湪");
+        }
+
+        ProductStructure newRoot = getProductStructure(newBom, productModel);
+        productStructureService.save(newRoot);
+        List<ProductStructure> sourceStructures = productStructureMapper.selectList(
+                        Wrappers.<ProductStructure>lambdaQuery()
+                                .eq(ProductStructure::getBomId, copyId.intValue()));
+        if (sourceStructures == null || sourceStructures.isEmpty()) {
+            return AjaxResult.success();
+        }
+
+        ProductStructure oldRoot = sourceStructures.stream()
+                .filter(s -> s.getParentId() == null)
+                .findFirst().orElse(new ProductStructure());
+
+        List<ProductStructure> children = sourceStructures
+                .stream()
+                .filter(s -> !s.getId().equals(oldRoot.getId()))
+                .collect(Collectors.toList());
+
+        Map<Long, Long> oldNewIdMap = new HashMap<>();
+
+        oldNewIdMap.put(oldRoot.getId(), newRoot.getId());
+
+        List<ProductStructure> insertList = children
+                .stream()
+                .map(item -> getProductStructures(item, newBom))
+                .collect(Collectors.toList());
+
+        productStructureService.saveBatch(insertList);
+
+        for (int i = 0; i < children.size(); i++) {
+            oldNewIdMap.put(
+                    children.get(i).getId(),
+                    insertList.get(i).getId()
+            );
+        }
+
+        List<ProductStructure> updateList = new ArrayList<>();
+        for (int i = 0; i < children.size(); i++) {
+            ProductStructure source = children.get(i);
+            ProductStructure inserted = insertList.get(i);
+            Long newParentId = oldNewIdMap.get(source.getParentId());
+            if (newParentId != null) {
+                inserted.setParentId(newParentId);
+                updateList.add(inserted);
+            }
+        }
+        if (!updateList.isEmpty()) {
+            productStructureService.updateBatchById(updateList);
+        }
+
+        return AjaxResult.success();
+    }
+
+    @NotNull
+    private static ProductStructure getProductStructures(ProductStructure item, ProductBom newBom) {
+        ProductStructure copy = new ProductStructure();
+        copy.setProductModelId(item.getProductModelId());
+        copy.setProcessId(item.getProcessId());
+        copy.setUnitQuantity(item.getUnitQuantity());
+        copy.setDemandedQuantity(item.getDemandedQuantity());
+        copy.setUnit(item.getUnit());
+        copy.setBomId(newBom.getId());
+        return copy;
+    }
+
+    @NotNull
+    private static ProductStructure getProductStructure(ProductBom newBom, ProductModel productModel) {
+        ProductStructure newRoot = new ProductStructure();
+        newRoot.setProductModelId(newBom.getProductModelId());
+        newRoot.setUnitQuantity(BigDecimal.valueOf(1));
+        newRoot.setUnit(productModel.getUnit());
+        newRoot.setBomId(newBom.getId());
+        return newRoot;
+    }
+
+    @NotNull
+    private static ProductBom getProductBom(ProductBomDto productBom, ProductBom sourceBom) {
+        ProductBom newBom = new ProductBom();
+        newBom.setProductModelId(productBom.getProductModelId() != null ? productBom.getProductModelId() : sourceBom.getProductModelId());
+        newBom.setRemark(productBom.getRemark());
+        newBom.setVersion(productBom.getVersion() != null ? productBom.getVersion() : sourceBom.getVersion());
+        return newBom;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
     public AjaxResult uploadBom(MultipartFile file) {
         ExcelUtil<BomImportDto> util = new ExcelUtil<>(BomImportDto.class);
         List<BomImportDto> list;

--
Gitblit v1.9.3