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 29ea160f..6a04ed30 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 @@ -48,8 +48,8 @@ import org.springblade.transport.pojo.entity.ExceptionDisposal; import org.springblade.transport.pojo.entity.TransportVehicle; import org.springblade.transport.service.ITransportVehicleService; import org.springblade.transport.support.CertificationGate; +import org.springblade.transport.support.LiveProcessConfigSupport; import org.springblade.transport.support.VehiclePlateSupport; -import org.springblade.transport.pojo.entity.ProcessConfig; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.entity.WaybillEnroutePunch; import org.springblade.transport.pojo.entity.WaybillNodePunch; @@ -1185,55 +1185,16 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { } /** - * 优先用项目启用中的过程配置;若动态配置无打卡节点,回退运单快照 processJson, - * 避免项目配置改坏后司机端打卡页空白。 + * 优先用项目启用中的过程配置;若动态配置无打卡节点,回退运单快照, + * 避免项目配置改坏后司机端打卡页空白。取法与规则都在 {@link LiveProcessConfigSupport}。 */ private String resolveProcessJson(Waybill waybill) { if (waybill == null) { return null; } - String snapshot = waybill.getProcessJson(); - String live = loadLiveProcessConfigJson(waybill.getProjectId()); - if (Func.isNotEmpty(live)) { - if (!WaybillProcessSupport.listDriverPunchNodes(live).isEmpty()) { - return live; - } - // 动态配置存在但无可打卡节点:仍回退快照 - if (Func.isNotEmpty(snapshot) - && !WaybillProcessSupport.listDriverPunchNodes(snapshot).isEmpty()) { - return snapshot; - } - return live; - } - return snapshot; - } - - private String loadLiveProcessConfigJson(Long projectId) { - if (projectId == null) { - return null; - } - String projectIdStr = String.valueOf(projectId); - return processConfigService.list(Wrappers.lambdaQuery() - .eq(ProcessConfig::getStatus, 1) - .eq(ProcessConfig::getIsDeleted, 0) - .like(ProcessConfig::getProjectIds, projectIdStr) - .orderByDesc(ProcessConfig::getUpdateTime) - .orderByDesc(ProcessConfig::getCreateTime)) - .stream() - .filter(cfg -> containsProjectId(cfg.getProjectIds(), projectIdStr)) - .map(ProcessConfig::getNodeConfigJson) - .filter(Func::isNotEmpty) - .findFirst() - .orElse(null); - } - - private boolean containsProjectId(String projectIds, String projectId) { - if (Func.isEmpty(projectIds) || Func.isEmpty(projectId)) { - return false; - } - return Arrays.stream(projectIds.split(",")) - .map(String::trim) - .anyMatch(projectId::equals); + return LiveProcessConfigSupport.resolveForPunch( + LiveProcessConfigSupport.byProject(processConfigService, waybill.getProjectId()), + waybill.getProcessJson()); } /** diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java index 9e8dd2ff..bfefd237 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java @@ -30,7 +30,6 @@ import lombok.RequiredArgsConstructor; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.tool.utils.Func; import org.springblade.transport.pojo.entity.LoadingManage; -import org.springblade.transport.pojo.entity.ProcessConfig; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.TripQueryVO; import org.springblade.transport.pojo.vo.TripStationVO; @@ -40,12 +39,12 @@ import org.springblade.transport.service.ILoadingManageService; import org.springblade.transport.service.IProcessConfigService; import org.springblade.transport.service.ITripQueryService; import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.support.LiveProcessConfigSupport; import org.springblade.transport.support.WaybillProcessSupport; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.ArrayList; -import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.LinkedHashMap; @@ -287,6 +286,7 @@ public class TripQueryServiceImpl implements ITripQueryService { * 按单票路径的口径把生效中的过程配置写回内存里的行:项目启用中的配置优先,它没有可打卡节点时回退运单快照。 *

* 只改内存里的对象,分档与卡片下发都读它,不落库;生效配置一次取回后按项目复用,不逐条回查。 + * 取法与二选一规则都在 {@link LiveProcessConfigSupport},这里只做批量写回。 */ private void applyLiveProcessConfig(List rows) { if (Func.isEmpty(rows)) { @@ -297,60 +297,18 @@ public class TripQueryServiceImpl implements ITripQueryService { if (projectIds.isEmpty()) { return; } - Map live = liveProcessConfigs(projectIds); + Map live = LiveProcessConfigSupport.byProjects(processConfigService, projectIds); if (live.isEmpty()) { return; } for (Waybill row : rows) { String liveJson = live.get(row.getProjectId()); if (Func.isNotEmpty(liveJson)) { - row.setProcessJson(resolveProcessJson(liveJson, row.getProcessJson())); + row.setProcessJson(LiveProcessConfigSupport.resolveForPunch(liveJson, row.getProcessJson())); } } } - /** 生效配置与快照二选一:生效配置没有可打卡节点时回退快照,两边都取不到节点就按生效配置来 */ - private String resolveProcessJson(String liveJson, String snapshot) { - if (WaybillProcessSupport.listDriverPunchNodes(liveJson).isEmpty() - && Func.isNotEmpty(snapshot) - && !WaybillProcessSupport.listDriverPunchNodes(snapshot).isEmpty()) { - return snapshot; - } - return liveJson; - } - - /** 生效中的过程配置:项目 → 节点配置 JSON,取最近更新的一条 */ - private Map liveProcessConfigs(Set projectIds) { - List configs = processConfigService.list(Wrappers.lambdaQuery() - .eq(ProcessConfig::getStatus, 1) - .eq(ProcessConfig::getIsDeleted, 0) - .orderByDesc(ProcessConfig::getUpdateTime) - .orderByDesc(ProcessConfig::getCreateTime)); - Map live = new LinkedHashMap<>(); - for (Long projectId : projectIds) { - if (projectId == null) { - continue; - } - String projectIdStr = String.valueOf(projectId); - for (ProcessConfig config : configs) { - if (Func.isEmpty(config.getNodeConfigJson()) - || !containsProjectId(config.getProjectIds(), projectIdStr)) { - continue; - } - live.put(projectId, config.getNodeConfigJson()); - break; - } - } - return live; - } - - private boolean containsProjectId(String projectIds, String projectId) { - if (Func.isEmpty(projectIds) || Func.isEmpty(projectId)) { - return false; - } - return Arrays.stream(projectIds.split(",")).map(String::trim).anyMatch(projectId::equals); - } - /** * 有子单被拒、且这趟车还没结束的运次键:整单拒绝后这趟车不再算待处理 */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index fe4a5160..f78a0512 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -45,7 +45,6 @@ import org.springblade.transport.mapper.WaybillMapper; import org.springblade.transport.mapper.WaybillNodePunchMapper; import org.springblade.transport.pojo.entity.LoadingManage; import org.springblade.transport.pojo.entity.ContractManage; -import org.springblade.transport.pojo.entity.ProcessConfig; import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.entity.WaybillEnroutePunch; @@ -66,6 +65,7 @@ import org.springblade.transport.service.IProjectApplyService; import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.support.LiveProcessConfigSupport; import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.support.WaybillProcessSupport; import org.springblade.transport.wrapper.WaybillWrapper; @@ -822,27 +822,20 @@ public class WaybillServiceImpl extends BaseServiceImpl return null; } - /** 动态过程配置优先,回退运单快照 */ + /** + * 生效配置优先,回退运单快照。 + *

+ * 取法与二选一规则在 {@link LiveProcessConfigSupport};这里不用「生效配置没有可打卡节点就回退快照」 + * 那一版——接单需求、节点门禁、节点订正都读这份配置判断节点存不存在,生效配置是权威, + * 不能因为快照里有节点而被顶掉。 + */ private String resolveLiveProcessJson(Waybill waybill) { - if (waybill.getProjectId() != null) { - String projectId = String.valueOf(waybill.getProjectId()); - String live = processConfigService.list(Wrappers.lambdaQuery() - .eq(ProcessConfig::getStatus, 1) - .eq(ProcessConfig::getIsDeleted, 0) - .like(ProcessConfig::getProjectIds, projectId) - .orderByDesc(ProcessConfig::getUpdateTime) - .orderByDesc(ProcessConfig::getCreateTime)) - .stream() - .filter(cfg -> containsProjectId(cfg.getProjectIds(), projectId)) - .map(ProcessConfig::getNodeConfigJson) - .filter(Func::isNotEmpty) - .findFirst() - .orElse(null); - if (Func.isNotEmpty(live)) { - return live; - } + if (waybill == null) { + return null; } - return waybill.getProcessJson(); + return LiveProcessConfigSupport.resolve( + LiveProcessConfigSupport.byProject(processConfigService, waybill.getProjectId()), + waybill.getProcessJson()); } private Map> buildVoucherTypesIndex(String processJson) { @@ -1175,15 +1168,6 @@ public class WaybillServiceImpl extends BaseServiceImpl waybill.setDriverRejectReason(null); } - private boolean containsProjectId(String projectIds, String projectId) { - if (Func.isEmpty(projectIds)) { - return false; - } - return List.of(projectIds.split(",")).stream() - .map(String::trim) - .anyMatch(projectId::equals); - } - @Override @Transactional(rollbackFor = Exception.class) public BusinessRemoveResultVO removeWaybill(String ids) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LiveProcessConfigSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LiveProcessConfigSupport.java new file mode 100644 index 00000000..366e03b1 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LiveProcessConfigSupport.java @@ -0,0 +1,161 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.support; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.pojo.entity.ProcessConfig; +import org.springblade.transport.service.IProcessConfigService; + +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * 过程配置的取法与「生效配置还是运单快照」的二选一规则,只此一处。 + *

+ * 生效配置({@code blade_process_config})按项目生效,是活的:改一次,该项目下所有运单下次读取 + * 都按新配置呈现;运单创建时另存一份 {@code process_json} 快照,只在生效配置取不到时兜底。 + * 生效配置按更新时间倒序取第一条命中该项目的,项目集合是逗号串,故先 LIKE 粗筛再按逗号分段精确比对 —— + * 只靠 LIKE 会把「1234」命中到「12345」上。 + *

+ * 两个二选一版本的区别是**得到什么结果**,不是风格: + *

+ */ +public final class LiveProcessConfigSupport { + + private LiveProcessConfigSupport() { + } + + /** + * 单个项目的生效配置 JSON,没有则返回 null + * + * @param service 过程配置服务 + * @param projectId 项目主键 + */ + public static String byProject(IProcessConfigService service, Long projectId) { + if (service == null || projectId == null) { + return null; + } + String projectIdStr = String.valueOf(projectId); + return service.list(Wrappers.lambdaQuery() + .eq(ProcessConfig::getStatus, 1) + .eq(ProcessConfig::getIsDeleted, 0) + .like(ProcessConfig::getProjectIds, projectIdStr) + .orderByDesc(ProcessConfig::getUpdateTime) + .orderByDesc(ProcessConfig::getCreateTime)) + .stream() + .filter(config -> containsProjectId(config.getProjectIds(), projectIdStr)) + .map(ProcessConfig::getNodeConfigJson) + .filter(Func::isNotEmpty) + .findFirst() + .orElse(null); + } + + /** + * 一批项目的生效配置:项目主键 → 节点配置 JSON,一次查全再按项目匹配,供列表页避免逐条回查 + * + * @param service 过程配置服务 + * @param projectIds 项目主键集合,允许含 null + */ + public static Map byProjects(IProcessConfigService service, Collection projectIds) { + Map live = new LinkedHashMap<>(); + if (service == null || Func.isEmpty(projectIds)) { + return live; + } + Set wanted = new LinkedHashSet<>(); + projectIds.stream().filter(Objects::nonNull).forEach(wanted::add); + if (wanted.isEmpty()) { + return live; + } + List configs = service.list(Wrappers.lambdaQuery() + .eq(ProcessConfig::getStatus, 1) + .eq(ProcessConfig::getIsDeleted, 0) + .orderByDesc(ProcessConfig::getUpdateTime) + .orderByDesc(ProcessConfig::getCreateTime)); + if (Func.isEmpty(configs)) { + return live; + } + for (Long projectId : wanted) { + String projectIdStr = String.valueOf(projectId); + for (ProcessConfig config : configs) { + if (Func.isEmpty(config.getNodeConfigJson()) + || !containsProjectId(config.getProjectIds(), projectIdStr)) { + continue; + } + live.put(projectId, config.getNodeConfigJson()); + break; + } + } + return live; + } + + /** + * 生效配置优先,取不到时回退运单快照 + * + * @param liveJson 生效配置 JSON,可为空 + * @param snapshot 运单快照 JSON,可为空 + */ + public static String resolve(String liveJson, String snapshot) { + return Func.isNotEmpty(liveJson) ? liveJson : snapshot; + } + + /** + * 生效配置优先;生效配置没有可打卡节点时回退快照,两边都没有节点就按生效配置来 + * + * @param liveJson 生效配置 JSON,可为空 + * @param snapshot 运单快照 JSON,可为空 + */ + public static String resolveForPunch(String liveJson, String snapshot) { + if (Func.isEmpty(liveJson)) { + return snapshot; + } + if (!WaybillProcessSupport.listDriverPunchNodes(liveJson).isEmpty()) { + return liveJson; + } + if (Func.isNotEmpty(snapshot) && !WaybillProcessSupport.listDriverPunchNodes(snapshot).isEmpty()) { + return snapshot; + } + return liveJson; + } + + /** 项目集合是逗号串,必须按逗号分段精确比对 */ + private static boolean containsProjectId(String projectIds, String projectId) { + if (Func.isEmpty(projectIds) || Func.isEmpty(projectId)) { + return false; + } + return Arrays.stream(projectIds.split(",")) + .map(String::trim) + .anyMatch(projectId::equals); + } +} diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LiveProcessConfigSupportTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LiveProcessConfigSupportTest.java new file mode 100644 index 00000000..7ae583a9 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LiveProcessConfigSupportTest.java @@ -0,0 +1,158 @@ +package org.springblade.transport.support; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +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.ArgumentMatchers; +import org.springblade.transport.pojo.entity.ProcessConfig; +import org.springblade.transport.service.IProcessConfigService; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +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.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * 生效配置的取法与「生效配置 vs 快照」二选一的唯一实现处。 + *

+ * 两条规则必须分得清:接单需求与节点门禁读 {@code resolve}(生效配置是权威,不能因为快照里有节点被顶掉), + * 司机端打卡页读 {@code resolveForPunch}(生效配置被改坏成没有可打卡节点时回退快照,避免打卡页空白)。 + * 排序由 SQL 负责(更新时间倒序、创建时间倒序),这里按「已排好序的列表」构造输入。 + */ +class LiveProcessConfigSupportTest { + + /** 有可打卡节点(装货,punch=是) */ + private static final String PUNCH_JSON = + "[{\"key\":\"load\",\"name\":\"装货\",\"enabled\":true,\"punch\":true,\"location\":true}]"; + /** 一条可打卡节点都没有(装货被关掉 punch) */ + private static final String NO_PUNCH_JSON = + "[{\"key\":\"load\",\"name\":\"装货\",\"enabled\":true,\"punch\":false}]"; + /** 另一个项目用的、与 PUNCH_JSON 可区分的一份 */ + private static final String OTHER_PUNCH_JSON = + "[{\"key\":\"unload\",\"name\":\"卸货\",\"enabled\":true,\"punch\":true,\"location\":true}]"; + + private IProcessConfigService processConfigService; + + @BeforeAll + static void initTableInfo() { + // 构造 LambdaQueryWrapper 需要 MyBatis-Plus 的实体元数据缓存 + MybatisConfiguration configuration = new MybatisConfiguration(); + MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, ""); + assistant.setCurrentNamespace("test"); + TableInfoHelper.initTableInfo(assistant, ProcessConfig.class); + } + + @BeforeEach + void setUp() { + processConfigService = mock(IProcessConfigService.class); + } + + private void stubConfigs(ProcessConfig... configs) { + when(processConfigService.list(anyConfigWrapper())).thenReturn(List.of(configs)); + } + + private static Wrapper anyConfigWrapper() { + return ArgumentMatchers.>any(); + } + + private static ProcessConfig config(String projectIds, String nodeConfigJson) { + ProcessConfig config = new ProcessConfig(); + config.setProjectIds(projectIds); + config.setNodeConfigJson(nodeConfigJson); + return config; + } + + @Test + @DisplayName("单项目:命中项目的第一条生效配置(列表已按更新时间倒序)") + void singleProjectTakesFirstHit() { + stubConfigs(config("999", OTHER_PUNCH_JSON), config("1234,5678", PUNCH_JSON)); + + assertEquals(PUNCH_JSON, LiveProcessConfigSupport.byProject(processConfigService, 1234L)); + } + + @Test + @DisplayName("单项目:项目 id 按逗号分段精确比对,1234 不命中 12345") + void singleProjectMatchesExactId() { + stubConfigs(config("12345", PUNCH_JSON)); + + assertNull(LiveProcessConfigSupport.byProject(processConfigService, 1234L)); + } + + @Test + @DisplayName("单项目:没有生效配置、或项目为空时返回 null") + void singleProjectWithoutConfig() { + stubConfigs(); + + assertNull(LiveProcessConfigSupport.byProject(processConfigService, 1234L)); + assertNull(LiveProcessConfigSupport.byProject(processConfigService, null)); + assertNull(LiveProcessConfigSupport.byProject(null, 1234L)); + } + + @Test + @DisplayName("单项目:节点配置为空的配置跳过,取后面那条有内容的") + void singleProjectSkipsEmptyNodeJson() { + stubConfigs(config("1234", ""), config("1234", PUNCH_JSON)); + + assertEquals(PUNCH_JSON, LiveProcessConfigSupport.byProject(processConfigService, 1234L)); + } + + @Test + @DisplayName("批量:一次查全后按项目分别命中,未命中的项目不进结果") + void batchByProject() { + stubConfigs(config("1234", PUNCH_JSON), config("5678", OTHER_PUNCH_JSON)); + + Map live = LiveProcessConfigSupport.byProjects(processConfigService, List.of(1234L, 5678L, 4321L)); + + assertEquals(2, live.size()); + assertEquals(PUNCH_JSON, live.get(1234L)); + assertEquals(OTHER_PUNCH_JSON, live.get(5678L)); + assertTrue(live.containsKey(1234L)); + } + + @Test + @DisplayName("resolve:生效配置优先,取不到才用快照") + void resolvePrefersLive() { + assertEquals(PUNCH_JSON, LiveProcessConfigSupport.resolve(PUNCH_JSON, NO_PUNCH_JSON)); + assertEquals(NO_PUNCH_JSON, LiveProcessConfigSupport.resolve(null, NO_PUNCH_JSON)); + assertNull(LiveProcessConfigSupport.resolve(null, null)); + } + + @Test + @DisplayName("resolveForPunch:生效配置没有可打卡节点时才回退快照,两边都没有节点仍用生效配置") + void resolveForPunchFallsBackToSnapshot() { + assertEquals(PUNCH_JSON, LiveProcessConfigSupport.resolveForPunch(PUNCH_JSON, NO_PUNCH_JSON), + "生效配置有可打卡节点:用生效配置"); + assertEquals(PUNCH_JSON, LiveProcessConfigSupport.resolveForPunch(NO_PUNCH_JSON, PUNCH_JSON), + "生效配置没有可打卡节点、快照有:回退快照"); + assertEquals(NO_PUNCH_JSON, LiveProcessConfigSupport.resolveForPunch(NO_PUNCH_JSON, NO_PUNCH_JSON), + "两边都没有可打卡节点:用生效配置"); + assertEquals(NO_PUNCH_JSON, LiveProcessConfigSupport.resolveForPunch(null, NO_PUNCH_JSON), + "项目没有生效配置:用快照"); + assertEquals(NO_PUNCH_JSON, LiveProcessConfigSupport.resolveForPunch(NO_PUNCH_JSON, null), + "快照为空:用生效配置"); + } + + @Test + @DisplayName("生效配置只查一次:批量取法与单项目取法都不逐条回查项目") + void configQueryIsInvokedOncePerCall() { + stubConfigs(config("1234", PUNCH_JSON), config("5678", OTHER_PUNCH_JSON)); + + LiveProcessConfigSupport.byProjects(processConfigService, List.of(1234L, 5678L)); + + verify(processConfigService, times(1)).list(ArgumentMatchers.>any()); + verify(processConfigService, never()).getById(any()); + } +}