添加小板增加板件材质校验规则

This commit is contained in:
gaoqr
2026-08-06 15:44:41 +08:00
parent 9adbf11a49
commit c3dc744c27
6 changed files with 249 additions and 20 deletions
@@ -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");
@@ -308,16 +308,13 @@ public class PlanServiceImpl implements PlanService {
return;
}
// 添加小板
List<OrderPlateIds> 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<OrderPlateIds> insertOrderPlateIds, PlanActualGoodsModelDO planActualGoodsModelDO) {
// 后面还需要看看是否需要进行混单的判断
public void insertPlanPlate(List<OrderPlateIds> insertOrderPlateIds) {
List<Long> 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<Long> orderIds = insertOrderPlateIds.stream().map(OrderPlateIds::getOrderId).collect(Collectors.toSet());
if (planActualGoodsModelDO != null) {
List<Integer> 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<OrderPlateIds> validatedOrderPlateIds = validateInsertPlanPlates(planDO, insertOrderPlateIds);
Set<Long> 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<String> sourceBlockIds = getSourceBlockIds(
insertOrderPlateIds.stream().map(OrderPlateIds::getPlateId).toList());
validatedOrderPlateIds.stream().map(OrderPlateIds::getPlateId).toList());
if (CollUtil.isNotEmpty(sourceBlockIds)) {
appendProducingPlanBlocks(planDO, sourceBlockIds);
}
}
/**
* 校验待添加小板的归属、未排单状态和材质规则,并将统计使用的订单、订单商品 ID 规范化为数据库值。
*/
private List<OrderPlateIds> validateInsertPlanPlates(PlanDO planDO, List<OrderPlateIds> insertOrderPlateIds) {
List<Long> 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<Long, OrderPlateIds> requestByPlateId = insertOrderPlateIds.stream()
.collect(Collectors.toMap(OrderPlateIds::getPlateId, Function.identity()));
List<PlateDO> plates = plateMapper.selectByIds(plateIds);
if (plates.size() != plateIds.size()) {
throw new ServiceException(PLAN_PLATE_INSERT_DATA_ERROR);
}
Map<Long, PlateDO> plateById = plates.stream()
.collect(Collectors.toMap(PlateDO::getId, Function.identity()));
List<Long> 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<GoodsDO> candidateGoods = goodsMapper.selectBatchIds(orderGoodsIds.stream().distinct().toList());
if (candidateGoods.size() != orderGoodsIds.stream().distinct().count()) {
throw new ServiceException(PLAN_PLATE_INSERT_DATA_ERROR);
}
Map<Long, GoodsDO> 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<OrderGoodsResp> 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<OrderGoodsResp> planGoods, GoodsDO candidateGoods) {
Set<String> materials = planGoods.stream().map(OrderGoodsResp::getMaterial).collect(Collectors.toSet());
Set<BigDecimal> 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<String> brands = planGoods.stream().map(OrderGoodsResp::getBrand).collect(Collectors.toSet());
Set<String> colors = planGoods.stream().map(OrderGoodsResp::getColor).collect(Collectors.toSet());
Set<Boolean> 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<OrderGoodsResp> planGoods,
PlateDO plate, GoodsDO candidateGoods) {
String plateNo = StringUtils.defaultIfBlank(plate.getCustomPlateNo(),
StringUtils.defaultIfBlank(plate.getPlateNo(), String.valueOf(plate.getId())));
Set<String> materials = planGoods.stream().map(OrderGoodsResp::getMaterial).collect(Collectors.toSet());
Set<BigDecimal> 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<String> 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<String> 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<Boolean> 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
@@ -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=當前排單已鎖定,無法修改方案組,請先解鎖
@@ -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
@@ -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=当前排单已被锁定,无法修改方案组,请解锁后再进行修改
@@ -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<OrderGoodsResp> 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;
}
}