✨ 配载提交与重新派单按项目实时配置逐张推导子运单状态并清除接单事实

This commit is contained in:
2026-10-18 14:27:44 +08:00
parent 543a690902
commit f684666a01
2 changed files with 538 additions and 4 deletions
@@ -28,7 +28,9 @@ import org.springblade.transport.pojo.vo.LoadingCarrierContractVO;
import org.springblade.transport.pojo.vo.LoadingManageVO;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.ILoadingManageService;
import org.springblade.transport.service.IProcessConfigService;
import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.transport.support.LiveProcessConfigSupport;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.WaybillProcessSupport;
import org.springblade.transport.wrapper.LoadingManageWrapper;
@@ -43,8 +45,10 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -76,6 +80,9 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
@Lazy
private IReceivablePayableDetailService receivablePayableDetailService;
@Resource
private IProcessConfigService processConfigService;
@Override
public IPage<LoadingManageVO> selectLoadingManagePage(IPage<LoadingManage> page, LoadingManageVO loadingManage) {
IPage<LoadingManage> entityPage = page(page, buildQuery(loadingManage));
@@ -163,7 +170,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()));
validateCarrierContract(loadingManage, true);
boolean result = saveOrUpdate(loadingManage);
syncAssociatedWaybills(loadingManage, STATUS_PENDING, oldWaybillIds);
writeWaybillsForDriver(loadingManage, oldWaybillIds);
return result;
}
@@ -412,8 +419,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()));
validateCarrierContract(loadingManage, true);
boolean result = updateById(loadingManage);
syncAssociatedWaybills(loadingManage, STATUS_PENDING, oldWaybillIds);
clearAssociatedWaybillAcceptRecords(loadingManage);
writeWaybillsForDriver(loadingManage, oldWaybillIds);
return result;
}
@@ -819,7 +825,59 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
.orderByDesc(ContractManage::getCreateTime));
}
/**
* 配载提交与重新派单共用:子运单状态逐张按该票自己项目的实时过程配置推导后落库,再清除接单事实。
* <p>
* 推导按「接单事实已清、尚未接单」的口径取值,故清接单事实放在状态写入之后,两者口径一致。
*/
private void writeWaybillsForDriver(LoadingManage loadingManage, List<Long> oldWaybillIds) {
syncAssociatedWaybills(loadingManage,
deriveWaybillBusinessStatuses(waybillIds(loadingManage.getWaybillIdsJson())), oldWaybillIds);
// 接单事实不清,被拒的票对新司机不可见——司机端一切入口都排除已拒绝的运单
clearAssociatedWaybillAcceptRecords(loadingManage);
}
/**
* 子运单业务状态逐张按各自项目的实时过程配置推导:需要接单 → 待执行,不需要 → 进行中。
* <p>
* 配置取法与运单读路径同一处:项目生效配置优先,取不到时才回退该运单的过程配置快照;
* 快照只作兜底、永不优先,否则项目改配置后写入值与读时的归一化结果会分叉。
* 项目为空且快照也为空 → 视为不需要接单 → 进行中。
*/
private Map<Long, String> deriveWaybillBusinessStatuses(List<Long> waybillIds) {
Map<Long, String> businessStatusByWaybillId = new LinkedHashMap<>();
if (Func.isEmpty(waybillIds)) {
return businessStatusByWaybillId;
}
List<Waybill> waybills = waybillMapper.selectList(Wrappers.<Waybill>lambdaQuery()
.select(Waybill::getId, Waybill::getProjectId, Waybill::getProcessJson)
.eq(Waybill::getIsDeleted, 0)
.in(Waybill::getId, waybillIds));
// 一次查全所有项目的生效配置,不逐张回查
Map<Long, String> liveConfigByProject = LiveProcessConfigSupport.byProjects(processConfigService,
waybills.stream().map(Waybill::getProjectId).toList());
for (Waybill waybill : waybills) {
String processJson = LiveProcessConfigSupport.resolve(
liveConfigByProject.get(waybill.getProjectId()), waybill.getProcessJson());
// 以「待执行 + 待接单」为输入走读路径同一个归一化:需要接单留下待执行,不需要接单提升为进行中
businessStatusByWaybillId.put(waybill.getId(), WaybillProcessSupport.normalizeBusinessStatus(
STATUS_PENDING, processJson, WaybillProcessSupport.ACCEPT_PENDING));
}
return businessStatusByWaybillId;
}
private void syncAssociatedWaybills(LoadingManage loadingManage, String status, List<Long> oldWaybillIds) {
Map<Long, String> statusByWaybillId = new LinkedHashMap<>();
for (Long waybillId : waybillIds(loadingManage.getWaybillIdsJson())) {
statusByWaybillId.put(waybillId, status);
}
syncAssociatedWaybills(loadingManage, statusByWaybillId, oldWaybillIds);
}
/**
* 逐张写状态:同状态的票合成一条批量更新,配载字段对每张票都写全。
*/
private void syncAssociatedWaybills(LoadingManage loadingManage, Map<Long, String> statusByWaybillId, List<Long> oldWaybillIds) {
List<Long> newWaybillIds = waybillIds(loadingManage.getWaybillIdsJson());
if (Func.isNotEmpty(oldWaybillIds)) {
List<Long> removedIds = new ArrayList<>(oldWaybillIds);
@@ -829,8 +887,18 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
if (Func.isEmpty(newWaybillIds)) {
return;
}
Map<String, List<Long>> waybillIdsByStatus = new LinkedHashMap<>();
for (Long waybillId : newWaybillIds) {
waybillIdsByStatus
.computeIfAbsent(statusByWaybillId.getOrDefault(waybillId, STATUS_PENDING), key -> new ArrayList<>())
.add(waybillId);
}
waybillIdsByStatus.forEach((status, ids) -> updateAssociatedWaybillFields(loadingManage, ids, status));
}
private void updateAssociatedWaybillFields(LoadingManage loadingManage, List<Long> waybillIds, String status) {
waybillMapper.update(null, Wrappers.<Waybill>lambdaUpdate()
.in(Waybill::getId, newWaybillIds)
.in(Waybill::getId, waybillIds)
.set(Waybill::getLoadingNo, loadingManage.getLoadingNo())
.set(Waybill::getBusinessStatus, status)
.set(Waybill::getCarrierType, loadingManage.getCarrierType())
@@ -0,0 +1,466 @@
package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.AfterEach;
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.mockito.ArgumentMatchers;
import org.mockito.MockedStatic;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.transport.mapper.WaybillMapper;
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.service.IContractManageService;
import org.springblade.transport.service.IProcessConfigService;
import org.springblade.transport.service.IReceivablePayableDetailService;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 配载提交 / 重新派单写入子运单状态:状态逐张按各票自己项目的实时过程配置推导,
* 并把这批票的接单事实清成「待接单」,使新司机能看到它们。
* <p>
* 推导必须与运单读路径的状态归一化同口径(同一份实时配置、同一个归一化函数),
* 否则写完再读会漂移。
*/
class LoadingManageChildStatusWriteTest {
/** 当前登录组织,与夹具运单的 deptId 一致 */
private static final String DEPT_ID = "1123598813738675201";
private static final Long DEPT_ID_LONG = 1123598813738675201L;
private static final String LOADING_NO = "PZ-20261018-001";
private static final Long LOADING_ID = 1800000000000000001L;
private static final String DRIVER_PHONE = "13800138000";
private static final Long NEED_ACCEPT_WAYBILL_ID = 3101L;
private static final Long NO_ACCEPT_WAYBILL_ID = 3102L;
private static final Long NEED_ACCEPT_PROJECT_ID = 2101L;
private static final Long NO_ACCEPT_PROJECT_ID = 2102L;
/** 接单节点「是否确认=是」→ 需要司机接单确认 */
private static final String NEED_ACCEPT_JSON =
"[{\"key\":\"accept\",\"name\":\"接单\",\"enabled\":true,\"confirmMode\":\"yes\"}]";
/** 接单节点「无需确认接单」→ 不需要接单 */
private static final String NO_ACCEPT_JSON =
"[{\"key\":\"accept\",\"name\":\"接单\",\"enabled\":true,\"confirmMode\":\"no_confirm_accept\"}]";
/** 配载写入给子运单落的全部配载字段,逐条都不能因为状态要分批写而漏掉 */
private static final List<String> SYNC_COLUMNS = List.of(
"loading_no",
"business_status",
"carrier_type",
"carrier_contract_id",
"carrier_name",
"driver_name",
"driver_phone",
"vehicle_no",
"trailer_vehicle_no",
"escort_name",
"escort_phone",
"mileage",
"estimated_start_time",
"estimated_end_time",
"task_remark",
"task_info_json",
"current_process_node");
private static final List<String> ACCEPT_CLEAR_COLUMNS = List.of(
"driver_accept_status",
"driver_accept_time",
"driver_accept_driver_id",
"driver_reject_time",
"driver_reject_reason");
private WaybillMapper waybillMapper;
private IProcessConfigService processConfigService;
private IContractManageService contractManageService;
private IReceivablePayableDetailService receivablePayableDetailService;
private LoadingManageServiceImpl service;
private MockedStatic<AuthUtil> authUtil;
@BeforeAll
static void initTableInfo() {
// 构造 LambdaQueryWrapper / LambdaUpdateWrapper 需要 MyBatis-Plus 的实体元数据缓存
MybatisConfiguration configuration = new MybatisConfiguration();
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
assistant.setCurrentNamespace("test");
TableInfoHelper.initTableInfo(assistant, Waybill.class);
TableInfoHelper.initTableInfo(assistant, ProcessConfig.class);
}
@BeforeEach
void setUp() {
waybillMapper = mock(WaybillMapper.class);
processConfigService = mock(IProcessConfigService.class);
contractManageService = mock(IContractManageService.class);
receivablePayableDetailService = mock(IReceivablePayableDetailService.class);
service = spy(new LoadingManageServiceImpl());
inject(service, "waybillMapper", waybillMapper);
inject(service, "processConfigService", processConfigService);
inject(service, "contractManageService", contractManageService);
inject(service, "receivablePayableDetailService", receivablePayableDetailService);
doReturnTrue(service);
authUtil = mockStatic(AuthUtil.class);
authUtil.when(AuthUtil::getDeptId).thenReturn(DEPT_ID);
}
@AfterEach
void tearDown() {
authUtil.close();
}
@Test
@DisplayName("不需接单的项目:提交时子单直接落进行中,不留待执行等读路径去提升")
void submitWritesRunningForProjectWithoutAcceptNode() {
stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID));
stubConfigs(config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON));
assertTrue(service.submit(loadingForm(NO_ACCEPT_WAYBILL_ID)));
CapturedUpdate statusUpdate = statusSyncUpdates().get(0);
assertEquals("running", statusUpdate.setValue("business_status"));
assertEquals(List.of(NO_ACCEPT_WAYBILL_ID), statusUpdate.whereIds());
assertEquals(SYNC_COLUMNS, statusUpdate.columns(), "配载字段一张不少地随状态一起写下去");
}
@Test
@DisplayName("需接单的项目:提交时子单落待执行,等新司机接单")
void submitWritesPendingForProjectWithAcceptNode() {
stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
stubConfigs(config(NEED_ACCEPT_PROJECT_ID, NEED_ACCEPT_JSON));
assertTrue(service.submit(loadingForm(NEED_ACCEPT_WAYBILL_ID)));
assertEquals("pending", statusSyncUpdates().get(0).setValue("business_status"));
}
@Test
@DisplayName("跨项目运次:两张子单各自按自己项目推导,互不牵连,配置只查一次")
void submitDerivesEachWaybillFromItsOwnProject() {
stubWaybills(
waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID),
waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID));
stubConfigs(
config(NEED_ACCEPT_PROJECT_ID, NEED_ACCEPT_JSON),
config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON));
assertTrue(service.submit(loadingForm(NEED_ACCEPT_WAYBILL_ID, NO_ACCEPT_WAYBILL_ID)));
assertEquals(Map.of(
NEED_ACCEPT_WAYBILL_ID, "pending",
NO_ACCEPT_WAYBILL_ID, "running"), statusByWaybillId());
verify(processConfigService, times(1)).list(ArgumentMatchers.<Wrapper<ProcessConfig>>any());
}
@Test
@DisplayName("项目生效配置优先:项目不需要接单时,运单自己的历史快照说需要接单也不采信")
void liveConfigWinsOverWaybillSnapshot() {
stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID, NEED_ACCEPT_JSON));
stubConfigs(config(NEED_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON));
assertTrue(service.submit(loadingForm(NEED_ACCEPT_WAYBILL_ID)));
assertEquals("running", statusSyncUpdates().get(0).setValue("business_status"));
}
@Test
@DisplayName("没有项目也没有快照:视为不需要接单,写进行中")
void waybillWithoutProjectAndSnapshotWritesRunning() {
stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, null));
stubConfigs();
assertTrue(service.submit(loadingForm(NO_ACCEPT_WAYBILL_ID)));
assertEquals("running", statusSyncUpdates().get(0).setValue("business_status"));
}
@Test
@DisplayName("项目为空的运单:配置取不到时按运单快照兜底,与读路径的归一化保持一致")
void waybillWithoutProjectFallsBackToSnapshot() {
stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, null, NEED_ACCEPT_JSON));
stubConfigs();
assertTrue(service.submit(loadingForm(NEED_ACCEPT_WAYBILL_ID)));
assertEquals("pending", statusSyncUpdates().get(0).setValue("business_status"));
}
@Test
@DisplayName("提交清除本次合进来子单的接单事实:置为待接单,接单/拒单痕迹一并清空")
void submitClearsAcceptRecordOfWaybillsTakenIn() {
stubWaybills(
waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID),
waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID));
stubConfigs(
config(NEED_ACCEPT_PROJECT_ID, NEED_ACCEPT_JSON),
config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON));
assertTrue(service.submit(loadingForm(NEED_ACCEPT_WAYBILL_ID, NO_ACCEPT_WAYBILL_ID)));
CapturedUpdate clearUpdate = acceptClearUpdates().get(0);
assertEquals(ACCEPT_CLEAR_COLUMNS, clearUpdate.columns());
assertEquals("pending", clearUpdate.setValue("driver_accept_status"));
assertNull(clearUpdate.setValue("driver_accept_time"));
assertNull(clearUpdate.setValue("driver_accept_driver_id"));
assertNull(clearUpdate.setValue("driver_reject_time"));
assertNull(clearUpdate.setValue("driver_reject_reason"));
assertEquals(
Set.of(NEED_ACCEPT_WAYBILL_ID, NO_ACCEPT_WAYBILL_ID),
Set.copyOf(clearUpdate.whereIds()));
}
@Test
@DisplayName("重新派单:与提交同一套逐张推导,并同样清除接单事实;先写状态后清事实")
void reassignDerivesStatusAndClearsAcceptRecord() {
stubLoadingRecord(loadingRecord("pending", NEED_ACCEPT_WAYBILL_ID));
stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
stubConfigs(config(NEED_ACCEPT_PROJECT_ID, NEED_ACCEPT_JSON));
stubRejectedWaybillExists();
LoadingManage form = editForm(LOADING_ID, NEED_ACCEPT_WAYBILL_ID);
assertTrue(service.reassign(form));
List<CapturedUpdate> updates = waybillUpdates();
assertEquals("pending", updates.get(0).setValue("business_status"));
assertEquals(List.of(NEED_ACCEPT_WAYBILL_ID), updates.get(1).whereIds());
assertEquals(ACCEPT_CLEAR_COLUMNS, updates.get(1).columns(), "清接单事实紧跟状态写入之后");
}
@Test
@DisplayName("编辑配载单移除运单:被移除的票照旧退回待执行,不参与本次的状态推导与清接单事实")
void removedWaybillIsReleasedWithoutDerivation() {
stubLoadingRecord(loadingRecord("pending", NEED_ACCEPT_WAYBILL_ID, NO_ACCEPT_WAYBILL_ID));
stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID));
stubConfigs(config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON));
assertTrue(service.submit(editForm(LOADING_ID, NO_ACCEPT_WAYBILL_ID)));
assertEquals(
Map.of(NO_ACCEPT_WAYBILL_ID, "running"),
statusByWaybillId(),
"留下的票按自己项目推导,被移除的票不出现在状态写入里");
CapturedUpdate releaseUpdate = waybillUpdates().stream()
.filter(update -> !update.columns().contains("driver_name"))
.findFirst()
.orElseThrow();
assertEquals(List.of(NEED_ACCEPT_WAYBILL_ID), releaseUpdate.whereIds(), "被移除的票退回未配载");
assertEquals("pending", releaseUpdate.setValue("business_status"));
assertEquals(List.of(NO_ACCEPT_WAYBILL_ID), acceptClearUpdates().get(0).whereIds());
}
@Test
@DisplayName("回归:开始配载仍整批置进行中,不按项目逐张推导")
void startStillWritesRunningForWholeBatch() {
stubLoadingRecord(loadingRecord("pending", NEED_ACCEPT_WAYBILL_ID, NO_ACCEPT_WAYBILL_ID));
assertTrue(service.start(LOADING_ID));
CapturedUpdate statusUpdate = statusSyncUpdates().get(0);
assertEquals("running", statusUpdate.setValue("business_status"));
assertEquals(
Set.of(NEED_ACCEPT_WAYBILL_ID, NO_ACCEPT_WAYBILL_ID),
Set.copyOf(statusUpdate.whereIds()));
verify(processConfigService, never()).list(ArgumentMatchers.<Wrapper<ProcessConfig>>any());
}
@Test
@DisplayName("回归:完成配载仍整批置已完成,结算触发照旧")
void completeStillWritesCompletedForWholeBatch() {
stubLoadingRecord(loadingRecord("running", NEED_ACCEPT_WAYBILL_ID, NO_ACCEPT_WAYBILL_ID));
assertTrue(service.complete(LOADING_ID));
CapturedUpdate statusUpdate = statusSyncUpdates().get(0);
assertEquals("completed", statusUpdate.setValue("business_status"));
assertEquals(
Set.of(NEED_ACCEPT_WAYBILL_ID, NO_ACCEPT_WAYBILL_ID),
Set.copyOf(statusUpdate.whereIds()));
verify(receivablePayableDetailService).generateForCompletedLoading(
List.of(NEED_ACCEPT_WAYBILL_ID, NO_ACCEPT_WAYBILL_ID), null, LOADING_NO);
verify(processConfigService, never()).list(ArgumentMatchers.<Wrapper<ProcessConfig>>any());
}
// ------------------------------------------------------------ 夹具
private LoadingManage loadingForm(Long... waybillIds) {
return fillForm(new LoadingManage(), List.of(waybillIds));
}
private LoadingManage editForm(Long loadingId, Long... waybillIds) {
LoadingManage form = new LoadingManage();
form.setId(loadingId);
return fillForm(form, List.of(waybillIds));
}
private LoadingManage fillForm(LoadingManage form, List<Long> ids) {
form.setLoadingNo(LOADING_NO);
form.setDeptId(DEPT_ID_LONG);
form.setDeptName("测试组织");
form.setWaybillIdsJson(JsonUtil.toJson(ids));
form.setLoadingSubNos(ids.stream().map(id -> "YD-" + id).reduce((left, right) -> left + "," + right).orElse(""));
form.setTransportType("公路运输");
form.setCarrierType("自运");
form.setVehicleNo("桂A12345");
form.setDriverName("张三");
form.setDriverPhone(DRIVER_PHONE);
form.setDepartureAddress("南宁");
form.setArrivalAddress("广州");
return form;
}
private static LoadingManage loadingRecord(String businessStatus, Long... waybillIds) {
LoadingManage record = new LoadingManage();
record.setId(LOADING_ID);
record.setLoadingNo(LOADING_NO);
record.setDeptId(DEPT_ID_LONG);
record.setDeptName("测试组织");
record.setCarrierType("自运");
record.setBusinessStatus(businessStatus);
record.setWaybillIdsJson(JsonUtil.toJson(List.of(waybillIds)));
return record;
}
private void stubLoadingRecord(LoadingManage record) {
doReturn(record).when(service).getById(LOADING_ID);
}
/** 重新派单只对「有被拒子单」的配载单开放 */
private void stubRejectedWaybillExists() {
when(waybillMapper.selectCount(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(1L);
}
private static Waybill waybill(Long id, Long projectId) {
return waybill(id, projectId, null);
}
private static Waybill waybill(Long id, Long projectId, String processJson) {
Waybill waybill = new Waybill();
waybill.setId(id);
waybill.setWaybillNo("YD-" + id);
waybill.setProjectId(projectId);
waybill.setProcessJson(processJson);
waybill.setDeptId(DEPT_ID_LONG);
waybill.setBusinessStatus("pending");
return waybill;
}
private void stubWaybills(Waybill... waybills) {
when(waybillMapper.selectList(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(List.of(waybills));
}
private void stubConfigs(ProcessConfig... configs) {
when(processConfigService.list(ArgumentMatchers.<Wrapper<ProcessConfig>>any())).thenReturn(List.of(configs));
}
private static ProcessConfig config(Long projectId, String nodeConfigJson) {
ProcessConfig config = new ProcessConfig();
config.setProjectIds(String.valueOf(projectId));
config.setNodeConfigJson(nodeConfigJson);
return config;
}
/** 提交路径上 mock 掉框架自带的落库与回查,只留业务写入可断言 */
private static void doReturnTrue(LoadingManageServiceImpl target) {
doReturn(true).when(target).saveOrUpdate(any(LoadingManage.class));
doReturn(true).when(target).updateById(any(LoadingManage.class));
}
/** 配载字段 + 状态那条批量更新;退回未配载的那条只写 loading_no/合同/状态,靠 driver_name 区分 */
private List<CapturedUpdate> statusSyncUpdates() {
return waybillUpdates().stream().filter(update -> update.columns().contains("driver_name")).toList();
}
/** 状态写入的 where 只有运单 id,故一条更新对应一张票 */
private Map<Long, String> statusByWaybillId() {
Map<Long, String> statusByWaybillId = new LinkedHashMap<>();
statusSyncUpdates().forEach(update -> statusByWaybillId.put(
update.whereIds().get(0), String.valueOf(update.setValue("business_status"))));
return statusByWaybillId;
}
private List<CapturedUpdate> acceptClearUpdates() {
return waybillUpdates().stream()
.filter(update -> update.columns().contains("driver_accept_status"))
.toList();
}
private List<CapturedUpdate> waybillUpdates() {
ArgumentCaptor<Wrapper<Waybill>> captor = ArgumentCaptor.forClass(Wrapper.class);
verify(waybillMapper, atLeastOnce()).update(ArgumentMatchers.<Waybill>isNull(), captor.capture());
return captor.getAllValues().stream().map(CapturedUpdate::of).toList();
}
private static void inject(LoadingManageServiceImpl target, String fieldName, Object value) {
try {
Field field = LoadingManageServiceImpl.class.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
} catch (ReflectiveOperationException exception) {
throw new IllegalStateException("注入测试替身失败:" + fieldName, exception);
}
}
/** 一条被捕获的批量更新:SET 的列 → 值、WHERE 的运单 id */
private record CapturedUpdate(String sqlSet, String whereSql, Map<String, Object> params, List<String> columns) {
@SuppressWarnings("unchecked")
static CapturedUpdate of(Wrapper<Waybill> wrapper) {
LambdaUpdateWrapper<Waybill> update = (LambdaUpdateWrapper<Waybill>) wrapper;
String sqlSet = update.getSqlSet();
// SET 与 WHERE 的参数都是渲染时才进参数表,取值前先把两段都渲染一遍
String whereSql = update.getTargetSql();
List<String> columns = Arrays.stream(sqlSet.split(","))
.map(part -> part.substring(0, part.indexOf('=')).trim())
.toList();
return new CapturedUpdate(sqlSet, whereSql, update.getParamNameValuePairs(), columns);
}
Object setValue(String column) {
for (String part : sqlSet.split(",")) {
int equalsIndex = part.indexOf('=');
if (!column.equals(part.substring(0, equalsIndex).trim())) {
continue;
}
String reference = part.substring(equalsIndex + 1).trim();
if (!reference.startsWith("#{")) {
return reference;
}
String key = reference.substring(reference.lastIndexOf('.') + 1, reference.length() - 1);
return params.get(key);
}
return null;
}
List<Long> whereIds() {
return params.values().stream().filter(Long.class::isInstance).map(Long.class::cast).toList();
}
}
}