package cn.iocoder.yudao.module.mes.service.dv.spare;
|
|
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
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.dataobject.dv.machinery.MesDvMachineryDO;
|
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.system.api.user.AdminUserApi;
|
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
|
import org.junit.jupiter.api.Test;
|
import org.springframework.aop.framework.ProxyFactory;
|
import org.springframework.jdbc.core.*;
|
import org.springframework.jdbc.datasource.*;
|
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
|
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
import java.lang.reflect.*;
|
import java.math.BigDecimal;
|
import java.util.*;
|
import java.util.concurrent.*;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
import static org.junit.jupiter.api.Assertions.*;
|
|
class MesDvSpareServiceTest {
|
@Test void openingStockIsRecordedAndMetadataCannotOverwriteStock() {
|
var f=new Fixture();
|
var req=f.save().setInitialQuantity(new BigDecimal("12"));
|
Long id=f.service.create(req,1L);
|
assertEquals(new BigDecimal("12.000"),f.stock(id));
|
assertEquals(1,f.count());
|
req.setId(id).setInitialQuantity(new BigDecimal("999")).setName("New name");
|
f.service.update(req);
|
assertEquals(new BigDecimal("12.000"),f.stock(id));
|
req.setUnit("箱");
|
assertThrows(ServiceException.class,()->f.service.update(req));
|
}
|
@Test void draftRepairAllowsIssueAndPartialReturnsPreserveTrace() {
|
var f=new Fixture();var issue=f.issue(4);
|
Long id=f.service.issue(issue,1L);
|
assertEquals(new BigDecimal("6.000"),f.stock(1L));
|
f.service.returnPart(f.action(1).setOriginalId(id),1L);
|
f.service.returnPart(f.action(2).setOriginalId(id),1L);
|
assertEquals(new BigDecimal("9.000"),f.stock(1L));
|
assertEquals(3,f.count());
|
assertEquals("R-001",f.jdbc.queryForObject("select repair_code from movement where type=4 limit 1",String.class));
|
assertThrows(ServiceException.class,()->f.service.returnPart(f.action(2).setOriginalId(id),1L));
|
assertEquals(3,f.count());
|
}
|
@Test void insufficientStockAndCompletedRepairCannotIssue() {
|
var f=new Fixture();
|
assertThrows(ServiceException.class,()->f.service.issue(f.issue(11),1L));
|
f.jdbc.update("update repair set status=4");
|
assertThrows(ServiceException.class,()->f.service.issue(f.issue(1),1L));
|
assertEquals(new BigDecimal("10.000"),f.stock(1L));assertEquals(0,f.count());
|
}
|
@Test void repeatedRequestDoesNotDoubleDeductAndChangedPayloadIsRejected() {
|
var f=new Fixture();var req=f.issue(2);
|
assertEquals(f.service.issue(req,1L),f.service.issue(req,1L));
|
assertEquals(new BigDecimal("8.000"),f.stock(1L));assertEquals(1,f.count());
|
req.setQuantity(BigDecimal.ONE);
|
assertThrows(ServiceException.class,()->f.service.issue(req,1L));
|
}
|
@Test void receiveAndReturnAreIdempotentAndDisabledPartsStillReturn() {
|
var f=new Fixture();var receive=f.action(3).setSparePartId(1L);
|
assertEquals(f.service.receive(receive,1L),f.service.receive(receive,1L));
|
var id=f.service.issue(f.issue(2),1L);
|
f.jdbc.update("update part set status=1 where id=1");
|
assertThrows(ServiceException.class,()->f.service.receive(f.action(1).setSparePartId(1L),1L));
|
assertThrows(ServiceException.class,()->f.service.issue(f.issue(1),1L));
|
var back=f.action(2).setOriginalId(id);
|
assertEquals(f.service.returnPart(back,1L),f.service.returnPart(back,1L));
|
assertEquals(new BigDecimal("13.000"),f.stock(1L));assertEquals(3,f.count());
|
}
|
@Test void movementFailureRollsBackStockChange() {
|
var f=new Fixture();f.failWrite.set(true);
|
assertThrows(IllegalStateException.class,()->f.service.issue(f.issue(3),1L));
|
assertEquals(new BigDecimal("10.000"),f.stock(1L));assertEquals(0,f.count());
|
}
|
@Test void concurrentRequestsCannotOverdrawStock() throws Exception {
|
var f=new Fixture();var start=new CountDownLatch(1);
|
var pool=Executors.newFixedThreadPool(2);
|
try {
|
Callable<Boolean> job=()->{start.await();try{f.service.issue(f.issue(7),1L);return true;}catch(ServiceException e){return false;}};
|
var a=pool.submit(job);var b=pool.submit(job);start.countDown();
|
assertNotEquals(a.get(10,TimeUnit.SECONDS),b.get(10,TimeUnit.SECONDS));
|
assertEquals(new BigDecimal("3.000"),f.stock(1L));assertEquals(1,f.count());
|
} finally {pool.shutdownNow();}
|
}
|
@Test void concurrentReturnsCannotExceedOriginalQuantity() throws Exception {
|
var f=new Fixture();Long id=f.service.issue(f.issue(4),1L);var start=new CountDownLatch(1);var pool=Executors.newFixedThreadPool(2);
|
try {
|
Callable<Boolean> job=()->{start.await();try{f.service.returnPart(f.action(3).setOriginalId(id),1L);return true;}catch(ServiceException e){return false;}};
|
var a=pool.submit(job);var b=pool.submit(job);start.countDown();
|
assertNotEquals(a.get(10,TimeUnit.SECONDS),b.get(10,TimeUnit.SECONDS));
|
assertEquals(new BigDecimal("9.000"),f.stock(1L));assertEquals(2,f.count());
|
} finally {pool.shutdownNow();}
|
}
|
@Test void invalidQuantityUserAndHistoryDeletionAreBlocked() {
|
var f=new Fixture();
|
assertThrows(ServiceException.class,()->f.service.issue(f.issue(0),1L));
|
assertThrows(ServiceException.class,()->f.service.issue(f.issue(1).setReceiverUserId(9L),1L));
|
assertThrows(ServiceException.class,()->f.service.delete(1L));
|
f.service.issue(f.issue(1),1L);
|
f.jdbc.update("update part set stock_quantity=0 where id=1");
|
assertThrows(ServiceException.class,()->f.service.delete(1L));
|
}
|
@Test void invalidReturnAndWhitespaceReasonAreBlocked() {
|
var f=new Fixture();
|
assertThrows(ServiceException.class,()->f.service.returnPart(f.action(1).setOriginalId(99L),1L));
|
assertThrows(ServiceException.class,()->f.service.issue(f.issue(1).setRemark(" "),1L));
|
assertEquals(0,f.count());
|
}
|
@SuppressWarnings("unchecked") static <T>T stub(Class<T> type,InvocationHandler h) { return (T)Proxy.newProxyInstance(type.getClassLoader(),new Class<?>[]{type},h); }
|
static class Fixture {
|
final JdbcTemplate jdbc;
|
final MesDvSpareService service;
|
final AtomicBoolean failWrite=new AtomicBoolean();
|
Fixture() {
|
var ds=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=5000","sa","");
|
jdbc=new JdbcTemplate(ds);
|
jdbc.execute("create table part(id bigint auto_increment primary key,code varchar(64) unique,name varchar(128),unit varchar(32),stock_quantity decimal(18,3),safety_quantity decimal(18,3),status int)");
|
jdbc.execute("create table repair(id bigint primary key,code varchar(64),machinery_id bigint,status int)");
|
jdbc.execute("create table movement(id bigint auto_increment primary key,request_key varchar(64) unique,spare_part_id bigint,spare_part_code varchar(64),spare_part_name varchar(128),unit varchar(32),type int,quantity decimal(18,3),before_quantity decimal(18,3),after_quantity decimal(18,3),repair_id bigint,repair_code varchar(64),machinery_id bigint,machinery_name varchar(128),original_id bigint,receiver_user_id bigint,receiver_name varchar(128),operator_user_id bigint,remark varchar(500))");
|
jdbc.update("insert into part values(1,'S-001','轴承','件',10,2,0)");
|
jdbc.update("insert into repair values(1,'R-001',1,0)");
|
var target=new MesDvSpareService();
|
ReflectionTestUtils.setField(target,"partMapper",stub(MesDvSparePartMapper.class,(p,m,a)->switch(m.getName()) {
|
case "lock" -> onePart((Long)a[0],true);
|
case "selectById" -> onePart((Long)a[0],false);
|
case "selectOne" -> jdbc.query("select * from part where code=?",new BeanPropertyRowMapper<>(MesDvSparePartDO.class),a[1]).stream().findFirst().orElse(null);
|
case "insert" -> {var row=(MesDvSparePartDO)a[0];jdbc.update("insert into part(code,name,unit,stock_quantity,safety_quantity,status)values(?,?,?,?,?,?)",row.getCode(),row.getName(),row.getUnit(),row.getStockQuantity(),row.getSafetyQuantity(),row.getStatus());row.setId(jdbc.queryForObject("select id from part where code=?",Long.class,row.getCode()));yield 1;}
|
case "updateById" -> {var row=(MesDvSparePartDO)a[0];if(row.getCode()!=null)yield jdbc.update("update part set code=?,name=?,unit=?,status=?,safety_quantity=?,stock_quantity=? where id=?",row.getCode(),row.getName(),row.getUnit(),row.getStatus(),row.getSafetyQuantity(),row.getStockQuantity(),row.getId());yield jdbc.update("update part set stock_quantity=? where id=?",row.getStockQuantity(),row.getId());}
|
default -> throw new AssertionError(m.getName());
|
}));
|
ReflectionTestUtils.setField(target,"movementMapper",stub(MesDvSpareMovementMapper.class,(p,m,a)->switch(m.getName()){
|
case "byKey" -> jdbc.query("select * from movement where request_key=?",new BeanPropertyRowMapper<>(MesDvSpareMovementDO.class),a[0]).stream().findFirst().orElse(null);
|
case "lock" -> jdbc.query("select * from movement where id=? for update",new BeanPropertyRowMapper<>(MesDvSpareMovementDO.class),a[0]).stream().findFirst().orElse(null);
|
case "returns" -> jdbc.query("select * from movement where type=4",new BeanPropertyRowMapper<>(MesDvSpareMovementDO.class)).stream().filter(row->((Collection<?>)a[0]).contains(row.getOriginalId())).toList();
|
case "selectCount" -> jdbc.queryForObject("select count(*) from movement where spare_part_id=?",Long.class,a[1]);
|
case "insert" -> {if(failWrite.get())throw new IllegalStateException("simulated write failure");var row=(MesDvSpareMovementDO)a[0];jdbc.update("insert into movement(request_key,spare_part_id,spare_part_code,spare_part_name,unit,type,quantity,before_quantity,after_quantity,repair_id,repair_code,machinery_id,machinery_name,original_id,receiver_user_id,receiver_name,operator_user_id,remark)values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",row.getRequestKey(),row.getSparePartId(),row.getSparePartCode(),row.getSparePartName(),row.getUnit(),row.getType(),row.getQuantity(),row.getBeforeQuantity(),row.getAfterQuantity(),row.getRepairId(),row.getRepairCode(),row.getMachineryId(),row.getMachineryName(),row.getOriginalId(),row.getReceiverUserId(),row.getReceiverName(),row.getOperatorUserId(),row.getRemark());row.setId(jdbc.queryForObject("select id from movement where request_key=?",Long.class,row.getRequestKey()));yield 1;}
|
default -> throw new AssertionError(m.getName());
|
}));
|
ReflectionTestUtils.setField(target,"repairMapper",stub(MesDvRepairMapper.class,(p,m,a)->jdbc.query("select * from repair where id=? for update",new BeanPropertyRowMapper<>(MesDvRepairDO.class),a[0]).stream().findFirst().orElse(null)));
|
ReflectionTestUtils.setField(target,"machineryMapper",stub(MesDvMachineryMapper.class,(p,m,a)->new MesDvMachineryDO().setId(1L).setName("设备A")));
|
ReflectionTestUtils.setField(target,"adminUserApi",stub(AdminUserApi.class,(p,m,a)->Objects.equals(a[0],1L)?new AdminUserRespDTO().setId(1L).setNickname("维修员").setStatus(0):null));
|
var proxy=new ProxyFactory(target);
|
proxy.addAdvice(new TransactionInterceptor(new DataSourceTransactionManager(ds),new AnnotationTransactionAttributeSource()));
|
service=(MesDvSpareService)proxy.getProxy();
|
}
|
MesDvSparePartDO onePart(Long id,boolean lock){return jdbc.query("select * from part where id=?"+(lock?" for update":""),new BeanPropertyRowMapper<>(MesDvSparePartDO.class),id).stream().findFirst().orElse(null);}
|
MesDvSparePartSaveReqVO save(){return new MesDvSparePartSaveReqVO().setCode("S-002").setName("皮带").setUnit("件").setInitialQuantity(BigDecimal.ZERO).setSafetyQuantity(BigDecimal.ONE).setStatus(0);}
|
MesDvSpareActionReqVO action(int qty){return new MesDvSpareActionReqVO().setRequestKey(UUID.randomUUID().toString()).setQuantity(BigDecimal.valueOf(qty)).setRemark("维修用途");}
|
MesDvSpareActionReqVO issue(int qty){return action(qty).setSparePartId(1L).setRepairId(1L).setReceiverUserId(1L);}
|
BigDecimal stock(Long id){return jdbc.queryForObject("select stock_quantity from part where id=?",BigDecimal.class,id);}
|
int count(){return jdbc.queryForObject("select count(*) from movement",Integer.class);}
|
}
|
}
|