Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ec4627db6 | ||
|
|
c324de9c32 |
@@ -1656,6 +1656,12 @@ CREATE TABLE ll_monthly_exchange_rate_history (
|
||||
KEY idx_ll_exchange_rate_history (exchange_rate_id,created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='月度汇率变更历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_dispatch_daily_sequence (
|
||||
business_date DATE NOT NULL COMMENT '排车编号业务日期(Asia/Shanghai)',
|
||||
last_sequence INT NOT NULL COMMENT '当日已分配排车流水号,范围0001至9999',
|
||||
PRIMARY KEY(business_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='排车单号每日独立原子流水';
|
||||
|
||||
CREATE TABLE ll_order (
|
||||
id bigint NOT NULL COMMENT '雪花 ID',
|
||||
order_no varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '尾程单号',
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.lastleg.dispatch.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Options;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/** 排车日期流水的原子分配与连接级结果读取,必须加入同一个短事务。 */
|
||||
@Mapper
|
||||
public interface DispatchNumberSequenceMapper {
|
||||
/** 日期主键串行递增,同时在当前数据库连接记住本次实际分配值。 */
|
||||
@Insert("INSERT INTO ll_dispatch_daily_sequence(business_date,last_sequence) VALUES(#{date},LAST_INSERT_ID(1)) "
|
||||
+ "ON DUPLICATE KEY UPDATE last_sequence=LAST_INSERT_ID(last_sequence+1)")
|
||||
int allocate(@Param("date") LocalDate date);
|
||||
|
||||
/** 连接级值不能使用会话或二级查询缓存,须读取本事务刚刚分配的值。 */
|
||||
@Select("SELECT LAST_INSERT_ID()")
|
||||
@Options(useCache=false,flushCache=Options.FlushCachePolicy.TRUE)
|
||||
Long allocatedValue();
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.lastleg.dispatch.repository;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchNumberSequenceMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/** 整车单号独立日流水;独立短事务归还序列锁后才进入车辆、司机及货物锁。 */
|
||||
@Repository
|
||||
public class DispatchNumberSequence {
|
||||
private final DispatchNumberSequenceMapper mapper;
|
||||
|
||||
/** 流水与排车主单使用相同服务数据库。 */
|
||||
public DispatchNumberSequence(DispatchNumberSequenceMapper mapper) { this.mapper = mapper; }
|
||||
|
||||
/** 唯一日期行原子递增,连接内取回本次值;失败不复用已分配的业务号。 */
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public long next(LocalDate date) {
|
||||
// 两个实际 Mapper 操作加入本次 REQUIRES_NEW,连接级结果不跨连接也不读缓存。
|
||||
mapper.allocate(date);
|
||||
Long value = mapper.allocatedValue();
|
||||
if (value == null || value < 1) throw new IllegalStateException("排车日流水分配失败");
|
||||
if (value > 9999) throw new BizException(ResultCode.CONFLICT, "当日排车单四位流水号已用满");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -32,6 +32,7 @@ import static com.baiyue.lastleg.dispatch.port.DispatchContracts.*;
|
||||
/** 真整车创建、加货与查询;客户只属于明细,不属于整车头。 */
|
||||
@Service
|
||||
public class DispatchAggregateService implements DispatchExecutionPort,DispatchAppointmentAmendmentPort {
|
||||
@jakarta.annotation.Resource private DispatchNumberGenerator numbers;
|
||||
@jakarta.annotation.Resource private DispatchAppointmentAllocations appointments;
|
||||
@jakarta.annotation.Resource private DispatchPhysicalClaimService physicalClaims;
|
||||
@jakarta.annotation.Resource private DispatchItemSourceService relabelSources;
|
||||
@@ -269,7 +270,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
var replay = commands.replay(actor,null,identity);
|
||||
if (replay.isPresent()) return replay.get();
|
||||
long id=IdWorker.getId();
|
||||
String number="DS-"+UUID.randomUUID().toString().replace("-","").toUpperCase(Locale.ROOT);
|
||||
String number=numbers.next();
|
||||
var shell=fleet.createDispatchShell(actor.operatorId(),new FleetExecutionModels.DispatchShellRequest(id,number,
|
||||
Long.parseLong(request.warehouseId()),request.idempotencyKey(),identity.commandHash(),request.plannedDepartureAt(),
|
||||
fleetVehicle(request.vehicle()),fleetDriver(request.driver()),request.remark()));
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.repository.DispatchNumberSequence;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
|
||||
/** 平台排车单号:PC、北京时间日期与每日独立四位流水。 */
|
||||
@Component
|
||||
public class DispatchNumberGenerator {
|
||||
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
|
||||
private final DispatchNumberSequence sequences;
|
||||
private final Clock clock;
|
||||
|
||||
/** 正式运行按统一业务时区取当天日期。 */
|
||||
@Autowired
|
||||
public DispatchNumberGenerator(DispatchNumberSequence sequences) { this(sequences, Clock.systemUTC()); }
|
||||
|
||||
/** 固定时钟用于核实跨午夜归零边界。 */
|
||||
DispatchNumberGenerator(DispatchNumberSequence sequences, Clock clock) { this.sequences = sequences; this.clock = clock; }
|
||||
|
||||
/** 已有命令重放由上层账本恢复原号,本方法只用于首次建单。 */
|
||||
public String next() {
|
||||
LocalDate date = LocalDate.now(clock.withZone(BUSINESS_ZONE));
|
||||
long value = sequences.next(date);
|
||||
if (value < 1 || value > 9999) throw new IllegalStateException("排车日流水不在四位范围内");
|
||||
return "PC" + date.format(DateTimeFormatter.BASIC_ISO_DATE) + String.format(Locale.ROOT, "%04d", value);
|
||||
}
|
||||
}
|
||||
+9
-3
@@ -68,14 +68,20 @@ public interface OwnedBusinessMapper {
|
||||
#{reason},#{snapshotJson},#{occurredAt})
|
||||
""")
|
||||
int insertHistory(History row);
|
||||
/** 历史按本页版本顺序分页。 */
|
||||
/** 两表文本转连接字符规则后合并,长度取原值以保留中文及真实尾空格;不改原历史或JSON。 */
|
||||
@Select("""
|
||||
SELECT * FROM (
|
||||
SELECT id,dispatch_id,record_version,NULL AS dispatch_record_version,'OPERATIONS' AS event_source,
|
||||
action,operator_id,reason,snapshot_json,occurred_at FROM ll_fleet_owned_business_history WHERE dispatch_id=#{id}
|
||||
RPAD(CAST(action AS CHAR(40)),CHAR_LENGTH(action),' ') AS action,
|
||||
RPAD(CAST(operator_id AS CHAR(128)),CHAR_LENGTH(operator_id),' ') AS operator_id,
|
||||
RPAD(CAST(reason AS CHAR(2000)),CHAR_LENGTH(reason),' ') AS reason,
|
||||
snapshot_json,occurred_at FROM ll_fleet_owned_business_history WHERE dispatch_id=#{id}
|
||||
UNION ALL
|
||||
SELECT id,dispatch_id,NULL AS record_version,record_version AS dispatch_record_version,'DISPATCH' AS event_source,
|
||||
action,operator_id,reason,snapshot_json,occurred_at FROM ll_dispatch_history WHERE dispatch_id=#{id}
|
||||
RPAD(CAST(action AS CHAR(40)),CHAR_LENGTH(action),' ') AS action,
|
||||
RPAD(CAST(operator_id AS CHAR(128)),CHAR_LENGTH(operator_id),' ') AS operator_id,
|
||||
RPAD(CAST(reason AS CHAR(2000)),CHAR_LENGTH(reason),' ') AS reason,
|
||||
snapshot_json,occurred_at FROM ll_dispatch_history WHERE dispatch_id=#{id}
|
||||
) events ORDER BY occurred_at DESC,event_source,id DESC LIMIT #{limit} OFFSET #{offset}
|
||||
""")
|
||||
List<History> history(@Param("id") long id,@Param("offset") int offset,@Param("limit") int limit);
|
||||
|
||||
+3
-1
@@ -16,10 +16,12 @@ public interface OwnedBusinessQueryMapper {
|
||||
o.loading_completion_hash AS owned_loading_completion_hash,o.loading_completed_at AS owned_loading_completed_at
|
||||
FROM ll_dispatch_order d LEFT JOIN ll_fleet_owned_business_ownership o ON o.dispatch_id=d.id
|
||||
""";
|
||||
/** 摘要按列上限128字节完整比较,不受新旧表默认排序规则影响,不能省略二进制宽度。 */
|
||||
String FILTER="""
|
||||
d.status='ACTIVE' AND d.stage IN ('AFTER_LOADING','DELIVERING','DELIVERED')
|
||||
AND d.loading_completed_at IS NOT NULL AND d.loading_confirmation_json IS NOT NULL
|
||||
AND o.vehicle_ownership=#{q.scope.ownership} AND o.loading_completion_hash=d.loading_completion_hash
|
||||
AND o.vehicle_ownership=#{q.scope.ownership}
|
||||
AND CAST(o.loading_completion_hash AS BINARY(128))=CAST(d.loading_completion_hash AS BINARY(128))
|
||||
AND o.loading_completed_at=d.loading_completed_at
|
||||
AND (o.resource_mode='MASTER' OR (#{q.scope.ownership}='DISPATCHED' AND o.resource_mode='EXTERNAL'))
|
||||
<if test='q.from != null'> AND d.planned_departure_at >= #{q.from}</if>
|
||||
|
||||
@@ -41,6 +41,12 @@ CREATE TABLE IF NOT EXISTS ll_order_no_daily_sequence (
|
||||
PRIMARY KEY(business_date)
|
||||
) ENGINE=InnoDB COMMENT='尾程单号按日原子流水';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_dispatch_daily_sequence (
|
||||
business_date DATE NOT NULL COMMENT '排车编号业务日期(Asia/Shanghai)',
|
||||
last_sequence INT NOT NULL COMMENT '当日已分配排车流水号,范围0001至9999',
|
||||
PRIMARY KEY(business_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='排车单号每日独立原子流水';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_order (
|
||||
id BIGINT NOT NULL COMMENT '雪花 ID',
|
||||
order_no VARCHAR(32) NOT NULL COMMENT '尾程单号',
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -229,4 +229,37 @@ class OwnedBusinessScopePersistenceTest {
|
||||
var bound=outsourced.bind(actor(),"DS-OUT-2",slot,body);assertEquals(uploadedPhoto.fileId(),bound.photos().get(0).fileId());
|
||||
verify(files,times(1)).upload(anyLong(),any(),anyString(),any());
|
||||
}
|
||||
/** 完整摘要严格匹配,同前缀末位、大小写和尾空格均不能误命中。 */
|
||||
@Test void queryHashComparesCompleteValueRatherThanDefaultBinaryWidth() {
|
||||
String prefix="a".repeat(63),exact=prefix+"1";
|
||||
baseline.jdbc.update("UPDATE ll_dispatch_order SET loading_completion_hash=? WHERE id=1",exact);
|
||||
baseline.jdbc.update("UPDATE ll_fleet_owned_business_ownership SET loading_completion_hash=? WHERE dispatch_id=1",exact);
|
||||
var query=new OwnedBusinessModels.Query(1,20,null,null,null,null,null);
|
||||
assertEquals(1,queries.list(OwnedBusinessFixtures.actor(),query).total());
|
||||
for(String different:List.of(prefix+"2","A"+exact.substring(1),prefix+" ")) {
|
||||
baseline.jdbc.update("UPDATE ll_fleet_owned_business_ownership SET loading_completion_hash=? WHERE dispatch_id=1",different);
|
||||
var result=queries.list(OwnedBusinessFixtures.actor(),query);assertEquals(0,result.total());assertTrue(result.records().isEmpty());
|
||||
}
|
||||
baseline.jdbc.update("UPDATE ll_fleet_owned_business_ownership SET loading_completion_hash=? WHERE dispatch_id=1",exact);
|
||||
assertEquals(1,queries.list(OwnedBusinessFixtures.actor(),query).total());
|
||||
}
|
||||
/** 历史投影统一字符比较规则,完整保留中文、原尾空格、动作和原操作账号。 */
|
||||
@Test void historyCharacterProjectionPreservesOriginalTextAndSnapshot() {
|
||||
// 原测试装配的原生历史采用较短替身列,这里只在隔离H2对齐实际原表上限。
|
||||
baseline.jdbc.execute("ALTER TABLE ll_dispatch_history ALTER COLUMN operator_id VARCHAR(128)");
|
||||
baseline.jdbc.execute("ALTER TABLE ll_dispatch_history ALTER COLUMN reason VARCHAR(2000)");
|
||||
baseline.jdbc.execute("ALTER TABLE ll_dispatch_history ALTER COLUMN action VARCHAR(40)");
|
||||
String nativeReason="原排车完整中文历史🌏"+"中".repeat(1200)+" ",operationReason="整车承运登记完整中文说明🌏 ",operator="o".repeat(127)+"X",nativeAction="A".repeat(39)+"X";
|
||||
baseline.jdbc.update("UPDATE ll_dispatch_history SET reason=?,operator_id=?,action=? WHERE dispatch_id=2",nativeReason,operator,nativeAction);
|
||||
var saved=outsourced.save(actor(),"DS-OUT-2",new OutsourcedBusinessModels.SaveOperations("0","history-character-original",
|
||||
new OutsourcedBusinessModels.Operations(new OwnedBusinessModels.Money("CNY","30"),List.of()),operationReason));
|
||||
var history=outsourced.history(actor(),"DS-OUT-2",1,20);assertEquals(2,history.total());
|
||||
var original=history.records().stream().filter(h->h.eventSource().equals("DISPATCH")).findFirst().orElseThrow();
|
||||
var operations=history.records().stream().filter(h->h.eventSource().equals("OPERATIONS")).findFirst().orElseThrow();
|
||||
assertEquals(nativeReason,original.reason());assertEquals(nativeAction,original.action());
|
||||
assertEquals(operationReason,operations.reason());assertEquals("SAVE_OPERATIONS",operations.action());
|
||||
assertEquals(saved.row().operations(),operations.operations());assertEquals("TEST外配操作员",operations.operatorName());
|
||||
var raw=baseline.repository.history(2,0,20).stream().filter(h->h.getEventSource().equals("DISPATCH")).findFirst().orElseThrow();
|
||||
assertEquals(operator,raw.getOperatorId());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user