package cn.iocoder.yudao.module.mes.service.dv.spare; import cn.iocoder.yudao.framework.common.exception.ErrorCode; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.mes.controller.admin.dv.spare.vo.*; import cn.iocoder.yudao.module.mes.dal.dataobject.dv.spare.*; import cn.iocoder.yudao.module.mes.dal.dataobject.dv.repair.MesDvRepairDO; import cn.iocoder.yudao.module.mes.dal.mysql.dv.spare.*; import cn.iocoder.yudao.module.mes.dal.mysql.dv.repair.MesDvRepairMapper; import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper; import cn.iocoder.yudao.module.mes.enums.dv.MesDvRepairStatusEnum; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.*; import java.math.BigDecimal; import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; /** 备件库存只通过留痕的流水变更,元数据编辑不允许覆盖库存。 */ @Service public class MesDvSpareService { private static final ErrorCode INVALID = new ErrorCode(1_040_807_001, "备件操作失败:{}"); private static final BigDecimal MAX_QUANTITY = new BigDecimal("999999999999999.999"); @Resource private MesDvSparePartMapper partMapper; @Resource private MesDvSpareMovementMapper movementMapper; @Resource private MesDvRepairMapper repairMapper; @Resource private MesDvMachineryMapper machineryMapper; @Resource private AdminUserApi adminUserApi; @Transactional(rollbackFor=Exception.class, isolation=Isolation.READ_COMMITTED) public Long create(MesDvSparePartSaveReqVO req, Long operator) { validateMetadata(req); MesDvSparePartDO part = BeanUtils.toBean(req,MesDvSparePartDO.class); part.setStockQuantity(req.getInitialQuantity()); partMapper.insert(part); if (req.getInitialQuantity().signum()>0) { writeMovement(part,1,req.getInitialQuantity(),BigDecimal.ZERO,null,null,null,operator, "opening-"+UUID.randomUUID(),"登记初始库存"); } return part.getId(); } @Transactional(rollbackFor=Exception.class, isolation=Isolation.READ_COMMITTED) public void update(MesDvSparePartSaveReqVO req) { MesDvSparePartDO part = requirePart(req.getId()); validateMetadata(req); MesDvSparePartDO changes = BeanUtils.toBean(req,MesDvSparePartDO.class); changes.setStockQuantity(part.getStockQuantity()); partMapper.updateById(changes); } private void validateMetadata(MesDvSparePartSaveReqVO req) { req.setCode(req.getCode().trim()); req.setName(req.getName().trim()); req.setUnit(req.getUnit().trim()); if (req.getCode().isEmpty() || req.getName().isEmpty() || req.getUnit().isEmpty()) fail("编码、名称和单位不能为空"); var old=partMapper.selectOne(MesDvSparePartDO::getCode,req.getCode()); if(old!=null&&!Objects.equals(old.getId(),req.getId())) fail("备件编码已存在"); if(req.getId()!=null && movementMapper.selectCount(MesDvSpareMovementDO::getSparePartId,req.getId())>0) { var saved=partMapper.selectById(req.getId()); if(!Objects.equals(saved.getUnit(),req.getUnit())) fail("已有库存流水的备件不能修改计量单位"); } } @Transactional(rollbackFor=Exception.class, isolation=Isolation.READ_COMMITTED) public void delete(Long id) { var part=requirePart(id); if(part.getStockQuantity().signum()!=0 || movementMapper.selectCount(MesDvSpareMovementDO::getSparePartId,id)>0) fail("有库存或已有流水的备件不能删除,请停用"); partMapper.deleteById(id); } public MesDvSparePartDO get(Long id) { return partMapper.selectById(id); } public PageResult page(MesDvSparePageReqVO req) { return partMapper.page(req); } public PageResult movements(MesDvSparePageReqVO req) { var page=movementMapper.page(req); var ids=page.getList().stream().filter(m->m.getType()==3).map(MesDvSpareMovementDO::getId).toList(); Map totals=new HashMap<>(); movementMapper.returns(ids).forEach(m->totals.merge(m.getOriginalId(),m.getQuantity(),BigDecimal::add)); return new PageResult<>(page.getList().stream().map(m-> { var vo=BeanUtils.toBean(m,MesDvSpareMovementRespVO.class); vo.setReturnedQuantity(totals.getOrDefault(m.getId(),BigDecimal.ZERO)); vo.setReturnableQuantity(m.getType()==3?m.getQuantity().subtract(vo.getReturnedQuantity()):BigDecimal.ZERO); return vo; }).toList(),page.getTotal()); } @Transactional(rollbackFor=Exception.class, isolation=Isolation.READ_COMMITTED) public Long receive(MesDvSpareActionReqVO req,Long operator) { validateAction(req); var part=requirePart(req.getSparePartId()); Long prior=duplicate(req,2,operator,part.getId(),null,null,null); if(prior!=null)return prior; requireEnabled(part); var before=part.getStockQuantity(); changeStock(part,req.getQuantity()); return writeMovement(part,2,req.getQuantity(),before,null,null,null,operator,req.getRequestKey(),req.getRemark()); } @Transactional(rollbackFor=Exception.class, isolation=Isolation.READ_COMMITTED) public Long issue(MesDvSpareActionReqVO req,Long operator) { validateAction(req); if(req.getRepairId()==null || req.getSparePartId()==null || req.getReceiverUserId()==null) fail("请选择维修单、备件和领用人"); // 与维修状态流转使用同一行锁;先维修单再备件,保持一致锁序。 var repair=repairMapper.lockForSpare(req.getRepairId()); if(repair==null)fail("维修单不存在"); var part=requirePart(req.getSparePartId()); Long prior=duplicate(req,3,operator,part.getId(),repair.getId(),null,req.getReceiverUserId()); if(prior!=null)return prior; if(!Objects.equals(repair.getStatus(),MesDvRepairStatusEnum.PREPARE.getStatus()) && !Objects.equals(repair.getStatus(),MesDvRepairStatusEnum.CONFIRMED.getStatus())) fail("仅草稿或维修中的维修单可以领用备件"); requireEnabled(part); var receiver=adminUserApi.getUser(req.getReceiverUserId()); if(receiver==null || !Objects.equals(receiver.getStatus(),0))fail("领用人不存在或已停用"); var before=part.getStockQuantity(); changeStock(part,req.getQuantity().negate()); return writeMovement(part,3,req.getQuantity(),before,repair,null,req.getReceiverUserId(), operator,req.getRequestKey(),req.getRemark()); } @Transactional(rollbackFor=Exception.class, isolation=Isolation.READ_COMMITTED) public Long returnPart(MesDvSpareActionReqVO req,Long operator) { validateAction(req); if(req.getOriginalId()==null)fail("请选择原领用记录"); var original=movementMapper.lock(req.getOriginalId()); if(original==null || original.getType()!=3)fail("原领用记录不存在"); var part=requirePart(original.getSparePartId()); Long prior=duplicate(req,4,operator,part.getId(),original.getRepairId(),original.getId(),original.getReceiverUserId()); if(prior!=null)return prior; BigDecimal returned=movementMapper.returns(List.of(original.getId())).stream() .map(MesDvSpareMovementDO::getQuantity).reduce(BigDecimal.ZERO,BigDecimal::add); if(returned.add(req.getQuantity()).compareTo(original.getQuantity())>0)fail("退回数量超过尚未退回的领用数量"); var before=part.getStockQuantity(); changeStock(part,req.getQuantity()); // 已停用备件仍允许退回。 var record=snapshot(part,4,req.getQuantity(),before,operator,req.getRequestKey(),req.getRemark()); record.setOriginalId(original.getId()); record.setRepairId(original.getRepairId()); record.setRepairCode(original.getRepairCode()); record.setMachineryId(original.getMachineryId()); record.setMachineryName(original.getMachineryName()); record.setReceiverUserId(original.getReceiverUserId()); record.setReceiverName(original.getReceiverName()); movementMapper.insert(record); return record.getId(); } private Long duplicate(MesDvSpareActionReqVO req,int type,Long operator,Long partId,Long repairId,Long originalId,Long receiverId) { var old=movementMapper.byKey(req.getRequestKey()); if(old==null)return null; if(old.getType()!=type || !Objects.equals(old.getOperatorUserId(),operator) || !Objects.equals(old.getSparePartId(),partId) || !Objects.equals(old.getRepairId(),repairId) || !Objects.equals(old.getOriginalId(),originalId) || !Objects.equals(old.getReceiverUserId(),receiverId) || old.getQuantity().compareTo(req.getQuantity())!=0 || !Objects.equals(old.getRemark(),req.getRemark())) fail("请求标识已用于其他操作,请刷新后重试"); return old.getId(); } private void validateAction(MesDvSpareActionReqVO req) { if(req.getQuantity()==null || req.getQuantity().signum()<=0 || req.getQuantity().scale()>3 || req.getQuantity().compareTo(MAX_QUANTITY)>0)fail("数量必须大于0,最多三位小数"); if(req.getRemark()==null || req.getRemark().trim().isEmpty())fail("请填写操作说明"); } private MesDvSparePartDO requirePart(Long id) { if(id==null)fail("请选择备件"); var part=partMapper.lock(id); if(part==null)fail("备件不存在"); return part; } private void requireEnabled(MesDvSparePartDO part) { if(!Objects.equals(part.getStatus(),0))fail("备件已停用"); } private void changeStock(MesDvSparePartDO part,BigDecimal delta) { var after=part.getStockQuantity().add(delta); if(after.signum()<0)fail("库存不足,当前可用:"+part.getStockQuantity()+part.getUnit()); if(after.compareTo(MAX_QUANTITY)>0)fail("库存数量超出系统允许范围"); part.setStockQuantity(after); partMapper.updateById(new MesDvSparePartDO().setId(part.getId()).setStockQuantity(after)); } private MesDvSpareMovementDO snapshot(MesDvSparePartDO part,int type,BigDecimal qty,BigDecimal before, Long operator,String key,String remark) { return new MesDvSpareMovementDO().setRequestKey(key).setSparePartId(part.getId()) .setSparePartCode(part.getCode()).setSparePartName(part.getName()).setSpecification(part.getSpecification()) .setUnit(part.getUnit()).setType(type).setQuantity(qty).setBeforeQuantity(before) .setAfterQuantity(part.getStockQuantity()).setOperatorUserId(operator).setRemark(remark); } private Long writeMovement(MesDvSparePartDO part,int type,BigDecimal qty,BigDecimal before,MesDvRepairDO repair, Long original,Long receiver,Long operator,String key,String remark) { var record=snapshot(part,type,qty,before,operator,key,remark).setOriginalId(original).setReceiverUserId(receiver); if(receiver!=null)record.setReceiverName(adminUserApi.getUser(receiver).getNickname()); if(repair!=null) { var machine=machineryMapper.selectById(repair.getMachineryId()); if(machine==null)fail("维修设备不存在"); record.setRepairId(repair.getId()).setRepairCode(repair.getCode()).setMachineryId(machine.getId()).setMachineryName(machine.getName()); } movementMapper.insert(record); return record.getId(); } private static void fail(String reason) { throw exception(INVALID,reason); } }