This commit is contained in:
2026-10-21 15:53:33 +08:00
parent 94595c810b
commit 090591076f
41 changed files with 459 additions and 63 deletions
@@ -2,6 +2,7 @@ package com.baiyue.lastleg.calendar;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.h2.jdbcx.JdbcDataSource;
@@ -28,12 +29,23 @@ class WarehouseCalendarMapperTest {
}
var config=new MybatisConfiguration(); config.setEnvironment(new Environment("test",new JdbcTransactionFactory(),ds));
config.addMapper(WarehouseCalendarMapper.class);
config.addMapper(LastlegOrderMapper.class);
try(var session=new MybatisSqlSessionFactoryBuilder().build(config).openSession()) {
var rows=session.getMapper(WarehouseCalendarMapper.class).calendar(LocalDateTime.parse("2026-11-01T00:00:00"),LocalDateTime.parse("2026-12-01T00:00:00"),List.of("1"));
assertEquals(3,rows.size());
assertEquals("1",rows.get(0).sourceContainerBookingId()); assertNull(rows.get(0).plannedAt());
assertEquals(2,rows.get(1).version()); assertEquals("2026-11-10T09:00",rows.get(1).plannedAt().toString());
assertEquals(1,rows.get(2).planCount()); assertNotNull(rows.get(2).plannedAt());
// 清关提柜与日历必须读取相同的人工/冻结事实,默认日期由前端统一推算。
var customs=session.getMapper(LastlegOrderMapper.class).findContainerArrivalTimes(List.of("1","2","3"));
assertEquals(3,customs.size());
for(var calendar:rows) {
var arrival=customs.stream().filter(row -> row.sourceContainerBookingId().equals(calendar.sourceContainerBookingId())).findFirst().orElseThrow();
assertEquals(calendar.plannedAt(),arrival.estimatedWarehouseArrivalAt());
assertEquals(calendar.actualAt(),arrival.actualWarehouseArrivalAt());
assertEquals(calendar.version(),arrival.version());
assertEquals(calendar.planCount(),arrival.planCount());
}
}
}
}
@@ -45,6 +45,15 @@ class HttpDispatchEvidenceGatewayTest {
var pdf=new HttpDispatchEvidenceGateway.Metadata(3L,0L,"OVERSEAS_DISPATCH_DRIVER_PHOTO_EVIDENCE","9","driver.pdf","application/pdf",128L);
assertThatThrownBy(()->HttpDispatchEvidenceGateway.verified(new HttpDispatchEvidenceGateway.Envelope("200",pdf),9,DispatchEvidenceGateway.Kind.DRIVER_PHOTO,3L)).isInstanceOf(ResponseStatusException.class);
}
/** 异常图片独立归属本车,审核照片及PDF均不能用作异常图片。 */
@Test void exceptionImagesRejectOtherKindsCarsAndPdf() {
var photo=new HttpDispatchEvidenceGateway.Metadata(3L,0L,"OVERSEAS_DISPATCH_EXCEPTION_EVIDENCE","9","exception.png","image/png",128L);
assertThat(HttpDispatchEvidenceGateway.verified(new HttpDispatchEvidenceGateway.Envelope("200",photo),9,DispatchEvidenceGateway.Kind.EXCEPTION,3L).fileId()).isEqualTo(3L);
assertThatThrownBy(()->HttpDispatchEvidenceGateway.verified(new HttpDispatchEvidenceGateway.Envelope("200",photo),10,DispatchEvidenceGateway.Kind.EXCEPTION,3L)).isInstanceOf(ResponseStatusException.class);
assertThatThrownBy(()->HttpDispatchEvidenceGateway.verified(new HttpDispatchEvidenceGateway.Envelope("200",photo),9,DispatchEvidenceGateway.Kind.DRIVER_PHOTO,3L)).isInstanceOf(ResponseStatusException.class);
var pdf=new HttpDispatchEvidenceGateway.Metadata(3L,0L,"OVERSEAS_DISPATCH_EXCEPTION_EVIDENCE","9","exception.pdf","application/pdf",128L);
assertThatThrownBy(()->HttpDispatchEvidenceGateway.verified(new HttpDispatchEvidenceGateway.Envelope("200",pdf),9,DispatchEvidenceGateway.Kind.EXCEPTION,3L)).isInstanceOf(ResponseStatusException.class);
}
/** 客户、旧主档或缺失媒体类型均拒绝,不因为文件本身存在而放行。 */
@Test void rejectsForeignTenantMasterScopeAndMalformedMetadata() {
for(var value:java.util.List.of(
@@ -0,0 +1,89 @@
package com.baiyue.lastleg.dispatch.service;
import com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway;
import com.baiyue.lastleg.dispatch.port.DispatchCommands;
import com.baiyue.lastleg.dispatch.port.DispatchContracts;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static com.baiyue.lastleg.dispatch.port.DispatchTypes.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
/** 通过真实Mapper和事务验证异常登记,不把登记事实解释成签收或退货。 */
class DispatchExceptionPersistenceTest {
/** 登记使用排车独立维护权限。 */
@BeforeEach void authorize() { SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("operator-test","","platform:overseas-warehouse:dispatch:edit")); }
/** 清除线程身份,避免影响其他权限回归。 */
@AfterEach void clear() { SecurityContextHolder.clearContext(); }
/** 每种原型类型均可保存,原键重试不追加,最终完成后仍恢复原结果。 */
@Test void recordsAllTypesAndReplaysWithoutChangingDeliveryFacts() throws Exception {
var f=new DispatchAggregatePersistenceTest.Fixture();var no=create(f);long version=0;
var before=f.db.queryForMap("SELECT stage,shipped_package_count,departed_at,review_state FROM ll_dispatch_order");
for(var type:ExceptionType.values()) {
var body=new DispatchCommands.MarkException(version,"exception-"+type,type,ReturnDirection.OVERSEAS_WAREHOUSE,"真实异常",List.of());
var saved=f.tx.execute(t->f.service.markException(f.actor,no,body));
var replay=f.tx.execute(t->f.service.markException(f.actor,no,body));
assertThat(saved.stage()).isEqualTo(Stage.DELIVERING);assertThat(replay.replayed()).isTrue();
assertThat(replay.recordVersion()).isEqualTo(saved.recordVersion());version=saved.recordVersion();
}
var records=f.service.detail(f.actor,no).dispatchInfo().exceptions();assertThat(records).hasSize(3);
assertThat(records).allSatisfy(r->{assertThat(r.description()).isEqualTo("真实异常");assertThat(r.occurredAt()).isNotNull();});
assertThat(f.db.queryForMap("SELECT stage,shipped_package_count,departed_at,review_state FROM ll_dispatch_order")).isEqualTo(before);
assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger",Long.class)).isZero();
assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_history WHERE action='MARK_EXCEPTION'",Long.class)).isEqualTo(3);
var first=new DispatchCommands.MarkException(0L,"exception-REFUSED",ExceptionType.REFUSED,ReturnDirection.OVERSEAS_WAREHOUSE,"真实异常",List.of());
f.db.update("UPDATE ll_dispatch_order SET stage='DELIVERED'");
assertThat(f.tx.execute(t->f.service.markException(f.actor,no,first)).replayed()).isTrue();
assertThatThrownBy(()->f.tx.execute(t->f.service.markException(f.actor,no,new DispatchCommands.MarkException(0L,"exception-REFUSED",ExceptionType.REFUSED,null,"改了内容",List.of())))).isInstanceOf(RuntimeException.class);
assertThat(f.service.detail(f.actor,no).dispatchInfo().exceptions()).hasSize(3);
}
/** 非派送阶段、过期版本、只读身份、PDA及运输任务不能登记。 */
@Test void rejectsWrongStageVersionPermissionAndTask() throws Exception {
var f=new DispatchAggregatePersistenceTest.Fixture();var no=create(f);var body=request(0,"blocked",List.of());
for(var stage:List.of("BEFORE_LOADING","LOADING","AFTER_LOADING","DELIVERED")) {
f.db.update("UPDATE ll_dispatch_order SET stage=?",stage);
assertThatThrownBy(()->f.tx.execute(t->f.service.markException(f.actor,no,body))).hasMessageContaining("仅派送中");
}
f.db.update("UPDATE ll_dispatch_order SET stage='DELIVERING'");
assertThatThrownBy(()->f.tx.execute(t->f.service.markException(f.actor,no,request(9,"stale",List.of())))).hasMessageContaining("版本");
SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("operator-test","","platform:overseas-warehouse:dispatch:list"));
assertThatThrownBy(()->f.tx.execute(t->f.service.markException(f.actor,no,body))).hasMessageContaining("权限");
authorize();f.db.update("UPDATE ll_dispatch_order SET task_type='LOCAL_TRUCK'");
assertThatThrownBy(()->f.tx.execute(t->f.service.markException(f.actor,no,body))).hasMessageContaining("仅派送中");
f.db.update("UPDATE ll_dispatch_order SET task_type='DELIVERY',status='CANCELLED'");
assertThatThrownBy(()->f.tx.execute(t->f.service.markException(f.actor,no,body))).hasMessageContaining("仅派送中");
assertThatThrownBy(()->f.tx.execute(t->f.service.markException(new DispatchContracts.Actor("pda",ActorMode.PDA,"9"),no,body))).hasMessageContaining("仅平台");
assertThat(f.db.queryForObject("SELECT record_version FROM ll_dispatch_order",Long.class)).isZero();
}
/** 图片严格核对当前车和类别;附件失败或缺类型不保存任何业务记录。 */
@Test void verifiesImageOwnershipAndKeepsFailedRequestUnchanged() throws Exception {
var f=new DispatchAggregatePersistenceTest.Fixture();var no=create(f);var id=f.repository.find(no).getId();
var files=mock(DispatchEvidenceGateway.class);ReflectionTestUtils.setField(f.service,"evidence",files);
when(files.metadata(id,DispatchEvidenceGateway.Kind.EXCEPTION,91L)).thenThrow(new IllegalArgumentException("文件不属于当前车"));
assertThatThrownBy(()->f.tx.execute(t->f.service.markException(f.actor,no,request(0,"files",List.of(91L))))).hasMessageContaining("不属于");
assertThat(f.service.detail(f.actor,no).dispatchInfo().exceptions()).isEmpty();
var missingType=new DispatchCommands.MarkException(0L,"no-type",null,null,null,List.of());
assertThatThrownBy(()->f.tx.execute(t->f.service.markException(f.actor,no,missingType))).hasMessageContaining("请选择异常类型");
assertThatThrownBy(()->f.tx.execute(t->f.service.markException(f.actor,no,request(0,"duplicates",List.of(91L,91L))))).hasMessageContaining("图片引用");
doReturn(new DispatchEvidenceGateway.File(91L,"现场.png","image/png",10L)).when(files).metadata(id,DispatchEvidenceGateway.Kind.EXCEPTION,91L);
f.tx.execute(t->f.service.markException(f.actor,no,request(0,"files",List.of(91L))));
assertThat(f.service.detail(f.actor,no).dispatchInfo().exceptions().get(0).imageFileIds()).containsExactly(91L);
verify(files,times(2)).metadata(id,DispatchEvidenceGateway.Kind.EXCEPTION,91L);
}
/** 创建真实排车后仅以SQL模拟已发车前置,不声称此用例执行装车流程。 */
private String create(DispatchAggregatePersistenceTest.Fixture f) {
var created=f.tx.execute(t->f.service.create(f.actor,f.configured("create",List.of())));
f.db.update("UPDATE ll_dispatch_order SET stage='DELIVERING',shipped_package_count=3,departed_at=CURRENT_TIMESTAMP");return created.dispatchNo();
}
/** 构造类型必填、其余选填的最小登记请求。 */
private DispatchCommands.MarkException request(long version,String key,List<Long> images) { return new DispatchCommands.MarkException(version,key,ExceptionType.VEHICLE_EXCEPTION,null,null,images); }
}
@@ -148,7 +148,7 @@ class LastlegOrderServiceTest {
when(orders.countContainerPlans("BOOKING-1")).thenReturn(0L);
when(orders.updateContainerExpectedWarehouseArrival("BOOKING-1", estimatedAt, 0)).thenReturn(2);
when(orders.findContainerArrivalTimes(List.of("BOOKING-1"))).thenReturn(List.of(
new ContainerArrivalTimeView("BOOKING-1", estimatedAt, null, 1)));
new ContainerArrivalTimeView("BOOKING-1", estimatedAt, null, 1, 0)));
ContainerArrivalTimeView result = service.changeExpectedWarehouseArrival("BOOKING-1", "arrival-1",
new LastlegOrderRequests.ChangeExpectedWarehouseArrival(estimatedAt, 0, null), "admin");