gaoluyang
2026-10-30 cbadfd8e691ab335965202a984db16e55ac8e42c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
package cn.iocoder.yudao.module.mes.service.wm.sorting;
 
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.salesnotice.vo.MesWmSalesNoticePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sorting.vo.MesWmSortPendingNoticeRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sorting.vo.MesWmSortProductionRequestRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sorting.vo.MesWmSortTaskPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sorting.vo.MesWmSortTaskRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sorting.vo.MesWmSortTaskScanReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.salesnotice.MesWmSalesNoticeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.salesnotice.MesWmSalesNoticeLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.sorting.MesWmSortTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.sorting.MesWmSortTaskLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.sorting.MesWmSortTaskLineMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.sorting.MesWmSortTaskMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmSalesNoticeStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmSortTaskStatusEnum;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.pro.mps.MesProMpsService;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.salesnotice.MesWmSalesNoticeLineService;
import cn.iocoder.yudao.module.mes.service.wm.salesnotice.MesWmSalesNoticeService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
 
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
 
/**
 * MES 分拣任务 Service 实现类
 */
@Service
@Validated
@Slf4j
public class MesWmSortTaskServiceImpl implements MesWmSortTaskService {
 
    private static final DateTimeFormatter CODE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
 
    @Resource
    private MesWmSortTaskMapper sortTaskMapper;
    @Resource
    private MesWmSortTaskLineMapper sortTaskLineMapper;
 
    @Resource
    private MesWmSalesNoticeService salesNoticeService;
    @Resource
    private MesWmSalesNoticeLineService salesNoticeLineService;
    @Resource
    private MesWmMaterialStockService materialStockService;
    @Resource
    private MesWmProductSalesMapper productSalesMapper;
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MesWmWarehouseService warehouseService;
    @Resource
    private MesWmWarehouseAreaService areaService;
    @Resource
    private MesWmWarehouseLocationService locationService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MesProMpsService mpsService;
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createSortTaskFromNotice(Long noticeId) {
        // 1. 校验发货通知单:必须存在且为「待出库」
        MesWmSalesNoticeDO notice = salesNoticeService.validateSalesNoticeExists(noticeId);
        if (!MesWmSalesNoticeStatusEnum.APPROVED.getStatus().equals(notice.getStatus())) {
            throw new ServiceException(WM_SORT_TASK_NOT_PENDING.getCode(),
                    "发货通知单[" + notice.getCode() + "]当前状态为["
                            + MesWmSalesNoticeStatusEnum.valueOf(notice.getStatus()).getName()
                            + "],只有「待出库」的发货通知单才能生成分拣任务,请先提交该发货通知单。");
        }
        // 2. 防止重复生成
        MesWmSortTaskDO exists = sortTaskMapper.selectByNoticeId(noticeId);
        if (exists != null) {
            throw new ServiceException(WM_SORT_TASK_NOTICE_EXISTS.getCode(),
                    "发货通知单[" + notice.getCode() + "]已存在分拣任务[" + exists.getCode()
                            + "],请直接在该任务上执行分拣,或先取消原任务。");
        }
        // 3. 发货明细 + 分配货位
        List<MesWmSalesNoticeLineDO> noticeLines = salesNoticeLineService.getSalesNoticeLineListByNoticeId(noticeId);
        if (CollUtil.isEmpty(noticeLines)) {
            throw new ServiceException(WM_SORT_TASK_NO_STOCK.getCode(),
                    "发货通知单[" + notice.getCode() + "]没有发货明细,无法分拣,请先补全发货明细。");
        }
        List<MesWmSortTaskLineDO> lines = allocateStock(notice, noticeLines);
        // 4. 插入任务
        MesWmSortTaskDO task = MesWmSortTaskDO.builder()
                .code(generateCode())
                .noticeId(notice.getId()).noticeCode(notice.getCode())
                .saleOrderId(notice.getSaleOrderId()).saleOrderCode(notice.getSaleOrderCode())
                .clientId(notice.getClientId())
                .recipientName(notice.getRecipientName()).recipientTelephone(notice.getRecipientTelephone())
                .recipientAddress(notice.getRecipientAddress())
                .status(MesWmSortTaskStatusEnum.PENDING.getStatus())
                .printCount(0)
                .build();
        sortTaskMapper.insert(task);
        lines.forEach(line -> line.setTaskId(task.getId()));
        lines.forEach(sortTaskLineMapper::insert);
        return task.getId();
    }
 
    /**
     * 按发货明细分配合格库存:同物料优先按入库时间 FIFO,可跨库位拆分
     */
    private List<MesWmSortTaskLineDO> allocateStock(MesWmSalesNoticeDO notice,
                                                    List<MesWmSalesNoticeLineDO> noticeLines) {
        List<MesWmSortTaskLineDO> result = new ArrayList<>();
        for (MesWmSalesNoticeLineDO noticeLine : noticeLines) {
            BigDecimal remaining = noticeLine.getQuantity() != null ? noticeLine.getQuantity() : BigDecimal.ZERO;
            if (remaining.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            // 查询该物料可用库存(按批次过滤,若有)
            MesWmMaterialStockListReqVO stockQuery = new MesWmMaterialStockListReqVO();
            stockQuery.setItemId(noticeLine.getItemId());
            stockQuery.setBatchId(noticeLine.getBatchId());
            List<MesWmMaterialStockDO> stocks = materialStockService.getMaterialStockList(stockQuery);
            stocks = stocks.stream()
                    .filter(stock -> stock.getQuantity() != null && stock.getQuantity().compareTo(BigDecimal.ZERO) > 0
                            && !Boolean.TRUE.equals(stock.getFrozen()))
                    .sorted(Comparator.comparing(MesWmMaterialStockDO::getReceiptTime,
                            Comparator.nullsLast(Comparator.naturalOrder())))
                    .collect(Collectors.toList());
            BigDecimal available = stocks.stream()
                    .map(stock -> stock.getQuantity() != null ? stock.getQuantity() : BigDecimal.ZERO)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            MesMdmItemDO item = mdmItemService.getItemMap(Collections.singletonList(noticeLine.getItemId()))
                    .get(noticeLine.getItemId());
            String itemName = item != null ? item.getName() : "ID=" + noticeLine.getItemId();
            if (available.compareTo(remaining) < 0) {
                throw new ServiceException(WM_SORT_TASK_NO_STOCK.getCode(),
                        "物料[" + itemName + "]在库位上的可用库存为[" + available.stripTrailingZeros().toPlainString()
                                + "],发货通知单需要[" + remaining.stripTrailingZeros().toPlainString()
                                + "],库存不足,请先补货入库或调整发货数量后再生成分拣任务。");
            }
            for (MesWmMaterialStockDO stock : stocks) {
                if (remaining.compareTo(BigDecimal.ZERO) <= 0) {
                    break;
                }
                BigDecimal stockQuantity = stock.getQuantity() != null ? stock.getQuantity() : BigDecimal.ZERO;
                BigDecimal allocate = stockQuantity.min(remaining);
                result.add(MesWmSortTaskLineDO.builder()
                        .noticeLineId(noticeLine.getId())
                        .itemId(noticeLine.getItemId())
                        .batchId(stock.getBatchId()).batchCode(stock.getBatchCode())
                        .materialStockId(stock.getId())
                        .warehouseId(stock.getWarehouseId()).areaId(stock.getAreaId()).locationId(stock.getLocationId())
                        .planQuantity(allocate).sortedQuantity(BigDecimal.ZERO)
                        .scanned(false)
                        .build());
                remaining = remaining.subtract(allocate);
            }
        }
        return result;
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void scanLine(MesWmSortTaskScanReqVO scanReqVO) {
        MesWmSortTaskDO task = validateTaskExists(scanReqVO.getTaskId());
        validateTaskPending(task);
        MesWmSortTaskLineDO line = sortTaskLineMapper.selectById(scanReqVO.getLineId());
        if (line == null || !ObjUtil.equal(line.getTaskId(), task.getId())) {
            throw exception(WM_SORT_TASK_LINE_NOT_EXISTS);
        }
        if (MesWmSortTaskStatusEnum.FINISHED.getStatus().equals(task.getStatus())) {
            throw new ServiceException(WM_SORT_TASK_LINE_ALREADY_SORTED.getCode(),
                    "分拣任务[" + task.getCode() + "]已完成,不能再扫码分拣。");
        }
        // 扫码校验:物料条码 或 批次号 匹配即可
        MesMdmItemDO item = mdmItemService.getItemMap(Collections.singletonList(line.getItemId()))
                .get(line.getItemId());
        String barcode = StrUtil.trim(scanReqVO.getBarcode());
        boolean matched = (item != null && StrUtil.equalsIgnoreCase(barcode, StrUtil.trim(item.getBarCode())))
                || StrUtil.equalsIgnoreCase(barcode, StrUtil.trim(line.getBatchCode()));
        if (!matched) {
            throw new ServiceException(WM_SORT_TASK_SCAN_MISMATCH.getCode(),
                    "扫码内容[" + barcode + "]与分拣明细[" + (item != null ? item.getName() : "ID=" + line.getItemId())
                            + "]不匹配:明细要求的物料条码为[" + (item != null && StrUtil.isNotBlank(item.getBarCode())
                            ? item.getBarCode() : "未维护")
                            + "],批次号为[" + StrUtil.blankToDefault(line.getBatchCode(), "无批次") + "]。"
                            + "请核对货品后重新扫码。");
        }
        BigDecimal quantity = scanReqVO.getQuantity() != null ? scanReqVO.getQuantity() : line.getPlanQuantity();
        if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
            quantity = line.getPlanQuantity();
        }
        // 更新明细:累加分拣数量,但不超过计划数量
        BigDecimal sorted = line.getSortedQuantity() != null ? line.getSortedQuantity() : BigDecimal.ZERO;
        BigDecimal newSorted = sorted.add(quantity).min(Objects.requireNonNullElse(line.getPlanQuantity(), quantity));
        sortTaskLineMapper.updateById(MesWmSortTaskLineDO.builder()
                .id(line.getId()).sortedQuantity(newSorted).scanned(true).build());
        // 任务进入分拣中
        if (MesWmSortTaskStatusEnum.PENDING.getStatus().equals(task.getStatus())) {
            sortTaskMapper.updateById(MesWmSortTaskDO.builder()
                    .id(task.getId()).status(MesWmSortTaskStatusEnum.SORTING.getStatus())
                    .sortUserId(getCurrentUserId()).build());
        }
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void finishSortTask(Long id, String carrier, String shippingNumber, String remark) {
        MesWmSortTaskDO task = validateTaskExists(id);
        validateTaskPending(task);
        // 1. 校验所有明细已扫码且分拣数量完整
        List<MesWmSortTaskLineDO> lines = sortTaskLineMapper.selectListByTaskId(id);
        if (CollUtil.isEmpty(lines)) {
            throw new ServiceException(WM_SORT_TASK_NO_STOCK.getCode(),
                    "分拣任务[" + task.getCode() + "]没有分拣明细,无法完成分拣。");
        }
        List<String> unscanned = new ArrayList<>();
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(convertSet(lines, MesWmSortTaskLineDO::getItemId));
        for (MesWmSortTaskLineDO line : lines) {
            BigDecimal sorted = line.getSortedQuantity() != null ? line.getSortedQuantity() : BigDecimal.ZERO;
            BigDecimal plan = line.getPlanQuantity() != null ? line.getPlanQuantity() : BigDecimal.ZERO;
            if (!Boolean.TRUE.equals(line.getScanned()) || sorted.compareTo(plan) < 0) {
                MesMdmItemDO item = itemMap.get(line.getItemId());
                unscanned.add((item != null ? item.getName() : "ID=" + line.getItemId())
                        + "(计划 " + plan.stripTrailingZeros().toPlainString()
                        + ",已分拣 " + sorted.stripTrailingZeros().toPlainString() + ")");
            }
        }
        if (!unscanned.isEmpty()) {
            throw new ServiceException(WM_SORT_TASK_NOT_PENDING.getCode(),
                    "分拣任务[" + task.getCode() + "]还有未完成扫码的明细:" + String.join(";", unscanned)
                            + "。请对上述明细逐条扫码至计划数量后再完成分拣。");
        }
        // 2. 触发配送:发货通知单尚未生成销售出库单时自动生成
        if (productSalesMapper.selectByNoticeId(task.getNoticeId()) == null) {
            salesNoticeService.generateProductSalesFromNotice(task.getNoticeId());
        }
        // 3. 更新任务为已完成并记录承运信息
        sortTaskMapper.updateById(MesWmSortTaskDO.builder()
                .id(id).status(MesWmSortTaskStatusEnum.FINISHED.getStatus())
                .carrier(carrier).shippingNumber(shippingNumber)
                .sortUserId(getCurrentUserId()).sortTime(LocalDateTime.now())
                .remark(StrUtil.isBlank(remark) ? null : remark).build());
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancelSortTask(Long id) {
        MesWmSortTaskDO task = validateTaskExists(id);
        if (MesWmSortTaskStatusEnum.FINISHED.getStatus().equals(task.getStatus())) {
            throw new ServiceException(WM_SORT_TASK_NOT_PENDING.getCode(),
                    "分拣任务[" + task.getCode() + "]已完成,不能取消。");
        }
        sortTaskMapper.updateById(MesWmSortTaskDO.builder()
                .id(id).status(MesWmSortTaskStatusEnum.CANCELED.getStatus()).build());
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteSortTask(Long id) {
        MesWmSortTaskDO task = validateTaskExists(id);
        if (!MesWmSortTaskStatusEnum.PENDING.getStatus().equals(task.getStatus())
                && !MesWmSortTaskStatusEnum.CANCELED.getStatus().equals(task.getStatus())) {
            throw new ServiceException(WM_SORT_TASK_NOT_PENDING.getCode(),
                    "只有「待分拣」或「已取消」的分拣任务才能删除,任务[" + task.getCode() + "]当前为["
                            + MesWmSortTaskStatusEnum.valueOf(task.getStatus()).getName()
                            + "],请先取消任务。");
        }
        sortTaskMapper.deleteById(id);
        sortTaskLineMapper.deleteByTaskId(id);
    }
 
    @Override
    public void updatePrintCount(Long id) {
        MesWmSortTaskDO task = validateTaskExists(id);
        sortTaskMapper.updateById(MesWmSortTaskDO.builder()
                .id(id).printCount((task.getPrintCount() != null ? task.getPrintCount() : 0) + 1).build());
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesWmSortProductionRequestRespVO submitProductionRequest(Long id) {
        MesWmSortTaskDO task = validateTaskExists(id);
        MesWmSalesNoticeDO notice = salesNoticeService.validateSalesNoticeExists(task.getNoticeId());
        List<MesWmSalesNoticeLineDO> noticeLines =
                salesNoticeLineService.getSalesNoticeLineListByNoticeId(task.getNoticeId());
        MesWmSortProductionRequestRespVO result = new MesWmSortProductionRequestRespVO();
        if (CollUtil.isEmpty(noticeLines)) {
            result.getFailures().add("分拣任务[" + task.getCode() + "]对应的发货通知单没有明细,无法提交生产申请");
            return result;
        }
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
                convertSet(noticeLines, MesWmSalesNoticeLineDO::getItemId));
        String requestDate = notice.getSalesDate() != null
                ? notice.getSalesDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) : null;
        for (MesWmSalesNoticeLineDO noticeLine : noticeLines) {
            MesMdmItemDO item = itemMap.get(noticeLine.getItemId());
            String itemName = item != null ? item.getName() : "ID=" + noticeLine.getItemId();
            if (noticeLine.getSaleOrderItemId() == null) {
                result.getFailures().add(itemName + ":发货明细未关联销售订单明细,无法提交生产申请");
                continue;
            }
            // 已存在生产申请(MPS)时跳过,避免重复提交
            if (mpsService.getMpsBySaleOrderItemId(noticeLine.getSaleOrderItemId()) != null) {
                result.setSkippedCount(result.getSkippedCount() + 1);
                continue;
            }
            try {
                mpsService.createMps(notice.getSaleOrderId(), notice.getSaleOrderCode(),
                        noticeLine.getSaleOrderItemId(), noticeLine.getItemId(), itemName,
                        noticeLine.getQuantity(), notice.getClientId(), requestDate,
                        "由分拣任务[" + task.getCode() + "]提交生产申请");
                result.setCreatedCount(result.getCreatedCount() + 1);
            } catch (ServiceException ex) {
                result.getFailures().add(itemName + ":" + ex.getMessage());
            }
        }
        return result;
    }
 
    @Override
    public MesWmSortTaskRespVO getSortTask(Long id) {
        MesWmSortTaskDO task = validateTaskExists(id);
        return buildRespList(Collections.singletonList(task)).get(0);
    }
 
    @Override
    public PageResult<MesWmSortTaskRespVO> getSortTaskPage(MesWmSortTaskPageReqVO pageReqVO) {
        PageResult<MesWmSortTaskDO> pageResult = sortTaskMapper.selectPage(pageReqVO);
        if (CollUtil.isEmpty(pageResult.getList())) {
            return PageResult.empty(pageResult.getTotal());
        }
        return new PageResult<>(buildRespList(pageResult.getList()), pageResult.getTotal());
    }
 
    @Override
    public List<MesWmSortPendingNoticeRespVO> getPendingNoticeList(String noticeCode) {
        // 1. 查询待出库的发货通知单
        MesWmSalesNoticePageReqVO pageReqVO = new MesWmSalesNoticePageReqVO();
        pageReqVO.setCode(noticeCode);
        pageReqVO.setStatus(MesWmSalesNoticeStatusEnum.APPROVED.getStatus());
        pageReqVO.setPageSize(cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE);
        List<MesWmSalesNoticeDO> notices = salesNoticeService.getSalesNoticePage(pageReqVO).getList();
        if (CollUtil.isEmpty(notices)) {
            return Collections.emptyList();
        }
        // 2. 剔除已存在未完成分拣任务的通知单
        Set<Long> taskNoticeIds = sortTaskMapper.selectList(new LambdaQueryWrapperX<MesWmSortTaskDO>()
                        .in(MesWmSortTaskDO::getNoticeId, convertSet(notices, MesWmSalesNoticeDO::getId))
                        .ne(MesWmSortTaskDO::getStatus, MesWmSortTaskStatusEnum.CANCELED.getStatus()))
                .stream().map(MesWmSortTaskDO::getNoticeId).collect(Collectors.toSet());
        return notices.stream().filter(notice -> !taskNoticeIds.contains(notice.getId()))
                .map(notice -> new MesWmSortPendingNoticeRespVO()
                        .setNoticeId(notice.getId()).setNoticeCode(notice.getCode())
                        .setSaleOrderCode(notice.getSaleOrderCode())
                        .setRecipientName(notice.getRecipientName())
                        .setRecipientTelephone(notice.getRecipientTelephone())
                        .setRecipientAddress(notice.getRecipientAddress())
                        .setDeliveryDate(notice.getSalesDate())
                        .setOutStatus(notice.getOutStatus()))
                .collect(Collectors.toList());
    }
 
    // ==================== 私有方法 ====================
 
    private String generateCode() {
        String prefix = "SORT" + LocalDateTime.now().format(CODE_DATE_FORMAT);
        long count = sortTaskMapper.selectCount(new LambdaQueryWrapperX<MesWmSortTaskDO>()
                .likeRight(MesWmSortTaskDO::getCode, prefix)) + 1;
        String code = prefix + String.format("%03d", count);
        while (sortTaskMapper.selectByCode(code) != null) {
            count++;
            code = prefix + String.format("%03d", count);
        }
        return code;
    }
 
    private MesWmSortTaskDO validateTaskExists(Long id) {
        MesWmSortTaskDO task = id != null ? sortTaskMapper.selectById(id) : null;
        if (task == null) {
            throw exception(WM_SORT_TASK_NOT_EXISTS);
        }
        return task;
    }
 
    private void validateTaskPending(MesWmSortTaskDO task) {
        if (!MesWmSortTaskStatusEnum.PENDING.getStatus().equals(task.getStatus())
                && !MesWmSortTaskStatusEnum.SORTING.getStatus().equals(task.getStatus())) {
            String statusName = MesWmSortTaskStatusEnum.valueOf(task.getStatus()) != null
                    ? MesWmSortTaskStatusEnum.valueOf(task.getStatus()).getName() : String.valueOf(task.getStatus());
            throw new ServiceException(WM_SORT_TASK_NOT_PENDING.getCode(),
                    "分拣任务[" + task.getCode() + "]当前状态为[" + statusName + "],只有「待分拣」或「分拣中」的任务才能扫码/完成分拣。");
        }
    }
 
    private List<MesWmSortTaskRespVO> buildRespList(List<MesWmSortTaskDO> list) {
        List<MesWmSortTaskLineDO> allLines = sortTaskLineMapper.selectListByTaskIds(
                convertSet(list, MesWmSortTaskDO::getId));
        Map<Long, List<MesWmSortTaskLineDO>> lineMap = allLines.stream()
                .collect(Collectors.groupingBy(MesWmSortTaskLineDO::getTaskId));
        // 关联名称
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(convertSet(allLines, MesWmSortTaskLineDO::getItemId));
        Map<Long, MesWmWarehouseDO> warehouseMap = warehouseService.getWarehouseMap(
                convertSet(allLines, MesWmSortTaskLineDO::getWarehouseId));
        Map<Long, MesWmWarehouseAreaDO> areaMap = areaService.getWarehouseAreaMap(
                convertSet(allLines, MesWmSortTaskLineDO::getAreaId));
        Map<Long, MesWmWarehouseLocationDO> locationMap = locationService.getWarehouseLocationMap(
                convertSet(allLines, MesWmSortTaskLineDO::getLocationId));
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesWmSortTaskDO::getSortUserId));
        List<MesWmSortTaskRespVO> result = new ArrayList<>(list.size());
        for (MesWmSortTaskDO task : list) {
            MesWmSortTaskRespVO vo = new MesWmSortTaskRespVO();
            vo.setId(task.getId()).setCode(task.getCode())
                    .setNoticeId(task.getNoticeId()).setNoticeCode(task.getNoticeCode())
                    .setSaleOrderId(task.getSaleOrderId()).setSaleOrderCode(task.getSaleOrderCode())
                    .setClientId(task.getClientId())
                    .setRecipientName(task.getRecipientName())
                    .setRecipientTelephone(task.getRecipientTelephone())
                    .setRecipientAddress(task.getRecipientAddress())
                    .setCarrier(task.getCarrier()).setShippingNumber(task.getShippingNumber())
                    .setStatus(task.getStatus()).setSortUserId(task.getSortUserId())
                    .setSortTime(task.getSortTime()).setPrintCount(task.getPrintCount())
                    .setRemark(task.getRemark()).setCreateTime(task.getCreateTime());
            AdminUserRespDTO user = userMap.get(task.getSortUserId());
            if (user != null) {
                vo.setSortUserName(user.getNickname());
            }
            List<MesWmSortTaskLineDO> lines = lineMap.getOrDefault(task.getId(), Collections.emptyList());
            BigDecimal totalPlan = BigDecimal.ZERO;
            BigDecimal totalSorted = BigDecimal.ZERO;
            List<MesWmSortTaskRespVO.Line> lineVOs = new ArrayList<>(lines.size());
            for (MesWmSortTaskLineDO line : lines) {
                MesWmSortTaskRespVO.Line lineVO = new MesWmSortTaskRespVO.Line();
                lineVO.setId(line.getId()).setNoticeLineId(line.getNoticeLineId())
                        .setItemId(line.getItemId()).setBatchCode(line.getBatchCode())
                        .setMaterialStockId(line.getMaterialStockId())
                        .setWarehouseId(line.getWarehouseId()).setAreaId(line.getAreaId())
                        .setLocationId(line.getLocationId())
                        .setPlanQuantity(line.getPlanQuantity()).setSortedQuantity(line.getSortedQuantity())
                        .setScanned(line.getScanned()).setRemark(line.getRemark());
                MesMdmItemDO item = itemMap.get(line.getItemId());
                if (item != null) {
                    lineVO.setItemCode(item.getCode()).setItemName(item.getName()).setItemBarCode(item.getBarCode());
                }
                MesWmWarehouseDO warehouse = warehouseMap.get(line.getWarehouseId());
                if (warehouse != null) {
                    lineVO.setWarehouseName(warehouse.getName());
                }
                MesWmWarehouseAreaDO area = areaMap.get(line.getAreaId());
                if (area != null) {
                    lineVO.setAreaName(area.getName());
                }
                MesWmWarehouseLocationDO location = locationMap.get(line.getLocationId());
                if (location != null) {
                    lineVO.setLocationName(location.getName());
                }
                totalPlan = totalPlan.add(line.getPlanQuantity() != null ? line.getPlanQuantity() : BigDecimal.ZERO);
                totalSorted = totalSorted.add(line.getSortedQuantity() != null ? line.getSortedQuantity() : BigDecimal.ZERO);
                lineVOs.add(lineVO);
            }
            vo.setLines(lineVOs).setTotalPlanQuantity(totalPlan).setTotalSortedQuantity(totalSorted);
            result.add(vo);
        }
        return result;
    }
 
    private Long getCurrentUserId() {
        try {
            return cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId();
        } catch (Exception ex) {
            return null;
        }
    }
 
}