diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java index 337e2fd0..03f85510 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java @@ -54,6 +54,12 @@ public class DriverPunchNodeVO implements Serializable { @Schema(description = "该节点自身是否被上报了未了结异常(仅异常所在节点为是,其后被牵连阻断的节点为否)") private Boolean hasException; + @Schema(description = "该运单是否存在未了结的整单异常;整单异常不归属任何节点,故每个节点都带此标记") + private Boolean hasWaybillException; + + @Schema(description = "与该节点相关的未了结异常ID,按上报时间升序;相关 = 归属该节点 ∪ 整单异常") + private List relatedExceptionIds = new ArrayList<>(); + @Schema(description = "阻断该节点的异常处置ID(司机端「异常详情」跳转用);未被阻断或异常已不存在时为空") @JsonSerialize(using = ToStringSerializer.class) private Long blockedExceptionId; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java index fc1c3297..c9138ac3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java @@ -1103,6 +1103,24 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { } } + // 「整单异常」标签与相关异常 id:整单异常不归属任何节点(节点编码为空), + // 它作用于整张运单,所以每个可打卡节点——包括已打卡的与在途的——都挂标签、都算相关。 + // 相关只有两种来源:归属本节点的、以及整单的;被前序节点异常牵连阻断的节点 + // 只是不能打卡,那条异常并不归属它,故不进入它的相关列表。 + List unresolved = unresolvedExceptions(waybill.getId()); + List unresolvedCodes = unresolved.stream() + .map(item -> Func.toStr(item.getNodeCode(), "").trim()) + .collect(Collectors.toList()); + List unresolvedIds = unresolved.stream() + .map(ExceptionDisposal::getId) + .collect(Collectors.toList()); + boolean hasWaybillException = WaybillProcessSupport.hasWaybillLevelException(unresolvedCodes); + for (DriverPunchNodeVO vo : nodes) { + vo.setHasWaybillException(hasWaybillException); + vo.setRelatedExceptionIds( + WaybillProcessSupport.relatedExceptionIds(vo.getKey(), unresolvedCodes, unresolvedIds)); + } + for (DriverPunchNodeVO vo : nodes) { if (!Boolean.TRUE.equals(vo.getDone()) && !Boolean.TRUE.equals(vo.getBlocked()) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java index a3d97307..c56778a7 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java @@ -385,6 +385,9 @@ public class ExceptionDisposalServiceImpl private LambdaQueryWrapper buildQuery(ExceptionDisposalVO query) { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .eq(ExceptionDisposal::getIsDeleted, 0) + // 按运单精确过滤,供司机端「本运单的异常」列表使用。 + // 不用运单号:它是模糊匹配,YD001 会命中 YD0011。 + .eq(query.getWaybillId() != null, ExceptionDisposal::getWaybillId, query.getWaybillId()) .like(Func.isNotEmpty(query.getWaybillNo()), ExceptionDisposal::getWaybillNo, query.getWaybillNo()) .like(Func.isNotEmpty(query.getVehicleNo()), ExceptionDisposal::getVehicleNo, query.getVehicleNo()) .like(Func.isNotEmpty(query.getReporterName()), ExceptionDisposal::getReporterName, query.getReporterName()) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java index bdc9c0fb..f890f5f7 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java @@ -390,6 +390,45 @@ public final class WaybillProcessSupport { return DISPOSAL_COMPLETED.equalsIgnoreCase(status) || DISPOSAL_REVOKED.equalsIgnoreCase(status); } + /** + * 是否存在未了结的整单异常。 + *

+ * 整单异常不归属任何过程节点,它的节点编码为空;它作用于整张运单, + * 所以该运单每个可打卡节点都要挂上「整单异常」标记。 + */ + public static boolean hasWaybillLevelException(List unresolvedNodeCodes) { + if (unresolvedNodeCodes == null) { + return false; + } + return unresolvedNodeCodes.stream().anyMatch(code -> stringVal(code).isEmpty()); + } + + /** + * 与某节点相关的未了结异常 id,一律以字符串返回。 + *

+ * 相关只有两种来源:归属该节点的异常,以及整单异常(节点编码为空,对每个节点都算相关)。 + * 被前序节点异常牵连阻断的节点不算相关——它自己并没有出过事。 + *

+ * 顺序保持入参顺序,调用方按上报时间升序取出未了结异常即可。 + * id 转成字符串是因为主键是 int64,前端直收数字会丢精度。 + */ + public static List relatedExceptionIds(String nodeKey, List unresolvedNodeCodes, + List unresolvedIds) { + if (unresolvedNodeCodes == null || unresolvedIds == null) { + return Collections.emptyList(); + } + String key = stringVal(nodeKey); + List related = new ArrayList<>(); + int size = Math.min(unresolvedNodeCodes.size(), unresolvedIds.size()); + for (int i = 0; i < size; i++) { + String code = stringVal(unresolvedNodeCodes.get(i)); + if (code.isEmpty() || code.equalsIgnoreCase(key)) { + related.add(String.valueOf(unresolvedIds.get(i))); + } + } + return related; + } + /** * 过程配置里的在途节点 key;配置中无在途节点时返回空串。 */ diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverPunchNodeCardTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverPunchNodeCardTest.java new file mode 100644 index 00000000..a3669df1 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverPunchNodeCardTest.java @@ -0,0 +1,232 @@ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.transport.mapper.ExceptionDisposalMapper; +import org.springblade.transport.mapper.WaybillEnroutePunchMapper; +import org.springblade.transport.mapper.WaybillNodePunchMapper; +import org.springblade.transport.pojo.entity.ExceptionDisposal; +import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.entity.WaybillEnroutePunch; +import org.springblade.transport.pojo.entity.WaybillNodePunch; +import org.springblade.transport.pojo.vo.DriverPunchNodeVO; +import org.springblade.transport.service.IDriverService; +import org.springblade.transport.service.IProcessConfigService; +import org.springblade.transport.service.ITransportVehicleService; +import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.support.CertificationGate; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * 司机端节点卡片上的异常标签与「相关异常」列表。 + *

+ * 两个字段回答两个不同的问题:这个节点上挂着哪些未了结异常、这张运单有没有整单异常。 + * 整单异常不归属任何节点,所以每个可打卡节点都挂它——包括已打卡的与在途的; + * 而某条节点异常只算它自己那个节点,被它牵连阻断的后续节点不算相关。 + */ +class DriverPunchNodeCardTest { + + private static final Long WAYBILL_ID = 5501L; + + /** 接单不参与打卡,故不出现在节点卡片里 */ + private static final String PROCESS_JSON = """ + [{"key":"accept","name":"接单","type":"confirm","enabled":true,"punch":true}, + {"key":"load","name":"装货","type":"cargo","enabled":true,"punch":true}, + {"key":"transit","name":"在途","type":"transit","enabled":true,"punch":true}, + {"key":"sign","name":"签收","type":"cargo","enabled":true,"punch":true}] + """; + + private IWaybillService waybillService; + private WaybillNodePunchMapper nodePunchMapper; + private WaybillEnroutePunchMapper enroutePunchMapper; + private ExceptionDisposalMapper exceptionDisposalMapper; + private DriverWaybillServiceImpl service; + + @BeforeAll + static void initTableInfo() { + // 构造 LambdaQueryWrapper 需要 MyBatis-Plus 的实体元数据缓存 + MybatisConfiguration configuration = new MybatisConfiguration(); + MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, ""); + assistant.setCurrentNamespace("test"); + TableInfoHelper.initTableInfo(assistant, WaybillNodePunch.class); + TableInfoHelper.initTableInfo(assistant, WaybillEnroutePunch.class); + TableInfoHelper.initTableInfo(assistant, ExceptionDisposal.class); + } + + @BeforeEach + void setUp() { + waybillService = mock(IWaybillService.class); + nodePunchMapper = mock(WaybillNodePunchMapper.class); + enroutePunchMapper = mock(WaybillEnroutePunchMapper.class); + exceptionDisposalMapper = mock(ExceptionDisposalMapper.class); + service = new DriverWaybillServiceImpl( + waybillService, + mock(IDriverService.class), + mock(IProcessConfigService.class), + mock(ITransportVehicleService.class), + mock(CertificationGate.class), + enroutePunchMapper, + nodePunchMapper, + exceptionDisposalMapper); + + Waybill waybill = waybill(); + when(waybillService.getById(WAYBILL_ID)).thenReturn(waybill); + when(waybillService.syncDriverAcceptState(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(nodePunchMapper.selectList(any())).thenReturn(List.of(nodePunch("load"))); + when(enroutePunchMapper.selectOne(any())).thenReturn(null); + when(enroutePunchMapper.selectList(any())).thenReturn(List.of()); + when(exceptionDisposalMapper.selectList(any())).thenReturn(List.of()); + } + + @Test + @DisplayName("整单异常挂到每个可打卡节点,含已打卡的与在途的") + void waybillLevelExceptionIsTaggedOnEveryNode() { + stubUnresolved(nodeException("load", 11L), waybillException(12L)); + + Map nodes = punchNodes(); + + assertEquals(3, nodes.size(), "接单不是打卡节点,不该出现在卡片里"); + assertTrue(Boolean.TRUE.equals(nodes.get("load").getDone()), "装货这条流水已打卡"); + for (DriverPunchNodeVO node : nodes.values()) { + assertTrue(Boolean.TRUE.equals(node.getHasWaybillException()), + "整单异常作用于整张运单," + node.getKey() + " 也该挂标签"); + assertTrue(node.getRelatedExceptionIds().contains("12"), + node.getKey() + " 的相关异常里应当有整单异常"); + } + } + + @Test + @DisplayName("节点自己的异常只进它自己的相关列表,不牵连后续节点") + void nodeExceptionOnlyBelongsToItsOwnNode() { + stubUnresolved(nodeException("load", 11L)); + + Map nodes = punchNodes(); + + assertTrue(Boolean.TRUE.equals(nodes.get("load").getHasException())); + assertEquals(List.of("11"), nodes.get("load").getRelatedExceptionIds()); + assertFalse(Boolean.TRUE.equals(nodes.get("sign").getHasException()), + "签收只是被装货的异常牵连阻断,不挂「异常」标签"); + assertTrue(nodes.get("sign").getRelatedExceptionIds().isEmpty()); + assertFalse(Boolean.TRUE.equals(nodes.get("sign").getHasWaybillException())); + } + + @Test + @DisplayName("两种异常同时存在时,该节点的相关列表有两条且按上报时间升序") + void bothKindsAreListedInReportOrder() { + stubUnresolved(nodeException("load", 11L), waybillException(12L)); + + DriverPunchNodeVO load = punchNodes().get("load"); + + assertEquals(List.of("11", "12"), load.getRelatedExceptionIds()); + } + + @Test + @DisplayName("异常全部了结后两个字段都回到「无」") + void withoutUnresolvedExceptionsNoTagAndNoRelatedIds() { + Map nodes = punchNodes(); + + for (DriverPunchNodeVO node : nodes.values()) { + assertFalse(Boolean.TRUE.equals(node.getHasWaybillException())); + assertTrue(node.getRelatedExceptionIds().isEmpty()); + } + } + + @Test + @DisplayName("未了结异常按上报时间升序取出,卡片顺序才有意义") + void unresolvedExceptionsAreReadInReportOrder() { + stubUnresolved(nodeException("load", 11L)); + + punchNodes(); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); + verify(exceptionDisposalMapper, atLeastOnce()).selectList(captor.capture()); + for (LambdaQueryWrapper wrapper : captor.getAllValues()) { + String sql = wrapper.getTargetSql(); + assertTrue(sql.contains("report_time"), "未了结异常要按上报时间取:" + sql); + assertTrue(sql.contains("disposal_status"), "未了结的判据要落在处置状态上:" + sql); + } + } + + @Test + @DisplayName("相关异常 id 序列化成字符串,前端拿到大整数不会丢精度") + void relatedExceptionIdsSerializeAsStrings() { + stubUnresolved(nodeException("load", 9007199254740993L)); + + String json = JsonUtil.toJson(punchNodes().get("load")); + + assertTrue(json.contains("\"9007199254740993\""), json); + } + + // ------------------------------------------------------------ 夹具 + + private void stubUnresolved(ExceptionDisposal... disposals) { + when(exceptionDisposalMapper.selectList(any())).thenReturn(List.of(disposals)); + } + + private Map punchNodes() { + return service.detailPunchSnapshot(WAYBILL_ID).getPunchNodes().stream() + .collect(Collectors.toMap(DriverPunchNodeVO::getKey, Function.identity())); + } + + private ExceptionDisposal nodeException(String nodeCode, Long id) { + return exception(id, nodeCode, LocalDateTime.of(2026, 10, 6, 9, 30)); + } + + private ExceptionDisposal waybillException(Long id) { + return exception(id, null, LocalDateTime.of(2026, 10, 6, 10, 0)); + } + + private ExceptionDisposal exception(Long id, String nodeCode, LocalDateTime reportTime) { + ExceptionDisposal disposal = new ExceptionDisposal(); + disposal.setId(id); + disposal.setWaybillId(WAYBILL_ID); + disposal.setNodeCode(nodeCode); + disposal.setDisposalStatus("pending"); + disposal.setIsDeleted(0); + disposal.setReportTime(reportTime); + return disposal; + } + + private WaybillNodePunch nodePunch(String nodeCode) { + WaybillNodePunch punch = new WaybillNodePunch(); + punch.setId(6501L); + punch.setWaybillId(WAYBILL_ID); + punch.setNodeCode(nodeCode); + punch.setPunchTime(new Date()); + return punch; + } + + private Waybill waybill() { + Waybill waybill = new Waybill(); + waybill.setId(WAYBILL_ID); + waybill.setWaybillNo("YD202610070001"); + waybill.setProcessJson(PROCESS_JSON); + waybill.setBusinessStatus("running"); + waybill.setIsDeleted(0); + return waybill; + } + +} diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalListQueryTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalListQueryTest.java new file mode 100644 index 00000000..9d0dd4a1 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalListQueryTest.java @@ -0,0 +1,115 @@ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springblade.transport.mapper.ExceptionDisposalFollowRecordMapper; +import org.springblade.transport.mapper.ExceptionDisposalMapper; +import org.springblade.transport.pojo.entity.ExceptionDisposal; +import org.springblade.transport.pojo.vo.ExceptionDisposalVO; +import org.springblade.transport.service.IDriverWaybillService; +import org.springblade.transport.service.IWaybillService; + +import java.lang.reflect.Field; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * 异常处置列表按运单过滤。 + *

+ * 过滤必须落在运单主键上:运单号是模糊匹配,{@code YD001} 会命中 {@code YD0011}, + * 从某个节点点进来的「本运单异常」会多出别人的记录。 + */ +class ExceptionDisposalListQueryTest { + + private static final Long WAYBILL_ID = 5401L; + + private ExceptionDisposalMapper disposalMapper; + private ExceptionDisposalServiceImpl service; + + @BeforeAll + static void initTableInfo() { + MybatisConfiguration configuration = new MybatisConfiguration(); + MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, ""); + assistant.setCurrentNamespace("test"); + TableInfoHelper.initTableInfo(assistant, ExceptionDisposal.class); + } + + @BeforeEach + void setUp() throws Exception { + disposalMapper = mock(ExceptionDisposalMapper.class); + service = new ExceptionDisposalServiceImpl( + mock(ExceptionDisposalFollowRecordMapper.class), + mock(IWaybillService.class), + mock(IDriverWaybillService.class)); + inject("baseMapper", disposalMapper); + when(disposalMapper.selectPage(any(), any())).thenReturn(new Page(1, 10)); + } + + @Test + @DisplayName("传运单ID时按主键精确过滤,而不是拿运单号模糊匹配") + void waybillIdFilterIsExact() { + ExceptionDisposalVO query = new ExceptionDisposalVO(); + query.setWaybillId(WAYBILL_ID); + + LambdaQueryWrapper wrapper = capturedQuery(query); + + assertTrue(wrapper.getTargetSql().contains("waybill_id ="), + "按运单过滤要用等值:" + wrapper.getTargetSql()); + assertTrue(wrapper.getParamNameValuePairs().values().stream() + .anyMatch(value -> WAYBILL_ID.equals(value) + || (value instanceof Collection values && values.contains(WAYBILL_ID))), + "过滤条件要带上运单ID:" + wrapper.getParamNameValuePairs()); + assertFalse(wrapper.getTargetSql().contains("waybill_no"), + "运单号是 like,会把前缀相同的运单一起捞回来:" + wrapper.getTargetSql()); + } + + @Test + @DisplayName("不传运单ID时不加运单条件,列表原有入口行为不变") + void withoutWaybillIdNoWaybillCondition() { + LambdaQueryWrapper wrapper = capturedQuery(new ExceptionDisposalVO()); + + assertFalse(wrapper.getTargetSql().contains("waybill_id"), wrapper.getTargetSql()); + } + + // ------------------------------------------------------------ 夹具 + + private LambdaQueryWrapper capturedQuery(ExceptionDisposalVO query) { + IPage page = service.selectPage(new Page<>(1, 10), query); + assertTrue(page.getRecords().isEmpty()); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); + verify(disposalMapper).selectPage(any(), captor.capture()); + return captor.getValue(); + } + + private void inject(String field, Object value) throws Exception { + Class type = service.getClass(); + while (type != null) { + try { + Field declared = type.getDeclaredField(field); + declared.setAccessible(true); + declared.set(service, value); + return; + } catch (NoSuchFieldException ignored) { + type = type.getSuperclass(); + } + } + throw new IllegalStateException("未找到字段:" + field); + } + +} diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/WaybillProcessSupportTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/WaybillProcessSupportTest.java index 223a17ec..477f70ca 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/WaybillProcessSupportTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/WaybillProcessSupportTest.java @@ -20,6 +20,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; * 两边都调 {@link WaybillProcessSupport#blockedNodeKeys} 与 * {@link WaybillProcessSupport#unfinishedPrerequisiteKeys}, * 所以断言下在它们身上就覆盖了两条链路。 + *

+ * 同域还放着异常标签的组装规则:哪个节点该挂「整单异常」、哪些异常算与某节点相关。 */ class WaybillProcessSupportTest { @@ -129,4 +131,62 @@ class WaybillProcessSupportTest { "在途报的是「此刻在哪」,不该因为某个环节没打卡就报不了"); } + // ------------------------------------------------------------ 异常标签与相关异常 + + @Test + @DisplayName("整单异常对每个节点都算相关,节点自己的异常只算它自己那个节点") + void relatedExceptionsCoverWaybillLevelForEveryNode() { + List nodeCodes = Arrays.asList("load", ""); + List ids = Arrays.asList(11L, 12L); + + assertEquals(Arrays.asList("11", "12"), WaybillProcessSupport.relatedExceptionIds("load", nodeCodes, ids)); + assertEquals(Collections.singletonList("12"), WaybillProcessSupport.relatedExceptionIds("sign", nodeCodes, ids), + "装货的异常不归属签收——签收只是被它牵连阻断,不该显示成自己也出了事"); + } + + @Test + @DisplayName("相关异常 id 保持入参顺序,即上报时间升序") + void relatedExceptionIdsKeepReportOrder() { + List nodeCodes = Arrays.asList("", "load"); + List ids = Arrays.asList(21L, 22L); + + assertEquals(Arrays.asList("21", "22"), WaybillProcessSupport.relatedExceptionIds("load", nodeCodes, ids)); + } + + @Test + @DisplayName("节点编码忽略大小写") + void relatedExceptionIdsIgnoreCase() { + assertEquals(Collections.singletonList("31"), + WaybillProcessSupport.relatedExceptionIds( + "load", Collections.singletonList("LOAD"), Collections.singletonList(31L))); + } + + @Test + @DisplayName("id 一律下发字符串:主键是 int64,前端直收数字会丢精度") + void relatedExceptionIdsAreStrings() { + List ids = WaybillProcessSupport.relatedExceptionIds( + "load", Collections.singletonList("load"), Collections.singletonList(9007199254740993L)); + + assertEquals(Collections.singletonList("9007199254740993"), ids); + } + + @Test + @DisplayName("整单异常的判定只看有没有不归属节点的异常") + void waybillLevelExceptionFlag() { + assertTrue(WaybillProcessSupport.hasWaybillLevelException(Arrays.asList("load", ""))); + assertTrue(WaybillProcessSupport.hasWaybillLevelException(Collections.singletonList(null))); + assertFalse(WaybillProcessSupport.hasWaybillLevelException(Collections.singletonList("load"))); + assertFalse(WaybillProcessSupport.hasWaybillLevelException(Collections.emptyList())); + assertFalse(WaybillProcessSupport.hasWaybillLevelException(null)); + } + + @Test + @DisplayName("异常全部了结后,整单标记与相关 id 都回到「无」") + void withoutUnresolvedExceptionsNothingIsTagged() { + assertFalse(WaybillProcessSupport.hasWaybillLevelException(Collections.emptyList())); + assertTrue(WaybillProcessSupport + .relatedExceptionIds("load", Collections.emptyList(), Collections.emptyList()) + .isEmpty()); + } + }