huminmin
8 天以前 7b8b2456bb15aa733b8599fce2ada5d9549ba881
销售台账,工艺路线配置设置是否完成;导出工艺路线
已添加5个文件
已修改4个文件
495 ■■■■■ 文件已修改
doc/sales_ledger_process_route_record.sql 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/SalesLedgerProcessRouteDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/SalesLedgerProcessRouteRecordMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProcessRouteRecord.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/ISalesLedgerProcessRouteRecordService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/ISalesLedgerService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProcessRouteRecordServiceImpl.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 302 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/sales_ledger_process_route_record.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
-- é”€å”®å°è´¦å·¥è‰ºè·¯çº¿å®Œæˆè®°å½•表
CREATE TABLE IF NOT EXISTS `sales_ledger_process_route_record` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `sales_ledger_id` bigint NOT NULL COMMENT '销售台账ID',
  `sales_ledger_process_route_id` bigint NOT NULL COMMENT '销售台账工艺路线ID',
  `is_completed` int DEFAULT 0 COMMENT '是否完成(0-否,1-是)',
  `completed_time` datetime DEFAULT NULL COMMENT '完成时间',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `tenant_id` bigint DEFAULT NULL COMMENT '租户ID',
  PRIMARY KEY (`id`),
  KEY `idx_slprr_sales_ledger_id` (`sales_ledger_id`),
  KEY `idx_slprr_sales_ledger_process_route_id` (`sales_ledger_process_route_id`),
  KEY `idx_slprr_completed_time` (`completed_time`),
  CONSTRAINT `fk_slprr_sales_ledger_process_route_id`
    FOREIGN KEY (`sales_ledger_process_route_id`) REFERENCES `sales_ledger_process_route` (`id`)
    ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='销售台账工艺路线完成记录';
src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java
@@ -23,9 +23,11 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -171,6 +173,20 @@
    }
    /**
     * å¯¼å‡ºå”®åŽå°è´¦å·¥è‰ºè·¯çº¿
     */
    @Log(title = "销售台账工艺路线", businessType = BusinessType.EXPORT)
    @PostMapping("/exportProcessRoute")
    @ApiOperation("导出售后台账工艺路线")
    public void exportProcessRoute(HttpServletResponse response,
                                   HttpServletRequest request,
                                   @RequestParam(value = "completedTimeStart", required = false) String completedTimeStart,
                                   @RequestParam(value = "completedTimeEnd", required = false) String completedTimeEnd) {
        List<Long> salesLedgerIds = parseSalesLedgerIds(request);
        salesLedgerService.exportProcessRoute(response, salesLedgerIds, completedTimeStart, completedTimeEnd);
    }
    /**
     * å¯¼å‡ºå¼€ç¥¨ç™»è®°åˆ—表
     */
    @Log(title = "导出开票登记列表", businessType = BusinessType.EXPORT)
@@ -198,8 +214,8 @@
     */
    @PostMapping("/saleProcessBind")
    @ApiOperation("销售订单绑定工艺路线")
    public AjaxResult saleProcessBind(@RequestBody SalesLedgerProcessRoute salesLedgerProcessRoute) {
        salesLedgerService.saleProcessBind(salesLedgerProcessRoute);
    public AjaxResult saleProcessBind(@RequestBody SalesLedgerProcessRouteDto salesLedgerProcessRouteDto) {
        salesLedgerService.saleProcessBind(salesLedgerProcessRouteDto);
        return AjaxResult.success();
    }
@@ -443,4 +459,53 @@
        excelUtil.importTemplateExcel(response, "未出库导入模板下载");
    }
    private List<Long> parseSalesLedgerIds(HttpServletRequest request) {
        if (request == null) {
            return Collections.emptyList();
        }
        List<Long> ids = new ArrayList<>();
        Map<String, String[]> parameterMap = request.getParameterMap();
        if (parameterMap == null || parameterMap.isEmpty()) {
            return ids;
        }
        String directValue = request.getParameter("salesLedgerIds");
        if (StringUtils.hasText(directValue)) {
            for (String value : directValue.split(",")) {
                addParsedLong(ids, value);
            }
        }
        for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
            String key = entry.getKey();
            if (!StringUtils.hasText(key)) {
                continue;
            }
            if (!"salesLedgerIds".equals(key) && !key.startsWith("salesLedgerIds[")) {
                continue;
            }
            String[] values = entry.getValue();
            if (values == null) {
                continue;
            }
            for (String value : values) {
                addParsedLong(ids, value);
            }
        }
        return ids.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
    }
    private void addParsedLong(List<Long> target, String value) {
        if (target == null || !StringUtils.hasText(value)) {
            return;
        }
        try {
            target.add(Long.valueOf(value.trim()));
        } catch (Exception ignored) {
            // ignore invalid id values
        }
    }
}
src/main/java/com/ruoyi/sales/dto/SalesLedgerProcessRouteDto.java
@@ -1,6 +1,7 @@
package com.ruoyi.sales.dto;
import com.ruoyi.sales.pojo.SalesLedgerProcessRoute;
import com.ruoyi.sales.pojo.SalesLedgerProcessRouteRecord;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -29,4 +30,7 @@
    @ApiModelProperty("销售订单绑定的工艺路线")
    List<SalesLedgerProcessRoute> list;
    @ApiModelProperty("销售订单工序完成记录")
    List<SalesLedgerProcessRouteRecord> recordList;
}
src/main/java/com/ruoyi/sales/mapper/SalesLedgerProcessRouteRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
package com.ruoyi.sales.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.sales.pojo.SalesLedgerProcessRouteRecord;
/**
 * é”€å”®å°è´¦å·¥è‰ºè·¯çº¿å®Œæˆè®°å½• Mapper
 */
public interface SalesLedgerProcessRouteRecordMapper extends BaseMapper<SalesLedgerProcessRouteRecord> {
}
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProcessRouteRecord.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
package com.ruoyi.sales.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * é”€å”®å°è´¦å·¥è‰ºè·¯çº¿å®Œæˆè®°å½•
 */
@Data
@TableName("sales_ledger_process_route_record")
@ApiModel(value = "SalesLedgerProcessRouteRecord对象", description = "销售台账工艺路线完成记录")
public class SalesLedgerProcessRouteRecord implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    @ApiModelProperty(value = "主键")
    private Long id;
    @ApiModelProperty(value = "销售台账ID")
    private Long salesLedgerId;
    @ApiModelProperty(value = "销售台账工艺路线ID")
    private Long salesLedgerProcessRouteId;
    @TableField(exist = false)
    @ApiModelProperty(value = "工艺路线节点ID")
    private Long processRouteItemId;
    @ApiModelProperty(value = "是否完成(0-否,1-是)")
    private Integer isCompleted;
    @ApiModelProperty(value = "完成时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime completedTime;
    @TableField(fill = FieldFill.INSERT)
    @ApiModelProperty(value = "创建时间")
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT)
    @ApiModelProperty(value = "租户ID")
    private Long tenantId;
}
src/main/java/com/ruoyi/sales/service/ISalesLedgerProcessRouteRecordService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
package com.ruoyi.sales.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.sales.pojo.SalesLedgerProcessRouteRecord;
/**
 * é”€å”®å°è´¦å·¥è‰ºè·¯çº¿å®Œæˆè®°å½• Service
 */
public interface ISalesLedgerProcessRouteRecordService extends IService<SalesLedgerProcessRouteRecord> {
}
src/main/java/com/ruoyi/sales/service/ISalesLedgerService.java
@@ -57,7 +57,7 @@
    IPage<SalesLedgerDto> listSalesLedger(SalesLedgerDto salesLedgerDto, Page page);
    void saleProcessBind(SalesLedgerProcessRoute salesLedgerProcessRoute);
    void saleProcessBind(SalesLedgerProcessRouteDto salesLedgerProcessRouteDto);
    SalesProcessCardDto processCard(Long salesLedgerId);
@@ -126,4 +126,13 @@
     * @param salesLedgerDto æŸ¥è¯¢æ¡ä»¶
     */
    void exportWithProducts(HttpServletResponse response, SalesLedgerDto salesLedgerDto);
    /**
     * å¯¼å‡ºå”®åŽå°è´¦å·¥è‰ºè·¯çº¿
     * @param response HttpServletResponse
     * @param salesLedgerIds é”€å”®å°è´¦ID列表
     * @param completedTimeStart å®Œæˆæ—¶é—´å¼€å§‹
     * @param completedTimeEnd å®Œæˆæ—¶é—´ç»“束
     */
    void exportProcessRoute(HttpServletResponse response, List<Long> salesLedgerIds, String completedTimeStart, String completedTimeEnd);
}
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProcessRouteRecordServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package com.ruoyi.sales.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.sales.mapper.SalesLedgerProcessRouteRecordMapper;
import com.ruoyi.sales.pojo.SalesLedgerProcessRouteRecord;
import com.ruoyi.sales.service.ISalesLedgerProcessRouteRecordService;
import org.springframework.stereotype.Service;
/**
 * é”€å”®å°è´¦å·¥è‰ºè·¯çº¿å®Œæˆè®°å½• Service å®žçް
 */
@Service
public class SalesLedgerProcessRouteRecordServiceImpl extends ServiceImpl<SalesLedgerProcessRouteRecordMapper, SalesLedgerProcessRouteRecord>
    implements ISalesLedgerProcessRouteRecordService {
}
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -53,6 +53,7 @@
import com.ruoyi.sales.mapper.*;
import com.ruoyi.sales.pojo.*;
import com.ruoyi.sales.service.ISalesLedgerProcessRouteService;
import com.ruoyi.sales.service.ISalesLedgerProcessRouteRecordService;
import com.ruoyi.sales.service.ISalesLedgerProductProcessBindService;
import com.ruoyi.sales.service.ISalesLedgerProductProcessService;
import com.ruoyi.sales.service.ISalesLedgerService;
@@ -152,6 +153,7 @@
    private final ISalesLedgerProductProcessBindService salesLedgerProductProcessBindService;
    private final ISalesLedgerProcessRouteService salesLedgerProcessRouteService;
    private final ISalesLedgerProcessRouteRecordService salesLedgerProcessRouteRecordService;
    private final StockInventoryService stockInventoryService;
    private final StockInRecordMapper stockInRecordMapper;
@@ -754,21 +756,22 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saleProcessBind(SalesLedgerProcessRoute salesLedgerProcessRoute) {
        if (salesLedgerProcessRoute == null) {
    public void saleProcessBind(SalesLedgerProcessRouteDto salesLedgerProcessRouteDto) {
        if (salesLedgerProcessRouteDto == null) {
            throw new ServiceException("绑定失败,数据不能为空");
        }
        SalesLedger salesLedger = baseMapper.selectById(salesLedgerProcessRoute.getSalesLedgerId());
        SalesLedger salesLedger = baseMapper.selectById(salesLedgerProcessRouteDto.getSalesLedgerId());
        if (salesLedger == null) {
            throw new ServiceException("绑定失败,销售订单不存在");
        }
        ProcessRoute processRoute = processRouteMapper.selectById(salesLedgerProcessRoute.getProcessRouteId());
        ProcessRoute processRoute = processRouteMapper.selectById(salesLedgerProcessRouteDto.getProcessRouteId());
        if (processRoute == null) {
            throw new ServiceException("绑定失败,工艺路线不存在");
        }
        //  æ¸…除已绑定的数据
        salesLedgerProcessRouteService.remove(new LambdaQueryWrapper<SalesLedgerProcessRoute>().eq(SalesLedgerProcessRoute::getSalesLedgerId, salesLedger.getId()));
        salesLedgerProcessRouteRecordService.remove(new LambdaQueryWrapper<SalesLedgerProcessRouteRecord>().eq(SalesLedgerProcessRouteRecord::getSalesLedgerId, salesLedger.getId()));
        //  å°†æ•°æ®è¿ç§»åˆ°sales_ledger_process_route
        List<ProcessRouteItem> routeItems = processRouteItemMapper.selectList(new LambdaQueryWrapper<ProcessRouteItem>().eq(ProcessRouteItem::getRouteId, processRoute.getId()));
@@ -783,6 +786,39 @@
            salesLedgerProcessRouteList.add(ledgerProcessRoute);
        }
        salesLedgerProcessRouteService.saveBatch(salesLedgerProcessRouteList);
        List<SalesLedgerProcessRoute> savedRoutes = salesLedgerProcessRouteService.list(new LambdaQueryWrapper<SalesLedgerProcessRoute>()
            .eq(SalesLedgerProcessRoute::getSalesLedgerId, salesLedger.getId())
            .eq(SalesLedgerProcessRoute::getProcessRouteId, processRoute.getId()));
        Map<Long, SalesLedgerProcessRoute> routeMap = savedRoutes.stream()
            .filter(item -> item.getProcessRouteItemId() != null)
            .collect(Collectors.toMap(SalesLedgerProcessRoute::getProcessRouteItemId, item -> item, (a, b) -> a));
        Map<Long, SalesLedgerProcessRouteRecord> inputRecordMap = new HashMap<>();
        if (CollectionUtils.isNotEmpty(salesLedgerProcessRouteDto.getRecordList())) {
            for (SalesLedgerProcessRouteRecord record : salesLedgerProcessRouteDto.getRecordList()) {
                if (record != null && record.getProcessRouteItemId() != null) {
                    inputRecordMap.put(record.getProcessRouteItemId(), record);
                }
            }
        }
        List<SalesLedgerProcessRouteRecord> routeRecordList = new ArrayList<>();
        for (ProcessRouteItem routeItem : routeItems) {
            SalesLedgerProcessRoute route = routeMap.get(routeItem.getId());
            if (route == null || route.getId() == null) {
                continue;
            }
            SalesLedgerProcessRouteRecord inputRecord = inputRecordMap.get(routeItem.getId());
            SalesLedgerProcessRouteRecord record = new SalesLedgerProcessRouteRecord();
            record.setSalesLedgerId(salesLedger.getId());
            record.setSalesLedgerProcessRouteId(route.getId());
            Integer isCompleted = inputRecord != null && inputRecord.getIsCompleted() != null ? inputRecord.getIsCompleted() : 0;
            record.setIsCompleted(isCompleted);
            record.setCompletedTime(Objects.equals(isCompleted, 1) ? LocalDateTime.now() : null);
            routeRecordList.add(record);
        }
        salesLedgerProcessRouteRecordService.saveBatch(routeRecordList);
    }
    /**
@@ -1175,6 +1211,11 @@
    public SalesLedgerProcessRouteDto salesProcess(Long salesLedgerId) {
        SalesLedgerProcessRouteDto dto = new SalesLedgerProcessRouteDto();
        List<SalesLedgerProcessRoute> list = baseMapper.selectSalesProcess(salesLedgerId);
        List<SalesLedgerProcessRouteRecord> recordList = salesLedgerProcessRouteRecordService.list(
            new LambdaQueryWrapper<SalesLedgerProcessRouteRecord>()
                .eq(SalesLedgerProcessRouteRecord::getSalesLedgerId, salesLedgerId)
                .orderByAsc(SalesLedgerProcessRouteRecord::getId)
        );
        if (CollectionUtils.isNotEmpty(list)) {
            Long processRouteId = list.get(0).getProcessRouteId();
            ProcessRoute processRoute = processRouteMapper.selectById(processRouteId);
@@ -1201,6 +1242,17 @@
            }
        }
        dto.setList(list);
        if (CollectionUtils.isNotEmpty(list) && CollectionUtils.isNotEmpty(recordList)) {
            Map<Long, Long> routeItemIdMap = list.stream()
                .filter(item -> item.getId() != null && item.getProcessRouteItemId() != null)
                .collect(Collectors.toMap(SalesLedgerProcessRoute::getId, SalesLedgerProcessRoute::getProcessRouteItemId, (a, b) -> a));
            recordList.forEach(record -> {
                if (record != null && record.getSalesLedgerProcessRouteId() != null) {
                    record.setProcessRouteItemId(routeItemIdMap.get(record.getSalesLedgerProcessRouteId()));
                }
            });
        }
        dto.setRecordList(recordList);
        return dto;
    }
@@ -3740,7 +3792,8 @@
                    .or().isNull(SalesLedger::getReviewStatus));
            }
            List<SalesLedger> ledgerList = salesLedgerMapper.selectList(page, queryWrapper);
            IPage<SalesLedger> ledgerPage = salesLedgerMapper.selectPage(page, queryWrapper);
            List<SalesLedger> ledgerList = ledgerPage.getRecords();
            // 2. æ”¶é›†æ•°æ®
            List<SalesLedgerExportDto> ledgerExportList = new ArrayList<>();
@@ -3823,6 +3876,245 @@
        }
    }
    @Override
    public void exportProcessRoute(HttpServletResponse response, List<Long> salesLedgerIds, String completedTimeStart, String completedTimeEnd) {
        try {
            if (CollectionUtils.isEmpty(salesLedgerIds)) {
                throw new ServiceException("请选择要导出的销售台账");
            }
            LocalDateTime startTime = parseCompletedTime(completedTimeStart);
            LocalDateTime endTime = parseCompletedTime(completedTimeEnd);
            if (startTime == null && endTime == null) {
                startTime = LocalDate.now().atStartOfDay();
                endTime = LocalDateTime.now();
            } else {
                if (startTime == null) {
                    startTime = LocalDate.now().atStartOfDay();
                }
                if (endTime == null) {
                    endTime = LocalDateTime.now();
                }
            }
            LambdaQueryWrapper<SalesLedgerProcessRouteRecord> queryWrapper = Wrappers.<SalesLedgerProcessRouteRecord>lambdaQuery()
                .eq(SalesLedgerProcessRouteRecord::getIsCompleted, 1)
                .ge(SalesLedgerProcessRouteRecord::getCompletedTime, startTime)
                .le(SalesLedgerProcessRouteRecord::getCompletedTime, endTime)
                .orderByAsc(SalesLedgerProcessRouteRecord::getCompletedTime)
                .orderByAsc(SalesLedgerProcessRouteRecord::getId);
            if (CollectionUtils.isNotEmpty(salesLedgerIds)) {
                queryWrapper.in(SalesLedgerProcessRouteRecord::getSalesLedgerId, salesLedgerIds);
            }
            List<SalesLedgerProcessRouteRecord> completedRoutes = salesLedgerProcessRouteRecordService.list(queryWrapper);
            Map<Long, SalesLedger> salesLedgerMap = Collections.emptyMap();
            Map<Long, SalesLedgerProcessRoute> routeMap = Collections.emptyMap();
            Map<Long, ProcessRouteItem> processRouteItemMap = Collections.emptyMap();
            Map<Long, List<SalesLedgerProduct>> productMap = Collections.emptyMap();
            if (CollectionUtils.isNotEmpty(completedRoutes)) {
                List<Long> routeSalesLedgerIds = completedRoutes.stream()
                    .map(SalesLedgerProcessRouteRecord::getSalesLedgerId)
                    .filter(Objects::nonNull)
                    .distinct()
                    .collect(Collectors.toList());
                List<Long> salesLedgerProcessRouteIds = completedRoutes.stream()
                    .map(SalesLedgerProcessRouteRecord::getSalesLedgerProcessRouteId)
                    .filter(Objects::nonNull)
                    .distinct()
                    .collect(Collectors.toList());
                if (CollectionUtils.isNotEmpty(routeSalesLedgerIds)) {
                    salesLedgerMap = salesLedgerMapper.selectBatchIds(routeSalesLedgerIds).stream()
                        .filter(Objects::nonNull)
                        .collect(Collectors.toMap(SalesLedger::getId, item -> item, (a, b) -> a));
                    List<SalesLedgerProduct> products = salesLedgerProductMapper.selectList(new LambdaQueryWrapper<SalesLedgerProduct>()
                        .in(SalesLedgerProduct::getSalesLedgerId, routeSalesLedgerIds)
                        .eq(SalesLedgerProduct::getType, 1)
                        .orderByAsc(SalesLedgerProduct::getSalesLedgerId)
                        .orderByAsc(SalesLedgerProduct::getId));
                    productMap = products.stream().collect(Collectors.groupingBy(
                        SalesLedgerProduct::getSalesLedgerId,
                        LinkedHashMap::new,
                        Collectors.toList()
                    ));
                }
                if (CollectionUtils.isNotEmpty(salesLedgerProcessRouteIds)) {
                    routeMap = salesLedgerProcessRouteService.listByIds(salesLedgerProcessRouteIds).stream()
                        .filter(Objects::nonNull)
                        .collect(Collectors.toMap(SalesLedgerProcessRoute::getId, item -> item, (a, b) -> a));
                    List<Long> processRouteItemIds = routeMap.values().stream()
                        .map(SalesLedgerProcessRoute::getProcessRouteItemId)
                        .filter(Objects::nonNull)
                        .distinct()
                        .collect(Collectors.toList());
                    if (CollectionUtils.isNotEmpty(processRouteItemIds)) {
                        processRouteItemMap = processRouteItemMapper.selectBatchIds(processRouteItemIds).stream()
                            .filter(Objects::nonNull)
                            .collect(Collectors.toMap(ProcessRouteItem::getId, item -> item, (a, b) -> a));
                    }
                }
            }
            Map<Long, List<SalesLedgerProcessRouteRecord>> routeGroupMap = new LinkedHashMap<>();
            for (SalesLedgerProcessRouteRecord record : completedRoutes) {
                SalesLedgerProcessRoute route = routeMap.get(record.getSalesLedgerProcessRouteId());
                if (route == null || route.getProcessRouteItemId() == null) {
                    continue;
                }
                routeGroupMap.computeIfAbsent(route.getProcessRouteItemId(), k -> new ArrayList<>()).add(record);
            }
            final Map<Long, ProcessRouteItem> finalProcessRouteItemMap = processRouteItemMap;
            LinkedHashMap<String, List<List<Object>>> sheetMap = new LinkedHashMap<>();
            List<Long> orderedProcessRouteItemIds = routeGroupMap.keySet().stream()
                .sorted((left, right) -> compareProcessRouteItem(left, right, finalProcessRouteItemMap))
                .collect(Collectors.toList());
            for (Long processRouteItemId : orderedProcessRouteItemIds) {
                ProcessRouteItem processRouteItem = finalProcessRouteItemMap.get(processRouteItemId);
                String sheetName = buildUniqueSheetName(sheetMap, processRouteItem, processRouteItemId);
                List<List<Object>> sheetData = new ArrayList<>();
                sheetData.add(buildProcessRouteHeader());
                for (SalesLedgerProcessRouteRecord route : routeGroupMap.getOrDefault(processRouteItemId, Collections.emptyList())) {
                    SalesLedger salesLedger = salesLedgerMap.get(route.getSalesLedgerId());
                    if (salesLedger == null) {
                        continue;
                    }
                    List<SalesLedgerProduct> products = productMap.getOrDefault(salesLedger.getId(), Collections.emptyList());
                    if (CollectionUtils.isEmpty(products)) {
                        sheetData.add(buildProcessRouteRow(salesLedger, null, route));
                        continue;
                    }
                    for (SalesLedgerProduct product : products) {
                        sheetData.add(buildProcessRouteRow(salesLedger, product, route));
                    }
                }
                if (sheetData.size() == 1) {
                    sheetData.add(Arrays.asList("", "", "", "", "", "", "", "", ""));
                }
                sheetMap.put(sheetName, sheetData);
            }
            if (sheetMap.isEmpty()) {
                List<List<Object>> sheetData = new ArrayList<>();
                sheetData.add(buildProcessRouteHeader());
                sheetData.add(Arrays.asList("", "", "", "", "", "", ""));
                sheetMap.put("工艺路线", sheetData);
            }
            com.ruoyi.common.utils.excel.ExcelUtils.exportManySheet(response, "销售台账工艺路线导出", sheetMap);
        } catch (Exception e) {
            log.error("导出售后台账工艺路线失败", e);
            throw new ServiceException("导出售后台账工艺路线失败:" + e.getMessage());
        }
    }
    private LocalDateTime parseCompletedTime(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        String text = value.trim();
        try {
            return LocalDateTime.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        } catch (Exception ex) {
            try {
                return LocalDateTime.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
            } catch (Exception ignored) {
                return LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd")).atStartOfDay();
            }
        }
    }
    private int compareProcessRouteItem(Long left, Long right, Map<Long, ProcessRouteItem> processRouteItemMap) {
        ProcessRouteItem leftItem = processRouteItemMap.get(left);
        ProcessRouteItem rightItem = processRouteItemMap.get(right);
        int leftSort = leftItem != null && leftItem.getDragSort() != null ? leftItem.getDragSort() : Integer.MAX_VALUE;
        int rightSort = rightItem != null && rightItem.getDragSort() != null ? rightItem.getDragSort() : Integer.MAX_VALUE;
        if (leftSort != rightSort) {
            return Integer.compare(leftSort, rightSort);
        }
        String leftName = leftItem != null && StringUtils.hasText(leftItem.getProcessName()) ? leftItem.getProcessName() : "";
        String rightName = rightItem != null && StringUtils.hasText(rightItem.getProcessName()) ? rightItem.getProcessName() : "";
        int nameCompare = leftName.compareTo(rightName);
        if (nameCompare != 0) {
            return nameCompare;
        }
        return Long.compare(left, right);
    }
    private List<Object> buildProcessRouteHeader() {
        return Arrays.asList("日期", "订单编号", "客户名称", "规格", "数量", "面积", "是否是工程");
    }
    private List<Object> buildProcessRouteRow(SalesLedger salesLedger, SalesLedgerProduct product, SalesLedgerProcessRouteRecord route) {
        List<Object> row = new ArrayList<>();
        row.add(salesLedger.getEntryDate() == null ? "" : DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, salesLedger.getEntryDate()));
        row.add(salesLedger.getSalesContractNo());
        row.add(salesLedger.getCustomerName());
        row.add(product == null ? "" : product.getSpecificationModel());
        row.add(product == null ? "" : product.getQuantity());
        row.add(product == null ? "" : resolveExportArea(product));
        row.add("");
        return row;
    }
    private BigDecimal resolveExportArea(SalesLedgerProduct product) {
        if (product == null) {
            return BigDecimal.ZERO;
        }
        if (product.getSettleTotalArea() != null) {
            return product.getSettleTotalArea();
        }
        if (product.getActualTotalArea() != null) {
            return product.getActualTotalArea();
        }
        BigDecimal qty = product.getQuantity() == null ? BigDecimal.ONE : product.getQuantity();
        if (product.getSettlePieceArea() != null) {
            return product.getSettlePieceArea().multiply(qty).setScale(2, RoundingMode.HALF_UP);
        }
        if (product.getActualPieceArea() != null) {
            return product.getActualPieceArea().multiply(qty).setScale(2, RoundingMode.HALF_UP);
        }
        if (product.getWidth() != null && product.getHeight() != null) {
            BigDecimal area = product.getWidth().multiply(product.getHeight())
                .divide(new BigDecimal("1000000"), 2, RoundingMode.HALF_UP);
            return area.multiply(qty).setScale(2, RoundingMode.HALF_UP);
        }
        return BigDecimal.ZERO;
    }
    private String buildUniqueSheetName(Map<String, List<List<Object>>> sheetMap, ProcessRouteItem processRouteItem, Long processRouteItemId) {
        String baseName = processRouteItem != null && StringUtils.hasText(processRouteItem.getProcessName())
            ? processRouteItem.getProcessName()
            : "工序" + processRouteItemId;
        baseName = sanitizeSheetName(baseName);
        String sheetName = baseName;
        int suffix = 2;
        while (sheetMap.containsKey(sheetName)) {
            String suffixText = "_" + suffix++;
            int maxBaseLength = 31 - suffixText.length();
            String trimmedBase = baseName.length() > maxBaseLength ? baseName.substring(0, maxBaseLength) : baseName;
            sheetName = trimmedBase + suffixText;
        }
        return sheetName;
    }
    private String sanitizeSheetName(String sheetName) {
        if (!StringUtils.hasText(sheetName)) {
            return "工艺路线";
        }
        String sanitized = sheetName.replaceAll("[\\\\/?*\\[\\]:]", "_").trim();
        if (sanitized.isEmpty()) {
            sanitized = "工艺路线";
        }
        return sanitized.length() > 31 ? sanitized.substring(0, 31) : sanitized;
    }
    /**
     * æ‰‹åЍ填充Sheet数据
     */