diff --git a/cf-module-prod-executor/cf-module-prod-executor-api/src/main/java/com/cf/imes/module/executor/enums/ErrorCodeConstants.java b/cf-module-prod-executor/cf-module-prod-executor-api/src/main/java/com/cf/imes/module/executor/enums/ErrorCodeConstants.java index 27b1dc585..590617031 100644 --- a/cf-module-prod-executor/cf-module-prod-executor-api/src/main/java/com/cf/imes/module/executor/enums/ErrorCodeConstants.java +++ b/cf-module-prod-executor/cf-module-prod-executor-api/src/main/java/com/cf/imes/module/executor/enums/ErrorCodeConstants.java @@ -148,6 +148,12 @@ public class ErrorCodeConstants { public static final ErrorCode PLATE_IS_OPTIMIZE = new ErrorCode(1_003_007_033, "plate.is.optimize"); public static final ErrorCode PACK_DATA_NULL = new ErrorCode(1_003_007_034, "pack.data.null"); public static final ErrorCode ORDER_PLAN_CREATE_PLATE_NUM_REACH_THRESHOLD_ERROR = new ErrorCode(1_003_007_036, "order.plan.create.plate.num.reach.threshold.error"); + public static final ErrorCode PLAN_PLATE_INSERT_DATA_ERROR = new ErrorCode(1_003_007_037, "plan.plate.insert.data.error"); + public static final ErrorCode PLAN_PLATE_MATERIAL_NOT_MATCH = new ErrorCode(1_003_007_038, "plan.plate.material.not.match"); + public static final ErrorCode PLAN_PLATE_THICKNESS_NOT_MATCH = new ErrorCode(1_003_007_039, "plan.plate.thickness.not.match"); + public static final ErrorCode PLAN_PLATE_BRAND_NOT_MATCH = new ErrorCode(1_003_007_040, "plan.plate.brand.not.match"); + public static final ErrorCode PLAN_PLATE_COLOR_NOT_MATCH = new ErrorCode(1_003_007_041, "plan.plate.color.not.match"); + public static final ErrorCode PLAN_PLATE_TEXTURE_NOT_MATCH = new ErrorCode(1_003_007_042, "plan.plate.texture.not.match"); //=========== 板材信息 1-003-008-000 ============ public static final ErrorCode PLATE_EXISTS = new ErrorCode(1_003_008_000, "plate.exists"); diff --git a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/service/plan/PlanServiceImpl.java b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/service/plan/PlanServiceImpl.java index 3c66edf3f..7bc52d8e8 100644 --- a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/service/plan/PlanServiceImpl.java +++ b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/service/plan/PlanServiceImpl.java @@ -308,16 +308,13 @@ public class PlanServiceImpl implements PlanService { return; } - // 添加小板 List insertOrderPlateIds = Optional.ofNullable(updateReqVO.getInsertOrderPlateIds()).orElse(new ArrayList<>()); - PlanActualGoodsModelDO planActualGoodsModelDO = updateReqVO.getPlanActualGoodsModelDO(); - if (CollUtil.isNotEmpty(insertOrderPlateIds)) { PlanServiceImpl p = (PlanServiceImpl) AopContext.currentProxy(); - p.insertPlanPlate(insertOrderPlateIds, planActualGoodsModelDO); + p.insertPlanPlate(insertOrderPlateIds); } @@ -1401,24 +1398,19 @@ public class PlanServiceImpl implements PlanService { @Transactional(rollbackFor = Exception.class) - public void insertPlanPlate(List insertOrderPlateIds, PlanActualGoodsModelDO planActualGoodsModelDO) { - - // 后面还需要看看是否需要进行混单的判断 + public void insertPlanPlate(List insertOrderPlateIds) { List insertPlanIds = insertOrderPlateIds.stream().map(OrderPlateIds::getPlanId).distinct().toList(); + if (CollUtil.isEmpty(insertPlanIds)) { + throw new ServiceException(PLAN_PLATE_INSERT_DATA_ERROR); + } + PlanDO planDO = validatePlan(insertPlanIds.get(0)); - Set orderIds = insertOrderPlateIds.stream().map(OrderPlateIds::getOrderId).collect(Collectors.toSet()); - - if (planActualGoodsModelDO != null) { - - List mixedTypes = insertOrderPlateIds.stream().map(OrderPlateIds::getMixedType).distinct().toList(); - - Integer mixedType = mixedTypes.get(0) == null ? 0 : mixedTypes.get(0); - - planDO.setType(PlanTypeEnum.MIXEDORDERPLAN.getType()); - planDO.setFilter(mixedType); - } + // 添加小板时,排单模式和混单规则只能以数据库中的排单数据为准。 + // 前端的 mixedType、planActualGoodsModelDO 不参与模式判定,避免添加小板时篡改既有排单规则。 + List validatedOrderPlateIds = validateInsertPlanPlates(planDO, insertOrderPlateIds); + Set orderIds = validatedOrderPlateIds.stream().map(OrderPlateIds::getOrderId).collect(Collectors.toSet()); // 已开料排单修改为开料中 if (planDO.getStatus().equals(PlanStatusEnum.OPENED.getStatus())) { @@ -1444,16 +1436,153 @@ public class PlanServiceImpl implements PlanService { ); // 新增排单板件 & 重新计算订单和材质下的数量和面积 - insertPlanOrderAndGoodsPlate(planDO.getId(), insertOrderPlateIds, orderIds); + insertPlanOrderAndGoodsPlate(planDO.getId(), validatedOrderPlateIds, orderIds); List sourceBlockIds = getSourceBlockIds( - insertOrderPlateIds.stream().map(OrderPlateIds::getPlateId).toList()); + validatedOrderPlateIds.stream().map(OrderPlateIds::getPlateId).toList()); if (CollUtil.isNotEmpty(sourceBlockIds)) { appendProducingPlanBlocks(planDO, sourceBlockIds); } } + /** + * 校验待添加小板的归属、未排单状态和材质规则,并将统计使用的订单、订单商品 ID 规范化为数据库值。 + */ + private List validateInsertPlanPlates(PlanDO planDO, List insertOrderPlateIds) { + List plateIds = insertOrderPlateIds.stream().map(OrderPlateIds::getPlateId).toList(); + if (CollUtil.isEmpty(plateIds) || plateIds.stream().anyMatch(Objects::isNull) + || plateIds.stream().distinct().count() != plateIds.size()) { + throw new ServiceException(PLAN_PLATE_INSERT_DATA_ERROR); + } + + Long organId = getUserOrganId(); + Map requestByPlateId = insertOrderPlateIds.stream() + .collect(Collectors.toMap(OrderPlateIds::getPlateId, Function.identity())); + List plates = plateMapper.selectByIds(plateIds); + if (plates.size() != plateIds.size()) { + throw new ServiceException(PLAN_PLATE_INSERT_DATA_ERROR); + } + + Map plateById = plates.stream() + .collect(Collectors.toMap(PlateDO::getId, Function.identity())); + List orderGoodsIds = new ArrayList<>(); + for (Long plateId : plateIds) { + PlateDO plate = plateById.get(plateId); + OrderPlateIds request = requestByPlateId.get(plateId); + if (plate == null + || !ObjectUtil.equals(plate.getOrganId(), organId) + || !ObjectUtil.equals(plate.getPlanId(), 0L) + || !ObjectUtil.equals(plate.getOrderId(), request.getOrderId()) + || !ObjectUtil.equals(plate.getGoodsId(), request.getId())) { + throw new ServiceException(PLAN_PLATE_INSERT_DATA_ERROR); + } + orderGoodsIds.add(plate.getGoodsId()); + } + + List candidateGoods = goodsMapper.selectBatchIds(orderGoodsIds.stream().distinct().toList()); + if (candidateGoods.size() != orderGoodsIds.stream().distinct().count()) { + throw new ServiceException(PLAN_PLATE_INSERT_DATA_ERROR); + } + Map goodsById = candidateGoods.stream() + .collect(Collectors.toMap(GoodsDO::getId, Function.identity())); + for (PlateDO plate : plates) { + GoodsDO goods = goodsById.get(plate.getGoodsId()); + if (goods == null + || Boolean.TRUE.equals(goods.getDeleted()) + || !ObjectUtil.equals(goods.getOrganId(), organId) + || !ObjectUtil.equals(goods.getOrderId(), plate.getOrderId())) { + throw new ServiceException(PLAN_PLATE_INSERT_DATA_ERROR); + } + } + + List planGoods = planMapper.selectPlanGoodsList(planDO.getId(), organId); + if (CollUtil.isEmpty(planGoods)) { + throw new ServiceException(PLAN_PLATE_DATA_ERROR); + } + for (PlateDO plate : plates) { + GoodsDO goods = goodsById.get(plate.getGoodsId()); + if (!matchesPlanMaterialRule(planDO, planGoods, goods)) { + throwPlanMaterialMismatch(planDO, planGoods, plate, goods); + } + } + + return plates.stream().map(plate -> { + OrderPlateIds item = new OrderPlateIds(); + item.setPlanId(planDO.getId()); + item.setPlateId(plate.getId()); + item.setOrderId(plate.getOrderId()); + item.setId(plate.getGoodsId()); + return item; + }).toList(); + } + + /** + * 按排单数据库中保存的模式校验候选小板材质,规则与未排单小板查询接口一致。 + */ + private boolean matchesPlanMaterialRule(PlanDO planDO, List planGoods, GoodsDO candidateGoods) { + Set materials = planGoods.stream().map(OrderGoodsResp::getMaterial).collect(Collectors.toSet()); + Set thicknesses = planGoods.stream().map(OrderGoodsResp::getThickness).collect(Collectors.toSet()); + if (PlanTypeEnum.MIXEDORDERPLAN.equals(planDO.getType())) { + if (MixConfigTypeEnum.SAMEMATERIALANDTHICKNESS.equals(planDO.getFilter())) { + return materials.contains(candidateGoods.getMaterial()) && thicknesses.contains(candidateGoods.getThickness()); + } + return thicknesses.contains(candidateGoods.getThickness()); + } + + Set brands = planGoods.stream().map(OrderGoodsResp::getBrand).collect(Collectors.toSet()); + Set colors = planGoods.stream().map(OrderGoodsResp::getColor).collect(Collectors.toSet()); + Set textures = planGoods.stream().map(OrderGoodsResp::getTexture).collect(Collectors.toSet()); + return materials.contains(candidateGoods.getMaterial()) + && thicknesses.contains(candidateGoods.getThickness()) + && brands.contains(candidateGoods.getBrand()) + && colors.contains(candidateGoods.getColor()) + && textures.contains(candidateGoods.getTexture()); + } + + /** + * 抛出带小板编号、当前属性和值域的国际化材质校验异常。 + */ + private void throwPlanMaterialMismatch(PlanDO planDO, List planGoods, + PlateDO plate, GoodsDO candidateGoods) { + String plateNo = StringUtils.defaultIfBlank(plate.getCustomPlateNo(), + StringUtils.defaultIfBlank(plate.getPlateNo(), String.valueOf(plate.getId()))); + Set materials = planGoods.stream().map(OrderGoodsResp::getMaterial).collect(Collectors.toSet()); + Set thicknesses = planGoods.stream().map(OrderGoodsResp::getThickness).collect(Collectors.toSet()); + if (!materials.contains(candidateGoods.getMaterial()) + && (!PlanTypeEnum.MIXEDORDERPLAN.equals(planDO.getType()) + || MixConfigTypeEnum.SAMEMATERIALANDTHICKNESS.equals(planDO.getFilter()))) { + throw new ServiceException(PLAN_PLATE_MATERIAL_NOT_MATCH, plateNo, + candidateGoods.getMaterial(), joinPlanMaterialValues(materials)); + } + if (!thicknesses.contains(candidateGoods.getThickness())) { + throw new ServiceException(PLAN_PLATE_THICKNESS_NOT_MATCH, plateNo, + candidateGoods.getThickness(), joinPlanMaterialValues(thicknesses)); + } + if (!PlanTypeEnum.MIXEDORDERPLAN.equals(planDO.getType())) { + Set brands = planGoods.stream().map(OrderGoodsResp::getBrand).collect(Collectors.toSet()); + if (!brands.contains(candidateGoods.getBrand())) { + throw new ServiceException(PLAN_PLATE_BRAND_NOT_MATCH, plateNo, + candidateGoods.getBrand(), joinPlanMaterialValues(brands)); + } + Set colors = planGoods.stream().map(OrderGoodsResp::getColor).collect(Collectors.toSet()); + if (!colors.contains(candidateGoods.getColor())) { + throw new ServiceException(PLAN_PLATE_COLOR_NOT_MATCH, plateNo, + candidateGoods.getColor(), joinPlanMaterialValues(colors)); + } + Set textures = planGoods.stream().map(OrderGoodsResp::getTexture).collect(Collectors.toSet()); + if (!textures.contains(candidateGoods.getTexture())) { + throw new ServiceException(PLAN_PLATE_TEXTURE_NOT_MATCH, plateNo, + candidateGoods.getTexture(), joinPlanMaterialValues(textures)); + } + } + throw new ServiceException(PLAN_PLATE_INSERT_DATA_ERROR); + } + + private String joinPlanMaterialValues(Collection values) { + return values.stream().map(String::valueOf).sorted().collect(Collectors.joining(", ")); + } + /** * 新增排单板件 & 重新计算订单和材质下的数量和面积 * @param planId diff --git a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/message_errorcode_cht.properties b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/message_errorcode_cht.properties index 93fa51919..31fd367c9 100644 --- a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/message_errorcode_cht.properties +++ b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/message_errorcode_cht.properties @@ -53,6 +53,12 @@ advanced.screening.config.error=當前篩選配置有誤,請重新設定 create.plan.plate.part.planned=當前選擇部分小板已排單,請重新查詢未排單資料後再操作 create.plan.plate.empty=當前排單未選擇板材資料,請先選擇 del.plan.plate.data.error=刪除的小板資料有誤,請檢查或重新建立排單 +plan.plate.insert.data.error=待新增小板資料異常,請重新整理後再選擇 +plan.plate.material.not.match=小板 {0} 的材質為「{1}」,不在當前排單允許的材質範圍「{2}」內,無法加入排單 +plan.plate.thickness.not.match=小板 {0} 的厚度為「{1}」,不在當前排單允許的厚度範圍「{2}」內,無法加入排單 +plan.plate.brand.not.match=小板 {0} 的品牌為「{1}」,不在當前排單允許的品牌範圍「{2}」內,無法加入排單 +plan.plate.color.not.match=小板 {0} 的顏色為「{1}」,不在當前排單允許的顏色範圍「{2}」內,無法加入排單 +plan.plate.texture.not.match=小板 {0} 的紋理屬性為「{1}」,不在當前排單允許的紋理範圍「{2}」內,無法加入排單 plan.process.config.is.null=排單:{0} 對應方案組為空,請先配置 plan.process.config.all.is.null=當前排單所有方案組配置均為空,請重新設定 plan.process.scheme.is.lock=當前排單已鎖定,無法修改方案組,請先解鎖 diff --git a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/message_errorcode_en.properties b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/message_errorcode_en.properties index 17cfe3827..af3a997da 100644 --- a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/message_errorcode_en.properties +++ b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/message_errorcode_en.properties @@ -53,6 +53,12 @@ advanced.screening.config.error=Invalid filtering configuration, please reconfig create.plan.plate.part.planned=Some selected plates are already planned, please query unplanned data again create.plan.plate.empty=No plates selected for planning del.plan.plate.data.error=Invalid plate data for deletion, please check or recreate plan +plan.plate.insert.data.error=Invalid plate data to add. Please refresh and select again +plan.plate.material.not.match=Plate {0} has material "{1}", which is not allowed by the current plan (allowed: "{2}") +plan.plate.thickness.not.match=Plate {0} has thickness "{1}", which is not allowed by the current plan (allowed: "{2}") +plan.plate.brand.not.match=Plate {0} has brand "{1}", which is not allowed by the current plan (allowed: "{2}") +plan.plate.color.not.match=Plate {0} has color "{1}", which is not allowed by the current plan (allowed: "{2}") +plan.plate.texture.not.match=Plate {0} has texture "{1}", which is not allowed by the current plan (allowed: "{2}") plan.process.config.is.null=Plan {0} has no process group configured plan.process.config.all.is.null=All process group configurations are empty, please reconfigure plan.process.scheme.is.lock=Plan is locked and cannot be modified, please unlock first diff --git a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/message_errorcode_zh.properties b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/message_errorcode_zh.properties index 5f188bd00..6e03ccfaa 100644 --- a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/message_errorcode_zh.properties +++ b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/message_errorcode_zh.properties @@ -53,6 +53,12 @@ advanced.screening.config.error=当前筛选的配置有误,请重新进行配 create.plan.plate.part.planned=当前选择部分小板已排单,请重新查询未排单信息后再次创建排单 create.plan.plate.empty=当前排单没有选择板材数据,请进行选择 del.plan.plate.data.error=当前删除的小板信息有误,请检查小板是否存在或重新创建排单 +plan.plate.insert.data.error=待添加小板数据异常,请刷新后重新选择 +plan.plate.material.not.match=小板 {0} 的材质为“{1}”,不在当前排单允许的材质范围“{2}”内,无法加入排单 +plan.plate.thickness.not.match=小板 {0} 的厚度为“{1}”,不在当前排单允许的厚度范围“{2}”内,无法加入排单 +plan.plate.brand.not.match=小板 {0} 的品牌为“{1}”,不在当前排单允许的品牌范围“{2}”内,无法加入排单 +plan.plate.color.not.match=小板 {0} 的颜色为“{1}”,不在当前排单允许的颜色范围“{2}”内,无法加入排单 +plan.plate.texture.not.match=小板 {0} 的纹理属性为“{1}”,不在当前排单允许的纹理范围“{2}”内,无法加入排单 plan.process.config.is.null=排单:{0} 对应的方案组为空,请进行配置 plan.process.config.all.is.null=当前所选排单对应的方案组配置均为空,请重新进行方案组配置 plan.process.scheme.is.lock=当前排单已被锁定,无法修改方案组,请解锁后再进行修改 diff --git a/cf-module-prod-executor/cf-module-prod-executor-biz/src/test/java/com/cf/imes/module/executor/service/plan/PlanServiceImplInsertMaterialRuleTest.java b/cf-module-prod-executor/cf-module-prod-executor-biz/src/test/java/com/cf/imes/module/executor/service/plan/PlanServiceImplInsertMaterialRuleTest.java new file mode 100644 index 000000000..b92935a31 --- /dev/null +++ b/cf-module-prod-executor/cf-module-prod-executor-biz/src/test/java/com/cf/imes/module/executor/service/plan/PlanServiceImplInsertMaterialRuleTest.java @@ -0,0 +1,76 @@ +package com.cf.imes.module.executor.service.plan; + +import com.cf.imes.framework.common.enums.MixConfigTypeEnum; +import com.cf.imes.framework.common.enums.PlanTypeEnum; +import com.cf.imes.module.executor.controller.admin.plan.vo.OrderGoodsResp; +import com.cf.imes.module.executor.dal.dataobject.goods.GoodsDO; +import com.cf.imes.module.executor.dal.dataobject.plan.PlanDO; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.math.BigDecimal; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class PlanServiceImplInsertMaterialRuleTest { + + private final PlanServiceImpl planService = new PlanServiceImpl(); + + @Test + void shouldRequireAllMaterialAttributesForOrderPlan() { + PlanDO plan = PlanDO.builder().type(PlanTypeEnum.ORDERPLAN.getType()).build(); + OrderGoodsResp expected = planGoods("多层板", "黑胡桃", "ABC", true, "9"); + + assertThat(matches(plan, List.of(expected), goods("多层板", "黑胡桃", "ABC", true, "9"))).isTrue(); + assertThat(matches(plan, List.of(expected), goods("多层板", "白橡", "ABC", true, "9"))).isFalse(); + } + + @Test + void shouldRequireOnlyMaterialAndThicknessForSameMaterialThicknessMixedPlan() { + PlanDO plan = PlanDO.builder() + .type(PlanTypeEnum.MIXEDORDERPLAN.getType()) + .filter(MixConfigTypeEnum.SAMEMATERIALANDTHICKNESS.getType()) + .build(); + OrderGoodsResp expected = planGoods("多层板", "黑胡桃", "ABC", true, "9"); + + assertThat(matches(plan, List.of(expected), goods("多层板", "白橡", "其他品牌", false, "9"))).isTrue(); + assertThat(matches(plan, List.of(expected), goods("颗粒板", "黑胡桃", "ABC", true, "9"))).isFalse(); + } + + @Test + void shouldRequireOnlyThicknessForSameThicknessMixedPlan() { + PlanDO plan = PlanDO.builder() + .type(PlanTypeEnum.MIXEDORDERPLAN.getType()) + .filter(MixConfigTypeEnum.SAMETHICKNESS.getType()) + .build(); + OrderGoodsResp expected = planGoods("多层板", "黑胡桃", "ABC", true, "9"); + + assertThat(matches(plan, List.of(expected), goods("颗粒板", "白橡", "其他品牌", false, "9"))).isTrue(); + assertThat(matches(plan, List.of(expected), goods("颗粒板", "白橡", "其他品牌", false, "18"))).isFalse(); + } + + private boolean matches(PlanDO plan, List planGoods, GoodsDO candidate) { + return ReflectionTestUtils.invokeMethod(planService, "matchesPlanMaterialRule", plan, planGoods, candidate); + } + + private OrderGoodsResp planGoods(String material, String color, String brand, Boolean texture, String thickness) { + OrderGoodsResp goods = new OrderGoodsResp(); + goods.setMaterial(material); + goods.setColor(color); + goods.setBrand(brand); + goods.setTexture(texture); + goods.setThickness(new BigDecimal(thickness)); + return goods; + } + + private GoodsDO goods(String material, String color, String brand, Boolean texture, String thickness) { + GoodsDO goods = new GoodsDO(); + goods.setMaterial(material); + goods.setColor(color); + goods.setBrand(brand); + goods.setTexture(texture); + goods.setThickness(new BigDecimal(thickness)); + return goods; + } +}