fix(dispatch): publish PC date and daily sequence numbering
This commit is contained in:
+83
@@ -0,0 +1,83 @@
|
||||
package com.baiyue.lastleg.dispatch.repository;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchNumberSequenceMapper;
|
||||
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.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/** 使用真实SQL、连接内流水与Spring事务代理核对唯一性和事务边界。 */
|
||||
class DispatchNumberSequenceTest {
|
||||
private static class Fixture {
|
||||
final JdbcTemplate jdbc;
|
||||
final DataSourceTransactionManager transactions;
|
||||
final DispatchNumberSequence sequence;
|
||||
final DispatchNumberSequenceMapper mapper;
|
||||
/** 隔离库造表只使用测试 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 ll_dispatch_daily_sequence(business_date DATE PRIMARY KEY,last_sequence BIGINT NOT NULL)");
|
||||
var configuration=new MybatisConfiguration();configuration.addMapper(DispatchNumberSequenceMapper.class);
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(data);factory.setConfiguration(configuration);factory.afterPropertiesSet();
|
||||
mapper=new SqlSessionTemplate(factory.getObject()).getMapper(DispatchNumberSequenceMapper.class);
|
||||
var proxy=new ProxyFactory(new DispatchNumberSequence(mapper));proxy.setProxyTargetClass(true);
|
||||
proxy.addAdvice(new TransactionInterceptor(transactions,new AnnotationTransactionAttributeSource()));
|
||||
sequence=(DispatchNumberSequence)proxy.getProxy();
|
||||
}
|
||||
}
|
||||
/** 每次业务事务独立取号,同一天连续增长,下一天从一开始。 */
|
||||
@Test void startsAtOneEachDayAndContinuesAcrossGeneratorInstances() throws Exception {
|
||||
var f=new Fixture();var today=LocalDate.of(2026,10,20);
|
||||
assertThat(f.sequence.next(today)).isEqualTo(1);
|
||||
assertThat(f.sequence.next(today)).isEqualTo(2);
|
||||
assertThat(f.sequence.next(today.plusDays(1))).isEqualTo(1);
|
||||
assertThat(f.sequence.next(today)).isEqualTo(3);
|
||||
}
|
||||
/** 两个真实事务连接争用同一天主键,全部已提交号码唯一且连续。 */
|
||||
@Test void concurrentConnectionsAllocateEveryNumberOnce() throws Exception {
|
||||
var f=new Fixture();var date=LocalDate.of(2026,10,20);var pool=Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
var work=new ArrayList<Callable<Long>>();for(int i=0;i<64;i++)work.add(()->f.sequence.next(date));
|
||||
var values=new TreeSet<Long>();for(var future:pool.invokeAll(work))values.add(future.get());
|
||||
assertThat(values).hasSize(64).first().isEqualTo(1L);
|
||||
assertThat(values.last()).isEqualTo(64L);
|
||||
} finally {pool.shutdownNow();}
|
||||
}
|
||||
/** 外层建单回滚不能回滚独立取号事务,也不能复用已分配号。 */
|
||||
@Test void outerBusinessRollbackDoesNotReuseAnAlreadyAllocatedNumber() throws Exception {
|
||||
var f=new Fixture();var date=LocalDate.of(2026,10,20);
|
||||
assertThatThrownBy(()->new TransactionTemplate(f.transactions).execute(status->{
|
||||
assertThat(f.sequence.next(date)).isEqualTo(1);throw new IllegalStateException("business rollback");
|
||||
})).isInstanceOf(IllegalStateException.class);
|
||||
assertThat(f.sequence.next(date)).isEqualTo(2);
|
||||
}
|
||||
/** 四位上限拒绝分配,并回滚本次递增,保持当日已用尽事实。 */
|
||||
@Test void fourDigitExhaustionDoesNotWrapOrIncrementTheStoredCounter() throws Exception {
|
||||
var f=new Fixture();var date=LocalDate.of(2026,10,20);
|
||||
f.jdbc.update("INSERT INTO ll_dispatch_daily_sequence VALUES(?,9999)",date);
|
||||
assertThatThrownBy(()->f.sequence.next(date)).hasMessageContaining("四位流水号已用满");
|
||||
assertThat(f.jdbc.queryForObject("SELECT last_sequence FROM ll_dispatch_daily_sequence WHERE business_date=?",Long.class,date)).isEqualTo(9999L);
|
||||
}
|
||||
|
||||
/** 直接改变连接级值后再次调用同一 Mapper,必须绕过一级会话缓存返回新值。 */
|
||||
@Test void connectionResultBypassesSessionCacheInTheSameTransaction() 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
+3
@@ -536,6 +536,9 @@ public class DispatchAggregatePersistenceTest {
|
||||
service=new DispatchAggregateService(repository,
|
||||
new DispatchSourceRepository(session.getMapper(DispatchCandidateMapper.class),session.getMapper(ReturnDispatchCandidateMapper.class),session.getMapper(DispatchSourceMapper.class)),rules,fleet,
|
||||
new DispatchCommandLedger(new DispatchCommandRepository(session.getMapper(DispatchCommandMapper.class)),new DispatchCommandCodec(json)),new DispatchCommandHasher(json),json);
|
||||
var numbers=mock(DispatchNumberGenerator.class);var dailySequence=new java.util.concurrent.atomic.AtomicInteger();
|
||||
when(numbers.next()).thenAnswer(invocation->"PC20261020"+String.format(java.util.Locale.ROOT,"%04d",dailySequence.incrementAndGet()));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service,"numbers",numbers);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service,"evidence",mock(com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service,"ownedBusinessOwnership",ownedBusinessOwnership);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(gateway,"workflow",service);
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.repository.DispatchNumberSequence;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.time.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class DispatchNumberGeneratorTest {
|
||||
@Test void usesBusinessDateAndFourDigitsWithoutWarehouseOrCustomerSuffix() {
|
||||
var sequences=mock(DispatchNumberSequence.class);
|
||||
when(sequences.next(LocalDate.of(2026,10,20))).thenReturn(1L,42L,9999L);
|
||||
var numbers=new DispatchNumberGenerator(sequences,Clock.fixed(Instant.parse("2026-10-20T08:00:00Z"),ZoneOffset.UTC));
|
||||
assertThat(numbers.next()).isEqualTo("PC202610200001");
|
||||
assertThat(numbers.next()).isEqualTo("PC202610200042");
|
||||
assertThat(numbers.next()).isEqualTo("PC202610209999");
|
||||
}
|
||||
@Test void switchesToTheNewDailySequenceAtShanghaiMidnight() {
|
||||
var sequences=mock(DispatchNumberSequence.class);
|
||||
when(sequences.next(LocalDate.of(2026,10,19))).thenReturn(238L);
|
||||
when(sequences.next(LocalDate.of(2026,10,20))).thenReturn(1L);
|
||||
assertThat(new DispatchNumberGenerator(sequences,Clock.fixed(Instant.parse("2026-10-19T15:59:59Z"),ZoneOffset.UTC)).next()).isEqualTo("PC202610190238");
|
||||
assertThat(new DispatchNumberGenerator(sequences,Clock.fixed(Instant.parse("2026-10-19T16:00:00Z"),ZoneOffset.UTC)).next()).isEqualTo("PC202610200001");
|
||||
}
|
||||
@Test void rejectsExhaustedOrInvalidSequenceInsteadOfWrappingOrReusingNumbers() {
|
||||
for(long value:new long[]{0,10000}) {
|
||||
var sequences=mock(DispatchNumberSequence.class);when(sequences.next(any())).thenReturn(value);
|
||||
assertThatThrownBy(()->new DispatchNumberGenerator(sequences).next()).isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user