110 lines
8.3 KiB
Java
110 lines
8.3 KiB
Java
package com.baiyue.lastleg.delivery;
|
|||
|
|
|
||
|
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||
|
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||
|
|
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||
|
|
import com.baiyue.lastleg.delivery.entity.*;
|
||
|
|
import com.baiyue.lastleg.delivery.mapper.*;
|
||
|
|
import com.baiyue.lastleg.delivery.model.*;
|
||
|
|
import com.baiyue.lastleg.delivery.repository.DispatchDeliveryRepository;
|
||
|
|
import com.baiyue.lastleg.delivery.service.*;
|
||
|
|
import com.baiyue.lastleg.dispatch.port.*;
|
||
|
|
import com.baiyue.lastleg.dispatch.port.DispatchContracts.*;
|
||
|
|
import com.baiyue.lastleg.dispatch.port.DispatchDeliveryModels.*;
|
||
|
|
import com.baiyue.lastleg.dispatch.port.DispatchTypes.*;
|
||
|
|
import com.baiyue.lastleg.entity.*;
|
||
|
|
import com.baiyue.lastleg.mapper.*;
|
||
|
|
import com.baiyue.lastleg.fleet.trip.FleetBookingFreight;
|
||
|
|
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
|
|
import org.junit.jupiter.api.BeforeEach;
|
||
|
|
import org.junit.jupiter.api.Test;
|
||
|
|
import org.mybatis.spring.SqlSessionTemplate;
|
||
|
|
import org.springframework.aop.framework.ProxyFactory;
|
||
|
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||
|
|
import org.springframework.jdbc.datasource.*;
|
||
|
|
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
|
||
|
|
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||
|
|
import java.lang.reflect.Modifier;
|
||
|
|
import java.time.LocalDateTime;
|
||
|
|
import java.util.*;
|
||
|
|
import java.util.stream.Collectors;
|
||
|
|
import static com.baiyue.lastleg.delivery.DeliveryDispatchFixtures.*;
|
||
|
|
import static org.junit.jupiter.api.Assertions.*;
|
||
|
|
import static org.mockito.ArgumentMatchers.*;
|
||
|
|
import static org.mockito.Mockito.*;
|
||
|
|
|
||
|
|
/** 真实生产Mapper及事务验证任何客户或主单CAS失败时全车原子回滚。 */
|
||
|
|
class DispatchDeliveryArrangementPersistenceTest {
|
||
|
|
private JdbcTemplate db;
|
||
|
|
private DispatchDeliveryService service;
|
||
|
|
private final DispatchExecutionPort execution=mock(DispatchExecutionPort.class);
|
||
|
|
private final DispatchDeliveryFees fees=mock(DispatchDeliveryFees.class);
|
||
|
|
private final ObjectMapper json=new ObjectMapper().findAndRegisterModules();
|
||
|
|
private final DeliveryCargoGroups grouping=new DeliveryCargoGroups(json);
|
||
|
|
private final Context context=context(List.of(cargo(1,"101","ONT8",51,2),cargo(2,"102","ONT8",52,3)),false);
|
||
|
|
private final Actor actor=new Actor("TEST",ActorMode.PLATFORM,null);
|
||
|
|
|
||
|
|
/** 隔离测试数据库只用于生产Mapper验证,不连接环境或启动服务。 */
|
||
|
|
@BeforeEach void prepare() throws Exception {
|
||
|
|
var ds=new DriverManagerDataSource("jdbc:h2:mem:arrangement-"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa","");
|
||
|
|
db=new JdbcTemplate(ds);
|
||
|
|
for (var type:List.of(OutboundOrder.class,OutboundPalletItem.class,DeliveryOrder.class,LastlegOrder.class,LastlegOrderStatusHistory.class)) {
|
||
|
|
String columns=Arrays.stream(type.getDeclaredFields()).filter(f->!Modifier.isStatic(f.getModifiers())).map(f->{
|
||
|
|
String name=f.getName().replaceAll("([A-Z])","_$1").toLowerCase();
|
||
|
|
String sqlType=f.getType()==String.class?"VARCHAR(8000)":f.getType()==LocalDateTime.class?"TIMESTAMP":
|
||
|
|
f.getType()==java.math.BigDecimal.class?"DECIMAL(24,6)":"BIGINT";
|
||
|
|
return name+" "+sqlType+(f.getName().equals("id")?(type==LastlegOrderStatusHistory.class?" AUTO_INCREMENT PRIMARY KEY":" PRIMARY KEY"):"");
|
||
|
|
}).collect(Collectors.joining(","));
|
||
|
|
db.execute("CREATE TABLE "+type.getAnnotation(TableName.class).value()+"("+columns+")");
|
||
|
|
}
|
||
|
|
db.execute("ALTER TABLE ll_outbound_order ADD UNIQUE(dispatch_order_id,customer_id)");
|
||
|
|
db.execute("ALTER TABLE ll_delivery_order ADD UNIQUE(customer_id,outbound_order_id,delivery_group_key)");
|
||
|
|
db.execute("ALTER TABLE ll_outbound_pallet_item ADD UNIQUE(customer_id,source_attribution_id)");
|
||
|
|
db.execute("""
|
||
|
|
CREATE TABLE ll_dispatch_delivery_arrangement(id BIGINT PRIMARY KEY,dispatch_order_id BIGINT UNIQUE,
|
||
|
|
dispatch_no VARCHAR(128),arrangement_key VARCHAR(128) UNIQUE,business_hash VARCHAR(64),cargo_hash VARCHAR(64),
|
||
|
|
loading_completion_hash VARCHAR(64),original_command_key VARCHAR(255) UNIQUE,original_operator_id VARCHAR(128),
|
||
|
|
result_json CLOB,cargo_json CLOB,created_at TIMESTAMP)
|
||
|
|
""");
|
||
|
|
db.update("INSERT INTO ll_order(id,customer_id,current_status,version) VALUES(1,'101','INBOUND_COMPLETED',0),(2,'102','INBOUND_COMPLETED',0)");
|
||
|
|
var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);
|
||
|
|
for(var mapper:List.of(DispatchDeliveryArrangementMapper.class,OutboundOrderMapper.class,OutboundPalletItemMapper.class,
|
||
|
|
DeliveryOrderMapper.class,DeliveryOperationMapper.class,LastlegOrderMapper.class,LastlegOrderStatusHistoryMapper.class))config.addMapper(mapper);
|
||
|
|
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(config);var session=new SqlSessionTemplate(factory.getObject());
|
||
|
|
var repository=new DispatchDeliveryRepository(session.getMapper(DispatchDeliveryArrangementMapper.class),session.getMapper(OutboundOrderMapper.class),
|
||
|
|
session.getMapper(OutboundPalletItemMapper.class),session.getMapper(DeliveryOrderMapper.class),session.getMapper(DeliveryOperationMapper.class),
|
||
|
|
session.getMapper(LastlegOrderMapper.class),session.getMapper(LastlegOrderStatusHistoryMapper.class));
|
||
|
|
var numbers=mock(LastlegOrderNoGenerator.class);when(numbers.next(anyString(),anyString(),anyLong())).thenAnswer(a->a.getArgument(0)+"-"+a.getArgument(2));
|
||
|
|
var target=new DispatchDeliveryService(execution,repository,grouping,fees,new NormalDeliveryCompletionPolicy(grouping),numbers,json);
|
||
|
|
var proxy=new ProxyFactory(target);proxy.setProxyTargetClass(true);proxy.addAdvice(new TransactionInterceptor(new DataSourceTransactionManager(ds),new AnnotationTransactionAttributeSource()));
|
||
|
|
service=(DispatchDeliveryService)proxy.getProxy();
|
||
|
|
when(execution.lockForArrangement(any(),eq("DP-10"))).thenReturn(context);when(execution.replay(any(),any(),any())).thenReturn(Optional.empty());
|
||
|
|
when(fees.external(any())).thenReturn(Map.of("101",new FleetBookingFreight.Preview("freight",true,List.of(),List.of()),
|
||
|
|
"102",new FleetBookingFreight.Preview("freight",true,List.of(),List.of())));
|
||
|
|
when(execution.recordArrangement(any(),any(),any(),any(),anyBoolean())).thenThrow(new IllegalStateException("主单CAS失败"));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 第二客户写失败会回滚首客户,主单CAS失败也不能留下两张出库或来源绑定。 */
|
||
|
|
@Test void mainCasFailureRollsBackAllCustomersAndSources() {
|
||
|
|
assertThrows(IllegalStateException.class,()->service.arrange(actor,"DP-10",request()));
|
||
|
|
for(var table:List.of("ll_outbound_order","ll_outbound_pallet_item","ll_delivery_order","ll_dispatch_delivery_arrangement","ll_loading_order_status_history"))
|
||
|
|
assertEquals(0,db.queryForObject("SELECT COUNT(*) FROM "+table,Integer.class));
|
||
|
|
assertEquals(2,db.queryForObject("SELECT COUNT(*) FROM ll_order WHERE outbound_order_no IS NULL AND version=0",Integer.class));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 一个客户费用失败不把另一客户已创建的单据单独提交。 */
|
||
|
|
@Test void laterCustomerFeeFailureRollsBackEveryDocument() {
|
||
|
|
doThrow(new IllegalStateException("费用发布失败")).when(fees).publish(any(),argThat(g->g.customerId().equals("102")),any(),any(),any(),any(),anyString(),anyString());
|
||
|
|
assertThrows(IllegalStateException.class,()->service.arrange(actor,"DP-10",request()));
|
||
|
|
assertEquals(0,db.queryForObject("SELECT COUNT(*) FROM ll_outbound_order",Integer.class));
|
||
|
|
assertEquals(0,db.queryForObject("SELECT COUNT(*) FROM ll_delivery_order",Integer.class));
|
||
|
|
verify(execution,never()).recordArrangement(any(),any(),any(),any(),anyBoolean());
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 按真实共享记录构造完整客户目的地请求。 */
|
||
|
|
private ArrangeDelivery request() { return new ArrangeDelivery(3L,"ARRANGE-1","cargo-hash","loading-hash",grouping.groups(context).stream().map(g->
|
||
|
|
new ArrangeGroup(g.key(),g.cargoHash(),DEPARTED,"TRUCK",null,null,"WAREHOUSE_PICKUP","PALLET",null,null,null,null,null,
|
||
|
|
"freight",g.cargoHash(),List.of(),List.of())).toList()); }
|
||
|
|
}
|