feat(validation): 增加字段长度限制和必填校验

在多个请求对象中添加了字段长度限制和必填校验,以确保数据的有效性和一致性。具体修改包括:
- MesTmToolTypeSaveReqVO 和 MesTmToolSaveReqVO 中的 code 和 name 字段增加长度限制
- MesWmItemConsumeLinePageReqVO 中的 feedbackId 字段增加必填校验
- MesMdWorkstationToolService 中新增获取指定工具类型工装夹具资源数量的方法
- ErrorCodeConstants 中新增工具类型被工作站工装资源引用的错误码

这些改动旨在提升数据的完整性和系统的健壮性
This commit is contained in:
YunaiV
2026-04-09 22:08:47 +08:00
parent 28d0cfd08e
commit dc0874b76c
15 changed files with 233 additions and 17 deletions

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.mes.controller.admin.tm.tool.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDateTime;
@ -16,16 +17,20 @@ public class MesTmToolSaveReqVO {
@Schema(description = "工具编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "T-001")
@NotEmpty(message = "工具编码不能为空")
@Size(max = 64, message = "工具编码长度不能超过 64 个字符")
private String code;
@Schema(description = "工具名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "5mm 铣刀")
@NotEmpty(message = "工具名称不能为空")
@Size(max = 255, message = "工具名称长度不能超过 255 个字符")
private String name;
@Schema(description = "品牌", example = "三菱")
@Size(max = 255, message = "品牌长度不能超过 255 个字符")
private String brand;
@Schema(description = "型号规格", example = "M5-100")
@Size(max = 255, message = "型号规格长度不能超过 255 个字符")
private String spec;
@Schema(description = "工具类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@ -36,7 +41,8 @@ public class MesTmToolSaveReqVO {
@NotNull(message = "数量不能为空")
private Integer quantity;
@Schema(description = "可用数量", example = "1")
@Schema(description = "可用数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "可用数量不能为空")
private Integer availableQuantity;
@Schema(description = "保养维护类型", example = "1")
@ -53,6 +59,7 @@ public class MesTmToolSaveReqVO {
private Integer status;
@Schema(description = "备注", example = "备注")
@Size(max = 500, message = "备注长度不能超过 500 个字符")
private String remark;
}
}

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.mes.controller.admin.tm.tool.vo.type;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - MES 工具类型新增/修改 Request VO")
@ -14,10 +15,12 @@ public class MesTmToolTypeSaveReqVO {
@Schema(description = "类型编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "TT-001")
@NotEmpty(message = "类型编码不能为空")
@Size(max = 64, message = "类型编码长度不能超过 64 个字符")
private String code;
@Schema(description = "类型名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "铣刀")
@NotEmpty(message = "类型名称不能为空")
@Size(max = 255, message = "类型名称长度不能超过 255 个字符")
private String name;
@Schema(description = "是否编码管理", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@ -31,6 +34,7 @@ public class MesTmToolTypeSaveReqVO {
private Integer maintenPeriod;
@Schema(description = "备注", example = "备注")
@Size(max = 500, message = "备注长度不能超过 500 个字符")
private String remark;
}
}

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.mes.controller.admin.wm.itemconsume.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ -13,6 +14,7 @@ import lombok.ToString;
public class MesWmItemConsumeLinePageReqVO extends PageParam {
@Schema(description = "报工记录编号", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "报工记录编号不能为空")
private Long feedbackId;
}

View File

@ -29,4 +29,8 @@ public interface MesMdWorkstationToolMapper extends BaseMapperX<MesMdWorkstation
delete(MesMdWorkstationToolDO::getWorkstationId, workstationId);
}
default Long selectCountByToolTypeId(Long toolTypeId) {
return selectCount(MesMdWorkstationToolDO::getToolTypeId, toolTypeId);
}
}

View File

@ -217,6 +217,7 @@ public interface ErrorCodeConstants {
ErrorCode TM_TOOL_TYPE_CODE_DUPLICATE = new ErrorCode(1_040_400_001, "工具类型编码已存在");
ErrorCode TM_TOOL_TYPE_NAME_DUPLICATE = new ErrorCode(1_040_400_002, "工具类型名称已存在");
ErrorCode TM_TOOL_TYPE_HAS_TOOL = new ErrorCode(1_040_400_003, "该工具类型下存在工具,无法删除");
ErrorCode TM_TOOL_TYPE_HAS_WORKSTATION_TOOL = new ErrorCode(1_040_400_004, "该工具类型已被工作站工装资源引用,无法删除");
// ========== MES 工具管理-工具台账1-040-401-000 ==========
ErrorCode TM_TOOL_NOT_EXISTS = new ErrorCode(1_040_401_000, "工具不存在");
@ -362,6 +363,7 @@ public interface ErrorCodeConstants {
ErrorCode QC_OQC_QUANTITY_MISMATCH = new ErrorCode(1_040_607_004, "合格品与不合格品数量之和须等于检测数量");
ErrorCode QC_OQC_NO_TEMPLATE = new ErrorCode(1_040_607_005, "当前产品未配置 OQC 检测模板");
ErrorCode QC_OQC_CHECK_RESULT_EMPTY = new ErrorCode(1_040_607_006, "完成检验单前,检测结果必须填写");
// ========== MES 质量管理-出货检验行1-040-607-100 ==========
ErrorCode QC_OQC_LINE_NOT_EXISTS = new ErrorCode(1_040_607_100, "出货检验行不存在");

View File

@ -50,4 +50,12 @@ public interface MesMdWorkstationToolService {
*/
void deleteWorkstationToolByWorkstationId(Long workstationId);
/**
* 获得指定工具类型的工装夹具资源数量
*
* @param toolTypeId 工具类型编号
* @return 数量
*/
Long getWorkstationToolCountByToolTypeId(Long toolTypeId);
}

View File

@ -89,4 +89,9 @@ public class MesMdWorkstationToolServiceImpl implements MesMdWorkstationToolServ
workstationToolMapper.deleteByWorkstationId(workstationId);
}
@Override
public Long getWorkstationToolCountByToolTypeId(Long toolTypeId) {
return workstationToolMapper.selectCountByToolTypeId(toolTypeId);
}
}

View File

@ -82,11 +82,14 @@ public class MesQcIndicatorResultServiceImpl implements MesQcIndicatorResultServ
@Override
@Transactional(rollbackFor = Exception.class)
public void updateIndicatorResult(MesQcIndicatorResultSaveReqVO updateReqVO) {
// 1. 校验存在
// 1.1 校验存在
validateIndicatorResultExists(updateReqVO.getId());
// 1.2 校验所有明细的 indicatorId 是否存在
validateIndicatorIds(updateReqVO.getItems());
// 2.1 更新主表
// 2.1 更新主表(锁定 qcId/qcType/itemId不允许改挂到其他质检单
MesQcIndicatorResultDO updateObj = BeanUtils.toBean(updateReqVO, MesQcIndicatorResultDO.class);
updateObj.setQcId(null).setQcType(null).setItemId(null);
resultMapper.updateById(updateObj);
// 2.2 批量更新明细
List<MesQcIndicatorResultDetailDO> details = BeanUtils.toBean(updateReqVO.getItems(),

View File

@ -283,7 +283,7 @@ public class MesQcIpqcServiceImpl implements MesQcIpqcService {
* 推导工序 ID
*
* <p>通过产品 → 工艺路线产品 → 工艺路线 → 校验工位工序是否在该路线中
* <p>如果工位的工序在路线中,则返回该 processId否则返回 null
* <p>如果工位的工序在路线中,则返回该 processId否则返回 null(与 KTG 保持一致)
*
* @param productId 产品 ID工单产品
* @param workstationProcessId 工位关联的工序 ID

View File

@ -221,8 +221,8 @@ public class MesQcOqcServiceImpl implements MesQcOqcService {
return null;
}
if (Objects.equals(sourceDocType, MesBizTypeConstants.WM_PRODUCT_SALES)) {
MesWmProductSalesLineDO salesLine = productSalesLineService.getProductSalesLine(sourceLineId);
if (salesLine != null && salesLine.getSalesId() != null) {
MesWmProductSalesLineDO salesLine = productSalesLineService.validateProductSalesLineExists(sourceLineId);
if (salesLine.getSalesId() != null) {
MesWmProductSalesDO sales = productSalesService.getProductSales(salesLine.getSalesId());
return sales != null ? sales.getCode() : null;
}

View File

@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.mes.controller.admin.tm.tool.vo.type.MesTmToolTyp
import cn.iocoder.yudao.module.mes.controller.admin.tm.tool.vo.type.MesTmToolTypeSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.tm.tool.MesTmToolTypeDO;
import cn.iocoder.yudao.module.mes.dal.mysql.tm.tool.MesTmToolTypeMapper;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationToolService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@ -37,6 +38,10 @@ public class MesTmToolTypeServiceImpl implements MesTmToolTypeService {
@Lazy
private MesTmToolService toolService;
@Resource
@Lazy
private MesMdWorkstationToolService workstationToolService;
@Override
public Long createToolType(MesTmToolTypeSaveReqVO createReqVO) {
// 校验数据
@ -75,6 +80,10 @@ public class MesTmToolTypeServiceImpl implements MesTmToolTypeService {
if (toolService.getToolCountByToolTypeId(id) > 0) {
throw exception(TM_TOOL_TYPE_HAS_TOOL);
}
// 校验是否被工作站工装资源引用
if (workstationToolService.getWorkstationToolCountByToolTypeId(id) > 0) {
throw exception(TM_TOOL_TYPE_HAS_WORKSTATION_TOOL);
}
// 删除
toolTypeMapper.deleteById(id);

View File

@ -39,6 +39,11 @@ import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@Slf4j
public class MesWmBatchServiceImpl implements MesWmBatchService {
/**
* 批次追溯最大递归深度,防止极端场景下性能问题
*/
private static final int MAX_TRACE_DEPTH = 20;
@Resource
private MesWmBatchMapper batchMapper;
@Resource
@ -196,11 +201,11 @@ public class MesWmBatchServiceImpl implements MesWmBatchService {
@Override
public List<MesWmBatchDO> getForwardBatchList(String code) {
return getForwardBatchList(code, new HashSet<>());
return getForwardBatchList(code, new HashSet<>(), 0);
}
private List<MesWmBatchDO> getForwardBatchList(String code, Set<String> visited) {
if (code == null || !visited.add(code)) {
private List<MesWmBatchDO> getForwardBatchList(String code, Set<String> visited, int depth) {
if (code == null || !visited.add(code) || depth >= MAX_TRACE_DEPTH) {
return new ArrayList<>();
}
List<MesWmBatchDO> list = batchMapper.selectListByForward(code);
@ -210,18 +215,18 @@ public class MesWmBatchServiceImpl implements MesWmBatchService {
// 继续递归查询下游批次
List<MesWmBatchDO> results = new ArrayList<>(list);
for (MesWmBatchDO batch : list) {
results.addAll(getForwardBatchList(batch.getCode(), visited));
results.addAll(getForwardBatchList(batch.getCode(), visited, depth + 1));
}
return results;
}
@Override
public List<MesWmBatchDO> getBackwardBatchList(String code) {
return getBackwardBatchList(code, new HashSet<>());
return getBackwardBatchList(code, new HashSet<>(), 0);
}
private List<MesWmBatchDO> getBackwardBatchList(String code, Set<String> visited) {
if (code == null || !visited.add(code)) {
private List<MesWmBatchDO> getBackwardBatchList(String code, Set<String> visited, int depth) {
if (code == null || !visited.add(code) || depth >= MAX_TRACE_DEPTH) {
return new ArrayList<>();
}
List<MesWmBatchDO> list = batchMapper.selectListByBackward(code);
@ -231,7 +236,7 @@ public class MesWmBatchServiceImpl implements MesWmBatchService {
// 继续递归查询上游批次
List<MesWmBatchDO> results = new ArrayList<>(list);
for (MesWmBatchDO batch : list) {
results.addAll(getBackwardBatchList(batch.getCode(), visited));
results.addAll(getBackwardBatchList(batch.getCode(), visited, depth + 1));
}
return results;
}

View File

@ -68,6 +68,14 @@ public interface MesWmProductSalesLineService {
*/
PageResult<MesWmProductSalesLineDO> getProductSalesLinePage(MesWmProductSalesLinePageReqVO pageReqVO);
/**
* 校验销售出库单行存在
*
* @param id 编号
* @return 销售出库单行
*/
MesWmProductSalesLineDO validateProductSalesLineExists(Long id);
/**
* OQC 检验完成后,更新销售出库单行的 OQC 质检状态
*

View File

@ -124,7 +124,8 @@ public class MesWmProductSalesLineServiceImpl implements MesWmProductSalesLineSe
return productSalesLineMapper.selectPage(pageReqVO);
}
private MesWmProductSalesLineDO validateProductSalesLineExists(Long id) {
@Override
public MesWmProductSalesLineDO validateProductSalesLineExists(Long id) {
MesWmProductSalesLineDO line = productSalesLineMapper.selectById(id);
if (line == null) {
throw exception(WM_PRODUCT_SALES_LINE_NOT_EXISTS);

View File

@ -8,6 +8,9 @@ import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
@ -54,6 +57,12 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest {
private AdminUserApi adminUserApi;
@MockitoBean
private MesProFeedbackService feedbackService;
@MockitoBean
private MesProRouteProductService routeProductService;
@MockitoBean
private MesProRouteProcessService routeProcessService;
@MockitoBean
private MesProTaskService taskService;
@Test
public void testFinishIpqc_writeBack_feedback() {
@ -211,4 +220,153 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest {
assertThrows(Exception.class, () -> ipqcService.finishIpqc(ipqc.getId()));
}
// ==================== processId 推导行为测试 ====================
/**
* 工位有工序 + 该工序在产品工艺路线中 → processId 应该被设置为工位工序
*/
@Test
public void testCreateIpqc_processId_workstationHasProcess() {
// 准备参数
Long workstationId = randomLongId();
Long workOrderId = randomLongId();
Long productId = randomLongId();
Long processId = randomLongId();
Long routeId = randomLongId();
Long templateId = randomLongId();
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
reqVO.setCode("IPQC-TEST-001");
reqVO.setWorkstationId(workstationId);
reqVO.setWorkOrderId(workOrderId);
reqVO.setInspectorUserId(randomLongId());
// mock 工位返回有工序
MesMdWorkstationDO workstation = new MesMdWorkstationDO();
workstation.setId(workstationId);
workstation.setProcessId(processId);
when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
// mock 工单
MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
workOrder.setId(workOrderId);
workOrder.setProductId(productId);
when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
// mock 模板匹配
MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
templateItem.setTemplateId(templateId);
when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
// mock 工艺路线
MesProRouteProductDO routeProduct = new MesProRouteProductDO();
routeProduct.setRouteId(routeId);
when(routeProductService.getRouteProductByItemId(productId)).thenReturn(routeProduct);
// mock 工序在路线中
when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
.thenReturn(new MesProRouteProcessDO());
// 调用
Long ipqcId = ipqcService.createIpqc(reqVO);
// 断言processId 应该是工位工序
MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
assertEquals(processId, ipqc.getProcessId());
}
/**
* 工位没有工序processId=null→ processId 应该留 null与 KTG 对齐,不回退到任务工序)
*/
@Test
public void testCreateIpqc_processId_workstationNoProcess_shouldBeNull() {
// 准备参数
Long workstationId = randomLongId();
Long workOrderId = randomLongId();
Long productId = randomLongId();
Long taskId = randomLongId();
Long templateId = randomLongId();
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
reqVO.setCode("IPQC-TEST-002");
reqVO.setWorkstationId(workstationId);
reqVO.setWorkOrderId(workOrderId);
reqVO.setTaskId(taskId); // 故意传入 taskId验证不会回退
reqVO.setInspectorUserId(randomLongId());
// mock 工位没有工序
MesMdWorkstationDO workstation = new MesMdWorkstationDO();
workstation.setId(workstationId);
workstation.setProcessId(null); // 无工序
when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
// mock 工单
MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
workOrder.setId(workOrderId);
workOrder.setProductId(productId);
when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
// mock 模板匹配
MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
templateItem.setTemplateId(templateId);
when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
// 调用
Long ipqcId = ipqcService.createIpqc(reqVO);
// 断言processId 应该是 null即使传入了 taskId 也不应该回退
MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
assertNull(ipqc.getProcessId(),
"工位无工序时 processId 应为 null与 KTG 对齐),不应回退到任务工序");
// 断言:不应该读取任务的 processId
verify(routeProductService, never()).getRouteProductByItemId(any());
}
/**
* 工位有工序但该工序不在产品工艺路线中 → processId 应该留 null
*/
@Test
public void testCreateIpqc_processId_processNotInRoute() {
// 准备参数
Long workstationId = randomLongId();
Long workOrderId = randomLongId();
Long productId = randomLongId();
Long processId = randomLongId();
Long routeId = randomLongId();
Long templateId = randomLongId();
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
reqVO.setCode("IPQC-TEST-003");
reqVO.setWorkstationId(workstationId);
reqVO.setWorkOrderId(workOrderId);
reqVO.setInspectorUserId(randomLongId());
// mock 工位有工序
MesMdWorkstationDO workstation = new MesMdWorkstationDO();
workstation.setId(workstationId);
workstation.setProcessId(processId);
when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
// mock 工单
MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
workOrder.setId(workOrderId);
workOrder.setProductId(productId);
when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
// mock 模板匹配
MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
templateItem.setTemplateId(templateId);
when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
// mock 工艺路线存在
MesProRouteProductDO routeProduct = new MesProRouteProductDO();
routeProduct.setRouteId(routeId);
when(routeProductService.getRouteProductByItemId(productId)).thenReturn(routeProduct);
// mock 工序不在路线中
when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
.thenReturn(null);
// 调用
Long ipqcId = ipqcService.createIpqc(reqVO);
// 断言processId 应该是 null
MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
assertNull(ipqc.getProcessId());
}
}