🔀 合并查询分档、数字态、名称表与状态下发

This commit is contained in:
2026-10-22 14:21:55 +08:00
14 changed files with 580 additions and 13 deletions
@@ -65,6 +65,9 @@ public class AdminWaybillCardVO implements Serializable {
@Schema(description = "状态:0待接单/1运输中/2已完成/3已取消")
private Integer status;
@Schema(description = "原始业务状态:pending / running / pending_return_confirm / completed / cancelled")
private String businessStatus;
@Schema(description = "承运方")
private String carrierName;
@@ -48,6 +48,9 @@ public class AdminWaybillDetailVO implements Serializable {
@Schema(description = "状态:0待接单/1运输中/2已完成/3已取消")
private Integer status;
@Schema(description = "原始业务状态:pending / running / pending_return_confirm / completed / cancelled")
private String businessStatus;
@Schema(description = "起点名称")
private String fromName;
@@ -76,6 +76,9 @@ public class DriverWaybillCardVO implements Serializable {
@Schema(description = "状态:0待接单/1运输中/2已完成/3已取消")
private Integer status;
@Schema(description = "原始业务状态:pending / running / pending_return_confirm / completed / cancelled")
private String businessStatus;
@Schema(description = "创建时间")
private String createTime;
@@ -118,13 +118,18 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
private static final List<String> PENDING_STATUSES = Arrays.asList(
STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING
);
/** Tab「全部」统计口径:待接 + 运输中 + 已完成(不含已取消) */
/** Tab「全部」统计口径:待接 + 运输中 + 待回单确认 + 已完成(不含已取消) */
private static final List<String> TAB_ALL_STATUSES = Arrays.asList(
STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING, STATUS_RUNNING, STATUS_COMPLETED
STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING, STATUS_RUNNING,
WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM, STATUS_COMPLETED
);
/** 允许完成运单的业务状态 */
private static final List<String> COMPLETABLE_STATUSES = Arrays.asList(
STATUS_RUNNING, STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING
/**
* 进行中口径:司机手上还没走完的单。
* 待回单确认也算进行中——司机侧已打完末位卡,等业务员确认回单才变已完成。
*/
private static final List<String> ACTIVE_STATUSES = Arrays.asList(
STATUS_RUNNING, STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING,
WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM
);
private static final int DEFAULT_PREVIEW_SIZE = 2;
@@ -838,7 +843,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
}
switch (status) {
case 0 -> wrapper.in(Waybill::getBusinessStatus, PENDING_STATUSES);
case 1 -> wrapper.in(Waybill::getBusinessStatus, COMPLETABLE_STATUSES);
case 1 -> wrapper.in(Waybill::getBusinessStatus, ACTIVE_STATUSES);
case 2 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_COMPLETED);
case 3 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_CANCELLED);
default -> wrapper.in(Waybill::getBusinessStatus, TAB_ALL_STATUSES);
@@ -1006,6 +1011,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
WaybillCargoSupport.Quantity weighing = WaybillCargoSupport.weighing(cargos);
card.setWeight(weighing == null ? "" : formatWeight(weighing.value(), weighing.unit()));
card.setStatus(toAppStatus(waybill.getBusinessStatus()));
card.setBusinessStatus(waybill.getBusinessStatus());
String createTime = formatDateTime(waybill.getCreateTime());
card.setCreateTime(createTime);
card.setPublishTime(createTime);
@@ -1514,12 +1520,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
}
/**
* 自动完成的前置:业务状态允许完成,且没有未了结异常。
* 自动完成的前置:业务状态在进行中口径内,且没有未了结异常。
* <p>
* 不满足返回 false,由调用方静默跳过,打卡不受影响。
*/
private boolean canAutoCompleteWaybill(Waybill waybill) {
return COMPLETABLE_STATUSES.contains(waybill.getBusinessStatus())
return ACTIVE_STATUSES.contains(waybill.getBusinessStatus())
&& unresolvedExceptionNodeCodeList(waybill.getId()).isEmpty();
}
@@ -1986,7 +1992,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
}
return switch (businessStatus) {
case STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING -> 0;
case STATUS_RUNNING -> 1;
case STATUS_RUNNING, WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM -> 1;
case STATUS_COMPLETED -> 2;
case STATUS_CANCELLED -> 3;
default -> null;
@@ -744,6 +744,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
detail.setId(waybill.getId());
detail.setWaybillNo(waybill.getWaybillNo());
detail.setStatus(toAppStatus(waybill.getBusinessStatus()));
detail.setBusinessStatus(waybill.getBusinessStatus());
String mode = Func.toStr(waybill.getTransportType(), "");
detail.setTransportMode(mode);
@@ -937,6 +938,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
card.setPlanTime(formatLocalDate(waybill.getEstimatedStartTime()));
card.setPlanTimeEnd(formatLocalDate(waybill.getEstimatedEndTime()));
card.setStatus(toAppStatus(waybill.getBusinessStatus()));
card.setBusinessStatus(waybill.getBusinessStatus());
card.setCarrierName(Func.toStr(waybill.getCarrierName(), ""));
card.setDriverName(Func.toStr(waybill.getDriverName(), ""));
card.setVehicleNo(Func.toStr(waybill.getVehicleNo(), ""));
@@ -994,7 +996,9 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
}
return switch (businessStatus) {
case STATUS_PENDING, "waiting_dispatch", "dispatching" -> 0;
case STATUS_RUNNING -> 1;
// 与司机端 toAppStatus 同一份数字枚举(0 待接单 / 1 运输中 / 2 已完成 / 3 已取消),待回单确认属进行中:
// 两处对同一状态必须给同一个数字,给 null 前端只能落到「未知」兜底
case STATUS_RUNNING, WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM -> 1;
case STATUS_COMPLETED -> 2;
case STATUS_CANCELLED -> 3;
default -> null;
@@ -362,7 +362,7 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
Map<String, BigDecimal> dispatchedGoods = dispatchedGoods(routeWaybills, routePlans);
route.put("dispatchedQuantity", dispatchedGoods.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add));
route.put("dispatchedGoods", dispatchedGoods);
route.put("arrivedQuantity", routeWaybills.stream().filter(item -> "completed".equals(item.getBusinessStatus())).flatMap(item -> waybillGoods(item).stream()).map(item -> decimal(item, "quantity")).reduce(BigDecimal.ZERO, BigDecimal::add));
route.put("arrivedQuantity", routeWaybills.stream().filter(item -> arrived(item.getBusinessStatus())).flatMap(item -> waybillGoods(item).stream()).map(item -> decimal(item, "quantity")).reduce(BigDecimal.ZERO, BigDecimal::add));
route.put("waybills", routeWaybills);
route.put("transportPlans", routePlans);
}
@@ -541,12 +541,22 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
}
private BigDecimal completedQuantity(String masterNo, Map<String, Object> route) {
return waybillsByMasterNo(masterNo).stream()
.filter(item -> "completed".equals(item.getBusinessStatus()))
.filter(item -> arrived(item.getBusinessStatus()))
.filter(item -> belongsToRoute(item, route))
.flatMap(item -> waybillGoods(item).stream())
.map(item -> decimal(item, "quantity"))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
/**
* 已到达口径:待回单确认也算到达——货物确实到了,回单只是单据。
* 不计入会按「调度总量不能大于上一段已到达数量」把多联总单的下一段挡在门外。
*/
static boolean arrived(String businessStatus) {
return "completed".equals(businessStatus)
|| WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM.equals(businessStatus);
}
private Map<String, BigDecimal> availableGoods(MasterOrder masterOrder) {
Map<String, BigDecimal> available = new LinkedHashMap<>();
for (Map<String, Object> goods : parseArray(masterOrder.getGoodsJson())) available.merge(goodsKey(goods), decimal(goods, "quantity"), BigDecimal::add);
@@ -61,6 +61,7 @@ import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.WaybillProcessSupport;
import org.springblade.transport.wrapper.ReceivablePayableDetailWrapper;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -3295,6 +3296,7 @@ public class ReceivablePayableDetailServiceImpl
case "completed" -> "已完成";
case "processing", "running", "in_progress", "inProgress" -> "进行中";
case "pending", "created" -> "待执行";
case WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM -> "待回单确认";
case "cancelled", "canceled" -> "已取消";
default -> status;
};
@@ -45,6 +45,7 @@ final class TripAggregation {
static final String RUNNING = "running";
static final String COMPLETED = "completed";
static final String CANCELLED = "cancelled";
static final String PENDING_RETURN_CONFIRM = WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM;
/** 待接单档:与运单侧 Tab「待接单」取同一组状态 */
static final List<String> PENDING_STATUSES = List.of(PENDING, WAITING_DISPATCH, DISPATCHING);
@@ -53,9 +54,10 @@ final class TripAggregation {
/**
* 进运次的子单状态:草稿尚未派单、不成运次;已取消要进,
* 否则「子单全已取消」会被算成运输中而不是已取消。
* 待回单确认要进:子单漏在运次外,剩下那几张会被判成整趟车已走完。
*/
static final List<String> CHILD_STATUSES = List.of(
PENDING, WAITING_DISPATCH, DISPATCHING, RUNNING, COMPLETED, CANCELLED
PENDING, WAITING_DISPATCH, DISPATCHING, RUNNING, PENDING_RETURN_CONFIRM, COMPLETED, CANCELLED
);
/** 待重新派单的口径:子单被拒且这趟车还没结束 */
@@ -122,6 +122,7 @@ public class WaybillWrapper extends BaseEntityWrapper<Waybill, WaybillVO> {
case "waiting_dispatch" -> "待调度";
case "dispatching" -> "调度中";
case "running", "processing", "in_progress", "inProgress" -> "进行中";
case WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM -> "待回单确认";
case "completed" -> "已完成";
case "cancelled" -> "已取消";
case "closed" -> "已关闭";
@@ -0,0 +1,183 @@
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.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
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.MockedStatic;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.transport.mapper.ExceptionDisposalMapper;
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
import org.springblade.transport.mapper.WaybillNodePunchMapper;
import org.springblade.transport.pojo.entity.Driver;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.entity.WaybillNodePunch;
import org.springblade.transport.pojo.vo.DriverWaybillCardVO;
import org.springblade.transport.pojo.vo.DriverWaybillTabCountsVO;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.IProcessConfigService;
import org.springblade.transport.service.ITransportVehicleService;
import org.springblade.transport.service.ITripQueryService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.CertificationGate;
import org.springblade.transport.support.WaybillProcessSupport;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Supplier;
import static org.junit.jupiter.api.Assertions.assertEquals;
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.mockStatic;
import static org.mockito.Mockito.when;
/**
* 司机端四个 Tab 的状态分档。
* <p>
* 待回单确认是「等业务员动手」的进行中:数字态沿用 1(进行中),查询口径也必须认它。
* 漏掉的话司机打完末位卡,这张单从他的列表里彻底消失——不是展示问题,是单子不见了。
*/
class DriverWaybillTabStatusTest {
private static final Long WAYBILL_ID = 5901L;
private static final Long USER_ID = 7001L;
private static final String PLATE = "桂AAW3256";
private static final String PHONE = "13800138002";
private IWaybillService waybillService;
private IDriverService driverService;
private WaybillNodePunchMapper nodePunchMapper;
private ExceptionDisposalMapper exceptionDisposalMapper;
private final List<Wrapper<Waybill>> queried = new ArrayList<>();
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, Waybill.class);
TableInfoHelper.initTableInfo(assistant, Driver.class);
TableInfoHelper.initTableInfo(assistant, WaybillNodePunch.class);
}
@BeforeEach
void setUp() {
waybillService = mock(IWaybillService.class);
driverService = mock(IDriverService.class);
nodePunchMapper = mock(WaybillNodePunchMapper.class);
exceptionDisposalMapper = mock(ExceptionDisposalMapper.class);
service = new DriverWaybillServiceImpl(
waybillService,
driverService,
mock(IMpUserClient.class),
mock(IProcessConfigService.class),
mock(ITransportVehicleService.class),
mock(CertificationGate.class),
mock(WaybillEnroutePunchMapper.class),
nodePunchMapper,
exceptionDisposalMapper,
mock(ITripQueryService.class));
when(driverService.getOne(any())).thenReturn(driver());
when(waybillService.syncDriverAcceptState(any())).thenAnswer(invocation -> invocation.getArgument(0));
when(waybillService.list(anyWrapper())).thenAnswer(invocation -> {
queried.add(invocation.getArgument(0));
return List.of(pendingReturnConfirmWaybill());
});
}
@Test
@DisplayName("待回单确认落在「进行中」档:列表查得到,数字态是 1")
void pendingReturnConfirmIsInProgressTier() {
IPage<DriverWaybillCardVO> page = runAsDriver(() -> service.page(1, 10, 1, null));
assertEquals(1, page.getRecords().size(), "进行中档必须包含待回单确认,否则司机看不到这张单");
assertEquals(Integer.valueOf(1), page.getRecords().get(0).getStatus(), "数字态沿用 1(进行中),不新增档位");
assertEquals(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM,
page.getRecords().get(0).getBusinessStatus(),
"原始业务状态随卡片下发:数字态恒为 1,前端靠它区分运输中与待回单确认");
}
@Test
@DisplayName("「进行中」查询口径在库层就带上待回单确认")
void inProgressQueryCarriesPendingReturnConfirm() {
runAsDriver(() -> service.page(1, 10, 1, null));
assertTrue(queriedStatuses().contains(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM));
}
@Test
@DisplayName("「全部」tab 不漏掉待回单确认的运单")
void allTabKeepsPendingReturnConfirm() {
IPage<DriverWaybillCardVO> page = runAsDriver(() -> service.page(1, 10, null, null));
assertEquals(1, page.getRecords().size(), "打完末位卡的单不能从全部 tab 消失");
assertEquals(Integer.valueOf(1), page.getRecords().get(0).getStatus());
assertEquals(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM,
page.getRecords().get(0).getBusinessStatus());
assertTrue(queriedStatuses().contains(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM));
}
@Test
@DisplayName("Tab 计数把它算进进行中,不算已完成")
void tabCountsCountsItAsDoing() {
DriverWaybillTabCountsVO counts = runAsDriver(service::tabCounts);
assertEquals(1, counts.getDoing(), "待回单确认仍是进行中,不是已完成");
assertEquals(0, counts.getDone());
assertEquals(1, counts.getAll());
}
// ------------------------------------------------------------ 夹具
private <T> T runAsDriver(Supplier<T> action) {
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
return action.get();
}
}
@SuppressWarnings("unchecked")
private Collection<Object> queriedStatuses() {
assertEquals(1, queried.size(), "一次列表查询");
return ((LambdaQueryWrapper<Waybill>) queried.get(0)).getParamNameValuePairs().values();
}
@SuppressWarnings("unchecked")
private static Wrapper<Waybill> anyWrapper() {
return org.mockito.ArgumentMatchers.<Wrapper<Waybill>>any();
}
private Waybill pendingReturnConfirmWaybill() {
Waybill waybill = new Waybill();
waybill.setId(WAYBILL_ID);
waybill.setWaybillNo("YD202610080002");
waybill.setVehicleNo(PLATE);
waybill.setDriverPhone(PHONE);
waybill.setBusinessStatus(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM);
waybill.setIsDeleted(0);
return waybill;
}
private Driver driver() {
Driver driver = new Driver();
driver.setId(9001L);
driver.setUserId(USER_ID);
driver.setDrivingVehicle(PLATE);
driver.setMobile(PHONE);
return driver;
}
}
@@ -0,0 +1,118 @@
package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
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.springblade.transport.pojo.entity.ExceptionDisposal;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.AdminWaybillCardVO;
import org.springblade.transport.pojo.vo.AdminWaybillDetailVO;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.IDriverWaybillService;
import org.springblade.transport.service.IExceptionDisposalService;
import org.springblade.transport.service.ILoadingManageService;
import org.springblade.transport.service.IRiskDisposalService;
import org.springblade.transport.service.ITripQueryService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.WaybillProcessSupport;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* 调度端运单详情的原始业务状态。
* <p>
* 数字态只有 0/1/2/3:待回单确认落进行中(1),与运输中同档。
* 调度端要区分这两者只能看原始状态码,所以详情与卡片都得带上它。
*/
class ManageWaybillBusinessStatusTest {
private static final Long WAYBILL_ID = 6201L;
private static final String PENDING_RETURN_CONFIRM = WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM;
private ManageWaybillServiceImpl service;
private IWaybillService waybillService;
@BeforeAll
static void initTableInfo() {
MybatisConfiguration configuration = new MybatisConfiguration();
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
assistant.setCurrentNamespace("test");
TableInfoHelper.initTableInfo(assistant, Waybill.class);
TableInfoHelper.initTableInfo(assistant, ExceptionDisposal.class);
}
@BeforeEach
void setUp() {
waybillService = mock(IWaybillService.class);
IExceptionDisposalService exceptionDisposalService = mock(IExceptionDisposalService.class);
service = new ManageWaybillServiceImpl(
waybillService,
mock(IDriverService.class),
exceptionDisposalService,
mock(IRiskDisposalService.class),
mock(IDriverWaybillService.class),
mock(ITripQueryService.class),
mock(ILoadingManageService.class));
when(exceptionDisposalService.list(anyWrapper())).thenReturn(new ArrayList<>());
}
@Test
@DisplayName("运单详情:数字态 1,原始业务状态是待回单确认")
void detailCarriesRawBusinessStatus() {
when(waybillService.getById(WAYBILL_ID)).thenReturn(waybill());
AdminWaybillDetailVO detail = service.detail(String.valueOf(WAYBILL_ID));
assertEquals(Integer.valueOf(1), detail.getStatus(), "数字态仍落进行中,未新增档位");
assertEquals(PENDING_RETURN_CONFIRM, detail.getBusinessStatus());
}
@Test
@DisplayName("待重新派单卡片:原始业务状态随卡片下发")
void reassignCardCarriesRawBusinessStatus() {
when(waybillService.page(any(), any())).thenAnswer(invocation -> {
Page<Waybill> rows = invocation.getArgument(0);
rows.setRecords(List.of(waybill()));
rows.setTotal(1);
return rows;
});
IPage<AdminWaybillCardVO> page = service.pendingList(1, 10, null, true);
assertEquals(1, page.getRecords().size());
assertEquals(Integer.valueOf(1), page.getRecords().get(0).getStatus(),
"数字态落进行中:null 到前端会掉进「非 0/1/2 即已取消」的展示分支");
assertEquals(PENDING_RETURN_CONFIRM, page.getRecords().get(0).getBusinessStatus());
}
// ------------------------------------------------------------ 夹具
private Waybill waybill() {
Waybill waybill = new Waybill();
waybill.setId(WAYBILL_ID);
waybill.setWaybillNo("YD202610080003");
waybill.setBusinessStatus(PENDING_RETURN_CONFIRM);
waybill.setDriverAcceptStatus("rejected");
waybill.setIsDeleted(0);
return waybill;
}
@SuppressWarnings("unchecked")
private static <T> Wrapper<T> anyWrapper() {
return org.mockito.ArgumentMatchers.<Wrapper<T>>any();
}
}
@@ -0,0 +1,150 @@
package org.springblade.transport.service.impl;
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.springblade.transport.mapper.MasterOrderMapper;
import org.springblade.transport.pojo.entity.MasterOrder;
import org.springblade.transport.pojo.entity.TransportPlan;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.MasterOrderVO;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.ITransportPlanService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.WaybillProcessSupport;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* 多联总单分段进度的「已到达数量」口径。
* <p>
* 到货但回单待确认的运单算到达:货物确实到了,回单只是单据。
* 不算会让下一段被「调度总量不能大于上一段已到达数量」挡住,整条线路卡在第二段。
*/
class MasterOrderProgressTest {
private static final Long MASTER_ID = 6101L;
private static final String MASTER_NO = "ZD20261008001";
private static final String ROUTE_JSON = """
[{"segmentNo":"1","transportType":"公路运输","departureName":"南宁","arrivalName":"北海"},
{"segmentNo":"2","transportType":"铁路运输","departureName":"北海","arrivalName":"昆明"}]
""";
private MasterOrderMapper masterOrderMapper;
private IWaybillService waybillService;
private ITransportPlanService transportPlanService;
private MasterOrderServiceImpl service;
@BeforeAll
static void initTableInfo() {
// 构造 LambdaQueryWrapper 需要 MyBatis-Plus 的实体元数据缓存
MybatisConfiguration configuration = new MybatisConfiguration();
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
assistant.setCurrentNamespace("test");
TableInfoHelper.initTableInfo(assistant, Waybill.class);
TableInfoHelper.initTableInfo(assistant, TransportPlan.class);
TableInfoHelper.initTableInfo(assistant, MasterOrder.class);
}
@BeforeEach
void setUp() throws Exception {
masterOrderMapper = mock(MasterOrderMapper.class);
waybillService = mock(IWaybillService.class);
transportPlanService = mock(ITransportPlanService.class);
service = new MasterOrderServiceImpl(
waybillService,
transportPlanService,
mock(IProjectApplyService.class),
mock(IContractManageService.class));
inject("baseMapper", masterOrderMapper);
when(transportPlanService.list(anyWrapper())).thenReturn(List.of());
}
@Test
@DisplayName("到货但回单待确认的运单计入「已到达数量」")
void arrivedQuantityCountsPendingReturnConfirm() {
when(masterOrderMapper.selectById(MASTER_ID)).thenReturn(masterOrder());
when(waybillService.list(anyWrapper())).thenReturn(List.of(
child(1L, "1", WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM, "8")));
MasterOrderVO vo = service.detail(MASTER_ID);
assertEquals(0, decimalOf(vo.getRouteProgress().get(0).get("arrivedQuantity"))
.compareTo(new BigDecimal("8")), "回单待确认不等于没到货");
assertEquals(0, decimalOf(vo.getRouteProgress().get(1).get("arrivedQuantity")).signum(),
"第二段还没有到货的运单");
}
@Test
@DisplayName("已到达口径:已完成 + 待回单确认,进行中不算")
void arrivedQuantityIgnoresRunning() {
when(masterOrderMapper.selectById(MASTER_ID)).thenReturn(masterOrder());
when(waybillService.list(anyWrapper())).thenReturn(List.of(
child(1L, "1", "running", "8"),
child(2L, "1", "completed", "5"),
child(3L, "1", WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM, "3")));
MasterOrderVO vo = service.detail(MASTER_ID);
assertEquals(0, decimalOf(vo.getRouteProgress().get(0).get("arrivedQuantity"))
.compareTo(new BigDecimal("8")), "5 + 3:进行中的 8 吨还没到,不能算到达");
}
// ------------------------------------------------------------ 夹具
private MasterOrder masterOrder() {
MasterOrder masterOrder = new MasterOrder();
masterOrder.setId(MASTER_ID);
masterOrder.setMasterNo(MASTER_NO);
masterOrder.setRouteJson(ROUTE_JSON);
return masterOrder;
}
private Waybill child(Long id, String segmentNo, String businessStatus, String quantity) {
Waybill waybill = new Waybill();
waybill.setId(id);
waybill.setMasterNo(MASTER_NO);
waybill.setRelationNo(segmentNo);
waybill.setBusinessStatus(businessStatus);
waybill.setGoodsJson("[{\"cargoName\":\"钢材\",\"quantity\":" + quantity + "}]");
return waybill;
}
private BigDecimal decimalOf(Object value) {
return value == null ? BigDecimal.ZERO : (BigDecimal) value;
}
@SuppressWarnings("unchecked")
private static <T> Wrapper<T> anyWrapper() {
return org.mockito.ArgumentMatchers.<Wrapper<T>>any();
}
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);
}
}
@@ -193,6 +193,23 @@ class TripAggregationTest {
assertTrue(TripAggregation.CHILD_STATUSES.containsAll(TripAggregation.TERMINAL_STATUSES));
}
@Test
@DisplayName("待回单确认的子单仍进运次,且不会把运次判成已完成")
void pendingReturnConfirmChildKeepsTripRunning() {
String pendingReturnConfirm = org.springblade.transport.support.WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM;
assertTrue(TripAggregation.CHILD_STATUSES.contains(pendingReturnConfirm),
"漏掉这个状态,子单会从运次读服务消失,配载单被当成全部完成");
// 与一张已完成的子单拼载:还有子单没走完,运次只能是运输中
List<Waybill> children = List.of(
waybill("completed", null, null),
waybill(pendingReturnConfirm, null, null));
String status = TripAggregation.statusOf(children, false, false);
assertEquals("running", status);
assertEquals(1, TripAggregation.appStatus(status), "运次不进已完成档");
}
private Waybill waybill(String businessStatus, String acceptStatus, String processJson) {
Waybill waybill = new Waybill();
waybill.setBusinessStatus(businessStatus);
@@ -0,0 +1,65 @@
package org.springblade.transport.service.impl;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springblade.transport.mapper.MasterOrderMapper;
import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper;
import org.springblade.transport.mapper.ReceivablePayableChangeRecordMapper;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.ICommonAddressService;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.WaybillProcessSupport;
import org.springblade.transport.wrapper.WaybillWrapper;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
/**
* 运单业务状态的中文名,两套表并存(历史结构,不合并):运单管理一套、应收应付一套。
* <p>
* 两套都要认待回单确认:漏一套就在页面上露出原始英文码。
*/
class WaybillBusinessStatusNameTest {
private static final String PENDING_RETURN_CONFIRM = WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM;
@Test
@DisplayName("运单管理名称表给出「待回单确认」——导出与详情共用这张表")
void waybillWrapperNamesPendingReturnConfirm() {
assertEquals("待回单确认", WaybillWrapper.businessStatusName(PENDING_RETURN_CONFIRM));
// 相邻档位不受影响
assertEquals("进行中", WaybillWrapper.businessStatusName("running"));
assertEquals("已完成", WaybillWrapper.businessStatusName("completed"));
}
@Test
@DisplayName("应收应付的第二套名称表同步给出「待回单确认」")
void receivablePayableNamesPendingReturnConfirm() throws Exception {
assertEquals("待回单确认", receivableStatusName(PENDING_RETURN_CONFIRM));
}
/**
* 应收应付那张表只被 waybillMap 用,而 waybillMap 当前没有调用方,
* 没有公共接缝;它是纯函数,按既有做法直接取私有方法(同 LoadingManageChildStatusWriteTest)。
*/
private String receivableStatusName(String status) throws Exception {
ReceivablePayableDetailServiceImpl service = new ReceivablePayableDetailServiceImpl(
mock(ReceivablePayableCargoFeeMapper.class),
mock(ReceivablePayableChangeRecordMapper.class),
mock(MasterOrderMapper.class),
mock(IWaybillService.class),
mock(IContractManageService.class),
mock(ICommonAddressService.class),
mock(IPreSettlementService.class),
mock(IFormalSettlementService.class));
Method method = ReceivablePayableDetailServiceImpl.class
.getDeclaredMethod("waybillStatusName", String.class);
method.setAccessible(true);
return (String) method.invoke(service, status);
}
}