liding
2026-04-27 050a6301777a6753c800f1999670f8d30f1589f9
feat:1.根据bom结构领料 2.额外领取库存产品
已添加4个文件
已修改23个文件
1119 ■■■■■ 文件已修改
src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/controller/ProductController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/mapper/ProductModelMapper.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/pojo/ProductModel.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/IProductService.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/ProductServiceImpl.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/vo/ProductModelVo.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickDto.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderPickRecordVo.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderPickVo.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderPickController.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionBomStructureMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionOrderPickMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionOrderPickRecordMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOrderPick.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOrderPickRecord.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOrderPickService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOrderService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java 641 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 103 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/mapper/StockInventoryMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/ProductModelMapper.xml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionBomStructureMapper.xml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderPickMapper.xml 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderPickRecordMapper.xml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInventoryMapper.xml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java
@@ -1,24 +1,15 @@
package com.ruoyi.ai.config;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Collections;
import java.util.List;
/**
 * @author :yys
@@ -35,8 +26,8 @@
    @Autowired
    private EmbeddingModel embeddingModel;
    @Value("${knowledge.one}")
    private String one;
//    @Value("${knowledge.one}")
//    private String one;
//
//    @Value("${knowledge.two}")
//    private String two;
@@ -53,33 +44,33 @@
                .build();
    }
    @Bean
    ContentRetriever contentRetrieverXiaozhi() {
        //使用FileSystemDocumentLoader读取指定目录下的知识库文档
        //并使用默认的文档解析器对文档进行解析
        Document document1 = FileSystemDocumentLoader.loadDocument(one);
//        Document document2 = FileSystemDocumentLoader.loadDocument(two);
//        Document document3 = FileSystemDocumentLoader.loadDocument(three);
//        List<Document> documents = Arrays.asList(document1, document2, document3);
        List<Document> documents = Collections.singletonList(document1);
//         2. å°†æ•°æ®åº“数据转为LangChain4j的Document对象
//        List<Document> documents = new ArrayList<>();
        //使用内存向量存储
        InMemoryEmbeddingStore<TextSegment> inMemoryEmbeddingStore = new InMemoryEmbeddingStore<>();
        //使用默认的文档分割器
        EmbeddingStoreIngestor.builder()
                .embeddingModel(embeddingModel)
                .embeddingStore(inMemoryEmbeddingStore)
                .build()
                .ingest(documents);
        //从嵌入存储(EmbeddingStore)里检索和查询内容相关的信息
        return EmbeddingStoreContentRetriever.builder()
                .embeddingModel(embeddingModel)
                .embeddingStore(inMemoryEmbeddingStore)
                .build();
    }
//    @Bean
//    ContentRetriever contentRetrieverXiaozhi() {
//        //使用FileSystemDocumentLoader读取指定目录下的知识库文档
//        //并使用默认的文档解析器对文档进行解析
////        Document document1 = FileSystemDocumentLoader.loadDocument(one);
////        Document document2 = FileSystemDocumentLoader.loadDocument(two);
////        Document document3 = FileSystemDocumentLoader.loadDocument(three);
////        List<Document> documents = Arrays.asList(document1, document2, document3);
//
////        List<Document> documents = Collections.singletonList(document1);
////         2. å°†æ•°æ®åº“数据转为LangChain4j的Document对象
////        List<Document> documents = new ArrayList<>();
//
//        //使用内存向量存储
//        InMemoryEmbeddingStore<TextSegment> inMemoryEmbeddingStore = new InMemoryEmbeddingStore<>();
//        //使用默认的文档分割器
//        EmbeddingStoreIngestor.builder()
//                .embeddingModel(embeddingModel)
//                .embeddingStore(inMemoryEmbeddingStore)
//                .build()
//                .ingest(documents);
//        //从嵌入存储(EmbeddingStore)里检索和查询内容相关的信息
//        return EmbeddingStoreContentRetriever.builder()
//                .embeddingModel(embeddingModel)
//                .embeddingStore(inMemoryEmbeddingStore)
//                .build();
//    }
    @Bean
    ContentRetriever contentRetrieverXiaozhiPincone() {
src/main/java/com/ruoyi/basic/controller/ProductController.java
@@ -10,6 +10,7 @@
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;
@@ -17,11 +18,8 @@
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.service.ISalesLedgerProductService;
import com.ruoyi.sales.service.ISalesLedgerService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@@ -118,7 +116,7 @@
    @Operation(summary = "分页查询所有产品型号")
    @GetMapping("/pageModel")
    public IPage<ProductModel> listPageProductModel(Page<ProductModel> page, ProductModel productModel) {
    public IPage<ProductModelVo> listPageProductModel(Page<ProductModelVo> page, ProductModel productModel) {
        return productService.listPageProductModel(page, productModel);
    }
src/main/java/com/ruoyi/basic/mapper/ProductModelMapper.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.vo.ProductModelVo;
import com.ruoyi.procurementrecord.dto.ProcurementPageDto;
import org.apache.ibatis.annotations.Param;
@@ -19,7 +20,7 @@
 */
public interface ProductModelMapper extends BaseMapper<ProductModel> {
    IPage<ProductModel> listPageProductModel(Page<ProductModel> page, @Param("c") ProductModel productModel);
    IPage<ProductModelVo> listPageProductModel(Page<ProductModelVo> page, @Param("c") ProductModel productModel);
    IPage<ProductModel> listPageProductionStock(Page<ProductModel> page, @Param("req") ProcurementPageDto req);
src/main/java/com/ruoyi/basic/pojo/ProductModel.java
@@ -5,12 +5,13 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("product_model")
public class ProductModel {
public class ProductModel implements Serializable {
    private static final long serialVersionUID = 1L;
src/main/java/com/ruoyi/basic/service/IProductService.java
@@ -7,6 +7,7 @@
import com.ruoyi.basic.dto.ProductTreeDto;
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.vo.ProductModelVo;
import java.util.List;
@@ -18,5 +19,5 @@
    List<ProductTreeDto> selectProductList(ProductDto productDto);
    IPage<ProductModel> listPageProductModel(Page<ProductModel> page, ProductModel productModel);
    IPage<ProductModelVo> listPageProductModel(Page<ProductModelVo> page, ProductModel productModel);
}
src/main/java/com/ruoyi/basic/service/impl/ProductServiceImpl.java
@@ -12,6 +12,7 @@
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.service.IProductService;
import com.ruoyi.basic.vo.ProductModelVo;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.domain.AjaxResult;
@@ -56,7 +57,7 @@
    }
    @Override
    public IPage<ProductModel> listPageProductModel(Page<ProductModel> page, ProductModel productModel) {
    public IPage<ProductModelVo> listPageProductModel(Page<ProductModelVo> page, ProductModel productModel) {
        return productModelMapper.listPageProductModel(page, productModel);
    }
src/main/java/com/ruoyi/basic/vo/ProductModelVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.basic.vo;
import com.ruoyi.basic.pojo.ProductModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
public class ProductModelVo extends ProductModel {
    private List<String> batchNoList;
}
src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package com.ruoyi.production.bean.dto;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.ruoyi.production.pojo.ProductionOrderPick;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
@Schema(name = "ProductionOrderPickDto", description = "生产订单领料保存参数")
public class ProductionOrderPickDto extends ProductionOrderPick {
    @Schema(description = "生产订单ID")
    private Long productionOrderId;
    @Schema(description = "生产工单ID")
    private Long productionOperationTaskId;
    @Schema(description = "批号")
    private String batchNo;
    @Schema(description = "批号列表")
    private List<String> batchNoList;
    @Schema(description = "领料数量")
    private BigDecimal pickQuantity;
    @Schema(description = "领料类型,1正常领料,2补料")
    private Byte pickType;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "领料明细列表")
    @JsonAlias({"dto", "productionOrderPickDto"})
    private List<ProductionOrderPickDto> pickList;
    @Schema(description = "需要删除的领料ID列表")
    private List<Long> deletePickIds;
}
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderPickRecordVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package com.ruoyi.production.bean.vo;
import com.ruoyi.production.pojo.ProductionOrderPickRecord;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(name = "ProductionOrderPickRecordVo", description = "已领料详情")
public class ProductionOrderPickRecordVo extends ProductionOrderPickRecord {
    @Schema(description = "工序名称")
    private String operationName;
    @Schema(description = "产品名称")
    private String productName;
    @Schema(description = "型号")
    private String model;
    @Schema(description = "单位")
    private String unit;
}
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderPickVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package com.ruoyi.production.bean.vo;
import com.ruoyi.production.pojo.ProductionOrderPick;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
public class ProductionOrderPickVo extends ProductionOrderPick {
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "库存数量")
    private BigDecimal stockQuantity;
    @Schema(description = "领用数量")
    private BigDecimal pickQuantity;
    @Schema(description = "产品名称")
    private String productName;
    @Schema(description = "规格型号")
    private String model;
    @Schema(description = "批号")
    private List<String> batchNoList;
}
src/main/java/com/ruoyi/production/controller/ProductionOrderController.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.production.bean.dto.ProductionOrderDto;
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.bean.vo.ProductionOrderVo;
import com.ruoyi.production.bean.vo.ProductionPlanVo;
import com.ruoyi.production.pojo.ProductionOrder;
@@ -75,4 +76,10 @@
    public R<List<ProductionPlanVo>> getSource(@PathVariable Long id) {
        return R.ok(productionOrderService.getSource(id));
    }
    @GetMapping("/pick/{productionOrderId}")
    @Operation(summary = "根据订单id查询bom领料单")
    public R<List<ProductionOrderPickVo>> pick(@PathVariable Long productionOrderId) {
        return R.ok(productionOrderService.pick(productionOrderId));
    }
}
src/main/java/com/ruoyi/production/controller/ProductionOrderPickController.java
@@ -1,7 +1,15 @@
package com.ruoyi.production.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.production.bean.dto.ProductionOrderPickDto;
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.service.ProductionOrderPickService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * <p>
@@ -13,6 +21,28 @@
 */
@RestController
@RequestMapping("/productionOrderPick")
@Tag(name = "生产订单领料")
@RequiredArgsConstructor
public class ProductionOrderPickController {
    private final ProductionOrderPickService productionOrderPickService;
    @PostMapping("/savePick")
    @Operation(summary = "领料保存到线边仓")
    public R<Boolean> savePick(@RequestBody ProductionOrderPickDto dto) {
        return R.ok(productionOrderPickService.savePick(dto));
}
    @PostMapping("/updatePick")
    @Operation(summary = "变更领料")
    public R<Boolean> updatePick(@RequestBody ProductionOrderPickDto dto) {
        return R.ok(productionOrderPickService.updatePick(dto));
    }
    @GetMapping("/detail/{productionOrderId}")
    @Operation(summary = "查询已领料详情")
    public R<List<ProductionOrderPickVo>> listPickedDetail(@PathVariable Long productionOrderId) {
        return R.ok(productionOrderPickService.listPickedDetail(productionOrderId));
    }
}
src/main/java/com/ruoyi/production/mapper/ProductionBomStructureMapper.java
@@ -21,4 +21,10 @@
    List<ProductionBomStructureVo> listByBomId(@Param("bomId") Long bomId);
    /**
     * é¢†æ–™bom
     * @param bomId
     * @return
     */
    List<ProductionBomStructureVo> pickByBomId(@Param("bomId") Long bomId);
}
src/main/java/com/ruoyi/production/mapper/ProductionOrderPickMapper.java
@@ -1,8 +1,12 @@
package com.ruoyi.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.pojo.ProductionOrderPick;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
@@ -15,4 +19,5 @@
@Mapper
public interface ProductionOrderPickMapper extends BaseMapper<ProductionOrderPick> {
    List<ProductionOrderPickVo> listPickedDetailByOrderId(@Param("productionOrderId") Long productionOrderId);
}
src/main/java/com/ruoyi/production/mapper/ProductionOrderPickRecordMapper.java
@@ -1,8 +1,12 @@
package com.ruoyi.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.pojo.ProductionOrderPickRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
@@ -15,4 +19,6 @@
@Mapper
public interface ProductionOrderPickRecordMapper extends BaseMapper<ProductionOrderPickRecord> {
    List<ProductionOrderPickVo> listPickedDetailByOrderId(@Param("productionOrderId") Long productionOrderId);
}
src/main/java/com/ruoyi/production/pojo/ProductionOrderPick.java
@@ -28,7 +28,7 @@
    private Long id;
    @Schema(description = "产品规格id")
    private Integer productModelId;
    private Long productModelId;
    @Schema(description = "数量")
    private BigDecimal quantity;
@@ -56,4 +56,17 @@
    @Schema(description = "部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @Schema(description = "工序名称")
    private String operationName;
    @Schema(description = "工序id")
    private Long technologyOperationId;
    @Schema(description = "需求数量")
    private BigDecimal demandedQuantity;
    @Schema(description = "是否bom领料")
    @TableField("is_bom")
    private Boolean bom;
}
src/main/java/com/ruoyi/production/pojo/ProductionOrderPickRecord.java
@@ -3,8 +3,6 @@
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.math.BigDecimal;
@@ -39,7 +37,7 @@
    private Long productionOperationTaskId;
    @Schema(description = "产品规格id")
    private Integer productModelId;
    private Long productModelId;
    @Schema(description = "批号")
    private String batchNo;
src/main/java/com/ruoyi/production/service/ProductionOrderPickService.java
@@ -1,7 +1,11 @@
package com.ruoyi.production.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.production.bean.dto.ProductionOrderPickDto;
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.pojo.ProductionOrderPick;
import java.util.List;
/**
 * <p>
@@ -13,4 +17,9 @@
 */
public interface ProductionOrderPickService extends IService<ProductionOrderPick> {
    Boolean savePick(ProductionOrderPickDto dto);
    Boolean updatePick(ProductionOrderPickDto dto);
    List<ProductionOrderPickVo> listPickedDetail(Long productionOrderId);
}
src/main/java/com/ruoyi/production/service/ProductionOrderService.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.production.bean.dto.ProductionOrderDto;
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.bean.vo.ProductionOrderVo;
import com.ruoyi.production.bean.vo.ProductionPlanVo;
import com.ruoyi.production.pojo.ProductionOrder;
@@ -27,4 +28,6 @@
    Object bindingRoute(ProductionOrderDto productionOrderDto);
    List<ProductionPlanVo> getSource(Long id);
    List<ProductionOrderPickVo> pick(Long productionOrderId);
}
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
@@ -1,20 +1,657 @@
package com.ruoyi.production.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.production.bean.dto.ProductionOrderPickDto;
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionOrderPickMapper;
import com.ruoyi.production.mapper.ProductionOrderPickRecordMapper;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionOrderPick;
import com.ruoyi.production.pojo.ProductionOrderPickRecord;
import com.ruoyi.production.service.ProductionOrderPickService;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInventory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
 * <p>
 * è®¢å•领料线边仓 æœåŠ¡å®žçŽ°ç±»
 * ç’ãˆ å´Ÿæ£°å—˜æž¡ç»¾èƒ¯ç«Ÿæµ ?鏈嶅姟瀹炵幇绫?
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @author é‘ºîˆšî‡±æžîˆ™æ¬¢é”›å Ÿç™é‘»å¿¥ç´šéˆå¤æªºéî„€å¾ƒ
 * @since 2026-04-21 03:55:52
 */
@Service
@RequiredArgsConstructor
public class ProductionOrderPickServiceImpl extends ServiceImpl<ProductionOrderPickMapper, ProductionOrderPick> implements ProductionOrderPickService {
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper;
    private final StockInventoryMapper stockInventoryMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean savePick(ProductionOrderPickDto dto) {
        List<ProductionOrderPickDto> pickItems = resolvePickItems(dto);
        for (int i = 0; i < pickItems.size(); i++) {
            int rowNo = i + 1;
            ProductionOrderPickDto resolvedDto = mergeDto(dto, pickItems.get(i));
            validatePickParam(resolvedDto, rowNo);
            List<String> batchNoList = resolveBatchNoList(resolvedDto);
            String inventoryBatchNo = pickInventoryBatchNo(batchNoList);
            String storedBatchNo = formatBatchNoStorage(batchNoList);
            subtractInventory(resolvedDto.getProductModelId(), inventoryBatchNo, resolvedDto.getPickQuantity(), rowNo);
            ProductionOrderPick orderPick = new ProductionOrderPick();
            orderPick.setProductionOrderId(resolvedDto.getProductionOrderId());
            orderPick.setProductModelId(resolvedDto.getProductModelId());
            orderPick.setBatchNo(storedBatchNo);
            orderPick.setQuantity(resolvedDto.getPickQuantity());
            orderPick.setRemark(resolvedDto.getRemark());
            orderPick.setOperationName(resolvedDto.getOperationName());
            orderPick.setTechnologyOperationId(resolvedDto.getTechnologyOperationId());
            orderPick.setDemandedQuantity(resolvedDto.getDemandedQuantity());
            orderPick.setBom(resolvedDto.getBom());
            baseMapper.insert(orderPick);
            insertPickRecord(orderPick.getId(),
                    resolvedDto.getProductionOrderId(),
                    resolvedDto.getProductionOperationTaskId(),
                    resolvedDto.getProductModelId(),
                    inventoryBatchNo,
                    resolvedDto.getPickQuantity(),
                    BigDecimal.ZERO,
                    resolvedDto.getPickQuantity(),
                    resolvedDto.getPickType(),
                    resolvedDto.getRemark());
}
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean updatePick(ProductionOrderPickDto dto) {
        if (dto == null) {
            throw new ServiceException("变更参数不能为空");
        }
        Long productionOrderId = resolveProductionOrderId(dto);
        if (productionOrderId == null) {
            throw new ServiceException("生产订单ID不能为空");
        }
        ProductionOrder productionOrder = productionOrderMapper.selectById(productionOrderId);
        if (productionOrder == null) {
            throw new ServiceException("生产订单不存在");
        }
        List<ProductionOrderPick> existingPickList = baseMapper.selectList(
                Wrappers.<ProductionOrderPick>lambdaQuery()
                        .eq(ProductionOrderPick::getProductionOrderId, productionOrderId));
        Map<Long, ProductionOrderPick> existingPickMap = existingPickList.stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(ProductionOrderPick::getId, Function.identity(), (a, b) -> a));
        processDeletePickIds(dto, existingPickMap, productionOrderId);
        List<ProductionOrderPickDto> pickItems = resolveUpdateItems(dto);
        Set<Long> keepPickIdSet = new HashSet<>();
        for (int i = 0; i < pickItems.size(); i++) {
            int rowNo = i + 1;
            ProductionOrderPickDto resolvedDto = mergeDto(dto, pickItems.get(i));
            if (isEmptyUpdateItem(resolvedDto)) {
                continue;
            }
            if (resolvedDto.getProductionOrderId() == null) {
                resolvedDto.setProductionOrderId(productionOrderId);
            }
            validatePickParam(resolvedDto, rowNo);
            if (resolvedDto.getId() == null) {
                addNewPickInUpdate(resolvedDto, rowNo);
                continue;
            }
            keepPickIdSet.add(resolvedDto.getId());
            updateExistingPick(resolvedDto, rowNo, existingPickMap);
        }
        processMissingPickItems(dto, existingPickMap, productionOrderId, keepPickIdSet);
        return true;
    }
    @Override
    public List<ProductionOrderPickVo> listPickedDetail(Long productionOrderId) {
        if (productionOrderId == null) {
            return Collections.emptyList();
        }
        List<ProductionOrderPickVo> detailList = baseMapper.listPickedDetailByOrderId(productionOrderId);
        fillBatchNoList(detailList);
        fillSelectableBatchNoList(detailList);
        return detailList;
    }
    private void processDeletePickIds(ProductionOrderPickDto rootDto,
                                      Map<Long, ProductionOrderPick> existingPickMap,
                                      Long productionOrderId) {
        if (rootDto.getDeletePickIds() == null || rootDto.getDeletePickIds().isEmpty()) {
            return;
        }
        Set<Long> deleteIdSet = new LinkedHashSet<>(rootDto.getDeletePickIds());
        for (Long deleteId : deleteIdSet) {
            if (deleteId == null) {
                continue;
            }
            ProductionOrderPick existingPick = existingPickMap.get(deleteId);
            if (existingPick == null || !Objects.equals(existingPick.getProductionOrderId(), productionOrderId)) {
                throw new ServiceException("要删除的领料记录不存在或不属于当前订单,ID=" + deleteId);
            }
            String oldBatchNo = resolveInventoryBatchNoFromStored(existingPick.getBatchNo());
            BigDecimal oldQuantity = defaultDecimal(existingPick.getQuantity());
            addInventory(existingPick.getProductModelId(), oldBatchNo, oldQuantity);
            int affected = baseMapper.deleteById(deleteId);
            if (affected <= 0) {
                throw new ServiceException("删除领料失败,ID=" + deleteId);
            }
            insertPickRecord(existingPick.getId(),
                    existingPick.getProductionOrderId(),
                    rootDto.getProductionOperationTaskId(),
                    existingPick.getProductModelId(),
                    oldBatchNo,
                    oldQuantity,
                    oldQuantity,
                    BigDecimal.ZERO,
                    rootDto.getPickType(),
                    rootDto.getRemark());
            existingPickMap.remove(deleteId);
        }
    }
    private void processMissingPickItems(ProductionOrderPickDto rootDto,
                                         Map<Long, ProductionOrderPick> existingPickMap,
                                         Long productionOrderId,
                                         Set<Long> keepPickIdSet) {
        if (rootDto.getPickList() == null) {
            return;
        }
        List<ProductionOrderPick> missingPickList = existingPickMap.values().stream()
                .filter(Objects::nonNull)
                .filter(item -> item.getId() != null)
                .filter(item -> Objects.equals(item.getProductionOrderId(), productionOrderId))
                .filter(item -> !keepPickIdSet.contains(item.getId()))
                .collect(Collectors.toList());
        for (ProductionOrderPick missingPick : missingPickList) {
            String oldBatchNo = resolveInventoryBatchNoFromStored(missingPick.getBatchNo());
            BigDecimal oldQuantity = defaultDecimal(missingPick.getQuantity());
            addInventory(missingPick.getProductModelId(), oldBatchNo, oldQuantity);
            int affected = baseMapper.deleteById(missingPick.getId());
            if (affected <= 0) {
                throw new ServiceException("删除领料失败,ID=" + missingPick.getId());
            }
            insertPickRecord(missingPick.getId(),
                    missingPick.getProductionOrderId(),
                    rootDto.getProductionOperationTaskId(),
                    missingPick.getProductModelId(),
                    oldBatchNo,
                    oldQuantity,
                    oldQuantity,
                    BigDecimal.ZERO,
                    rootDto.getPickType(),
                    rootDto.getRemark());
            existingPickMap.remove(missingPick.getId());
        }
    }
    private void addNewPickInUpdate(ProductionOrderPickDto dto, int rowNo) {
        List<String> batchNoList = resolveBatchNoList(dto);
        String inventoryBatchNo = pickInventoryBatchNo(batchNoList);
        String storedBatchNo = formatBatchNoStorage(batchNoList);
        subtractInventory(dto.getProductModelId(), inventoryBatchNo, dto.getPickQuantity(), rowNo);
        ProductionOrderPick orderPick = new ProductionOrderPick();
        orderPick.setProductionOrderId(dto.getProductionOrderId());
        orderPick.setProductModelId(dto.getProductModelId());
        orderPick.setBatchNo(storedBatchNo);
        orderPick.setQuantity(dto.getPickQuantity());
        orderPick.setRemark(dto.getRemark());
        orderPick.setOperationName(dto.getOperationName());
        orderPick.setTechnologyOperationId(dto.getTechnologyOperationId());
        orderPick.setDemandedQuantity(dto.getDemandedQuantity());
        orderPick.setBom(dto.getBom());
        baseMapper.insert(orderPick);
        insertPickRecord(orderPick.getId(),
                dto.getProductionOrderId(),
                dto.getProductionOperationTaskId(),
                dto.getProductModelId(),
                inventoryBatchNo,
                dto.getPickQuantity(),
                BigDecimal.ZERO,
                dto.getPickQuantity(),
                dto.getPickType(),
                dto.getRemark());
    }
    private void updateExistingPick(ProductionOrderPickDto dto,
                                    int rowNo,
                                    Map<Long, ProductionOrderPick> existingPickMap) {
        ProductionOrderPick oldPick = existingPickMap.get(dto.getId());
        if (oldPick == null || !Objects.equals(oldPick.getProductionOrderId(), dto.getProductionOrderId())) {
            throw new ServiceException("第" + rowNo + "条领料记录不存在或不属于当前订单");
        }
        Long oldProductModelId = oldPick.getProductModelId();
        String oldBatchNo = resolveInventoryBatchNoFromStored(oldPick.getBatchNo());
        BigDecimal oldQuantity = defaultDecimal(oldPick.getQuantity());
        Long newProductModelId = dto.getProductModelId();
        List<String> newBatchNoList = resolveBatchNoList(dto);
        String newBatchNo = pickInventoryBatchNo(newBatchNoList);
        String newStoredBatchNo = formatBatchNoStorage(newBatchNoList);
        BigDecimal newQuantity = dto.getPickQuantity();
        boolean sameStockKey = Objects.equals(oldProductModelId, newProductModelId)
                && Objects.equals(oldBatchNo, newBatchNo);
        if (sameStockKey) {
            BigDecimal delta = newQuantity.subtract(oldQuantity);
            if (delta.compareTo(BigDecimal.ZERO) > 0) {
                subtractInventory(newProductModelId, newBatchNo, delta, rowNo);
            } else if (delta.compareTo(BigDecimal.ZERO) < 0) {
                addInventory(oldProductModelId, oldBatchNo, delta.abs());
            }
        } else {
            addInventory(oldProductModelId, oldBatchNo, oldQuantity);
            subtractInventory(newProductModelId, newBatchNo, newQuantity, rowNo);
        }
        oldPick.setProductModelId(newProductModelId);
        oldPick.setBatchNo(newStoredBatchNo);
        oldPick.setQuantity(newQuantity);
        oldPick.setRemark(dto.getRemark());
        oldPick.setOperationName(dto.getOperationName());
        oldPick.setTechnologyOperationId(dto.getTechnologyOperationId());
        if (dto.getDemandedQuantity() != null) {
            oldPick.setDemandedQuantity(dto.getDemandedQuantity());
        }
        if (dto.getBom() != null) {
            oldPick.setBom(dto.getBom());
        }
        int affected = baseMapper.updateById(oldPick);
        if (affected <= 0) {
            throw new ServiceException("第" + rowNo + "条领料更新失败");
        }
        BigDecimal recordQuantity = sameStockKey ? oldQuantity.subtract(newQuantity).abs() : newQuantity;
        if (recordQuantity.compareTo(BigDecimal.ZERO) > 0 || oldQuantity.compareTo(newQuantity) != 0 || !sameStockKey) {
            insertPickRecord(oldPick.getId(),
                    dto.getProductionOrderId(),
                    dto.getProductionOperationTaskId(),
                    newProductModelId,
                    newBatchNo,
                    recordQuantity,
                    oldQuantity,
                    newQuantity,
                    dto.getPickType(),
                    dto.getRemark());
        }
    }
    private void insertPickRecord(Long pickId,
                                  Long productionOrderId,
                                  Long productionOperationTaskId,
                                  Long productModelId,
                                  String batchNo,
                                  BigDecimal pickQuantity,
                                  BigDecimal beforeQuantity,
                                  BigDecimal afterQuantity,
                                  Byte pickType,
                                  String remark) {
        ProductionOrderPickRecord pickRecord = new ProductionOrderPickRecord();
        pickRecord.setPickId(pickId);
        pickRecord.setProductionOrderId(productionOrderId);
        pickRecord.setProductionOperationTaskId(productionOperationTaskId);
        pickRecord.setProductModelId(productModelId);
        pickRecord.setBatchNo(batchNo);
        pickRecord.setPickQuantity(defaultDecimal(pickQuantity));
        pickRecord.setBeforeQuantity(defaultDecimal(beforeQuantity));
        pickRecord.setAfterQuantity(defaultDecimal(afterQuantity));
        pickRecord.setPickType(pickType == null ? (byte) 1 : pickType);
        pickRecord.setRemark(remark);
        productionOrderPickRecordMapper.insert(pickRecord);
    }
    private void subtractInventory(Long productModelId, String batchNo, BigDecimal quantity, int rowNo) {
        StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, batchNo));
        if (stockInventory == null) {
            throw new ServiceException("第" + rowNo + "条领料对应库存不存在");
        }
        BigDecimal availableQuantity = defaultDecimal(stockInventory.getQualitity())
                .subtract(defaultDecimal(stockInventory.getLockedQuantity()));
        if (quantity.compareTo(availableQuantity) > 0) {
            throw new ServiceException("第" + rowNo + "条领料可用库存不足");
        }
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryDto.setBatchNo(batchNo);
        stockInventoryDto.setQualitity(quantity);
        int affected = stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto);
        if (affected <= 0) {
            throw new ServiceException("第" + rowNo + "条领料扣减库存失败");
        }
    }
    private void addInventory(Long productModelId, String batchNo, BigDecimal quantity) {
        BigDecimal addQuantity = defaultDecimal(quantity);
        if (addQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, batchNo));
        if (stockInventory == null) {
            StockInventory newStockInventory = new StockInventory();
            newStockInventory.setProductModelId(productModelId);
            newStockInventory.setBatchNo(batchNo);
            newStockInventory.setQualitity(addQuantity);
            newStockInventory.setLockedQuantity(BigDecimal.ZERO);
            newStockInventory.setVersion(1);
            stockInventoryMapper.insert(newStockInventory);
            return;
        }
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryDto.setBatchNo(batchNo);
        stockInventoryDto.setQualitity(addQuantity);
        int affected = stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
        if (affected <= 0) {
            throw new ServiceException("库存回退失败,产品规格ID=" + productModelId);
        }
    }
    private List<ProductionOrderPickDto> resolvePickItems(ProductionOrderPickDto dto) {
        if (dto == null) {
            throw new ServiceException("领料参数不能为空");
        }
        if (dto.getPickList() != null && !dto.getPickList().isEmpty()) {
            return dto.getPickList();
        }
        return Collections.singletonList(dto);
    }
    private List<ProductionOrderPickDto> resolveUpdateItems(ProductionOrderPickDto dto) {
        if (dto.getPickList() != null) {
            return dto.getPickList();
        }
        if (isEmptyUpdateItem(dto)) {
            return Collections.emptyList();
        }
        return Collections.singletonList(dto);
    }
    private boolean isEmptyUpdateItem(ProductionOrderPickDto dto) {
        return dto.getId() == null
                && dto.getProductModelId() == null
                && dto.getPickQuantity() == null
                && StringUtils.isEmpty(dto.getBatchNo())
                && (dto.getBatchNoList() == null || dto.getBatchNoList().isEmpty())
                && dto.getPickType() == null
                && dto.getProductionOperationTaskId() == null
                && dto.getTechnologyOperationId() == null
                && StringUtils.isEmpty(dto.getOperationName())
                && dto.getDemandedQuantity() == null
                && dto.getBom() == null
                && StringUtils.isEmpty(dto.getRemark());
    }
    private Long resolveProductionOrderId(ProductionOrderPickDto dto) {
        if (dto.getProductionOrderId() != null) {
            return dto.getProductionOrderId();
        }
        if (dto.getPickList() == null || dto.getPickList().isEmpty()) {
            return null;
        }
        return dto.getPickList().stream()
                .filter(Objects::nonNull)
                .map(ProductionOrderPickDto::getProductionOrderId)
                .filter(Objects::nonNull)
                .findFirst()
                .orElse(null);
    }
    private ProductionOrderPickDto mergeDto(ProductionOrderPickDto rootDto, ProductionOrderPickDto itemDto) {
        ProductionOrderPickDto merged = new ProductionOrderPickDto();
        if (itemDto != null) {
            merged.setId(itemDto.getId());
            merged.setProductionOrderId(itemDto.getProductionOrderId());
            merged.setProductionOperationTaskId(itemDto.getProductionOperationTaskId());
            merged.setProductModelId(itemDto.getProductModelId());
            merged.setBatchNo(itemDto.getBatchNo());
            merged.setBatchNoList(itemDto.getBatchNoList());
            merged.setPickQuantity(itemDto.getPickQuantity());
            merged.setPickType(itemDto.getPickType());
            merged.setRemark(itemDto.getRemark());
            merged.setTechnologyOperationId(itemDto.getTechnologyOperationId());
            merged.setOperationName(itemDto.getOperationName());
            merged.setDemandedQuantity(itemDto.getDemandedQuantity());
            merged.setBom(itemDto.getBom());
        }
        if (merged.getId() == null) {
            merged.setId(rootDto.getId());
        }
        if (merged.getProductionOrderId() == null) {
            merged.setProductionOrderId(rootDto.getProductionOrderId());
        }
        if (merged.getProductionOperationTaskId() == null) {
            merged.setProductionOperationTaskId(rootDto.getProductionOperationTaskId());
        }
        if (merged.getProductModelId() == null) {
            merged.setProductModelId(rootDto.getProductModelId());
        }
        if (merged.getBatchNo() == null) {
            merged.setBatchNo(rootDto.getBatchNo());
        }
        if (merged.getBatchNoList() == null || merged.getBatchNoList().isEmpty()) {
            merged.setBatchNoList(rootDto.getBatchNoList());
        }
        if (merged.getPickQuantity() == null) {
            merged.setPickQuantity(rootDto.getPickQuantity());
        }
        if (merged.getPickType() == null) {
            merged.setPickType(rootDto.getPickType());
        }
        if (merged.getRemark() == null) {
            merged.setRemark(rootDto.getRemark());
        }
        if (merged.getTechnologyOperationId() == null) {
            merged.setTechnologyOperationId(rootDto.getTechnologyOperationId());
        }
        if (merged.getOperationName() == null) {
            merged.setOperationName(rootDto.getOperationName());
        }
        if (merged.getDemandedQuantity() == null) {
            merged.setDemandedQuantity(rootDto.getDemandedQuantity());
        }
        if (merged.getBom() == null) {
            merged.setBom(rootDto.getBom());
        }
        return merged;
    }
    private void validatePickParam(ProductionOrderPickDto dto, int rowNo) {
        if (dto.getProductionOrderId() == null) {
            throw new ServiceException("第" + rowNo + "条生产订单ID不能为空");
        }
        if (dto.getProductModelId() == null) {
            throw new ServiceException("第" + rowNo + "条产品规格ID不能为空");
        }
        if (dto.getPickQuantity() == null || dto.getPickQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("第" + rowNo + "条领料数量必须大于0");
        }
        if (dto.getPickType() != null && dto.getPickType() != 1 && dto.getPickType() != 2) {
            throw new ServiceException("第" + rowNo + "条领料类型只能是1或2");
        }
    }
    private String normalizeBatchNo(String batchNo) {
        if (StringUtils.isEmpty(batchNo)) {
            return null;
        }
        String trimBatchNo = batchNo.trim();
        return trimBatchNo.isEmpty() ? null : trimBatchNo;
    }
    private List<String> resolveBatchNoList(ProductionOrderPickDto dto) {
        List<String> normalizedBatchNoList = normalizeBatchNoList(dto.getBatchNoList());
        if (!normalizedBatchNoList.isEmpty()) {
            return normalizedBatchNoList;
        }
        return parseBatchNoValue(dto.getBatchNo());
    }
    private String pickInventoryBatchNo(List<String> batchNoList) {
        if (batchNoList == null || batchNoList.isEmpty()) {
            return null;
        }
        return batchNoList.get(0);
    }
    private String resolveInventoryBatchNoFromStored(String storedBatchNo) {
        return pickInventoryBatchNo(parseBatchNoValue(storedBatchNo));
    }
    private String formatBatchNoStorage(List<String> batchNoList) {
        if (batchNoList == null || batchNoList.isEmpty()) {
            return null;
        }
        if (batchNoList.size() == 1) {
            return batchNoList.get(0);
        }
        return String.join(",", batchNoList);
    }
    private List<String> normalizeBatchNoList(List<String> batchNoList) {
        if (batchNoList == null || batchNoList.isEmpty()) {
            return Collections.emptyList();
        }
        LinkedHashSet<String> normalizedSet = new LinkedHashSet<>();
        for (String batchNo : batchNoList) {
            String normalizedBatchNo = normalizeBatchNo(batchNo);
            if (!StringUtils.isEmpty(normalizedBatchNo)) {
                normalizedSet.add(normalizedBatchNo);
            }
        }
        return new ArrayList<>(normalizedSet);
    }
    private void fillBatchNoList(List<ProductionOrderPickVo> detailList) {
        if (detailList == null || detailList.isEmpty()) {
            return;
        }
        Map<String, LinkedHashSet<String>> batchNoGroupMap = new HashMap<>();
        for (ProductionOrderPickVo detail : detailList) {
            String key = buildBatchNoGroupKey(detail);
            LinkedHashSet<String> batchSet = batchNoGroupMap.computeIfAbsent(key, k -> new LinkedHashSet<>());
            batchSet.addAll(parseBatchNoValue(detail.getBatchNo()));
            if (detail.getBatchNoList() != null && !detail.getBatchNoList().isEmpty()) {
                batchSet.addAll(normalizeBatchNoList(detail.getBatchNoList()));
            }
        }
        for (ProductionOrderPickVo detail : detailList) {
            String key = buildBatchNoGroupKey(detail);
            LinkedHashSet<String> batchSet = batchNoGroupMap.get(key);
            detail.setBatchNoList(batchSet == null ? Collections.emptyList() : new ArrayList<>(batchSet));
        }
    }
    private void fillSelectableBatchNoList(List<ProductionOrderPickVo> detailList) {
        if (detailList == null || detailList.isEmpty()) {
            return;
        }
        Set<Long> productModelIdSet = detailList.stream()
                .map(ProductionOrderPickVo::getProductModelId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (productModelIdSet.isEmpty()) {
            return;
        }
        List<StockInventory> stockBatchList = stockInventoryMapper.listSelectableBatchNoByProductModelIds(
                new ArrayList<>(productModelIdSet));
        Map<Long, LinkedHashSet<String>> stockBatchMap = new HashMap<>();
        for (StockInventory stockInventory : stockBatchList) {
            if (stockInventory == null || stockInventory.getProductModelId() == null) {
                continue;
            }
            String normalizedBatchNo = normalizeBatchNo(stockInventory.getBatchNo());
            if (StringUtils.isEmpty(normalizedBatchNo)) {
                continue;
            }
            stockBatchMap.computeIfAbsent(stockInventory.getProductModelId(), k -> new LinkedHashSet<>())
                    .add(normalizedBatchNo);
        }
        for (ProductionOrderPickVo detail : detailList) {
            LinkedHashSet<String> mergedBatchSet = new LinkedHashSet<>();
            mergedBatchSet.addAll(normalizeBatchNoList(detail.getBatchNoList()));
            LinkedHashSet<String> selectableBatchSet = stockBatchMap.get(detail.getProductModelId());
            if (selectableBatchSet != null) {
                mergedBatchSet.addAll(selectableBatchSet);
            }
            detail.setBatchNoList(new ArrayList<>(mergedBatchSet));
        }
    }
    private String buildBatchNoGroupKey(ProductionOrderPickVo detail) {
        return String.valueOf(detail.getProductionOrderId()) + "|"
                + String.valueOf(detail.getProductModelId()) + "|"
                + String.valueOf(detail.getTechnologyOperationId()) + "|"
                + String.valueOf(detail.getOperationName());
    }
    private List<String> parseBatchNoValue(String rawBatchNoValue) {
        String normalizedValue = normalizeBatchNo(rawBatchNoValue);
        if (StringUtils.isEmpty(normalizedValue)) {
            return Collections.emptyList();
        }
        if (normalizedValue.startsWith("[") && normalizedValue.endsWith("]")) {
            String value = normalizedValue.substring(1, normalizedValue.length() - 1);
            if (StringUtils.isEmpty(value)) {
                return Collections.emptyList();
            }
            List<String> parsed = Arrays.stream(value.split(","))
                    .map(item -> item == null ? null : item.trim().replace("\"", "").replace("'", ""))
                    .collect(Collectors.toList());
            return normalizeBatchNoList(parsed);
        }
        if (normalizedValue.contains(",")) {
            List<String> parsed = Arrays.stream(normalizedValue.split(","))
                    .map(item -> item == null ? null : item.trim())
                    .collect(Collectors.toList());
            return normalizeBatchNoList(parsed);
        }
        return Collections.singletonList(normalizedValue);
    }
    private LambdaQueryWrapper<StockInventory> buildStockWrapper(Long productModelId, String batchNo) {
        LambdaQueryWrapper<StockInventory> wrapper = Wrappers.<StockInventory>lambdaQuery()
                .eq(StockInventory::getProductModelId, productModelId);
        if (StringUtils.isEmpty(batchNo)) {
            wrapper.isNull(StockInventory::getBatchNo);
        } else {
            wrapper.eq(StockInventory::getBatchNo, batchNo);
        }
        return wrapper;
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -15,6 +15,8 @@
import com.ruoyi.common.constant.StorageAttachmentConstants;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.production.bean.dto.ProductionOrderDto;
import com.ruoyi.production.bean.vo.ProductionBomStructureVo;
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.bean.vo.ProductionOrderVo;
import com.ruoyi.production.bean.vo.ProductionPlanVo;
import com.ruoyi.production.enums.ProductOrderStatusEnum;
@@ -23,6 +25,8 @@
import com.ruoyi.production.service.ProductionOrderService;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.technology.mapper.*;
import com.ruoyi.technology.pojo.*;
import lombok.RequiredArgsConstructor;
@@ -50,6 +54,7 @@
    private final ProductionOrderPickMapper productionOrderPickMapper;
    private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper;
    private final ProductionPlanMapper productionPlanMapper;
    private final StockInventoryMapper stockInventoryMapper;
    private final StorageAttachmentMapper storageAttachmentMapper;
    private final StorageBlobMapper storageBlobMapper;
    private final SalesLedgerMapper salesLedgerMapper;
@@ -112,11 +117,7 @@
                || productionOrderRoutingMapper.selectCount(Wrappers.<ProductionOrderRouting>lambdaQuery()
                        .eq(ProductionOrderRouting::getProductionOrderId, productionOrder.getId())) == 0);
        if (needSync) {
            // å·¥è‰ºã€äº§å“æˆ–数量变化后,订单快照必须和当前下单数据重新对齐。
            syncProductionOrderSnapshot(productionOrder.getId());
        } else {
            // æœªé‡å»ºå¿«ç…§æ—¶ï¼Œä¹Ÿè¦ç¡®ä¿å¤‡æ–™ä¸»å•和订单数量保持同步。
            upsertOrderPick(productionOrder);
        }
        return true;
    }
@@ -274,8 +275,6 @@
                syncedParamCount++;
            }
        }
        upsertOrderPick(productionOrder);
        return syncedParamCount;
    }
@@ -551,29 +550,6 @@
        return issuedQuantity.compareTo(requiredQuantity) < 0 ? 1 : 2;
    }
    private void upsertOrderPick(ProductionOrder productionOrder) {
        if (productionOrder == null || productionOrder.getId() == null) {
            return;
        }
        // è®¢å•下达后自动生成一张备料主单,后续领料记录都挂在这张单上。
        ProductionOrderPick orderPick = productionOrderPickMapper.selectOne(
                Wrappers.<ProductionOrderPick>lambdaQuery()
                        .eq(ProductionOrderPick::getProductionOrderId, productionOrder.getId())
                        .last("limit 1"));
        if (orderPick == null) {
            orderPick = new ProductionOrderPick();
            orderPick.setProductionOrderId(productionOrder.getId());
        }
        orderPick.setProductModelId(productionOrder.getProductModelId() == null ? null : Math.toIntExact(productionOrder.getProductModelId()));
        orderPick.setQuantity(defaultDecimal(productionOrder.getQuantity()));
        orderPick.setRemark("下单自动生成");
        if (orderPick.getId() == null) {
            productionOrderPickMapper.insert(orderPick);
        } else {
            productionOrderPickMapper.updateById(orderPick);
        }
    }
    private List<Long> parsePlanIds(String productionPlanIds) {
        if (productionPlanIds == null || productionPlanIds.trim().isEmpty()) {
            return new ArrayList<>();
@@ -677,4 +653,73 @@
        vo.setDownloadURL(fileUtil.buildSignedDownloadUrl(vo));
        return vo;
    }
    @Override
    public List<ProductionOrderPickVo> pick(Long productionOrderId) {
        if (productionOrderId == null) {
            return Collections.emptyList();
        }
        ProductionOrderBom orderBom = productionOrderBomMapper.selectOne(
                Wrappers.<ProductionOrderBom>lambdaQuery()
                        .eq(ProductionOrderBom::getProductionOrderId, productionOrderId)
                        .orderByDesc(ProductionOrderBom::getId)
                        .last("limit 1"));
        if (orderBom == null || orderBom.getId() == null) {
            return Collections.emptyList();
        }
        List<ProductionBomStructureVo> bomStructureList = productionBomStructureMapper.pickByBomId(orderBom.getId());
        if (bomStructureList == null || bomStructureList.isEmpty()) {
            return Collections.emptyList();
        }
        List<Long> productModelIds = bomStructureList.stream()
                .map(ProductionBomStructureVo::getProductModelId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        Map<Long, BigDecimal> stockQuantityMap = new HashMap<>();
        Map<Long, LinkedHashSet<String>> stockBatchNoMap = new HashMap<>();
        if (!productModelIds.isEmpty()) {
            List<StockInventory> stockList = stockInventoryMapper.selectList(
                    Wrappers.<StockInventory>lambdaQuery()
                            .in(StockInventory::getProductModelId, productModelIds));
            for (StockInventory stockItem : stockList) {
                if (stockItem == null || stockItem.getProductModelId() == null) {
                    continue;
                }
                Long productModelId = stockItem.getProductModelId();
                stockQuantityMap.merge(productModelId, defaultDecimal(stockItem.getQualitity()), BigDecimal::add);
                String batchNo = stockItem.getBatchNo();
                if (batchNo != null && !batchNo.trim().isEmpty()) {
                    stockBatchNoMap.computeIfAbsent(productModelId, key -> new LinkedHashSet<>()).add(batchNo);
                }
            }
        }
        List<ProductionOrderPickVo> result = new ArrayList<>(bomStructureList.size());
        for (ProductionBomStructureVo structure : bomStructureList) {
            if (structure == null || structure.getProductModelId() == null) {
                continue;
            }
            Long productModelId = structure.getProductModelId();
            ProductionOrderPickVo vo = new ProductionOrderPickVo();
            vo.setProductModelId(productModelId);
            vo.setOperationName(structure.getOperationName());
            vo.setTechnologyOperationId(structure.getTechnologyOperationId());
            vo.setProductName(structure.getProductName());
            vo.setModel(structure.getModel());
            vo.setDemandedQuantity(defaultDecimal(structure.getDemandedQuantity()));
            vo.setUnit(structure.getUnit());
            List<String> batchNoList = stockBatchNoMap.get(productModelId) == null
                    ? Collections.emptyList()
                    : new ArrayList<>(stockBatchNoMap.get(productModelId));
            vo.setBatchNoList(batchNoList);
            vo.setStockQuantity(stockQuantityMap.getOrDefault(productModelId, BigDecimal.ZERO));
            vo.setBom(true);
            result.add(vo);
        }
        return result;
    }
}
src/main/java/com/ruoyi/stock/mapper/StockInventoryMapper.java
@@ -52,4 +52,6 @@
    BigDecimal selectTotalByDate(@Param("now") LocalDate now);
    BigDecimal selectPendingOutQuantity(@Param("productModelId") Long productModelId, @Param("batchNo") String batchNo, @Param("type") String type);
    List<StockInventory> listSelectableBatchNoByProductModelIds(@Param("productModelIds") List<Long> productModelIds);
}
src/main/resources/mapper/basic/ProductModelMapper.xml
@@ -14,7 +14,16 @@
        <result column="product_name" property="productName" />
        <result column="product_id" property="productId" />
    </resultMap>
    <select id="listPageProductModel" resultType="com.ruoyi.basic.pojo.ProductModel">
    <resultMap id="ProductModelVoResultMap" type="com.ruoyi.basic.vo.ProductModelVo" extends="BaseResultMap">
        <result column="create_time" property="createTime" />
        <collection property="batchNoList"
                    ofType="java.lang.String"
                    column="{productModelId=id}"
                    select="selectBatchNoListByProductModelId"/>
    </resultMap>
    <select id="listPageProductModel" resultMap="ProductModelVoResultMap">
        select pm.*,p.product_name
        from product_model pm
        left join product p on pm.product_id = p.id
@@ -44,6 +53,15 @@
        </where>
        order by  pm.id
    </select>
    <select id="selectBatchNoListByProductModelId" resultType="java.lang.String">
        select distinct si.batch_no
        from stock_inventory si
        where si.product_model_id = #{productModelId}
          and si.batch_no is not null
          and si.batch_no != ''
        order by si.batch_no
    </select>
    <select id="selectLatestRecord" resultType="com.ruoyi.basic.pojo.ProductModel">
            SELECT * FROM product_model
            ORDER BY create_time DESC, id DESC
src/main/resources/mapper/production/ProductionBomStructureMapper.xml
@@ -31,4 +31,23 @@
        order by pbs.id
    </select>
    <select id="pickByBomId" resultType="com.ruoyi.production.bean.vo.ProductionBomStructureVo">
        SELECT
            pbs.*,
            p.product_name AS productName,
            pm.product_id AS productId,
            pm.model,
            top1.NAME AS operationName
        FROM
            production_bom_structure pbs
                LEFT JOIN product_model pm ON pbs.product_model_id = pm.id
                LEFT JOIN product p ON pm.product_id = p.id
                LEFT JOIN technology_operation top1 ON pbs.technology_operation_id = top1.id
        WHERE
            pbs.parent_id IS NOT NULL
          AND pbs.production_order_bom_id = #{bomId}
        ORDER BY
            pbs.id
    </select>
</mapper>
src/main/resources/mapper/production/ProductionOrderPickMapper.xml
@@ -14,6 +14,24 @@
        <result column="remark" property="remark" />
        <result column="create_user" property="createUser" />
        <result column="dept_id" property="deptId" />
        <result column="operation_name" property="operationName" />
        <result column="technology_operation_id" property="technologyOperationId" />
        <result column="demanded_quantity" property="demandedQuantity" />
        <result column="is_bom" property="bom" />
    </resultMap>
    <select id="listPickedDetailByOrderId" resultType="com.ruoyi.production.bean.vo.ProductionOrderPickVo">
        select pop.*,
               pop.is_bom as bom,
               pop.quantity as pickQuantity,
               p.product_name as productName,
               pm.model as model,
               pm.unit as unit
        from production_order_pick pop
                 left join product_model pm on pop.product_model_id = pm.id
                 left join product p on pm.product_id = p.id
        where pop.production_order_id = #{productionOrderId}
        order by pop.create_time desc, pop.id desc
    </select>
</mapper>
src/main/resources/mapper/production/ProductionOrderPickRecordMapper.xml
@@ -21,4 +21,19 @@
        <result column="dept_id" property="deptId" />
    </resultMap>
    <select id="listPickedDetailByOrderId" resultType="com.ruoyi.production.bean.vo.ProductionOrderPickVo">
        select popr.*,
               poro.operation_name as operationName,
               p.product_name as productName,
               pm.model as model,
               pm.unit as unit
        from production_order_pick_record popr
                 left join production_operation_task pot on popr.production_operation_task_id = pot.id
                 left join production_order_routing_operation poro on pot.technology_routing_operation_id = poro.id
                 left join product_model pm on popr.product_model_id = pm.id
                 left join product p on pm.product_id = p.id
        where popr.production_order_id = #{productionOrderId}
        order by popr.create_time desc, popr.id desc
    </select>
</mapper>
src/main/resources/mapper/stock/StockInventoryMapper.xml
@@ -439,4 +439,18 @@
          AND sor.approval_status = 0
    </select>
    <select id="listSelectableBatchNoByProductModelIds" resultType="com.ruoyi.stock.pojo.StockInventory">
        select distinct si.product_model_id,
                        si.batch_no
        from stock_inventory si
        where si.product_model_id in
        <foreach collection="productModelIds" item="productModelId" open="(" separator="," close=")">
            #{productModelId}
        </foreach>
          and si.batch_no is not null
          and si.batch_no != ''
          and (si.qualitity - ifnull(si.locked_quantity, 0)) > 0
        order by si.product_model_id, si.batch_no
    </select>
</mapper>