Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c59525dec | ||
|
|
c76b9888f5 |
+36
-6
@@ -223,14 +223,37 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
requirePlatform(actor);var row=required(actor,number,false);requireEditing(row,row.getRecordVersion());
|
||||
return evidence.upload(row.getId(),kind,actor.operatorId(),file);
|
||||
}
|
||||
/** 列表权限含详情只读,但文件仍限当前整车复合归属。 */
|
||||
/** 列表权限含详情只读;仅冻结MASTER证照走司机归属,其余保持本车附件复合归属。 */
|
||||
public com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.File evidenceMetadata(Actor actor,String number,
|
||||
com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind kind,long fileId) {
|
||||
requirePlatform(actor);var row=required(actor,number,false);if(fileId<=0)throw bad("文件引用无效");return evidence.metadata(row.getId(),kind,fileId);
|
||||
requirePlatform(actor);var row=required(actor,number,false);if(fileId<=0)throw bad("文件引用无效");
|
||||
var driverId=masterEvidenceDriverId(row,kind,fileId);
|
||||
if(driverId!=null)return driverEvidenceMetadata(fleet.dispatchDriverLicense(driverId,fileId).metadata(),fileId);
|
||||
return evidence.metadata(row.getId(),kind,fileId);
|
||||
}
|
||||
/** 内容仅经服务端鉴权代理,不返回匿名存储URL。 */
|
||||
/** 内容沿同一次确定的归属代理读取,不尝试类别回退,不返回匿名存储URL。 */
|
||||
public byte[] evidenceContent(Actor actor,String number,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind kind,long fileId) {
|
||||
requirePlatform(actor);var row=required(actor,number,false);if(fileId<=0)throw bad("文件引用无效");return evidence.download(row.getId(),kind,fileId);
|
||||
requirePlatform(actor);var row=required(actor,number,false);if(fileId<=0)throw bad("文件引用无效");
|
||||
var driverId=masterEvidenceDriverId(row,kind,fileId);
|
||||
if(driverId!=null) {
|
||||
var file=fleet.dispatchDriverLicense(driverId,fileId);driverEvidenceMetadata(file.metadata(),fileId);return file.content();
|
||||
}
|
||||
return evidence.download(row.getId(),kind,fileId);
|
||||
}
|
||||
/** 未命中冻结MASTER引用的文件仅可按原排车归属读取,保留编辑中尚未保存的本车外配上传。 */
|
||||
private Long masterEvidenceDriverId(DispatchAggregate row,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind kind,long fileId) {
|
||||
if(kind!=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER)return null;
|
||||
var resources=read(row.getResourcesJson(),Resources.class);var driver=resources==null?null:resources.driver();
|
||||
if(driver==null || driver.mode()!=ResourceMode.MASTER || driver.evidence()==null || driver.evidence().fileIds()==null
|
||||
|| !driver.evidence().fileIds().contains(fileId))return null;
|
||||
if(driver.driverId()==null || driver.driverId()<=0)throw bad("排车冻结司机档案身份无效,不能读取证照");
|
||||
return driver.driverId();
|
||||
}
|
||||
/** 司机域已执行当前/历史关联及File四元组校验,转换元数据时仍保留请求文件身份。 */
|
||||
private static com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.File driverEvidenceMetadata(
|
||||
com.baiyue.lastleg.fleet.driver.DriverModels.File file,long fileId) {
|
||||
if(file==null || !Long.toString(fileId).equals(file.fileId()))throw bad("司机证照回执文件身份不一致");
|
||||
return new com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.File(fileId,file.fileName(),file.contentType(),file.sizeBytes());
|
||||
}
|
||||
|
||||
/** 空单或同仓多客户选货一次建车,与资源关系、来源锁和账本同事务。 */
|
||||
@@ -346,7 +369,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
var row=lockPlatform(actor,number);var identity=identity(actor,number,Action.SUBMIT_REVIEW,request.recordVersion(),request.idempotencyKey(),request);
|
||||
var original=commands.replay(actor,number,identity);if(original.isPresent())return original.get();
|
||||
requireEditing(row,request.recordVersion());
|
||||
if(!Set.of(ReviewState.DRAFT.name(),ReviewState.DISPATCH_REJECTED.name(),ReviewState.LOADING_REJECTED.name()).contains(row.getReviewState()))throw conflict("当前审核已提交,请刷新核对");
|
||||
if(!canSubmitReview(row.getReviewState()))throw conflict("当前审核已提交,请刷新核对");
|
||||
verifyReviewResources(row);
|
||||
if(n(row.getPackageCount())<=0 || repository.cargo(row.getId()).isEmpty())throw bad("空排车不能提交审核");
|
||||
long round=row.getReviewRound()+1;String basis=basis(row);
|
||||
@@ -716,6 +739,12 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
if(!Status.ACTIVE.name().equals(row.getStatus()) || !Stage.BEFORE_LOADING.name().equals(row.getStage()) || n(row.getPickedPackageCount())>0 || n(row.getShippedPackageCount())>0) throw conflict("仅装车前且未扫码的有效排车可以调整货物");
|
||||
}
|
||||
|
||||
/** 提交资格和写入共用可重提状态,已进入两级待审的整车不得再次显示提交入口。 */
|
||||
private static boolean canSubmitReview(String reviewState) {
|
||||
return ReviewState.DRAFT.name().equals(reviewState) || ReviewState.DISPATCH_REJECTED.name().equals(reviewState)
|
||||
|| ReviewState.LOADING_REJECTED.name().equals(reviewState);
|
||||
}
|
||||
|
||||
/** 动作快照只反映实际当前资格,写入仍须再次加锁校验。 */
|
||||
Map<Action,Gate> actions(Actor actor,DispatchAggregate row) {
|
||||
var gates=new EnumMap<Action,Gate>(Action.class); boolean active=Status.ACTIVE.name().equals(row.getStatus());
|
||||
@@ -723,7 +752,8 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
for(var action:Action.values()) {
|
||||
boolean state=switch(action) {
|
||||
case CREATE -> actor.mode()==ActorMode.PLATFORM;
|
||||
case EDIT,ADD_CARGO,REMOVE_CARGO,CANCEL,SUBMIT_REVIEW -> active && stage==Stage.BEFORE_LOADING && n(row.getPickedPackageCount())==0;
|
||||
case EDIT,ADD_CARGO,REMOVE_CARGO,CANCEL -> active && stage==Stage.BEFORE_LOADING && n(row.getPickedPackageCount())==0;
|
||||
case SUBMIT_REVIEW -> active && stage==Stage.BEFORE_LOADING && n(row.getPickedPackageCount())==0 && canSubmitReview(row.getReviewState());
|
||||
case DISPATCH_REVIEW -> active && review==ReviewState.DISPATCH_PENDING;
|
||||
case LOADING_REVIEW -> active && review==ReviewState.LOADING_PENDING;
|
||||
case RETURN_TO_REVIEW -> active && stage==Stage.LOADING && n(row.getPickedPackageCount())==0;
|
||||
|
||||
+5
@@ -34,6 +34,11 @@ public class FleetTripExecutionService {
|
||||
private final ObjectMapper json;
|
||||
private final FleetBookingReservations bookingReservations;
|
||||
|
||||
/** 排车域先核对冻结司机及附件引用;本域继续校验该司机当前或历史证照归属。 */
|
||||
public DriverModels.Download dispatchDriverLicense(long driverId,long fileId) {
|
||||
return drivers.download(driverId,fileId);
|
||||
}
|
||||
|
||||
public FleetTripExecutionService(FleetTripMapper trips, FleetTripDispatchMapper bindings, FleetResourceMapper resources,
|
||||
FleetDispatchGateway dispatches, FleetTripService history, DriverService drivers,
|
||||
VehicleRepository vehicles, ObjectMapper json, FleetBookingReservations bookingReservations) {
|
||||
|
||||
+180
@@ -35,6 +35,128 @@ import static com.baiyue.lastleg.dispatch.port.DispatchTypes.*;
|
||||
|
||||
/** 主单/资源/占用/账本真实SQL回归;外部目录与选货权威提供者使用受控输入。 */
|
||||
public class DispatchAggregatePersistenceTest {
|
||||
/** 正式司机附件从排车冻结引用选出真实司机,再复用司机域及File的读取边界。 */
|
||||
@Test void masterDriverEvidenceUsesFrozenDriverOwnerForMetadataAndContent() throws Exception {
|
||||
var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.MASTER,71L,List.of(81L));
|
||||
e.allowFile(81L);var before=e.f.db.queryForMap("SELECT resources_json,record_version FROM ll_dispatch_order WHERE dispatch_no=?",number);
|
||||
assertThat(e.f.service.evidenceMetadata(e.f.actor,number,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER,81L).fileId()).isEqualTo(81L);
|
||||
assertThat(e.f.service.evidenceContent(e.f.actor,number,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER,81L)).containsExactly(1,2,3);
|
||||
verify(e.files,times(2)).metadata(71L,81L);verify(e.files,times(2)).download(71L,81L);verifyNoInteractions(e.dispatchFiles);
|
||||
assertThat(e.f.db.queryForMap("SELECT resources_json,record_version FROM ll_dispatch_order WHERE dispatch_no=?",number)).isEqualTo(before);
|
||||
}
|
||||
|
||||
/** 主档后来换证不改写本车冻结证照;旧附件必须仍满足该司机的历史关联规则。 */
|
||||
@Test void frozenHistoricalDriverLicenseRemainsReadableAfterMasterReplacement() throws Exception {
|
||||
var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.MASTER,71L,List.of(81L));
|
||||
e.driver.setLicenseFileId(82L);when(e.history.selectCount(any())).thenReturn(1L);e.allowFile(81L);
|
||||
assertThat(e.f.service.evidenceMetadata(e.f.actor,number,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER,81L).fileId()).isEqualTo(81L);
|
||||
assertThat(e.f.service.evidenceContent(e.f.actor,number,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER,81L)).containsExactly(1,2,3);
|
||||
verify(e.history,times(2)).selectCount(any());verify(e.files,times(2)).metadata(71L,81L);verifyNoInteractions(e.dispatchFiles);
|
||||
}
|
||||
|
||||
/** 外配附件仍限本车证据;不能因为共用DRIVER展示类型而改走正式司机档案。 */
|
||||
@Test void externalDriverEvidenceRetainsExactDispatchOwnerForBothReads() throws Exception {
|
||||
var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.EXTERNAL,null,List.of(81L));
|
||||
var dispatch=e.f.repository.find(number).getId();var kind=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER;
|
||||
var metadata=new com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.File(81L,"external.pdf","application/pdf",3L);
|
||||
when(e.dispatchFiles.metadata(dispatch,kind,81L)).thenReturn(metadata);when(e.dispatchFiles.download(dispatch,kind,81L)).thenReturn(new byte[]{4,5,6});
|
||||
assertThat(e.f.service.evidenceMetadata(e.f.actor,number,kind,81L)).isEqualTo(metadata);
|
||||
assertThat(e.f.service.evidenceContent(e.f.actor,number,kind,81L)).containsExactly(4,5,6);
|
||||
verify(e.dispatchFiles).metadata(dispatch,kind,81L);verify(e.dispatchFiles).download(dispatch,kind,81L);
|
||||
verifyNoInteractions(e.driverRows,e.history,e.files);
|
||||
}
|
||||
|
||||
/** 冻结引用不绕过司机当前或历史关联,提供者明确拒绝后不得尝试排车类别。 */
|
||||
@Test void unlinkedFrozenMasterFileCannotFallBackToDispatchEvidence() throws Exception {
|
||||
var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.MASTER,71L,List.of(81L));
|
||||
e.driver.setLicenseFileId(82L);when(e.history.selectCount(any())).thenReturn(0L);
|
||||
var kind=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER;
|
||||
assertThatThrownBy(()->e.f.service.evidenceMetadata(e.f.actor,number,kind,81L)).hasMessageContaining("未关联");
|
||||
assertThatThrownBy(()->e.f.service.evidenceContent(e.f.actor,number,kind,81L)).hasMessageContaining("未关联");
|
||||
verifyNoInteractions(e.files,e.dispatchFiles);
|
||||
}
|
||||
|
||||
/** File拒绝主档四元组时直接失败,不以另一业务类别或其他司机重新读取。 */
|
||||
@Test void masterFileProviderRejectionCannotFallBackToAnotherCategory() throws Exception {
|
||||
var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.MASTER,71L,List.of(81L));
|
||||
when(e.files.metadata(71L,81L)).thenThrow(new BizException(com.baiyue.common.core.api.ResultCode.NOT_FOUND,"业务附件不存在或归属不匹配"));
|
||||
var kind=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER;
|
||||
assertThatThrownBy(()->e.f.service.evidenceMetadata(e.f.actor,number,kind,81L)).hasMessageContaining("归属不匹配");
|
||||
assertThatThrownBy(()->e.f.service.evidenceContent(e.f.actor,number,kind,81L)).hasMessageContaining("归属不匹配");
|
||||
verify(e.files,never()).download(anyLong(),anyLong());verifyNoInteractions(e.dispatchFiles);
|
||||
}
|
||||
|
||||
/** MASTER改外配的编辑尚未保存时,新上传文件只走本车归属,不误借旧司机白名单。 */
|
||||
@Test void unsavedExternalUploadOnMasterDispatchRetainsOriginalDispatchFileReads() throws Exception {
|
||||
var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.MASTER,71L,List.of(81L));
|
||||
var dispatch=e.f.repository.find(number).getId();var kind=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER;
|
||||
var metadata=new com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.File(91L,"new-external.pdf","application/pdf",3L);
|
||||
when(e.dispatchFiles.metadata(dispatch,kind,91L)).thenReturn(metadata);when(e.dispatchFiles.download(dispatch,kind,91L)).thenReturn(new byte[]{4,5,6});
|
||||
assertThat(e.f.service.evidenceMetadata(e.f.actor,number,kind,91L)).isEqualTo(metadata);
|
||||
assertThat(e.f.service.evidenceContent(e.f.actor,number,kind,91L)).containsExactly(4,5,6);
|
||||
verifyNoInteractions(e.driverRows,e.history,e.files);
|
||||
assertThat(e.f.db.queryForObject("SELECT resources_json FROM ll_dispatch_order WHERE dispatch_no=?",String.class,number)).contains("MASTER").doesNotContain("91");
|
||||
}
|
||||
|
||||
/** 未列入MASTER冻结集合或声明为外配的档案文件只能接受本车四元组核验,失败后不转司机域。 */
|
||||
@Test void foreignOrWrongModeMasterFilesNeverGainDriverDomainAccess() throws Exception {
|
||||
for(var mode:List.of(ResourceMode.MASTER,ResourceMode.EXTERNAL)) {
|
||||
var e=new DriverEvidenceFixture();var number=e.dispatch(mode,71L,mode==ResourceMode.MASTER?List.of(81L):List.of(91L));
|
||||
var dispatch=e.f.repository.find(number).getId();var kind=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER;
|
||||
when(e.dispatchFiles.metadata(dispatch,kind,91L)).thenThrow(new BizException(com.baiyue.common.core.api.ResultCode.NOT_FOUND,"业务附件不存在或归属不匹配"));
|
||||
when(e.dispatchFiles.download(dispatch,kind,91L)).thenThrow(new BizException(com.baiyue.common.core.api.ResultCode.NOT_FOUND,"业务附件不存在或归属不匹配"));
|
||||
assertThatThrownBy(()->e.f.service.evidenceMetadata(e.f.actor,number,kind,91L)).hasMessageContaining("归属不匹配");
|
||||
assertThatThrownBy(()->e.f.service.evidenceContent(e.f.actor,number,kind,91L)).hasMessageContaining("归属不匹配");
|
||||
verifyNoInteractions(e.driverRows,e.history,e.files);
|
||||
}
|
||||
}
|
||||
|
||||
/** 命中冻结文件集合仍须有本车真实正数司机身份,不能拿车号或其他司机替代。 */
|
||||
@Test void masterFileWithMissingOrInvalidDriverIdentityFailsBeforeAnyFileProvider() throws Exception {
|
||||
for(Long driverId:Arrays.asList(null,0L,-1L)) {
|
||||
var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.MASTER,driverId,List.of(81L));
|
||||
var kind=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER;
|
||||
assertThatThrownBy(()->e.f.service.evidenceMetadata(e.f.actor,number,kind,81L)).hasMessageContaining("身份无效");
|
||||
assertThatThrownBy(()->e.f.service.evidenceContent(e.f.actor,number,kind,81L)).hasMessageContaining("身份无效");
|
||||
verifyNoInteractions(e.driverRows,e.history,e.files,e.dispatchFiles);
|
||||
}
|
||||
}
|
||||
|
||||
/** 无法解释的资源模式不选择任何文件域,防止异常快照放宽读取。 */
|
||||
@Test void unknownDriverModeFailsBeforeEitherFileProvider() throws Exception {
|
||||
var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.MASTER,71L,List.of(81L));
|
||||
e.f.db.update("UPDATE ll_dispatch_order SET resources_json=REPLACE(resources_json,'MASTER','UNKNOWN') WHERE dispatch_no=?",number);
|
||||
var kind=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER;
|
||||
assertThatThrownBy(()->e.f.service.evidenceMetadata(e.f.actor,number,kind,81L)).hasMessageContaining("快照读取失败");
|
||||
assertThatThrownBy(()->e.f.service.evidenceContent(e.f.actor,number,kind,81L)).hasMessageContaining("快照读取失败");
|
||||
verifyNoInteractions(e.driverRows,e.history,e.files,e.dispatchFiles);
|
||||
}
|
||||
|
||||
/** 使用真实排车Mapper/资源JSON及司机Service,只替代外部文件内容和主档持久提供者。 */
|
||||
private static final class DriverEvidenceFixture {
|
||||
final Fixture f;final DriverMapper driverRows=mock(DriverMapper.class);final DriverHistoryMapper history=mock(DriverHistoryMapper.class);
|
||||
final DriverFileGateway files=mock(DriverFileGateway.class);final DriverEntity driver=new DriverEntity();
|
||||
final com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway dispatchFiles;
|
||||
/** 真实司机Service继续执行当前/历史附件关联检查,不由排车测试替身伪装通过。 */
|
||||
DriverEvidenceFixture() throws Exception {
|
||||
f=new Fixture();driver.setId(71L);driver.setLicenseFileId(81L);when(driverRows.selectById(71L)).thenReturn(driver);
|
||||
var drivers=new DriverService(driverRows,history,files,f.json,mock(FleetWarehouseDirectory.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(f.fleet,"drivers",drivers);
|
||||
dispatchFiles=(com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway)org.springframework.test.util.ReflectionTestUtils.getField(f.service,"evidence");
|
||||
}
|
||||
/** 在真实主单保存服务端资源快照,HTTP请求本身不能指定司机、模式或允许文件集合。 */
|
||||
String dispatch(ResourceMode mode,Long driverId,List<Long> ids) throws Exception {
|
||||
var created=f.tx.execute(t->f.service.create(f.actor,f.create("evidence-"+UUID.randomUUID(),List.of())));
|
||||
var resource=new DispatchContracts.Resources(null,new DispatchContracts.DriverSnapshot(mode,driverId,"测试司机","123456789",
|
||||
new DispatchContracts.DriverEvidence("TEST-LICENSE",LocalDate.of(2099,1,1),ids,Instant.parse("2098-01-01T00:00:00Z"))));
|
||||
f.db.update("UPDATE ll_dispatch_order SET resources_json=? WHERE dispatch_no=?",f.json.writeValueAsString(resource),created.dispatchNo());return created.dispatchNo();
|
||||
}
|
||||
/** File内容固定为离线测试字节;真实复合归属由既有两个HTTP网关合同分别覆盖。 */
|
||||
void allowFile(long fileId) {
|
||||
var metadata=new DriverModels.File(Long.toString(fileId),"driver.pdf","application/pdf",3L);
|
||||
when(files.metadata(71L,fileId)).thenReturn(metadata);when(files.download(71L,fileId)).thenReturn(new byte[]{1,2,3});
|
||||
}
|
||||
}
|
||||
/** 空单零版本取消使用真实来源规则,成功及原键重放均不制造来源、库存或重复命令。 */
|
||||
@Test void emptyVersionZeroCancellationUsesRealSourceRuleAndPreservesOriginalReplay() throws Exception {
|
||||
var f=new Fixture();var created=f.tx.execute(t->f.service.create(f.actor,f.create("empty-real-cancel",List.of())));
|
||||
@@ -165,6 +287,64 @@ public class DispatchAggregatePersistenceTest {
|
||||
assertThatThrownBy(()->f.tx.execute(s->f.service.loadingComplete(f.actor,created.dispatchNo(),new DispatchCommands.LoadingComplete(approved.recordVersion(),"missing-rejected",preview.manifestHash(),List.of(),null)))).isInstanceOf(BizException.class).hasMessageContaining("核平");
|
||||
assertThat(f.db.queryForObject("SELECT stage FROM ll_dispatch_order",String.class)).isEqualTo("LOADING");
|
||||
}
|
||||
/** 已提交根的公开资格关闭;新键重复提交不改变事实,原键仍先于当前状态恢复结果。 */
|
||||
@Test void pendingSubmissionGateRejectsNewKeyWithoutMutationAndRestoresOriginalCommand() throws Exception {
|
||||
var security=org.springframework.security.core.context.SecurityContextHolder.getContext();var previous=security.getAuthentication();
|
||||
security.setAuthentication(new org.springframework.security.authentication.TestingAuthenticationToken("operator-test","",
|
||||
"platform:overseas-warehouse:dispatch:list","platform:overseas-warehouse:dispatch:edit"));
|
||||
try {
|
||||
var f=new Fixture();var source=f.source(1,"101",501,"ONT8",3);
|
||||
var created=f.tx.execute(s->f.service.create(f.actor,f.configured("submission-gate-create",List.of(source))));
|
||||
var request=new DispatchCommands.SubmitReview(created.recordVersion(),"submission-gate-original",null);
|
||||
var submitted=f.tx.execute(s->f.service.submitReview(f.actor,created.dispatchNo(),request));
|
||||
var detail=f.tx.execute(s->f.service.detail(f.actor,created.dispatchNo()));
|
||||
assertThat(detail.reviewState()).isEqualTo(ReviewState.DISPATCH_PENDING);
|
||||
assertThat(detail.actions().get(Action.SUBMIT_REVIEW).allowed()).isFalse();
|
||||
var before=f.json.writeValueAsString(f.repository.find(created.dispatchNo()));
|
||||
var commands=f.db.queryForList("SELECT id,idempotency_key,record_version,command_hash FROM ll_dispatch_command ORDER BY id");
|
||||
assertThatThrownBy(()->f.tx.execute(s->f.service.submitReview(f.actor,created.dispatchNo(),
|
||||
new DispatchCommands.SubmitReview(detail.recordVersion(),"submission-gate-duplicate",null))))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("当前审核已提交");
|
||||
assertThat(f.json.writeValueAsString(f.repository.find(created.dispatchNo()))).isEqualTo(before);
|
||||
assertThat(f.db.queryForList("SELECT id,idempotency_key,record_version,command_hash FROM ll_dispatch_command ORDER BY id")).isEqualTo(commands);
|
||||
var replay=f.tx.execute(s->f.service.submitReview(f.actor,created.dispatchNo(),request));
|
||||
assertThat(replay.replayed()).isTrue();assertThat(replay.recordVersion()).isEqualTo(submitted.recordVersion());
|
||||
assertThat(replay.result()).isEqualTo(submitted.result());
|
||||
assertThat(f.json.writeValueAsString(f.repository.find(created.dispatchNo()))).isEqualTo(before);
|
||||
assertThat(f.db.queryForList("SELECT id,idempotency_key,record_version,command_hash FROM ll_dispatch_command ORDER BY id")).isEqualTo(commands);
|
||||
} finally { security.setAuthentication(previous); }
|
||||
}
|
||||
|
||||
/** 真实两级审核流证明三种可重提状态开放,而两种待审状态均关闭提交资格。 */
|
||||
@Test void submissionActionMatchesDraftRejectedAndPendingReviewStates() throws Exception {
|
||||
var security=org.springframework.security.core.context.SecurityContextHolder.getContext();var previous=security.getAuthentication();
|
||||
security.setAuthentication(new org.springframework.security.authentication.TestingAuthenticationToken("operator-test","",
|
||||
"platform:overseas-warehouse:dispatch:list","platform:overseas-warehouse:dispatch:edit",
|
||||
"platform:fleet:trip:dispatch-review","platform:fleet:trip:loading-review"));
|
||||
try {
|
||||
var f=new Fixture();var source=f.source(1,"101",501,"ONT8",3);
|
||||
var created=f.tx.execute(s->f.service.create(f.actor,f.configured("submission-states-create",List.of(source))));
|
||||
assertSubmissionGate(f,created.dispatchNo(),ReviewState.DRAFT,true);
|
||||
var submitted=f.tx.execute(s->f.service.submitReview(f.actor,created.dispatchNo(),new DispatchCommands.SubmitReview(created.recordVersion(),"submission-states-1",null)));
|
||||
assertSubmissionGate(f,created.dispatchNo(),ReviewState.DISPATCH_PENDING,false);
|
||||
var rejected=f.tx.execute(s->f.service.review(f.actor,created.dispatchNo(),false,new DispatchCommands.ReviewCommand(submitted.recordVersion(),"submission-states-dispatch-rejected",Decision.REJECTED,"核对调度")));
|
||||
assertSubmissionGate(f,created.dispatchNo(),ReviewState.DISPATCH_REJECTED,true);
|
||||
var resubmitted=f.tx.execute(s->f.service.submitReview(f.actor,created.dispatchNo(),new DispatchCommands.SubmitReview(rejected.recordVersion(),"submission-states-2",null)));
|
||||
var approved=f.tx.execute(s->f.service.review(f.actor,created.dispatchNo(),false,new DispatchCommands.ReviewCommand(resubmitted.recordVersion(),"submission-states-dispatch-approved",Decision.APPROVED,null)));
|
||||
assertSubmissionGate(f,created.dispatchNo(),ReviewState.LOADING_PENDING,false);
|
||||
var loadingRejected=f.tx.execute(s->f.service.review(f.actor,created.dispatchNo(),true,new DispatchCommands.ReviewCommand(approved.recordVersion(),"submission-states-loading-rejected",Decision.REJECTED,"核对装载")));
|
||||
assertSubmissionGate(f,created.dispatchNo(),ReviewState.LOADING_REJECTED,true);
|
||||
f.tx.execute(s->f.service.submitReview(f.actor,created.dispatchNo(),new DispatchCommands.SubmitReview(loadingRejected.recordVersion(),"submission-states-3",null)));
|
||||
assertSubmissionGate(f,created.dispatchNo(),ReviewState.DISPATCH_PENDING,false);
|
||||
} finally { security.setAuthentication(previous); }
|
||||
}
|
||||
|
||||
/** 从真实详情DTO读取动作资格,避免只测内部枚举或权限缺失导致的假阴性。 */
|
||||
private static void assertSubmissionGate(Fixture f,String number,ReviewState state,boolean allowed) {
|
||||
var detail=f.tx.execute(s->f.service.detail(f.actor,number));
|
||||
assertThat(detail.reviewState()).isEqualTo(state);assertThat(detail.actions().get(Action.SUBMIT_REVIEW).allowed()).isEqualTo(allowed);
|
||||
}
|
||||
|
||||
/** 两级顺序、驳回重提、原结果恢复和零扫码退回均实际提交到同一整车。 */
|
||||
@Test void reviewsUseActualRootRoundAndImmutableCommandsBeforeAndAfterLoading() throws Exception {
|
||||
var f=new Fixture();var source=f.source(1,"101",501,"ONT8",3);
|
||||
|
||||
Reference in New Issue
Block a user