| | |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPageReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPreGenerateReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeBindBatchReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO; |
| | | import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletBindBatchReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchWorkerDO; |
| | |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchWorkerMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.redis.MesCodeRedisDAO; |
| | | import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.line.MesProLineService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeService; |
| | | import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService; |
| | | import cn.iocoder.yudao.module.mes.service.trace.integrity.MesTraceIntegrityService; |
| | | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| | |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | import java.util.regex.Pattern; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| | |
| | | @Slf4j |
| | | public class MesProBatchServiceImpl implements MesProBatchService { |
| | | |
| | | /** |
| | | * 批次号格式:4 位年份 + 3 位流水(如 2026001) |
| | | */ |
| | | private static final Pattern BATCH_CODE_PATTERN = Pattern.compile("^[0-9]{7}$"); |
| | | private static final int BATCH_CODE_MAX_SERIAL = 999; |
| | | |
| | | @Resource |
| | | private MesProBatchMapper batchMapper; |
| | | @Resource |
| | |
| | | |
| | | @Resource |
| | | private MesProPalletService palletService; |
| | | @Resource |
| | | private MesProBagCodeService bagCodeService; |
| | | @Resource |
| | | private MesMdmItemService mdmItemService; |
| | | @Resource |
| | | private MdmUnitMeasureApi unitMeasureApi; |
| | | |
| | | @Resource |
| | | private MesCodeRedisDAO codeRedisDao; |
| | | |
| | | @Resource |
| | | private MesMdAutoCodeRecordService autoCodeRecordService; |
| | | @Resource |
| | | private AdminUserApi adminUserApi; |
| | | @Resource |
| | |
| | | validateRelations(createReqVO); |
| | | createReqVO.setItemId(resolveBindingItem(null, createReqVO)); |
| | | |
| | | // 2. 生成批次号(编码规则:前缀 + 日期 + 产线编码 + 流水号) |
| | | String code = generateBatchCode(line.getCode()); |
| | | // 2. 确定批次号(人工输入优先,未输入则按「年份+3位流水」自动生成) |
| | | String code = resolveBatchCode(createReqVO.getCode(), null); |
| | | |
| | | // 3. 插入批次(产线编码/名称冗余回填;勾选绑定托盘时批次直接进入生产中) |
| | | boolean bindPallets = CollUtil.isNotEmpty(createReqVO.getPalletIds()); |
| | | boolean hasBindings = hasBindings(createReqVO); |
| | | MesProBatchDO batch = BeanUtils.toBean(createReqVO, MesProBatchDO.class) |
| | | .setCode(code) |
| | | .setBusinessCode(codeRedisDao.generate()) |
| | |
| | | .setLineName(line.getName()) |
| | | .setBagQuantity(0) |
| | | .setPalletQuantity(0) |
| | | .setStatus(bindPallets ? MesProBatchStatusEnum.PRODUCING.getStatus() |
| | | .setStatus(hasBindings ? MesProBatchStatusEnum.PRODUCING.getStatus() |
| | | : MesProBatchStatusEnum.DRAFT.getStatus()); |
| | | batchMapper.insert(batch); |
| | | |
| | |
| | | insertBatchWorkers(batch.getId(), createReqVO.getWorkers()); |
| | | // 5. 绑定托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚) |
| | | bindPalletsToBatch(batch.getId(), createReqVO.getPalletIds()); |
| | | bindTonBagsToBatch(batch.getId(), createReqVO.getTonBagIds()); |
| | | // 6. 生成批次完整性摘要 |
| | | integrityService.recordBatchIntegrity(batch.getId()); |
| | | return batch.getId(); |
| | |
| | | if (StrUtil.isNotEmpty(batch.getCode())) { |
| | | throw exception(PRO_BATCH_ALREADY_FILLED); |
| | | } |
| | | // 2. 校验关联数据并生成正式批次号 |
| | | // 2. 校验关联数据并确定正式批次号(人工输入优先,未输入则自动生成) |
| | | MesProLineDO line = lineService.validateLineExistsAndEnable(fillReqVO.getLineId()); |
| | | validateRelations(fillReqVO); |
| | | fillReqVO.setItemId(resolveBindingItem(batch, fillReqVO)); |
| | | String code = generateBatchCode(line.getCode()); |
| | | String code = resolveBatchCode(fillReqVO.getCode(), null); |
| | | // 3. 落库业务信息(保留临时批次码用于历史扫码追溯;产线编码/名称冗余回填;勾选绑定托盘时批次直接进入生产中) |
| | | boolean bindPallets = CollUtil.isNotEmpty(fillReqVO.getPalletIds()); |
| | | boolean hasBindings = hasBindings(fillReqVO); |
| | | MesProBatchDO updateObj = BeanUtils.toBean(fillReqVO, MesProBatchDO.class) |
| | | .setCode(code) |
| | | .setLineCode(line.getCode()) |
| | | .setLineName(line.getName()) |
| | | .setStatus(bindPallets ? MesProBatchStatusEnum.PRODUCING.getStatus() |
| | | .setStatus(hasBindings ? MesProBatchStatusEnum.PRODUCING.getStatus() |
| | | : MesProBatchStatusEnum.DRAFT.getStatus()); |
| | | batchMapper.updateById(updateObj); |
| | | // 4. 重建生产人员配置 |
| | |
| | | insertBatchWorkers(fillReqVO.getId(), fillReqVO.getWorkers()); |
| | | // 5. 绑定托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚) |
| | | bindPalletsToBatch(fillReqVO.getId(), fillReqVO.getPalletIds()); |
| | | bindTonBagsToBatch(fillReqVO.getId(), fillReqVO.getTonBagIds()); |
| | | // 6. 生成批次完整性摘要(补录正式信息后覆盖) |
| | | integrityService.recordBatchIntegrity(fillReqVO.getId()); |
| | | } |
| | |
| | | validateRelations(updateReqVO); |
| | | updateReqVO.setItemId(resolveBindingItem(oldBatch, updateReqVO)); |
| | | |
| | | // 3. 未生成正式批次号或产线变化时重新生成批次号(批次号包含产线编码要素) |
| | | // 3. 批次号仅含年份与流水号,生成后不再随产线变化;未生成正式批次号时补生成 |
| | | boolean codeEmpty = StrUtil.isEmpty(oldBatch.getCode()); |
| | | boolean lineChanged = !Objects.equals(oldBatch.getLineId(), updateReqVO.getLineId()); |
| | | String code = oldBatch.getCode(); |
| | | String code = codeEmpty ? resolveBatchCode(updateReqVO.getCode(), oldBatch.getId()) : oldBatch.getCode(); |
| | | MesProBatchDO updateObj = BeanUtils.toBean(updateReqVO, MesProBatchDO.class); |
| | | if (codeEmpty || lineChanged) { |
| | | // 变更产线时,目标产线必须启用;未变更则仅校验存在(避免停用产线的历史批次无法编辑其他字段) |
| | | MesProLineDO line = (lineChanged || codeEmpty) |
| | | ? lineService.validateLineExistsAndEnable(updateReqVO.getLineId()) |
| | | : lineService.validateLineExists(updateReqVO.getLineId()); |
| | | code = generateBatchCode(line.getCode()); |
| | | if (lineChanged || codeEmpty) { |
| | | // 变更产线时,目标产线必须启用 |
| | | MesProLineDO line = lineService.validateLineExistsAndEnable(updateReqVO.getLineId()); |
| | | updateObj.setLineCode(line.getCode()).setLineName(line.getName()); |
| | | } |
| | | // 4. 更新批次(勾选绑定托盘时批次直接进入生产中;取消勾选不回退,已绑定托盘不可解绑) |
| | | boolean bindPallets = CollUtil.isNotEmpty(updateReqVO.getPalletIds()); |
| | | boolean hasBindings = hasBindings(updateReqVO); |
| | | updateObj.setCode(code); |
| | | if (bindPallets) { |
| | | if (hasBindings) { |
| | | updateObj.setStatus(MesProBatchStatusEnum.PRODUCING.getStatus()); |
| | | } |
| | | batchMapper.updateById(updateObj); |
| | |
| | | insertBatchWorkers(updateReqVO.getId(), updateReqVO.getWorkers()); |
| | | // 6. 绑定新增勾选的托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚) |
| | | bindPalletsToBatch(updateReqVO.getId(), updateReqVO.getPalletIds()); |
| | | bindTonBagsToBatch(updateReqVO.getId(), updateReqVO.getTonBagIds()); |
| | | // 7. 摘要覆盖范围含品种/产线/生产日期/计划数量,编辑后重新生成避免误报篡改 |
| | | integrityService.recordBatchIntegrity(updateReqVO.getId()); |
| | | } |
| | |
| | | @Override |
| | | public MesProBatchDO getBatch(Long id) { |
| | | return batchMapper.selectById(id); |
| | | } |
| | | |
| | | @Override |
| | | public String getBindingMode(Long id) { |
| | | if (!palletMapper.selectListByBatchId(id).isEmpty()) return "pallet"; |
| | | return bagCodeMapper.selectListByBatchId(id).stream() |
| | | .filter(bag -> bag.getPalletId() == null) |
| | | .map(MesProBagCodeDO::getItemId) |
| | | .filter(Objects::nonNull) |
| | | .distinct() |
| | | .anyMatch(this::isTonItem) |
| | | ? "tonBag" : null; |
| | | } |
| | | |
| | | @Override |
| | |
| | | * 生成正式托盘码并级联录入托盘下未绑批次的袋码,失败整体回滚 |
| | | */ |
| | | private Long resolveBindingItem(MesProBatchDO batch, MesProBatchSaveReqVO req) { |
| | | boolean palletsRequested = CollUtil.isNotEmpty(req.getPalletIds()); |
| | | boolean tonsRequested = CollUtil.isNotEmpty(req.getTonBagIds()); |
| | | if (palletsRequested && tonsRequested) throw bindingError("绑定托盘和绑定吨袋只能选择一种"); |
| | | if (batch != null && (palletsRequested || tonsRequested)) { |
| | | String existingMode = getBindingMode(batch.getId()); |
| | | if ((tonsRequested && "pallet".equals(existingMode)) |
| | | || (palletsRequested && "tonBag".equals(existingMode))) |
| | | throw bindingError("该批次已绑定另一种码,绑定托盘和绑定吨袋只能选择一种"); |
| | | } |
| | | Long itemId = batch == null ? null : batch.getItemId(); |
| | | if (batch != null) { |
| | | for (Long existing : bagCodeMapper.selectDistinctItemIdsByBatchId(batch.getId())) |
| | |
| | | if (candidate == null) throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "托盘缺少品种,请先绑定同品种袋码"); |
| | | itemId = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.merge(itemId, candidate); |
| | | } |
| | | // 请求里的品种只能与已绑定/待绑定托盘一致,不能通过编辑改变批次品种。 |
| | | if (tonsRequested) { |
| | | if (req.getTonBagIds().contains(null)) throw bindingError("请选择有效的吨袋"); |
| | | List<Long> ids = req.getTonBagIds().stream().distinct().sorted().toList(); |
| | | List<MesProBagCodeDO> bags = bagCodeMapper.selectListByIdsForUpdate(ids); |
| | | if (bags.size() != ids.size()) throw bindingError("所选吨袋不存在"); |
| | | for (MesProBagCodeDO bag : bags) { |
| | | if (bag.getBatchId() != null || bag.getPalletId() != null || !isTonItem(bag.getItemId())) |
| | | throw bindingError("只能绑定未录入批次、没有托盘且计量单位为吨的袋码"); |
| | | itemId = cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletItemResolver.merge(itemId, bag.getItemId()); |
| | | } |
| | | } |
| | | // 请求里的品种只能与已绑定或待绑定的托盘/吨袋一致,不能通过编辑改变批次品种。 |
| | | if (req.getItemId() != null && !Objects.equals(req.getItemId(), itemId)) |
| | | throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "品种由绑定托盘带入,不可修改为其他品种"); |
| | | throw bindingError("品种由绑定托盘或吨袋带入,不可修改为其他品种"); |
| | | return itemId; |
| | | } |
| | | |
| | | private boolean hasBindings(MesProBatchSaveReqVO req) { |
| | | return CollUtil.isNotEmpty(req.getPalletIds()) || CollUtil.isNotEmpty(req.getTonBagIds()); |
| | | } |
| | | |
| | | private boolean isTonItem(Long itemId) { |
| | | if (itemId == null) return false; |
| | | MesMdmItemDO item = mdmItemService.getItem(itemId); |
| | | if (item == null || item.getUnitMeasureId() == null) return false; |
| | | var result = unitMeasureApi.getUnitMeasure(item.getUnitMeasureId()); |
| | | return result != null && result.isSuccess() && result.getData() != null |
| | | && "吨".equals(StrUtil.trim(result.getData().getType())); |
| | | } |
| | | |
| | | private cn.iocoder.yudao.framework.common.exception.ServiceException bindingError(String message) { |
| | | return new cn.iocoder.yudao.framework.common.exception.ServiceException(400, message); |
| | | } |
| | | |
| | | private void bindTonBagsToBatch(Long batchId, List<Long> tonBagIds) { |
| | | if (CollUtil.isEmpty(tonBagIds)) return; |
| | | MesProBagCodeBindBatchReqVO req = new MesProBagCodeBindBatchReqVO(); |
| | | req.setBatchId(batchId); |
| | | req.setIds(tonBagIds); |
| | | bagCodeService.bindBatch(req); |
| | | } |
| | | |
| | | private void bindPalletsToBatch(Long batchId, List<Long> palletIds) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * 生成批次号 |
| | | * <p> |
| | | * 编码规则:固定前缀(PC) + 日期(yyyyMMdd) + 输入字符(产线编码) + 流水号(按日循环) |
| | | * 确定批次号:人工输入优先,未输入则按「当前年份 + 3 位流水」自动生成 |
| | | * |
| | | * @param lineCode 产线编码 |
| | | * @return 批次号 |
| | | * @param requestedCode 人工输入的批次号,可为空 |
| | | * @param selfId 当前批次编号(更新时排除自身,新增传 null) |
| | | * @return 最终批次号 |
| | | */ |
| | | private String generateBatchCode(String lineCode) { |
| | | String inputChar = StrUtil.emptyToDefault(lineCode, ""); |
| | | return autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_BATCH_CODE.getCode(), inputChar); |
| | | private String resolveBatchCode(String requestedCode, Long selfId) { |
| | | if (StrUtil.isBlank(requestedCode)) { |
| | | return generateNextBatchCode(); |
| | | } |
| | | String code = StrUtil.trim(requestedCode); |
| | | validateBatchCode(code, selfId); |
| | | return code; |
| | | } |
| | | |
| | | /** |
| | | * 校验批次号格式(4 位年份 + 3 位流水,如 2026001)及唯一性 |
| | | */ |
| | | private void validateBatchCode(String code, Long selfId) { |
| | | if (!BATCH_CODE_PATTERN.matcher(code).matches()) { |
| | | throw exception(PRO_BATCH_CODE_INVALID); |
| | | } |
| | | int year = Integer.parseInt(code.substring(0, 4)); |
| | | int serial = Integer.parseInt(code.substring(4)); |
| | | if (year < 2000 || year > 2100 || serial < 1) { |
| | | throw exception(PRO_BATCH_CODE_INVALID); |
| | | } |
| | | MesProBatchDO existing = batchMapper.selectByCodeIgnoreDeleted(code); |
| | | if (existing != null && !Objects.equals(existing.getId(), selfId)) { |
| | | throw exception(PRO_BATCH_CODE_DUPLICATE, code); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 生成当前年份的下一个批次号 |
| | | */ |
| | | private String generateNextBatchCode() { |
| | | String year = String.valueOf(LocalDate.now().getYear()); |
| | | Integer maxSerial = batchMapper.selectMaxCodeSerialByYear(year); |
| | | int serial = codeRedisDao.allocateBatchNo(year, maxSerial); |
| | | if (serial > BATCH_CODE_MAX_SERIAL) { |
| | | throw exception(PRO_BATCH_CODE_EXHAUSTED); |
| | | } |
| | | return year + String.format("%03d", serial); |
| | | } |
| | | |
| | | /** |