gaoluyang
7 天以前 a3d3ba426c2a5a6015888d5e32e0f9704b4d125c
Merge remote-tracking branch 'origin/dev_天津_建诚恒商贸' into dev_天津_建诚恒商贸
已添加4个文件
已修改17个文件
921 ■■■■■ 文件已修改
doc/20260913_alter_quantity_scale_8.sql 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/product-inventory-management-new-pro.sql 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/aftersalesservice/service/impl/AfterSalesServiceServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java 74 ●●●●● 补丁 | 查看 | 原始文档 | 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 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/ProductModelServiceImpl.java 213 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/ProductServiceImpl.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyBomServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-qhddzxc.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/AccountStatementMapper.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/SupplierManageMapper.xml 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInventoryMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/产品导入模板.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260913_alter_quantity_scale_8.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
-- æ•°é‡å­—段统一保留 8 ä½å°æ•°ï¼ˆdecimal(20, 8):整数位保留 12 ä½ï¼‰
-- æ¶‰åŠåŠŸèƒ½ï¼š
--   1. äº§å“ç»“构(BOM)单位产出所需数量(产品结构详情、编辑弹窗)
--   2. ç”Ÿäº§è®¢å•-工艺路线 BOM ç»“构单位产出所需数量
--   3. ç”Ÿäº§è®¢å•-领料数量(领料台账弹窗)
--   4. åº“存链路(领料扣减库存、出库/入库记录、合格/不合格库存、订单锁定数量)
-- ----------------------------
-- BOM ç»“构表
-- ----------------------------
ALTER TABLE `technology_bom_structure`
    MODIFY COLUMN `unit_quantity` decimal(20, 8) NOT NULL COMMENT '单位产出需要数量';
ALTER TABLE `production_bom_structure`
    MODIFY COLUMN `unit_quantity` decimal(20, 8) NOT NULL COMMENT '单位产出需要数量';
-- ----------------------------
-- é¢†æ–™è¡¨ï¼ˆproduction_order_pick.quantity å­˜é¢†ç”¨æ•°é‡ï¼‰
-- ----------------------------
ALTER TABLE `production_order_pick`
    MODIFY COLUMN `quantity` decimal(20, 8) NULL DEFAULT NULL COMMENT '数量';
ALTER TABLE `production_order_pick_record`
    MODIFY COLUMN `pick_quantity` decimal(20, 8) NULL DEFAULT NULL COMMENT '领料数量',
    MODIFY COLUMN `before_quantity` decimal(20, 8) NULL DEFAULT NULL COMMENT '领料前数量',
    MODIFY COLUMN `after_quantity` decimal(20, 8) NULL DEFAULT NULL COMMENT '领料后剩余数量';
-- ----------------------------
-- åº“存链路表
-- ----------------------------
ALTER TABLE `stock_inventory`
    MODIFY COLUMN `qualitity` decimal(20, 8) NULL DEFAULT NULL COMMENT '数量',
    MODIFY COLUMN `locked_quantity` decimal(20, 8) NULL DEFAULT 0.00000000 COMMENT '被订单锁定数量';
ALTER TABLE `stock_uninventory`
    MODIFY COLUMN `qualitity` decimal(20, 8) NULL DEFAULT NULL COMMENT '数量';
ALTER TABLE `stock_in_record`
    MODIFY COLUMN `stock_in_num` decimal(20, 8) NULL DEFAULT NULL COMMENT '入库数量';
ALTER TABLE `stock_out_record`
    MODIFY COLUMN `stock_out_num` decimal(20, 8) NULL DEFAULT NULL COMMENT '入库数量';
sql/product-inventory-management-new-pro.sql
@@ -2705,7 +2705,7 @@
  `product_model_id` bigint NOT NULL COMMENT '产品规格id',
  `technology_operation_id` bigint NULL DEFAULT NULL COMMENT '工序id',
  `production_order_bom_id` bigint NULL DEFAULT NULL COMMENT '生产订单BOMid',
  `unit_quantity` decimal(16, 4) NOT NULL COMMENT '单位产出需要数量',
  `unit_quantity` decimal(20, 8) NOT NULL COMMENT '单位产出需要数量',
  `demanded_quantity` decimal(16, 4) NULL DEFAULT NULL COMMENT '需求数量',
  `unit` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '单位',
  `create_user` bigint NULL DEFAULT NULL COMMENT '创建人ID',
@@ -2815,7 +2815,7 @@
CREATE TABLE `production_order_pick`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `product_model_id` int NULL DEFAULT NULL COMMENT '产品规格id',
  `quantity` decimal(16, 2) NULL DEFAULT NULL COMMENT '数量',
  `quantity` decimal(20, 8) NULL DEFAULT NULL COMMENT '数量',
  `batch_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '批号',
  `create_time` timestamp NULL DEFAULT NULL,
  `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
@@ -2845,9 +2845,9 @@
  `production_operation_task_id` bigint NULL DEFAULT 0 COMMENT '生产工单id',
  `product_model_id` int NULL DEFAULT NULL COMMENT '产品规格id',
  `batch_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '批号',
  `pick_quantity` decimal(16, 4) NULL DEFAULT NULL COMMENT '领料数量',
  `before_quantity` decimal(16, 4) NULL DEFAULT NULL COMMENT '领料前数量',
  `after_quantity` decimal(16, 4) NULL DEFAULT NULL COMMENT '领料后剩余数量',
  `pick_quantity` decimal(20, 8) NULL DEFAULT NULL COMMENT '领料数量',
  `before_quantity` decimal(20, 8) NULL DEFAULT NULL COMMENT '领料前数量',
  `after_quantity` decimal(20, 8) NULL DEFAULT NULL COMMENT '领料后剩余数量',
  `pick_type` tinyint NULL DEFAULT 1 COMMENT '领料类型:1正常领料  2补料',
  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -4763,7 +4763,7 @@
  `id` int NOT NULL AUTO_INCREMENT,
  `inbound_batches` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '入库批次',
  `batch_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '批号',
  `stock_in_num` decimal(16, 4) NULL DEFAULT NULL COMMENT '入库数量',
  `stock_in_num` decimal(20, 8) NULL DEFAULT NULL COMMENT '入库数量',
  `record_id` int NULL DEFAULT NULL COMMENT '入库来源id',
  `record_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '入库类型',
  `product_model_id` int NULL DEFAULT NULL COMMENT '产品规格id',
@@ -4788,11 +4788,11 @@
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `product_model_id` int NULL DEFAULT NULL COMMENT '规格id',
  `batch_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '批号',
  `qualitity` decimal(16, 4) NULL DEFAULT NULL COMMENT '数量',
  `qualitity` decimal(20, 8) NULL DEFAULT NULL COMMENT '数量',
  `create_time` timestamp NULL DEFAULT NULL,
  `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
  `version` int NULL DEFAULT NULL COMMENT '版本号',
  `locked_quantity` decimal(16, 4) NULL DEFAULT 0.0000 COMMENT '被订单锁定数量',
  `locked_quantity` decimal(20, 8) NULL DEFAULT 0.00000000 COMMENT '被订单锁定数量',
  `warn_num` decimal(16, 4) NULL DEFAULT NULL COMMENT '预警数量',
  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
  `create_user` bigint NULL DEFAULT NULL COMMENT '创建人ID',
@@ -4808,7 +4808,7 @@
  `id` int NOT NULL AUTO_INCREMENT,
  `outbound_batches` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '入库批次',
  `batch_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '批号',
  `stock_out_num` decimal(16, 4) NULL DEFAULT NULL COMMENT '入库数量',
  `stock_out_num` decimal(20, 8) NULL DEFAULT NULL COMMENT '入库数量',
  `record_id` int NULL DEFAULT NULL COMMENT '入库来源id',
  `record_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '入库类型',
  `product_model_id` int NULL DEFAULT NULL COMMENT '产品规格id',
@@ -4831,7 +4831,7 @@
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `product_model_id` int NULL DEFAULT NULL COMMENT '规格id',
  `batch_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '批号',
  `qualitity` decimal(16, 4) NULL DEFAULT NULL COMMENT '数量',
  `qualitity` decimal(20, 8) NULL DEFAULT NULL COMMENT '数量',
  `create_time` timestamp NULL DEFAULT NULL,
  `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
  `version` int NULL DEFAULT NULL COMMENT '版本号',
@@ -5302,7 +5302,7 @@
  `parent_id` bigint NULL DEFAULT NULL COMMENT '父节点ID',
  `product_model_id` bigint NOT NULL COMMENT '产品id',
  `operation_id` bigint NULL DEFAULT NULL COMMENT '工序id',
  `unit_quantity` decimal(16, 4) NOT NULL COMMENT '单位产出需要数量',
  `unit_quantity` decimal(20, 8) NOT NULL COMMENT '单位产出需要数量',
  `demanded_quantity` decimal(16, 4) NULL DEFAULT NULL COMMENT '需求数量',
  `unit` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '单位',
  `bom_id` bigint NOT NULL COMMENT 'bom的id',
src/main/java/com/ruoyi/aftersalesservice/service/impl/AfterSalesServiceServiceImpl.java
@@ -49,12 +49,12 @@
    @Override
    public IPage<AfterSalesServiceNewDto> listPage(Page page, AfterSalesServiceNewDto afterSalesService) {
        Long tenantId = SecurityUtils.getLoginUser().getTenantId();
        SysDept sysDept = sysDeptMapper.selectDeptById(tenantId);
        //Long tenantId = SecurityUtils.getLoginUser().getTenantId();
        //SysDept sysDept = sysDeptMapper.selectDeptById(tenantId);
        IPage<AfterSalesServiceNewDto> afterSalesServiceIPage = afterSalesServiceMapper.listPage(page, afterSalesService);
        afterSalesServiceIPage.getRecords().forEach(item -> {
            item.setDeptName(sysDept.getDeptName());
        });
        //afterSalesServiceIPage.getRecords().forEach(item -> {
        //    item.setDeptName(sysDept.getDeptName());
        //});
        return afterSalesServiceIPage;
    }
src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java
@@ -7,15 +7,26 @@
import dev.langchain4j.store.embedding.pinecone.PineconeServerlessIndexConfig;
import io.pinecone.clients.Index;
import io.pinecone.clients.Pinecone;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.util.function.Supplier;
/**
 * å‘量存储配置
 */
@Slf4j
@Configuration
public class EmbeddingStoreConfig {
    /** Pinecone è¿žæŽ¥é‡è¯•次数:构造期会同步发起 HTTP/2 æ¡æ‰‹ï¼Œå¶å‘ "Required SETTINGS preface not received" */
    private static final int CONNECT_ATTEMPTS = 3;
    /** é‡è¯•间隔基数(毫秒),按次数线性退避 */
    private static final long RETRY_BACKOFF_MILLIS = 1500L;
    @Value("${pinecone.api-key:pcsk_4SJLnh_tNB3wSLJU8tc4E5P28PcXX8eCLdURqZpVhg1FMV8CRYxjneWdzqRdB5Ftqooi9}")
    private String pineconeApiKey;
@@ -33,12 +44,12 @@
    @Bean
    public Index pineconeIndex(Pinecone pinecone) {
        return pinecone.getIndexConnection(indexName);
        return retry("获取 Pinecone ç´¢å¼•连接", () -> pinecone.getIndexConnection(indexName));
    }
    @Bean
    public EmbeddingStore<TextSegment> embeddingStore(EmbeddingModel embeddingModel) {
        return PineconeEmbeddingStore.builder()
        return retry("初始化 Pinecone å‘量存储", () -> PineconeEmbeddingStore.builder()
                .apiKey(pineconeApiKey)
                .index(indexName)
                .nameSpace(namespace)
@@ -47,6 +58,63 @@
                        .region("us-east-1")
                        .dimension(embeddingModel.dimension())
                        .build())
                .build();
                .build());
    }
    /**
     * å¯¹ Pinecone å¯åŠ¨æœŸè¿žæŽ¥åšæœ‰é™æ¬¡é‡è¯•ã€‚
     * <p>
     * Pinecone å®¢æˆ·ç«¯å›ºå®šèµ° HTTP/2,构造 EmbeddingStore / èŽ·å–ç´¢å¼•è¿žæŽ¥æ—¶ä¼šåŒæ­¥è°ƒç”¨æŽ§åˆ¶é¢æŽ¥å£ï¼Œ
     * å¶å‘握手失败(java.io.IOException: Required SETTINGS preface not received)会以运行时异常抛出,
     * ç›´æŽ¥å¯¼è‡´æ•´ä¸ª ApplicationContext åŠ è½½å¤±è´¥ã€‚è¿™é‡Œä»…å¯¹è¿™ç±»çž¬æ—¶ç½‘ç»œå¼‚å¸¸é‡è¯•ï¼Œå…¶å®ƒå¼‚å¸¸ç«‹å³æŠ›å‡ºã€‚
     */
    private <T> T retry(String action, Supplier<T> supplier) {
        RuntimeException last = null;
        for (int attempt = 1; attempt <= CONNECT_ATTEMPTS; attempt++) {
            try {
                return supplier.get();
            } catch (RuntimeException e) {
                if (!isTransientConnectFailure(e) || attempt == CONNECT_ATTEMPTS) {
                    throw e;
                }
                last = e;
                log.warn("{}失败(第 {}/{} æ¬¡ï¼‰ï¼Œ{}ms åŽé‡è¯•:{}",
                        action, attempt, CONNECT_ATTEMPTS, RETRY_BACKOFF_MILLIS * attempt, rootMessage(e));
                sleep(RETRY_BACKOFF_MILLIS * attempt);
            }
        }
        throw last;
    }
    /** æ²¿ cause é“¾åˆ¤æ–­æ˜¯å¦ä¸º HTTP/2 æ¡æ‰‹ç±»çš„瞬时连接失败 */
    private boolean isTransientConnectFailure(Throwable e) {
        for (Throwable t = e; t != null; t = t.getCause()) {
            if (t instanceof IOException) {
                return true;
            }
            String message = t.getMessage();
            if (message != null && (message.contains("SETTINGS preface")
                    || message.contains("Connection reset")
                    || message.contains("timed out"))) {
                return true;
            }
        }
        return false;
    }
    private String rootMessage(Throwable e) {
        Throwable root = e;
        while (root.getCause() != null && root.getCause() != root) {
            root = root.getCause();
        }
        return root.getMessage();
    }
    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
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,6 +8,7 @@
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;
@@ -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());
            if (CollectionUtils.isEmpty(productModelList)) {
                return AjaxResult.error("导入数据不能为空");
            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("未找到 [原料/半成品/成品] å·¥ä½œè¡¨ï¼Œæˆ–文件中没有数据");
            }
            //  èŽ·å–å½“å‰äº§å“ä¸‹æ‰€æœ‰çš„è§„æ ¼åž‹å·å
            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 successCount = 0;
            int skipCount = 0;
            for (int i = 0; i < productModelList.size(); i++) {
                ProductModel item = productModelList.get(i);
        Set<String> generatedCodes = new HashSet<>();
        for (String sheetName : IMPORT_SHEET_NAMES) {
            List<ProductImportRowDto> rows = sheetMap.get(sheetName);
            if (CollectionUtils.isEmpty(rows)) {
                continue;
            }
            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);
                existingModelNames.add(item.getModel());
                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;
            }
            if (!waitToSaveList.isEmpty()) {
                saveBatch(waitToSaveList);
                ProductModel productModel = new ProductModel();
                productModel.setProductId(leafId);
                productModel.setProductCode(code);
                productModel.setModel(model);
                productModel.setUnit(trimToNull(row.getUnit()));
                productModelMapper.insert(productModel);
                successCount++;
            }
            }
            if (skipCount == 0) {
                return AjaxResult.success(String.format("成功导入 %d æ¡æ•°æ®", waitToSaveList.size()));
        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 {
                return AjaxResult.success(String.format("成功导入 %d æ¡ï¼Œè·³è¿‡å·²å­˜åœ¨æ•°æ® %d æ¡", waitToSaveList.size(), skipCount));
                models.add(productModel.getModel().trim());
            }
        } catch (Exception e) {
            log.error("导入产品规格异常", e);
            throw new ServiceException("导入失败");
        }
        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/production/service/impl/ProductionBomStructureServiceImpl.java
@@ -217,22 +217,28 @@
            return;
        }
        List<ProductionBomStructure> updateList = new ArrayList<>();
        BigDecimal lastProcessDemandedQuantity = orderQuantity;
        // éœ€æ±‚量按“父节点需求量 Ã— æœ¬è¡Œå•耗”逐层计算,兄弟节点互不影响,结果与录入顺序无关。
        Map<Long, BigDecimal> demandedQuantityById = new HashMap<>();
        for (ProductionBomStructure structure : structureList) {
            if (structure == null || structure.getId() == null) {
                continue;
            }
            BigDecimal demandedQuantity = lastProcessDemandedQuantity.multiply(defaultDecimal(structure.getUnitQuantity()));
//            if (compareDecimal(structure.getDemandedQuantity(), demandedQuantity) == 0) {
//                continue;
//            }
            // æ ¹èŠ‚ç‚¹ä»¥è®¢å•æ•°é‡ä¸ºåŸºæ•°ï¼Œå­èŠ‚ç‚¹ä»¥çˆ¶èŠ‚ç‚¹éœ€æ±‚é‡ä¸ºåŸºæ•°ã€‚
            boolean rootNode = structure.getParentId() == null || structure.getParentId() == 0L;
            BigDecimal baseQuantity = rootNode
                    ? orderQuantity
                    : demandedQuantityById.get(structure.getParentId());
            if (baseQuantity == null) {
                throw new ServiceException("BOM结构数据异常,存在找不到父节点的子节点");
            }
            BigDecimal demandedQuantity = baseQuantity.multiply(defaultDecimal(structure.getUnitQuantity()));
            ProductionBomStructure update = new ProductionBomStructure();
            update.setId(structure.getId());
            update.setDemandedQuantity(demandedQuantity);
            updateList.add(update);
            structure.setDemandedQuantity(demandedQuantity);
            lastProcessDemandedQuantity = demandedQuantity;
            demandedQuantityById.put(structure.getId(), demandedQuantity);
        }
        if (!updateList.isEmpty()) {
            this.updateBatchById(updateList);
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -439,7 +439,8 @@
        productionOrderBomMapper.insert(orderBom);
        Map<Long, Long> idMap = new HashMap<>();
        BigDecimal lastProcessDemandedQuantity = orderQuantity;
        // éœ€æ±‚量按“父节点需求量 Ã— æœ¬è¡Œå•耗”逐层计算,兄弟节点互不影响,结果与录入顺序无关。
        Map<Long, BigDecimal> demandedQuantityBySourceId = new HashMap<>();
        for (TechnologyBomStructure source : structureList) {
            // å­èŠ‚ç‚¹ parentId éœ€è¦æ˜ å°„成新快照节点 id,才能保留原始 BOM å±‚级。
            ProductionBomStructure target = new ProductionBomStructure();
@@ -449,11 +450,19 @@
            target.setProductModelId(source.getProductModelId());
            target.setTechnologyOperationId(source.getOperationId());
            target.setUnitQuantity(source.getUnitQuantity());
            target.setDemandedQuantity(lastProcessDemandedQuantity.multiply(source.getUnitQuantity()));
            // æ ¹èŠ‚ç‚¹ä»¥è®¢å•æ•°é‡ä¸ºåŸºæ•°ï¼Œå­èŠ‚ç‚¹ä»¥çˆ¶èŠ‚ç‚¹éœ€æ±‚é‡ä¸ºåŸºæ•°ã€‚
            boolean rootNode = source.getParentId() == null || source.getParentId() == 0L;
            BigDecimal baseQuantity = rootNode
                    ? orderQuantity
                    : demandedQuantityBySourceId.get(source.getParentId());
            if (baseQuantity == null) {
                throw new ServiceException("BOM结构数据异常,存在找不到父节点的子节点");
            }
            target.setDemandedQuantity(baseQuantity.multiply(defaultDecimal(source.getUnitQuantity())));
            target.setUnit(source.getUnit());
            productionBomStructureMapper.insert(target);
            idMap.put(source.getId(), target.getId());
            lastProcessDemandedQuantity = target.getDemandedQuantity();
            demandedQuantityBySourceId.put(source.getId(), target.getDemandedQuantity());
        }
        return orderBom;
    }
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -8,6 +8,9 @@
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.TextRenderData;
import com.deepoove.poi.policy.TextRenderPolicy;
import com.deepoove.poi.template.run.RunTemplate;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
@@ -32,6 +35,7 @@
import com.ruoyi.stock.service.StockInventoryService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -43,6 +47,7 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
@@ -247,6 +252,11 @@
        InputStream inputStream = this.getClass().getResourceAsStream("/static/report-template.docx");
        Configure configure = Configure.builder()
                .bind("paramList", new HackLoopTableRenderPolicy())
                .bind("inspect.checkTime", (eleTemplate, data, xwpfTemplate) -> {
                    XWPFRun run = ((RunTemplate) eleTemplate).getRun();
                    String text = data == null ? "" : DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, (Date) data);
                    TextRenderPolicy.Helper.renderTextRun(run, new TextRenderData(text));
                })
                .build();
        String finalInspectType = inspectType;
        XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java
@@ -38,7 +38,10 @@
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
@@ -133,11 +136,13 @@
            shippingInfoService.delete(shippingInfos.stream().map(ShippingInfo::getId).collect(Collectors.toList()));
        }
        // å¯èƒ½å±žäºŽå¤šä¸ªä¸»è¡¨
        Set<Long> mainIds = deletedProducts.stream()
                .map(SalesLedgerProduct::getSalesLedgerId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        // å¯èƒ½å±žäºŽå¤šä¸ªä¸»è¡¨ï¼ŒæŒ‰ type åˆ†ç»„(销售/采购产品子表共用且主表 id å¯èƒ½ç›¸åŒï¼‰
        Map<Integer, Set<Long>> mainIdsByType = new HashMap<>();
        deletedProducts.stream()
                .filter(product -> product.getSalesLedgerId() != null)
                .forEach(product -> mainIdsByType
                        .computeIfAbsent(product.getType() == null ? 1 : product.getType(), key -> new HashSet<>())
                        .add(product.getSalesLedgerId()));
        // 2. æ‰§è¡Œåˆ é™¤æ“ä½œ
        int result = salesLedgerProductMapper.deleteBatchIds(Arrays.asList(ids));
@@ -145,12 +150,15 @@
        deleteProductionData(Arrays.asList(ids));
        // 3. å¯¹æ¯ä¸ªä¸»è¡¨ID进行金额更新
        mainIdsByType.forEach((type, mainIds) -> {
        for (Long salesLedgerId : mainIds) {
            LambdaQueryWrapper<SalesLedgerProduct> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(SalesLedgerProduct::getSalesLedgerId, salesLedgerId);
                wrapper.eq(SalesLedgerProduct::getSalesLedgerId, salesLedgerId)
                        .eq(SalesLedgerProduct::getType, type);
            List<SalesLedgerProduct> remainingProducts = salesLedgerProductMapper.selectList(wrapper);
            // è°ƒç”¨é€šç”¨æ–¹æ³•更新主表金额
                if (type == 1) {
            updateMainContractAmount(
                    salesLedgerId,
                    remainingProducts,
@@ -158,7 +166,17 @@
                    salesLedgerMapper,
                    SalesLedger.class
            );
                } else {
                    updateMainContractAmount(
                            salesLedgerId,
                            remainingProducts,
                            SalesLedgerProduct::getTaxInclusiveTotalPrice,
                            purchaseLedgerMapper,
                            PurchaseLedger.class
                    );
        }
            }
        });
        return result;
    }
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -687,8 +687,10 @@
            return 0;
        }
        // åˆ é™¤é”€å”®ç®¡ç†æ•°æ®
        // äº§å“å­è¡¨é”€å”®/采购共用,且主表 id å¯èƒ½ç›¸åŒï¼Œå¿…须按 type åŒºåˆ†ï¼Œå¦åˆ™ä¼šè¯¯åˆ é‡‡è´­å°è´¦çš„产品数据
        LambdaQueryWrapper<SalesLedgerProduct> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(SalesLedgerProduct::getSalesLedgerId, idList)
                .eq(SalesLedgerProduct::getType, 1)
                .select(SalesLedgerProduct::getId);
        List<SalesLedgerProduct> products = salesLedgerProductMapper.selectList(queryWrapper);
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()));
            // è§„格变化时仅更新根节点指向的规格,保留下级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/application-qhddzxc.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,268 @@
# é¡¹ç›®ç›¸å…³é…ç½®
ruoyi:
  # åç§°
  name: RuoYi
  # ç‰ˆæœ¬
  version: 3.8.9
  # ç‰ˆæƒå¹´ä»½
  copyrightYear: 2025
  # æ–‡ä»¶è·¯å¾„ ç¤ºä¾‹ï¼ˆ Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
  profile: /javaWork/product-inventory-management/file
  # èŽ·å–ip地址开关
  addressEnabled: false
  # éªŒè¯ç ç±»åž‹ math æ•°å­—计算 char å­—符验证
  captchaType: math
  # ååŒå®¡æ‰¹ç¼–号前缀(配置文件后缀命名)
  approvalNumberPrefix: NEW
  # ä¸ªæŽ¨ Unipush é…ç½®
  getui:
    appId: PfjyAAE0FK64FaO1w2CMb1
    appKey: zTMb831OEL6J4GK1uE3Ob4
    masterSecret: K1GFtsv42v61tXGnF7SGE5
    domain: https://restapi.getui.cn/v2/
    # ç¦»çº¿æŽ¨é€ä½¿ç”¨çš„包名/组件名
    intentComponent: uni.app.UNI099A590/io.dcloud.PandoraEntry
# å¼€å‘环境配置
server:
  # æœåŠ¡å™¨çš„HTTP端口,默认为8080
  port: 9003
  servlet:
    # åº”用的访问路径
    context-path: /
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
    # è¿žæŽ¥æ•°æ»¡åŽçš„æŽ’队数,默认为100
    accept-count: 1000
    threads:
      # tomcat最大线程数,默认为200
      max: 800
      # Tomcat启动初始化的线程数,默认值10
      min-spare: 100
# æ—¥å¿—配置
logging:
  level:
    com.ruoyi: warn
    org.springframework: warn
minio:
  endpoint: http://114.132.189.42/
  port: 7019
  secure: false
  accessKey: admin
  secretKey: 12345678
  preview-expiry: 24 # é¢„览地址默认24小时
  default-bucket: jxc
# ç”¨æˆ·é…ç½®
user:
  password:
    # å¯†ç æœ€å¤§é”™è¯¯æ¬¡æ•°
    maxRetryCount: 5
    # å¯†ç é”å®šæ—¶é—´ï¼ˆé»˜è®¤10分钟)
    lockTime: 10
# Spring配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # ä¸»åº“数据源
      master:
        url: jdbc:mysql://172.17.0.1:3306/product-inventory-management-qhddzxc?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: xd@123456..
      # ä»Žåº“数据源
      slave:
        # ä»Žæ•°æ®æºå¼€å…³/默认关闭
        enabled: false
        url:
        username:
        password:
      # åˆå§‹è¿žæŽ¥æ•°
      initialSize: 5
      # æœ€å°è¿žæŽ¥æ± æ•°é‡
      minIdle: 10
      # æœ€å¤§è¿žæŽ¥æ± æ•°é‡
      maxActive: 20
      # é…ç½®èŽ·å–è¿žæŽ¥ç­‰å¾…è¶…æ—¶çš„æ—¶é—´
      maxWait: 60000
      # é…ç½®è¿žæŽ¥è¶…æ—¶æ—¶é—´
      connectTimeout: 30000
      # é…ç½®ç½‘络超时时间
      socketTimeout: 60000
      # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
      minEvictableIdleTimeMillis: 300000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å¤§ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # é…ç½®æ£€æµ‹è¿žæŽ¥æ˜¯å¦æœ‰æ•ˆ
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      webStatFilter:
        enabled: true
      statViewServlet:
        enabled: true
        # è®¾ç½®ç™½åå•,不填则允许所有访问
        allow:
        url-pattern: /druid/*
        # æŽ§åˆ¶å°ç®¡ç†ç”¨æˆ·åå’Œå¯†ç 
        login-username: ruoyi
        login-password: 123456
      filter:
        stat:
          enabled: true
          # æ…¢SQL记录
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true
  # èµ„源信息
  messages:
    # å›½é™…化资源文件路径
    basename: i18n/messages
  # æ–‡ä»¶ä¸Šä¼ 
  servlet:
    multipart:
      # å•个文件大小
      max-file-size: 1GB
      # è®¾ç½®æ€»ä¸Šä¼ çš„æ–‡ä»¶å¤§å°
      max-request-size: 2GB
  # æœåŠ¡æ¨¡å—
  devtools:
    restart:
      # çƒ­éƒ¨ç½²å¼€å…³
      enabled: false
  # redis é…ç½®
  data:
    mongodb:
      uri: mongodb://114.132.189.42:9028/chat_memory_db_qhddzxc
    # redis é…ç½®
    redis:
      # åœ°å€
#      host: 127.0.0.1
      host: 172.17.0.1
      # ç«¯å£ï¼Œé»˜è®¤ä¸º6379
      port: 6379
      # æ•°æ®åº“索引
      database: 0
      # å¯†ç 
      #    password: root2022!
      password:
      # è¿žæŽ¥è¶…æ—¶æ—¶é—´
      timeout: 10s
      lettuce:
        pool:
          # è¿žæŽ¥æ± ä¸­çš„æœ€å°ç©ºé—²è¿žæŽ¥
          min-idle: 0
          # è¿žæŽ¥æ± ä¸­çš„æœ€å¤§ç©ºé—²è¿žæŽ¥
          max-idle: 8
          # è¿žæŽ¥æ± çš„æœ€å¤§æ•°æ®åº“连接数
          max-active: 8
          # #连接池最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
  # Quartz定时任务配置(新增部分)
  quartz:
    job-store-type: jdbc  # ä½¿ç”¨æ•°æ®åº“存储
    jdbc:
      initialize-schema: never  # é¦–次运行时自动创建表结构,成功后改为never
      schema: classpath:org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql  # MySQL表结构脚本
    properties:
      org:
        quartz:
          scheduler:
            instanceName: RuoYiScheduler
            instanceId: AUTO
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate  # MySQL适配
            tablePrefix: qrtz_  # è¡¨åå‰ç¼€ï¼Œä¸Žè„šæœ¬ä¸€è‡´
            isClustered: false  # å•节点模式(集群需改为true)
            clusterCheckinInterval: 10000
            txIsolationLevelSerializable: true
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10  # çº¿ç¨‹æ± å¤§å°
            threadPriority: 5
            makeThreadsDaemons: true
          updateCheck: false  # å…³é—­ç‰ˆæœ¬æ£€æŸ¥
# token配置
token:
  # ä»¤ç‰Œè‡ªå®šä¹‰æ ‡è¯†
  header: Authorization
  # ä»¤ç‰Œå¯†é’¥
  secret: xpAVjhCjQDaDB7mjPAzMDSbQWXNu2zYkTdDNUsPMS5Xx8QMmQVYN7n74eZrYJxDJ
  # ä»¤ç‰Œæœ‰æ•ˆæœŸï¼ˆé»˜è®¤30分钟)
  expireTime: 450
# MyBatis Plus配置
mybatis-plus:
  # æœç´¢æŒ‡å®šåŒ…别名   æ ¹æ®è‡ªå·±çš„项目来
  typeAliasesPackage: com.ruoyi.**.pojo
  # é…ç½®mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath*:mapper/**/*Mapper.xml
  # åŠ è½½å…¨å±€çš„é…ç½®æ–‡ä»¶
  configLocation: classpath:mybatis/mybatis-config.xml
  global-config:
    enable-sql-runner: true
    db-config:
      id-type: auto
# PageHelper分页插件
pagehelper:
  helperDialect: mysql
  supportMethodsArguments: true
  params: count=countSql
# Swagger配置
swagger:
  # æ˜¯å¦å¼€å¯swagger
  enabled: true
  # è¯·æ±‚前缀
  pathMapping: /dev-api
# é˜²æ­¢XSS攻击
xss:
  # è¿‡æ»¤å¼€å…³
  enabled: true
  # æŽ’除链接(多个用逗号分隔)
  excludes: /system/notice
  # åŒ¹é…é“¾æŽ¥
  urlPatterns: /system/*,/monitor/*,/tool/*
# ä»£ç ç”Ÿæˆ
gen:
  # ä½œè€…
  author: ruoyi
  # é»˜è®¤ç”ŸæˆåŒ…路径 system éœ€æ”¹æˆè‡ªå·±çš„æ¨¡å—名称 å¦‚ system monitor tool
  packageName: com.ruoyi.project.system
  # è‡ªåŠ¨åŽ»é™¤è¡¨å‰ç¼€ï¼Œé»˜è®¤æ˜¯true
  autoRemovePre: false
  # è¡¨å‰ç¼€ï¼ˆç”Ÿæˆç±»åä¸ä¼šåŒ…含表前缀,多个用逗号分隔)
  tablePrefix: sys_
  # æ˜¯å¦å…è®¸ç”Ÿæˆæ–‡ä»¶è¦†ç›–到本地(自定义路径),默认不允许
  allowOverwrite: false
# æ–‡ä»¶ä¸Šä¼ é…ç½®
file:
  temp-dir: /javaWork/product-inventory-management/file/temp/uploads   # ä¸´æ—¶ç›®å½•
  upload-dir: /javaWork/product-inventory-management/file/prod/uploads # æ­£å¼ç›®å½•
  path: /javaWork/product-inventory-management/file # ä¸Šä¼ ç›®å½•
  urlPrefix: /prod-api/common # é“¾æŽ¥å‰ç¼€
  domain: http://1.15.17.182:9111 # åŸŸåå‰ç¼€
  expired: 120 # è¿‡æœŸæ—¶é—´(单位:分钟)
  useLimit: 10 # ä½¿ç”¨æ¬¡æ•°
  compress: true # æ˜¯å¦åŽ‹ç¼©
  needCompressSize: 10MB # åŽ‹ç¼©é˜ˆå€¼
  compressQuality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
src/main/resources/mapper/account/AccountStatementMapper.xml
@@ -122,12 +122,12 @@
            -- è¿›é¡¹ç¨Žæ˜Žç»†ï¼ˆé‡‡è´­è®¢å•)
            SELECT
                pl.purchase_contract_number AS invoiceNo,
                pl.sales_contract_no AS salesContractNo,
                pl.supplier_name AS supplierName,
                NULL AS customerName,
                pl.purchase_contract_number AS salesContractNo,
                NULL AS supplierName,
                pl.supplier_name AS customerName,
                '进项' AS orderType,
                pl.entry_date AS invoiceDate,
                slp.tax_rate AS taxRate,
                MAX(slp.tax_rate) AS taxRate,
                ROUND(SUM(slp.tax_inclusive_total_price - slp.tax_exclusive_total_price), 2) AS jTaxAmount,
                0 AS xTaxAmount,
                ROUND(SUM(slp.tax_inclusive_total_price - slp.tax_exclusive_total_price), 2) AS taxAmount
@@ -148,7 +148,7 @@
                sl.customer_name AS customerName,
                '销项' AS orderType,
                sl.entry_date AS invoiceDate,
                slp.tax_rate AS taxRate,
                MAX(slp.tax_rate)  AS taxRate,
                0 AS jTaxAmount,
                ROUND(SUM(slp.tax_inclusive_total_price - slp.tax_exclusive_total_price), 2) AS xTaxAmount,
                ROUND(SUM(slp.tax_inclusive_total_price - slp.tax_exclusive_total_price), 2) AS taxAmount
src/main/resources/mapper/basic/SupplierManageMapper.xml
@@ -70,44 +70,74 @@
        </where>
    </select>
    <select id="supplierTransactions" resultType="com.ruoyi.purchase.vo.SupplierTransactionsVo">
        select T1.supplier_id,
        select c.supplier_id,
               sm.supplier_name,
               T1.contractAmounts,
               IFNULL(T2.paymentAmount, 0) AS paymentAmount,
               IFNULL(T3.InboundAmount, 0) - IFNULL(T4.returnAmount, 0) AS payableAmount
        from (select supplier_id, sum(contract_amount) as contractAmounts from purchase_ledger group by supplier_id) T1
        left join (select supplier_id, sum(payment_amount) as paymentAmount from account_purchase_payment group by supplier_id) T2 on T1.supplier_id = T2.supplier_id
        left join (
            SELECT
                pl.supplier_id,
                sum(sir.stock_in_num * slp.tax_inclusive_unit_price) AS InboundAmount
            FROM stock_in_record sir
                     -- 10 ç±»åž‹æ‰å…³è”质检表
                     LEFT JOIN quality_inspect qi ON sir.record_type = 10 AND sir.record_id = qi.id
                -- åŠ¨æ€å…³è”é‡‡è´­ï¼ˆè‡ªåŠ¨é€‚é… 7 å’Œ 10)
                     LEFT JOIN purchase_ledger pl
                               ON pl.id = IF(sir.record_type = 7, sir.record_id, qi.purchase_ledger_id)
                -- äº§å“å…³è”不动
                     LEFT JOIN sales_ledger_product slp ON pl.id = slp.sales_ledger_id
            -- æ¡ä»¶
            WHERE sir.approval_status = 1 AND slp.type = 2
              AND sir.record_type IN ('7','10')
            group by pl.supplier_id
        ) T3 on T3.supplier_id=T1.supplier_id
               c.contractAmounts,
               -- ä»˜æ¬¾é‡‘额:逐合同按合同金额封顶后再汇总,避免重复付款导致付款金额大于合同金额
               c.paymentAmount,
               -- åº”付金额:逐合同「取(入库金额, åˆåŒé‡‘额)较小者 - å°é¡¶åŽä»˜æ¬¾é‡‘额 - é€€è´§é‡‘额」后再汇总,下限为 0
               c.payableAmount
        from (
            select pl.supplier_id,
                   sum(COALESCE(pl.contract_amount, 0)) as contractAmounts,
                   sum(LEAST(IFNULL(pay.paymentAmount, 0), COALESCE(pl.contract_amount, 0))) as paymentAmount,
                   sum(GREATEST(
                       IFNULL(ib.InboundAmount, 0)
                           - IF(ib.InboundAmount <![CDATA[ > ]]> COALESCE(pl.contract_amount, 0), ib.InboundAmount - COALESCE(pl.contract_amount, 0), 0)
                       - LEAST(IFNULL(pay.paymentAmount, 0), COALESCE(pl.contract_amount, 0))
                       - IFNULL(rt.returnAmount, 0)
                   , 0)) as payableAmount
            from purchase_ledger pl
        left join (
            select
                supplier_id,
                sum(total_amount) as returnAmount
                    t.purchase_ledger_id as id,
                    sum(t.payment_amount) as paymentAmount
                from (
                    select
                        app.id,
                        app.payment_amount,
                        pl.id as purchase_ledger_id
                    from account_purchase_payment app
                    left join account_payment_application apa on app.account_payment_application_id = apa.id
                    left join stock_in_record sir on FIND_IN_SET(sir.id, apa.stock_in_record_ids) > 0
                    LEFT JOIN quality_inspect qi ON sir.record_type = 10 AND sir.record_id = qi.id
                    LEFT JOIN purchase_ledger pl
                              ON pl.id = IF(sir.record_type = 7, sir.record_id, qi.purchase_ledger_id)
                    WHERE sir.approval_status = 1
                      AND sir.record_type IN ('7','10')
                    group by app.id, pl.id
                ) t
                group by t.purchase_ledger_id
            ) pay on pay.id = pl.id
            left join (
                SELECT
                    pl.id,
                    sum(sir.stock_in_num * slp.tax_inclusive_unit_price) AS InboundAmount
                FROM stock_in_record sir
                         LEFT JOIN quality_inspect qi ON sir.record_type = 10 AND sir.record_id = qi.id
                         LEFT JOIN purchase_ledger pl
                                   ON pl.id = IF(sir.record_type = 7, sir.record_id, qi.purchase_ledger_id)
                         LEFT JOIN sales_ledger_product slp ON pl.id = slp.sales_ledger_id
                WHERE sir.approval_status = 1 AND slp.type = 2
                  AND sir.record_type IN ('7','10')
                group by pl.id
            ) ib on ib.id = pl.id
            left join (
                select pl.id,
                       sum(pro.total_amount) as returnAmount
            from purchase_return_orders pro
            group by supplier_id
        ) T4 on T4.supplier_id=T1.supplier_id
        left join supplier_manage sm on T1.supplier_id = sm.id
                         left join purchase_ledger pl on pro.purchase_ledger_id = pl.id
                group by pl.id
            ) rt on rt.id = pl.id
            group by pl.supplier_id
        ) c
        left join supplier_manage sm on c.supplier_id = sm.id
        <where>
            <if test="supplierName!=null and supplierName!=''">
                AND sm.supplier_name LIKE CONCAT('%',#{supplierName},'%')
            </if>
        </where>
        order by T1.supplier_id desc
        order by c.supplier_id desc
    </select>
    <select id="supplierTransactionsDetails"
            resultType="com.ruoyi.purchase.vo.SupplierTransactionsDetailsVo">
@@ -115,8 +145,17 @@
              pl.purchase_contract_number,
              pl.execution_date,
              pl.contract_amount,
              IFNULL(T1.paymentAmount, 0) AS paymentAmount,
              IFNULL(T2.InboundAmount, 0) - IFNULL(T3.returnAmount, 0) AS payableAmount
              -- ä»˜æ¬¾é‡‘额按合同金额封顶,避免重复付款导致付款金额大于合同金额
              LEAST(IFNULL(T1.paymentAmount, 0), COALESCE(pl.contract_amount, 0)) AS paymentAmount,
              -- åº”付金额 = å–(入库金额, åˆåŒé‡‘额)的较小者 - å°é¡¶åŽä»˜æ¬¾é‡‘额 - é€€è´§é‡‘额
              -- å…¥åº“金额按合同金额封顶,避免入库流程重复提交导致应付金额大于合同金额;
              -- å·²ä»˜æ¬¾é‡‘额(回款/付款单)与退货金额均需从应付中扣减。
              GREATEST(
                  IFNULL(T2.InboundAmount, 0)
                      - IF(T2.InboundAmount <![CDATA[ > ]]> COALESCE(pl.contract_amount, 0), T2.InboundAmount - COALESCE(pl.contract_amount, 0), 0)
                  - LEAST(IFNULL(T1.paymentAmount, 0), COALESCE(pl.contract_amount, 0))
                  - IFNULL(T3.returnAmount, 0)
              , 0) AS payableAmount
       from purchase_ledger pl
       left join (
           select
src/main/resources/mapper/stock/StockInventoryMapper.xml
@@ -222,7 +222,7 @@
        unit,
        product_name,
        product_id
        order by combined.create_time desc
        order by MAX(combined.create_time) desc
    </select>
    <select id="listStockInventoryExportData" resultType="com.ruoyi.stock.execl.StockInventoryExportData">
@@ -537,6 +537,7 @@
                 left join product p on pm.product_id = p.id
        where si.product_model_id = #{productModelId}
        and si.qualitity > IFNULL(sd.qualitity, 0)
        order by si.create_time asc
    </select>
    <select id="getBatchNoQty" resultType="com.ruoyi.stock.dto.StockInventoryDto">
src/main/resources/static/²úÆ·µ¼ÈëÄ£°å.xlsx
Binary files differ