优化
This commit is contained in:
+6
-3
@@ -32,7 +32,7 @@ class InboundMeasurementReviewContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("OpenAPI不包含待客户确认并声明幂等和字符串长ID")
|
||||
@DisplayName("测量审核不进入待客户确认,并声明幂等和字符串长ID")
|
||||
void shouldDocumentReviewContract() throws Exception {
|
||||
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
@@ -46,8 +46,11 @@ class InboundMeasurementReviewContractTest {
|
||||
.contains("name: inboundId")
|
||||
.contains("schema: {type: string, pattern:")
|
||||
.doesNotContain("/api/platform/v1/inbounds/{inboundId}/measurements/reject:")
|
||||
.doesNotContain("/api/platform/v1/inbounds/{inboundId}/measurements/exception-release:")
|
||||
.doesNotContain("待客户确认");
|
||||
.doesNotContain("/api/platform/v1/inbounds/{inboundId}/measurements/exception-release:");
|
||||
// 约束仅针对测量审核;客户费用确认属于另一业务,允许独立存在。
|
||||
int start = openapi.indexOf(" /api/platform/v1/inbounds/{inboundId}/measurements/approve:");
|
||||
int end = openapi.indexOf("\n /", start + 1);
|
||||
assertThat(openapi.substring(start, end < 0 ? openapi.length() : end)).doesNotContain("待客户确认");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+59
-5
@@ -28,6 +28,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class ContainerCustomsServiceTest {
|
||||
@org.mockito.Spy private com.baiyue.firstleg.service.FirstlegDocumentNumberGenerator documentNumbers = com.baiyue.firstleg.service.TestDocumentNumbers.documents();
|
||||
|
||||
@Mock ContainerCustomsMapper mapper;
|
||||
@Mock CustomsFileClient files;
|
||||
@Mock ObjectMapper objectMapper;
|
||||
@@ -35,6 +37,58 @@ class ContainerCustomsServiceTest {
|
||||
@InjectMocks ContainerCustomsService service;
|
||||
@BeforeEach void setUp(){MockitoAnnotations.openMocks(this);}
|
||||
|
||||
/** 服务端分配批次号后,命令重放必须返回原号且不再消耗流水。 */
|
||||
@Test void declarationCreationReplayKeepsAllocatedNumber() throws Exception {
|
||||
String action = "CREATE_CUSTOMS_DECLARATION_BATCH_9";
|
||||
var command = new AtomicReference<ContainerCustomsMapper.CommandRow>();
|
||||
var batch = new AtomicReference<ContainerCustomsMapper.DeclarationBatchRow>();
|
||||
when(mapper.claimCommand(anyLong(), eq(5L), eq(action), eq("batch-key"), anyString())).thenAnswer(call -> {
|
||||
if (command.get() != null) return 0;
|
||||
command.set(new ContainerCustomsMapper.CommandRow(1L, 5L, action, "batch-key", call.getArgument(4), "PROCESSING", null, null, null));
|
||||
return 1;
|
||||
});
|
||||
when(mapper.command(5L, action, "batch-key")).thenAnswer(call -> command.get());
|
||||
when(mapper.lockContainer(9L)).thenReturn(container(ContainerCustomsService.DECLARING, 3));
|
||||
when(mapper.taxRefundCount(9L)).thenReturn(1);
|
||||
when(mapper.brokerSubmittedAt(2L)).thenReturn(java.time.LocalDateTime.of(2026, 10, 20, 9, 0));
|
||||
when(objectMapper.writeValueAsString(any())).thenReturn("[]");
|
||||
when(mapper.insertDeclarationBatch(anyLong(), eq(2L), eq(9L), anyString(), eq(3), eq("[]"), isNull())).thenAnswer(call -> {
|
||||
batch.set(new ContainerCustomsMapper.DeclarationBatchRow(call.getArgument(0), 2L, 9L, call.getArgument(3),
|
||||
"READY", 3, null, null, null, null, null, null, 0, null, null));
|
||||
return 1;
|
||||
});
|
||||
when(mapper.declarationBatch(anyLong())).thenAnswer(call -> batch.get());
|
||||
when(mapper.insertHistory(anyLong(), eq(2L), eq("CREATE_CUSTOMS_DECLARATION_BATCH"), anyString(), anyString(), anyString(), eq(5L), eq("admin"))).thenReturn(1);
|
||||
when(mapper.completeCommand(eq(5L), eq(action), eq("batch-key"), anyLong())).thenAnswer(call -> {
|
||||
var old = command.get();
|
||||
command.set(new ContainerCustomsMapper.CommandRow(old.id(), 5L, action, "batch-key", old.requestHash(), "SUCCESS", call.getArgument(3), null, null));
|
||||
return 1;
|
||||
});
|
||||
var request = new com.baiyue.firstleg.customs.container.dto.ContainerCustomsDtos.DeclarationBatchRequest(3, null, null);
|
||||
var created = service.createDeclarationBatch(9L, 5L, "admin", "batch-key", request);
|
||||
var replay = service.createDeclarationBatch(9L, 5L, "admin", "batch-key", request);
|
||||
assertThat(created.batchNo()).isEqualTo("DECL202610200001");
|
||||
assertThat(replay.id()).isEqualTo(created.id());
|
||||
assertThat(replay.batchNo()).isEqualTo(created.batchNo());
|
||||
verify(documentNumbers, times(1)).next("DECL");
|
||||
}
|
||||
|
||||
/** 外部报关单号与内部批次号独立,正式提交不能修改内部号。 */
|
||||
@Test void declarationSubmissionPreservesBatchNumber() {
|
||||
var batch = new ContainerCustomsMapper.DeclarationBatchRow(41L, 2L, 9L, "DECL202610200001",
|
||||
"READY", 3, null, null, null, null, null, null, 0, null, null);
|
||||
when(mapper.claimCommand(anyLong(), eq(5L), eq("SUBMIT_CUSTOMS_DECLARATION_BATCH_41"), eq("submit-key"), anyString())).thenReturn(1);
|
||||
when(mapper.lockDeclarationBatch(41L)).thenReturn(batch);
|
||||
when(mapper.submitDeclarationBatch(41L, 0, batch.batchNo(), "CUSTOMS-EXTERNAL-1", 5L, "admin", null)).thenReturn(1);
|
||||
when(mapper.completeCommand(5L, "SUBMIT_CUSTOMS_DECLARATION_BATCH_41", "submit-key", 41L)).thenReturn(1);
|
||||
when(mapper.declarationBatch(41L)).thenReturn(batch);
|
||||
var result = service.submitDeclarationBatch(41L, 5L, "admin", "submit-key",
|
||||
new com.baiyue.firstleg.customs.container.dto.ContainerCustomsDtos.DeclarationBatchRequest(0, "CUSTOMS-EXTERNAL-1", null));
|
||||
assertThat(result.batchNo()).isEqualTo(batch.batchNo());
|
||||
verify(mapper).submitDeclarationBatch(41L, 0, batch.batchNo(), "CUSTOMS-EXTERNAL-1", 5L, "admin", null);
|
||||
verifyNoInteractions(documentNumbers);
|
||||
}
|
||||
|
||||
@Test void merchantOwnBuyOrderIsNotFound(){
|
||||
assertMerchantOrderNotFound(7L,31L);
|
||||
}
|
||||
@@ -211,7 +265,7 @@ class ContainerCustomsServiceTest {
|
||||
var declaring=container(ContainerCustomsService.DECLARING,3);
|
||||
var source=new ContainerCustomsMapper.DocumentRow(31L,2L,7L,10L,"source.bin","a".repeat(64),
|
||||
"application/octet-stream",1L,"ACTIVE",5L,java.time.LocalDateTime.now(),1,java.time.LocalDateTime.now(),1);
|
||||
var submission=new ContainerCustomsMapper.BrokerSubmissionRow(41L,2L,9L,"BR-LP9-01","SENT",3,"admin",
|
||||
var submission=new ContainerCustomsMapper.BrokerSubmissionRow(41L,2L,9L,"BR202610200001","SENT",3,"admin",
|
||||
java.time.LocalDateTime.now(),1);
|
||||
when(mapper.claimCommand(anyLong(),eq(5L),eq("SEND_CUSTOMS_INITIAL_TO_BROKER_9"),eq("send-key"),anyString())).thenReturn(1);
|
||||
when(mapper.lockContainer(9L)).thenReturn(declaring);
|
||||
@@ -220,7 +274,7 @@ class ContainerCustomsServiceTest {
|
||||
when(mapper.documents(2L,null)).thenReturn(List.of(source));
|
||||
when(mapper.frozenScopes(31L)).thenReturn(List.of(new ContainerCustomsMapper.ScopeRow(31L,11L,21L,"I")));
|
||||
when(objectMapper.writeValueAsString(any())).thenReturn("[]");
|
||||
when(mapper.insertBrokerSubmission(anyLong(),eq(2L),eq(9L),eq("BR-LP9-01"),eq(3),eq("[]"),eq(5L),eq("admin"))).thenReturn(1);
|
||||
when(mapper.insertBrokerSubmission(anyLong(),eq(2L),eq(9L),eq("BR202610200001"),eq(3),eq("[]"),eq(5L),eq("admin"))).thenReturn(1);
|
||||
when(mapper.bumpVersionAfterBrokerSubmission(2L,3)).thenReturn(1);
|
||||
when(mapper.insertHistory(anyLong(),eq(2L),eq("SEND_CUSTOMS_INITIAL_TO_BROKER"),eq(ContainerCustomsService.DECLARING),eq(ContainerCustomsService.DECLARING),isNull(),eq(5L),eq("admin"))).thenReturn(1);
|
||||
when(mapper.completeCommand(eq(5L),eq("SEND_CUSTOMS_INITIAL_TO_BROKER_9"),eq("send-key"),anyLong())).thenReturn(1);
|
||||
@@ -235,9 +289,9 @@ class ContainerCustomsServiceTest {
|
||||
var result=service.sendInitialToBroker(9L,5L,"admin","send-key",new ActionRequest(3,true,null));
|
||||
|
||||
assertThat(result.brokerSubmission()).isNotNull();
|
||||
assertThat(result.brokerSubmission().batchNo()).isEqualTo("BR-LP9-01");
|
||||
assertThat(result.brokerSubmission().batchNo()).isEqualTo("BR202610200001");
|
||||
assertThat(result.brokerSubmission().sourceVersion()).isEqualTo(3);
|
||||
verify(mapper).insertBrokerSubmission(anyLong(),eq(2L),eq(9L),eq("BR-LP9-01"),eq(3),eq("[]"),eq(5L),eq("admin"));
|
||||
verify(mapper).insertBrokerSubmission(anyLong(),eq(2L),eq(9L),eq("BR202610200001"),eq(3),eq("[]"),eq(5L),eq("admin"));
|
||||
}
|
||||
|
||||
@Test void initialBrokerSendRejectsStaleAggregateVersion(){
|
||||
@@ -535,7 +589,7 @@ class ContainerCustomsServiceTest {
|
||||
}
|
||||
|
||||
private static ContainerCustomsMapper.DocumentRow document(long id){return new ContainerCustomsMapper.DocumentRow(id,2L,7L,10L,"source.bin","a".repeat(64),"application/octet-stream",1L,"ACTIVE",5L,java.time.LocalDateTime.now(),1,java.time.LocalDateTime.now(),1);}
|
||||
private static ContainerCustomsMapper.BrokerSubmissionRow brokerSubmission(){return new ContainerCustomsMapper.BrokerSubmissionRow(41L,2L,9L,"BR-LP9-01","SENT",3,"admin",java.time.LocalDateTime.now(),1);}
|
||||
private static ContainerCustomsMapper.BrokerSubmissionRow brokerSubmission(){return new ContainerCustomsMapper.BrokerSubmissionRow(41L,2L,9L,"BR202610200001","SENT",3,"admin",java.time.LocalDateTime.now(),1);}
|
||||
private static ContainerCustomsMapper.ContainerRow container(String status,int version){return new ContainerCustomsMapper.ContainerRow(2L,9L,8L,status,null,null,null,null,null,null,null,null,version);}
|
||||
private void assertMerchantOrderNotFound(long tenantId,long orderId){
|
||||
assertThatThrownBy(()->service.merchantOrderView(tenantId,orderId))
|
||||
|
||||
+10
-1
@@ -37,8 +37,17 @@ import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(ColoadGroupService.class)
|
||||
@SpringJUnitConfig({ColoadGroupService.class, ColoadGroupServiceTest.Numbers.class})
|
||||
class ColoadGroupServiceTest {
|
||||
@org.springframework.context.annotation.Configuration
|
||||
static class Numbers {
|
||||
/** 同柜组业务测试使用真实格式,序列由隔离测试替身提供。 */
|
||||
@org.springframework.context.annotation.Bean
|
||||
com.baiyue.firstleg.service.FirstlegDocumentNumberGenerator documentNumbers() {
|
||||
return com.baiyue.firstleg.service.TestDocumentNumbers.documents();
|
||||
}
|
||||
}
|
||||
|
||||
@MockBean
|
||||
private ColoadGroupMapper mapper;
|
||||
@MockBean private PendingReviewOrderClient pendingReviewOrders;
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ class ArrivalCalendarTest {
|
||||
void rejectsInvalidRangesAndPassesTheWholeHalfOpenRangeToOneQuery() {
|
||||
var repository = mock(PortScheduleRepository.class);
|
||||
var service = new PortScheduleService();
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "documentNumbers", com.baiyue.firstleg.service.TestDocumentNumbers.documents());
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
var from = LocalDate.of(2026, 9, 28);
|
||||
assertThrows(BizException.class, () -> service.arrivalCalendar(from, from));
|
||||
|
||||
+2
@@ -35,6 +35,8 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class PortScheduleServiceTest {
|
||||
@org.mockito.Spy private com.baiyue.firstleg.service.FirstlegDocumentNumberGenerator documentNumbers = com.baiyue.firstleg.service.TestDocumentNumbers.documents();
|
||||
|
||||
@Test
|
||||
void platformPublicQueryAllowsZeroButRejectsNegativeCustomerScope() {
|
||||
try (var factory = Validation.buildDefaultValidatorFactory()) {
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baiyue.firstleg.mapper.DocumentSequenceMapper;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
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 org.springframework.transaction.support.TransactionTemplate;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/** 实际 Mapper 和隔离 H2 MySQL 模式验证原子序列,不访问部署环境。 */
|
||||
class DocumentSequenceRepositoryTest {
|
||||
private static class Fixture {
|
||||
final JdbcTemplate jdbc;
|
||||
final DataSourceTransactionManager transactions;
|
||||
final DocumentSequenceMapper mapper;
|
||||
final DocumentSequenceRepository repository;
|
||||
/** 仅测试造表允许 JDBC;被测路径全部使用 Mapper。 */
|
||||
Fixture() throws Exception {
|
||||
var data=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=10000","sa","");
|
||||
jdbc=new JdbcTemplate(data);transactions=new DataSourceTransactionManager(data);
|
||||
jdbc.execute("CREATE TABLE fl_document_sequence(sequence_key VARCHAR(32) PRIMARY KEY,current_value BIGINT NOT NULL)");
|
||||
var configuration=new MybatisConfiguration();configuration.addMapper(DocumentSequenceMapper.class);
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(data);factory.setConfiguration(configuration);factory.afterPropertiesSet();
|
||||
mapper=new SqlSessionTemplate(factory.getObject()).getMapper(DocumentSequenceMapper.class);
|
||||
var proxy=new ProxyFactory(new DocumentSequenceRepository(mapper));proxy.setProxyTargetClass(true);
|
||||
proxy.addAdvice(new TransactionInterceptor(transactions,new AnnotationTransactionAttributeSource()));
|
||||
repository=(DocumentSequenceRepository)proxy.getProxy();
|
||||
}
|
||||
}
|
||||
/** 日期和前缀各自独立,重复分配持续递增。 */
|
||||
@Test void dateAndPrefixHaveIndependentSequences() throws Exception {
|
||||
var f=new Fixture();
|
||||
assertThat(f.repository.next("AA:20261020")).isEqualTo(1);
|
||||
assertThat(f.repository.next("AA:20261020")).isEqualTo(2);
|
||||
assertThat(f.repository.next("BB:20261020")).isEqualTo(1);
|
||||
assertThat(f.repository.next("AA:20261021")).isEqualTo(1);
|
||||
}
|
||||
/** 两个线程使用真实事务连接并发争用,不能产生重号。 */
|
||||
@Test void concurrentConnectionsAllocateUniqueNumbers() throws Exception {
|
||||
var f=new Fixture();var pool=Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
var work=new ArrayList<Callable<Long>>();
|
||||
for(int i=0;i<64;i++)work.add(()->f.repository.next("AA:20261020"));
|
||||
var values=new TreeSet<Long>();
|
||||
for(var future:pool.invokeAll(work))values.add(future.get());
|
||||
assertThat(values).hasSize(64);assertThat(values.first()).isEqualTo(1L);assertThat(values.last()).isEqualTo(64L);
|
||||
} finally {pool.shutdownNow();}
|
||||
}
|
||||
/** 外层建单失败后不能把已经分配的单号再交给其他单据。 */
|
||||
@Test void businessRollbackDoesNotReuseAllocatedNumber() throws Exception {
|
||||
var f=new Fixture();
|
||||
assertThatThrownBy(()->new TransactionTemplate(f.transactions).execute(status->{
|
||||
assertThat(f.repository.next("AA:20261020")).isEqualTo(1);
|
||||
throw new IllegalStateException("业务回滚");
|
||||
})).isInstanceOf(IllegalStateException.class);
|
||||
assertThat(f.repository.next("AA:20261020")).isEqualTo(2);
|
||||
}
|
||||
/** 同一会话中连接值变化后,读取必须绕过 MyBatis 缓存。 */
|
||||
@Test void connectionResultIsNeverCached() throws Exception {
|
||||
var f=new Fixture();
|
||||
new TransactionTemplate(f.transactions).executeWithoutResult(status->{
|
||||
f.jdbc.queryForObject("SELECT LAST_INSERT_ID(7)",Long.class);
|
||||
assertThat(f.mapper.allocatedValue()).isEqualTo(7L);
|
||||
f.jdbc.queryForObject("SELECT LAST_INSERT_ID(37)",Long.class);
|
||||
assertThat(f.mapper.allocatedValue()).isEqualTo(37L);
|
||||
});
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -80,7 +80,7 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
@Test
|
||||
void initializationPassIsUniqueAndFinal() throws Exception {
|
||||
String sql = sql();
|
||||
int expectedTables = 228;
|
||||
int expectedTables = 229;
|
||||
long actualTables = java.util.regex.Pattern.compile("(?m)^CREATE TABLE\\s+")
|
||||
.matcher(sql).results().count();
|
||||
assertThat(actualTables).isEqualTo(expectedTables);
|
||||
|
||||
+2
@@ -24,6 +24,8 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class SailingCalendarServiceTest {
|
||||
@org.mockito.Spy private com.baiyue.firstleg.service.FirstlegDocumentNumberGenerator documentNumbers = com.baiyue.firstleg.service.TestDocumentNumbers.documents();
|
||||
|
||||
@Mock private SailingCalendarRepository repository;
|
||||
@InjectMocks private SailingCalendarService service;
|
||||
private final LocalDateTime copiedDeparture = LocalDateTime.now(ZoneId.of("Asia/Shanghai")).plusDays(14);
|
||||
|
||||
+37
-58
@@ -1,73 +1,52 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.firstleg.repository.DocumentSequenceRepository;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import java.time.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class FirstlegDocumentNumberGeneratorTest {
|
||||
private static final Clock CLOCK = Clock.fixed(
|
||||
Instant.parse("2026-08-18T02:00:00Z"), ZoneId.of("Asia/Shanghai"));
|
||||
|
||||
@Test
|
||||
void startsAsASpringBeanWhenTheTestClockConstructorAlsoExists() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
|
||||
context.registerBean(JdbcTemplate.class, () -> jdbc);
|
||||
context.register(FirstlegDocumentNumberGenerator.class);
|
||||
context.refresh();
|
||||
private static final Clock CLOCK=Clock.fixed(Instant.parse("2026-10-19T16:00:00Z"),ZoneOffset.UTC);
|
||||
|
||||
/** 多构造器情况下正式依赖可以被 Spring 正确选择。 */
|
||||
@Test void springCreatesGeneratorWithRepository() {
|
||||
try(var context=new AnnotationConfigApplicationContext()) {
|
||||
context.registerBean(DocumentSequenceRepository.class,()->mock(DocumentSequenceRepository.class));
|
||||
context.register(FirstlegDocumentNumberGenerator.class);context.refresh();
|
||||
assertThat(context.getBean(FirstlegDocumentNumberGenerator.class)).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatsAllOperationalDocumentPrefixes() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class))
|
||||
.thenReturn(1L, 1L, 1L, 1L, 1L);
|
||||
FirstlegDocumentNumberGenerator generator = new FirstlegDocumentNumberGenerator(jdbc, CLOCK);
|
||||
|
||||
assertThat(generator.next("OB")).isEqualTo("OB-260818-0001");
|
||||
assertThat(generator.next("TA")).isEqualTo("TA-260818-0001");
|
||||
assertThatThrownBy(() -> generator.next("CB")).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(generator.next("LP")).isEqualTo("LP-260818-0001");
|
||||
assertThat(generator.nextCabinetNo()).isEqualTo("D0001");
|
||||
|
||||
verify(jdbc).update(org.mockito.ArgumentMatchers.startsWith("INSERT INTO fl_document_sequence"),
|
||||
eq("OB:20260818"));
|
||||
verify(jdbc).update(org.mockito.ArgumentMatchers.startsWith("INSERT INTO fl_document_sequence"),
|
||||
eq("CABINET"));
|
||||
/** 主机使用 UTC 时仍按北京时间跨午夜,且四位流水不截断。 */
|
||||
@Test void usesShanghaiDateAndIndependentSequenceKey() {
|
||||
var sequences=mock(DocumentSequenceRepository.class);
|
||||
when(sequences.next("LP:20261020")).thenReturn(1L,2L,9999L);
|
||||
var numbers=new FirstlegDocumentNumberGenerator(sequences,CLOCK);
|
||||
assertThat(numbers.next("LP")).isEqualTo("LP202610200001");
|
||||
assertThat(numbers.next("LP")).isEqualTo("LP202610200002");
|
||||
assertThat(numbers.next("LP")).isEqualTo("LP202610209999");
|
||||
verify(sequences,times(3)).next("LP:20261020");
|
||||
}
|
||||
/** 未登记类型不能消耗流水,分配异常或溢出时拒绝出号。 */
|
||||
@Test void rejectsUnknownTypeAndOverflow() {
|
||||
var sequences=mock(DocumentSequenceRepository.class);
|
||||
var numbers=new FirstlegDocumentNumberGenerator(sequences,CLOCK);
|
||||
assertThatThrownBy(()->numbers.next("ZZZZ")).isInstanceOf(IllegalArgumentException.class);
|
||||
verifyNoInteractions(sequences);
|
||||
when(sequences.next("LP:20261020")).thenReturn(10000L);
|
||||
assertThatThrownBy(()->numbers.next("LP")).isInstanceOf(BizException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cabinetSequenceDoesNotResetOrStopAtFourDigits() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class)).thenReturn(9_999L, 10_000L);
|
||||
FirstlegDocumentNumberGenerator generator = new FirstlegDocumentNumberGenerator(jdbc, CLOCK);
|
||||
|
||||
assertThat(generator.nextCabinetNo()).isEqualTo("D9999");
|
||||
assertThat(generator.nextCabinetNo()).isEqualTo("D10000");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnsupportedTypeAndDailyOverflow() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
FirstlegDocumentNumberGenerator generator = new FirstlegDocumentNumberGenerator(jdbc, CLOCK);
|
||||
assertThatThrownBy(() -> generator.next("MSCU")).isInstanceOf(IllegalArgumentException.class);
|
||||
|
||||
when(jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class)).thenReturn(10_000L);
|
||||
assertThatThrownBy(() -> generator.next("LP")).isInstanceOf(BizException.class);
|
||||
/** 柜次和实物标签明确不改格式,柜次允许超过四位。 */
|
||||
@Test void preservesCabinetAndPalletRules() {
|
||||
var sequences=mock(DocumentSequenceRepository.class);
|
||||
when(sequences.next("CABINET")).thenReturn(9999L,10000L);
|
||||
when(sequences.next("DP:20261020")).thenReturn(1L);
|
||||
var numbers=new FirstlegDocumentNumberGenerator(sequences,CLOCK);
|
||||
assertThat(numbers.nextCabinetNo()).isEqualTo("D9999");
|
||||
assertThat(numbers.nextCabinetNo()).isEqualTo("D10000");
|
||||
assertThat(numbers.next("DP")).isEqualTo("DP-261020-0001");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.firstleg.repository.DocumentSequenceRepository;
|
||||
import java.time.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/** 业务单测使用真实格式及隔离计数器;数据库并发由 Repository 集成测试覆盖。 */
|
||||
public final class TestDocumentNumbers {
|
||||
/** 测试工具不实例化。 */
|
||||
private TestDocumentNumbers() {}
|
||||
/** 每个测试生成器独立计数,类型前缀之间互不占号。 */
|
||||
public static FirstlegDocumentNumberGenerator documents() {
|
||||
var sequences=mock(DocumentSequenceRepository.class);
|
||||
var counters=new ConcurrentHashMap<String,AtomicLong>();
|
||||
lenient().when(sequences.next(anyString())).thenAnswer(call->counters.computeIfAbsent(call.getArgument(0),key->new AtomicLong()).incrementAndGet());
|
||||
return new FirstlegDocumentNumberGenerator(sequences,Clock.fixed(Instant.parse("2026-10-20T00:00:00Z"),ZoneOffset.UTC));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user