✨ 管理端运单列表与详情按运次下发,首页待办按运次去重

管理端列表/待处理/详情/统计切到共享运次服务,运次主键按字符串下发;
首页「重新派单待处理」的 feed 与角标改为按运次键在库层分组去重,
一张配载单的多张子单被拒只出一条待办。重新派单写入与组织过滤未动。
This commit is contained in:
2026-10-17 19:47:56 +08:00
parent 751b246d8e
commit 5d980278f2
6 changed files with 865 additions and 146 deletions
@@ -92,4 +92,30 @@ public class AdminWaybillCardVO implements Serializable {
@Schema(description = "采购方是否已付款(预留)")
private Boolean buyerPaid;
/* ===== 运次(一趟车)字段:列表按运次下发,卡片的展示单号与路线取这些 ===== */
@Schema(description = "运次主键:配载单主键或运单主键,按字符串下发(19 位雪花号按数字下发会被前端取整)")
private String tripId;
@Schema(description = "配载单号,未配载为空")
private String loadingNo;
@Schema(description = "展示单号:有配载取配载单号,否则取运单号")
private String tripNo;
@Schema(description = "途经站点:整趟车中间各站按顺序以「 → 」连接,无中间站为空")
private String viaName;
@Schema(description = "站点数(整趟车,含起终点)")
private Integer stationCount;
@Schema(description = "货物汇总(整趟车,同名货物合并且单位一致时取数量合计)")
private String cargoSummary;
@Schema(description = "合计重量(整趟车同单位时才有值,单位不一致为空)")
private String totalWeight;
@Schema(description = "子运单数(可见口径;配载运次即整趟车的子单数)")
private Integer waybillCount;
}
@@ -135,6 +135,29 @@ public class AdminWaybillDetailVO implements Serializable {
@Schema(description = "途打卡记录")
private List<DriverEnrouteRecordVO> enrouteRecords;
/* ===== 运次(一趟车)字段:详情按运次下发,与司机端接单页同一口径 ===== */
@Schema(description = "运次主键:配载单主键或运单主键,按字符串下发(19 位雪花号按数字下发会被前端取整)")
private String tripId;
@Schema(description = "配载单号,未配载为空")
private String loadingNo;
@Schema(description = "展示单号:有配载取配载单号,否则取运单号")
private String tripNo;
@Schema(description = "途经站点:整趟车中间各站按顺序以「 → 」连接,无中间站为空")
private String viaName;
@Schema(description = "站点数(整趟车,含起终点)")
private Integer stationCount;
@Schema(description = "货物汇总(整趟车,同名货物合并且单位一致时取数量合计)")
private String cargoSummary;
@Schema(description = "站点任务安排(整趟车按实际途经顺序,第 1 站即 ①;配载运次含每站装卸与货物条目)")
private List<TripStationVO> stations;
@Data
@Schema(description = "装卸点")
public static class AdminRoutePointVO implements Serializable {
@@ -99,9 +99,9 @@ public class ManageWaybillController extends BladeController {
@GetMapping("/detail")
@ApiOperationSupport(order = 4)
@Operation(summary = "运单详情", description = "调度端查看运单详情(含 punchNodes / enrouteRecords),不校验司机归属与组织;字段对齐小程序 pages/waybill/detail")
@Operation(summary = "运次详情", description = "调度端查看整趟车详情(含 stations / punchNodes / enrouteRecords),不校验司机归属与组织;id 传运次主键字符串,已拒单的运单主键回落成单票视图")
public R<AdminWaybillDetailVO> detail(
@Parameter(description = "运单ID", required = true) @RequestParam Long id) {
@Parameter(description = "运次主键(配载单主键或运单主键,字符串)", required = true) @RequestParam String id) {
return R.data(manageWaybillService.detail(id));
}
@@ -63,9 +63,12 @@ public interface IManageWaybillService {
String exception, String transportType, String startDate, String endDate);
/**
* 调度端运单详情(不校验司机归属)
* 调度端运次详情(不校验司机归属)
* <p>
* 传运次主键:配载单主键或未配载运单主键,都按字符串。已拒单的子运单按运次取不到(运次口径
* 不含已拒子单),此时按运单主键回落成单票视图,供重新派单等从运单进入的场景使用。
*/
AdminWaybillDetailVO detail(Long id);
AdminWaybillDetailVO detail(String tripId);
/**
* 待处理运单(待接单 / 运输中;可筛需重新派单)
@@ -2,7 +2,7 @@ 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.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
@@ -17,7 +17,9 @@ 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.ILoadingManageService;
import org.springblade.transport.service.IRiskDisposalService;
import org.springblade.transport.service.ITripQueryService;
import org.springblade.transport.service.IWaybillService;
import java.time.LocalDateTime;
@@ -49,6 +51,7 @@ class ManageAdminHomeFeedTest {
private static final Long EXCEPTION_ID = 8801L;
private static final String REJECTED_WAYBILL_NO = "YD-20261010-0001";
private static final String REJECTED_LOADING_NO = "PZ-20261017-001";
private ManageWaybillServiceImpl service;
private IWaybillService waybillService;
@@ -72,7 +75,9 @@ class ManageAdminHomeFeedTest {
mock(IDriverService.class),
exceptionService,
mock(IRiskDisposalService.class),
mock(IDriverWaybillService.class));
mock(IDriverWaybillService.class),
mock(ITripQueryService.class),
mock(ILoadingManageService.class));
when(exceptionService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any()))
.thenReturn(new ArrayList<>());
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any()))
@@ -141,6 +146,43 @@ class ManageAdminHomeFeedTest {
"拒单发生在昨日 22:00,异常上报于 26 小时前(更早),故重派条目在前");
}
@Test
@DisplayName("整单拒绝的多张子单按运次收成一条:同一配载单只出一条重新派单待办")
void rejectedLoadingChildrenCollapseToOneTodo() {
// 库层按运次分组后,一张配载单无论几张子单被拒都只回一行(分组键见下面的 SQL 断言)
Waybill grouped = rejectedWaybill(7210L, "黄大富", "车辆不符");
grouped.setLoadingNo(REJECTED_LOADING_NO);
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(List.of(grouped));
List<AdminTodoItemVO> feed = service.buildAdminFeed();
assertEquals(1, feed.size(), "整单拒绝只应出一条待办");
assertEquals(REJECTED_LOADING_NO, feed.get(0).getWaybillNo(), "配载运次的待办展示配载单号");
assertEquals(Long.valueOf(7210L), feed.get(0).getId(), "跳转参数仍是运单主键:重新派单按运单写入");
ArgumentCaptor<Wrapper<Waybill>> captor = ArgumentCaptor.forClass(Wrapper.class);
verify(waybillService).list(captor.capture());
QueryWrapper<Waybill> wrapper = (QueryWrapper<Waybill>) captor.getValue();
String sql = wrapper.getTargetSql();
assertTrue(sql.contains("GROUP BY"), "去重要落在库层分组上,否则多张被拒子单会各出一条:" + sql);
assertTrue(sql.contains("CONCAT('W', id)"),
"分组键与运次读服务同源:有配载按配载单号、未配载每张运单自成一组:" + sql);
}
@Test
@DisplayName("两张未配载运单被拒仍是两条待办:未配载运单各自就是一趟运次")
void twoLoneRejectedWaybillsStayTwoTodos() {
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(List.of(
rejectedWaybill(7221L, "黄大富", null),
rejectedWaybill(7222L, "黄宇富", null)));
List<AdminTodoItemVO> feed = service.buildAdminFeed();
assertEquals(2, feed.size(), "未配载运单不合并");
assertEquals(List.of(7221L, 7222L),
feed.stream().map(AdminTodoItemVO::getId).sorted().collect(Collectors.toList()));
}
@Test
@DisplayName("判据只认「已拒单 + 未取消未完成」,已取消已完成即便被拒过也不参与")
void reassignQueryExcludesTerminalStatus() {
@@ -148,7 +190,7 @@ class ManageAdminHomeFeedTest {
ArgumentCaptor<Wrapper<Waybill>> captor = ArgumentCaptor.forClass(Wrapper.class);
verify(waybillService).list(captor.capture());
LambdaQueryWrapper<Waybill> wrapper = (LambdaQueryWrapper<Waybill>) captor.getValue();
QueryWrapper<Waybill> wrapper = (QueryWrapper<Waybill>) captor.getValue();
String sql = wrapper.getTargetSql();
assertTrue(sql.contains("driver_accept_status"), "重派判据要落在接单状态上:" + sql);