Files
baiyue-firstleg/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryArrangementPersistenceTest.java
T

110 lines
8.3 KiB
Java
Raw Normal View History

2026-10-19 17:56:36 +08:00
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()); }
}