已添加3个文件
已修改12个文件
631 ■■■■■ 文件已修改
doc/20260829_alter_sales_purchase_amount_scale_6.sql 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/purchase/AccountPurchasePaymentServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesCollectionServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/controller/ProductController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/dto/ProductImportRowDto.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/IProductModelService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/IProductService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/ProductModelServiceImpl.java 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/ProductServiceImpl.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesQuotationProduct.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyBomServiceImpl.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/产品导入模板.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260829_alter_sales_purchase_amount_scale_6.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
-- é”€å”®/采购相关页面金额、单价、数量、总价统一保留 6 ä½å°æ•°
-- æ¶‰åŠé¡µé¢ï¼šé”€å”®æŠ¥ä»·ã€é”€å”®å°è´¦ã€å®¢æˆ·é€€è´§ã€å®¢æˆ·å¾€æ¥ã€é‡‡è´­å°è´¦ã€ä¾›åº”商往来、采购退货、采购报表
-- ç»Ÿä¸€ä½¿ç”¨ decimal(18, 6):整数位保留 12 ä½ï¼Œé¿å…ç”± decimal(10, 2) ç›´æŽ¥æ”¹å°æ•°ä½å¯¼è‡´çš„æº¢å‡º
-- ----------------------------
-- é”€å”®æŠ¥ä»·ï¼ˆsales_quotation / sales_quotation_product)
-- ----------------------------
ALTER TABLE `sales_quotation`
    MODIFY COLUMN `total_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '报价金额';
ALTER TABLE `sales_quotation_product`
    MODIFY COLUMN `quantity` decimal(18, 6) NULL DEFAULT NULL COMMENT '数量',
    MODIFY COLUMN `unit_price` decimal(18, 6) NULL DEFAULT NULL COMMENT '单价',
    MODIFY COLUMN `amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '金额';
-- ----------------------------
-- é”€å”®å°è´¦ï¼ˆsales_ledger / sales_ledger_product)
-- ----------------------------
ALTER TABLE `sales_ledger`
    MODIFY COLUMN `contract_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '合同金额(产品含税总价)',
    MODIFY COLUMN `net_contract_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '实际合同金额(扣减退货后)';
ALTER TABLE `sales_ledger_product`
    MODIFY COLUMN `quantity` decimal(18, 6) NOT NULL COMMENT '数量',
    MODIFY COLUMN `min_stock` decimal(18, 6) NULL DEFAULT NULL COMMENT '最低库存',
    MODIFY COLUMN `tax_inclusive_unit_price` decimal(18, 6) NOT NULL COMMENT '含税单价',
    MODIFY COLUMN `tax_inclusive_total_price` decimal(18, 6) NOT NULL COMMENT '含税总价',
    MODIFY COLUMN `tax_exclusive_total_price` decimal(18, 6) NOT NULL COMMENT '不含税总价',
    MODIFY COLUMN `warn_num` decimal(18, 6) NULL DEFAULT NULL COMMENT '预警数量';
-- å°è´¦æ¨¡æ¿ï¼ˆä¿å­˜/套用模板时与产品表结构保持一致)
ALTER TABLE `sales_ledger_product_template`
    MODIFY COLUMN `quantity` decimal(18, 6) NULL DEFAULT NULL COMMENT '数量',
    MODIFY COLUMN `min_stock` decimal(18, 6) NULL DEFAULT NULL COMMENT '最低库存',
    MODIFY COLUMN `tax_inclusive_unit_price` decimal(18, 6) NULL DEFAULT NULL COMMENT '含税单价',
    MODIFY COLUMN `tax_inclusive_total_price` decimal(18, 6) NULL DEFAULT NULL COMMENT '含税总价',
    MODIFY COLUMN `tax_exclusive_total_price` decimal(18, 6) NULL DEFAULT NULL COMMENT '不含税总价',
    MODIFY COLUMN `warn_num` decimal(18, 6) NULL DEFAULT NULL COMMENT '预警数量';
-- ----------------------------
-- å®¢æˆ·é€€è´§ï¼ˆreturn_management / return_sale_product / sales_refund_amount_order)
-- ----------------------------
ALTER TABLE `return_management`
    MODIFY COLUMN `refund_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '退款总额';
ALTER TABLE `return_sale_product`
    MODIFY COLUMN `num` decimal(18, 6) NOT NULL COMMENT '退货产品数量',
    MODIFY COLUMN `price` decimal(18, 6) NULL DEFAULT NULL COMMENT '单价',
    MODIFY COLUMN `amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '金额';
ALTER TABLE `sales_refund_amount_order`
    MODIFY COLUMN `refund_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '应退款金额',
    MODIFY COLUMN `refunded_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '已退款金额',
    MODIFY COLUMN `not_refunded_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '未退款金额';
-- ----------------------------
-- å®¢æˆ·å¾€æ¥ï¼ˆå›žæ¬¾é‡‘额来源:account_sales_collection、receipt_payment)
-- ----------------------------
ALTER TABLE `account_sales_collection`
    MODIFY COLUMN `collection_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '收款金额';
ALTER TABLE `receipt_payment`
    MODIFY COLUMN `receipt_payment_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '回款金额';
-- ----------------------------
-- é‡‡è´­å°è´¦ï¼ˆpurchase_ledger / purchase_ledger_template)
-- ----------------------------
ALTER TABLE `purchase_ledger`
    MODIFY COLUMN `contract_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '合同金额',
    MODIFY COLUMN `net_contract_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '实际合同金额(扣减退货后)';
ALTER TABLE `purchase_ledger_template`
    MODIFY COLUMN `contract_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '合同金额';
-- ----------------------------
-- ä¾›åº”商往来(付款金额来源:account_purchase_payment)
-- ----------------------------
ALTER TABLE `account_purchase_payment`
    MODIFY COLUMN `payment_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '付款金额';
-- ----------------------------
-- é‡‡è´­é€€è´§ï¼ˆpurchase_return_orders / purchase_return_order_products)
-- ----------------------------
ALTER TABLE `purchase_return_orders`
    MODIFY COLUMN `total_discount_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '整单折扣额',
    MODIFY COLUMN `total_discount_rate` decimal(18, 6) NULL DEFAULT NULL COMMENT '整单折扣率',
    MODIFY COLUMN `total_amount` decimal(18, 6) NULL DEFAULT NULL COMMENT '成交金额';
ALTER TABLE `purchase_return_order_products`
    MODIFY COLUMN `return_quantity` decimal(18, 6) NOT NULL DEFAULT 0.000000 COMMENT '退货数量';
src/main/java/com/ruoyi/account/service/impl/purchase/AccountPurchasePaymentServiceImpl.java
@@ -97,7 +97,7 @@
        if (dto.getPaymentDate() == null) {
            throw new ServiceException("请选择付款日期");
        }
        if (dto.getPaymentAmount() == null || dto.getPaymentAmount().compareTo(BigDecimal.valueOf(0.01)) < 0) {
        if (dto.getPaymentAmount() == null || dto.getPaymentAmount().compareTo(new BigDecimal("0.000001")) < 0) {
            throw new ServiceException("请输入付款金额");
        }
        if (StringUtils.isEmpty(dto.getPurchaseContractNumber())) {
src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesCollectionServiceImpl.java
@@ -102,7 +102,7 @@
        if (dto.getPaymentDate() == null) {
            throw new ServiceException("请选择回款日期");
        }
        if (dto.getPaymentAmount() == null || dto.getPaymentAmount().compareTo(BigDecimal.valueOf(0.01)) < 0) {
        if (dto.getPaymentAmount() == null || dto.getPaymentAmount().compareTo(new BigDecimal("0.000001")) < 0) {
            throw new ServiceException("请输入回款金额");
        }
        if (StringUtils.isEmpty(dto.getSalesContractNo())) {
src/main/java/com/ruoyi/basic/controller/ProductController.java
@@ -5,13 +5,11 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.dto.ProductDto;
import com.ruoyi.basic.dto.ProductModelDto;
import com.ruoyi.basic.dto.ProductModelExportDto;
import com.ruoyi.basic.dto.ProductTreeDto;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.service.IProductModelService;
import com.ruoyi.basic.service.IProductService;
import com.ruoyi.basic.vo.ProductModelVo;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
@@ -125,8 +123,8 @@
     */
    @PostMapping("/import")
    @Log(title = "导入产品", businessType = BusinessType.IMPORT)
    public AjaxResult importProductModel(@RequestParam("file") MultipartFile file, Integer productId) {
        return productModelService.importProductModel(file, productId);
    public AjaxResult importProductModel(@RequestParam("file") MultipartFile file) {
        return productModelService.importProductModel(file);
    }
    /**
@@ -136,7 +134,6 @@
    @Operation(summary = "产品导入模板")
    @Log(title = "产品导入模板", businessType = BusinessType.EXPORT)
    public void importProduct(HttpServletResponse response) {
        ExcelUtil<ProductModelExportDto> excelUtil = new ExcelUtil<>(ProductModelExportDto.class);
        excelUtil.importTemplateExcel(response, "产品规格导入模板");
        productService.exportImportTemplate(response);
    }
}
src/main/java/com/ruoyi/basic/dto/ProductImportRowDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package com.ruoyi.basic.dto;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import lombok.Data;
/**
 * äº§å“æ‰¹é‡å¯¼å…¥è¡Œï¼ˆä¸‰ä¸ªsheet:原料/半成品/成品)
 */
@Data
public class ProductImportRowDto {
    @Excel(name = "产品父类")
    private String productParentName;
    @Excel(name = "产品大类")
    private String productCategoryName;
    @Excel(name = "产品名称")
    private String productName;
    @Excel(name = "产品编码")
    private String productCode;
    @Excel(name = "规格型号")
    private String model;
    @Excel(name = "单位")
    private String unit;
}
src/main/java/com/ruoyi/basic/service/IProductModelService.java
@@ -34,5 +34,5 @@
     */
    IPage<ProductModel> modelListPage(Page page , ProductDto productDto);
    AjaxResult importProductModel(MultipartFile file, Integer productId);
    AjaxResult importProductModel(MultipartFile file);
}
src/main/java/com/ruoyi/basic/service/IProductService.java
@@ -8,11 +8,12 @@
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.vo.ProductModelVo;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
public interface IProductService extends IService<Product> {
    int addOrEditProduct(ProductDto productDto);
    int delProductByIds(Long[] ids);
@@ -20,4 +21,6 @@
    List<ProductTreeDto> selectProductList(ProductDto productDto);
    IPage<ProductModelVo> listPageProductModel(Page<ProductModelVo> page, ProductModel productModel);
    void exportImportTemplate(HttpServletResponse response);
}
src/main/java/com/ruoyi/basic/service/impl/ProductModelServiceImpl.java
@@ -7,6 +7,7 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.basic.dto.ProductDto;
import com.ruoyi.basic.dto.ProductImportRowDto;
import com.ruoyi.basic.dto.ProductModelDto;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
@@ -18,7 +19,6 @@
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.sales.dto.LossProductModelDto;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.technology.mapper.TechnologyBomMapper;
@@ -28,11 +28,13 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.concurrent.ThreadLocalRandom;
/**
 * ã€è¯·å¡«å†™åŠŸèƒ½åç§°ã€‘Service业务层处理
@@ -48,6 +50,8 @@
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final TechnologyBomMapper technologyBomMapper;
    private ProductModelMapper productModelMapper;
    private static final List<String> IMPORT_SHEET_NAMES = Arrays.asList("原材料", "原料", "半成品", "成品");
    @Override
    public int addOrEditProductModel(ProductModelDto productModelDto) {
@@ -124,69 +128,184 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult importProductModel(MultipartFile file, Integer productId) {
        if (productId == null) {
            return AjaxResult.error("请先选择产品再导入规格型号");
    public AjaxResult importProductModel(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            return AjaxResult.error("请选择要导入的文件");
        }
        Product product = productMapper.selectById(productId);
        if (product == null) {
            return AjaxResult.error("选择的产品不存在");
        }
        Map<String, List<ProductImportRowDto>> sheetMap;
        try {
            ExcelUtil<ProductModel> productModelExcelUtil = new ExcelUtil<>(ProductModel.class);
            List<ProductModel> productModelList = productModelExcelUtil.importExcel(file.getInputStream());
            ExcelUtil<ProductImportRowDto> excelUtil = new ExcelUtil<>(ProductImportRowDto.class);
            sheetMap = excelUtil.importExcelMultiSheet(IMPORT_SHEET_NAMES, file.getInputStream(), 0);
        } catch (Exception e) {
            log.error("解析产品导入文件异常", e);
            throw new ServiceException("产品导入文件解析失败");
        }
        if (sheetMap.isEmpty()) {
            return AjaxResult.error("未找到 [原料/半成品/成品] å·¥ä½œè¡¨ï¼Œæˆ–文件中没有数据");
        }
            if (CollectionUtils.isEmpty(productModelList)) {
                return AjaxResult.error("导入数据不能为空");
        int successCount = 0;
        int skipCount = 0;
        Set<String> generatedCodes = new HashSet<>();
        for (String sheetName : IMPORT_SHEET_NAMES) {
            List<ProductImportRowDto> rows = sheetMap.get(sheetName);
            if (CollectionUtils.isEmpty(rows)) {
                continue;
            }
            //  èŽ·å–å½“å‰äº§å“ä¸‹æ‰€æœ‰çš„è§„æ ¼åž‹å·å
            List<ProductModel> existingModels = list(new LambdaQueryWrapper<ProductModel>().eq(ProductModel::getProductId, productId));
            Set<String> existingModelNames = existingModels.stream().map(ProductModel::getModel).collect(Collectors.toSet());
            List<ProductModel> waitToSaveList = new ArrayList<>();
            int skipCount = 0;
            for (int i = 0; i < productModelList.size(); i++) {
                ProductModel item = productModelList.get(i);
            Long topId = findOrCreateTopProduct(sheetName);
            Map<String, Long> nodeCache = new HashMap<>();
            Map<Long, Set<String>> codeCache = new HashMap<>();
            Map<Long, Set<String>> modelCache = new HashMap<>();
            for (int i = 0; i < rows.size(); i++) {
                ProductImportRowDto row = rows.get(i);
                int rowNum = i + 2;
                if (StringUtils.isEmpty(item.getProductCode())) {
                    return AjaxResult.error("第 " + rowNum + " è¡Œå¯¼å…¥å¤±è´¥: [产品编码] ä¸èƒ½ä¸ºç©º");
                }
                if (StringUtils.isEmpty(item.getModel())) {
                    return AjaxResult.error("第 " + rowNum + " è¡Œå¯¼å…¥å¤±è´¥: [规格型号] ä¸èƒ½ä¸ºç©º");
                }
                if (StringUtils.isEmpty(item.getUnit())) {
                    return AjaxResult.error("第 " + rowNum + " è¡Œå¯¼å…¥å¤±è´¥: [单位] ä¸èƒ½ä¸ºç©º");
                String parentName = trimToNull(row.getProductParentName());
                String categoryName = trimToNull(row.getProductCategoryName());
                String productName = trimToNull(row.getProductName());
                if (productName == null) {
                    throw new ServiceException("第 " + rowNum + " è¡Œ [产品名称] ä¸èƒ½ä¸ºç©º");
                }
                //  åŽ»é‡,如果已包含该型号,则跳过
                if (existingModelNames.contains(item.getModel())) {
                String path = String.valueOf(topId);
                Long currentParentId = topId;
                if (parentName != null) {
                    path = path + ">" + parentName;
                    currentParentId = ensureChildNode(nodeCache, path, currentParentId, parentName);
                }
                if (categoryName != null) {
                    path = path + ">" + categoryName;
                    currentParentId = ensureChildNode(nodeCache, path, currentParentId, categoryName);
                }
                path = path + ">" + productName;
                Long leafId = ensureChildNode(nodeCache, path, currentParentId, productName);
                String model = StringUtils.isEmpty(row.getModel()) ? "/" : row.getModel().trim();
                // åŒä¸€äº§å“ä¸‹åç§°ï¼ˆäº§å“èŠ‚ç‚¹ï¼‰+ åž‹å·ç›¸åŒåˆ™è·³è¿‡
                Set<String> models = modelCache.computeIfAbsent(leafId, this::loadProductModels);
                if (!models.add(model)) {
                    skipCount++;
                    continue;
                }
                item.setProductId(product.getId());
                waitToSaveList.add(item);
                String code = trimToNull(row.getProductCode());
                if (code == null) {
                    code = generateUniqueCode(generatedCodes);
                }
                Set<String> codes = codeCache.computeIfAbsent(leafId, this::loadProductCodes);
                if (!codes.add(code)) {
                    skipCount++;
                    continue;
                }
                existingModelNames.add(item.getModel());
                ProductModel productModel = new ProductModel();
                productModel.setProductId(leafId);
                productModel.setProductCode(code);
                productModel.setModel(model);
                productModel.setUnit(trimToNull(row.getUnit()));
                productModelMapper.insert(productModel);
                successCount++;
            }
            if (!waitToSaveList.isEmpty()) {
                saveBatch(waitToSaveList);
            }
            if (skipCount == 0) {
                return AjaxResult.success(String.format("成功导入 %d æ¡æ•°æ®", waitToSaveList.size()));
            } else {
                return AjaxResult.success(String.format("成功导入 %d æ¡ï¼Œè·³è¿‡å·²å­˜åœ¨æ•°æ® %d æ¡", waitToSaveList.size(), skipCount));
            }
        } catch (Exception e) {
            log.error("导入产品规格异常", e);
            throw new ServiceException("导入失败");
        }
        String message = skipCount > 0
                ? String.format("成功导入 %d æ¡ï¼Œè·³è¿‡å·²å­˜åœ¨ %d æ¡", successCount, skipCount)
                : String.format("成功导入 %d æ¡", successCount);
        return AjaxResult.success(message);
    }
    private Long findOrCreateTopProduct(String name) {
        Product root = productMapper.selectOne(new LambdaQueryWrapper<Product>()
                .isNull(Product::getParentId)
                .eq(Product::getProductName, name)
                .last("limit 1"));
        if (root != null) {
            return root.getId();
        }
        Product product = new Product();
        product.setProductName(name);
        productMapper.insert(product);
        return product.getId();
    }
    private Long ensureChildNode(Map<String, Long> nodeCache, String path, Long parentId, String name) {
        Long id = nodeCache.get(path);
        if (id != null) {
            return id;
        }
        Product existing = productMapper.selectOne(new LambdaQueryWrapper<Product>()
                .eq(Product::getParentId, parentId)
                .eq(Product::getProductName, name)
                .last("limit 1"));
        if (existing != null) {
            nodeCache.put(path, existing.getId());
            return existing.getId();
        }
        Product product = new Product();
        product.setParentId(parentId);
        product.setProductName(name);
        productMapper.insert(product);
        nodeCache.put(path, product.getId());
        return product.getId();
    }
    private Set<String> loadProductCodes(Long productId) {
        List<ProductModel> list = productModelMapper.selectList(new LambdaQueryWrapper<ProductModel>()
                .eq(ProductModel::getProductId, productId));
        Set<String> codes = new HashSet<>();
        for (ProductModel productModel : list) {
            if (productModel.getProductCode() != null) {
                codes.add(productModel.getProductCode().trim());
            }
        }
        return codes;
    }
    private Set<String> loadProductModels(Long productId) {
        List<ProductModel> list = productModelMapper.selectList(new LambdaQueryWrapper<ProductModel>()
                .eq(ProductModel::getProductId, productId));
        Set<String> models = new HashSet<>();
        for (ProductModel productModel : list) {
            if (StringUtils.isEmpty(productModel.getModel())) {
                models.add("/");
            } else {
                models.add(productModel.getModel().trim());
            }
        }
        return models;
    }
    private String generateUniqueCode(Set<String> generatedCodes) {
        for (int i = 0; i < 200; i++) {
            String code = randomCode(5);
            if (generatedCodes.contains(code)) {
                continue;
            }
            Long count = productModelMapper.selectCount(new LambdaQueryWrapper<ProductModel>()
                    .eq(ProductModel::getProductCode, code));
            if (count == null || count == 0) {
                generatedCodes.add(code);
                return code;
            }
        }
        throw new ServiceException("生成唯一产品编码失败,请重试");
    }
    private static String randomCode(int length) {
        String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++) {
            sb.append(chars.charAt(ThreadLocalRandom.current().nextInt(chars.length())));
        }
        return sb.toString();
    }
    private static String trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }
}
src/main/java/com/ruoyi/basic/service/impl/ProductServiceImpl.java
@@ -15,15 +15,23 @@
import com.ruoyi.basic.vo.ProductModelVo;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Service
@AllArgsConstructor
@Slf4j
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements IProductService {
    private ProductMapper productMapper;
@@ -130,16 +138,40 @@
    }
    @Override
    public void exportImportTemplate(HttpServletResponse response) {
        String templatePath = "static/产品导入模板.xlsx";
        try (InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(templatePath)) {
            if (inputStream == null) {
                throw new FileNotFoundException("模板文件不存在:" + templatePath);
            }
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            String fileName = URLEncoder.encode("产品导入模板.xlsx", "utf-8").replaceAll("\\+", "%20");
            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);
            try (OutputStream outputStream = response.getOutputStream()) {
                byte[] buffer = new byte[1024];
                int len;
                while ((len = inputStream.read(buffer)) > 0) {
                    outputStream.write(buffer, 0, len);
                }
                outputStream.flush();
            }
        } catch (IOException e) {
            log.error("导出产品导入模板失败", e);
            try {
                response.getWriter().write("模板导出失败:" + e.getMessage());
            } catch (IOException ex) {
                log.error("响应输出错误", ex);
            }
        }
    }
    @Override
    public int delProductByIds(Long[] ids) {
        // 1. åˆ é™¤å­è¡¨ product_model ä¸­å…³è”的数据
        LambdaQueryWrapper<ProductModel> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(ProductModel::getProductId, ids);
        productModelMapper.delete(queryWrapper);
        // 2. åˆ é™¤ä¸»è¡¨ product æ•°æ®
        int deleteCount = productMapper.deleteBatchIds(Arrays.asList(ids));
        return deleteCount;
        return productMapper.deleteBatchIds(Arrays.asList(ids));
    }
}
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
@@ -22,10 +22,10 @@
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.enums.ApprovalStatusEnum;
import com.ruoyi.common.enums.ReviewStatusEnum;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
@@ -129,6 +129,7 @@
        if (purchaseLedger.getApprovalStatus() != null) {
            queryWrapper.eq(PurchaseLedger::getApprovalStatus, purchaseLedger.getApprovalStatus());
        }
        queryWrapper.orderByDesc(PurchaseLedger::getEntryDate);
        return purchaseLedgerMapper.selectList(queryWrapper);
    }
@@ -910,20 +911,37 @@
            List<SysUser> sysUsers = sysUserMapper.selectList(new LambdaQueryWrapper<SysUser>().in(SysUser::getNickName,
                    salesLedgerImportDtoList.stream().map(PurchaseLedgerImportDto::getRecorderName).collect(Collectors.toList())));
            for (PurchaseLedgerImportDto salesLedgerImportDto : salesLedgerImportDtoList) {
                PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectOne(new LambdaQueryWrapper<PurchaseLedger>()
                PurchaseLedger existPurchaseLedger = purchaseLedgerMapper.selectOne(new LambdaQueryWrapper<PurchaseLedger>()
                        .eq(PurchaseLedger::getPurchaseContractNumber, salesLedgerImportDto.getPurchaseContractNumber())
                        .last("limit 1"));
                if(purchaseLedger != null){
                // å·²å®¡æ ¸é€šè¿‡çš„采购单跳过,不再重复导入
                if (existPurchaseLedger != null && ApprovalStatusEnum.APPROVED.getCode().equals(existPurchaseLedger.getApprovalStatus())) {
                    continue;
                }
                PurchaseLedger salesLedger = new PurchaseLedger();
                BeanUtils.copyProperties(salesLedgerImportDto, salesLedger);
                // é€šè¿‡ä¾›åº”商名称查询ID
                salesLedger.setSupplierId(customers.stream()
                        .filter(customer -> customer.getSupplierName().equals(salesLedger.getSupplierName()))
                        .findFirst()
                        .map(SupplierManage::getId)
                        .orElse(null));
                // å¾…审核状态的采购单:删除该单下的产品详情和审批实例,重新录入详情
                if (existPurchaseLedger != null) {
                    salesLedgerProductMapper.delete(new LambdaQueryWrapper<SalesLedgerProduct>()
                            .eq(SalesLedgerProduct::getSalesLedgerId, existPurchaseLedger.getId())
                            .eq(SalesLedgerProduct::getType, 2));
                    // æ¸…理该单下的质检记录及质检参数
                    deleteQualityInspectByPurchaseLedger(existPurchaseLedger.getId());
                    ApprovalInstance existApprovalInstance = approvalInstanceService.getOne(
                            Wrappers.<ApprovalInstance>lambdaQuery()
                                    .eq(ApprovalInstance::getBusinessId, existPurchaseLedger.getId())
                                    .eq(ApprovalInstance::getBusinessType, 5L)
                                    .eq(ApprovalInstance::getDeleted, 0)
                                    .orderByDesc(ApprovalInstance::getId)
                                    .last("limit 1"));
                    if (existApprovalInstance != null) {
                        approvalInstanceService.delete(Collections.singletonList(existApprovalInstance.getId()));
                    }
                    salesLedger.setId(existPurchaseLedger.getId());
                    salesLedger.setApprovalStatus(ApprovalStatusEnum.PENDING.getCode());
                }
                // é€šè¿‡ä¾›åº”商名称查询ID,不存在则自动新增
                salesLedger.setSupplierId(findOrCreateSupplier(salesLedger.getSupplierName(), customers));
                Long aLong = sysUsers.stream()
                        .filter(sysUser -> sysUser.getNickName().equals(salesLedger.getRecorderName()))
                        .findFirst()
@@ -963,7 +981,12 @@
                    String collect = ids.stream().map(Object::toString).collect(Collectors.joining(","));
                    salesLedger.setApproveUserIds(collect);
                }
                purchaseLedgerMapper.insert(salesLedger);
                if (salesLedger.getId() == null) {
                    purchaseLedgerMapper.insert(salesLedger);
                } else {
                    // å¾…审核状态重新录入时,更新主表
                    purchaseLedgerMapper.updateById(salesLedger);
                }
                for (PurchaseLedgerProductImportDto salesLedgerProductImportDto : salesLedgerProductImportDtos) {
                    SalesLedgerProduct salesLedgerProduct = new SalesLedgerProduct();
@@ -971,22 +994,25 @@
                    salesLedgerProduct.setSalesLedgerId(salesLedger.getId());
                    salesLedgerProduct.setType(2);
                    // è®¡ç®—不含税总价
                    salesLedgerProduct.setTaxExclusiveTotalPrice(salesLedgerProduct.getTaxInclusiveTotalPrice().divide(new BigDecimal(1).add(salesLedgerProduct.getTaxRate().divide(new BigDecimal(100))), 2, RoundingMode.HALF_UP));
                    list.stream()
                            .filter(map -> map.get("productName").equals(salesLedgerProduct.getProductCategory()) && map.get("model").equals(salesLedgerProduct.getSpecificationModel()))
                            .findFirst()
                            .ifPresent(map -> {
                                salesLedgerProduct.setProductModelId(Long.parseLong(map.get("modelId").toString()));
                                salesLedgerProduct.setProductId(Long.parseLong(map.get("id").toString()));
                            });
                    salesLedgerProduct.setTaxExclusiveTotalPrice(salesLedgerProduct.getTaxInclusiveTotalPrice().divide(new BigDecimal(1).add(salesLedgerProduct.getTaxRate().divide(new BigDecimal(100))), 6, RoundingMode.HALF_UP));
                    // äº§å“å¤§ç±»/规格型号不存在时自动创建
                    String productName = salesLedgerProduct.getProductCategory();
                    if (StringUtils.isNotBlank(productName)) {
                        // è§„格型号为空时用产品大类作为规格型号
                        String model = StringUtils.isNotBlank(salesLedgerProduct.getSpecificationModel())
                                ? salesLedgerProduct.getSpecificationModel()
                                : productName;
                        salesLedgerProduct.setSpecificationModel(model);
                        Long productId = findOrCreateProduct(productName, list);
                        Long modelId = findOrCreateProductModel(productId, productName, model,
                                salesLedgerProduct.getUnit(), list);
                        salesLedgerProduct.setProductId(productId);
                        salesLedgerProduct.setProductModelId(modelId);
                    }
                    salesLedgerProduct.setRegister(loginUser.getNickName());
                    salesLedgerProduct.setRegisterDate(LocalDateTime.now());
                    salesLedgerProduct.setApproveStatus(0);
                    // æ˜¯å¦è´¨æ£€åˆ¤æ–­
                    salesLedgerProduct.setIsChecked(salesLedgerProductImportDto.getIsChecked() == 1);
                    if(salesLedgerProductImportDto.getIsChecked() == 1){
                        addQualityInspect(salesLedger, salesLedgerProduct);
                    }
                    salesLedgerProductMapper.insert(salesLedgerProduct);
                }
                // é‡‡è´­å®¡æ ¸
@@ -1000,6 +1026,130 @@
        return AjaxResult.success("导入失败");
    }
    /**
     * æŸ¥æ‰¾æˆ–新增供应商:supplier_manage è¡¨æŒ‰ supplier_name åŒ¹é…ï¼Œä¸å­˜åœ¨åˆ™æ–°å¢žï¼Œç¼ºå¤±ä¿¡æ¯é»˜è®¤å†™"/"
     */
    private Long findOrCreateSupplier(String supplierName, List<SupplierManage> supplierList) {
        if (StringUtils.isBlank(supplierName)) {
            return null;
        }
        // å†…存列表中查找(本次导入已新增的也包含在内)
        for (SupplierManage supplier : supplierList) {
            if (supplierName.equals(supplier.getSupplierName())) {
                return supplier.getId();
            }
        }
        // æ•°æ®åº“查找
        SupplierManage existSupplier = supplierManageMapper.selectOne(new LambdaQueryWrapper<SupplierManage>()
                .eq(SupplierManage::getSupplierName, supplierName)
                .last("limit 1"));
        if (existSupplier != null) {
            supplierList.add(existSupplier);
            return existSupplier.getId();
        }
        // æ–°å¢žä¾›åº”商,缺失信息(税号、地址、电话等)默认写"/"
        SupplierManage newSupplier = new SupplierManage();
        newSupplier.setSupplierName(supplierName);
        newSupplier.setTaxpayerIdentificationNum("/");
        newSupplier.setCompanyAddress("/");
        newSupplier.setSupplierType("/");
        newSupplier.setCompanyPhone("/");
        newSupplier.setBankAccountName("/");
        newSupplier.setBankAccountNum("/");
        newSupplier.setContactUserName("/");
        newSupplier.setContactUserPhone("/");
        supplierManageMapper.insert(newSupplier);
        supplierList.add(newSupplier);
        return newSupplier.getId();
    }
    /**
     * æŸ¥æ‰¾æˆ–新增产品大类:product è¡¨æŒ‰ product_name åŒ¹é…ï¼Œä¸å­˜åœ¨åˆ™æ–°å¢žï¼Œé»˜è®¤çˆ¶ID为278
     */
    private Long findOrCreateProduct(String productName, List<Map<String, Object>> productAndModelList) {
        // å†…存列表中查找(本次导入已新增的也包含在内)
        for (Map<String, Object> map : productAndModelList) {
            if (map.get("productName") != null && productName.equals(map.get("productName"))) {
                return Long.valueOf(map.get("id").toString());
            }
        }
        // æ•°æ®åº“查找(已存在但暂无规格型号的产品不在内存列表中)
        Product existProduct = productMapper.selectOne(new LambdaQueryWrapper<Product>()
                .eq(Product::getProductName, productName)
                .last("limit 1"));
        if (existProduct != null) {
            return existProduct.getId();
        }
        // æ–°å¢žäº§å“å¤§ç±»ï¼Œé»˜è®¤çˆ¶ID为278
        Product product = new Product();
        product.setProductName(productName);
        product.setParentId(278L);
        productMapper.insert(product);
        return product.getId();
    }
    /**
     * æŸ¥æ‰¾æˆ–新增产品规格型号:product_model æŒ‰ äº§å“ID+规格型号 åŒ¹é…ï¼Œä¸å­˜åœ¨åˆ™æ–°å¢žã€‚
     * äº§å“ç¼–码 = è§„格型号 + "_" + ä¸‰ä½æµæ°´å·ï¼›è§„格型号为空时用产品大类作为规格型号。
     */
    private Long findOrCreateProductModel(Long productId, String productName, String model, String unit,
                                          List<Map<String, Object>> productAndModelList) {
        // å†…存列表中查找
        for (Map<String, Object> map : productAndModelList) {
            if (map.get("id") != null && Long.valueOf(map.get("id").toString()).equals(productId)
                    && model.equals(map.get("model"))) {
                return Long.valueOf(map.get("modelId").toString());
            }
        }
        // æ•°æ®åº“查找
        ProductModel existProductModel = productModelMapper.selectOne(new LambdaQueryWrapper<ProductModel>()
                .eq(ProductModel::getProductId, productId)
                .eq(ProductModel::getModel, model)
                .last("limit 1"));
        if (existProductModel != null) {
            return existProductModel.getId();
        }
        // æ–°å¢žè§„格型号,产品编码 = è§„格型号 + "_" + ä¸‰ä½æµæ°´å·ï¼ˆåŒäº§å“ä¸‹è‡ªå¢žï¼‰
        long count = productModelMapper.selectCount(new LambdaQueryWrapper<ProductModel>()
                .eq(ProductModel::getProductId, productId));
        ProductModel productModel = new ProductModel();
        productModel.setProductId(productId);
        productModel.setModel(model);
        productModel.setUnit(unit);
        productModel.setProductCode(model + "_" + String.format("%03d", count + 1));
        productModelMapper.insert(productModel);
        // è®°å½•到内存列表,避免本次导入内重复创建
        Map<String, Object> newMap = new HashMap<>();
        newMap.put("id", productId);
        newMap.put("modelId", productModel.getId());
        newMap.put("productName", productName);
        newMap.put("model", model);
        productAndModelList.add(newMap);
        return productModel.getId();
    }
    /**
     * åˆ é™¤é‡‡è´­å•下的质检记录及质检参数(已提交的检验单不允许删除)
     */
    private void deleteQualityInspectByPurchaseLedger(Long purchaseLedgerId) {
        LambdaQueryWrapper<QualityInspect> inspectWrapper = new LambdaQueryWrapper<>();
        inspectWrapper.eq(QualityInspect::getPurchaseLedgerId, purchaseLedgerId);
        List<QualityInspect> qualityInspects = qualityInspectMapper.selectList(inspectWrapper);
        for (QualityInspect qualityInspect : qualityInspects) {
            if (ObjectUtils.isNotEmpty(qualityInspect.getInspectState()) && qualityInspect.getInspectState().equals(1)) {
                throw new BaseException("已提交的检验单不能删除");
            }
        }
        List<Long> inspectIds = qualityInspects.stream()
                .map(QualityInspect::getId)
                .collect(Collectors.toList());
        if (!inspectIds.isEmpty()) {
            qualityInspectParamMapper.delete(new LambdaQueryWrapper<QualityInspectParam>()
                    .in(QualityInspectParam::getInspectId, inspectIds));
        }
        qualityInspectMapper.delete(inspectWrapper);
    }
    @Override
    public PurchaseLedgerDto getPurchaseByCode(PurchaseLedgerDto purchaseLedgerDto) {
        // 1. æŸ¥è¯¢ä¸»è¡¨
src/main/java/com/ruoyi/sales/pojo/SalesQuotationProduct.java
@@ -4,6 +4,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@@ -27,11 +28,11 @@
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "单价")
    private Double unitPrice;
    private BigDecimal unitPrice;
    @Schema(description = "数量")
    private Integer quantity;
    private BigDecimal quantity;
    @Schema(description = "金额")
    private Double amount;
    private BigDecimal amount;
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java
@@ -321,7 +321,7 @@
            BigDecimal remain = stockInventory.getQualitity()
                    .subtract(stockInventory.getLockedQuantity())
                    .subtract(required)
                    .divide(BigDecimal.ONE, 2, RoundingMode.CEILING);
                    .divide(BigDecimal.ONE, 6, RoundingMode.CEILING);
            if (remain.compareTo(BigDecimal.ZERO) < 0) {
                count++;
                stringBuffer.append(structure.getProductName())
src/main/java/com/ruoyi/technology/service/impl/TechnologyBomServiceImpl.java
@@ -116,9 +116,21 @@
        }
        if (oldBom.getProductModelId() != null && !oldBom.getProductModelId().equals(technologyBom.getProductModelId())) {
            technologyRoutingMapper.updateProductModelByBomId(technologyBom.getProductModelId(), technologyBom.getId().longValue());
            technologyBomStructureService.remove(Wrappers.<TechnologyBomStructure>lambdaQuery()
                    .eq(TechnologyBomStructure::getBomId, technologyBom.getId().longValue()));
            initRootStructure(technologyBom.getId().longValue(), technologyBom.getProductModelId());
            // è§„格变化时仅更新根节点指向的规格,保留下级BOM结构
            List<TechnologyBomStructure> roots = technologyBomStructureService.list(Wrappers.<TechnologyBomStructure>lambdaQuery()
                    .eq(TechnologyBomStructure::getBomId, technologyBom.getId().longValue())
                    //.isNull(TechnologyBomStructure::getParentId)
            );
            if (roots.isEmpty()) {
                initRootStructure(technologyBom.getId().longValue(), technologyBom.getProductModelId());
            } else {
                ProductModel productModel = productModelService.getById(technologyBom.getProductModelId());
                for (TechnologyBomStructure root : roots) {
                    root.setProductModelId(technologyBom.getProductModelId());
                    root.setUnit(productModel.getUnit());
                    technologyBomStructureService.updateById(root);
                }
            }
        }
        if (technologyBom.getBomNo() == null) {
            technologyBom.setBomNo(oldBom.getBomNo());
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml
@@ -160,8 +160,8 @@
        SUM(slp.quantity) AS purchaseNum,
        SUM(slp.tax_inclusive_total_price) AS purchaseAmount,
        COUNT(DISTINCT slp.sales_ledger_id) AS purchaseTimes,
        <!-- å¹³å‡å•ä»· = æ€»é‡‡è´­é‡‘额/总采购数量,保留2位小数,避免除0 -->
        ROUND(IF(SUM(slp.quantity) = 0, 0, SUM(slp.tax_inclusive_total_price) / SUM(slp.quantity)), 2) AS averagePrice,
        <!-- å¹³å‡å•ä»· = æ€»é‡‡è´­é‡‘额/总采购数量,保留6位小数,避免除0 -->
        ROUND(IF(SUM(slp.quantity) = 0, 0, SUM(slp.tax_inclusive_total_price) / SUM(slp.quantity)), 6) AS averagePrice,
        <!-- è¯¥äº§å“å¤§ç±»ä¸‹æœ€åŽä¸€ä¸ªå½•入日期(取台账主表的entry_date) -->
        MAX(sl.entry_date) AS entryDate,
        COALESCE(NULLIF(SUM(t1.return_quantity), 0), 0) AS return_quantity,
src/main/resources/static/²úÆ·µ¼ÈëÄ£°å.xlsx
Binary files differ