From 724a3d9d2cdb001bd9fae4eaea0ffabfa5ffaf6d Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 20 十月 2026 11:19:09 +0800
Subject: [PATCH] feat(mes): 完善种子质检功能实现
---
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java | 20 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityActionRespVO.java | 29 +++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java | 7
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityGroupTest.java | 140 ++++++++++++--
docs/seed_quality_action_operator_integration.md | 61 ++++++
scripts/mobile-quality-sales-smoke.cjs | 4
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityIndicatorMapper.java | 7
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java | 269 ++++++++++++++++++++++-------
8 files changed, 439 insertions(+), 98 deletions(-)
diff --git a/docs/seed_quality_action_operator_integration.md b/docs/seed_quality_action_operator_integration.md
new file mode 100644
index 0000000..ab1f448
--- /dev/null
+++ b/docs/seed_quality_action_operator_integration.md
@@ -0,0 +1,61 @@
+# 绉嶅瓙璐ㄦ娴佺▼璁板綍鎿嶄綔浜烘樀绉� - 鍓嶇鑱旇皟鏂规
+
+## 娑夊強椤甸潰
+
+- 璐ㄩ噺绠$悊 > 绉嶅瓙璐ㄦ > 璐ㄦ璁板綍锛堟祦绋嬭褰曞脊绐楋級
+
+## 鑳屾櫙
+
+娴佺▼璁板綍姝ゅ墠鐩存帴灞曠ず `operatorId`锛堝鈥滄搷浣滀汉 1鈥濓級锛岄渶瑕佸睍绀虹敤鎴锋樀绉般��
+
+## API
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| GET | `/mes/qc/seed-quality/actions?id={qualityId}` | 鑾峰緱绉嶅瓙璐ㄦ娴佺▼鐣欑棔 |
+
+**鍝嶅簲瀛楁鏂板锛�**
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| operatorName | String | 鎿嶄綔浜烘樀绉帮紝鍙栫郴缁熺敤鎴锋樀绉帮紱鐢ㄦ埛涓嶅瓨鍦ㄦ椂涓虹┖ |
+
+**鍝嶅簲绀轰緥锛�**
+
+```json
+{
+ "code": 0,
+ "data": [
+ {
+ "id": 1,
+ "qualityId": 192,
+ "action": "INITIAL",
+ "result": 1,
+ "opinion": "",
+ "indicatorsJson": "[...]",
+ "operatorId": 1,
+ "operatorName": "瓒呯骇绠$悊鍛�",
+ "createTime": "2026-10-20 10:29:30"
+ }
+ ]
+}
+```
+
+## 瀛楁灞曠ず瑙勫垯
+
+| 瀛楁 | 灞曠ず浣嶇疆 | 璇存槑 |
+|------|----------|------|
+| operatorName | 娴佺▼璁板綍寮圭獥姣忔潯璁板綍澶撮儴 | 涓庢搷浣滄椂闂村悓琛屽睍绀轰负鈥滄搷浣滀汉 xxx鈥� |
+| operatorId | 鍏滃簳 | `operatorName` 涓虹┖鏃跺睍绀轰负鈥滄搷浣滀汉 {id}鈥濓紝鍐嶄负绌烘樉绀衡�溾�斺�� |
+
+## 涓氬姟瑙勫垯璇存槑
+
+| 鍦烘櫙 | 瑙勫垯 |
+|------|------|
+| 鎿嶄綔浜哄凡绂昏亴/鍒犻櫎 | `operatorName` 涓虹┖锛屽墠绔洖閫�灞曠ず `operatorId` |
+| 涓�娆℃煡璇㈠鏉$暀鐥� | 鍚庣缁熶竴鎵归噺鏌ヨ鐢ㄦ埛锛屼粎澧炲姞涓�娆$敤鎴锋煡璇㈣皟鐢� |
+
+## 娉ㄦ剰浜嬮」
+
+- 璇ユ帴鍙d粎鏂板鍝嶅簲瀛楁锛屼笉鏀瑰彉璇锋眰鍙傛暟锛屾棫鍓嶇鍙户缁娇鐢ㄣ��
+- 閮ㄧ讲闇�閲嶅惎鍚庣锛屾樀绉版墠浼氳繑鍥炪��
diff --git a/scripts/mobile-quality-sales-smoke.cjs b/scripts/mobile-quality-sales-smoke.cjs
index e25fd16..8dace72 100644
--- a/scripts/mobile-quality-sales-smoke.cjs
+++ b/scripts/mobile-quality-sales-smoke.cjs
@@ -74,7 +74,9 @@
await page.getByRole('heading', { name: '璐ㄦ缁撴灉寰呯‘璁�' }).waitFor(); assert.equal(writes.length, 1); assert.equal(writes[0].quality.code, 'TRACE-1');
await page.reload(); await page.getByRole('button', { name: '鏍稿疄璐ㄦ缁撴灉' }).click();
await page.getByText(/宸叉牳瀹烇細鏈琚嬬爜鍧囧凡鏈夎川妫�璁板綍/).waitFor(); assert.equal(writes.length, 1, 'Verification must not resubmit quality');
- await page.goto(base + '/mobile/quality-records'); await page.getByText('鍚堟牸', { exact: true }).last().waitFor();
+ await page.goto(base + '/mobile/quality-records');
+ await page.getByRole('heading', { name: '鎵归噺妫�楠屼笌瀹℃牳', exact: true }).waitFor();
+ await page.getByText('鍚堟牸', { exact: true }).last().waitFor();
await capture('quality-records-480.png');
for (const type of ['PALLET', 'BATCH']) {
quality = null;
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java
index 65ec3f5..8e6c366 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java
@@ -12,7 +12,11 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityBatchRetestReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityScopeRetestReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedIndicatorRangeSaveReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityActionRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityActionDO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
import cn.iocoder.yudao.module.mes.service.qc.seedquality.MesQcSeedQualityService;
import cn.iocoder.yudao.module.mes.service.qc.seedquality.MesQcSeedIndicatorRangeService;
@@ -36,6 +40,7 @@
@Resource private MesQcSeedQualityService service;
@Resource private MesQcSeedIndicatorRangeService rangeService;
@Resource private MesProBagCodeMapper bagCodeMapper;
+ @Resource private AdminUserApi adminUserApi;
@GetMapping("/ranges") @Operation(summary = "鑾峰緱绉嶅瓙璐ㄦ鍚堟牸鑼冨洿")
@PreAuthorize("@ss.hasAnyPermissions('mes:qc-seed-quality:submit', 'mes:qc-seed-quality:retest', 'mes:qc-seed-range:query')")
public CommonResult<java.util.List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedIndicatorRangeDO>> ranges() {
@@ -50,7 +55,7 @@
}
@GetMapping("/resolve")
@Operation(summary = "瑙f瀽寰呰川妫�琚嬬爜鍙婄‘璁よ琚嬭川妫�缁撴灉")
- @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:submit')")
+ @PreAuthorize("@ss.hasAnyPermissions('mes:qc-seed-quality:submit', 'mes:qc-seed-quality:retest', 'mes:qc-seed-quality:process-approve', 'mes:qc-seed-quality:company-approve')")
public CommonResult<java.util.Map<String, Object>> resolve(@RequestParam String code, @RequestParam(defaultValue = "BAG") String scanType) {
return success(service.resolveScan(code, scanType));
}
@@ -80,8 +85,15 @@
}
@GetMapping("/actions") @Operation(summary = "鑾峰緱绉嶅瓙璐ㄦ娴佺▼鐣欑棔")
@PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:query')")
- public CommonResult<java.util.List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityActionDO>> actions(@RequestParam Long id) {
- return success(service.getActions(id));
+ public CommonResult<List<MesQcSeedQualityActionRespVO>> actions(@RequestParam Long id) {
+ List<MesQcSeedQualityActionDO> actions = service.getActions(id);
+ Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(actions.stream()
+ .map(MesQcSeedQualityActionDO::getOperatorId).filter(java.util.Objects::nonNull)
+ .collect(Collectors.toSet()));
+ return success(BeanUtils.toBean(actions, MesQcSeedQualityActionRespVO.class, vo -> {
+ AdminUserRespDTO user = userMap.get(vo.getOperatorId());
+ if (user != null) vo.setOperatorName(user.getNickname());
+ }));
}
@PostMapping("/submit") @Operation(summary = "鎻愪氦绉嶅瓙璐ㄦ")
@@ -109,7 +121,7 @@
}
@GetMapping("/retest-scope/resolve") @Operation(summary = "鎸夋墭鐩樼爜鎴栫敓浜ф壒娆$爜瑙f瀽寰呮壒閲忚川妫�璁板綍")
- @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:retest')")
+ @PreAuthorize("@ss.hasAnyPermissions('mes:qc-seed-quality:submit', 'mes:qc-seed-quality:retest', 'mes:qc-seed-quality:process-approve', 'mes:qc-seed-quality:company-approve')")
public CommonResult<java.util.Map<String, Object>> resolveRetestScope(@RequestParam String code, @RequestParam String scanType) {
return success(service.resolveRetestScope(code, scanType));
}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityActionRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityActionRespVO.java
new file mode 100644
index 0000000..e9e65c4
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityActionRespVO.java
@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "绠$悊鍚庡彴 - MES 绉嶅瓙璐ㄦ娴佺▼璁板綍 Response VO")
+@Data
+public class MesQcSeedQualityActionRespVO {
+ @Schema(description = "缂栧彿")
+ private Long id;
+ @Schema(description = "璐ㄦ璁板綍缂栧彿")
+ private Long qualityId;
+ @Schema(description = "鎿嶄綔绫诲瀷")
+ private String action;
+ @Schema(description = "妫�楠岀粨鏋�")
+ private Integer result;
+ @Schema(description = "瀹℃壒鎰忚 / 澶囨敞")
+ private String opinion;
+ @Schema(description = "鎸囨爣蹇収")
+ private String indicatorsJson;
+ @Schema(description = "鎿嶄綔浜虹紪鍙�")
+ private Long operatorId;
+ @Schema(description = "鎿嶄綔浜烘樀绉�")
+ private String operatorName;
+ @Schema(description = "鎿嶄綔鏃堕棿")
+ private LocalDateTime createTime;
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityIndicatorMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityIndicatorMapper.java
index a7fc1d8..479f477 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityIndicatorMapper.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityIndicatorMapper.java
@@ -11,4 +11,11 @@
.eq(MesQcSeedQualityIndicatorDO::getQualityId, qualityId)
.orderByAsc(MesQcSeedQualityIndicatorDO::getId));
}
+ default List<MesQcSeedQualityIndicatorDO> selectListByQualityIds(java.util.Collection<Long> qualityIds) {
+ if (qualityIds.isEmpty()) return List.of();
+ return selectList(new LambdaQueryWrapperX<MesQcSeedQualityIndicatorDO>()
+ .in(MesQcSeedQualityIndicatorDO::getQualityId, qualityIds)
+ .orderByAsc(MesQcSeedQualityIndicatorDO::getQualityId)
+ .orderByAsc(MesQcSeedQualityIndicatorDO::getId));
+ }
}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java
index b75c547..8078d31 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java
@@ -38,6 +38,13 @@
default MesQcSeedQualityDO selectByIdForUpdate(Long id) {
return selectOne(new LambdaQueryWrapperX<MesQcSeedQualityDO>().eq(MesQcSeedQualityDO::getId, id).last("FOR UPDATE"));
}
+ default List<MesQcSeedQualityDO> selectListByIdsForUpdate(java.util.Collection<Long> ids) {
+ if (ids.isEmpty()) return List.of();
+ return selectList(new LambdaQueryWrapperX<MesQcSeedQualityDO>()
+ .in(MesQcSeedQualityDO::getId, ids)
+ .orderByAsc(MesQcSeedQualityDO::getId)
+ .last("FOR UPDATE"));
+ }
default List<MesQcSeedQualityDO> selectListByBatchCode(String batchCode) {
return selectList(new LambdaQueryWrapperX<MesQcSeedQualityDO>()
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
index 386ab71..45248cf 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
@@ -47,6 +47,7 @@
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_QUALIFIED_WAREHOUSE_NOT_CONFIGURED;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_WAREHOUSE_NOT_EXISTS;
@@ -55,6 +56,7 @@
@Service @Validated @Slf4j
public class MesQcSeedQualityServiceImpl implements MesQcSeedQualityService {
private static final Set<String> REQUIRED = Set.of("purity", "cleanliness", "germination_rate", "moisture");
+ private static final int MAX_BATCH_SIZE = 500, WRITE_BATCH_SIZE = 100;
private static final int WAIT_RETEST = 10, WAIT_PROCESS = 20, WAIT_INITIAL_FAIL_REVIEW = 25, WAIT_COMPANY = 30,
RELEASED = 40, WAIT_INITIAL_RECHECK = 50, WAIT_COMPANY_FAIL = 60, SCRAPPED = 70;
@Resource private MesQcSeedQualityMapper qualityMapper;
@@ -104,23 +106,23 @@
private Scope scope(String code, String type, boolean lock) {
if ("BAG".equals(type)) {
MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(code);
- if (bag == null) throw new IllegalArgumentException("鏈壘鍒拌鐮侊紝璇锋牳瀵规壂鐮佺被鍨�");
+ if (bag == null) throw invalidParamException("鏈壘鍒拌鐮侊紝璇锋牳瀵规壂鐮佺被鍨嬪拰鐮佸��");
return new Scope(bag.getCode(), bag.getBusinessCode(), bag.getBatchCode(), Integer.valueOf(30).equals(bag.getStatus()), List.of(bag));
}
if ("PALLET".equals(type)) {
var pallet = lock ? palletMapper.selectByScanCodeForUpdate(code) : palletMapper.selectByScanCode(code);
- if (pallet == null) throw new IllegalArgumentException("鏈壘鍒版墭鐩樼爜锛岃鏍稿鎵爜绫诲瀷");
- if (pallet.getBatchId() == null) throw new IllegalArgumentException("鎵樼洏灏氭湭缁戝畾鐢熶骇鎵规锛屾棤娉曡川妫�");
+ if (pallet == null) throw invalidParamException("鏈壘鍒版墭鐩樼爜锛岃鏍稿鎵爜绫诲瀷鍜岀爜鍊�");
+ if (pallet.getBatchId() == null) throw invalidParamException("鎵樼洏灏氭湭缁戝畾鐢熶骇鎵规锛屾棤娉曡川妫�");
return new Scope(StrUtil.blankToDefault(pallet.getCode(), pallet.getTempCode()), pallet.getBusinessCode(), pallet.getBatchCode(),
Integer.valueOf(20).equals(pallet.getStatus()), lock ? bagCodeMapper.selectListByPalletIdForUpdate(pallet.getId()) : bagCodeMapper.selectListByPalletId(pallet.getId()));
}
if ("BATCH".equals(type)) {
var batch = lock ? batchMapper.selectByScanCodeForUpdate(code) : batchMapper.selectByScanCode(code);
- if (batch == null) throw new IllegalArgumentException("鏈壘鍒扮敓浜ф壒娆$爜锛岃鏍稿鎵爜绫诲瀷");
+ if (batch == null) throw invalidParamException("鏈壘鍒扮敓浜ф壒娆$爜锛岃鏍稿鎵爜绫诲瀷鍜岀爜鍊�");
return new Scope(StrUtil.blankToDefault(batch.getCode(), batch.getTempCode()), batch.getBusinessCode(), batch.getCode(),
Integer.valueOf(40).equals(batch.getStatus()), lock ? bagCodeMapper.selectListByBatchIdForUpdate(batch.getId()) : bagCodeMapper.selectListByBatchId(batch.getId()));
}
- throw new IllegalArgumentException("涓嶆敮鎸佺殑璐ㄦ鎵爜绫诲瀷");
+ throw invalidParamException("涓嶆敮鎸佺殑璐ㄦ鎵爜绫诲瀷");
}
@Override
@@ -145,32 +147,76 @@
@Override @Transactional(rollbackFor = Exception.class)
public Map<String, Object> submitGroup(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO req) {
- Scope scope = scope(req.getCode(), req.getScanType(), true);
- if (scope.canceled()) throw new IllegalArgumentException("璐ㄦ瀵硅薄宸蹭綔搴�");
+ List<Long> confirmed = batchIds(req.getBagIds(), "琚嬬爜");
+ Scope scope = scope(req.getCode(), req.getScanType(), false);
+ if (scope.canceled()) throw invalidParamException("璐ㄦ瀵硅薄宸蹭綔搴�");
List<MesProBagCodeDO> effective = scope.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())).toList();
if (effective.isEmpty()) throw new IllegalArgumentException("娌℃湁鏈夋晥琚嬬爜");
- List<MesProBagCodeDO> locked = bagCodeMapper.selectListByIdsForUpdate(effective.stream().map(MesProBagCodeDO::getId).toList());
- if (locked.size() != effective.size() || locked.stream().anyMatch(b -> Integer.valueOf(30).equals(b.getStatus()) || b.getInboundEventId() == null))
+ Set<String> inspected = qualityMapper.selectListByCodes(effective.stream().map(MesProBagCodeDO::getCode).toList()).stream()
+ .map(MesQcSeedQualityDO::getCode).collect(java.util.stream.Collectors.toSet());
+ List<MesProBagCodeDO> pendingPreview = effective.stream().filter(b -> b.getInboundEventId() != null && !inspected.contains(b.getCode()))
+ .sorted(Comparator.comparing(MesProBagCodeDO::getId)).toList();
+ List<Long> expectedChunk = pendingPreview.stream().limit(MAX_BATCH_SIZE).map(MesProBagCodeDO::getId).toList();
+ if (expectedChunk.isEmpty() || !confirmed.equals(expectedChunk))
+ throw new IllegalArgumentException("寰呰川妫�娓呭崟宸插彉鍖栨垨宸插叏閮ㄨ川妫�锛岃閲嶆柊鎵弿锛岀姝㈤噸澶嶅鐞�");
+
+ List<MesProBagCodeDO> locked = bagCodeMapper.selectListByIdsForUpdate(confirmed);
+ if (locked.size() != confirmed.size() || locked.stream().anyMatch(b -> Integer.valueOf(30).equals(b.getStatus()) || b.getInboundEventId() == null))
throw new IllegalArgumentException("琚嬬爜宸插彉鍖栨垨灏氭湭鍏ㄩ儴鍏ュ簱锛岃閲嶆柊鎵弿鏍稿");
// 鍦ㄨ鐮侀攣涔嬪悗閲嶆柊鏍稿褰掑睘锛涘叾浠栫粦瀹氭搷浣滀笉寰楁敼鍙樺凡纭鐨勬楠岃寖鍥淬��
Scope current = scope(req.getCode(), req.getScanType(), false);
Set<Long> currentIds = current.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())).map(MesProBagCodeDO::getId).collect(java.util.stream.Collectors.toSet());
- if (!currentIds.equals(new HashSet<>(locked.stream().map(MesProBagCodeDO::getId).toList())))
+ if (!currentIds.containsAll(confirmed))
throw new IllegalArgumentException("琚嬬爜褰掑睘宸插彉鍖栵紝璇烽噸鏂版壂鎻忔牳瀵�");
- Set<String> inspected = qualityMapper.selectListByCodesForUpdate(locked.stream().map(MesProBagCodeDO::getCode).toList()).stream()
- .map(MesQcSeedQualityDO::getCode).collect(java.util.stream.Collectors.toSet());
- List<MesProBagCodeDO> pending = locked.stream().filter(b -> !inspected.contains(b.getCode())).sorted(Comparator.comparing(MesProBagCodeDO::getId)).toList();
- List<Long> confirmed = req.getBagIds().stream().sorted().toList();
- if (pending.isEmpty() || !confirmed.equals(pending.stream().map(MesProBagCodeDO::getId).toList()))
+ if (!qualityMapper.selectListByCodesForUpdate(locked.stream().map(MesProBagCodeDO::getCode).toList()).isEmpty())
throw new IllegalArgumentException("寰呰川妫�娓呭崟宸插彉鍖栨垨宸插叏閮ㄨ川妫�锛岃閲嶆柊鎵弿锛岀姝㈤噸澶嶅鐞�");
- for (MesProBagCodeDO bag : pending) {
- MesQcSeedQualitySubmitReqVO single = new MesQcSeedQualitySubmitReqVO();
- single.setCode(bag.getCode());single.setIndicators(req.getIndicators());single.setReason(req.getReason());
- single.setQualifiedWarehouseId(req.getQualifiedWarehouseId());
- // 澶栧眰浜嬪姟瑕嗙洊鍏ㄩ儴琚嬬爜锛涗换浣曚竴琚嬪簱瀛樻祦杞け璐ラ兘浼氬洖婊氭暣娆℃搷浣溿��
- submit(single);
- }
+ List<MesProBagCodeDO> pending = locked.stream().sorted(Comparator.comparing(MesProBagCodeDO::getId)).toList();
+ submitInitialBatch(pending, req);
return Map.of("processedCount", pending.size());
+ }
+
+ private void submitInitialBatch(List<MesProBagCodeDO> bags,
+ cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO req) {
+ boolean passed = validateIndicators(req.getIndicators());
+ if (!passed && StrUtil.isBlank(req.getReason())) throw new IllegalArgumentException("璐ㄦ涓嶅悎鏍煎繀椤诲~鍐欏師鍥�");
+ Long batchId = bags.get(0).getBatchId();
+ if (batchId == null || bags.stream().anyMatch(bag -> !Objects.equals(batchId, bag.getBatchId())))
+ throw new IllegalArgumentException("鎵归噺鍒濇鍙厑璁稿悓涓�鐢熶骇鎵规鐨勮鐮�");
+ MesProBatchDO batch = proBatchService.getBatch(batchId);
+ if (batch == null || batch.getItemId() == null) throw new IllegalArgumentException("鏈牴鎹鐮佽В鏋愬埌鐗╂枡淇℃伅");
+ if (Integer.valueOf(40).equals(batch.getStatus())) throw new IllegalArgumentException("鐢熶骇鎵规宸蹭綔搴燂紝鏃犳硶璐ㄦ");
+ MesWmBatchDO wmBatch = mesWmBatchMapper.selectByCode(batch.getCode());
+ if (wmBatch == null || materialStockMapper.selectTempStockCountByBatchId(wmBatch.getId()) == 0)
+ throw new IllegalArgumentException("璇ユ壒娆″簱瀛樹笉鍦ㄦ殏瀛樺簱涓紝鏃犳硶杩涜璐ㄦ");
+ if (warehouseService.getFirstQualifiedWarehouse() == null) throw exception(WM_QUALIFIED_WAREHOUSE_NOT_CONFIGURED);
+ MesWmWarehouseDO qualifiedWarehouse = warehouseService.getWarehouse(req.getQualifiedWarehouseId());
+ if (qualifiedWarehouse == null) throw exception(WM_WAREHOUSE_NOT_EXISTS);
+ if (!MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType()))
+ throw exception(WM_WAREHOUSE_TYPE_NOT_QUALIFIED);
+
+ LocalDateTime now = LocalDateTime.now();
+ Long operatorId = getLoginUserId();
+ int result = passed ? 1 : 2;
+ int status = passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW;
+ List<MesQcSeedQualityDO> qualities = bags.stream().map(bag -> MesQcSeedQualityDO.builder()
+ .code(bag.getCode()).itemId(batch.getItemId()).batchId(batch.getId()).batchCode(batch.getCode())
+ .result(result).reason(passed ? "" : req.getReason().trim()).workflowStatus(status)
+ .qualifiedWarehouseId(qualifiedWarehouse.getId()).inspectorId(operatorId).inspectedAt(now).build()).toList();
+ qualityMapper.insertBatch(qualities, WRITE_BATCH_SIZE);
+ if (qualities.stream().anyMatch(quality -> quality.getId() == null))
+ throw new IllegalStateException("鎵归噺鍒濇璁板綍缂栧彿鐢熸垚澶辫触");
+
+ List<MesQcSeedQualityIndicatorDO> indicators = new ArrayList<>(qualities.size() * REQUIRED.size());
+ List<MesQcSeedQualityActionDO> actions = new ArrayList<>(qualities.size());
+ for (MesQcSeedQualityDO quality : qualities) {
+ for (MesQcSeedQualitySubmitReqVO.Indicator item : req.getIndicators()) {
+ indicators.add(MesQcSeedQualityIndicatorDO.builder().qualityId(quality.getId()).indicatorCode(item.getCode())
+ .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
+ }
+ actions.add(newAction(quality.getId(), "INITIAL", result, req.getReason(), req.getIndicators(), operatorId, now));
+ }
+ indicatorMapper.insertBatch(indicators, WRITE_BATCH_SIZE * REQUIRED.size());
+ actionMapper.insertBatch(actions, WRITE_BATCH_SIZE);
}
@Override @Transactional(rollbackFor = Exception.class)
@@ -352,13 +398,13 @@
@Override @Transactional(rollbackFor = Exception.class)
public void retestBatch(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityBatchRetestReqVO req) {
List<Long> ids = approvalIds(req.getIds());
+ List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByIdsForUpdate(ids);
+ validateLockedIds(ids, qualities);
String batchCode = null;
Integer workflowStatus = null;
boolean first = true;
- List<MesQcSeedQualityDO> qualities = new ArrayList<>();
- for (Long id : ids) {
- MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
- if (quality == null || !Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(quality.getWorkflowStatus()))
+ for (MesQcSeedQualityDO quality : qualities) {
+ if (!Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(quality.getWorkflowStatus()))
throw new IllegalArgumentException("鎵�閫夎褰曞寘鍚笉鍙楠岀殑鐘舵�侊紝璇峰埛鏂板悗閲嶈瘯");
if (StrUtil.isBlank(quality.getBatchCode()))
throw new IllegalArgumentException("璁板綍缂哄皯鐢熶骇鎵规锛屼笉鑳芥壒閲忚川妫�");
@@ -369,16 +415,8 @@
} else if (!Objects.equals(batchCode, quality.getBatchCode()) || !Objects.equals(workflowStatus, quality.getWorkflowStatus())) {
throw new IllegalArgumentException("鎵归噺璐ㄦ鍙厑璁稿悓鐢熶骇鎵规銆佸悓娴佺▼鐘舵�佺殑璁板綍");
}
- qualities.add(quality);
}
- for (MesQcSeedQualityDO quality : qualities) {
- cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO single =
- new cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO();
- single.setCode(quality.getCode());
- single.setIndicators(req.getIndicators());
- single.setReason(req.getReason());
- retest(quality.getId(), single);
- }
+ applyRetestBatch(qualities, req.getIndicators(), req.getReason());
}
private void checkRetestScanType(String scanType) {
@@ -390,7 +428,7 @@
public Map<String, Object> resolveRetestScope(String code, String scanType) {
checkRetestScanType(scanType);
Scope scope = scope(code, scanType, false);
- if (scope.canceled()) throw new IllegalArgumentException("璐ㄦ瀵硅薄宸蹭綔搴�");
+ if (scope.canceled()) throw invalidParamException("璐ㄦ瀵硅薄宸蹭綔搴�");
List<String> codes = scope.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() != null)
.map(MesProBagCodeDO::getCode).toList();
List<MesQcSeedQualityDO> qualities = codes.isEmpty() ? List.of() : qualityMapper.selectListByCodes(codes);
@@ -398,16 +436,22 @@
.map(MesQcSeedQualityDO::getId).sorted().toList();
List<Long> initialRecheckIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_INITIAL_RECHECK))
.map(MesQcSeedQualityDO::getId).sorted().toList();
- long pendingProcessCount = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_PROCESS)
- || Objects.equals(q.getWorkflowStatus(), WAIT_INITIAL_FAIL_REVIEW)).count();
- long pendingCompanyCount = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_COMPANY)
- || Objects.equals(q.getWorkflowStatus(), WAIT_COMPANY_FAIL)).count();
+ List<Long> pendingProcessIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_PROCESS))
+ .map(MesQcSeedQualityDO::getId).sorted().toList();
+ List<Long> pendingInitialFailIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_INITIAL_FAIL_REVIEW))
+ .map(MesQcSeedQualityDO::getId).sorted().toList();
+ List<Long> pendingCompanyIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_COMPANY))
+ .map(MesQcSeedQualityDO::getId).sorted().toList();
+ List<Long> pendingCompanyFailIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_COMPANY_FAIL))
+ .map(MesQcSeedQualityDO::getId).sorted().toList();
Map<String, Object> result = new HashMap<>();
result.put("code", scope.code()); result.put("scanType", scanType); result.put("batchCode", scope.batchCode());
result.put("totalCount", codes.size()); result.put("retestIds", retestIds);
result.put("initialRecheckIds", initialRecheckIds);
- result.put("pendingProcessCount", pendingProcessCount);
- result.put("pendingCompanyCount", pendingCompanyCount);
+ result.put("pendingProcessIds", pendingProcessIds); result.put("pendingInitialFailIds", pendingInitialFailIds);
+ result.put("pendingCompanyIds", pendingCompanyIds); result.put("pendingCompanyFailIds", pendingCompanyFailIds);
+ result.put("pendingProcessCount", pendingProcessIds.size() + pendingInitialFailIds.size());
+ result.put("pendingCompanyCount", pendingCompanyIds.size() + pendingCompanyFailIds.size());
return result;
}
@@ -417,30 +461,70 @@
if (!Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(req.getWorkflowStatus()))
throw new IllegalArgumentException("鎵归噺璐ㄦ鐘舵�佹棤鏁�");
List<Long> confirmed = approvalIds(req.getQualityIds());
- Scope scope = scope(req.getCode(), req.getScanType(), true);
- if (scope.canceled()) throw new IllegalArgumentException("璐ㄦ瀵硅薄宸蹭綔搴�");
+ Scope scope = scope(req.getCode(), req.getScanType(), false);
+ if (scope.canceled()) throw invalidParamException("璐ㄦ瀵硅薄宸蹭綔搴�");
List<MesProBagCodeDO> effective = scope.bags().stream()
.filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() != null).toList();
if (effective.isEmpty()) throw new IllegalArgumentException("褰撳墠鑼冨洿娌℃湁宸插叆搴撶殑鏈夋晥琚嬬爜");
- List<MesProBagCodeDO> locked = bagCodeMapper.selectListByIdsForUpdate(effective.stream().map(MesProBagCodeDO::getId).toList());
- if (locked.size() != effective.size() || locked.stream().anyMatch(b -> Integer.valueOf(30).equals(b.getStatus()) || b.getInboundEventId() == null))
- throw new IllegalArgumentException("琚嬬爜鐘舵�佸凡鍙樺寲锛岃閲嶆柊鎵弿");
- Scope current = scope(req.getCode(), req.getScanType(), false);
- Set<Long> currentIds = current.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() != null)
- .map(MesProBagCodeDO::getId).collect(java.util.stream.Collectors.toSet());
- if (!currentIds.equals(new HashSet<>(locked.stream().map(MesProBagCodeDO::getId).toList())))
- throw new IllegalArgumentException("琚嬬爜褰掑睘宸插彉鍖栵紝璇烽噸鏂版壂鎻�");
- List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByCodesForUpdate(locked.stream().map(MesProBagCodeDO::getCode).toList());
- List<MesQcSeedQualityDO> pending = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), req.getWorkflowStatus()))
+ Set<String> effectiveCodes = effective.stream().map(MesProBagCodeDO::getCode).collect(java.util.stream.Collectors.toSet());
+ List<MesQcSeedQualityDO> pendingPreview = qualityMapper.selectListByCodes(effectiveCodes).stream()
+ .filter(q -> Objects.equals(q.getWorkflowStatus(), req.getWorkflowStatus()))
.sorted(Comparator.comparing(MesQcSeedQualityDO::getId)).toList();
- if (pending.isEmpty() || !confirmed.equals(pending.stream().map(MesQcSeedQualityDO::getId).toList()))
+ List<Long> expectedChunk = pendingPreview.stream().limit(MAX_BATCH_SIZE).map(MesQcSeedQualityDO::getId).toList();
+ if (expectedChunk.isEmpty() || !confirmed.equals(expectedChunk))
throw new IllegalArgumentException("寰呰川妫�娓呭崟宸插彉鍖栵紝璇烽噸鏂版壂鎻忔牳瀵�");
- for (MesQcSeedQualityDO quality : pending) {
- var single = new cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO();
- single.setCode(quality.getCode()); single.setIndicators(req.getIndicators()); single.setReason(req.getReason());
- retest(quality.getId(), single);
- }
+ List<MesQcSeedQualityDO> pending = qualityMapper.selectListByIdsForUpdate(confirmed);
+ validateLockedIds(confirmed, pending);
+ if (pending.stream().anyMatch(quality -> !Objects.equals(quality.getWorkflowStatus(), req.getWorkflowStatus())
+ || !effectiveCodes.contains(quality.getCode())))
+ throw new IllegalArgumentException("寰呰川妫�璁板綍鐘舵�佹垨褰掑睘宸插彉鍖栵紝璇烽噸鏂版壂鎻忔牳瀵�");
+ applyRetestBatch(pending, req.getIndicators(), req.getReason());
return Map.of("processedCount", pending.size());
+ }
+
+ private void applyRetestBatch(List<MesQcSeedQualityDO> qualities,
+ List<MesQcSeedQualitySubmitReqVO.Indicator> submittedIndicators, String reason) {
+ boolean passed = validateIndicators(submittedIndicators);
+ if (!passed && StrUtil.isBlank(reason)) throw new IllegalArgumentException("妫�楠屼笉鍚堟牸蹇呴』濉啓鍘熷洜");
+ List<Long> ids = qualities.stream().map(MesQcSeedQualityDO::getId).toList();
+ Map<Long, Map<String, MesQcSeedQualityIndicatorDO>> existingByQuality = new HashMap<>();
+ for (MesQcSeedQualityIndicatorDO indicator : indicatorMapper.selectListByQualityIds(ids)) {
+ existingByQuality.computeIfAbsent(indicator.getQualityId(), ignored -> new HashMap<>())
+ .put(indicator.getIndicatorCode(), indicator);
+ }
+
+ LocalDateTime now = LocalDateTime.now();
+ Long operatorId = getLoginUserId();
+ int result = passed ? 1 : 2;
+ List<MesQcSeedQualityDO> qualityUpdates = new ArrayList<>(qualities.size());
+ List<MesQcSeedQualityIndicatorDO> indicatorUpdates = new ArrayList<>(qualities.size() * REQUIRED.size());
+ List<MesQcSeedQualityActionDO> actions = new ArrayList<>(qualities.size());
+ for (MesQcSeedQualityDO quality : qualities) {
+ boolean initialRecheck = Objects.equals(quality.getWorkflowStatus(), WAIT_INITIAL_RECHECK);
+ if (!initialRecheck && !Objects.equals(quality.getWorkflowStatus(), WAIT_RETEST))
+ throw new IllegalArgumentException("鎵�閫夎褰曞寘鍚笉鍙楠岀殑鐘舵�侊紝璇峰埛鏂板悗閲嶈瘯");
+ MesQcSeedQualityDO update = MesQcSeedQualityDO.builder().id(quality.getId()).result(result)
+ .reason(passed ? "" : reason.trim()).inspectorId(operatorId).inspectedAt(now)
+ .workflowStatus(initialRecheck ? (passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW)
+ : (passed ? WAIT_COMPANY : WAIT_COMPANY_FAIL)).build();
+ if (!initialRecheck) {
+ update.setRetestResult(result);
+ update.setRetestedAt(now);
+ }
+ qualityUpdates.add(update);
+ Map<String, MesQcSeedQualityIndicatorDO> existing = existingByQuality.getOrDefault(quality.getId(), Map.of());
+ for (MesQcSeedQualitySubmitReqVO.Indicator item : submittedIndicators) {
+ MesQcSeedQualityIndicatorDO indicator = existing.get(item.getCode());
+ if (indicator == null) throw new IllegalArgumentException("璐ㄦ鎸囨爣璁板綍缂哄け");
+ indicatorUpdates.add(MesQcSeedQualityIndicatorDO.builder().id(indicator.getId())
+ .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
+ }
+ actions.add(newAction(quality.getId(), initialRecheck ? "INITIAL_RECHECK" : "RETEST", result,
+ reason, submittedIndicators, operatorId, now));
+ }
+ qualityMapper.updateBatch(qualityUpdates, WRITE_BATCH_SIZE);
+ indicatorMapper.updateBatch(indicatorUpdates, WRITE_BATCH_SIZE * REQUIRED.size());
+ actionMapper.insertBatch(actions, WRITE_BATCH_SIZE);
}
@Override @Transactional(rollbackFor = Exception.class)
@@ -474,20 +558,63 @@
@Override @Transactional(rollbackFor = Exception.class)
public void processApproveBatch(List<Long> ids, boolean approved, String opinion) {
- for (Long id : approvalIds(ids)) processApprove(id, approved, opinion);
+ List<Long> confirmed = approvalIds(ids);
+ List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByIdsForUpdate(confirmed);
+ validateLockedIds(confirmed, qualities);
+ if (qualities.stream().anyMatch(quality -> !Set.of(WAIT_PROCESS, WAIT_INITIAL_FAIL_REVIEW).contains(quality.getWorkflowStatus())))
+ throw new IllegalArgumentException("鎵�閫夎褰曞寘鍚笉鑳借繘琛屽垵妫�瀹℃牳鐨勭姸鎬侊紝璇峰埛鏂板悗閲嶈瘯");
+ approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(),
+ MesQcApproveNodeEnum.PROCESS.getNode(), getLoginUserId());
+ if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("椹冲洖鏃惰濉啓瀹℃壒鎰忚");
+ LocalDateTime now = LocalDateTime.now();
+ Long operatorId = getLoginUserId();
+ int next = approved ? WAIT_RETEST : WAIT_INITIAL_RECHECK;
+ qualityMapper.updateBatch(qualities.stream().map(quality -> MesQcSeedQualityDO.builder().id(quality.getId())
+ .workflowStatus(next).processApprovedAt(now).build()).toList(), WRITE_BATCH_SIZE);
+ actionMapper.insertBatch(qualities.stream().map(quality -> newAction(quality.getId(),
+ approved ? "PROCESS_APPROVE" : "PROCESS_REJECT", quality.getResult(), opinion, null, operatorId, now)).toList(),
+ WRITE_BATCH_SIZE);
}
@Override @Transactional(rollbackFor = Exception.class)
public void companyApproveBatch(List<Long> ids, boolean approved, String opinion) {
- for (Long id : approvalIds(ids)) companyApprove(id, approved, opinion);
+ List<Long> confirmed = approvalIds(ids);
+ List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByIdsForUpdate(confirmed);
+ validateLockedIds(confirmed, qualities);
+ if (qualities.stream().anyMatch(quality -> !Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(quality.getWorkflowStatus())))
+ throw new IllegalArgumentException("鎵�閫夎褰曞寘鍚笉鑳借繘琛屽妫�瀹℃牳鐨勭姸鎬侊紝璇峰埛鏂板悗閲嶈瘯");
+ approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(),
+ MesQcApproveNodeEnum.COMPANY.getNode(), getLoginUserId());
+ if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("椹冲洖鏃惰濉啓瀹℃壒鎰忚");
+ if (approved) {
+ for (MesQcSeedQualityDO quality : qualities) finalizeStock(quality, quality.getWorkflowStatus() == WAIT_COMPANY);
+ }
+ LocalDateTime now = LocalDateTime.now();
+ Long operatorId = getLoginUserId();
+ List<MesQcSeedQualityDO> updates = qualities.stream().map(quality -> MesQcSeedQualityDO.builder().id(quality.getId())
+ .workflowStatus(approved ? (quality.getWorkflowStatus() == WAIT_COMPANY ? RELEASED : SCRAPPED) : WAIT_RETEST)
+ .companyApprovedAt(now).build()).toList();
+ qualityMapper.updateBatch(updates, WRITE_BATCH_SIZE);
+ actionMapper.insertBatch(qualities.stream().map(quality -> newAction(quality.getId(),
+ approved ? "COMPANY_APPROVE" : "COMPANY_REJECT", quality.getResult(), opinion, null, operatorId, now)).toList(),
+ WRITE_BATCH_SIZE);
}
private List<Long> approvalIds(List<Long> ids) {
- if (ids == null || ids.isEmpty() || ids.size() > 500 || ids.stream().anyMatch(Objects::isNull))
- throw new IllegalArgumentException("璇烽�夋嫨 1 鑷� 500 鏉¤川妫�璁板綍");
+ return batchIds(ids, "璐ㄦ璁板綍");
+ }
+
+ private List<Long> batchIds(List<Long> ids, String label) {
+ if (ids == null || ids.isEmpty() || ids.size() > MAX_BATCH_SIZE || ids.stream().anyMatch(Objects::isNull))
+ throw new IllegalArgumentException("璇烽�夋嫨 1 鑷� " + MAX_BATCH_SIZE + " 鏉�" + label);
List<Long> sorted = ids.stream().distinct().sorted().toList();
- if (sorted.size() != ids.size()) throw new IllegalArgumentException("璐ㄦ璁板綍涓嶈兘閲嶅閫夋嫨");
+ if (sorted.size() != ids.size()) throw new IllegalArgumentException(label + "涓嶈兘閲嶅閫夋嫨");
return sorted;
+ }
+
+ private void validateLockedIds(List<Long> expectedIds, List<MesQcSeedQualityDO> qualities) {
+ List<Long> actualIds = qualities.stream().map(MesQcSeedQualityDO::getId).sorted().toList();
+ if (!expectedIds.equals(actualIds)) throw new IllegalArgumentException("璐ㄦ璁板綍涓嶅瓨鍦ㄦ垨宸插彉鍖栵紝璇峰埛鏂板悗閲嶈瘯");
}
private MesQcSeedQualityDO requireStatus(Long id, int expected) {
@@ -516,15 +643,21 @@
private void recordAction(Long id, String action, Integer result, String opinion,
List<MesQcSeedQualitySubmitReqVO.Indicator> indicators) {
+ actionMapper.insert(newAction(id, action, result, opinion, indicators, getLoginUserId(), LocalDateTime.now()));
+ }
+
+ private MesQcSeedQualityActionDO newAction(Long id, String action, Integer result, String opinion,
+ List<MesQcSeedQualitySubmitReqVO.Indicator> indicators,
+ Long operatorId, LocalDateTime createTime) {
MesQcSeedQualityActionDO event = new MesQcSeedQualityActionDO();
event.setQualityId(id);
event.setAction(action);
event.setResult(result);
event.setOpinion(opinion);
event.setIndicatorsJson(indicators == null ? null : JSONUtil.toJsonStr(indicators));
- event.setOperatorId(getLoginUserId());
- event.setCreateTime(LocalDateTime.now());
- actionMapper.insert(event);
+ event.setOperatorId(operatorId);
+ event.setCreateTime(createTime);
+ return event;
}
/**
diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityGroupTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityGroupTest.java
index 6ccfae7..5189d27 100644
--- a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityGroupTest.java
+++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityGroupTest.java
@@ -5,10 +5,22 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityIndicatorDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedQualityActionMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedQualityIndicatorMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedQualityMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper;
+import cn.iocoder.yudao.module.mes.enums.wm.MesWmWarehouseTypeEnum;
+import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
+import cn.iocoder.yudao.module.mes.service.qc.approver.MesQcApproverService;
+import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
import org.junit.jupiter.api.*;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
@@ -21,58 +33,104 @@
private final MesProBatchMapper batches=mock(MesProBatchMapper.class);
private final MesProPalletMapper pallets=mock(MesProPalletMapper.class);
private final MesQcSeedQualityMapper qualities=mock(MesQcSeedQualityMapper.class);
+ private final MesQcSeedQualityIndicatorMapper indicators=mock(MesQcSeedQualityIndicatorMapper.class);
+ private final MesQcSeedQualityActionMapper actions=mock(MesQcSeedQualityActionMapper.class);
+ private final MesProBatchService batchService=mock(MesProBatchService.class);
+ private final MesWmBatchMapper wmBatches=mock(MesWmBatchMapper.class);
+ private final MesWmMaterialStockMapper stocks=mock(MesWmMaterialStockMapper.class);
+ private final MesWmWarehouseService warehouses=mock(MesWmWarehouseService.class);
+ private final MesQcApproverService approvers=mock(MesQcApproverService.class);
private final MesProBagCodeDO first=bag(1L),second=bag(2L),canceled=bag(3L).setStatus(30);
private MesQcSeedQualityGroupSubmitReqVO req;
private MesProBagCodeDO bag(Long id) {return new MesProBagCodeDO().setId(id).setCode("B"+id).setBatchId(10L).setPalletId(20L).setStatus(20).setInboundEventId(9L);}
+ private List<MesQcSeedQualitySubmitReqVO.Indicator> validIndicators() {
+ return List.of(indicator("purity"), indicator("cleanliness"), indicator("germination_rate"), indicator("moisture"));
+ }
+ private MesQcSeedQualitySubmitReqVO.Indicator indicator(String code) {
+ var item = new MesQcSeedQualitySubmitReqVO.Indicator(); item.setCode(code);
+ item.setValue(java.math.BigDecimal.ONE); item.setMinValue(java.math.BigDecimal.ZERO); item.setMaxValue(java.math.BigDecimal.TEN);
+ return item;
+ }
+ private List<MesQcSeedQualityIndicatorDO> storedIndicators(Long... qualityIds) {
+ var rows = new java.util.ArrayList<MesQcSeedQualityIndicatorDO>(); long id = 100;
+ for (Long qualityId : qualityIds) for (var item : validIndicators()) rows.add(MesQcSeedQualityIndicatorDO.builder()
+ .id(id++).qualityId(qualityId).indicatorCode(item.getCode()).value(item.getValue())
+ .minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
+ return rows;
+ }
@BeforeEach void setup() {
ReflectionTestUtils.setField(service,"bagCodeMapper",bags);ReflectionTestUtils.setField(service,"batchMapper",batches);
ReflectionTestUtils.setField(service,"palletMapper",pallets);ReflectionTestUtils.setField(service,"qualityMapper",qualities);
- var batch=new MesProBatchDO().setId(10L).setCode("BATCH").setBusinessCode("BATCH-BUS").setStatus(30);
+ ReflectionTestUtils.setField(service,"indicatorMapper",indicators);ReflectionTestUtils.setField(service,"actionMapper",actions);
+ ReflectionTestUtils.setField(service,"proBatchService",batchService);ReflectionTestUtils.setField(service,"mesWmBatchMapper",wmBatches);
+ ReflectionTestUtils.setField(service,"materialStockMapper",stocks);ReflectionTestUtils.setField(service,"warehouseService",warehouses);
+ ReflectionTestUtils.setField(service,"approverService",approvers);
+ var batch=new MesProBatchDO().setId(10L).setCode("BATCH").setBusinessCode("BATCH-BUS").setItemId(99L).setStatus(30);
when(batches.selectByScanCode(anyString())).thenReturn(batch);when(batches.selectByScanCodeForUpdate(anyString())).thenReturn(batch);
+ when(batchService.getBatch(10L)).thenReturn(batch);
+ when(wmBatches.selectByCode("BATCH")).thenReturn(new MesWmBatchDO().setId(30L).setCode("BATCH"));
+ when(stocks.selectTempStockCountByBatchId(30L)).thenReturn(2L);
+ var warehouse = new MesWmWarehouseDO().setId(8L).setType(MesWmWarehouseTypeEnum.QUALIFIED.getType());
+ when(warehouses.getFirstQualifiedWarehouse()).thenReturn(warehouse); when(warehouses.getWarehouse(8L)).thenReturn(warehouse);
+ doAnswer(invocation -> { long id = 1000; for (var quality : (java.util.Collection<MesQcSeedQualityDO>) invocation.getArgument(0)) quality.setId(id++); return true; })
+ .when(qualities).insertBatch(anyCollection(), anyInt());
var pallet=new MesProPalletDO().setId(20L).setCode("PALLET").setBusinessCode("PALLET-BUS").setBatchCode("BATCH").setBatchId(10L).setStatus(10);
when(pallets.selectByScanCode(anyString())).thenReturn(pallet);when(pallets.selectByScanCodeForUpdate(anyString())).thenReturn(pallet);
when(bags.selectListByBatchId(10L)).thenReturn(List.of(first,second,canceled));
when(bags.selectListByBatchIdForUpdate(10L)).thenReturn(List.of(first,second,canceled));
when(bags.selectListByPalletId(20L)).thenReturn(List.of(first,second,canceled));
when(bags.selectListByPalletIdForUpdate(20L)).thenReturn(List.of(first,second,canceled));
- when(bags.selectListByIdsForUpdate(any())).thenReturn(List.of(first,second));
+ when(bags.selectListByIdsForUpdate(any())).thenAnswer(invocation -> {
+ java.util.Collection<Long> ids = invocation.getArgument(0);
+ return List.of(first, second).stream().filter(bag -> ids.contains(bag.getId())).toList();
+ });
req=new MesQcSeedQualityGroupSubmitReqVO();req.setCode("BATCH-BUS");req.setScanType("BATCH");req.setBagIds(List.of(1L,2L));
- req.setQualifiedWarehouseId(8L);req.setIndicators(List.of());
- doReturn(101L).when(service).submit(any(MesQcSeedQualitySubmitReqVO.class));
+ req.setQualifiedWarehouseId(8L);req.setIndicators(validIndicators());
}
@Test void batchPreviewCountsEffectiveInspectedAndPendingBags() {
when(qualities.selectListByCodes(any())).thenReturn(List.of(MesQcSeedQualityDO.builder().code("B1").build()));
var result=service.resolveScan("BATCH-BUS","BATCH");assertEquals(2,result.get("totalCount"));
assertEquals(List.of(1L),result.get("completedBagIds"));assertEquals(List.of(2L),result.get("pendingBagIds"));
- verify(service,never()).submit(any());
+ verify(qualities,never()).insertBatch(anyCollection(),anyInt());
}
@Test void palletProcessesOnlyConfirmedUninspectedBags() {
req.setScanType("PALLET");req.setCode("PALLET-BUS");req.setBagIds(List.of(2L));
- when(qualities.selectListByCodesForUpdate(any())).thenReturn(List.of(MesQcSeedQualityDO.builder().code("B1").build()));
+ when(qualities.selectListByCodes(any())).thenReturn(List.of(MesQcSeedQualityDO.builder().code("B1").build()));
+ when(qualities.selectListByCodesForUpdate(List.of("B2"))).thenReturn(List.of());
assertEquals(1,service.submitGroup(req).get("processedCount"));
- var capture=org.mockito.ArgumentCaptor.forClass(MesQcSeedQualitySubmitReqVO.class);verify(service).submit(capture.capture());
- assertEquals("B2",capture.getValue().getCode());assertEquals(8L,capture.getValue().getQualifiedWarehouseId());
+ verify(qualities).insertBatch(argThat(rows -> rows.size() == 1 && rows.iterator().next().getCode().equals("B2")
+ && rows.iterator().next().getQualifiedWarehouseId().equals(8L)),eq(100));
+ verify(indicators).insertBatch(argThat(rows -> rows.size() == 4),eq(400));
+ verify(actions).insertBatch(argThat(rows -> rows.size() == 1),eq(100));
}
@Test void changedConfirmedListDoesNotSubmitAnyBag() {
- req.setBagIds(List.of(1L));assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));verify(service,never()).submit(any());
+ req.setBagIds(List.of(1L));assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));verify(qualities,never()).insertBatch(anyCollection(),anyInt());
}
@Test void unenteredBagBlocksWholeGroup() {
- second.setInboundEventId(null);assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));verify(service,never()).submit(any());
+ second.setInboundEventId(null);assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));verify(qualities,never()).insertBatch(anyCollection(),anyInt());
}
@Test void wrongTypeAndCanceledBatchCannotSubmit() {
- req.setScanType("OTHER");assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));
- req.setScanType("BATCH");when(batches.selectByScanCodeForUpdate(anyString())).thenReturn(new MesProBatchDO().setId(10L).setCode("BATCH").setStatus(40));
- assertThrows(IllegalArgumentException.class,()->service.submitGroup(req));verify(service,never()).submit(any());
+ req.setScanType("OTHER");assertThrows(ServiceException.class,()->service.submitGroup(req));
+ req.setScanType("BATCH");when(batches.selectByScanCode(anyString())).thenReturn(new MesProBatchDO().setId(10L).setCode("BATCH").setStatus(40));
+ assertThrows(ServiceException.class,()->service.submitGroup(req));verify(qualities,never()).insertBatch(anyCollection(),anyInt());
+ }
+ @Test void unknownPalletReturnsActionableBadRequest() {
+ when(pallets.selectByScanCode("UNKNOWN")).thenReturn(null);
+ var error = assertThrows(ServiceException.class, () -> service.resolveRetestScope("UNKNOWN", "PALLET"));
+ assertEquals(400, error.getCode());
+ assertTrue(error.getMessage().contains("鎵樼洏鐮�"));
}
@Test void singleBagUsesSameConfirmedGroupProtocol() {
req.setScanType("BAG");req.setCode("B1");req.setBagIds(List.of(1L));
- when(bags.selectByScanCode("B1")).thenReturn(first);when(bags.selectListByIdsForUpdate(any())).thenReturn(List.of(first));
- assertEquals(1,service.submitGroup(req).get("processedCount"));verify(service).submit(any());
+ when(bags.selectByScanCode("B1")).thenReturn(first);
+ assertEquals(1,service.submitGroup(req).get("processedCount"));verify(qualities).insertBatch(anyCollection(),eq(100));
}
@Test void failingSecondBagRollsBackWholeTransaction() {
var source=new org.h2.jdbcx.JdbcDataSource();source.setURL("jdbc:h2:mem:qcgroup"+java.util.UUID.randomUUID()+";DB_CLOSE_DELAY=-1");
var jdbc=new org.springframework.jdbc.core.JdbcTemplate(source);jdbc.execute("CREATE TABLE qc_result(id BIGINT)");
- doAnswer(invocation->{var single=(MesQcSeedQualitySubmitReqVO)invocation.getArgument(0);jdbc.update("INSERT INTO qc_result(id) VALUES(?)",single.getCode().equals("B1")?1:2);if(single.getCode().equals("B2"))throw new IllegalStateException("搴撳瓨娴佽浆澶辫触");return 101L;}).when(service).submit(any());
+ doAnswer(invocation->{long id=1;for(var quality:(java.util.Collection<MesQcSeedQualityDO>)invocation.getArgument(0)){jdbc.update("INSERT INTO qc_result(id) VALUES(?)",id);quality.setId(id++);}return true;})
+ .when(qualities).insertBatch(anyCollection(),anyInt());
+ doThrow(new IllegalStateException("鏃ュ織鎵归噺鍐欏叆澶辫触")).when(actions).insertBatch(anyCollection(),anyInt());
var factory=new org.springframework.aop.framework.ProxyFactory(service);
factory.addAdvice(new org.springframework.transaction.interceptor.TransactionInterceptor(new org.springframework.jdbc.datasource.DataSourceTransactionManager(source),new org.springframework.transaction.annotation.AnnotationTransactionAttributeSource()));
var proxy=(MesQcSeedQualityService)factory.getProxy();
@@ -86,25 +144,57 @@
assertEquals(List.of(11L), result.get("retestIds"));
assertEquals(List.of(12L), result.get("initialRecheckIds"));
}
+ @Test void retestScopePreviewExposesPendingReviewGroups() {
+ var third = bag(3L); var fourth = bag(4L);
+ when(bags.selectListByPalletId(20L)).thenReturn(List.of(first, second, third, fourth));
+ when(qualities.selectListByCodes(any())).thenReturn(List.of(
+ MesQcSeedQualityDO.builder().id(11L).code("B1").workflowStatus(20).build(),
+ MesQcSeedQualityDO.builder().id(12L).code("B2").workflowStatus(25).build(),
+ MesQcSeedQualityDO.builder().id(13L).code("B3").workflowStatus(30).build(),
+ MesQcSeedQualityDO.builder().id(14L).code("B4").workflowStatus(60).build()));
+ var result = service.resolveRetestScope("PALLET-BUS", "PALLET");
+ assertEquals(List.of(11L), result.get("pendingProcessIds"));
+ assertEquals(List.of(12L), result.get("pendingInitialFailIds"));
+ assertEquals(List.of(13L), result.get("pendingCompanyIds"));
+ assertEquals(List.of(14L), result.get("pendingCompanyFailIds"));
+ assertEquals(2, result.get("pendingProcessCount"));
+ assertEquals(2, result.get("pendingCompanyCount"));
+ }
@Test void retestScopeRejectsStaleConfirmedList() {
- when(qualities.selectListByCodesForUpdate(any())).thenReturn(List.of(
+ when(qualities.selectListByCodes(any())).thenReturn(List.of(
MesQcSeedQualityDO.builder().id(11L).code("B1").workflowStatus(10).build(),
MesQcSeedQualityDO.builder().id(12L).code("B2").workflowStatus(10).build()));
var request = new MesQcSeedQualityScopeRetestReqVO();
request.setCode("BATCH-BUS"); request.setScanType("BATCH"); request.setWorkflowStatus(10);
- request.setQualityIds(List.of(11L)); request.setIndicators(List.of());
+ request.setQualityIds(List.of(11L)); request.setIndicators(validIndicators());
assertThrows(IllegalArgumentException.class, () -> service.retestScope(request));
- verify(service, never()).retest(anyLong(), any());
+ verify(indicators, never()).updateBatch(anyCollection(), anyInt());
}
@Test void retestScopeProcessesAllConfirmedBags() {
- when(qualities.selectListByCodesForUpdate(any())).thenReturn(List.of(
- MesQcSeedQualityDO.builder().id(11L).code("B1").workflowStatus(10).build(),
- MesQcSeedQualityDO.builder().id(12L).code("B2").workflowStatus(10).build()));
- doNothing().when(service).retest(anyLong(), any());
+ var pending = List.of(MesQcSeedQualityDO.builder().id(11L).code("B1").workflowStatus(10).build(),
+ MesQcSeedQualityDO.builder().id(12L).code("B2").workflowStatus(10).build());
+ when(qualities.selectListByCodes(any())).thenReturn(pending);
+ when(qualities.selectListByIdsForUpdate(List.of(11L, 12L))).thenReturn(pending);
+ when(indicators.selectListByQualityIds(any())).thenReturn(storedIndicators(11L, 12L));
var request = new MesQcSeedQualityScopeRetestReqVO();
request.setCode("BATCH-BUS"); request.setScanType("BATCH"); request.setWorkflowStatus(10);
- request.setQualityIds(List.of(11L, 12L)); request.setIndicators(List.of());
+ request.setQualityIds(List.of(11L, 12L)); request.setIndicators(validIndicators());
assertEquals(2, service.retestScope(request).get("processedCount"));
- verify(service, times(2)).retest(anyLong(), any());
+ verify(indicators).selectListByQualityIds(List.of(11L, 12L));
+ verify(qualities).updateBatch(argThat(rows -> rows.size() == 2), eq(100));
+ verify(indicators).updateBatch(argThat(rows -> rows.size() == 8), eq(400));
+ verify(actions).insertBatch(argThat(rows -> rows.size() == 2), eq(100));
+ }
+
+ @Test void processApprovalLocksAndWritesWholeBatchOnce() {
+ when(qualities.selectListByIdsForUpdate(List.of(11L, 12L))).thenReturn(List.of(
+ MesQcSeedQualityDO.builder().id(11L).result(1).workflowStatus(20).build(),
+ MesQcSeedQualityDO.builder().id(12L).result(2).workflowStatus(25).build()));
+ service.processApproveBatch(List.of(12L, 11L), true, "鎵归噺閫氳繃");
+ verify(qualities).selectListByIdsForUpdate(List.of(11L, 12L));
+ verify(approvers, times(1)).validateApprover(anyInt(), anyInt(), nullable(Long.class));
+ verify(qualities).updateBatch(argThat(rows -> rows.size() == 2 && rows.stream().allMatch(row -> row.getWorkflowStatus() == 10)), eq(100));
+ verify(actions).insertBatch(argThat(rows -> rows.size() == 2), eq(100));
+ verify(qualities, never()).selectByIdForUpdate(anyLong());
}
}
--
Gitblit v1.9.3