昨天 d5bbd17a1428811da046ec3be3c0cc943a7ae059
src/main/java/com/ruoyi/mock/service/impl/DataGenerateServiceImpl.java
@@ -5,27 +5,51 @@
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.ai.assistant.Assistant;
import com.ruoyi.approve.mapper.ApprovalInstanceMapper;
import com.ruoyi.approve.mapper.ApprovalInstanceNodeMapper;
import com.ruoyi.approve.mapper.ApprovalRecordMapper;
import com.ruoyi.approve.mapper.ApprovalTaskMapper;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.approve.pojo.ApprovalInstanceNode;
import com.ruoyi.approve.pojo.ApprovalRecord;
import com.ruoyi.approve.pojo.ApprovalTask;
import com.ruoyi.approve.service.ApprovalInstanceService;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.basic.service.ICustomerService;
import com.ruoyi.basic.service.ISupplierService;
import com.ruoyi.common.enums.ReviewStatusEnum;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.mock.dto.DataGenerateRequest;
import com.ruoyi.mock.prompt.MockDataPrompt;
import com.ruoyi.mock.service.DataGenerateService;
import com.ruoyi.mock.vo.DataGenerateResult;
import com.ruoyi.mock.vo.DataGenerateResult.ModuleSummary;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionPlan;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.production.bean.dto.ProductionOrderPickDto;
import com.ruoyi.production.bean.dto.ProductionPlanDto;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.bean.vo.ProductionBomStructureVo;
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.pojo.*;
import com.ruoyi.production.service.ProductionOrderPickService;
import com.ruoyi.production.service.ProductionOrderService;
import com.ruoyi.production.service.ProductionPlanService;
import com.ruoyi.production.service.ProductionProductMainService;
import com.ruoyi.purchase.dto.PurchaseLedgerDto;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.purchase.service.IPurchaseLedgerService;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.mapper.SysUserDeptMapper;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityTestStandard;
import com.ruoyi.quality.pojo.QualityTestStandardBinding;
@@ -33,11 +57,29 @@
import com.ruoyi.quality.service.IQualityTestStandardService;
import com.ruoyi.quality.service.QualityTestStandardBindingService;
import com.ruoyi.sales.dto.SalesLedgerDto;
import com.ruoyi.sales.dto.ShippingInfoDto;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.mapper.ShippingInfoMapper;
import com.ruoyi.sales.mapper.ShippingProductDetailMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.pojo.ShippingProductDetail;
import com.ruoyi.sales.service.ISalesLedgerService;
import com.ruoyi.sales.service.ShippingInfoService;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.stock.pojo.StockOutRecord;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.service.StockInRecordService;
import com.ruoyi.stock.service.StockOutRecordService;
import com.ruoyi.stock.service.StockInventoryService;
import com.ruoyi.technology.mapper.TechnologyBomStructureMapper;
import com.ruoyi.technology.mapper.TechnologyRoutingMapper;
import com.ruoyi.technology.mapper.TechnologyRoutingOperationMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -46,11 +88,15 @@
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
@@ -76,8 +122,43 @@
    // 采购完整流程需要的service
    private final ApprovalInstanceService approvalInstanceService;
    private final ApprovalInstanceMapper approvalInstanceMapper;
    private final ApprovalInstanceNodeMapper approvalInstanceNodeMapper;
    private final ApprovalTaskMapper approvalTaskMapper;
    private final ApprovalRecordMapper approvalRecordMapper;
    private final IQualityInspectService qualityInspectService;
    private final QualityInspectMapper qualityInspectMapper;
    private final StockInRecordService stockInRecordService;
    private final StockInRecordMapper stockInRecordMapper;
    // 销售完整流程需要的service/mapper
    private final SalesLedgerMapper salesLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final ProductionPlanMapper productionPlanMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionProductMainService productionProductMainService;
    private final ShippingInfoService shippingInfoService;
    private final ShippingInfoMapper shippingInfoMapper;
    private final ShippingProductDetailMapper shippingProductDetailMapper;
    private final StockUtils stockUtils;
    private final StockOutRecordMapper stockOutRecordMapper;
    private final StockOutRecordService stockOutRecordService;
    private final StockInventoryMapper stockInventoryMapper;
    // 工艺数据检测
    private final TechnologyRoutingMapper technologyRoutingMapper;
    private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
    private final TechnologyBomStructureMapper technologyBomStructureMapper;
    // 领料
    private final ProductionOrderPickService productionOrderPickService;
    private final ProductionOrderBomMapper productionOrderBomMapper;
    private final ProductionBomStructureMapper productionBomStructureMapper;
    // 用户/角色查询
    private final SysUserMapper sysUserMapper;
    private final SysDeptMapper sysDeptMapper;
    private final SysUserDeptMapper sysUserDeptMapper;
    @Override
    public DataGenerateResult generate(DataGenerateRequest request) {
@@ -87,10 +168,18 @@
        int totalGenerated = 0;
        try {
            List<Long> productIds = productMapper.selectList(null).stream()
                    .map(p -> p.getId()).collect(Collectors.toList());
            List<Long> productModelIds = productModelMapper.selectList(null).stream()
                    .map(m -> m.getId()).collect(Collectors.toList());
            // 构建产品规格分类:通过Product树根节点判断成品/原材料
            Map<Long, String> productModelIdToCategory = buildProductModelCategoryMap();
            List<Long> productModelIds = new ArrayList<>(productModelIdToCategory.keySet());
            // 加载已有客户/供应商(不新增,使用已有数据)
            Map<String, Long> customerNameToId = loadExistingCustomers();
            Map<String, Long> supplierNameToId = loadExistingSuppliers();
            List<String> existingCustomerNames = new ArrayList<>(customerNameToId.keySet());
            List<String> existingSupplierNames = new ArrayList<>(supplierNameToId.keySet());
            // 加载系统用户信息(昵称、角色、部门)
            List<SysUser> systemUsers = loadSystemUsersWithDetail();
            String systemPrompt = MockDataPrompt.buildSystemPrompt();
            String userMessage = MockDataPrompt.buildUserMessage(
@@ -98,7 +187,9 @@
                    request.getCountMin(), request.getCountMax(),
                    request.getDateStart(), request.getDateEnd(),
                    request.getAdditionalInfo(),
                    productIds, productModelIds);
                    productModelIdToCategory,
                    systemUsers,
                    existingCustomerNames, existingSupplierNames);
            String fullPrompt = systemPrompt + "\n\n" + userMessage;
            log.info("调用AI生成模拟数据, modules={}, industries={}", request.getModules(), request.getIndustries());
@@ -120,24 +211,17 @@
                return result;
            }
            // 修正AI生成的日期到指定时间范围内
            String dateStart = request.getDateStart();
            String dateEnd = request.getDateEnd();
            if (dateStart != null && dateEnd != null) {
                fixDatesInRange(entities, dateStart, dateEnd);
            }
            Map<String, List<JSONObject>> grouped = entities.stream()
                    .collect(Collectors.groupingBy(e -> e.getString("entity")));
            // 名称→ID 映射表,用于创建业务单据时回填外键
            Map<String, Long> customerNameToId = new HashMap<>();
            Map<String, Long> supplierNameToId = new HashMap<>();
            // Tier 0: 基础数据
            if (grouped.containsKey("customer")) {
                ModuleSummary s = createCustomers(grouped.get("customer"), customerNameToId);
                summaries.add(s);
                totalGenerated += s.getSuccessCount();
            }
            if (grouped.containsKey("supplier")) {
                ModuleSummary s = createSuppliers(grouped.get("supplier"), supplierNameToId);
                summaries.add(s);
                totalGenerated += s.getSuccessCount();
            }
            // Tier 0: 基础数据(客户和供应商不自动生成,使用已有数据)
            if (grouped.containsKey("qualityTestStandard")) {
                ModuleSummary s = createQualityStandards(grouped.get("qualityTestStandard"));
                summaries.add(s);
@@ -146,13 +230,13 @@
            // Tier 1: 业务单据
            if (grouped.containsKey("salesLedger")) {
                ModuleSummary s = createSalesLedgers(grouped.get("salesLedger"), customerNameToId);
                ModuleSummary s = createSalesLedgers(grouped.get("salesLedger"), customerNameToId, request.getDateStart());
                summaries.add(s);
                totalGenerated += s.getSuccessCount();
            }
            if (grouped.containsKey("purchaseLedger")) {
                ModuleSummary s = createPurchaseLedgers(grouped.get("purchaseLedger"), supplierNameToId,
                        request.getAdditionalInfo(), request.getDateEnd());
                        request.getAdditionalInfo(), request.getDateStart(), request.getDateEnd());
                summaries.add(s);
                totalGenerated += s.getSuccessCount();
            }
@@ -216,7 +300,154 @@
        }
    }
    /**
     * 修正AI生成的日期到指定时间范围内。
     * 遍历所有实体的所有字段,如果发现日期字符串(yyyy-MM-dd格式)超出范围,替换为范围内的随机日期。
     */
    private void fixDatesInRange(List<JSONObject> entities, String dateStart, String dateEnd) {
        LocalDate start;
        LocalDate end;
        try {
            start = LocalDate.parse(dateStart);
            end = LocalDate.parse(dateEnd);
        } catch (Exception e) {
            log.warn("日期范围解析失败: {} ~ {}", dateStart, dateEnd);
            return;
        }
        long daysBetween = ChronoUnit.DAYS.between(start, end);
        if (daysBetween <= 0) {
            daysBetween = 1;
        }
        // 所有已知的日期字段名
        java.util.Set<String> dateFields = java.util.Set.of(
                "entryDate", "executionDate", "deliveryDate", "requiredDate",
                "promisedDeliveryDate", "planCompleteTime", "maintenanceTime",
                "checkTime", "registerDate"
        );
        for (JSONObject entity : entities) {
            for (String key : dateFields) {
                String val = entity.getString(key);
                if (val != null && val.matches("\\d{4}-\\d{2}-\\d{2}")) {
                    try {
                        LocalDate d = LocalDate.parse(val);
                        if (d.isBefore(start) || d.isAfter(end)) {
                            LocalDate fixed = start.plusDays(ThreadLocalRandom.current().nextLong(daysBetween + 1));
                            entity.put(key, fixed.toString());
                            log.debug("修正日期 {}: {} -> {}", key, val, fixed);
                        }
                    } catch (Exception ignored) {
                    }
                } else if (val != null && !val.matches("\\d{4}-\\d{2}-\\d{2}")) {
                    // AI返回了无效日期格式,直接替换为范围内的随机日期
                    LocalDate fixed = start.plusDays(ThreadLocalRandom.current().nextLong(daysBetween + 1));
                    entity.put(key, fixed.toString());
                    log.debug("修正无效日期 {}: {} -> {}", key, val, fixed);
                }
            }
            // 修正合同编号中的日期: CG-20230601-001 -> CG-YYYYMMDD-001
            for (String key : java.util.Set.of("purchaseContractNumber", "salesContractNo", "standardNo", "batchNo")) {
                String val = entity.getString(key);
                if (val != null && val.matches(".*\\d{8}.*")) {
                    String datePart = val.replaceAll(".*?(\\d{8}).*", "$1");
                    try {
                        // 验证提取到的是有效日期
                        String dateStr = datePart.substring(0, 4) + "-" + datePart.substring(4, 6) + "-" + datePart.substring(6, 8);
                        LocalDate d = LocalDate.parse(dateStr);
                        if (d.isBefore(start) || d.isAfter(end)) {
                            LocalDate fixed = start.plusDays(ThreadLocalRandom.current().nextLong(daysBetween + 1));
                            String newDatePart = fixed.format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE);
                            String newVal = val.replace(datePart, newDatePart);
                            entity.put(key, newVal);
                            log.debug("修正编号 {}: {} -> {}", key, val, newVal);
                        }
                    } catch (Exception ignored) {
                    }
                }
            }
        }
    }
    // ---- Tier 0: 基础数据 ----
    /**
     * 从数据库加载已有客户名称→ID映射
     */
    private Map<String, Long> loadExistingCustomers() {
        Map<String, Long> map = new HashMap<>();
        List<Customer> customers = customerService.list();
        for (Customer c : customers) {
            if (c.getCustomerName() != null) {
                map.put(c.getCustomerName(), c.getId());
            }
        }
        return map;
    }
    /**
     * 从数据库加载已有供应商名称→ID映射
     */
    private Map<String, Long> loadExistingSuppliers() {
        Map<String, Long> map = new HashMap<>();
        List<SupplierManage> suppliers = supplierService.list();
        for (SupplierManage s : suppliers) {
            if (s.getSupplierName() != null) {
                map.put(s.getSupplierName(), s.getId());
            }
        }
        return map;
    }
    /**
     * 加载系统用户信息(含角色和部门名称)
     */
    private List<SysUser> loadSystemUsersWithDetail() {
        return sysUserMapper.selectUserListWithDetail();
    }
    /**
     * 构建产品规格ID→顶级产品分类名称的映射
     * 通过Product树向上找到根节点,判断"成品"/"原材料"/"半成品"
     */
    private Map<Long, String> buildProductModelCategoryMap() {
        // 加载所有Product节点
        List<Product> allProducts = productMapper.selectList(null);
        Map<Long, Product> productMap = allProducts.stream()
                .collect(Collectors.toMap(Product::getId, p -> p, (a, b) -> a));
        // 对每个Product,沿parentId向上找到根节点名称
        Map<Long, String> productIdToRootName = new HashMap<>();
        for (Product p : allProducts) {
            if (productIdToRootName.containsKey(p.getId())) continue;
            // 向上遍历找根
            List<Long> chain = new ArrayList<>();
            Long currentId = p.getId();
            while (currentId != null) {
                chain.add(currentId);
                Product current = productMap.get(currentId);
                if (current == null || current.getParentId() == null) break;
                currentId = current.getParentId();
            }
            // 根节点的productName就是分类名
            Product root = productMap.get(chain.get(chain.size() - 1));
            String rootName = root != null ? root.getProductName() : "其他";
            for (Long id : chain) {
                productIdToRootName.put(id, rootName);
            }
        }
        // 对每个ProductModel,通过productId找到其所属分类
        List<ProductModel> allModels = productModelMapper.selectList(null);
        Map<Long, String> result = new HashMap<>();
        for (ProductModel m : allModels) {
            String category = m.getProductId() != null
                    ? productIdToRootName.getOrDefault(m.getProductId(), "其他")
                    : "其他";
            result.put(m.getId(), category);
        }
        return result;
    }
    private ModuleSummary createCustomers(List<JSONObject> items, Map<String, Long> nameToId) {
        int success = 0, fail = 0;
@@ -232,7 +463,10 @@
                c.setTaxpayerIdentificationNumber(item.getString("taxpayerIdentificationNumber"));
                c.setMaintainer(item.getString("maintainer"));
                if (item.containsKey("maintenanceTime")) {
                    c.setMaintenanceTime(java.sql.Date.valueOf(item.getString("maintenanceTime")));
                    String mtStr = item.getString("maintenanceTime");
                    if (mtStr != null && mtStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        c.setMaintenanceTime(java.sql.Date.valueOf(mtStr));
                    }
                } else {
                    c.setMaintenanceTime(new java.sql.Date(System.currentTimeMillis()));
                }
@@ -309,7 +543,7 @@
    // ---- Tier 1: 业务单据 ----
    private ModuleSummary createSalesLedgers(List<JSONObject> items, Map<String, Long> customerNameToId) {
    private ModuleSummary createSalesLedgers(List<JSONObject> items, Map<String, Long> customerNameToId, String dateStart) {
        int success = 0, fail = 0;
        for (JSONObject item : items) {
            try {
@@ -323,20 +557,56 @@
                dto.setSalesman(item.getString("salesman"));
                dto.setPaymentMethod(item.getString("paymentMethod"));
                dto.setType(item.getInteger("type"));
                // 录入人:优先使用AI提供的数据,否则用当前登录用户
                if (item.containsKey("entryPerson")) {
                    dto.setEntryPerson(item.getString("entryPerson"));
                // 录入人:优先用AI返回的,否则从"销售"角色/部门随机选
                String entryPerson = item.getString("entryPerson");
                if (entryPerson == null || entryPerson.isBlank()) {
                    SysUser salesUser = randomUserByKeyword("销售");
                    entryPerson = salesUser != null ? salesUser.getNickName() : "系统";
                }
                dto.setEntryPerson(entryPerson);
                // 业务员:优先用AI返回的,否则从"销售"角色/部门随机选
                String salesman = dto.getSalesman();
                if (salesman == null || salesman.isBlank()) {
                    SysUser salesUser = randomUserByKeyword("销售");
                    salesman = salesUser != null ? salesUser.getNickName() : "系统";
                    dto.setSalesman(salesman);
                }
                // customerId为null会导致"客户不存在"异常,跳过
                if (customerId == null) {
                    log.warn("跳过销售台账,客户[{}]不存在", customerName);
                    fail++;
                    continue;
                }
                LocalDate entryDate = null;
                if (item.containsKey("entryDate")) {
                    dto.setEntryDate(java.sql.Date.valueOf(item.getString("entryDate")));
                    String entryDateStr = item.getString("entryDate");
                    if (entryDateStr != null && entryDateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        entryDate = LocalDate.parse(entryDateStr);
                        dto.setEntryDate(java.sql.Date.valueOf(entryDateStr));
                    } else {
                        dto.setEntryDate(java.sql.Date.valueOf(dateStart));
                    }
                }
                if (item.containsKey("executionDate")) {
                    dto.setExecutionDate(LocalDate.parse(item.getString("executionDate")));
                    String execDateStr = item.getString("executionDate");
                    if (execDateStr != null && execDateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        dto.setExecutionDate(LocalDate.parse(execDateStr));
                    } else {
                        dto.setExecutionDate(LocalDate.parse(dateStart));
                    }
                }
                if (item.containsKey("deliveryDate")) {
                    dto.setDeliveryDate(LocalDate.parse(item.getString("deliveryDate")));
                    String delDateStr = item.getString("deliveryDate");
                    if (delDateStr != null && delDateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        dto.setDeliveryDate(LocalDate.parse(delDateStr));
                    } else {
                        dto.setDeliveryDate(LocalDate.parse(dateStart));
                    }
                }
                JSONArray productData = item.getJSONArray("productData");
                boolean hasProduction = false;
                if (productData != null) {
                    List<SalesLedgerProduct> products = new ArrayList<>();
                    for (int i = 0; i < productData.size(); i++) {
@@ -353,7 +623,6 @@
                        if (pd.containsKey("taxExclusiveTotalPrice")) {
                            slp.setTaxExclusiveTotalPrice(pd.getBigDecimal("taxExclusiveTotalPrice"));
                        } else if (pd.getBigDecimal("taxInclusiveTotalPrice") != null && pd.getBigDecimal("taxRate") != null) {
                            // 不含税总价 = 含税总价 / (1 + 税率/100)
                            slp.setTaxExclusiveTotalPrice(
                                pd.getBigDecimal("taxInclusiveTotalPrice").divide(
                                    BigDecimal.ONE.add(pd.getBigDecimal("taxRate").divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP)),
@@ -361,22 +630,603 @@
                        } else {
                            slp.setTaxExclusiveTotalPrice(BigDecimal.ZERO);
                        }
                        slp.setIsProduction(true);
                        products.add(slp);
                        hasProduction = true;
                    }
                    dto.setProductData(products);
                }
                salesLedgerService.addOrUpdateSalesLedger(dto);
                // addOrUpdateSalesLedger内部会自动生成合同号并insert,但dto上没有回写
                // 直接用dto的entryDate和customerId查最新创建的记录
                SalesLedger savedLedger = salesLedgerMapper.selectOne(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SalesLedger>()
                                .eq(SalesLedger::getCustomerId, dto.getCustomerId())
                                .orderByDesc(SalesLedger::getId)
                                .last("limit 1"));
                if (savedLedger != null && hasProduction) {
                    processSalesFullFlow(savedLedger, entryDate);
                }
                success++;
            } catch (Exception e) {
                log.warn("创建销售台账失败: {}", e.getMessage());
                log.warn("创建销售台账失败: {}", e.getMessage(), e);
                fail++;
            }
        }
        return summary("sales", "销售台账", items.size(), success, fail);
    }
    // ==================== 销售完整流程 ====================
    /**
     * 销售完整流程: 生产计划合并下发 → 领料 → 报工 → 质检(可选) → 入库 → 发货 → 出库
     * 时间以销售录入日期为基准,各环节递推0-3天
     */
    private void processSalesFullFlow(SalesLedger salesLedger, LocalDate entryDate) {
        try {
            LocalDate baseDate = entryDate != null ? entryDate : LocalDate.now();
            // 1. 查询销售台账产品(含生产计划)
            List<SalesLedgerProduct> products = salesLedgerProductMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SalesLedgerProduct>()
                            .eq(SalesLedgerProduct::getSalesLedgerId, salesLedger.getId())
                            .eq(SalesLedgerProduct::getIsProduction, true));
            if (products.isEmpty()) {
                log.info("销售台账[{}]无需生产,跳过生产流程", salesLedger.getSalesContractNo());
                return;
            }
            // 检查产品是否具备完整的工艺数据(工艺路线、工序、BOM)
            List<Long> missingTechProductIds = checkTechnologyDataReadiness(products);
            if (!missingTechProductIds.isEmpty()) {
                log.warn("销售台账[{}]以下产品缺少工艺数据(工艺路线/工序/BOM),跳过生产流程: {}",
                        salesLedger.getSalesContractNo(), missingTechProductIds);
                return;
            }
            // 2. 生产计划合并下发 → 生产订单
            LocalDate planDate = baseDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
            processProductionCombine(salesLedger, planDate);
            // 3. 领料(物料不足自动入库后再领)
            LocalDate pickDate = planDate.plusDays(ThreadLocalRandom.current().nextInt(0, 3));
            processMaterialPick(salesLedger, pickDate);
            // 4. 生产报工
            LocalDate reportDate = pickDate.plusDays(ThreadLocalRandom.current().nextInt(1, 4));
            processProductionReport(salesLedger, reportDate);
            // 5. 质检提交 + 入库审批
            LocalDate qualityDate = reportDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
            processSalesQualityAndStockIn(salesLedger, qualityDate);
            // 6. 发货 + 发货审批 + 出库审批
            LocalDate shipDate = qualityDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
            processSalesShipping(salesLedger, shipDate);
            log.info("销售台账[{}]完整流程处理完毕", salesLedger.getSalesContractNo());
        } catch (Exception e) {
            log.warn("销售完整流程处理失败[{}]: {}", salesLedger.getSalesContractNo(), e.getMessage());
        }
    }
    /**
     * 查询角色或部门含指定关键字的用户,随机返回一个
     */
    private SysUser randomUserByKeyword(String keyword) {
        try {
            List<Long> userIds = new ArrayList<>(sysUserMapper.getUserByRole(keyword));
            List<SysDept> depts = sysDeptMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysDept>()
                            .like(SysDept::getDeptName, keyword)
                            .eq(SysDept::getDelFlag, "0")
                            .eq(SysDept::getStatus, "0"));
            if (!depts.isEmpty()) {
                List<Long> deptIds = depts.stream().map(SysDept::getDeptId).collect(Collectors.toList());
                List<Long> deptUserIds = sysUserDeptMapper.selectDistinctUserIdsByDeptIds(deptIds);
                userIds.addAll(deptUserIds);
            }
            if (userIds.isEmpty()) {
                return null;
            }
            List<Long> distinctIds = userIds.stream().distinct().collect(Collectors.toList());
            Long randomId = distinctIds.get(ThreadLocalRandom.current().nextInt(distinctIds.size()));
            return sysUserMapper.selectUserById(randomId);
        } catch (Exception e) {
            log.warn("查询[{}]角色/部门用户失败: {}", keyword, e.getMessage());
            return null;
        }
    }
    /**
     * 查询角色或部门含"生产"的用户,随机返回一个
     */
    private SysUser randomProductionUser() {
        return randomUserByKeyword("生产");
    }
    /**
     * 检查产品是否具备完整的工艺数据(工艺路线、工序、BOM)
     * @return 缺少工艺数据的产品规格ID列表(空列表表示全部就绪)
     */
    private List<Long> checkTechnologyDataReadiness(List<SalesLedgerProduct> products) {
        List<Long> missingIds = new ArrayList<>();
        for (SalesLedgerProduct slp : products) {
            Long productModelId = slp.getProductModelId();
            if (productModelId == null) continue;
            // 1. 检查工艺路线
            List<com.ruoyi.technology.pojo.TechnologyRouting> routings =
                    technologyRoutingMapper.selectList(
                            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.technology.pojo.TechnologyRouting>()
                                    .eq(com.ruoyi.technology.pojo.TechnologyRouting::getProductModelId, productModelId));
            if (routings.isEmpty()) {
                log.warn("产品规格[{}]缺少工艺路线", productModelId);
                missingIds.add(productModelId);
                continue;
            }
            Long routingId = routings.get(0).getId();
            // 2. 检查工艺路线是否有关联工序
            List<com.ruoyi.technology.pojo.TechnologyRoutingOperation> routingOps =
                    technologyRoutingOperationMapper.selectList(
                            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.technology.pojo.TechnologyRoutingOperation>()
                                    .eq(com.ruoyi.technology.pojo.TechnologyRoutingOperation::getTechnologyRoutingId, routingId));
            if (routingOps.isEmpty()) {
                log.warn("产品规格[{}]工艺路线[{}]缺少工序", productModelId, routingId);
                missingIds.add(productModelId);
                continue;
            }
            // 3. 检查BOM(可选但有更好)
            com.ruoyi.technology.pojo.TechnologyRouting routing = routings.get(0);
            if (routing.getBomId() != null) {
                List<com.ruoyi.technology.pojo.TechnologyBomStructure> bomStructures =
                        technologyBomStructureMapper.selectList(
                                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.technology.pojo.TechnologyBomStructure>()
                                        .eq(com.ruoyi.technology.pojo.TechnologyBomStructure::getBomId, routing.getBomId()));
                if (bomStructures.isEmpty()) {
                    log.warn("产品规格[{}]BOM[{}]缺少产品结构", productModelId, routing.getBomId());
                    missingIds.add(productModelId);
                }
            }
        }
        return missingIds;
    }
    /**
     * 生产计划合并下发 → 生产订单(按productModelId分组,同型号合并下发)
     */
    private void processProductionCombine(SalesLedger salesLedger, LocalDate planDate) {
        try {
            List<ProductionPlan> plans = productionPlanMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProductionPlan>()
                            .eq(ProductionPlan::getSalesLedgerId, salesLedger.getId())
                            .and(w -> w.eq(ProductionPlan::getStatus, 0).or().isNull(ProductionPlan::getStatus)));
            if (plans.isEmpty()) {
                log.info("销售台账[{}]无待下发生产计划", salesLedger.getSalesContractNo());
                return;
            }
            // 按productModelId分组,同型号才能合并下发;productModelId为null的单独逐个下发
            Map<Long, List<ProductionPlan>> grouped = plans.stream()
                    .filter(p -> p.getProductModelId() != null)
                    .collect(Collectors.groupingBy(ProductionPlan::getProductModelId));
            for (Map.Entry<Long, List<ProductionPlan>> entry : grouped.entrySet()) {
                try {
                    List<ProductionPlan> sameModelPlans = entry.getValue();
                    BigDecimal totalQty = sameModelPlans.stream()
                            .map(p -> Optional.ofNullable(p.getQtyRequired()).orElse(BigDecimal.ZERO))
                            .reduce(BigDecimal.ZERO, BigDecimal::add);
                    ProductionPlanDto combineDto = new ProductionPlanDto();
                    combineDto.setIds(sameModelPlans.stream().map(ProductionPlan::getId).collect(Collectors.toList()));
                    combineDto.setTotalAssignedQuantity(totalQty);
                    combineDto.setPlanCompleteTime(planDate.plusDays(ThreadLocalRandom.current().nextInt(3, 7)));
                    productionPlanService.combine(combineDto);
                    // 修正生产订单的planCompleteTime
                    List<ProductionOrder> orders = productionOrderMapper.selectList(
                            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProductionOrder>()
                                    .like(ProductionOrder::getProductionPlanIds, String.valueOf(sameModelPlans.get(0).getId())));
                    for (ProductionOrder order : orders) {
                        order.setPlanCompleteTime(planDate.plusDays(ThreadLocalRandom.current().nextInt(3, 7)));
                        order.setStartTime(planDate.atStartOfDay());
                        productionOrderMapper.updateById(order);
                    }
                    log.info("销售台账[{}]生产计划下发成功, productModelId={}, 计划数: {}",
                            salesLedger.getSalesContractNo(), entry.getKey(), sameModelPlans.size());
                } catch (Exception e) {
                    log.warn("销售台账[{}]生产计划下发失败, productModelId={}: {}",
                            salesLedger.getSalesContractNo(), entry.getKey(), e.getMessage());
                }
            }
        } catch (Exception e) {
            log.warn("生产计划下发失败[{}]: {}", salesLedger.getSalesContractNo(), e.getMessage());
        }
    }
    /**
     * 领料: 查询生产订单的BOM领料清单,检查库存,不足则先自动入库审批后再领料
     */
    private void processMaterialPick(SalesLedger salesLedger, LocalDate pickDate) {
        try {
            List<ProductionOrder> orders = findOrdersForSalesLedger(salesLedger);
            if (orders.isEmpty()) {
                log.info("销售台账[{}]无生产订单,跳过领料", salesLedger.getSalesContractNo());
                return;
            }
            for (ProductionOrder order : orders) {
                try {
                    // 查询订单BOM领料清单
                    List<com.ruoyi.production.bean.vo.ProductionOrderPickVo> pickList = productionOrderService.pick(order.getId());
                    if (pickList == null || pickList.isEmpty()) {
                        log.info("生产订单[{}]无领料清单", order.getId());
                        continue;
                    }
                    // 检查库存不足的物料,先自动入库
                    ensureMaterialStock(pickList, pickDate);
                    // 重新获取领料清单(入库后批号和库存量已更新)
                    pickList = productionOrderService.pick(order.getId());
                    if (pickList == null || pickList.isEmpty()) {
                        log.info("生产订单[{}]重新获取领料清单为空", order.getId());
                        continue;
                    }
                    // 执行领料
                    List<ProductionOrderPickDto> pickDtoList = new ArrayList<>();
                    for (com.ruoyi.production.bean.vo.ProductionOrderPickVo pickVo : pickList) {
                        ProductionOrderPickDto pickDto = new ProductionOrderPickDto();
                        pickDto.setProductionOrderId(order.getId());
                        pickDto.setProductModelId(pickVo.getProductModelId());
                        pickDto.setPickQuantity(pickVo.getDemandedQuantity());
                        pickDto.setPickType((byte) 1);
                        pickDto.setOperationName(pickVo.getOperationName());
                        pickDto.setTechnologyOperationId(pickVo.getTechnologyOperationId());
                        pickDto.setDemandedQuantity(pickVo.getDemandedQuantity());
                        pickDto.setBom(pickVo.getBom());
                        // 设置批号(用更新后的批号列表)
                        if (pickVo.getBatchNoList() != null && !pickVo.getBatchNoList().isEmpty()) {
                            pickDto.setBatchNoList(pickVo.getBatchNoList());
                            pickDto.setBatchNo(pickVo.getBatchNoList().get(0));
                        }
                        pickDtoList.add(pickDto);
                    }
                    if (!pickDtoList.isEmpty()) {
                        ProductionOrderPickDto batchDto = new ProductionOrderPickDto();
                        batchDto.setPickList(pickDtoList);
                        productionOrderPickService.savePick(batchDto);
                        log.info("生产订单[{}]领料成功, 领料项数: {}", order.getId(), pickDtoList.size());
                    }
                } catch (Exception e) {
                    log.warn("生产订单[{}]领料失败: {}", order.getId(), e.getMessage());
                }
            }
        } catch (Exception e) {
            log.warn("领料流程失败[{}]: {}", salesLedger.getSalesContractNo(), e.getMessage());
        }
    }
    /**
     * 确保物料库存充足,不足则先自动入库审批
     */
    private void ensureMaterialStock(List<com.ruoyi.production.bean.vo.ProductionOrderPickVo> pickList, LocalDate pickDate) {
        LocalDateTime pickDateTime = pickDate.atTime(9, 0, 0).plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
        String pickDateStr = pickDate.format(DateTimeFormatter.BASIC_ISO_DATE);
        for (com.ruoyi.production.bean.vo.ProductionOrderPickVo pickVo : pickList) {
            BigDecimal demanded = pickVo.getDemandedQuantity() != null ? pickVo.getDemandedQuantity() : BigDecimal.ZERO;
            BigDecimal stockQty = pickVo.getStockQuantity() != null ? pickVo.getStockQuantity() : BigDecimal.ZERO;
            BigDecimal shortage = demanded.subtract(stockQty);
            if (shortage.compareTo(BigDecimal.ZERO) <= 0) {
                continue; // 库存充足
            }
            // 库存不足,自动入库补充
            try {
                StockInventoryDto dto = new StockInventoryDto();
                dto.setProductModelId(pickVo.getProductModelId());
                dto.setQualitity(shortage);
                dto.setRecordType(StockInQualifiedRecordTypeEnum.CUSTOMIZATION_STOCK_IN.getCode());
                dto.setRecordId(0L);
                stockInventoryService.addStockInRecordOnly(dto);
                // 查找刚创建的入库记录
                List<com.ruoyi.stock.pojo.StockInRecord> records = stockInRecordService.list(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.stock.pojo.StockInRecord>()
                                .eq(com.ruoyi.stock.pojo.StockInRecord::getProductModelId, pickVo.getProductModelId())
                                .eq(com.ruoyi.stock.pojo.StockInRecord::getApprovalStatus, 0)
                                .orderByDesc(com.ruoyi.stock.pojo.StockInRecord::getId)
                                .last("limit 1"));
                if (!records.isEmpty()) {
                    com.ruoyi.stock.pojo.StockInRecord record = records.get(0);
                    record.setCreateTime(pickDateTime);
                    record.setBatchNo(fixBatchNoDate(record.getBatchNo(), pickDateStr));
                    record.setInboundBatches(fixBatchNoDate(record.getInboundBatches(), pickDateStr));
                    stockInRecordMapper.updateById(record);
                    // 审批通过
                    stockInRecordService.batchApprove(List.of(record.getId()), ReviewStatusEnum.APPROVED.getCode());
                    log.info("物料[{}]自动入库审批完成, 入库数量: {}", pickVo.getProductModelId(), shortage);
                }
            } catch (Exception e) {
                log.warn("物料[{}]自动入库失败: {}", pickVo.getProductModelId(), e.getMessage());
            }
        }
    }
    /**
     * 生产报工: 遍历工单逐个报工,从生产角色用户中随机选取报工人
     */
    private void processProductionReport(SalesLedger salesLedger, LocalDate reportDate) {
        try {
            List<ProductionOrder> orders = findOrdersForSalesLedger(salesLedger);
            if (orders.isEmpty()) {
                log.info("销售台账[{}]无生产订单", salesLedger.getSalesContractNo());
                return;
            }
            for (ProductionOrder order : orders) {
                List<ProductionOperationTask> tasks = productionOperationTaskMapper.selectList(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProductionOperationTask>()
                                .eq(ProductionOperationTask::getProductionOrderId, order.getId())
                                .orderByAsc(ProductionOperationTask::getId));
                for (ProductionOperationTask task : tasks) {
                    if (task.getStatus() != null && task.getStatus() >= 3) continue;
                    // 每次报工随机选一个生产用户
                    SysUser productionUser = randomProductionUser();
                    Long userId = productionUser != null ? productionUser.getUserId() : 1L;
                    String userName = productionUser != null ? productionUser.getNickName() : "系统";
                    // 报工
                    ProductionProductMainDto reportDto = new ProductionProductMainDto();
                    reportDto.setProductionOperationTaskId(task.getId());
                    reportDto.setQuantity(task.getPlanQuantity());
                    reportDto.setScrapQty(BigDecimal.ZERO);
                    reportDto.setUserId(userId);
                    reportDto.setUserName(userName);
                    productionProductMainService.addProductMain(reportDto);
                    // 修正报工时间(重新从DB读取,避免覆盖addProductMain更新的completeQuantity)
                    fixProductionReportTimes(task.getId(), reportDate);
                }
                // 修正生产订单时间(重新从DB读取,避免覆盖addProductMain更新的completeQuantity)
                fixProductionOrderTimes(order.getId(), reportDate);
            }
            log.info("销售台账[{}]生产报工完成, 订单数: {}", salesLedger.getSalesContractNo(), orders.size());
        } catch (Exception e) {
            log.warn("生产报工失败[{}]: {}", salesLedger.getSalesContractNo(), e.getMessage());
        }
    }
    /**
     * 查找销售台账关联的生产订单
     */
    private List<ProductionOrder> findOrdersForSalesLedger(SalesLedger salesLedger) {
        List<ProductionOrder> orders = new ArrayList<>();
        List<ProductionPlan> plans = productionPlanMapper.selectList(
                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProductionPlan>()
                        .eq(ProductionPlan::getSalesLedgerId, salesLedger.getId()));
        for (ProductionPlan plan : plans) {
            List<ProductionOrder> po = productionOrderMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProductionOrder>()
                            .like(ProductionOrder::getProductionPlanIds, String.valueOf(plan.getId())));
            orders.addAll(po);
        }
        // 去重
        return orders.stream()
                .collect(Collectors.toMap(ProductionOrder::getId, o -> o, (a, b) -> a))
                .values().stream().collect(Collectors.toList());
    }
    /**
     * 修正生产报工相关时间(重新从DB读取task,避免覆盖addProductMain已更新的completeQuantity)
     */
    private void fixProductionReportTimes(Long taskId, LocalDate reportDate) {
        try {
            ProductionOperationTask freshTask = productionOperationTaskMapper.selectById(taskId);
            if (freshTask != null) {
                freshTask.setActualStartTime(reportDate);
                freshTask.setActualEndTime(reportDate);
                freshTask.setStatus(4);
                productionOperationTaskMapper.updateById(freshTask);
            }
        } catch (Exception e) {
            log.warn("修正生产报工时间失败: {}", e.getMessage());
        }
    }
    /**
     * 修正生产订单时间(重新从DB读取order,避免覆盖addProductMain已更新的completeQuantity)
     */
    private void fixProductionOrderTimes(Long orderId, LocalDate reportDate) {
        try {
            ProductionOrder freshOrder = productionOrderMapper.selectById(orderId);
            if (freshOrder != null) {
                freshOrder.setStartTime(reportDate.minusDays(1).atStartOfDay());
                freshOrder.setEndTime(reportDate.plusDays(1).atStartOfDay());
                freshOrder.setStatus(3);
                productionOrderMapper.updateById(freshOrder);
            }
        } catch (Exception e) {
            log.warn("修正生产订单时间失败: {}", e.getMessage());
        }
    }
    /**
     * 销售质检提交 + 入库审批
     */
    private void processSalesQualityAndStockIn(SalesLedger salesLedger, LocalDate qualityDate) {
        try {
            LocalDateTime qualityDateTime = qualityDate.atTime(9, 0, 0)
                    .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
            // 查所有未提交的质检单(inspectType=1过程检验, 2=出厂检验,由生产报工生成)
            List<QualityInspect> allUnsubmitted = qualityInspectService.list(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<QualityInspect>()
                            .eq(QualityInspect::getInspectState, 0)
                            .in(QualityInspect::getInspectType, java.util.Arrays.asList(1, 2)));
            for (QualityInspect qi : allUnsubmitted) {
                qualityInspectService.autoSubmit(qi.getId());
                qi.setCreateTime(qualityDateTime);
                qualityInspectMapper.updateById(qi);
            }
            log.info("销售台账[{}]质检提交完成, 质检单数: {}", salesLedger.getSalesContractNo(), allUnsubmitted.size());
            // 入库审批:审批所有待审批的入库记录
            LocalDate stockDate = qualityDate.plusDays(ThreadLocalRandom.current().nextInt(0, 3));
            LocalDateTime stockDateTime = stockDate.atTime(9, 0, 0)
                    .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
            String stockDateStr = stockDate.format(DateTimeFormatter.BASIC_ISO_DATE);
            List<com.ruoyi.stock.pojo.StockInRecord> stockRecords = stockInRecordService.list(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.stock.pojo.StockInRecord>()
                            .eq(com.ruoyi.stock.pojo.StockInRecord::getApprovalStatus, 0));
            List<Long> recordIds = stockRecords.stream()
                    .map(com.ruoyi.stock.pojo.StockInRecord::getId)
                    .collect(Collectors.toList());
            if (!recordIds.isEmpty()) {
                for (com.ruoyi.stock.pojo.StockInRecord sr : stockRecords) {
                    sr.setCreateTime(stockDateTime);
                    sr.setBatchNo(fixBatchNoDate(sr.getBatchNo(), stockDateStr));
                    sr.setInboundBatches(fixBatchNoDate(sr.getInboundBatches(), stockDateStr));
                    stockInRecordMapper.updateById(sr);
                }
                stockInRecordService.batchApprove(recordIds, ReviewStatusEnum.APPROVED.getCode());
                for (com.ruoyi.stock.pojo.StockInRecord sr : stockRecords) {
                    StockInventory si = stockInventoryMapper.selectOne(
                            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<StockInventory>()
                                    .eq(StockInventory::getProductModelId, sr.getProductModelId())
                                    .eq(StockInventory::getBatchNo, sr.getBatchNo()));
                    if (si != null) {
                        si.setBatchNo(fixBatchNoDate(si.getBatchNo(), stockDateStr));
                        stockInventoryMapper.updateById(si);
                    }
                }
                log.info("销售台账[{}]入库审批完成, 入库记录数: {}", salesLedger.getSalesContractNo(), recordIds.size());
            }
        } catch (Exception e) {
            log.warn("销售质检入库失败[{}]: {}", salesLedger.getSalesContractNo(), e.getMessage());
        }
    }
    /**
     * 发货 + 发货审批通过 + 出库审批
     */
    private void processSalesShipping(SalesLedger salesLedger, LocalDate shipDate) {
        try {
            LocalDateTime shipDateTime = shipDate.atTime(9, 0, 0)
                    .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
            String shipDateStr = shipDate.format(DateTimeFormatter.BASIC_ISO_DATE);
            // 查找有库存的产品规格
            List<SalesLedgerProduct> products = salesLedgerProductMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SalesLedgerProduct>()
                            .eq(SalesLedgerProduct::getSalesLedgerId, salesLedger.getId()));
            List<ShippingProductDetail> details = new ArrayList<>();
            for (SalesLedgerProduct slp : products) {
                StockInventory si = stockInventoryMapper.selectOne(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<StockInventory>()
                                .eq(StockInventory::getProductModelId, slp.getProductModelId())
                                .gt(StockInventory::getQualitity, BigDecimal.ZERO)
                                .orderByDesc(StockInventory::getId)
                                .last("limit 1"));
                if (si != null) {
                    ShippingProductDetail detail = new ShippingProductDetail();
                    detail.setStockInventoryId(si.getId());
                    detail.setProductModelId(slp.getProductModelId());
                    detail.setBatchNo(si.getBatchNo());
                    detail.setQuantity(slp.getQuantity());
                    details.add(detail);
                }
            }
            if (details.isEmpty()) {
                log.info("销售台账[{}]无可用库存,跳过发货", salesLedger.getSalesContractNo());
                return;
            }
            // 创建发货单
            ShippingInfoDto shipDto = new ShippingInfoDto();
            shipDto.setSalesLedgerId(salesLedger.getId());
            shipDto.setSalesLedgerProductId(products.get(0).getId());
            shipDto.setCreateTime(shipDateTime);
            shipDto.setBatchNoDetailList(details);
            shippingInfoService.addReq(shipDto);
            // 找到发货单和审批实例
            ShippingInfo shippingInfo = shippingInfoMapper.selectOne(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ShippingInfo>()
                            .eq(ShippingInfo::getShippingNo, shipDto.getShippingNo())
                            .last("limit 1"));
            if (shippingInfo != null) {
                shippingInfo.setCreateTime(shipDateTime);
                shippingInfo.setShippingDate(java.sql.Date.valueOf(shipDate));
                // 根据客户地址生成发货车牌号
                shippingInfo.setShippingCarNumber(generateCarNumber(salesLedger.getCustomerName()));
                shippingInfoMapper.updateById(shippingInfo);
                // 发货审批自动通过
                ApprovalInstance shipApproval = approvalInstanceMapper.selectOne(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ApprovalInstance>()
                                .eq(ApprovalInstance::getBusinessId, shippingInfo.getId())
                                .eq(ApprovalInstance::getBusinessType, 7L)
                                .eq(ApprovalInstance::getDeleted, 0)
                                .orderByDesc(ApprovalInstance::getId)
                                .last("limit 1"));
                if (shipApproval != null) {
                    LocalDate shipApproveDate = shipDate.plusDays(ThreadLocalRandom.current().nextInt(0, 3));
                    LocalDateTime shipApproveDateTime = shipApproveDate.atTime(9, 0, 0)
                            .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
                    approvalInstanceService.autoApprove(shipApproval.getId());
                    fixApprovalTimes(shipApproval.getId(), shipApproveDateTime);
                    shippingInfo.setStatus("已发货");
                    shippingInfo.setShippingDate(java.sql.Date.valueOf(shipApproveDate));
                    shippingInfoMapper.updateById(shippingInfo);
                }
                // 出库审批
                LocalDate outDate = shipDate.plusDays(ThreadLocalRandom.current().nextInt(0, 3));
                LocalDateTime outDateTime = outDate.atTime(9, 0, 0)
                        .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
                String outDateStr = outDate.format(DateTimeFormatter.BASIC_ISO_DATE);
                List<StockOutRecord> outRecords = stockOutRecordMapper.selectList(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<StockOutRecord>()
                                .eq(StockOutRecord::getRecordId, shippingInfo.getId())
                                .eq(StockOutRecord::getRecordType,
                                        String.valueOf(StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode())));
                List<Long> outIds = new ArrayList<>();
                for (StockOutRecord sr : outRecords) {
                    if (sr.getApprovalStatus() == null || sr.getApprovalStatus() == 0 || sr.getApprovalStatus() == 3) {
                        sr.setCreateTime(outDateTime);
                        sr.setOutboundBatches(fixBatchNoDate(sr.getOutboundBatches(), outDateStr));
                        sr.setBatchNo(fixBatchNoDate(sr.getBatchNo(), outDateStr));
                        stockOutRecordMapper.updateById(sr);
                        outIds.add(sr.getId());
                    }
                }
                if (!outIds.isEmpty()) {
                    // 出库审批通过(走正式审批流程,内部会扣减库存)
                    stockOutRecordService.batchApprove(outIds, ReviewStatusEnum.APPROVED.getCode());
                    log.info("销售台账[{}]出库审批完成, 出库记录数: {}",
                            salesLedger.getSalesContractNo(), outIds.size());
                }
            }
            log.info("销售台账[{}]发货流程完成", salesLedger.getSalesContractNo());
        } catch (Exception e) {
            log.warn("销售发货流程失败[{}]: {}", salesLedger.getSalesContractNo(), e.getMessage());
        }
    }
    private ModuleSummary createPurchaseLedgers(List<JSONObject> items, Map<String, Long> supplierNameToId,
                                                 String additionalInfo, String dateEnd) {
                                                 String additionalInfo, String dateStart, String dateEnd) {
        // 是否需要质检,从补充信息判断,默认不需要
        boolean needQualityInspect = additionalInfo != null
                && (additionalInfo.contains("质检") || additionalInfo.contains("需要检验"));
@@ -396,12 +1246,19 @@
                String entryDateStr = item.getString("entryDate");
                LocalDate entryDate = null;
                if (item.containsKey("entryDate")) {
                if (item.containsKey("entryDate") && entryDateStr != null && entryDateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                    entryDate = LocalDate.parse(entryDateStr);
                    dto.setEntryDate(java.sql.Date.valueOf(entryDateStr));
                } else {
                    dto.setEntryDate(java.sql.Date.valueOf(dateStart));
                }
                if (item.containsKey("executionDate")) {
                    dto.setExecutionDate(java.sql.Date.valueOf(item.getString("executionDate")));
                    String execDateStr = item.getString("executionDate");
                    if (execDateStr != null && execDateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        dto.setExecutionDate(java.sql.Date.valueOf(execDateStr));
                    } else {
                        dto.setExecutionDate(java.sql.Date.valueOf(dateStart));
                    }
                }
                JSONArray productData = item.getJSONArray("productData");
                if (productData != null) {
@@ -445,7 +1302,7 @@
                }
                success++;
            } catch (Exception e) {
                log.warn("创建采购台账失败: {}", e.getMessage());
                log.warn("创建采购台账失败: {}", e.getMessage(), e);
                fail++;
            }
        }
@@ -454,10 +1311,18 @@
    /**
     * 采购完整流程: 审核通过 → 质检(可选) → 入库审核通过
     * 各环节时间以采购录入日期为基准,偶尔推迟1-3天
     */
    private void processPurchaseFullFlow(PurchaseLedger purchaseLedger, boolean needQualityInspect,
                                          LocalDate entryDate, String dateEnd) {
        try {
            // 基准日期:采购录入日期,为空则用当天
            LocalDate baseDate = entryDate != null ? entryDate : LocalDate.now();
            // 审批时间:基于录入日期,随机推0-3天
            LocalDate approveDate = baseDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
            LocalDateTime approveDateTime = approveDate.atTime(9, 0, 0)
                    .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
            // 1. 审批自动通过
            ApprovalInstance approvalInstance = approvalInstanceMapper.selectOne(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ApprovalInstance>()
@@ -467,21 +1332,25 @@
                            .orderByDesc(ApprovalInstance::getId)
                            .last("limit 1"));
            if (approvalInstance != null) {
                // 结算审批开始时间:基于录入日期,随机推0-3天
                LocalDate baseDate = entryDate != null ? entryDate : LocalDate.now();
                LocalDate approveDate = baseDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
                // 使用autoApprove完成审批
                approvalInstanceService.autoApprove(approvalInstance.getId());
                // 修正审批相关时间为采购录入日期范围
                fixApprovalTimes(approvalInstance.getId(), approveDateTime);
                log.info("采购台账[{}]审批通过, 审批日期: {}", purchaseLedger.getPurchaseContractNumber(), approveDate);
            }
            // 2. 质检流程(如果需要质检)
            if (needQualityInspect) {
                processQualityInspect(purchaseLedger, entryDate, dateEnd);
                // 质检时间:审批之后再推0-3天
                LocalDate inspectDate = approveDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
                processQualityInspect(purchaseLedger, inspectDate);
            }
            // 3. 入库审批通过
            processStockInApprove(purchaseLedger, dateEnd);
            // 入库时间:质检时间(有质检)或审批时间之后再推0-3天
            LocalDate stockBaseDate = needQualityInspect
                    ? approveDate.plusDays(ThreadLocalRandom.current().nextInt(1, 4))
                    : approveDate.plusDays(ThreadLocalRandom.current().nextInt(0, 4));
            processStockInApprove(purchaseLedger, stockBaseDate);
        } catch (Exception e) {
            log.warn("采购完整流程处理失败[{}]: {}", purchaseLedger.getPurchaseContractNumber(), e.getMessage());
@@ -489,17 +1358,68 @@
    }
    /**
     * 修正审批实例及关联节点、任务、记录的时间
     */
    private void fixApprovalTimes(Long instanceId, LocalDateTime dateTime) {
        try {
            // 更新审批实例的finishTime和createTime
            ApprovalInstance instance = approvalInstanceMapper.selectById(instanceId);
            if (instance != null) {
                instance.setFinishTime(dateTime);
                instance.setCreateTime(dateTime.minusHours(1));
                approvalInstanceMapper.updateById(instance);
            }
            // 更新审批实例节点的finishTime
            List<ApprovalInstanceNode> nodes = approvalInstanceNodeMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ApprovalInstanceNode>()
                            .eq(ApprovalInstanceNode::getInstanceId, instanceId));
            for (ApprovalInstanceNode node : nodes) {
                node.setFinishTime(dateTime);
                node.setCreateTime(dateTime.minusHours(1));
                approvalInstanceNodeMapper.updateById(node);
            }
            // 更新审批任务的createTime
            List<ApprovalTask> tasks = approvalTaskMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ApprovalTask>()
                            .eq(ApprovalTask::getInstanceId, instanceId));
            for (ApprovalTask task : tasks) {
                task.setCreateTime(dateTime.minusMinutes(30));
                approvalTaskMapper.updateById(task);
            }
            // 更新审批记录的createTime
            List<ApprovalRecord> records = approvalRecordMapper.selectList(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ApprovalRecord>()
                            .eq(ApprovalRecord::getInstanceId, instanceId));
            for (ApprovalRecord record : records) {
                record.setCreateTime(dateTime.minusMinutes(10));
                approvalRecordMapper.updateById(record);
            }
        } catch (Exception e) {
            log.warn("修正审批时间失败: {}", e.getMessage());
        }
    }
    /**
     * 质检: 找到采购关联的质检单,自动提交为合格
     */
    private void processQualityInspect(PurchaseLedger purchaseLedger, LocalDate entryDate, String dateEnd) {
    private void processQualityInspect(PurchaseLedger purchaseLedger, LocalDate inspectDate) {
        try {
            LocalDateTime inspectDateTime = inspectDate.atTime(9, 0, 0)
                    .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
            List<QualityInspect> inspectList = qualityInspectService.list(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<QualityInspect>()
                            .eq(QualityInspect::getPurchaseLedgerId, purchaseLedger.getId()));
            for (QualityInspect qi : inspectList) {
                if (qi.getInspectState() == null || qi.getInspectState() == 0) {
                    qualityInspectService.autoSubmit(qi.getId());
                    log.info("采购台账[{}]质检单[{}]自动提交合格", purchaseLedger.getPurchaseContractNumber(), qi.getId());
                    // 修正质检时间
                    qi.setCreateTime(inspectDateTime);
                    qualityInspectMapper.updateById(qi);
                    log.info("采购台账[{}]质检单[{}]自动提交合格, 质检日期: {}",
                            purchaseLedger.getPurchaseContractNumber(), qi.getId(), inspectDate);
                }
            }
        } catch (Exception e) {
@@ -508,16 +1428,19 @@
    }
    /**
     * 入库审核: 找到入库记录并审批通过
     * 入库审核: 找到入库记录并审批通过,并修正时间与批号日期
     */
    private void processStockInApprove(PurchaseLedger purchaseLedger, String dateEnd) {
    private void processStockInApprove(PurchaseLedger purchaseLedger, LocalDate stockDate) {
        try {
            LocalDateTime stockDateTime = stockDate.atTime(9, 0, 0)
                    .plusMinutes(ThreadLocalRandom.current().nextInt(0, 480));
            String stockDateStr = stockDate.format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE);
            List<com.ruoyi.stock.pojo.StockInRecord> stockRecords = stockInRecordService.list(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.stock.pojo.StockInRecord>()
                            .eq(com.ruoyi.stock.pojo.StockInRecord::getRecordId, purchaseLedger.getId())
                            .eq(com.ruoyi.stock.pojo.StockInRecord::getRecordType,
                                    String.valueOf(com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode())));
            // 如果按PURCHASE_STOCK_IN找不到,尝试CUSTOMIZATION_UNSTOCK_OUT(质检合格入库)
            if (stockRecords.isEmpty()) {
                stockRecords = stockInRecordService.list(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.stock.pojo.StockInRecord>()
@@ -525,7 +1448,6 @@
                                .eq(com.ruoyi.stock.pojo.StockInRecord::getRecordType,
                                        String.valueOf(com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_OUT.getCode())));
            }
            // 也尝试按质检单ID查找(质检单的recordId是质检单ID)
            for (QualityInspect qi : qualityInspectService.list(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<QualityInspect>()
                            .eq(QualityInspect::getPurchaseLedgerId, purchaseLedger.getId()))) {
@@ -534,7 +1456,6 @@
                                .eq(com.ruoyi.stock.pojo.StockInRecord::getRecordId, qi.getId()));
                stockRecords.addAll(qiRecords);
            }
            // 去重
            stockRecords = stockRecords.stream()
                    .collect(Collectors.toMap(com.ruoyi.stock.pojo.StockInRecord::getId, r -> r, (a, b) -> a))
                    .values().stream().collect(Collectors.toList());
@@ -545,8 +1466,20 @@
                        .map(com.ruoyi.stock.pojo.StockInRecord::getId)
                        .collect(Collectors.toList());
                if (!recordIds.isEmpty()) {
                    // 先修正入库记录的createTime和批号日期(batchApprove内部会用batchNo创建StockInventory)
                    for (Long recordId : recordIds) {
                        com.ruoyi.stock.pojo.StockInRecord sr = stockInRecordMapper.selectById(recordId);
                        if (sr != null) {
                            sr.setCreateTime(stockDateTime);
                            sr.setBatchNo(fixBatchNoDate(sr.getBatchNo(), stockDateStr));
                            String s = OrderUtils.countTodayByCreateTime(stockInRecordMapper, "RK", "inbound_batches", stockDateTime);
                            sr.setInboundBatches(s);
                            stockInRecordMapper.updateById(sr);
                        }
                    }
                    stockInRecordService.batchApprove(recordIds, ReviewStatusEnum.APPROVED.getCode());
                    log.info("采购台账[{}]入库审批通过, 入库记录数: {}", purchaseLedger.getPurchaseContractNumber(), recordIds.size());
                    log.info("采购台账[{}]入库审批通过, 入库日期: {}, 入库记录数: {}",
                            purchaseLedger.getPurchaseContractNumber(), stockDate, recordIds.size());
                }
            }
        } catch (Exception e) {
@@ -554,8 +1487,137 @@
        }
    }
    /**
     * 替换批号中的日期部分(yyyyMMdd → 指定日期)
     */
    /**
     * 根据客户名称查地址,推断省份简称生成车牌号
     * 格式: 省份简称 + 字母 + 5位随机数字字母, 如 苏A12345
     */
    private String generateCarNumber(String customerName) {
        // 省份简称到城市字母的常见映射
        Map<String, String[]> provinceCityMap = new HashMap<>();
        provinceCityMap.put("京", new String[]{"A", "B", "C", "E", "F", "G"});
        provinceCityMap.put("沪", new String[]{"A", "B", "D", "E", "F"});
        provinceCityMap.put("粤", new String[]{"A", "B", "E", "F", "G", "H", "J", "K"});
        provinceCityMap.put("苏", new String[]{"A", "B", "E", "F", "G", "H", "J", "K", "L", "M"});
        provinceCityMap.put("浙", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L"});
        provinceCityMap.put("鲁", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "Y"});
        provinceCityMap.put("豫", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "U"});
        provinceCityMap.put("川", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"});
        provinceCityMap.put("渝", new String[]{"A", "B", "C", "D"});
        provinceCityMap.put("鄂", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S"});
        provinceCityMap.put("湘", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "S"});
        provinceCityMap.put("闽", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K"});
        provinceCityMap.put("赣", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M"});
        provinceCityMap.put("皖", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "R", "S"});
        provinceCityMap.put("冀", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "R", "T"});
        provinceCityMap.put("辽", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P"});
        provinceCityMap.put("吉", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K"});
        provinceCityMap.put("黑", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "R"});
        provinceCityMap.put("晋", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M"});
        provinceCityMap.put("陕", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "V"});
        provinceCityMap.put("桂", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "R"});
        provinceCityMap.put("云", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S"});
        provinceCityMap.put("贵", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J"});
        provinceCityMap.put("甘", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P"});
        provinceCityMap.put("琼", new String[]{"A", "B", "C", "D", "E", "F"});
        provinceCityMap.put("宁", new String[]{"A", "B", "C", "D", "E"});
        provinceCityMap.put("青", new String[]{"A", "B", "C", "D", "E", "F", "G", "H"});
        provinceCityMap.put("藏", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J"});
        provinceCityMap.put("蒙", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M"});
        provinceCityMap.put("新", new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R"});
        provinceCityMap.put("津", new String[]{"A", "B", "C", "E", "F", "G", "H", "J", "K", "L", "M", "N", "Q", "R"});
        // 地址关键词到省份简称的映射
        Map<String, String> addressToProvince = new HashMap<>();
        addressToProvince.put("北京", "京"); addressToProvince.put("上海", "沪");
        addressToProvince.put("广州", "粤"); addressToProvince.put("深圳", "粤");
        addressToProvince.put("东莞", "粤"); addressToProvince.put("佛山", "粤");
        addressToProvince.put("南京", "苏"); addressToProvince.put("苏州", "苏");
        addressToProvince.put("无锡", "苏"); addressToProvince.put("南通", "苏");
        addressToProvince.put("常州", "苏"); addressToProvince.put("徐州", "苏");
        addressToProvince.put("杭州", "浙"); addressToProvince.put("宁波", "浙");
        addressToProvince.put("温州", "浙"); addressToProvince.put("嘉兴", "浙");
        addressToProvince.put("济南", "鲁"); addressToProvince.put("青岛", "鲁");
        addressToProvince.put("郑州", "豫"); addressToProvince.put("成都", "川");
        addressToProvince.put("重庆", "渝"); addressToProvince.put("武汉", "鄂");
        addressToProvince.put("长沙", "湘"); addressToProvince.put("福州", "闽");
        addressToProvince.put("厦门", "闽"); addressToProvince.put("南昌", "赣");
        addressToProvince.put("合肥", "皖"); addressToProvince.put("石家庄", "冀");
        addressToProvince.put("沈阳", "辽"); addressToProvince.put("大连", "辽");
        addressToProvince.put("长春", "吉"); addressToProvince.put("哈尔滨", "黑");
        addressToProvince.put("太原", "晋"); addressToProvince.put("西安", "陕");
        addressToProvince.put("南宁", "桂"); addressToProvince.put("昆明", "云");
        addressToProvince.put("贵阳", "贵"); addressToProvince.put("兰州", "甘");
        addressToProvince.put("海口", "琼"); addressToProvince.put("银川", "宁");
        addressToProvince.put("西宁", "青"); addressToProvince.put("拉萨", "藏");
        addressToProvince.put("呼和浩特", "蒙"); addressToProvince.put("乌鲁木齐", "新");
        addressToProvince.put("天津", "津");
        // 省名映射
        addressToProvince.put("江苏", "苏"); addressToProvince.put("浙江", "浙");
        addressToProvince.put("山东", "鲁"); addressToProvince.put("河南", "豫");
        addressToProvince.put("四川", "川"); addressToProvince.put("湖北", "鄂");
        addressToProvince.put("湖南", "湘"); addressToProvince.put("福建", "闽");
        addressToProvince.put("江西", "赣"); addressToProvince.put("安徽", "皖");
        addressToProvince.put("河北", "冀"); addressToProvince.put("辽宁", "辽");
        addressToProvince.put("吉林", "吉"); addressToProvince.put("黑龙江", "黑");
        addressToProvince.put("山西", "晋"); addressToProvince.put("陕西", "陕");
        addressToProvince.put("广东", "粤"); addressToProvince.put("广西", "桂");
        addressToProvince.put("云南", "云"); addressToProvince.put("贵州", "贵");
        addressToProvince.put("甘肃", "甘"); addressToProvince.put("海南", "琼");
        addressToProvince.put("宁夏", "宁"); addressToProvince.put("青海", "青");
        addressToProvince.put("西藏", "藏"); addressToProvince.put("内蒙古", "蒙");
        addressToProvince.put("新疆", "新");
        String province = "苏"; // 默认江苏
        // 尝试从客户地址推断省份
        if (customerName != null) {
            try {
                Customer customer = customerService.getOne(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Customer>()
                                .eq(Customer::getCustomerName, customerName)
                                .last("limit 1"));
                if (customer != null && customer.getCompanyAddress() != null) {
                    String addr = customer.getCompanyAddress();
                    for (Map.Entry<String, String> e : addressToProvince.entrySet()) {
                        if (addr.contains(e.getKey())) {
                            province = e.getValue();
                            break;
                        }
                    }
                }
            } catch (Exception e) {
                log.debug("查客户地址失败: {}", e.getMessage());
            }
        }
        // 生成车牌号: 省份简称 + 城市字母 + 5位随机字符(数字+大写字母)
        String[] cities = provinceCityMap.getOrDefault(province, new String[]{"A", "B", "C"});
        String city = cities[ThreadLocalRandom.current().nextInt(cities.length)];
        String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ0123456789";
        StringBuilder suffix = new StringBuilder();
        for (int i = 0; i < 5; i++) {
            suffix.append(chars.charAt(ThreadLocalRandom.current().nextInt(chars.length())));
        }
        return province + city + suffix;
    }
    private String fixBatchNoDate(String batchNo, String newDate) {
        if (batchNo == null || batchNo.length() < 8) {
            return batchNo;
        }
        // 批号格式: yyyyMMdd-产品编码-NNN
        String prefix = batchNo.substring(0, 8);
        if (prefix.matches("\\d{8}")) {
            return newDate + batchNo.substring(8);
        }
        return batchNo;
    }
    private ModuleSummary createProductionPlans(List<JSONObject> items) {
        int success = 0, fail = 0;
        List<ProductionPlan> createdPlans = new ArrayList<>();
        for (JSONObject item : items) {
            try {
                ProductionPlan plan = new ProductionPlan();
@@ -564,18 +1626,48 @@
                plan.setSource(item.getString("source"));
                plan.setRemark(item.getString("remark"));
                if (item.containsKey("requiredDate")) {
                    plan.setRequiredDate(LocalDate.parse(item.getString("requiredDate")));
                    String dateStr = item.getString("requiredDate");
                    if (dateStr != null && dateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        plan.setRequiredDate(LocalDate.parse(dateStr));
                    }
                }
                if (item.containsKey("promisedDeliveryDate")) {
                    plan.setPromisedDeliveryDate(LocalDate.parse(item.getString("promisedDeliveryDate")));
                    String dateStr = item.getString("promisedDeliveryDate");
                    if (dateStr != null && dateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        plan.setPromisedDeliveryDate(LocalDate.parse(dateStr));
                    }
                }
                productionPlanService.save(plan);
                createdPlans.add(plan);
                success++;
            } catch (Exception e) {
                log.warn("创建生产计划失败: {}", e.getMessage());
                fail++;
            }
        }
        // 按productModelId分组,自动合并下发
        Map<Long, List<ProductionPlan>> grouped = createdPlans.stream()
                .filter(p -> p.getProductModelId() != null)
                .collect(Collectors.groupingBy(ProductionPlan::getProductModelId));
        for (Map.Entry<Long, List<ProductionPlan>> entry : grouped.entrySet()) {
            try {
                List<Long> planIds = entry.getValue().stream().map(ProductionPlan::getId).collect(Collectors.toList());
                BigDecimal totalQty = entry.getValue().stream()
                        .map(ProductionPlan::getQtyRequired)
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                ProductionPlanDto combineDto = new ProductionPlanDto();
                combineDto.setIds(planIds);
                combineDto.setTotalAssignedQuantity(totalQty);
                combineDto.setPlanCompleteTime(LocalDate.now().plusDays(ThreadLocalRandom.current().nextInt(3, 10)));
                productionPlanService.combine(combineDto);
                log.info("生产计划自动下发成功, productModelId={}, 计划数: {}, 下发数量: {}",
                        entry.getKey(), planIds.size(), totalQty);
            } catch (Exception e) {
                log.warn("生产计划自动下发失败, productModelId={}: {}", entry.getKey(), e.getMessage());
            }
        }
        return summary("production", "生产计划", items.size(), success, fail);
    }
@@ -589,7 +1681,10 @@
                order.setProductModelId(item.getLong("productModelId"));
                order.setQuantity(item.getBigDecimal("quantity"));
                if (item.containsKey("planCompleteTime")) {
                    order.setPlanCompleteTime(LocalDate.parse(item.getString("planCompleteTime")));
                    String dateStr = item.getString("planCompleteTime");
                    if (dateStr != null && dateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
                        order.setPlanCompleteTime(LocalDate.parse(dateStr));
                    }
                }
                productionOrderService.saveProductionOrder(order);
                success++;