✨ 调度首页把司机已拒单的运单列为重新派单待处理,并在司机端让该运单脱离
This commit is contained in:
+3
@@ -44,4 +44,7 @@ public class AdminHomeBadgesVO implements Serializable {
|
||||
@Schema(description = "风险记录待办数(disposalStatus=pending)")
|
||||
private long risk;
|
||||
|
||||
@Schema(description = "重新派单待办数(司机已拒单且运单未取消未完成)")
|
||||
private long pendingReassign;
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ public class AdminHomeVO implements Serializable {
|
||||
@Schema(description = "快捷入口角标")
|
||||
private AdminHomeBadgesVO badges = new AdminHomeBadgesVO();
|
||||
|
||||
@Schema(description = "待处理事项(异常处置状态≠已完成)")
|
||||
@Schema(description = "待处理事项(异常处置状态≠已完成,或司机已拒单待重新派单)")
|
||||
private List<AdminTodoItemVO> feed = new ArrayList<>();
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ public class ManageWaybillController extends BladeController {
|
||||
|
||||
@GetMapping("/home")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "首页聚合", description = "统计 + 异常/风险角标 + 待处理事项(异常处置≠已完成)+ 当前用户名")
|
||||
@Operation(summary = "首页聚合", description = "统计 + 异常/风险/重新派单角标 + 待处理事项(异常处置≠已完成,或司机已拒单待重新派单)+ 当前用户名")
|
||||
public R<AdminHomeVO> home() {
|
||||
return R.data(manageWaybillService.home());
|
||||
}
|
||||
|
||||
+33
-5
@@ -262,7 +262,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
throw new ServiceException("运单ID不能为空");
|
||||
}
|
||||
Driver driver = requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver));
|
||||
// 在途打卡单独保留原取单判据,不排除已拒单(见 scopedQuery 的重载说明)
|
||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver), true);
|
||||
waybill = normalizeAcceptStatus(waybill);
|
||||
|
||||
String processJson = resolveProcessJson(waybill);
|
||||
@@ -483,6 +484,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
if (WaybillProcessSupport.isAccepted(waybill.getDriverAcceptStatus())) {
|
||||
throw new ServiceException("该运单已接单");
|
||||
}
|
||||
// 拒单后运单已对本人脱离,不给自己接回来的入口;要改主意只能由调度重新派单
|
||||
if (WaybillProcessSupport.isRejected(waybill.getDriverAcceptStatus())) {
|
||||
throw new ServiceException("该运单已拒单,请联系调度重新派单");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertRejectable(Waybill waybill) {
|
||||
@@ -498,13 +503,19 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
|
||||
private Waybill loadDriverWaybill(Long id, List<String> plates) {
|
||||
return loadDriverWaybill(id, plates, false);
|
||||
}
|
||||
|
||||
/** @param includeRejected 仅 submitEnroute 传 true,见 scopedQuery 的重载说明 */
|
||||
private Waybill loadDriverWaybill(Long id, List<String> plates, boolean includeRejected) {
|
||||
if (id == null) {
|
||||
throw new ServiceException("运单ID不能为空");
|
||||
}
|
||||
if (plates.isEmpty()) {
|
||||
throw new ServiceException("当前司机未绑定车辆");
|
||||
}
|
||||
Waybill waybill = waybillService.getOne(scopedQuery(plates).eq(Waybill::getId, id).last("LIMIT 1"));
|
||||
Waybill waybill = waybillService.getOne(scopedQuery(plates, includeRejected)
|
||||
.eq(Waybill::getId, id).last("LIMIT 1"));
|
||||
if (waybill == null) {
|
||||
throw new ServiceException("运单不存在或无权操作");
|
||||
}
|
||||
@@ -560,11 +571,23 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 司机可见运单范围:运单车牌(主车/挂车)落在当前司机绑定车牌内。
|
||||
* 绑定来源:blade_transport_driver.driving_vehicle
|
||||
* 司机可见运单范围:运单车牌(主车/挂车)落在当前司机绑定车牌内,
|
||||
* 且该运单未被这个司机拒单。绑定来源:blade_transport_driver.driving_vehicle
|
||||
* <p>
|
||||
* 拒单后运单对司机彻底脱离——列表、计数、详情、打卡、接单一律不可达,等管理员重新派单。
|
||||
* 运单上的司机姓名/手机/车牌不清空,调度重新派单时要知道原来派给谁。
|
||||
*/
|
||||
private LambdaQueryWrapper<Waybill> scopedQuery(List<String> plates) {
|
||||
return Wrappers.<Waybill>lambdaQuery().and(w -> {
|
||||
return scopedQuery(plates, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param includeRejected true 时不排除已拒单的运单,仅限在途打卡使用:
|
||||
* 拒单的运单不可能处于运输中,收口拦不到合法操作,
|
||||
* 而保留原判据可避免「已拒单 + 进行中」的历史脏数据被误伤
|
||||
*/
|
||||
private LambdaQueryWrapper<Waybill> scopedQuery(List<String> plates, boolean includeRejected) {
|
||||
LambdaQueryWrapper<Waybill> wrapper = Wrappers.<Waybill>lambdaQuery().and(w -> {
|
||||
w.in(Waybill::getVehicleNo, plates)
|
||||
.or().in(Waybill::getTrailerVehicleNo, plates);
|
||||
for (String plate : plates) {
|
||||
@@ -578,6 +601,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
);
|
||||
}
|
||||
});
|
||||
if (!includeRejected) {
|
||||
wrapper.and(w -> w.isNull(Waybill::getDriverAcceptStatus)
|
||||
.or().ne(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED));
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private void applyKeyword(LambdaQueryWrapper<Waybill> wrapper, String keyword) {
|
||||
|
||||
+94
-4
@@ -57,6 +57,7 @@ import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
@@ -85,6 +86,13 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
private static final String DISPOSAL_PROCESSING = "processing";
|
||||
private static final String RISK_PENDING = "pending";
|
||||
|
||||
/** 小程序首页待处理事项的类型:异常待处置 */
|
||||
private static final String TODO_EXCEPTION = "exception";
|
||||
/** 小程序首页待处理事项的类型:重新派单待处理 */
|
||||
private static final String TODO_REASSIGN = "reassign";
|
||||
/** 重派条目跳转页码:参数 id 是运单主键(不是运单号),该页据此回显运单;buyerPaid 恒为 0,与列表卡片一致 */
|
||||
private static final String REASSIGN_TARGET_URL = "/pages/waybill/reassign?id=%s&buyerPaid=0";
|
||||
|
||||
private static final String EXCEPTION_YES = "exception";
|
||||
private static final String EXCEPTION_NO = "normal";
|
||||
private static final String TRANSPORT_COMMON = "common";
|
||||
@@ -122,8 +130,9 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
AdminHomeBadgesVO badges = new AdminHomeBadgesVO();
|
||||
badges.setException(home.getStats().getException());
|
||||
badges.setRisk(countPendingRisks());
|
||||
badges.setPendingReassign(countPendingReassignments());
|
||||
home.setBadges(badges);
|
||||
home.setFeed(buildExceptionFeed());
|
||||
home.setFeed(buildAdminFeed());
|
||||
return home;
|
||||
}
|
||||
|
||||
@@ -561,18 +570,66 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
.eq(RiskDisposal::getDisposalStatus, RISK_PENDING));
|
||||
}
|
||||
|
||||
private List<AdminTodoItemVO> buildExceptionFeed() {
|
||||
/** 待重新派单:司机已拒单,且运单尚未取消/完成 */
|
||||
private long countPendingReassignments() {
|
||||
return waybillService.count(pendingReassignQuery());
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<Waybill> pendingReassignQuery() {
|
||||
return Wrappers.<Waybill>lambdaQuery()
|
||||
.eq(Waybill::getDriverAcceptStatus, ACCEPT_REJECTED)
|
||||
.in(Waybill::getBusinessStatus, STATUS_PENDING, STATUS_RUNNING);
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页待处理事项:异常待处置 + 重新派单待处理。
|
||||
* 两类各按自己的业务发生时间(上报时刻 / 拒单时刻)倒序,合并后再整体倒序取前 N 条。
|
||||
* 排序时间随条目一起携带,不塞进 id 字段——id 在两型下各自是处置主键与运单主键。
|
||||
*/
|
||||
List<AdminTodoItemVO> buildAdminFeed() {
|
||||
List<TimedTodo> merged = new ArrayList<>();
|
||||
merged.addAll(buildExceptionFeed());
|
||||
merged.addAll(buildReassignFeed());
|
||||
merged.sort(java.util.Comparator.comparing(
|
||||
(TimedTodo entry) -> entry.time, java.util.Comparator.nullsFirst(java.util.Comparator.naturalOrder())));
|
||||
java.util.Collections.reverse(merged);
|
||||
int to = Math.min(merged.size(), FEED_LIMIT);
|
||||
return merged.subList(0, to).stream().map(TimedTodo::item).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/** 待处理事项 + 它的业务发生时间(仅用于排序) */
|
||||
private record TimedTodo(AdminTodoItemVO item, LocalDateTime time) {
|
||||
}
|
||||
|
||||
private List<TimedTodo> buildExceptionFeed() {
|
||||
List<ExceptionDisposal> list = exceptionDisposalService.list(Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)
|
||||
.orderByDesc(ExceptionDisposal::getReportTime)
|
||||
.last("LIMIT " + FEED_LIMIT));
|
||||
return list.stream().map(this::toTodoItem).collect(Collectors.toList());
|
||||
return list.stream()
|
||||
.map(disposal -> new TimedTodo(toTodoItem(disposal), exceptionTimeOf(disposal)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<TimedTodo> buildReassignFeed() {
|
||||
List<Waybill> list = waybillService.list(pendingReassignQuery()
|
||||
.orderByDesc(Waybill::getDriverRejectTime)
|
||||
.last("LIMIT " + FEED_LIMIT));
|
||||
return list.stream()
|
||||
.map(waybill -> new TimedTodo(toReassignTodoItem(waybill), rejectTimeOf(waybill)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private LocalDateTime exceptionTimeOf(ExceptionDisposal disposal) {
|
||||
return disposal.getReportTime() != null
|
||||
? disposal.getReportTime()
|
||||
: toLocalDateTime(disposal.getCreateTime());
|
||||
}
|
||||
|
||||
private AdminTodoItemVO toTodoItem(ExceptionDisposal disposal) {
|
||||
AdminTodoItemVO item = new AdminTodoItemVO();
|
||||
item.setId(disposal.getId());
|
||||
item.setType("exception");
|
||||
item.setType(TODO_EXCEPTION);
|
||||
item.setTitle("异常待处置");
|
||||
item.setTimeAgo(formatTimeAgo(disposal.getReportTime() != null
|
||||
? disposal.getReportTime()
|
||||
@@ -585,6 +642,38 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
return item;
|
||||
}
|
||||
|
||||
private AdminTodoItemVO toReassignTodoItem(Waybill waybill) {
|
||||
AdminTodoItemVO item = new AdminTodoItemVO();
|
||||
item.setId(waybill.getId());
|
||||
item.setType(TODO_REASSIGN);
|
||||
item.setTitle("重新派单待处理");
|
||||
item.setTimeAgo(formatTimeAgo(rejectTimeOf(waybill)));
|
||||
item.setDesc(buildReassignDesc(waybill));
|
||||
item.setWaybillNo(Func.toStr(waybill.getWaybillNo(), ""));
|
||||
item.setActionLabel("重新派单");
|
||||
item.setTargetUrl(String.format(REASSIGN_TARGET_URL, waybill.getId()));
|
||||
return item;
|
||||
}
|
||||
|
||||
/** 拒单时刻是「被拒了多久」的唯一依据;为空(历史数据)才回退到最后修改时间 */
|
||||
private LocalDateTime rejectTimeOf(Waybill waybill) {
|
||||
if (waybill.getDriverRejectTime() != null) {
|
||||
return toLocalDateTime(waybill.getDriverRejectTime());
|
||||
}
|
||||
return toLocalDateTime(waybill.getUpdateTime());
|
||||
}
|
||||
|
||||
/** 拒单原因的输入是可选的,没填时正文不能留下空的「:」 */
|
||||
private String buildReassignDesc(Waybill waybill) {
|
||||
String driver = Func.toStr(waybill.getDriverName(), "原司机");
|
||||
String text = "运单已指派" + driver + ",被拒绝,请重新派单";
|
||||
String reason = Func.toStr(waybill.getDriverRejectReason(), "").trim();
|
||||
if (Func.isNotBlank(reason)) {
|
||||
text = text + ":" + reason;
|
||||
}
|
||||
return text.length() > 80 ? text.substring(0, 80) + "…" : text;
|
||||
}
|
||||
|
||||
private String buildExceptionDesc(ExceptionDisposal disposal) {
|
||||
String reporter = Func.toStr(disposal.getReporterName(), "司机");
|
||||
String type = Func.toStr(disposal.getExceptionType(), "异常");
|
||||
@@ -606,6 +695,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
return Func.toStr(AuthUtil.getUserName(), "");
|
||||
}
|
||||
|
||||
/** 相对时间文案;时间取不到时留空,避免把「现在」当成发生时刻显示成「刚刚」 */
|
||||
private String formatTimeAgo(LocalDateTime time) {
|
||||
if (time == null) {
|
||||
return "";
|
||||
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
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.ArgumentCaptor;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
import org.springblade.transport.pojo.dto.EnrouteSubmitDTO;
|
||||
import org.springblade.transport.pojo.entity.Driver;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
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.IWaybillService;
|
||||
import org.springblade.transport.support.CertificationGate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 司机拒单后运单对本人脱离。
|
||||
* <p>
|
||||
* 拒单不再只是一个标记:该运单从列表、计数、详情到接单全部不可达,等管理员重新派单。
|
||||
* <p>
|
||||
* 拦截落在司机端共用的取单条件上,而单元测试里 {@code waybillService} 是 mock、不会真的执行
|
||||
* 这个条件,所以「已拒单被排除」只能断言条件本身;过滤之后的行为则用「已过滤的输入」来验,
|
||||
* 不假装 mock 会替我们过滤。
|
||||
*/
|
||||
class DriverWaybillRejectDetachTest {
|
||||
|
||||
private static final Long USER_ID = 7401L;
|
||||
private static final Long DRIVER_ID = 9001L;
|
||||
private static final String PLATE = "沪AE31312";
|
||||
private static final Long REJECTED_ID = 7201L;
|
||||
|
||||
private IDriverService driverService;
|
||||
private CertificationGate certificationGate;
|
||||
private IWaybillService waybillService;
|
||||
private DriverWaybillServiceImpl service;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
|
||||
assistant.setCurrentNamespace("test");
|
||||
TableInfoHelper.initTableInfo(assistant, Waybill.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
driverService = mock(IDriverService.class);
|
||||
waybillService = mock(IWaybillService.class);
|
||||
certificationGate = mock(CertificationGate.class);
|
||||
lenient().when(certificationGate.isDriverAvailable(any())).thenReturn(true);
|
||||
service = new DriverWaybillServiceImpl(
|
||||
waybillService,
|
||||
driverService,
|
||||
mock(IProcessConfigService.class),
|
||||
mock(ITransportVehicleService.class),
|
||||
certificationGate,
|
||||
mock(WaybillEnroutePunchMapper.class),
|
||||
mock(WaybillNodePunchMapper.class),
|
||||
mock(ExceptionDisposalMapper.class));
|
||||
lenient().when(driverService.getOne(any())).thenReturn(driver());
|
||||
lenient().when(waybillService.updateById(any())).thenReturn(true);
|
||||
// syncDriverAcceptState 是 IWaybillService 的方法,mock 默认返 null 会让调用方 NPE;
|
||||
// 真实实现是把入参校正后返回同一个对象
|
||||
lenient().when(waybillService.syncDriverAcceptState(any()))
|
||||
.thenAnswer(invocation -> invocation.getArgument(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("首页待接预览的取单条件排除已拒单,且排除子句与车牌条件并列")
|
||||
void pendingPreviewQueryExcludesRejected() {
|
||||
when(waybillService.list(ArgumentMatchers.<com.baomidou.mybatisplus.core.conditions.Wrapper<Waybill>>any())).thenReturn(List.of());
|
||||
|
||||
runAsDriver(() -> service.pendingPreview(5));
|
||||
|
||||
String sql = capturedListQuery().getTargetSql();
|
||||
assertTrue(sql.contains("driver_accept_status"),
|
||||
"取单条件必须排除已拒单:" + sql);
|
||||
assertTrue(sql.toUpperCase().contains("AND"),
|
||||
"排除子句不能与车牌条件「或」在一起,否则等于没排除:" + sql);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运单列表的取单条件同样排除已拒单")
|
||||
void pageQueryExcludesRejected() {
|
||||
when(waybillService.list(ArgumentMatchers.<com.baomidou.mybatisplus.core.conditions.Wrapper<Waybill>>any())).thenReturn(List.of());
|
||||
|
||||
runAsDriver(() -> service.page(1, 10, 0, null));
|
||||
|
||||
String sql = capturedListQuery().getTargetSql();
|
||||
assertTrue(sql.contains("driver_accept_status"), "列表也要排除已拒单:" + sql);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Tab 计数不再把已拒单算进待接单")
|
||||
void tabCountsOnFilteredInputCountsOnlyUnrejected() {
|
||||
// 查询层已排除拒单后落回来的输入:一张待接、一张运输中、一张已完成
|
||||
when(waybillService.list(ArgumentMatchers.<com.baomidou.mybatisplus.core.conditions.Wrapper<Waybill>>any())).thenReturn(List.of(
|
||||
waybill(7202L, "YD-20261001-0002", "pending", "pending"),
|
||||
waybill(7203L, "YD-20261001-0003", "running", "accepted"),
|
||||
waybill(7204L, "YD-20261001-0004", "completed", "accepted")));
|
||||
|
||||
DriverWaybillTabCountsVO counts = runAsDriver(() -> service.tabCounts());
|
||||
|
||||
assertEquals(1, counts.getPending(), "待接单只算那张未被拒的");
|
||||
assertEquals(1, counts.getDoing());
|
||||
assertEquals(1, counts.getDone());
|
||||
assertEquals(3, counts.getAll(), "全部 = 待接 + 进行中 + 已完成");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("未拒单的运单不被误伤:待接与运输中照常出现")
|
||||
void unrejectedWaybillsUnaffected() {
|
||||
when(waybillService.list(ArgumentMatchers.<com.baomidou.mybatisplus.core.conditions.Wrapper<Waybill>>any())).thenReturn(List.of(
|
||||
waybill(7202L, "YD-20261001-0002", "pending", "pending"),
|
||||
waybill(7203L, "YD-20261001-0003", "running", "accepted")));
|
||||
|
||||
IPage<DriverWaybillCardVO> page = runAsDriver(() -> service.page(1, 10, null, null));
|
||||
|
||||
assertEquals(2, page.getTotal(), "未被拒的运单条数不变");
|
||||
assertEquals(2, page.getRecords().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已拒单运单的详情不可达:取单条件已把它挡住")
|
||||
void detailOfRejectedIsUnreachable() {
|
||||
// 被取单条件排除后 getOne 落回 null,与「不属于这个司机」同一条报错
|
||||
when(waybillService.getOne(any())).thenReturn(null);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> runAsDriver(() -> service.detail(REJECTED_ID)));
|
||||
|
||||
assertEquals("运单不存在或无权操作", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已拒单运单不能自行接回来")
|
||||
void acceptOfRejectedIsRejected() {
|
||||
when(waybillService.getOne(any())).thenReturn(null);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> runAsDriver(() -> service.accept(REJECTED_ID)));
|
||||
|
||||
assertEquals("运单不存在或无权操作", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("在途打卡保留原取单判据,不排除已拒单的历史脏数据")
|
||||
void enrouteSubmitKeepsUnfilteredScope() {
|
||||
EnrouteSubmitDTO dto = new EnrouteSubmitDTO();
|
||||
dto.setWaybillId(REJECTED_ID);
|
||||
when(waybillService.getOne(any())).thenReturn(rejectedWaybill());
|
||||
|
||||
// 断言的是「取单这一步没有被拒单条件拦住」:若被拦住会抛「运单不存在或无权操作」,
|
||||
// 而后面因过程配置为空会走「该运单未启用在途打卡」,两者可区分
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> runAsDriver(() -> service.submitEnroute(dto)));
|
||||
|
||||
assertEquals("该运单未启用在途打卡", exception.getMessage(),
|
||||
"在途打卡不应因运单已拒单而在取单阶段被挡下");
|
||||
verify(waybillService, org.mockito.Mockito.atLeastOnce()).getOne(any());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private <T> T runAsDriver(Supplier<T> action) {
|
||||
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
|
||||
return action.get();
|
||||
}
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<Waybill> capturedListQuery() {
|
||||
ArgumentCaptor<LambdaQueryWrapper<Waybill>> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
verify(waybillService).list(captor.capture());
|
||||
return captor.getValue();
|
||||
}
|
||||
|
||||
private Driver driver() {
|
||||
Driver driver = new Driver();
|
||||
driver.setId(DRIVER_ID);
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDriverName("黄大富");
|
||||
driver.setDrivingVehicle(PLATE);
|
||||
return driver;
|
||||
}
|
||||
|
||||
private Waybill rejectedWaybill() {
|
||||
return waybill(REJECTED_ID, "YD-20261010-0001", "pending", "rejected");
|
||||
}
|
||||
|
||||
private Waybill waybill(Long id, String no, String businessStatus, String acceptStatus) {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(id);
|
||||
waybill.setWaybillNo(no);
|
||||
waybill.setVehicleNo(PLATE);
|
||||
waybill.setBusinessStatus(businessStatus);
|
||||
waybill.setDriverAcceptStatus(acceptStatus);
|
||||
waybill.setIsDeleted(0);
|
||||
return waybill;
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
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.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.mockito.ArgumentMatchers;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.AdminTodoItemVO;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IExceptionDisposalService;
|
||||
import org.springblade.transport.service.IRiskDisposalService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
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.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 调度端首页待处理事项:异常待处置 + 重新派单待处理。
|
||||
* <p>
|
||||
* 拒单条目的判据是「司机已拒单且运单未取消未完成」,必须与待处理运单列表的 needReassign
|
||||
* 口径一致,否则两个页面的条数对不上;卡片上「多久之前」取拒单时刻,不取最后修改时间
|
||||
* (后者会被任何无关改动刷新)。
|
||||
* <p>
|
||||
* 这里直接验归并方法本身,不走 {@code home()}:那个入口会解析当前用户名、依赖 Spring 缓存,
|
||||
* 在单测里没有上下文。
|
||||
*/
|
||||
class ManageAdminHomeFeedTest {
|
||||
|
||||
private static final Long EXCEPTION_ID = 8801L;
|
||||
private static final String REJECTED_WAYBILL_NO = "YD-20261010-0001";
|
||||
|
||||
private ManageWaybillServiceImpl service;
|
||||
private IWaybillService waybillService;
|
||||
private IExceptionDisposalService exceptionService;
|
||||
|
||||
@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);
|
||||
exceptionService = mock(IExceptionDisposalService.class);
|
||||
service = new ManageWaybillServiceImpl(
|
||||
waybillService,
|
||||
mock(IDriverService.class),
|
||||
exceptionService,
|
||||
mock(IRiskDisposalService.class),
|
||||
mock(IDriverWaybillService.class));
|
||||
when(exceptionService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any()))
|
||||
.thenReturn(new ArrayList<>());
|
||||
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any()))
|
||||
.thenReturn(new ArrayList<>());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("拒单运单产出一条重新派单待处理,字段与跳转参数对齐小程序约定")
|
||||
void rejectedWaybillAppearsAsReassignTodo() {
|
||||
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any()))
|
||||
.thenReturn(List.of(rejectedWaybill(7201L, "黄大富", "时间冲突")));
|
||||
|
||||
List<AdminTodoItemVO> feed = service.buildAdminFeed();
|
||||
|
||||
assertEquals(1, feed.size());
|
||||
AdminTodoItemVO item = feed.get(0);
|
||||
assertEquals("reassign", item.getType());
|
||||
assertEquals("重新派单待处理", item.getTitle());
|
||||
assertEquals("重新派单", item.getActionLabel());
|
||||
assertEquals(REJECTED_WAYBILL_NO, item.getWaybillNo());
|
||||
assertEquals(Long.valueOf(7201L), item.getId(), "重派条目应当带运单主键");
|
||||
assertEquals("运单已指派黄大富,被拒绝,请重新派单:时间冲突", item.getDesc());
|
||||
assertEquals("/pages/waybill/reassign?id=7201&buyerPaid=0", item.getTargetUrl(),
|
||||
"跳转参数必须是运单主键:该页按主键回显运单,填运单号读不出数据");
|
||||
assertFalse(item.getTimeAgo().isEmpty(), "拒单时刻应换算成相对时间");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("拒单原因为空时正文不带空冒号")
|
||||
void withoutRejectReasonKeepsDescClean() {
|
||||
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any()))
|
||||
.thenReturn(List.of(rejectedWaybill(7202L, "黄宇富", null)));
|
||||
|
||||
AdminTodoItemVO item = service.buildAdminFeed().get(0);
|
||||
|
||||
assertEquals("运单已指派黄宇富,被拒绝,请重新派单", item.getDesc());
|
||||
assertFalse(item.getDesc().contains(":"), "原因缺失时不得留下空的「:」");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("没有拒单时只出异常条目,不凭空造重派条目")
|
||||
void withoutRejectedWaybillNoReassignTodo() {
|
||||
when(exceptionService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any()))
|
||||
.thenReturn(List.of(exceptionDisposal()));
|
||||
|
||||
List<AdminTodoItemVO> feed = service.buildAdminFeed();
|
||||
|
||||
assertEquals(1, feed.size());
|
||||
assertEquals("exception", feed.get(0).getType());
|
||||
assertEquals("异常待处置", feed.get(0).getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("两类条目按各自业务发生时间倒序混排")
|
||||
void feedIsMergedByBusinessTime() {
|
||||
when(exceptionService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any()))
|
||||
.thenReturn(List.of(exceptionDisposal()));
|
||||
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any()))
|
||||
.thenReturn(List.of(rejectedWaybill(7203L, "黄大富", "时间冲突")));
|
||||
|
||||
List<AdminTodoItemVO> feed = service.buildAdminFeed();
|
||||
|
||||
assertEquals(2, feed.size());
|
||||
assertEquals(List.of("reassign", "exception"),
|
||||
feed.stream().map(AdminTodoItemVO::getType).collect(Collectors.toList()),
|
||||
"拒单发生在昨日 22:00,异常上报于 26 小时前(更早),故重派条目在前");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("判据只认「已拒单 + 未取消未完成」,已取消已完成即便被拒过也不参与")
|
||||
void reassignQueryExcludesTerminalStatus() {
|
||||
service.buildAdminFeed();
|
||||
|
||||
ArgumentCaptor<Wrapper<Waybill>> captor = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(waybillService).list(captor.capture());
|
||||
LambdaQueryWrapper<Waybill> wrapper = (LambdaQueryWrapper<Waybill>) captor.getValue();
|
||||
String sql = wrapper.getTargetSql();
|
||||
|
||||
assertTrue(sql.contains("driver_accept_status"), "重派判据要落在接单状态上:" + sql);
|
||||
assertTrue(sql.contains("business_status"),
|
||||
"已取消/已完成的运单不该再催重新派单:" + sql);
|
||||
|
||||
Set<Object> values = new HashSet<>(wrapper.getParamNameValuePairs().values());
|
||||
assertTrue(values.contains("rejected"), "要带上已拒绝:" + values);
|
||||
assertTrue(values.contains("pending") && values.contains("running"),
|
||||
"业务状态只应放待执行与进行中:" + values);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private Waybill rejectedWaybill(Long id, String driverName, String reason) {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(id);
|
||||
waybill.setWaybillNo(REJECTED_WAYBILL_NO);
|
||||
waybill.setDriverName(driverName);
|
||||
waybill.setDriverAcceptStatus("rejected");
|
||||
waybill.setBusinessStatus("pending");
|
||||
waybill.setDriverRejectReason(reason);
|
||||
waybill.setDriverRejectTime(yesterdayAt22());
|
||||
return waybill;
|
||||
}
|
||||
|
||||
private ExceptionDisposal exceptionDisposal() {
|
||||
ExceptionDisposal disposal = new ExceptionDisposal();
|
||||
disposal.setId(EXCEPTION_ID);
|
||||
disposal.setWaybillNo("YD-20261006-0001");
|
||||
disposal.setReporterName("黄宇富");
|
||||
disposal.setExceptionType("交通异常");
|
||||
disposal.setDisposalStatus("pending");
|
||||
disposal.setReportTime(LocalDateTime.now().minusHours(26));
|
||||
return disposal;
|
||||
}
|
||||
|
||||
private Date yesterdayAt22() {
|
||||
java.util.Calendar calendar = java.util.Calendar.getInstance();
|
||||
calendar.add(java.util.Calendar.DAY_OF_MONTH, -1);
|
||||
calendar.set(java.util.Calendar.HOUR_OF_DAY, 22);
|
||||
calendar.set(java.util.Calendar.MINUTE, 0);
|
||||
calendar.set(java.util.Calendar.SECOND, 0);
|
||||
calendar.set(java.util.Calendar.MILLISECOND, 0);
|
||||
return calendar.getTime();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user